feat: distinguish duplicate application names by folder

This commit is contained in:
2026-10-11 21:30:05 +02:00
parent 3714fc516b
commit 77ed65a22a
2 changed files with 51 additions and 7 deletions
+2
View File
@@ -41,6 +41,8 @@ With an empty search, applications are ordered by successful launch count, then
Background discovery preserves the selected application even if its row changes. If that application disappears, the first remaining result is selected. Changing the query or reopening the launcher selects the first result.
Applications with identical display names show a muted containing-folder hint beside the name. Copies sharing the same folder show their full application bundle paths instead. Home paths abbreviate to `~`, and long hints truncate in the middle. Unique names have no hint; locations are recalculated whenever discovery refreshes.
## Native UI
The launcher keeps its Raycast-inspired 750 × 474-point, titlebar-free rounded panel, large borderless search field, compact icon rows, right-aligned application labels, and bottom-right launch pill. Its Sunset palette uses a dark brown background, soft amber and rust glows, warm cream text, and orange accents on the caret, selection, and action outline.
+49 -7
View File
@@ -11,6 +11,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
private let history: LaunchHistory
private var applications: [Application] = []
private var results: [Application] = []
private var locationLabels: [URL: String] = [:]
private var discoveryTask: Task<Void, Never>?
private var isLaunching = false
@@ -228,11 +229,34 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
}.value
guard let self else { return }
self.applications = applications
self.updateLocationLabels()
self.discoveryTask = nil
self.updateResults(preservingSelection: true)
}
}
private func updateLocationLabels() {
var nameCounts: [String: Int] = [:]
nameCounts.reserveCapacity(applications.count)
for application in applications { nameCounts[application.name, default: 0] += 1 }
var groups: [String: [(url: URL, folder: URL)]] = [:]
for application in applications where (nameCounts[application.name] ?? 0) > 1 {
groups[application.name, default: []].append(
(application.url, application.url.deletingLastPathComponent())
)
}
locationLabels.removeAll(keepingCapacity: true)
for group in groups.values {
var folderCounts: [URL: Int] = [:]
folderCounts.reserveCapacity(group.count)
for entry in group { folderCounts[entry.folder, default: 0] += 1 }
for entry in group {
let location = (folderCounts[entry.folder] ?? 0) > 1 ? entry.url : entry.folder
locationLabels[entry.url] = (location.path as NSString).abbreviatingWithTildeInPath
}
}
}
private func updateResults(preservingSelection: Bool = false) {
let selectedURL = preservingSelection && results.indices.contains(tableView.selectedRow)
? results[tableView.selectedRow].id : nil
@@ -299,10 +323,13 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let identifier = NSUserInterfaceItemIdentifier("applicationCell")
let cell = (tableView.makeView(withIdentifier: identifier, owner: self) as? NSTableCellView)
let cell = (tableView.makeView(withIdentifier: identifier, owner: self) as? ApplicationCell)
?? makeCell(identifier: identifier)
let application = results[row]
cell.textField?.stringValue = application.name
let location = locationLabels[application.id]
cell.locationLabel.stringValue = location ?? ""
cell.locationLabel.isHidden = location == nil
cell.imageView?.image = NSWorkspace.shared.icon(forFile: application.url.path)
cell.toolTip = application.url.path
return cell
@@ -316,8 +343,8 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
return view
}
private func makeCell(identifier: NSUserInterfaceItemIdentifier) -> NSTableCellView {
let cell = NSTableCellView()
private func makeCell(identifier: NSUserInterfaceItemIdentifier) -> ApplicationCell {
let cell = ApplicationCell()
cell.identifier = identifier
let icon = NSImageView()
icon.imageScaling = .scaleProportionallyDown
@@ -326,6 +353,16 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
label.font = .systemFont(ofSize: 14, weight: .medium)
label.textColor = SunsetPalette.text
label.lineBreakMode = .byTruncatingTail
let location = cell.locationLabel
location.font = .systemFont(ofSize: 12)
location.textColor = SunsetPalette.secondaryText
location.lineBreakMode = .byTruncatingMiddle
location.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
location.isHidden = true
let title = NSStackView(views: [label, location])
title.orientation = .horizontal
title.alignment = .centerY
title.spacing = 8
let kind = NSTextField(labelWithString: "Application")
kind.font = .systemFont(ofSize: 14)
kind.textColor = SunsetPalette.secondaryText
@@ -333,7 +370,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
kind.setAccessibilityElement(false)
cell.imageView = icon
cell.textField = label
for view in [icon, label, kind] {
for view in [icon, title, kind] {
view.translatesAutoresizingMaskIntoConstraints = false
cell.addSubview(view)
}
@@ -342,9 +379,9 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
icon.centerYAnchor.constraint(equalTo: cell.centerYAnchor),
icon.widthAnchor.constraint(equalToConstant: 22),
icon.heightAnchor.constraint(equalToConstant: 22),
label.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: 12),
label.centerYAnchor.constraint(equalTo: cell.centerYAnchor),
label.trailingAnchor.constraint(lessThanOrEqualTo: kind.leadingAnchor, constant: -12),
title.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: 12),
title.centerYAnchor.constraint(equalTo: cell.centerYAnchor),
title.trailingAnchor.constraint(lessThanOrEqualTo: kind.leadingAnchor, constant: -12),
kind.centerYAnchor.constraint(equalTo: cell.centerYAnchor),
kind.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -8),
])
@@ -374,6 +411,11 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
}
}
@MainActor
private final class ApplicationCell: NSTableCellView {
let locationLabel = NSTextField(labelWithString: "")
}
@MainActor
private final class ApplicationTableView: NSTableView {
var openSelection: (() -> Void)?