@@ -38,7 +38,7 @@ use std::{
3838} ;
3939
4040use crate :: config:: { Config , Integration } ;
41- use crate :: os_broker:: { AccessibilityPermissionStatus , OsBroker } ;
41+ use crate :: os_broker:: { AccessibilityPermissionStatus , AppSearchResult , OsBroker } ;
4242use crate :: rect:: { ActionableLint , Rect } ;
4343
4444const WINDOW_MOVEMENT_SETTLE_DURATION : Duration = Duration :: from_millis ( 150 ) ;
@@ -417,6 +417,103 @@ impl OsBroker for MacBroker {
417417
418418 Ok ( ( ) )
419419 }
420+
421+ fn search_apps ( & self , query : & str ) -> Result < Vec < AppSearchResult > , String > {
422+ let query = query. trim ( ) ;
423+
424+ if query. is_empty ( ) {
425+ return Ok ( Vec :: new ( ) ) ;
426+ }
427+
428+ // Search for apps by display name using mdfind
429+ // Use case-insensitive search and ensure we only get .app bundles
430+ let escaped_query = query. replace ( '\\' , "\\ \\ " ) . replace ( '"' , "\\ \" " ) ;
431+ let output = Command :: new ( "mdfind" )
432+ . arg ( format ! (
433+ "kMDItemContentType == 'com.apple.application-bundle' && kMDItemDisplayName == '*{escaped_query}*'cd"
434+ ) )
435+ . output ( )
436+ . map_err ( |error| format ! ( "Failed to execute mdfind: {error}" ) ) ?;
437+
438+ if !output. status . success ( ) {
439+ return Err ( format ! (
440+ "mdfind failed: {}" ,
441+ String :: from_utf8_lossy( & output. stderr)
442+ ) ) ;
443+ }
444+
445+ let mut results = Vec :: new ( ) ;
446+ let mut seen_bundle_ids = std:: collections:: HashSet :: new ( ) ;
447+
448+ for line in String :: from_utf8_lossy ( & output. stdout ) . lines ( ) {
449+ let line = line. trim ( ) ;
450+
451+ // Skip if not an .app bundle
452+ if !line. ends_with ( ".app" ) {
453+ continue ;
454+ }
455+
456+ if let Some ( display_name) = display_name_from_app_path ( line) {
457+ // Skip if display name looks like a bundle ID (contains dots)
458+ if display_name. contains ( '.' ) {
459+ continue ;
460+ }
461+
462+ if let Some ( bundle_id) = bundle_id_from_app_path ( line) {
463+ // Skip if we've already seen this bundle ID (deduplication)
464+ if seen_bundle_ids. contains ( & bundle_id) {
465+ continue ;
466+ }
467+
468+ // Skip if bundle ID looks like a path or is invalid
469+ if bundle_id. contains ( '/' ) || bundle_id. is_empty ( ) {
470+ continue ;
471+ }
472+
473+ seen_bundle_ids. insert ( bundle_id. clone ( ) ) ;
474+ results. push ( AppSearchResult {
475+ name : display_name,
476+ bundle_id,
477+ } ) ;
478+ }
479+ }
480+ }
481+
482+ Ok ( results)
483+ }
484+ }
485+
486+ fn bundle_id_from_app_path ( path : & str ) -> Option < String > {
487+ let output = Command :: new ( "mdls" )
488+ . arg ( "-name" )
489+ . arg ( "kMDItemCFBundleIdentifier" )
490+ . arg ( path)
491+ . output ( )
492+ . ok ( ) ?;
493+
494+ if !output. status . success ( ) {
495+ return None ;
496+ }
497+
498+ let output = String :: from_utf8_lossy ( & output. stdout ) ;
499+
500+ // Parse the output from mdls which looks like:
501+ // kMDItemCFBundleIdentifier = "com.example.app"
502+ let bundle_id = output
503+ . lines ( )
504+ . find ( |line| line. contains ( "kMDItemCFBundleIdentifier" ) )
505+ . and_then ( |line| {
506+ line. split ( '=' )
507+ . nth ( 1 )
508+ . map ( |s| s. trim ( ) . trim_matches ( '"' ) . to_string ( ) )
509+ } ) ?;
510+
511+ // Filter out null or empty bundle IDs
512+ if bundle_id. is_empty ( ) || bundle_id == "(null)" || bundle_id == "null" {
513+ return None ;
514+ }
515+
516+ Some ( bundle_id)
420517}
421518
422519fn system_integration_display_name ( bundle_id : & str ) -> Option < String > {
0 commit comments