Skip to content

Commit 4a7eff1

Browse files
authored
Merge pull request #206 from tanishq-dubey/master
Feature: Change File Search to use `mfind`
2 parents 9de6dbf + a64ee73 commit 4a7eff1

8 files changed

Lines changed: 222 additions & 107 deletions

File tree

Cargo.lock

Lines changed: 0 additions & 40 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ emojis = "0.8.0"
1515
global-hotkey = "0.7.0"
1616
iced = { version = "0.14.0", features = ["image", "tokio"] }
1717
icns = "0.3.1"
18-
ignore = "0.4.25"
1918
image = { version = "0.25.9", features = ["tiff"] }
2019
libc = "0.2.180"
2120
log = "0.4.29"

src/app.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ pub const WINDOW_WIDTH: f32 = 500.;
2121
/// The default window height
2222
pub const DEFAULT_WINDOW_HEIGHT: f32 = 100.;
2323

24+
/// Maximum file search results returned by a single mdfind invocation.
25+
pub const FILE_SEARCH_MAX_RESULTS: u32 = 400;
26+
27+
/// Number of results to accumulate before flushing a batch to the UI.
28+
pub const FILE_SEARCH_BATCH_SIZE: u32 = 10;
29+
2430
/// The rustcast descriptor name to be put for all rustcast commands
2531
pub const RUSTCAST_DESC_NAME: &str = "Utility";
2632

@@ -85,6 +91,9 @@ pub enum Message {
8591
SwitchToPage(Page),
8692
ClipboardHistory(ClipBoardContentType),
8793
ChangeFocus(ArrowKey, u32),
94+
FileSearchResult(Vec<App>),
95+
FileSearchClear,
96+
SetFileSearchSender(tokio::sync::watch::Sender<(String, Vec<String>)>),
8897
DebouncedSearch(Id),
8998
}
9099

src/app/tile.rs

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use log::{info, warn};
2727
use objc2::rc::Retained;
2828
use objc2_app_kit::NSRunningApplication;
2929
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
30+
use tokio::io::AsyncBufReadExt;
3031
use tray_icon::TrayIcon;
3132

3233
use std::collections::HashMap;
@@ -130,6 +131,7 @@ pub struct Tile {
130131
sender: Option<ExtSender>,
131132
page: Page,
132133
pub height: f32,
134+
pub file_search_sender: Option<tokio::sync::watch::Sender<(String, Vec<String>)>>,
133135
debouncer: Debouncer,
134136
}
135137

@@ -182,6 +184,7 @@ impl Tile {
182184
Subscription::run(check_version),
183185
Subscription::run(handle_hot_reloading),
184186
Subscription::run(handle_clipboard_history),
187+
Subscription::run(handle_file_search),
185188
window::close_events().map(Message::HideWindow),
186189
keyboard::listen().filter_map(|event| {
187190
if let keyboard::Event::KeyPressed { key, modifiers, .. } = event {
@@ -377,6 +380,145 @@ fn handle_clipboard_history() -> impl futures::Stream<Item = Message> {
377380
})
378381
}
379382

383+
/// Read mdfind stdout line-by-line, sending batched results to the UI.
384+
///
385+
/// Returns when stdout reaches EOF, the receiver signals a new query, or
386+
/// max results are reached. Caller is responsible for process lifetime.
387+
async fn read_mdfind_results(
388+
stdout: tokio::process::ChildStdout,
389+
home_dir: &str,
390+
receiver: &mut tokio::sync::watch::Receiver<(String, Vec<String>)>,
391+
output: &mut iced::futures::channel::mpsc::Sender<Message>,
392+
) {
393+
use crate::app::{FILE_SEARCH_BATCH_SIZE, FILE_SEARCH_MAX_RESULTS};
394+
395+
let mut reader = tokio::io::BufReader::new(stdout);
396+
let mut batch: Vec<crate::app::apps::App> = Vec::with_capacity(FILE_SEARCH_BATCH_SIZE as usize);
397+
let mut total_sent: u32 = 0;
398+
399+
loop {
400+
let mut line = String::new();
401+
let read_result = tokio::select! {
402+
result = reader.read_line(&mut line) => result,
403+
_ = receiver.changed() => {
404+
// New query arrived — caller will handle it.
405+
break;
406+
}
407+
};
408+
409+
match read_result {
410+
Ok(0) => {
411+
// EOF — flush remaining batch.
412+
if !batch.is_empty() {
413+
output
414+
.send(Message::FileSearchResult(std::mem::take(&mut batch)))
415+
.await
416+
.ok();
417+
}
418+
break;
419+
}
420+
Ok(_) => {
421+
if let Some(app) = crate::commands::path_to_app(line.trim(), home_dir) {
422+
batch.push(app);
423+
total_sent += 1;
424+
}
425+
if batch.len() as u32 >= FILE_SEARCH_BATCH_SIZE {
426+
output
427+
.send(Message::FileSearchResult(std::mem::take(&mut batch)))
428+
.await
429+
.ok();
430+
}
431+
if total_sent >= FILE_SEARCH_MAX_RESULTS {
432+
if !batch.is_empty() {
433+
output
434+
.send(Message::FileSearchResult(std::mem::take(&mut batch)))
435+
.await
436+
.ok();
437+
}
438+
break;
439+
}
440+
}
441+
Err(_) => break,
442+
}
443+
}
444+
}
445+
446+
/// Async subscription that spawns `mdfind` for file search queries.
447+
///
448+
/// Uses a `watch` channel so the Tile can push new (query, dirs) pairs.
449+
/// Each query change cancels any running `mdfind` and starts a fresh one.
450+
fn handle_file_search() -> impl futures::Stream<Item = Message> {
451+
stream::channel(100, async |mut output| {
452+
let (sender, mut receiver) =
453+
tokio::sync::watch::channel((String::new(), Vec::<String>::new()));
454+
output
455+
.send(Message::SetFileSearchSender(sender))
456+
.await
457+
.expect("Failed to send file search sender.");
458+
459+
let home_dir = std::env::var("HOME").unwrap_or_else(|_| "/".to_string());
460+
assert!(!home_dir.is_empty(), "HOME must not be empty.");
461+
462+
let mut child: Option<tokio::process::Child> = None;
463+
464+
loop {
465+
if receiver.changed().await.is_err() {
466+
return;
467+
}
468+
receiver.borrow_and_update();
469+
470+
// Kill previous mdfind if still running.
471+
if let Some(ref mut proc) = child {
472+
proc.kill().await.ok();
473+
proc.wait().await.ok();
474+
}
475+
child = None;
476+
477+
let (query, dirs) = receiver.borrow().clone();
478+
assert!(query.len() < 1024, "Query too long.");
479+
480+
if query.len() < 2 {
481+
output.send(Message::FileSearchClear).await.ok();
482+
continue;
483+
}
484+
485+
// The query is passed as a -name argument to mdfind. mdfind interprets
486+
// this as a substring match on filenames — not as a glob or shell expression.
487+
// Passed via args (not shell), so no shell injection risk.
488+
// When dirs is empty, omit -onlyin so mdfind searches system-wide.
489+
let mut args: Vec<String> = vec!["-name".to_string(), query.clone()];
490+
for dir in &dirs {
491+
let expanded = dir.replace("~", &home_dir);
492+
args.push("-onlyin".to_string());
493+
args.push(expanded);
494+
}
495+
496+
let spawn_result = tokio::process::Command::new("mdfind")
497+
.args(&args)
498+
.stdout(std::process::Stdio::piped())
499+
.stderr(std::process::Stdio::null())
500+
.kill_on_drop(true)
501+
.spawn();
502+
503+
let mut proc = match spawn_result {
504+
Ok(p) => p,
505+
Err(err) => {
506+
warn!("Failed to spawn mdfind: {err}");
507+
continue;
508+
}
509+
};
510+
511+
let stdout = match proc.stdout.take() {
512+
Some(s) => s,
513+
None => continue,
514+
};
515+
child = Some(proc);
516+
517+
read_mdfind_results(stdout, &home_dir, &mut receiver, &mut output).await;
518+
}
519+
})
520+
}
521+
380522
/// Handles the rx / receiver for sending and receiving messages
381523
fn handle_recipient() -> impl futures::Stream<Item = Message> {
382524
stream::channel(100, async |mut output| {

src/app/tile/elm.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ pub fn new(hotkey: HotKey, config: &Config) -> (Tile, Task<Message>) {
8383
sender: None,
8484
page: Page::Main,
8585
height: DEFAULT_WINDOW_HEIGHT,
86+
file_search_sender: None,
8687
debouncer: Debouncer::new(config.debounce_delay),
8788
},
8889
Task::batch([open.map(|_| Message::OpenWindow)]),

src/app/tile/update.rs

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ use crate::app::tile::AppIndex;
2727
use crate::app::{Message, Page, tile::Tile};
2828
use crate::calculator::Expr;
2929
use crate::commands::Function;
30-
use crate::commands::search_for_file;
3130
use crate::config::Config;
3231
use crate::debounce::DebouncePolicy;
3332
use crate::unit_conversion;
@@ -432,6 +431,37 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {
432431
Task::none()
433432
}
434433

434+
Message::SetFileSearchSender(sender) => {
435+
tile.file_search_sender = Some(sender);
436+
Task::none()
437+
}
438+
439+
Message::FileSearchResult(apps) => {
440+
assert!(apps.len() <= 50, "Batch must not exceed 50 results.");
441+
if tile.page == Page::FileSearch {
442+
let prev_display_count = std::cmp::min(5, tile.results.len());
443+
tile.results.extend(apps);
444+
let new_display_count = std::cmp::min(5, tile.results.len());
445+
// Only resize when the visible row count changes (max 5).
446+
if new_display_count != prev_display_count && new_display_count > 0 {
447+
return window::latest().map(move |x| {
448+
Message::ResizeWindow(
449+
x.unwrap(),
450+
((new_display_count * 55) + 35 + DEFAULT_WINDOW_HEIGHT as usize) as f32,
451+
)
452+
});
453+
}
454+
}
455+
Task::none()
456+
}
457+
458+
Message::FileSearchClear => {
459+
if tile.page == Page::FileSearch {
460+
tile.results.clear();
461+
}
462+
Task::none()
463+
}
464+
435465
Message::SearchQueryChanged(input, id) => {
436466
tile.focus_id = 0;
437467

@@ -589,15 +619,20 @@ fn execute_query(tile: &mut Tile, id: Id) -> Task<Message> {
589619
}
590620
}
591621

592-
if tile.page != Page::FileSearch {
593-
tile.handle_search_query_changed();
594-
} else {
595-
tile.results = search_for_file(
596-
&tile.query_lc,
597-
tile.config.search_dirs.iter().map(|x| x.as_str()).collect(),
598-
);
622+
if tile.page == Page::FileSearch {
623+
// File search is async — dispatch to the mdfind subscription and
624+
// return immediately. Results arrive via FileSearchResult messages.
625+
if let Some(ref sender) = tile.file_search_sender {
626+
tile.results.clear();
627+
sender
628+
.send((tile.query_lc.clone(), tile.config.search_dirs.clone()))
629+
.ok();
630+
}
631+
return task;
599632
}
600633

634+
tile.handle_search_query_changed();
635+
601636
if !tile.results.is_empty() {
602637
tile.results.par_sort_by_key(|x| -x.ranking);
603638

0 commit comments

Comments
 (0)