macos: allow searching sessions by color too
parent
842583b628
commit
e1d0b22029
|
|
@ -157,6 +157,7 @@
|
||||||
"Helpers/Extensions/KeyboardShortcut+Extension.swift",
|
"Helpers/Extensions/KeyboardShortcut+Extension.swift",
|
||||||
"Helpers/Extensions/NSAppearance+Extension.swift",
|
"Helpers/Extensions/NSAppearance+Extension.swift",
|
||||||
"Helpers/Extensions/NSApplication+Extension.swift",
|
"Helpers/Extensions/NSApplication+Extension.swift",
|
||||||
|
"Helpers/Extensions/NSColor+Extension.swift",
|
||||||
"Helpers/Extensions/NSImage+Extension.swift",
|
"Helpers/Extensions/NSImage+Extension.swift",
|
||||||
"Helpers/Extensions/NSMenu+Extension.swift",
|
"Helpers/Extensions/NSMenu+Extension.swift",
|
||||||
"Helpers/Extensions/NSMenuItem+Extension.swift",
|
"Helpers/Extensions/NSMenuItem+Extension.swift",
|
||||||
|
|
|
||||||
|
|
@ -67,14 +67,23 @@ struct CommandPaletteView: View {
|
||||||
@FocusState private var isTextFieldFocused: Bool
|
@FocusState private var isTextFieldFocused: Bool
|
||||||
|
|
||||||
// The options that we should show, taking into account any filtering from
|
// 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] {
|
var filteredOptions: [CommandOption] {
|
||||||
if query.isEmpty {
|
if query.isEmpty {
|
||||||
return options
|
return options
|
||||||
} else {
|
} else {
|
||||||
return options.filter {
|
// Filter by title/subtitle match OR color match
|
||||||
|
let filtered = options.filter {
|
||||||
$0.title.localizedCaseInsensitiveContains(query) ||
|
$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
|
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.
|
/// The text field for building the query for the command palette.
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue