Skip to content

Commit 4662f8d

Browse files
committed
fix: use bundle identifiers for app filtering
The app filter was not working for apps like Spotify because the filter stored app names but checked against NSRunningApplication.localizedName. - Store bundle identifiers when filtering apps (UI still shows names) - Check both bundle ID (exact match) and app name (contains) for compatibility - Add CoreServices path to include Finder in app list - Include custom app directories in filter scan Fixes #915
1 parent 6e74b34 commit 4662f8d

File tree

3 files changed

+112
-46
lines changed

3 files changed

+112
-46
lines changed

DockDoor/Utilities/DockObserver.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,10 @@ final class DockObserver {
171171
return
172172
}
173173

174+
if WindowUtil.isAppFiltered(currentApp) {
175+
return
176+
}
177+
174178
let currentAppInfo = ApplicationInfo(
175179
processIdentifier: currentApp.processIdentifier,
176180
bundleIdentifier: currentApp.bundleIdentifier,
@@ -537,6 +541,10 @@ final class DockObserver {
537541
}
538542

539543
private func showPreviewForFocusedApp(app: NSRunningApplication) {
544+
if WindowUtil.isAppFiltered(app) {
545+
return
546+
}
547+
540548
Task { @MainActor [weak self] in
541549
guard let self else { return }
542550

DockDoor/Utilities/Window Management/WindowUtil.swift

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,17 @@ enum WindowAction: String, Hashable, CaseIterable, Defaults.Serializable {
218218
enum WindowUtil {
219219
private static let desktopSpaceWindowCacheManager = SpaceWindowCacheManager()
220220

221+
static func isAppFiltered(_ app: NSRunningApplication) -> Bool {
222+
let filters = Defaults[.appNameFilters]
223+
guard !filters.isEmpty else { return false }
224+
225+
let bundleId = app.bundleIdentifier ?? ""
226+
let appName = app.localizedName ?? ""
227+
228+
// Check bundle ID (new format) or app name (legacy format)
229+
return filters.contains(bundleId) || filters.contains(where: { $0.caseInsensitiveCompare(appName) == .orderedSame })
230+
}
231+
221232
// Track windows explicitly updated by bringWindowToFront to prevent observer duplication
222233
private static var timestampUpdates: [AXUIElement: Date] = [:]
223234
private static let updateTimestampLock = NSLock()
@@ -531,6 +542,11 @@ extension WindowUtil {
531542
}
532543

533544
static func getActiveWindows(of app: NSRunningApplication, context: WindowFetchContext = .dockPreview) async throws -> [WindowInfo] {
545+
if isAppFiltered(app) {
546+
purgeAppCache(with: app.processIdentifier)
547+
return []
548+
}
549+
534550
var sckWindowIDs = Set<CGWindowID>()
535551

536552
// Skip SCK if user has disabled image previews (compact mode only)
@@ -583,9 +599,7 @@ extension WindowUtil {
583599
return 0
584600
}
585601

586-
let appName = app.localizedName ?? ""
587-
let appNameFilters = Defaults[.appNameFilters]
588-
if !appNameFilters.isEmpty, appNameFilters.contains(where: { appName.lowercased().contains($0.lowercased()) }) {
602+
if isAppFiltered(app) {
589603
purgeAppCache(with: pid)
590604
return 0
591605
}
@@ -726,16 +740,9 @@ extension WindowUtil {
726740
return
727741
}
728742

729-
if let appName = app.localizedName {
730-
let appNameFilters = Defaults[.appNameFilters]
731-
if !appNameFilters.isEmpty {
732-
for filter in appNameFilters {
733-
if appName.lowercased().contains(filter.lowercased()) {
734-
purgeAppCache(with: app.processIdentifier)
735-
return
736-
}
737-
}
738-
}
743+
if isAppFiltered(app) {
744+
purgeAppCache(with: app.processIdentifier)
745+
return
739746
}
740747

741748
if let windowTitle = window.title {

DockDoor/Views/Settings/FiltersSettingsView.swift

Lines changed: 84 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@ import AppKit
22
import Defaults
33
import SwiftUI
44

5+
struct InstalledApp: Identifiable {
6+
let id: String
7+
let name: String
8+
let icon: NSImage
9+
10+
var bundleIdentifier: String { id }
11+
}
12+
513
struct FiltersSettingsView: View {
614
@Default(.appNameFilters) var appNameFilters
715
@Default(.windowTitleFilters) var windowTitleFilters
@@ -10,43 +18,65 @@ struct FiltersSettingsView: View {
1018
@State private var showingAddFilterSheet = false
1119
@State private var newFilter = FilterEntry(text: "")
1220
@State private var showingDirectoryPicker = false
21+
@State private var installedApps: [InstalledApp] = []
22+
@State private var isLoadingApps = true
1323

1424
struct FilterEntry: Identifiable, Hashable {
1525
let id = UUID()
1626
var text: String
1727
}
1828

19-
private var installedApps: [(name: String, icon: NSImage)] {
20-
var apps: [(String, NSImage)] = []
21-
let appLocations = [
22-
"/Applications",
23-
"/System/Applications",
24-
"/System/Applications/Utilities",
25-
"~/Applications",
26-
]
27-
28-
for location in appLocations {
29-
let expandedPath = NSString(string: location).expandingTildeInPath
29+
private func loadInstalledApps() async -> [InstalledApp] {
30+
await Task.detached(priority: .userInitiated) {
31+
var apps: [InstalledApp] = []
32+
let workspace = NSWorkspace.shared
3033
let fileManager = FileManager.default
31-
guard let urls = try? fileManager.contentsOfDirectory(
32-
at: URL(fileURLWithPath: expandedPath),
33-
includingPropertiesForKeys: nil,
34-
options: [.skipsHiddenFiles]
35-
) else { continue }
36-
37-
let locationApps = urls
38-
.filter { $0.pathExtension == "app" }
39-
.compactMap { url -> (String, NSImage)? in
40-
guard let bundle = Bundle(url: url),
34+
35+
let defaultLocations = [
36+
"/Applications",
37+
"/System/Applications",
38+
"/System/Applications/Utilities",
39+
"~/Applications",
40+
].map { NSString(string: $0).expandingTildeInPath }
41+
42+
let allLocations = Set(defaultLocations + Defaults[.customAppDirectories])
43+
44+
for directory in allLocations {
45+
guard let enumerator = fileManager.enumerator(
46+
at: URL(fileURLWithPath: directory),
47+
includingPropertiesForKeys: [.isApplicationKey],
48+
options: [.skipsHiddenFiles, .skipsPackageDescendants]
49+
) else { continue }
50+
51+
for case let fileURL as URL in enumerator {
52+
guard fileURL.pathExtension == "app" else { continue }
53+
54+
guard let bundle = Bundle(url: fileURL),
55+
let bundleId = bundle.bundleIdentifier,
4156
let name = bundle.infoDictionary?["CFBundleName"] as? String ?? bundle.infoDictionary?["CFBundleDisplayName"] as? String
42-
else { return nil }
43-
return (name, NSWorkspace.shared.icon(forFile: url.path))
57+
else { continue }
58+
59+
apps.append(InstalledApp(id: bundleId, name: name, icon: workspace.icon(forFile: fileURL.path)))
4460
}
61+
}
4562

46-
apps.append(contentsOf: locationApps)
47-
}
63+
// Add Finder explicitly
64+
apps.append(InstalledApp(
65+
id: "com.apple.finder",
66+
name: "Finder",
67+
icon: workspace.icon(forFile: "/System/Library/CoreServices/Finder.app")
68+
))
4869

49-
return apps.sorted { $0.0 < $1.0 }
70+
// Remove duplicates and sort
71+
var seenBundleIds = Set<String>()
72+
return apps.filter { app in
73+
if seenBundleIds.contains(app.id) {
74+
return false
75+
}
76+
seenBundleIds.insert(app.id)
77+
return true
78+
}.sorted { $0.name.localizedStandardCompare($1.name) == .orderedAscending }
79+
}.value
5080
}
5181

5282
var body: some View {
@@ -141,21 +171,37 @@ struct FiltersSettingsView: View {
141171

142172
ScrollView {
143173
VStack(alignment: .leading, spacing: 4) {
144-
if installedApps.isEmpty {
145-
Text("No applications found or scanned yet.")
174+
if isLoadingApps {
175+
HStack {
176+
Spacer()
177+
ProgressView()
178+
.scaleEffect(0.8)
179+
Text("Loading applications...")
180+
.foregroundColor(.secondary)
181+
Spacer()
182+
}
183+
.padding()
184+
} else if installedApps.isEmpty {
185+
Text("No applications found.")
146186
.foregroundColor(.secondary)
147187
.padding()
148188
} else {
149-
ForEach(installedApps, id: \.name) { app in
189+
ForEach(installedApps) { app in
150190
HStack(spacing: 8) {
151191
Toggle(isOn: Binding(
152-
get: { !appNameFilters.contains(app.name) },
192+
get: {
193+
// Check both bundle ID and legacy app name
194+
!appNameFilters.contains(app.bundleIdentifier) &&
195+
!appNameFilters.contains(where: { $0.caseInsensitiveCompare(app.name) == .orderedSame })
196+
},
153197
set: { isEnabled in
154198
if isEnabled {
155-
appNameFilters.removeAll { $0 == app.name }
199+
// Remove both bundle ID and legacy app name
200+
appNameFilters.removeAll { $0 == app.bundleIdentifier }
201+
appNameFilters.removeAll { $0.caseInsensitiveCompare(app.name) == .orderedSame }
156202
} else {
157-
if !appNameFilters.contains(app.name) {
158-
appNameFilters.append(app.name)
203+
if !appNameFilters.contains(app.bundleIdentifier) {
204+
appNameFilters.append(app.bundleIdentifier)
159205
}
160206
}
161207
}
@@ -183,6 +229,11 @@ struct FiltersSettingsView: View {
183229
)
184230
}
185231
}
232+
.task(id: customAppDirectories) {
233+
isLoadingApps = true
234+
installedApps = await loadInstalledApps()
235+
isLoadingApps = false
236+
}
186237

187238
// Window Title Filters Section
188239
StyledGroupBox(label: "Window Title Filters") {

0 commit comments

Comments
 (0)