@@ -2,6 +2,14 @@ import AppKit
22import Defaults
33import 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+
513struct 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