macos: simplify command palette sort keys

Store the Comparable ObjectIdentifier directly instead of wrapping the
only sort key type in AnySortKey.

The expected deterministic ordering of equal terminal command titles is
also now verified by a unit test.
This commit is contained in:
Jon Parise
2026-08-17 10:37:02 -04:00
parent ee57b94170
commit 5c952ac977
4 changed files with 43 additions and 43 deletions

View File

@@ -20,7 +20,7 @@ struct CommandOption: Identifiable, Hashable {
/// Whether to visually emphasize this option.
let emphasis: Bool
/// Sort key for stable ordering when titles are equal.
let sortKey: AnySortKey?
let sortKey: ObjectIdentifier?
/// The action to perform when this option is selected.
let action: () -> Void
@@ -33,7 +33,7 @@ struct CommandOption: Identifiable, Hashable {
leadingColor: Color? = nil,
badge: String? = nil,
emphasis: Bool = false,
sortKey: AnySortKey? = nil,
sortKey: ObjectIdentifier? = nil,
action: @escaping () -> Void
) {
self.title = title

View File

@@ -1,6 +1,21 @@
import SwiftUI
import GhosttyKit
func sortedTerminalPaletteOptions(_ options: [CommandOption]) -> [CommandOption] {
options.sorted { lhs, rhs in
let lhsTitle = lhs.title.replacingOccurrences(of: ":", with: "\t")
let rhsTitle = rhs.title.replacingOccurrences(of: ":", with: "\t")
let comparison = lhsTitle.localizedCaseInsensitiveCompare(rhsTitle)
if comparison != .orderedSame {
return comparison == .orderedAscending
}
if let lhsKey = lhs.sortKey, let rhsKey = rhs.sortKey {
return lhsKey < rhsKey
}
return false
}
}
struct TerminalCommandPaletteView: View {
/// The surface that this command palette represents.
let surfaceView: Ghostty.SurfaceView
@@ -64,19 +79,7 @@ struct TerminalCommandPaletteView: View {
// Sort the rest. We replace ":" with a character that sorts before space
// 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
let aNormalized = a.title.replacingOccurrences(of: ":", with: "\t")
let bNormalized = b.title.replacingOccurrences(of: ":", with: "\t")
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
})
options.append(contentsOf: sortedTerminalPaletteOptions(jumpOptions + terminalOptions))
return options
}
@@ -168,7 +171,7 @@ struct TerminalCommandPaletteView: View {
subtitle: subtitle,
leadingIcon: "rectangle.on.rectangle",
leadingColor: displayColor?.displayColor.map { Color($0) },
sortKey: AnySortKey(ObjectIdentifier(surface))
sortKey: ObjectIdentifier(surface)
) {
NotificationCenter.default.post(
name: Ghostty.Notification.ghosttyPresentTerminal,

View File

@@ -1,25 +0,0 @@
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
}
}

View File

@@ -14,13 +14,15 @@ struct CommandPaletteFilterTests {
title: String,
subtitle: String? = nil,
description: String? = nil,
leadingColor: Color? = nil
leadingColor: Color? = nil,
sortKey: ObjectIdentifier? = nil
) -> CommandOption {
CommandOption(
title: title,
subtitle: subtitle,
description: description,
leadingColor: leadingColor
leadingColor: leadingColor,
sortKey: sortKey
) {}
}
@@ -46,4 +48,24 @@ struct CommandPaletteFilterTests {
#expect([first, second].filteredAndSorted(query: "new") == [first, second])
#expect([second, first].filteredAndSorted(query: "new") == [second, first])
}
/// Equal titles use their sort keys independent of input order.
@Test func equalTitlesUseSortKey() {
let firstKey = NSObject()
let secondKey = NSObject()
let first = option(
title: "Focus: Shell",
subtitle: "/tmp",
sortKey: ObjectIdentifier(firstKey)
)
let second = option(
title: "Focus: Shell",
subtitle: "/tmp",
sortKey: ObjectIdentifier(secondKey)
)
let forward = sortedTerminalPaletteOptions([first, second])
let reverse = sortedTerminalPaletteOptions([second, first])
#expect(forward == reverse)
}
}