macos: allow searching sessions by color too

pull/9945/head
Mitchell Hashimoto 2025-12-17 10:48:39 -08:00
parent 842583b628
commit e1d0b22029
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
3 changed files with 78 additions and 3 deletions

View File

@ -157,6 +157,7 @@
"Helpers/Extensions/KeyboardShortcut+Extension.swift",
"Helpers/Extensions/NSAppearance+Extension.swift",
"Helpers/Extensions/NSApplication+Extension.swift",
"Helpers/Extensions/NSColor+Extension.swift",
"Helpers/Extensions/NSImage+Extension.swift",
"Helpers/Extensions/NSMenu+Extension.swift",
"Helpers/Extensions/NSMenuItem+Extension.swift",

View File

@ -67,14 +67,23 @@ struct CommandPaletteView: View {
@FocusState private var isTextFieldFocused: Bool
// The options that we should show, taking into account any filtering from
// the query.
// the query. Options with matching leadingColor are ranked higher.
var filteredOptions: [CommandOption] {
if query.isEmpty {
return options
} else {
return options.filter {
// Filter by title/subtitle match OR color match
let filtered = options.filter {
$0.title.localizedCaseInsensitiveContains(query) ||
($0.subtitle?.localizedCaseInsensitiveContains(query) ?? false)
($0.subtitle?.localizedCaseInsensitiveContains(query) ?? false) ||
colorMatchScore(for: $0.leadingColor, query: query) > 0
}
// Sort by color match score (higher scores first), then maintain original order
return filtered.sorted { a, b in
let scoreA = colorMatchScore(for: a.leadingColor, query: query)
let scoreB = colorMatchScore(for: b.leadingColor, query: query)
return scoreA > scoreB
}
}
}
@ -191,6 +200,32 @@ struct CommandPaletteView: View {
isTextFieldFocused = isPresented
}
}
/// Returns a score (0.0 to 1.0) indicating how well a color matches a search query color name.
/// Returns 0 if no color name in the query matches, or if the color is nil.
private func colorMatchScore(for color: Color?, query: String) -> Double {
guard let color = color else { return 0 }
let queryLower = query.lowercased()
let nsColor = NSColor(color)
var bestScore: Double = 0
for name in NSColor.colorNames {
guard queryLower.contains(name),
let systemColor = NSColor(named: name) else { continue }
let distance = nsColor.distance(to: systemColor)
// Max distance in weighted RGB space is ~3.0, so normalize and invert
// Use a threshold to determine "close enough" matches
let maxDistance: Double = 1.5
if distance < maxDistance {
let score = 1.0 - (distance / maxDistance)
bestScore = max(bestScore, score)
}
}
return bestScore
}
}
/// The text field for building the query for the command palette.

View File

@ -0,0 +1,39 @@
import AppKit
extension NSColor {
/// Using a color list let's us get localized names.
private static let appleColorList: NSColorList? = NSColorList(named: "Apple")
convenience init?(named name: String) {
guard let colorList = Self.appleColorList,
let color = colorList.color(withKey: name.capitalized) else {
return nil
}
guard let components = color.usingColorSpace(.sRGB) else {
return nil
}
self.init(
red: components.redComponent,
green: components.greenComponent,
blue: components.blueComponent,
alpha: components.alphaComponent
)
}
static var colorNames: [String] {
appleColorList?.allKeys.map { $0.lowercased() } ?? []
}
/// Calculates the perceptual distance to another color in RGB space.
func distance(to other: NSColor) -> Double {
guard let a = self.usingColorSpace(.sRGB),
let b = other.usingColorSpace(.sRGB) else { return .infinity }
let dr = a.redComponent - b.redComponent
let dg = a.greenComponent - b.greenComponent
let db = a.blueComponent - b.blueComponent
// Weighted Euclidean distance (human eye is more sensitive to green)
return sqrt(2 * dr * dr + 4 * dg * dg + 3 * db * db)
}
}