feat: configure launcher color scheme
This commit is contained in:
@@ -7,12 +7,12 @@ final class ActionPill: NSView {
|
||||
wantsLayer = true
|
||||
layer?.cornerRadius = 18
|
||||
layer?.borderWidth = 0.5
|
||||
layer?.borderColor = SunsetPalette.accent.withAlphaComponent(0.35).cgColor
|
||||
layer?.backgroundColor = SunsetPalette.background.withAlphaComponent(0.25).cgColor
|
||||
layer?.borderColor = ThemePalette.accent.withAlphaComponent(0.35).cgColor
|
||||
layer?.backgroundColor = ThemePalette.background.withAlphaComponent(0.25).cgColor
|
||||
for button in [primaryButton, actionsButton] {
|
||||
button.isBordered = false
|
||||
button.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
button.contentTintColor = SunsetPalette.text
|
||||
button.contentTintColor = ThemePalette.text
|
||||
}
|
||||
primaryButton.keyEquivalent = primaryCommand ?? "\r"
|
||||
primaryButton.keyEquivalentModifierMask = primaryCommand == nil ? [] : .command
|
||||
|
||||
@@ -74,23 +74,23 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
selectionScroll = nil
|
||||
selectionViewportHeight = nil
|
||||
entries = [
|
||||
ActionEntry(title: "Open", symbol: "arrow.up.forward.app", color: SunsetPalette.text),
|
||||
ActionEntry(title: "Favorite", symbol: "star", color: SunsetPalette.text),
|
||||
ActionEntry(title: "Show in Finder", symbol: "finder", color: SunsetPalette.text),
|
||||
ActionEntry(title: "Reset ranking", symbol: "arrow.counterclockwise", color: SunsetPalette.text),
|
||||
ActionEntry(title: "Quit application", symbol: "stop.circle", color: SunsetPalette.text),
|
||||
ActionEntry(title: "Uninstall application", symbol: "trash", color: .systemRed),
|
||||
ActionEntry(title: "Copy Emoji", symbol: "doc.on.doc", color: SunsetPalette.text),
|
||||
ActionEntry(title: "Change source language…", symbol: "character.bubble", color: SunsetPalette.text),
|
||||
ActionEntry(title: "Change target language…", symbol: "character.bubble", color: SunsetPalette.text),
|
||||
ActionEntry(title: "Swap languages", symbol: "arrow.left.arrow.right", color: SunsetPalette.text),
|
||||
ActionEntry(title: "MyMemory (no key)", symbol: "checkmark", color: SunsetPalette.text),
|
||||
ActionEntry(title: "OpenAI · gpt-6-luna", symbol: "checkmark", color: SunsetPalette.text),
|
||||
ActionEntry(title: "Browse Notes", symbol: "list.bullet", color: SunsetPalette.text, commandKey: "P"),
|
||||
ActionEntry(title: "New Note", symbol: "square.and.pencil", color: SunsetPalette.text, commandKey: "N"),
|
||||
ActionEntry(title: "Delete Note", symbol: "trash", color: .systemRed),
|
||||
ActionEntry(title: "Copy as Markdown", symbol: "doc.on.doc", color: SunsetPalette.text),
|
||||
ActionEntry(title: "Export as Markdown…", symbol: "square.and.arrow.up", color: SunsetPalette.text)
|
||||
ActionEntry(title: "Open", symbol: "arrow.up.forward.app", color: ThemePalette.text),
|
||||
ActionEntry(title: "Favorite", symbol: "star", color: ThemePalette.text),
|
||||
ActionEntry(title: "Show in Finder", symbol: "finder", color: ThemePalette.text),
|
||||
ActionEntry(title: "Reset ranking", symbol: "arrow.counterclockwise", color: ThemePalette.text),
|
||||
ActionEntry(title: "Quit application", symbol: "stop.circle", color: ThemePalette.text),
|
||||
ActionEntry(title: "Uninstall application", symbol: "trash", color: ThemePalette.error),
|
||||
ActionEntry(title: "Copy Emoji", symbol: "doc.on.doc", color: ThemePalette.text),
|
||||
ActionEntry(title: "Change source language…", symbol: "character.bubble", color: ThemePalette.text),
|
||||
ActionEntry(title: "Change target language…", symbol: "character.bubble", color: ThemePalette.text),
|
||||
ActionEntry(title: "Swap languages", symbol: "arrow.left.arrow.right", color: ThemePalette.text),
|
||||
ActionEntry(title: "MyMemory (no key)", symbol: "checkmark", color: ThemePalette.text),
|
||||
ActionEntry(title: "OpenAI · gpt-6-luna", symbol: "checkmark", color: ThemePalette.text),
|
||||
ActionEntry(title: "Browse Notes", symbol: "list.bullet", color: ThemePalette.text, commandKey: "P"),
|
||||
ActionEntry(title: "New Note", symbol: "square.and.pencil", color: ThemePalette.text, commandKey: "N"),
|
||||
ActionEntry(title: "Delete Note", symbol: "trash", color: ThemePalette.error),
|
||||
ActionEntry(title: "Copy as Markdown", symbol: "doc.on.doc", color: ThemePalette.text),
|
||||
ActionEntry(title: "Export as Markdown…", symbol: "square.and.arrow.up", color: ThemePalette.text)
|
||||
]
|
||||
view = ActionsPanelView()
|
||||
view.wantsLayer = true
|
||||
@@ -106,13 +106,13 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
content.layer?.borderWidth = 0.5
|
||||
background.setAccessibilityElement(false)
|
||||
content.addSubview(background)
|
||||
applicationLabel.textColor = SunsetPalette.secondaryText
|
||||
applicationLabel.textColor = ThemePalette.secondaryText
|
||||
applicationLabel.lineBreakMode = .byTruncatingTail
|
||||
applicationLabel.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
search.font = .systemFont(ofSize: LauncherRowView.textSize)
|
||||
search.textColor = SunsetPalette.text
|
||||
search.textColor = ThemePalette.text
|
||||
search.placeholderAttributedString = NSAttributedString(string: "Search for actions…", attributes: [
|
||||
.foregroundColor: SunsetPalette.secondaryText, .font: NSFont.systemFont(ofSize: LauncherRowView.textSize)
|
||||
.foregroundColor: ThemePalette.secondaryText, .font: NSFont.systemFont(ofSize: LauncherRowView.textSize)
|
||||
])
|
||||
search.isBordered = false
|
||||
search.drawsBackground = false
|
||||
@@ -126,7 +126,7 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
search.setAccessibilityLabel("Search application actions")
|
||||
search.closeActions = { [weak self] in self?.close() }
|
||||
search.commandKey = { [weak self] key in self?.handleCommandKey(key) ?? false }
|
||||
empty.textColor = SunsetPalette.secondaryText
|
||||
empty.textColor = ThemePalette.secondaryText
|
||||
empty.font = .systemFont(ofSize: 14)
|
||||
empty.isHidden = true
|
||||
content.translatesAutoresizingMaskIntoConstraints = false
|
||||
@@ -173,7 +173,7 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
search.bottomAnchor.constraint(equalTo: content.bottomAnchor, constant: -8),
|
||||
search.heightAnchor.constraint(equalToConstant: 24)
|
||||
])
|
||||
content.layer?.borderColor = SunsetPalette.secondaryText.withAlphaComponent(0.22).cgColor
|
||||
content.layer?.borderColor = ThemePalette.secondaryText.withAlphaComponent(0.22).cgColor
|
||||
if choices != nil {
|
||||
height.isActive = false
|
||||
let table = NSTableView()
|
||||
@@ -290,7 +290,7 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
let favorite = entries[1]
|
||||
let favoriteTitle = isFavorite ? "Remove Favorite" : "Favorite"
|
||||
favorite.button.attributedTitle = NSAttributedString(string: favoriteTitle, attributes: [
|
||||
.foregroundColor: SunsetPalette.text, .font: NSFont.systemFont(ofSize: LauncherRowView.textSize, weight: .medium)
|
||||
.foregroundColor: ThemePalette.text, .font: NSFont.systemFont(ofSize: LauncherRowView.textSize, weight: .medium)
|
||||
])
|
||||
favorite.button.setAccessibilityLabel(favoriteTitle)
|
||||
favorite.icon.image = NSImage(systemSymbolName: isFavorite ? "star.fill" : "star", accessibilityDescription: nil)
|
||||
@@ -299,7 +299,7 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
}
|
||||
search.placeholderAttributedString = NSAttributedString(
|
||||
string: choices != nil ? "Search for \(selectionName)…" : belowAnchor ? "Search for providers…" : "Search for actions…",
|
||||
attributes: [.foregroundColor: SunsetPalette.secondaryText,
|
||||
attributes: [.foregroundColor: ThemePalette.secondaryText,
|
||||
.font: NSFont.systemFont(ofSize: LauncherRowView.textSize)])
|
||||
search.stringValue = ""
|
||||
controlTextDidChange(Notification(name: NSControl.textDidChangeNotification))
|
||||
@@ -332,7 +332,7 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
coveredPill?.animator().alphaValue = 0
|
||||
}
|
||||
window.makeFirstResponder(search)
|
||||
(search.currentEditor() as? NSTextView)?.insertionPointColor = SunsetPalette.accent
|
||||
(search.currentEditor() as? NSTextView)?.insertionPointColor = ThemePalette.accent
|
||||
mouseMonitor = NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown, .rightMouseDown]) { [weak self, weak pill] event in
|
||||
MainActor.assumeIsolated {
|
||||
guard let self, self.isShown, event.window === self.view.window else { return }
|
||||
@@ -595,7 +595,7 @@ private struct ActionEntry {
|
||||
|
||||
@MainActor
|
||||
private final class SelectionCell: NSView {
|
||||
let entry = ActionEntry(title: "", symbol: "checkmark", color: SunsetPalette.text)
|
||||
let entry = ActionEntry(title: "", symbol: "checkmark", color: ThemePalette.text)
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import AppKit
|
||||
|
||||
@MainActor
|
||||
struct ColorScheme {
|
||||
var background: NSColor
|
||||
var text: NSColor
|
||||
var secondaryText: NSColor
|
||||
var accent: NSColor
|
||||
var glow: NSColor
|
||||
var keyBackground: NSColor
|
||||
var error: NSColor
|
||||
|
||||
static let sunset = ColorScheme(
|
||||
background: NSColor(srgbRed: 0.12, green: 0.09, blue: 0.08, alpha: 1),
|
||||
text: NSColor(srgbRed: 1.00, green: 0.89, blue: 0.71, alpha: 1),
|
||||
secondaryText: NSColor(srgbRed: 0.85, green: 0.78, blue: 0.65, alpha: 1),
|
||||
accent: NSColor(srgbRed: 0.95, green: 0.58, blue: 0.23, alpha: 1),
|
||||
glow: NSColor(srgbRed: 0.76, green: 0.27, blue: 0.18, alpha: 1),
|
||||
keyBackground: NSColor(srgbRed: 0.26, green: 0.22, blue: 0.19, alpha: 1),
|
||||
error: .systemRed
|
||||
)
|
||||
|
||||
static var configurationDirectory: URL {
|
||||
let base: URL
|
||||
if let xdg = ProcessInfo.processInfo.environment["XDG_CONFIG_HOME"], xdg.hasPrefix("/") {
|
||||
base = URL(fileURLWithPath: xdg, isDirectory: true)
|
||||
} else {
|
||||
base = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".config", isDirectory: true)
|
||||
}
|
||||
return base.appendingPathComponent("conjure", isDirectory: true)
|
||||
}
|
||||
|
||||
static func load(from directory: URL) throws -> ColorScheme {
|
||||
let configURL = directory.appendingPathComponent("config.json")
|
||||
guard let config = try readObject(at: configURL, missingAllowed: true) else { return sunset }
|
||||
try validateKeys(config, allowed: ["theme", "colors"], at: configURL, prefix: "")
|
||||
let theme: String
|
||||
if let value = config["theme"] {
|
||||
guard let name = value as? String, ["sunset", "tint"].contains(name) else {
|
||||
throw ColorSchemeError(path: configURL.path, reason: "theme must be \"sunset\" or \"tint\".")
|
||||
}
|
||||
theme = name
|
||||
} else {
|
||||
theme = "sunset"
|
||||
}
|
||||
|
||||
var result = sunset
|
||||
if theme == "tint" {
|
||||
let themeURL = directory.appendingPathComponent("theme.json")
|
||||
if let generated = try readObject(at: themeURL, missingAllowed: false) {
|
||||
try validateKeys(generated, allowed: ["colors"], at: themeURL, prefix: "")
|
||||
try result.applyColors(from: generated, at: themeURL)
|
||||
}
|
||||
}
|
||||
try result.applyColors(from: config, at: configURL)
|
||||
return result
|
||||
}
|
||||
|
||||
private static let colorFields: [String: WritableKeyPath<ColorScheme, NSColor>] = [
|
||||
"background": \.background,
|
||||
"text": \.text,
|
||||
"secondaryText": \.secondaryText,
|
||||
"accent": \.accent,
|
||||
"glow": \.glow,
|
||||
"keyBackground": \.keyBackground,
|
||||
"error": \.error,
|
||||
]
|
||||
|
||||
private mutating func applyColors(from object: [String: Any], at url: URL) throws {
|
||||
guard let value = object["colors"] else { return }
|
||||
guard let colors = value as? [String: Any] else {
|
||||
throw ColorSchemeError(path: url.path, reason: "colors must be an object.")
|
||||
}
|
||||
try Self.validateKeys(colors, allowed: Set(Self.colorFields.keys), at: url, prefix: "colors.")
|
||||
for (name, value) in colors {
|
||||
guard let hex = value as? String, hex.utf8.count == 7, hex.first == "#",
|
||||
hex.utf8.dropFirst().allSatisfy({ (48...57).contains($0) || (65...70).contains($0) || (97...102).contains($0) }),
|
||||
let rgb = UInt32(hex.dropFirst(), radix: 16) else {
|
||||
throw ColorSchemeError(path: url.path, reason: "colors.\(name) must be #RRGGBB ASCII hex (no alpha).")
|
||||
}
|
||||
if let field = Self.colorFields[name] {
|
||||
self[keyPath: field] = NSColor(
|
||||
srgbRed: CGFloat((rgb >> 16) & 0xff) / 255,
|
||||
green: CGFloat((rgb >> 8) & 0xff) / 255,
|
||||
blue: CGFloat(rgb & 0xff) / 255,
|
||||
alpha: 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func validateKeys(_ object: [String: Any], allowed: Set<String>, at url: URL, prefix: String) throws {
|
||||
if let key = object.keys.sorted().first(where: { !allowed.contains($0) }) {
|
||||
throw ColorSchemeError(path: url.path, reason: "Unknown key \"\(prefix)\(key)\".")
|
||||
}
|
||||
}
|
||||
|
||||
private static func readObject(at url: URL, missingAllowed: Bool) throws -> [String: Any]? {
|
||||
let data: Data
|
||||
do {
|
||||
data = try Data(contentsOf: url)
|
||||
} catch {
|
||||
let cocoa = error as NSError
|
||||
if missingAllowed, cocoa.domain == NSCocoaErrorDomain, cocoa.code == NSFileReadNoSuchFileError {
|
||||
return nil
|
||||
}
|
||||
throw ColorSchemeError(path: url.path, reason: error.localizedDescription)
|
||||
}
|
||||
let value: Any
|
||||
do {
|
||||
value = try JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed])
|
||||
} catch {
|
||||
throw ColorSchemeError(path: url.path, reason: "Invalid JSON: \(error.localizedDescription)")
|
||||
}
|
||||
guard let object = value as? [String: Any] else {
|
||||
throw ColorSchemeError(path: url.path, reason: "Expected a JSON object.")
|
||||
}
|
||||
return object
|
||||
}
|
||||
}
|
||||
|
||||
private struct ColorSchemeError: LocalizedError {
|
||||
let path: String
|
||||
let reason: String
|
||||
|
||||
var errorDescription: String? { "\(path): \(reason)" }
|
||||
}
|
||||
@@ -108,7 +108,7 @@ private final class ApplicationDelegate: NSObject, NSApplicationDelegate {
|
||||
controller.retryUninstallRecovery = { [uninstaller] in await uninstaller.recover() }
|
||||
return controller
|
||||
}
|
||||
private var startupShortcutError: String?
|
||||
private var startupError = ThemePalette.configurationError
|
||||
private var startupRecoveryMessage: String?
|
||||
|
||||
private var isLoginLaunch: Bool {
|
||||
@@ -137,24 +137,23 @@ private final class ApplicationDelegate: NSObject, NSApplicationDelegate {
|
||||
launcher.present()
|
||||
}
|
||||
let shortcutErrors = shortcuts.start()
|
||||
if !shortcutErrors.isEmpty {
|
||||
startupShortcutError = shortcutErrors.joined(separator: "\n")
|
||||
if !isLoginLaunch { showSettings(nil) }
|
||||
}
|
||||
startupError = ([startupError].compactMap { $0 } + shortcutErrors).joined(separator: "\n")
|
||||
if startupError?.isEmpty == true { startupError = nil }
|
||||
if startupError != nil && !loginLaunch { showSettings(nil) }
|
||||
}
|
||||
|
||||
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows: Bool) -> Bool {
|
||||
guard !isLoginLaunch else { return false }
|
||||
launcher.present()
|
||||
if startupShortcutError != nil || startupRecoveryMessage != nil {
|
||||
if startupError != nil || startupRecoveryMessage != nil {
|
||||
showSettings(nil)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@objc func showSettings(_ sender: Any?) {
|
||||
settings.present(error: startupShortcutError)
|
||||
startupShortcutError = nil
|
||||
settings.present(error: startupError)
|
||||
startupError = nil
|
||||
startupRecoveryMessage = nil
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ final class EmojiPickerController: NSViewController, NSCollectionViewDataSource,
|
||||
category.action = #selector(categoryChanged)
|
||||
category.font = .systemFont(ofSize: 12)
|
||||
category.isBordered = false
|
||||
category.contentTintColor = SunsetPalette.secondaryText
|
||||
category.contentTintColor = ThemePalette.secondaryText
|
||||
category.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
layout.itemSize = NSSize(width: 80, height: 80)
|
||||
layout.minimumInteritemSpacing = 8
|
||||
@@ -59,9 +59,9 @@ final class EmojiPickerController: NSViewController, NSCollectionViewDataSource,
|
||||
scroll.scrollerStyle = .overlay
|
||||
scroll.drawsBackground = false
|
||||
scroll.contentView.drawsBackground = false
|
||||
empty.textColor = SunsetPalette.secondaryText
|
||||
empty.textColor = ThemePalette.secondaryText
|
||||
status.font = .systemFont(ofSize: 12)
|
||||
status.textColor = SunsetPalette.secondaryText
|
||||
status.textColor = ThemePalette.secondaryText
|
||||
status.lineBreakMode = .byTruncatingTail
|
||||
status.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
copyButton.target = self
|
||||
@@ -323,10 +323,10 @@ private final class EmojiSectionHeader: NSView {
|
||||
|
||||
func setTitle(_ title: String, count: Int) {
|
||||
let text = NSMutableAttributedString(string: title, attributes: [
|
||||
.font: NSFont.systemFont(ofSize: 13), .foregroundColor: SunsetPalette.secondaryText,
|
||||
.font: NSFont.systemFont(ofSize: 13), .foregroundColor: ThemePalette.secondaryText,
|
||||
])
|
||||
text.append(NSAttributedString(string: " \(count)", attributes: [
|
||||
.font: NSFont.systemFont(ofSize: 12), .foregroundColor: SunsetPalette.secondaryText.withAlphaComponent(0.55),
|
||||
.font: NSFont.systemFont(ofSize: 12), .foregroundColor: ThemePalette.secondaryText.withAlphaComponent(0.55),
|
||||
]))
|
||||
label.attributedStringValue = text
|
||||
}
|
||||
@@ -348,7 +348,7 @@ private final class EmojiItem: NSCollectionViewItem {
|
||||
}
|
||||
|
||||
func highlight(_ selected: Bool) {
|
||||
button.layer?.backgroundColor = (selected ? SunsetPalette.selectionFill : SunsetPalette.text.withAlphaComponent(0.045)).cgColor
|
||||
button.layer?.backgroundColor = (selected ? ThemePalette.selectionFill : ThemePalette.text.withAlphaComponent(0.045)).cgColor
|
||||
button.setAccessibilitySelected(selected)
|
||||
button.selected = selected
|
||||
}
|
||||
@@ -378,7 +378,7 @@ private final class EmojiButton: NSButton {
|
||||
|
||||
private func updateBorder() {
|
||||
layer?.borderWidth = selected || hovered ? 0.5 : 0
|
||||
layer?.borderColor = (selected ? SunsetPalette.accent.withAlphaComponent(0.8)
|
||||
: SunsetPalette.secondaryText.withAlphaComponent(0.35)).cgColor
|
||||
layer?.borderColor = (selected ? ThemePalette.accent.withAlphaComponent(0.8)
|
||||
: ThemePalette.secondaryText.withAlphaComponent(0.35)).cgColor
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,8 +58,8 @@ private final class AlignmentGuideView: NSView {
|
||||
var snapsHorizontally = false
|
||||
var snapsVertically = false
|
||||
private static let dash: [CGFloat] = [1, 5]
|
||||
private static let muted = SunsetPalette.accent.withAlphaComponent(0.4).cgColor
|
||||
private static let aligned = SunsetPalette.accent.withAlphaComponent(0.95).cgColor
|
||||
private static let muted = ThemePalette.accent.withAlphaComponent(0.4).cgColor
|
||||
private static let aligned = ThemePalette.accent.withAlphaComponent(0.95).cgColor
|
||||
private static let shadow = NSColor.black.withAlphaComponent(0.6).cgColor
|
||||
|
||||
override func hitTest(_ point: NSPoint) -> NSView? { nil }
|
||||
|
||||
@@ -1,34 +1,44 @@
|
||||
import AppKit
|
||||
|
||||
@MainActor
|
||||
enum SunsetPalette {
|
||||
static let background = NSColor(srgbRed: 0.12, green: 0.09, blue: 0.08, alpha: 0.55)
|
||||
static let text = NSColor(srgbRed: 1.00, green: 0.89, blue: 0.71, alpha: 1)
|
||||
static let secondaryText = NSColor(srgbRed: 0.85, green: 0.78, blue: 0.65, alpha: 1)
|
||||
static let accent = NSColor(srgbRed: 0.95, green: 0.58, blue: 0.23, alpha: 1)
|
||||
enum ThemePalette {
|
||||
private static let loaded: (scheme: ColorScheme, error: String?) = {
|
||||
do {
|
||||
return (try ColorScheme.load(from: ColorScheme.configurationDirectory), nil)
|
||||
} catch {
|
||||
return (.sunset, "Color configuration: \(error.localizedDescription)\nUsing the built-in sunset palette. Fix the file and restart Conjure.")
|
||||
}
|
||||
}()
|
||||
static let configurationError = loaded.error
|
||||
static let background = loaded.scheme.background.withAlphaComponent(0.55)
|
||||
static let text = loaded.scheme.text
|
||||
static let secondaryText = loaded.scheme.secondaryText
|
||||
static let accent = loaded.scheme.accent
|
||||
static let selectionFill = accent.withAlphaComponent(0.12)
|
||||
static let selectionBorder = accent.withAlphaComponent(0.30)
|
||||
static let ember = NSColor(srgbRed: 0.76, green: 0.27, blue: 0.18, alpha: 1)
|
||||
static let glow = loaded.scheme.glow
|
||||
static let keyBackground = loaded.scheme.keyBackground.withAlphaComponent(0.5)
|
||||
static let error = loaded.scheme.error
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class LauncherBackground: NSView {
|
||||
weak var canvas: NSView?
|
||||
private let amber = NSGradient(colors: [
|
||||
SunsetPalette.accent.withAlphaComponent(0.18),
|
||||
SunsetPalette.accent.withAlphaComponent(0.065),
|
||||
SunsetPalette.accent.withAlphaComponent(0),
|
||||
ThemePalette.accent.withAlphaComponent(0.18),
|
||||
ThemePalette.accent.withAlphaComponent(0.065),
|
||||
ThemePalette.accent.withAlphaComponent(0),
|
||||
])!
|
||||
private let ember = NSGradient(colors: [
|
||||
SunsetPalette.ember.withAlphaComponent(0.26),
|
||||
SunsetPalette.ember.withAlphaComponent(0.10),
|
||||
SunsetPalette.ember.withAlphaComponent(0),
|
||||
ThemePalette.glow.withAlphaComponent(0.26),
|
||||
ThemePalette.glow.withAlphaComponent(0.10),
|
||||
ThemePalette.glow.withAlphaComponent(0),
|
||||
])!
|
||||
|
||||
override func hitTest(_ point: NSPoint) -> NSView? { nil }
|
||||
|
||||
override func draw(_ dirtyRect: NSRect) {
|
||||
SunsetPalette.background.setFill()
|
||||
ThemePalette.background.setFill()
|
||||
dirtyRect.fill()
|
||||
let canvasBounds = canvas.map { convert($0.bounds, from: $0) } ?? bounds
|
||||
let upperGlow = NSPoint(x: canvasBounds.minX + canvasBounds.width * 0.58,
|
||||
|
||||
@@ -169,7 +169,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
if let notes { notes.focusEditor() }
|
||||
else { window?.makeFirstResponder(searchField) }
|
||||
(window as? LauncherWindow)?.sampleNumberHints()
|
||||
(searchField.currentEditor() as? NSTextView)?.insertionPointColor = SunsetPalette.accent
|
||||
(searchField.currentEditor() as? NSTextView)?.insertionPointColor = ThemePalette.accent
|
||||
refreshApplications()
|
||||
}
|
||||
|
||||
@@ -276,9 +276,9 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
searchField.font = searchFont
|
||||
searchField.placeholderAttributedString = NSAttributedString(
|
||||
string: "Search apps, calculate, or convert…",
|
||||
attributes: [.foregroundColor: SunsetPalette.secondaryText, .font: searchFont]
|
||||
attributes: [.foregroundColor: ThemePalette.secondaryText, .font: searchFont]
|
||||
)
|
||||
searchField.textColor = SunsetPalette.text
|
||||
searchField.textColor = ThemePalette.text
|
||||
searchField.isBordered = false
|
||||
searchField.drawsBackground = false
|
||||
searchField.focusRingType = .none
|
||||
@@ -292,11 +292,11 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
searchField.setAccessibilityLabel("Search applications, calculate, or convert units and currencies")
|
||||
|
||||
searchSymbol.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: nil)
|
||||
searchSymbol.contentTintColor = SunsetPalette.accent
|
||||
searchSymbol.contentTintColor = ThemePalette.accent
|
||||
commandBack.image = IconStyle.symbol("chevron.left", for: searchFont)
|
||||
commandBack.imageScaling = .scaleProportionallyDown
|
||||
commandBack.isBordered = false
|
||||
commandBack.contentTintColor = SunsetPalette.secondaryText
|
||||
commandBack.contentTintColor = ThemePalette.secondaryText
|
||||
commandBack.setAccessibilityLabel("Back to applications")
|
||||
commandBack.toolTip = "Back to applications"
|
||||
commandBack.isHidden = true
|
||||
@@ -311,7 +311,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
header.spacing = 12
|
||||
|
||||
sectionLabel.font = .systemFont(ofSize: 12, weight: .semibold)
|
||||
sectionLabel.textColor = SunsetPalette.secondaryText
|
||||
sectionLabel.textColor = ThemePalette.secondaryText
|
||||
|
||||
let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("application"))
|
||||
column.resizingMask = .autoresizingMask
|
||||
@@ -350,9 +350,9 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
scrollView.contentInsets = NSEdgeInsets(top: LauncherScrollView.fadeHeight, left: 0, bottom: 16, right: 0)
|
||||
|
||||
statusLabel.font = .systemFont(ofSize: 12)
|
||||
statusLabel.textColor = SunsetPalette.secondaryText
|
||||
statusLabel.textColor = ThemePalette.secondaryText
|
||||
statusLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
emptyLabel.textColor = SunsetPalette.secondaryText
|
||||
emptyLabel.textColor = ThemePalette.secondaryText
|
||||
emptyLabel.alignment = .center
|
||||
|
||||
openButton.target = self
|
||||
@@ -395,7 +395,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
footer.heightAnchor.constraint(equalToConstant: 36),
|
||||
])
|
||||
window.effectiveAppearance.performAsCurrentDrawingAppearance {
|
||||
let border = SunsetPalette.secondaryText.withAlphaComponent(0.22).cgColor
|
||||
let border = ThemePalette.secondaryText.withAlphaComponent(0.22).cgColor
|
||||
content.layer?.borderColor = border
|
||||
}
|
||||
window.initialFirstResponder = searchField
|
||||
@@ -569,7 +569,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
cell.kindLabel.stringValue = isCurrencyCalculation ? "Daily rates"
|
||||
: calculation.copyValue == nil ? "Error" : "Calculator"
|
||||
cell.imageView?.image = NSImage(systemSymbolName: "equal", accessibilityDescription: nil)
|
||||
cell.imageView?.contentTintColor = SunsetPalette.accent
|
||||
cell.imageView?.contentTintColor = ThemePalette.accent
|
||||
cell.toolTip = calculation.detail
|
||||
} else if results[row].command != nil {
|
||||
let entry = results[row]
|
||||
@@ -1016,7 +1016,7 @@ final class ApplicationCell: NSTableCellView {
|
||||
private static let fadedMask = [NSColor.black.cgColor, NSColor.clear.cgColor]
|
||||
private static let timing = CAMediaTimingFunction(name: .easeOut)
|
||||
static let applicationKind = NSAttributedString(string: "Application", attributes: [
|
||||
.font: NSFont.systemFont(ofSize: LauncherRowView.textSize), .foregroundColor: SunsetPalette.secondaryText
|
||||
.font: NSFont.systemFont(ofSize: LauncherRowView.textSize), .foregroundColor: ThemePalette.secondaryText
|
||||
])
|
||||
static let commandKind = NSAttributedString(string: "Command", attributes: applicationKind.attributes(at: 0, effectiveRange: nil))
|
||||
static let favoriteKind = favorited(applicationKind)
|
||||
@@ -1028,7 +1028,7 @@ final class ApplicationCell: NSTableCellView {
|
||||
attachment.image = NSImage(systemSymbolName: "star.fill", accessibilityDescription: "Favorite")?
|
||||
.withSymbolConfiguration(IconStyle.symbolConfiguration(for: font)
|
||||
.applying(.init(scale: .small))
|
||||
.applying(.init(paletteColors: [SunsetPalette.secondaryText])))
|
||||
.applying(.init(paletteColors: [ThemePalette.secondaryText])))
|
||||
let size = attachment.image?.size ?? .zero
|
||||
attachment.bounds = NSRect(x: 0, y: (font.capHeight - size.height) / 2,
|
||||
width: size.width, height: size.height)
|
||||
@@ -1041,7 +1041,7 @@ final class ApplicationCell: NSTableCellView {
|
||||
let locationLabel = NSTextField(labelWithString: "")
|
||||
let kindLabel = NSTextField(labelWithString: "Application")
|
||||
let numberHint = ShortcutIndicator(
|
||||
"1", backgroundColor: NSColor(srgbRed: 0.26, green: 0.22, blue: 0.19, alpha: 0.5),
|
||||
"1", backgroundColor: ThemePalette.keyBackground,
|
||||
fontSize: 10, keySize: 20
|
||||
)
|
||||
private let kindFade = CAGradientLayer()
|
||||
@@ -1054,10 +1054,10 @@ final class ApplicationCell: NSTableCellView {
|
||||
let icon = IconView(font: font)
|
||||
let label = NSTextField(labelWithString: "")
|
||||
label.font = font
|
||||
label.textColor = SunsetPalette.text
|
||||
label.textColor = ThemePalette.text
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
locationLabel.font = .systemFont(ofSize: 12)
|
||||
locationLabel.textColor = SunsetPalette.secondaryText
|
||||
locationLabel.textColor = ThemePalette.secondaryText
|
||||
locationLabel.lineBreakMode = .byTruncatingMiddle
|
||||
locationLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
locationLabel.isHidden = true
|
||||
@@ -1066,7 +1066,7 @@ final class ApplicationCell: NSTableCellView {
|
||||
title.alignment = .centerY
|
||||
title.spacing = 8
|
||||
kindLabel.font = .systemFont(ofSize: font.pointSize)
|
||||
kindLabel.textColor = SunsetPalette.secondaryText
|
||||
kindLabel.textColor = ThemePalette.secondaryText
|
||||
kindLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
kindLabel.setAccessibilityElement(false)
|
||||
kindLabel.wantsLayer = true
|
||||
@@ -1416,10 +1416,10 @@ final class LauncherRowView: NSTableRowView {
|
||||
selectionBounds.origin.y += topSpacing
|
||||
selectionBounds.size.height -= topSpacing
|
||||
let selection = NSBezierPath(roundedRect: selectionBounds.insetBy(dx: 0.75, dy: 0.75), xRadius: 10, yRadius: 10)
|
||||
SunsetPalette.selectionFill.setFill()
|
||||
ThemePalette.selectionFill.setFill()
|
||||
selection.fill()
|
||||
let increasedContrast = NSWorkspace.shared.accessibilityDisplayShouldIncreaseContrast
|
||||
(increasedContrast ? SunsetPalette.accent : SunsetPalette.selectionBorder).setStroke()
|
||||
(increasedContrast ? ThemePalette.accent : ThemePalette.selectionBorder).setStroke()
|
||||
selection.lineWidth = increasedContrast ? 1.5 : 0.5
|
||||
selection.stroke()
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ final class NotesController: NSViewController, NSTextViewDelegate {
|
||||
actionsButton.action = #selector(showActions)
|
||||
let controls = ActionPill(primaryButton: browseButton, actionsButton: actionsButton, primaryCommand: "p")
|
||||
noteTitle.font = .systemFont(ofSize: 13)
|
||||
noteTitle.textColor = SunsetPalette.secondaryText
|
||||
noteTitle.textColor = ThemePalette.secondaryText
|
||||
noteTitle.alignment = .center
|
||||
noteTitle.lineBreakMode = .byTruncatingTail
|
||||
noteTitle.setAccessibilityLabel("Note title")
|
||||
@@ -68,7 +68,7 @@ final class NotesController: NSViewController, NSTextViewDelegate {
|
||||
vimButton.action = #selector(toggleVim)
|
||||
vimButton.bezelStyle = .accessoryBarAction
|
||||
vimButton.font = .monospacedSystemFont(ofSize: 10, weight: .medium)
|
||||
vimButton.contentTintColor = SunsetPalette.secondaryText
|
||||
vimButton.contentTintColor = ThemePalette.secondaryText
|
||||
vimButton.setAccessibilityLabel("Toggle Vim editing")
|
||||
vimButton.toolTip = "Toggle Vim editing. Escape: Normal mode, then close; i: Insert; v: Visual."
|
||||
editor.vimModeChanged = { [weak self] in self?.updateVimIndicator() }
|
||||
@@ -130,9 +130,9 @@ final class NotesController: NSViewController, NSTextViewDelegate {
|
||||
editor.importsGraphics = true
|
||||
editor.allowsUndo = true
|
||||
editor.drawsBackground = false
|
||||
editor.textColor = SunsetPalette.text
|
||||
editor.insertionPointColor = SunsetPalette.accent
|
||||
editor.selectedTextAttributes = [.backgroundColor: SunsetPalette.selectionFill]
|
||||
editor.textColor = ThemePalette.text
|
||||
editor.insertionPointColor = ThemePalette.accent
|
||||
editor.selectedTextAttributes = [.backgroundColor: ThemePalette.selectionFill]
|
||||
editor.font = NotesAppearance.font
|
||||
editor.defaultParagraphStyle = NotesAppearance.paragraph
|
||||
editor.isAutomaticQuoteSubstitutionEnabled = false
|
||||
@@ -155,7 +155,7 @@ final class NotesController: NSViewController, NSTextViewDelegate {
|
||||
scroll.contentInsets = NSEdgeInsets(top: scroll.edgeFadeHeight, left: 0,
|
||||
bottom: scroll.edgeFadeHeight, right: 0)
|
||||
status.font = .systemFont(ofSize: 12)
|
||||
status.textColor = SunsetPalette.secondaryText
|
||||
status.textColor = ThemePalette.secondaryText
|
||||
status.setAccessibilityLabel("Notes save status")
|
||||
status.lineBreakMode = .byTruncatingTail
|
||||
status.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
@@ -220,7 +220,7 @@ final class NotesController: NSViewController, NSTextViewDelegate {
|
||||
button.imagePosition = .imageOnly
|
||||
button.bezelStyle = .accessoryBarAction
|
||||
button.showsBorderOnlyWhileMouseInside = true
|
||||
button.contentTintColor = SunsetPalette.secondaryText
|
||||
button.contentTintColor = ThemePalette.secondaryText
|
||||
button.toolTip = label
|
||||
button.setAccessibilityLabel(label)
|
||||
NSLayoutConstraint.activate([button.widthAnchor.constraint(equalToConstant: 28), button.heightAnchor.constraint(equalToConstant: 28)])
|
||||
@@ -358,7 +358,7 @@ final class NotesController: NSViewController, NSTextViewDelegate {
|
||||
status.isHidden = false
|
||||
status.stringValue = "Couldn’t export note: \(error.localizedDescription)"
|
||||
status.toolTip = status.stringValue
|
||||
status.textColor = .systemRed
|
||||
status.textColor = ThemePalette.error
|
||||
}
|
||||
|
||||
|
||||
@@ -371,7 +371,7 @@ final class NotesController: NSViewController, NSTextViewDelegate {
|
||||
alert.informativeText = "This permanently deletes the note and its embedded images. This cannot be undone."
|
||||
alert.addButton(withTitle: "Cancel").keyEquivalent = "\u{1b}"
|
||||
let delete = alert.addButton(withTitle: "Delete Note")
|
||||
delete.contentTintColor = .systemRed
|
||||
delete.contentTintColor = ThemePalette.error
|
||||
delete.keyEquivalent = "\r"
|
||||
alert.window.initialFirstResponder = delete
|
||||
alert.window.defaultButtonCell = delete.cell as? NSButtonCell
|
||||
@@ -416,7 +416,7 @@ final class NotesController: NSViewController, NSTextViewDelegate {
|
||||
status.isHidden = false
|
||||
status.stringValue = "Couldn’t save, load, or delete notes: \(error.localizedDescription)"
|
||||
status.toolTip = status.stringValue
|
||||
status.textColor = .systemRed
|
||||
status.textColor = ThemePalette.error
|
||||
}
|
||||
|
||||
@objc func browseNotes() {
|
||||
|
||||
@@ -136,7 +136,7 @@ final class NotesTextView: NSTextView {
|
||||
|
||||
private func attributes(at position: Int) -> [NSAttributedString.Key: Any] {
|
||||
guard let storage = textStorage, storage.length > 0 else {
|
||||
return [.font: NotesAppearance.font, .foregroundColor: SunsetPalette.text, .paragraphStyle: NotesAppearance.paragraph]
|
||||
return [.font: NotesAppearance.font, .foregroundColor: ThemePalette.text, .paragraphStyle: NotesAppearance.paragraph]
|
||||
}
|
||||
var result = storage.attributes(at: min(max(0, position), storage.length - 1), effectiveRange: nil)
|
||||
result.removeValue(forKey: .attachment)
|
||||
@@ -157,7 +157,7 @@ final class NotesTextView: NSTextView {
|
||||
weight: attrs[NoteAttributes.bold] as? Bool == true ? .bold : heading ? .semibold : .regular)
|
||||
attrs[.font] = attrs[NoteAttributes.italic] as? Bool == true
|
||||
? NSFontManager.shared.convert(font, toHaveTrait: .italicFontMask) : font
|
||||
attrs[.foregroundColor] = attrs[NoteAttributes.inlineCode] as? Bool == true ? SunsetPalette.accent : SunsetPalette.text
|
||||
attrs[.foregroundColor] = attrs[NoteAttributes.inlineCode] as? Bool == true ? ThemePalette.accent : ThemePalette.text
|
||||
typingAttributes = attrs
|
||||
emptyTypingAttributes = paragraph(at: selectedRange().location).length == 0 ? attrs : nil
|
||||
}
|
||||
@@ -512,8 +512,8 @@ final class NotesTextView: NSTextView {
|
||||
storage.addAttribute(NoteAttributes.italic, value: italic, range: range)
|
||||
let font = NSFont.monospacedSystemFont(ofSize: size, weight: bold ? .bold : kind == "heading" ? .semibold : .regular)
|
||||
storage.addAttribute(.font, value: italic ? NSFontManager.shared.convert(font, toHaveTrait: .italicFontMask) : font, range: range)
|
||||
storage.addAttribute(.foregroundColor, value: run[NoteAttributes.inlineCode] as? Bool == true || run[.link] != nil ? SunsetPalette.accent : SunsetPalette.text, range: range)
|
||||
if run[NoteAttributes.inlineCode] as? Bool == true || kind == "code" { storage.addAttribute(.backgroundColor, value: SunsetPalette.selectionFill, range: range) }
|
||||
storage.addAttribute(.foregroundColor, value: run[NoteAttributes.inlineCode] as? Bool == true || run[.link] != nil ? ThemePalette.accent : ThemePalette.text, range: range)
|
||||
if run[NoteAttributes.inlineCode] as? Bool == true || kind == "code" { storage.addAttribute(.backgroundColor, value: ThemePalette.selectionFill, range: range) }
|
||||
}
|
||||
index = NSMaxRange(row)
|
||||
}
|
||||
@@ -557,7 +557,7 @@ final class NotesTextView: NSTextView {
|
||||
let line = layoutManager.lineFragmentRect(forGlyphAt: glyph, effectiveRange: nil)
|
||||
let baseline = line.minY + layoutManager.location(forGlyphAt: glyph).y + textContainerOrigin.y
|
||||
let label = item.kind == "ordered" ? "\(item.number)." : "•"
|
||||
let attrs: [NSAttributedString.Key: Any] = [.font: NotesAppearance.font, .foregroundColor: SunsetPalette.accent]
|
||||
let attrs: [NSAttributedString.Key: Any] = [.font: NotesAppearance.font, .foregroundColor: ThemePalette.accent]
|
||||
let width = (label as NSString).size(withAttributes: attrs).width
|
||||
let right = textContainerOrigin.x + CGFloat(item.depth) * 20 + markerColumn - 4
|
||||
let x = item.kind == "ordered" ? right - width : right - (markerColumn - 4 + width) / 2
|
||||
@@ -572,7 +572,7 @@ final class NotesTextView: NSTextView {
|
||||
while checkboxes.count > tasks.count { checkboxes.removeLast().removeFromSuperview() }
|
||||
while checkboxes.count < tasks.count {
|
||||
let button = NSButton(checkboxWithTitle: "", target: self, action: #selector(toggleTask(_:)))
|
||||
button.controlSize = .small; button.contentTintColor = SunsetPalette.accent; button.sizeToFit()
|
||||
button.controlSize = .small; button.contentTintColor = ThemePalette.accent; button.sizeToFit()
|
||||
checkboxes.append(button); addSubview(button)
|
||||
}
|
||||
for (button, task) in zip(checkboxes, tasks) {
|
||||
|
||||
@@ -110,8 +110,8 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
|
||||
instructions.textColor = .secondaryLabelColor
|
||||
statusLabel.textColor = .secondaryLabelColor
|
||||
statusLabel.setAccessibilityLabel("Global shortcut registration status")
|
||||
errorLabel.textColor = .systemRed
|
||||
errorLabel.setAccessibilityLabel("Shortcut feedback")
|
||||
errorLabel.textColor = ThemePalette.error
|
||||
errorLabel.setAccessibilityLabel("Settings feedback")
|
||||
|
||||
let spotlight = NSTextField(wrappingLabelWithString:
|
||||
"To use ⌘Space, first free it in System Settings → Keyboard → Keyboard Shortcuts → Spotlight: turn off or change “Show Spotlight search.” Check Input Sources for other reserved shortcuts. Conjure never changes these settings."
|
||||
@@ -128,7 +128,7 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
|
||||
loginSettings.action = #selector(openLoginSettings)
|
||||
loginSettings.bezelStyle = .rounded
|
||||
loginStatus.setAccessibilityLabel("Launch at login status")
|
||||
recoveryLabel.textColor = SunsetPalette.secondaryText
|
||||
recoveryLabel.textColor = ThemePalette.secondaryText
|
||||
recoveryLabel.isHidden = true
|
||||
recoveryLabel.setAccessibilityLabel("Uninstall recovery status")
|
||||
recoveryButton.isHidden = true
|
||||
@@ -137,19 +137,19 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
|
||||
recoveryButton.action = #selector(showUninstallRecovery)
|
||||
|
||||
heading.font = .systemFont(ofSize: 20, weight: .semibold)
|
||||
heading.textColor = SunsetPalette.text
|
||||
heading.textColor = ThemePalette.text
|
||||
for label in [instructions, statusLabel, spotlight, loginStatus] {
|
||||
label.textColor = SunsetPalette.secondaryText
|
||||
label.textColor = ThemePalette.secondaryText
|
||||
}
|
||||
for button in [recorder, commandSpaceButton, clearButton, keyboardSettings, loginToggle, loginSettings, recoveryButton] {
|
||||
button.contentTintColor = SunsetPalette.accent
|
||||
button.contentTintColor = ThemePalette.accent
|
||||
}
|
||||
let loginHeading = NSTextField(labelWithString: "Startup")
|
||||
loginHeading.font = .systemFont(ofSize: 20, weight: .semibold)
|
||||
loginHeading.textColor = SunsetPalette.text
|
||||
loginHeading.textColor = ThemePalette.text
|
||||
let translationHeading = NSTextField(labelWithString: "Conjure Translate")
|
||||
translationHeading.font = .systemFont(ofSize: 20, weight: .semibold)
|
||||
translationHeading.textColor = SunsetPalette.text
|
||||
translationHeading.textColor = ThemePalette.text
|
||||
|
||||
let stack = NSStackView(views: [
|
||||
heading, destinationSelector, controls, instructions, statusLabel, errorLabel, spotlight, keyboardSettings,
|
||||
@@ -206,7 +206,7 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
|
||||
recoveryLabel.stringValue = report.needsRecovery
|
||||
? "An interrupted uninstall needs attention. Files have been retained for recovery."
|
||||
: "Interrupted uninstall recovery completed."
|
||||
recoveryLabel.textColor = report.needsRecovery ? .systemRed : SunsetPalette.secondaryText
|
||||
recoveryLabel.textColor = report.needsRecovery ? ThemePalette.error : ThemePalette.secondaryText
|
||||
recoveryButton.title = report.needsRecovery ? "Retry uninstall recovery…" : "View recovery details…"
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
|
||||
|
||||
private func refreshLogin() {
|
||||
let status = SMAppService.mainApp.status
|
||||
loginStatus.textColor = SunsetPalette.secondaryText
|
||||
loginStatus.textColor = ThemePalette.secondaryText
|
||||
loginToggle.state = (status == .enabled || status == .requiresApproval) ? .on : .off
|
||||
loginSettings.isHidden = status != .requiresApproval
|
||||
switch status {
|
||||
@@ -243,7 +243,7 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
|
||||
loginStatus.stringValue = "Install Conjure in Applications before enabling launch at login."
|
||||
@unknown default:
|
||||
loginStatus.stringValue = "Launch at login status is unavailable."
|
||||
loginStatus.textColor = .systemRed
|
||||
loginStatus.textColor = ThemePalette.error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
|
||||
} catch {
|
||||
refreshLogin()
|
||||
loginStatus.stringValue = "Could not change launch at login: \(error.localizedDescription)"
|
||||
loginStatus.textColor = .systemRed
|
||||
loginStatus.textColor = ThemePalette.error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,12 +15,12 @@ final class ShortcutIndicator: NSStackView {
|
||||
for key in keys {
|
||||
let badge = ShortcutKeyBadge(labelWithString: key)
|
||||
badge.font = .systemFont(ofSize: fontSize, weight: .medium)
|
||||
badge.textColor = SunsetPalette.secondaryText
|
||||
badge.textColor = ThemePalette.secondaryText
|
||||
badge.alignment = .center
|
||||
badge.wantsLayer = true
|
||||
badge.layer?.cornerRadius = 5
|
||||
badge.layer?.borderWidth = 1
|
||||
badge.layer?.borderColor = SunsetPalette.secondaryText.withAlphaComponent(0.35).cgColor
|
||||
badge.layer?.borderColor = ThemePalette.secondaryText.withAlphaComponent(0.35).cgColor
|
||||
badge.layer?.backgroundColor = backgroundColor?.cgColor
|
||||
badge.setAccessibilityElement(false)
|
||||
badge.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
@@ -85,11 +85,11 @@ final class TranslationController: NSViewController {
|
||||
providerButton.toolTip = "Choose translation provider (Command-P)"
|
||||
providerButton.isBordered = false
|
||||
providerButton.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
providerButton.contentTintColor = SunsetPalette.text
|
||||
providerButton.contentTintColor = ThemePalette.text
|
||||
let chevron = NSButton(image: NSImage(systemSymbolName: "chevron.down", accessibilityDescription: nil)!,
|
||||
target: self, action: #selector(showProviders))
|
||||
chevron.isBordered = false
|
||||
chevron.contentTintColor = SunsetPalette.secondaryText
|
||||
chevron.contentTintColor = ThemePalette.secondaryText
|
||||
chevron.setAccessibilityElement(false)
|
||||
stylePill(providerControl)
|
||||
for control in [providerButton, chevron] {
|
||||
@@ -109,7 +109,7 @@ final class TranslationController: NSViewController {
|
||||
languageButton.action = #selector(showActions)
|
||||
languageButton.isBordered = false
|
||||
languageButton.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
languageButton.contentTintColor = SunsetPalette.text
|
||||
languageButton.contentTintColor = ThemePalette.text
|
||||
languageButton.toolTip = "Change or swap translation languages (Command-K)"
|
||||
languageButton.setAccessibilityLabel("Translation languages")
|
||||
actionsButton.target = self
|
||||
@@ -140,7 +140,7 @@ final class TranslationController: NSViewController {
|
||||
output.isEditable = false
|
||||
output.isSelectable = true
|
||||
output.drawsBackground = false
|
||||
output.textColor = SunsetPalette.text
|
||||
output.textColor = ThemePalette.text
|
||||
output.font = .systemFont(ofSize: 20)
|
||||
output.textContainerInset = .zero
|
||||
output.textContainer?.lineFragmentPadding = 0
|
||||
@@ -156,7 +156,7 @@ final class TranslationController: NSViewController {
|
||||
status.setAccessibilityLabel("Translation status and errors")
|
||||
status.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
sourcePreview.font = .systemFont(ofSize: 14)
|
||||
sourcePreview.textColor = SunsetPalette.secondaryText
|
||||
sourcePreview.textColor = ThemePalette.secondaryText
|
||||
sourcePreview.maximumNumberOfLines = 3
|
||||
sourcePreview.lineBreakMode = .byTruncatingTail
|
||||
sourcePreview.setAccessibilityLabel("Original text to translate")
|
||||
@@ -192,8 +192,8 @@ final class TranslationController: NSViewController {
|
||||
pill.wantsLayer = true
|
||||
pill.layer?.cornerRadius = 18
|
||||
pill.layer?.borderWidth = 0.5
|
||||
pill.layer?.borderColor = SunsetPalette.accent.withAlphaComponent(0.35).cgColor
|
||||
pill.layer?.backgroundColor = SunsetPalette.background.withAlphaComponent(0.25).cgColor
|
||||
pill.layer?.borderColor = ThemePalette.accent.withAlphaComponent(0.35).cgColor
|
||||
pill.layer?.backgroundColor = ThemePalette.background.withAlphaComponent(0.25).cgColor
|
||||
}
|
||||
|
||||
@objc func showProviders() {
|
||||
@@ -308,7 +308,7 @@ final class TranslationController: NSViewController {
|
||||
|
||||
private func setStatus(_ message: String, isError: Bool = false) {
|
||||
status.stringValue = message
|
||||
status.textColor = isError ? .systemRed : SunsetPalette.secondaryText
|
||||
status.textColor = isError ? ThemePalette.error : ThemePalette.secondaryText
|
||||
}
|
||||
|
||||
@objc func performPrimaryAction() {
|
||||
@@ -376,11 +376,11 @@ final class TranslationController: NSViewController {
|
||||
guard !Task.isCancelled, let self else { return }
|
||||
let rendered = NSMutableAttributedString(string: translation, attributes: [
|
||||
.font: NSFont.systemFont(ofSize: 20),
|
||||
.foregroundColor: SunsetPalette.text,
|
||||
.foregroundColor: ThemePalette.text,
|
||||
])
|
||||
if !myMemory, let separator = translation.range(of: "\n\n") {
|
||||
rendered.addAttributes([
|
||||
.foregroundColor: SunsetPalette.secondaryText,
|
||||
.foregroundColor: ThemePalette.secondaryText,
|
||||
.font: NSFont.systemFont(ofSize: 14),
|
||||
], range: NSRange(separator.upperBound..<translation.endIndex, in: translation))
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ final class TranslationSettingsController: NSViewController {
|
||||
let forget = NSButton(title: "Forget Key", target: self, action: #selector(forgetKey))
|
||||
for button in [save, forget] {
|
||||
button.bezelStyle = .rounded
|
||||
button.contentTintColor = SunsetPalette.accent
|
||||
button.contentTintColor = ThemePalette.accent
|
||||
}
|
||||
let controls = NSStackView(views: [keyField, save, forget])
|
||||
controls.orientation = .horizontal
|
||||
@@ -37,7 +37,7 @@ final class TranslationSettingsController: NSViewController {
|
||||
"OpenAI · gpt-6-luna · Text and optional situation/context are sent to OpenAI only when you explicitly translate, not when advancing to context. Saving a key does not send your text. Your API key is stored in Keychain."
|
||||
)
|
||||
for label in [status, myMemory, openAI] {
|
||||
label.textColor = SunsetPalette.secondaryText
|
||||
label.textColor = ThemePalette.secondaryText
|
||||
}
|
||||
let stack = NSStackView(views: [controls, status, myMemory, openAI])
|
||||
stack.orientation = .vertical
|
||||
@@ -85,6 +85,6 @@ final class TranslationSettingsController: NSViewController {
|
||||
|
||||
private func setStatus(_ message: String, isError: Bool = false) {
|
||||
status.stringValue = message
|
||||
status.textColor = isError ? .systemRed : SunsetPalette.secondaryText
|
||||
status.textColor = isError ? ThemePalette.error : ThemePalette.secondaryText
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ final class UninstallReviewController: NSViewController, NSSearchFieldDelegate,
|
||||
discoveryState = .loading
|
||||
updateStatus()
|
||||
window.makeFirstResponder(search)
|
||||
(search.currentEditor() as? NSTextView)?.insertionPointColor = SunsetPalette.accent
|
||||
(search.currentEditor() as? NSTextView)?.insertionPointColor = ThemePalette.accent
|
||||
scanTask = Task { [weak self, application] in
|
||||
do {
|
||||
let found = try await Task.detached(priority: .userInitiated) {
|
||||
@@ -62,13 +62,13 @@ final class UninstallReviewController: NSViewController, NSSearchFieldDelegate,
|
||||
view = content
|
||||
let context = NSTextField(wrappingLabelWithString: "Uninstall \(application.name)")
|
||||
context.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
context.textColor = SunsetPalette.text
|
||||
context.textColor = ThemePalette.text
|
||||
let guidance = NSTextField(wrappingLabelWithString: "Return toggles file inclusion · Command-Return opens confirmation")
|
||||
guidance.font = .systemFont(ofSize: 12)
|
||||
guidance.textColor = SunsetPalette.secondaryText
|
||||
guidance.textColor = ThemePalette.secondaryText
|
||||
search.placeholderAttributedString = NSAttributedString(
|
||||
string: "Filter files and folders by name or path…",
|
||||
attributes: [.foregroundColor: SunsetPalette.secondaryText, .font: NSFont.systemFont(ofSize: 20)]
|
||||
attributes: [.foregroundColor: ThemePalette.secondaryText, .font: NSFont.systemFont(ofSize: 20)]
|
||||
)
|
||||
if let cell = search.cell as? NSSearchFieldCell {
|
||||
cell.searchButtonCell = nil
|
||||
@@ -79,12 +79,12 @@ final class UninstallReviewController: NSViewController, NSSearchFieldDelegate,
|
||||
search.setAccessibilityLabel("Filter uninstall files for \(application.name)")
|
||||
search.setAccessibilityHelp(guidance.stringValue)
|
||||
search.font = .systemFont(ofSize: 20)
|
||||
search.textColor = SunsetPalette.text
|
||||
search.textColor = ThemePalette.text
|
||||
search.isBordered = false
|
||||
search.drawsBackground = false
|
||||
search.focusRingType = .none
|
||||
status.font = .systemFont(ofSize: 12)
|
||||
status.textColor = SunsetPalette.secondaryText
|
||||
status.textColor = ThemePalette.secondaryText
|
||||
status.setAccessibilityLabel("Uninstall selection and progress")
|
||||
back.target = self
|
||||
back.action = #selector(goBack)
|
||||
@@ -92,7 +92,7 @@ final class UninstallReviewController: NSViewController, NSSearchFieldDelegate,
|
||||
back.image = IconStyle.symbol("chevron.left", for: search.font!)
|
||||
back.imageScaling = .scaleProportionallyDown
|
||||
back.isBordered = false
|
||||
back.contentTintColor = SunsetPalette.secondaryText
|
||||
back.contentTintColor = ThemePalette.secondaryText
|
||||
back.setAccessibilityLabel("Back to applications")
|
||||
let header = NSStackView(views: [back, search])
|
||||
header.orientation = .horizontal
|
||||
@@ -100,7 +100,7 @@ final class UninstallReviewController: NSViewController, NSSearchFieldDelegate,
|
||||
header.spacing = 12
|
||||
remove.target = self
|
||||
remove.action = #selector(confirmUninstall)
|
||||
remove.contentTintColor = .systemRed
|
||||
remove.contentTintColor = ThemePalette.error
|
||||
remove.isBordered = false
|
||||
remove.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
remove.keyEquivalent = "\r"
|
||||
@@ -128,8 +128,8 @@ final class UninstallReviewController: NSViewController, NSSearchFieldDelegate,
|
||||
pill.wantsLayer = true
|
||||
pill.layer?.cornerRadius = 18
|
||||
pill.layer?.borderWidth = 0.5
|
||||
pill.layer?.borderColor = SunsetPalette.accent.withAlphaComponent(0.35).cgColor
|
||||
pill.layer?.backgroundColor = SunsetPalette.background.withAlphaComponent(0.25).cgColor
|
||||
pill.layer?.borderColor = ThemePalette.accent.withAlphaComponent(0.35).cgColor
|
||||
pill.layer?.backgroundColor = ThemePalette.background.withAlphaComponent(0.25).cgColor
|
||||
let shortcut = ShortcutIndicator("⌘", "↵")
|
||||
for child in [remove, shortcut] {
|
||||
child.translatesAutoresizingMaskIntoConstraints = false
|
||||
@@ -204,14 +204,14 @@ final class UninstallReviewController: NSViewController, NSSearchFieldDelegate,
|
||||
}
|
||||
|
||||
private func updateStatus() {
|
||||
status.textColor = SunsetPalette.secondaryText
|
||||
status.textColor = ThemePalette.secondaryText
|
||||
remove.isEnabled = false
|
||||
switch discoveryState {
|
||||
case .loading:
|
||||
status.stringValue = "Finding associated files…"
|
||||
case .failed(let message):
|
||||
status.stringValue = message
|
||||
status.textColor = .systemRed
|
||||
status.textColor = ThemePalette.error
|
||||
case .ready:
|
||||
let chosen = candidates.filter { selected.contains($0.url) }
|
||||
let size = chosen.compactMap(\.size).reduce(Int64(0), +)
|
||||
@@ -260,7 +260,7 @@ final class UninstallReviewController: NSViewController, NSSearchFieldDelegate,
|
||||
cell.toggle.isEnabled = candidate.isSelectable && !busy
|
||||
cell.toggle.setAccessibilityLabel("Select \(candidate.url.lastPathComponent)")
|
||||
cell.name.stringValue = candidate.url.lastPathComponent
|
||||
cell.name.textColor = candidate.isSelectable ? SunsetPalette.text : SunsetPalette.secondaryText
|
||||
cell.name.textColor = candidate.isSelectable ? ThemePalette.text : ThemePalette.secondaryText
|
||||
let path = (candidate.url.deletingLastPathComponent().path as NSString).abbreviatingWithTildeInPath
|
||||
cell.detail.stringValue = candidate.reason.map { "\(path) · \($0)" } ?? path
|
||||
cell.size.stringValue = candidate.size.map { ByteCountFormatter.string(fromByteCount: $0, countStyle: .file) } ?? "—"
|
||||
@@ -318,7 +318,7 @@ final class UninstallReviewController: NSViewController, NSSearchFieldDelegate,
|
||||
return
|
||||
}
|
||||
do {
|
||||
status.textColor = SunsetPalette.secondaryText
|
||||
status.textColor = ThemePalette.secondaryText
|
||||
status.stringValue = "Waiting for application to quit…"
|
||||
for app in running where !app.isTerminated {
|
||||
guard app.terminate() else { throw ReviewError("\(app.localizedName ?? application.name) could not quit. No files were moved.") }
|
||||
@@ -368,7 +368,7 @@ final class UninstallReviewController: NSViewController, NSSearchFieldDelegate,
|
||||
alert.addButton(withTitle: "OK")
|
||||
if canRecover { alert.addButton(withTitle: "Retry recovery") }
|
||||
guard await alert.beginSheetModal(for: window) == .alertSecondButtonReturn, canRecover else { return }
|
||||
status.textColor = SunsetPalette.secondaryText
|
||||
status.textColor = ThemePalette.secondaryText
|
||||
status.stringValue = "Recovering pending uninstall…"
|
||||
let report = await uninstaller.recover()
|
||||
changed?()
|
||||
@@ -432,7 +432,7 @@ private final class UninstallFileCell: NSTableCellView {
|
||||
super.init(frame: .zero)
|
||||
name.font = font
|
||||
detail.font = .systemFont(ofSize: 11)
|
||||
detail.textColor = SunsetPalette.secondaryText
|
||||
detail.textColor = ThemePalette.secondaryText
|
||||
for label in [name, detail] {
|
||||
label.lineBreakMode = .byTruncatingMiddle
|
||||
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
@@ -441,7 +441,7 @@ private final class UninstallFileCell: NSTableCellView {
|
||||
labels.orientation = .vertical
|
||||
labels.alignment = .leading
|
||||
labels.spacing = 3
|
||||
size.textColor = SunsetPalette.secondaryText
|
||||
size.textColor = ThemePalette.secondaryText
|
||||
size.font = .systemFont(ofSize: 12)
|
||||
size.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
let row = NSStackView(views: [toggle, labels, NSView(), size, fileIcon])
|
||||
|
||||
Reference in New Issue
Block a user