macos: stable sort for surfaces
parent
1fd3f27e26
commit
d23f7e051f
|
|
@ -1,15 +1,27 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct CommandOption: Identifiable, Hashable {
|
struct CommandOption: Identifiable, Hashable {
|
||||||
|
/// Unique identifier for this option.
|
||||||
let id = UUID()
|
let id = UUID()
|
||||||
|
/// The primary text displayed for this command.
|
||||||
let title: String
|
let title: String
|
||||||
|
/// Secondary text displayed below the title.
|
||||||
let subtitle: String?
|
let subtitle: String?
|
||||||
|
/// Tooltip text shown on hover.
|
||||||
let description: String?
|
let description: String?
|
||||||
|
/// Keyboard shortcut symbols to display.
|
||||||
let symbols: [String]?
|
let symbols: [String]?
|
||||||
|
/// SF Symbol name for the leading icon.
|
||||||
let leadingIcon: String?
|
let leadingIcon: String?
|
||||||
|
/// Color for the leading indicator circle.
|
||||||
let leadingColor: Color?
|
let leadingColor: Color?
|
||||||
|
/// Badge text displayed as a pill.
|
||||||
let badge: String?
|
let badge: String?
|
||||||
|
/// Whether to visually emphasize this option.
|
||||||
let emphasis: Bool
|
let emphasis: Bool
|
||||||
|
/// Sort key for stable ordering when titles are equal.
|
||||||
|
let sortKey: AnySortKey?
|
||||||
|
/// The action to perform when this option is selected.
|
||||||
let action: () -> Void
|
let action: () -> Void
|
||||||
|
|
||||||
init(
|
init(
|
||||||
|
|
@ -21,6 +33,7 @@ struct CommandOption: Identifiable, Hashable {
|
||||||
leadingColor: Color? = nil,
|
leadingColor: Color? = nil,
|
||||||
badge: String? = nil,
|
badge: String? = nil,
|
||||||
emphasis: Bool = false,
|
emphasis: Bool = false,
|
||||||
|
sortKey: AnySortKey? = nil,
|
||||||
action: @escaping () -> Void
|
action: @escaping () -> Void
|
||||||
) {
|
) {
|
||||||
self.title = title
|
self.title = title
|
||||||
|
|
@ -31,6 +44,7 @@ struct CommandOption: Identifiable, Hashable {
|
||||||
self.leadingColor = leadingColor
|
self.leadingColor = leadingColor
|
||||||
self.badge = badge
|
self.badge = badge
|
||||||
self.emphasis = emphasis
|
self.emphasis = emphasis
|
||||||
|
self.sortKey = sortKey
|
||||||
self.action = action
|
self.action = action
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,11 +67,20 @@ struct TerminalCommandPaletteView: View {
|
||||||
options.append(contentsOf: updateOptions)
|
options.append(contentsOf: updateOptions)
|
||||||
|
|
||||||
// Sort the rest. We replace ":" with a character that sorts before space
|
// Sort the rest. We replace ":" with a character that sorts before space
|
||||||
// so that "Foo:" sorts before "Foo Bar:".
|
// so that "Foo:" sorts before "Foo Bar:". Use sortKey as a tie-breaker
|
||||||
|
// for stable ordering when titles are equal.
|
||||||
options.append(contentsOf: (jumpOptions + terminalOptions).sorted { a, b in
|
options.append(contentsOf: (jumpOptions + terminalOptions).sorted { a, b in
|
||||||
let aNormalized = a.title.replacingOccurrences(of: ":", with: "\t")
|
let aNormalized = a.title.replacingOccurrences(of: ":", with: "\t")
|
||||||
let bNormalized = b.title.replacingOccurrences(of: ":", with: "\t")
|
let bNormalized = b.title.replacingOccurrences(of: ":", with: "\t")
|
||||||
return aNormalized.localizedCaseInsensitiveCompare(bNormalized) == .orderedAscending
|
let comparison = aNormalized.localizedCaseInsensitiveCompare(bNormalized)
|
||||||
|
if comparison != .orderedSame {
|
||||||
|
return comparison == .orderedAscending
|
||||||
|
}
|
||||||
|
// Tie-breaker: use sortKey if both have one
|
||||||
|
if let aSortKey = a.sortKey, let bSortKey = b.sortKey {
|
||||||
|
return aSortKey < bSortKey
|
||||||
|
}
|
||||||
|
return false
|
||||||
})
|
})
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
@ -153,7 +162,8 @@ struct TerminalCommandPaletteView: View {
|
||||||
title: "Focus: \(displayTitle)",
|
title: "Focus: \(displayTitle)",
|
||||||
subtitle: subtitle,
|
subtitle: subtitle,
|
||||||
leadingIcon: "rectangle.on.rectangle",
|
leadingIcon: "rectangle.on.rectangle",
|
||||||
leadingColor: displayColor?.displayColor.map { Color($0) }
|
leadingColor: displayColor?.displayColor.map { Color($0) },
|
||||||
|
sortKey: AnySortKey(ObjectIdentifier(surface))
|
||||||
) {
|
) {
|
||||||
NotificationCenter.default.post(
|
NotificationCenter.default.post(
|
||||||
name: Ghostty.Notification.ghosttyPresentTerminal,
|
name: Ghostty.Notification.ghosttyPresentTerminal,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Type-erased wrapper for any Comparable type to use as a sort key.
|
||||||
|
struct AnySortKey: Comparable {
|
||||||
|
private let value: Any
|
||||||
|
private let comparator: (Any, Any) -> ComparisonResult
|
||||||
|
|
||||||
|
init<T: Comparable>(_ value: T) {
|
||||||
|
self.value = value
|
||||||
|
self.comparator = { lhs, rhs in
|
||||||
|
guard let l = lhs as? T, let r = rhs as? T else { return .orderedSame }
|
||||||
|
if l < r { return .orderedAscending }
|
||||||
|
if l > r { return .orderedDescending }
|
||||||
|
return .orderedSame
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func < (lhs: AnySortKey, rhs: AnySortKey) -> Bool {
|
||||||
|
lhs.comparator(lhs.value, rhs.value) == .orderedAscending
|
||||||
|
}
|
||||||
|
|
||||||
|
static func == (lhs: AnySortKey, rhs: AnySortKey) -> Bool {
|
||||||
|
lhs.comparator(lhs.value, rhs.value) == .orderedSame
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue