From 62adb290e8b527bca2baf9410e8c12fe30ff51e9 Mon Sep 17 00:00:00 2001 From: Tanishq Dubey Date: Wed, 25 Mar 2026 12:49:52 -0400 Subject: [PATCH 1/3] Switch to NSMetadataQuery for search, add file icons --- Cargo.toml | 13 +- src/app.rs | 9 +- src/app/tile.rs | 408 +++++++++++++++++++++++++++----------- src/app/tile/update.rs | 11 + src/platform/macos/mod.rs | 1 + src/platform/mod.rs | 11 + 6 files changed, 339 insertions(+), 114 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 65fbdbd..7b0963f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,18 @@ objc2-application-services = { version = "0.3.2", default-features = false, feat "Processes", ] } objc2-core-foundation = "0.3.2" -objc2-foundation = { version = "0.3.2", features = ["NSString"] } +objc2-foundation = { version = "0.3.2", features = [ + "NSString", + "NSArray", + "NSDate", + "NSMetadata", + "NSMetadataAttributes", + "NSNotification", + "NSObjCRuntime", + "NSOperation", + "NSPredicate", + "NSRunLoop", +] } once_cell = "1.21.3" rand = "0.9.2" rayon = "1.11.0" diff --git a/src/app.rs b/src/app.rs index c074dc0..8c38754 100644 --- a/src/app.rs +++ b/src/app.rs @@ -21,12 +21,18 @@ pub const WINDOW_WIDTH: f32 = 500.; /// The default window height pub const DEFAULT_WINDOW_HEIGHT: f32 = 100.; -/// Maximum file search results returned by a single mdfind invocation. +/// Maximum file search results returned by a single query. pub const FILE_SEARCH_MAX_RESULTS: u32 = 400; /// Number of results to accumulate before flushing a batch to the UI. pub const FILE_SEARCH_BATCH_SIZE: u32 = 10; +/// Maximum number of file search results that get icons loaded. +pub const FILE_SEARCH_MAX_ICONS: usize = 20; + +/// Number of icons to accumulate before flushing a batch to the UI. +pub const FILE_SEARCH_ICON_BATCH_SIZE: usize = 5; + /// The rustcast descriptor name to be put for all rustcast commands pub const RUSTCAST_DESC_NAME: &str = "Utility"; @@ -111,6 +117,7 @@ pub enum Message { ChangeFocus(ArrowKey, u32), FileSearchResult(Vec), FileSearchClear, + FileSearchIcons(Vec<(usize, iced::widget::image::Handle)>), SetFileSearchSender(tokio::sync::watch::Sender<(String, Vec)>), DebouncedSearch(Id), } diff --git a/src/app/tile.rs b/src/app/tile.rs index 337d8ab..e0db2ff 100644 --- a/src/app/tile.rs +++ b/src/app/tile.rs @@ -3,13 +3,17 @@ pub mod elm; pub mod update; use crate::app::apps::App; -use crate::app::{ArrowKey, Message, Move, Page}; +use crate::app::{ + ArrowKey, Message, Move, Page, FILE_SEARCH_BATCH_SIZE, FILE_SEARCH_ICON_BATCH_SIZE, + FILE_SEARCH_MAX_ICONS, FILE_SEARCH_MAX_RESULTS, +}; use crate::clipboard::ClipBoardContentType; use crate::config::Config; use crate::debounce::Debouncer; -use crate::platform::default_app_paths; +use crate::platform::{default_app_paths, icon_of_path_ns}; use arboard::Clipboard; +use block2::RcBlock; use global_hotkey::hotkey::HotKey; use global_hotkey::{GlobalHotKeyEvent, HotKeyState}; @@ -26,14 +30,20 @@ use iced::{event, window}; use log::{info, warn}; use objc2::rc::Retained; use objc2_app_kit::NSRunningApplication; +use objc2_foundation::{ + NSArray, NSDate, NSDefaultRunLoopMode, NSMetadataItemPathKey, NSMetadataQuery, + NSMetadataQueryDidFinishGatheringNotification, NSNotificationCenter, NSPredicate, NSRunLoop, + NSString, +}; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use rayon::slice::ParallelSliceMut; -use tokio::io::AsyncBufReadExt; use tray_icon::TrayIcon; use std::collections::HashMap; use std::fmt::Debug; use std::str::FromStr; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use std::time::Duration; /// This is a wrapper around the sender to disable dropping @@ -373,65 +383,209 @@ fn handle_clipboard_history() -> impl futures::Stream { }) } -/// Read mdfind stdout line-by-line, sending batched results to the UI. +/// This represents the messages sent from the NSMetadataQuery thread back to the iced subscription +enum QueryThreadMsg { + Batch(Vec), + Clear, + Icons(Vec<(usize, iced::widget::image::Handle)>), +} + +/// This is the subscription function that bridges the NSMetadataQuery thread to the UI /// -/// Returns when stdout reaches EOF, the receiver signals a new query, or -/// max results are reached. Caller is responsible for process lifetime. -async fn read_mdfind_results( - stdout: tokio::process::ChildStdout, +/// Creates a watch channel for query input and a tokio mpsc channel for results. +/// Spawns a dedicated thread for NSMetadataQuery (which needs an NSRunLoop). +fn handle_file_search() -> impl futures::Stream { + stream::channel(100, async |mut output| { + let (watch_tx, watch_rx) = + tokio::sync::watch::channel((String::new(), Vec::::new())); + output + .send(Message::SetFileSearchSender(watch_tx)) + .await + .expect("Failed to send file search sender."); + + let (msg_tx, mut msg_rx) = tokio::sync::mpsc::channel::(64); + + std::thread::Builder::new() + .name("nsmetadata-query".into()) + .spawn(move || metadata_query_thread(watch_rx, msg_tx)) + .expect("Failed to spawn metadata query thread."); + + while let Some(msg) = msg_rx.recv().await { + match msg { + QueryThreadMsg::Batch(apps) => { + output.send(Message::FileSearchResult(apps)).await.ok(); + } + QueryThreadMsg::Clear => { + output.send(Message::FileSearchClear).await.ok(); + } + QueryThreadMsg::Icons(icons) => { + output.send(Message::FileSearchIcons(icons)).await.ok(); + } + } + } + }) +} + +/// This configures the NSMetadataQuery predicate and search scopes +/// +/// Uses NSPredicate LIKE[cd] for case+diacritic insensitive glob matching. +/// Whitespace-separated tokens are joined with `*` wildcards so "img jpg" +/// matches filenames containing "img" followed by "jpg". +/// +/// NSPredicate LIKE uses `*` (multi-char) and `?` (single-char) as wildcards. +/// The underscore `_` is NOT special in LIKE (unlike SQL), so filenames like +/// `IMG_1234.jpg` match correctly. +fn configure_metadata_query( + query: &objc2_foundation::NSMetadataQuery, + search_text: &str, + dirs: &[String], home_dir: &str, - receiver: &mut tokio::sync::watch::Receiver<(String, Vec)>, - output: &mut iced::futures::channel::mpsc::Sender, ) { - use crate::app::{FILE_SEARCH_BATCH_SIZE, FILE_SEARCH_MAX_RESULTS}; + assert!(!search_text.is_empty(), "Search text must not be empty."); + assert!(!home_dir.is_empty(), "Home dir must not be empty."); + + // Escape LIKE wildcards in each token, then join with `*`. + let tokens: Vec = search_text + .split_whitespace() + .map(|t| t.replace('*', "\\*").replace('?', "\\?")) + .collect(); + assert!(!tokens.is_empty(), "Tokens must not be empty."); + let pattern = format!("*{}*", tokens.join("*")); + + // Use predicateWithFormat with %@ so the pattern is properly quoted. + let format_str = NSString::from_str("kMDItemDisplayName LIKE[cd] %@"); + let pattern_ns = NSString::from_str(&pattern); + let pattern_obj: objc2::rc::Retained = + objc2::rc::Retained::into_super(objc2::rc::Retained::into_super(pattern_ns)); + let args = NSArray::from_retained_slice(&[pattern_obj]); + // SAFETY: predicateWithFormat_argumentArray is an NSPredicate class method that + // parses the format string and substitutes %@ with the argument array values. + // The format string is a compile-time constant and args contains a single valid + // NSString, so the call is well-formed. + let predicate = unsafe { + NSPredicate::predicateWithFormat_argumentArray(&format_str, Some(&args)) + }; + query.setPredicate(Some(&predicate)); + + let scope_strings: Vec> = dirs + .iter() + .map(|d| NSString::from_str(&d.replace("~", home_dir))) + .collect(); + let scope_objects: Vec> = scope_strings + .into_iter() + .map(|s| objc2::rc::Retained::into_super(objc2::rc::Retained::into_super(s))) + .collect(); + let scopes = NSArray::from_retained_slice(&scope_objects); + // SAFETY: setSearchScopes expects an NSArray of scope objects (NSString paths + // or NSURL). We pass an array of NSString path values which is a valid scope type. + unsafe { query.setSearchScopes(&scopes) }; +} - let mut reader = tokio::io::BufReader::new(stdout); - let mut batch: Vec = Vec::with_capacity(FILE_SEARCH_BATCH_SIZE as usize); - let mut total_sent: u32 = 0; +/// This extracts paths from an NSMetadataQuery and sends them as batched App results +/// +/// Called on the query thread after the gather-complete notification fires. +/// Disables updates during iteration to prevent mutation. +/// Returns the absolute paths of accepted results for subsequent icon loading. +fn drain_metadata_results( + query: &objc2_foundation::NSMetadataQuery, + home_dir: &str, + msg_tx: &tokio::sync::mpsc::Sender, +) -> Vec { + assert!(!home_dir.is_empty(), "Home dir must not be empty."); + assert!(!msg_tx.is_closed(), "Message channel must be open."); + + query.disableUpdates(); + let count = query.resultCount(); + let limit = count.min(FILE_SEARCH_MAX_RESULTS as usize); + let attr_key = unsafe { NSMetadataItemPathKey }; + + let mut batch: Vec = + Vec::with_capacity(FILE_SEARCH_BATCH_SIZE as usize); + let mut paths: Vec = Vec::with_capacity(limit); + let mut idx: usize = 0; + + while idx < limit { + let path_str = query + .resultAtIndex(idx) + .downcast::() + .ok() + .and_then(|item| item.valueForAttribute(attr_key)) + .and_then(|val| val.downcast::().ok()) + .map(|ns| ns.to_string()); + + if let Some(path_str) = path_str { + if let Some(app) = crate::commands::path_to_app(&path_str, home_dir) { + batch.push(app); + paths.push(path_str); + } + } + idx += 1; - loop { - let mut line = String::new(); - let read_result = tokio::select! { - result = reader.read_line(&mut line) => result, - _ = receiver.changed() => { - // New query arrived — caller will handle it. - break; + if batch.len() as u32 >= FILE_SEARCH_BATCH_SIZE { + if let Err(e) = msg_tx.try_send(QueryThreadMsg::Batch(std::mem::take(&mut batch))) { + warn!("Failed to send file search batch: {e}"); } - }; + } + } - match read_result { - Ok(0) => { - // EOF — flush remaining batch. - if !batch.is_empty() { - output - .send(Message::FileSearchResult(std::mem::take(&mut batch))) - .await - .ok(); - } - break; + if !batch.is_empty() { + if let Err(e) = msg_tx.try_send(QueryThreadMsg::Batch(batch)) { + warn!("Failed to send final file search batch: {e}"); + } + } + query.enableUpdates(); + paths +} + +/// This loads file icons for search results and sends them to the UI +/// +/// Loads icons via NSWorkspace::iconForFile on the query thread (which has +/// Cocoa runtime). Only loads the first `max_icons` results. Checks for +/// new queries between batches to allow cancellation. +fn load_file_search_icons( + paths: &[String], + msg_tx: &tokio::sync::mpsc::Sender, + watch_rx: &tokio::sync::watch::Receiver<(String, Vec)>, +) { + let limit = paths.len().min(FILE_SEARCH_MAX_ICONS); + + assert!(FILE_SEARCH_ICON_BATCH_SIZE > 0, "Batch size must be positive."); + + let mut icon_batch: Vec<(usize, iced::widget::image::Handle)> = + Vec::with_capacity(FILE_SEARCH_ICON_BATCH_SIZE); + + let mut idx: usize = 0; + while idx < limit { + // Cancel if a new query has arrived. + if watch_rx.has_changed().unwrap_or(false) { + return; + } + + if let Some(png_data) = icon_of_path_ns(&paths[idx]) { + let handle = image::ImageReader::new(std::io::Cursor::new(png_data)) + .with_guessed_format() + .ok() + .and_then(|r| r.decode().ok()) + .map(|img| { + let rgba = img.to_rgba8(); + iced::widget::image::Handle::from_rgba( + rgba.width(), + rgba.height(), + rgba.into_raw(), + ) + }); + if let Some(h) = handle { + icon_batch.push((idx, h)); } - Ok(_) => { - if let Some(app) = crate::commands::path_to_app(line.trim(), home_dir) { - batch.push(app); - total_sent += 1; - } - if batch.len() as u32 >= FILE_SEARCH_BATCH_SIZE { - output - .send(Message::FileSearchResult(std::mem::take(&mut batch))) - .await - .ok(); - } - if total_sent >= FILE_SEARCH_MAX_RESULTS { - if !batch.is_empty() { - output - .send(Message::FileSearchResult(std::mem::take(&mut batch))) - .await - .ok(); - } - break; + } + idx += 1; + + if icon_batch.len() >= FILE_SEARCH_ICON_BATCH_SIZE || idx >= limit { + if !icon_batch.is_empty() { + if let Err(e) = msg_tx.try_send(QueryThreadMsg::Icons(std::mem::take(&mut icon_batch))) { + warn!("Failed to send icon batch: {e}"); } } - Err(_) => break, } } } @@ -475,80 +629,110 @@ fn count_dirs_in_dir(dir: impl AsRef) -> usize { .count() } -/// Async subscription that spawns `mdfind` for file search queries. +/// This creates an NSMetadataQuery and registers a gather-complete notification observer /// -/// Uses a `watch` channel so the Tile can push new (query, dirs) pairs. -/// Each query change cancels any running `mdfind` and starts a fresh one. -fn handle_file_search() -> impl futures::Stream { - stream::channel(100, async |mut output| { - let (sender, mut receiver) = - tokio::sync::watch::channel((String::new(), Vec::::new())); - output - .send(Message::SetFileSearchSender(sender)) - .await - .expect("Failed to send file search sender."); +/// Returns the query, the results-ready flag, and the observer handle. +/// The observer sets the AtomicBool flag when NSMetadataQueryDidFinishGathering fires. +fn metadata_query_thread_setup() -> ( + objc2::rc::Retained, + std::sync::Arc, + objc2::rc::Retained>, +) { + let query = NSMetadataQuery::new(); + query.setNotificationBatchingInterval(0.0); + + let results_ready = Arc::new(AtomicBool::new(false)); + let flag = results_ready.clone(); + let block = RcBlock::new(move |_: core::ptr::NonNull| { + flag.store(true, Ordering::Release); + }); + + let center = NSNotificationCenter::defaultCenter(); + // SAFETY: addObserverForName registers a notification block with the default center. + // The block and notification name are valid for the lifetime of the returned observer. + let observer = unsafe { + center.addObserverForName_object_queue_usingBlock( + Some(NSMetadataQueryDidFinishGatheringNotification), + None, + None, + &block, + ) + }; + + assert!(!results_ready.load(Ordering::Acquire), "Flag must start false."); + assert!(query.resultCount() == 0, "Query must start empty."); - let home_dir = std::env::var("HOME").unwrap_or_else(|_| "/".to_string()); - assert!(!home_dir.is_empty(), "HOME must not be empty."); + (query, results_ready, observer) +} - let mut child: Option = None; +/// This is the dedicated thread that runs NSMetadataQuery with an NSRunLoop +/// +/// Polls the watch channel for new queries every 50ms run-loop tick. +/// When the gather-complete notification fires (via AtomicBool flag), +/// drains results and sends them back through the mpsc channel. +fn metadata_query_thread( + mut watch_rx: tokio::sync::watch::Receiver<(String, Vec)>, + msg_tx: tokio::sync::mpsc::Sender, +) { + let home_dir = std::env::var("HOME").unwrap_or_else(|_| "/".to_string()); + assert!(!home_dir.is_empty(), "HOME must not be empty."); + assert!(!msg_tx.is_closed(), "Message channel must be open."); - loop { - if receiver.changed().await.is_err() { - return; - } - receiver.borrow_and_update(); + let (query, results_ready, observer) = metadata_query_thread_setup(); - // Kill previous mdfind if still running. - if let Some(ref mut proc) = child { - proc.kill().await.ok(); - proc.wait().await.ok(); + let run_loop = NSRunLoop::currentRunLoop(); + let run_loop_mode = unsafe { NSDefaultRunLoopMode }; + let tick_seconds: f64 = 0.05; + + loop { + // Tick the run loop to process notifications. + let timeout = NSDate::dateWithTimeIntervalSinceNow(tick_seconds); + run_loop.runMode_beforeDate(run_loop_mode, &timeout); + + // Drain results only when the finish-gathering notification has fired. + if results_ready.swap(false, Ordering::AcqRel) { + if query.resultCount() > 0 { + let paths = drain_metadata_results(&query, &home_dir, &msg_tx); + load_file_search_icons(&paths, &msg_tx, &watch_rx); } - child = None; + } - let (query, dirs) = receiver.borrow().clone(); - assert!(query.len() < 1024, "Query too long."); + // Check for new query from the UI. + if watch_rx.has_changed().unwrap_or(false) { + let (ref q, ref dirs) = *watch_rx.borrow_and_update(); - if query.len() < 2 { - output.send(Message::FileSearchClear).await.ok(); - continue; - } + // Clear the flag before stopping so a final gather-complete + // notification from the old query does not leak through. + results_ready.store(false, Ordering::Release); + query.stopQuery(); - // The query is passed as a -name argument to mdfind. mdfind interprets - // this as a substring match on filenames — not as a glob or shell expression. - // Passed via args (not shell), so no shell injection risk. - // When dirs is empty, omit -onlyin so mdfind searches system-wide. - let mut args: Vec = vec!["-name".to_string(), query.clone()]; - for dir in &dirs { - let expanded = dir.replace("~", &home_dir); - args.push("-onlyin".to_string()); - args.push(expanded); + // Tell the UI to discard previous results before new ones arrive. + if let Err(e) = msg_tx.try_send(QueryThreadMsg::Clear) { + warn!("Failed to send file search clear: {e}"); } - let spawn_result = tokio::process::Command::new("mdfind") - .args(&args) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::null()) - .kill_on_drop(true) - .spawn(); - - let mut proc = match spawn_result { - Ok(p) => p, - Err(err) => { - warn!("Failed to spawn mdfind: {err}"); - continue; - } - }; + if q.len() < 2 { + continue; + } + assert!(q.len() < 1024, "Query too long."); - let stdout = match proc.stdout.take() { - Some(s) => s, - None => continue, - }; - child = Some(proc); + configure_metadata_query(&query, q, dirs, &home_dir); + let started = query.startQuery(); + if !started { + warn!("NSMetadataQuery failed to start."); + } + } - read_mdfind_results(stdout, &home_dir, &mut receiver, &mut output).await; + if msg_tx.is_closed() { + break; } - }) + } + + query.stopQuery(); + let center = NSNotificationCenter::defaultCenter(); + let observer_ref: &objc2::runtime::AnyObject = + objc2::runtime::ProtocolObject::as_ref(&*observer); + unsafe { center.removeObserver(observer_ref) }; } /// Handles the rx / receiver for sending and receiving messages diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs index c3dcfb4..ff598ea 100644 --- a/src/app/tile/update.rs +++ b/src/app/tile/update.rs @@ -522,6 +522,17 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { Task::none() } + Message::FileSearchIcons(icons) => { + if tile.page == Page::FileSearch { + for (idx, handle) in icons { + if let Some(app) = tile.results.get_mut(idx) { + app.icons = Some(handle); + } + } + } + Task::none() + } + Message::SearchQueryChanged(input, id) => { tile.focus_id = 0; diff --git a/src/platform/macos/mod.rs b/src/platform/macos/mod.rs index e8a8856..5547a55 100644 --- a/src/platform/macos/mod.rs +++ b/src/platform/macos/mod.rs @@ -5,6 +5,7 @@ pub mod haptics; use iced::wgpu::rwh::WindowHandle; pub(super) use self::discovery::get_installed_apps; +pub(super) use self::discovery::icon_of_path_ns; pub(super) use self::haptics::perform_haptic; /// This sets the activation policy of the app to Accessory, allowing rustcast to be visible ontop diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 38de61e..0cbbb9a 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -56,3 +56,14 @@ pub fn get_installed_apps(store_icons: bool) -> Vec { pub fn get_installed_apps(store_icons: bool) -> Vec { self::cross::get_installed_apps(store_icons) } + +/// This loads the system icon for a file path, returning PNG bytes +#[cfg(target_os = "macos")] +pub fn icon_of_path_ns(path: &str) -> Option> { + self::macos::icon_of_path_ns(path) +} + +#[cfg(not(target_os = "macos"))] +pub fn icon_of_path_ns(_path: &str) -> Option> { + None +} From 318944f0e485ba6e58081fe63d0f611e09344f24 Mon Sep 17 00:00:00 2001 From: Tanishq Dubey Date: Wed, 25 Mar 2026 13:11:44 -0400 Subject: [PATCH 2/3] fmt --- src/app/tile.rs | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/app/tile.rs b/src/app/tile.rs index e0db2ff..3b04828 100644 --- a/src/app/tile.rs +++ b/src/app/tile.rs @@ -4,8 +4,8 @@ pub mod update; use crate::app::apps::App; use crate::app::{ - ArrowKey, Message, Move, Page, FILE_SEARCH_BATCH_SIZE, FILE_SEARCH_ICON_BATCH_SIZE, - FILE_SEARCH_MAX_ICONS, FILE_SEARCH_MAX_RESULTS, + ArrowKey, FILE_SEARCH_BATCH_SIZE, FILE_SEARCH_ICON_BATCH_SIZE, FILE_SEARCH_MAX_ICONS, + FILE_SEARCH_MAX_RESULTS, Message, Move, Page, }; use crate::clipboard::ClipBoardContentType; use crate::config::Config; @@ -42,8 +42,8 @@ use tray_icon::TrayIcon; use std::collections::HashMap; use std::fmt::Debug; use std::str::FromStr; -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; /// This is a wrapper around the sender to disable dropping @@ -462,9 +462,8 @@ fn configure_metadata_query( // parses the format string and substitutes %@ with the argument array values. // The format string is a compile-time constant and args contains a single valid // NSString, so the call is well-formed. - let predicate = unsafe { - NSPredicate::predicateWithFormat_argumentArray(&format_str, Some(&args)) - }; + let predicate = + unsafe { NSPredicate::predicateWithFormat_argumentArray(&format_str, Some(&args)) }; query.setPredicate(Some(&predicate)); let scope_strings: Vec> = dirs @@ -499,8 +498,7 @@ fn drain_metadata_results( let limit = count.min(FILE_SEARCH_MAX_RESULTS as usize); let attr_key = unsafe { NSMetadataItemPathKey }; - let mut batch: Vec = - Vec::with_capacity(FILE_SEARCH_BATCH_SIZE as usize); + let mut batch: Vec = Vec::with_capacity(FILE_SEARCH_BATCH_SIZE as usize); let mut paths: Vec = Vec::with_capacity(limit); let mut idx: usize = 0; @@ -549,7 +547,10 @@ fn load_file_search_icons( ) { let limit = paths.len().min(FILE_SEARCH_MAX_ICONS); - assert!(FILE_SEARCH_ICON_BATCH_SIZE > 0, "Batch size must be positive."); + assert!( + FILE_SEARCH_ICON_BATCH_SIZE > 0, + "Batch size must be positive." + ); let mut icon_batch: Vec<(usize, iced::widget::image::Handle)> = Vec::with_capacity(FILE_SEARCH_ICON_BATCH_SIZE); @@ -582,7 +583,9 @@ fn load_file_search_icons( if icon_batch.len() >= FILE_SEARCH_ICON_BATCH_SIZE || idx >= limit { if !icon_batch.is_empty() { - if let Err(e) = msg_tx.try_send(QueryThreadMsg::Icons(std::mem::take(&mut icon_batch))) { + if let Err(e) = + msg_tx.try_send(QueryThreadMsg::Icons(std::mem::take(&mut icon_batch))) + { warn!("Failed to send icon batch: {e}"); } } @@ -643,9 +646,11 @@ fn metadata_query_thread_setup() -> ( let results_ready = Arc::new(AtomicBool::new(false)); let flag = results_ready.clone(); - let block = RcBlock::new(move |_: core::ptr::NonNull| { - flag.store(true, Ordering::Release); - }); + let block = RcBlock::new( + move |_: core::ptr::NonNull| { + flag.store(true, Ordering::Release); + }, + ); let center = NSNotificationCenter::defaultCenter(); // SAFETY: addObserverForName registers a notification block with the default center. @@ -659,7 +664,10 @@ fn metadata_query_thread_setup() -> ( ) }; - assert!(!results_ready.load(Ordering::Acquire), "Flag must start false."); + assert!( + !results_ready.load(Ordering::Acquire), + "Flag must start false." + ); assert!(query.resultCount() == 0, "Query must start empty."); (query, results_ready, observer) From 898df72368cf70852cff53f6861eb4cbdbc6d54a Mon Sep 17 00:00:00 2001 From: Tanishq Dubey Date: Wed, 25 Mar 2026 13:21:09 -0400 Subject: [PATCH 3/3] Use public module --- src/app/tile.rs | 3 ++- src/platform/macos/mod.rs | 1 - src/platform/mod.rs | 11 ----------- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/app/tile.rs b/src/app/tile.rs index 3b04828..2aaae03 100644 --- a/src/app/tile.rs +++ b/src/app/tile.rs @@ -10,7 +10,8 @@ use crate::app::{ use crate::clipboard::ClipBoardContentType; use crate::config::Config; use crate::debounce::Debouncer; -use crate::platform::{default_app_paths, icon_of_path_ns}; +use crate::platform::default_app_paths; +use crate::platform::macos::discovery::icon_of_path_ns; use arboard::Clipboard; use block2::RcBlock; diff --git a/src/platform/macos/mod.rs b/src/platform/macos/mod.rs index 5547a55..e8a8856 100644 --- a/src/platform/macos/mod.rs +++ b/src/platform/macos/mod.rs @@ -5,7 +5,6 @@ pub mod haptics; use iced::wgpu::rwh::WindowHandle; pub(super) use self::discovery::get_installed_apps; -pub(super) use self::discovery::icon_of_path_ns; pub(super) use self::haptics::perform_haptic; /// This sets the activation policy of the app to Accessory, allowing rustcast to be visible ontop diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 0cbbb9a..38de61e 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -56,14 +56,3 @@ pub fn get_installed_apps(store_icons: bool) -> Vec { pub fn get_installed_apps(store_icons: bool) -> Vec { self::cross::get_installed_apps(store_icons) } - -/// This loads the system icon for a file path, returning PNG bytes -#[cfg(target_os = "macos")] -pub fn icon_of_path_ns(path: &str) -> Option> { - self::macos::icon_of_path_ns(path) -} - -#[cfg(not(target_os = "macos"))] -pub fn icon_of_path_ns(_path: &str) -> Option> { - None -}