|
1 | | -use druid::{widget, AppLauncher, Data, Lens, LocalizedString, Widget, WidgetExt, WindowDesc}; |
| 1 | +use druid::widget::{Button, Flex, Label, List, Scroll, TextBox}; |
| 2 | +use druid::widget::prelude::*; |
| 3 | +use druid::{ |
| 4 | + AppDelegate, AppLauncher, Command, Data, DelegateCtx, Env, Lens, Selector, Target, |
| 5 | + Widget, WidgetExt, WindowDesc, |
| 6 | +}; |
2 | 7 | use regex::Regex; |
3 | | - |
4 | | -use std::path::{Path, PathBuf}; |
5 | | -use std::sync::{mpsc, Arc, Mutex}; |
| 8 | +use std::path::PathBuf; |
| 9 | +use std::sync::Arc; |
6 | 10 | use std::thread; |
7 | 11 | use walkdir::WalkDir; |
8 | 12 |
|
9 | | -#[derive(Clone, Data, Lens)] |
10 | | -struct AppState { |
11 | | - #[lens(ignore)] |
12 | | - root_path: Arc<Mutex<PathBuf>>, |
13 | | - search_term: String, |
14 | | - result: String, |
15 | | -} |
| 13 | +// A selector for updating search results from a background thread. |
| 14 | +// Note: Now the payload is an Arc<Vec<String>> |
| 15 | +const UPDATE_SEARCH_RESULTS: Selector<Arc<Vec<String>>> = |
| 16 | + Selector::new("update_search_results"); |
16 | 17 |
|
17 | 18 | #[derive(Clone, Data, Lens)] |
18 | | -struct SearchUpdate { |
19 | | - result: String, |
| 19 | +struct AppState { |
| 20 | + pub root_path: String, |
| 21 | + pub search_term: String, |
| 22 | + // Change from im::Vector<String> to Arc<Vec<String>> for compatibility with ListIter |
| 23 | + pub search_results: Arc<Vec<String>>, |
20 | 24 | } |
21 | 25 |
|
22 | 26 | fn build_ui() -> impl Widget<AppState> { |
23 | | - let label = widget::Label::new(|data: &AppState, _env: &_| data.result.clone()).with_text_size(20.0); |
24 | | - |
25 | | - let scrollable_label = druid::widget::Scroll::new(label); |
26 | | - |
27 | | - widget::Flex::column() |
28 | | - .with_child(widget::TextBox::new().lens(AppState::search_term)) |
29 | | - .with_child( |
30 | | - widget::Button::new("Search").on_click(|_, data: &mut AppState, _| { |
31 | | - let root_path = data.root_path.lock().unwrap().clone(); |
32 | | - let search_term = data.search_term.clone(); |
33 | | - |
34 | | - let (tx, rx) = mpsc::channel(); |
35 | | - let tx_copy = tx.clone(); |
36 | | - |
37 | | - thread::spawn(move || { |
38 | | - let result = search_files(&root_path, &search_term, Some(tx)); |
39 | | - let _ = tx_copy.send(SearchUpdate { result }); |
40 | | - }); |
41 | | - |
42 | | - let mut result = String::new(); |
43 | | - for update in rx.iter() { |
44 | | - result.push_str(&update.result); |
45 | | - data.result = result.clone(); |
46 | | - } |
47 | | - }), |
48 | | - ) |
49 | | - .with_child(scrollable_label) |
| 27 | + // Button to let the user choose a directory (using rfd for a native dialog) |
| 28 | + let choose_dir_btn = Button::new("Choose Directory").on_click(|_ctx, data: &mut AppState, _env| { |
| 29 | + // Use rfd's file dialog (this will show a native folder chooser on macOS) |
| 30 | + if let Some(path) = rfd::FileDialog::new().pick_folder() { |
| 31 | + data.root_path = path.to_string_lossy().to_string(); |
| 32 | + // Clear any previous search results when the directory changes. |
| 33 | + data.search_results = Arc::new(Vec::new()); |
| 34 | + } |
| 35 | + }); |
| 36 | + |
| 37 | + // A label showing the currently selected directory. |
| 38 | + let dir_label = Label::new(|data: &AppState, _env: &_| { |
| 39 | + format!("Current Directory: {}", data.root_path) |
| 40 | + }) |
| 41 | + .with_text_size(14.0); |
| 42 | + |
| 43 | + // Text box for entering the search term. |
| 44 | + let search_box = TextBox::new() |
| 45 | + .with_placeholder("Enter search term") |
| 46 | + .lens(AppState::search_term); |
| 47 | + |
| 48 | + // Button to kick off the search. |
| 49 | + let search_btn = Button::new("Search").on_click(|ctx, data: &mut AppState, _env| { |
| 50 | + let root = data.root_path.clone(); |
| 51 | + let term = data.search_term.clone(); |
| 52 | + |
| 53 | + // Clear any previous search results. |
| 54 | + data.search_results = Arc::new(Vec::new()); |
| 55 | + |
| 56 | + let sink = ctx.get_external_handle(); |
| 57 | + |
| 58 | + thread::spawn(move || { |
| 59 | + let results = search_files(&root, &term); |
| 60 | + // Send the search results back to the UI thread. |
| 61 | + sink.submit_command(UPDATE_SEARCH_RESULTS, results, Target::Auto) |
| 62 | + .expect("Failed to submit command"); |
| 63 | + }); |
| 64 | + }); |
| 65 | + |
| 66 | + // Create a list widget to display search results. |
| 67 | + let results_list = List::new(|| { |
| 68 | + Label::new(|item: &String, _env: &_| format!("{}", item)) |
| 69 | + .padding(5.0) |
| 70 | + }) |
| 71 | + .with_spacing(2.0) |
| 72 | + // Lens into the search_results field (which is now an Arc<Vec<String>>) |
| 73 | + .lens(AppState::search_results); |
| 74 | + |
| 75 | + // Layout the UI elements vertically. |
| 76 | + Flex::column() |
| 77 | + .with_child(choose_dir_btn.padding(5.0)) |
| 78 | + .with_child(dir_label.padding(5.0)) |
| 79 | + .with_child(search_box.padding(5.0)) |
| 80 | + .with_child(search_btn.padding(5.0)) |
| 81 | + .with_flex_child(Scroll::new(results_list), 1.0) |
50 | 82 | } |
51 | 83 |
|
52 | | -// This function takes in a root path and a search term and returns a string of the search results |
53 | | -fn search_files(root_path: &Path, search_term: &str, tx: Option<mpsc::Sender<SearchUpdate>>) -> String { |
54 | | - |
55 | | - let mut result = String::new(); |
56 | | - let search_term_regex = Regex::new(&format!(r"(?i){}", search_term)).expect("Invalid regex"); |
57 | | - |
58 | | - WalkDir::new(root_path) |
59 | | - .into_iter() |
60 | | - .filter_map(|entry| entry.ok()) |
61 | | - .filter(|entry| entry.path().is_file()) |
62 | | - .filter(|entry| { |
63 | | - entry |
64 | | - .file_name() |
65 | | - .to_str() |
66 | | - .map(|name| search_term_regex.is_match(name)) |
67 | | - .unwrap_or(false) |
68 | | - }) |
69 | | - .for_each(|entry| { |
70 | | - let found_path = format!("{}\n", entry.path().display()); |
71 | | - result.push_str(&found_path); |
72 | | - if let Some(tx) = &tx { |
73 | | - let _ = tx.send(SearchUpdate { |
74 | | - result: found_path.clone(), |
75 | | - }); |
| 84 | +/// Searches files under the given directory whose names match the search term (case-insensitive) |
| 85 | +/// and returns an Arc<Vec<String>>. |
| 86 | +fn search_files(root_path: &str, search_term: &str) -> Arc<Vec<String>> { |
| 87 | + let regex = Regex::new(&format!(r"(?i){}", search_term)).unwrap(); |
| 88 | + let root = PathBuf::from(root_path); |
| 89 | + let mut results = Vec::new(); |
| 90 | + for entry in WalkDir::new(root).into_iter().filter_map(|e| e.ok()) { |
| 91 | + if entry.path().is_file() { |
| 92 | + if let Some(name) = entry.path().file_name().and_then(|n| n.to_str()) { |
| 93 | + if regex.is_match(name) { |
| 94 | + results.push(entry.path().display().to_string()); |
| 95 | + } |
76 | 96 | } |
77 | | - }); |
78 | | - |
79 | | - result |
| 97 | + } |
| 98 | + } |
| 99 | + Arc::new(results) |
80 | 100 | } |
81 | 101 |
|
| 102 | +/// A delegate to handle commands coming from the background thread. |
| 103 | +struct Delegate; |
| 104 | + |
| 105 | +impl AppDelegate<AppState> for Delegate { |
| 106 | + fn command( |
| 107 | + &mut self, |
| 108 | + _ctx: &mut DelegateCtx, |
| 109 | + _target: Target, |
| 110 | + cmd: &Command, |
| 111 | + data: &mut AppState, |
| 112 | + _env: &Env, |
| 113 | + ) -> druid::Handled { |
| 114 | + if let Some(results) = cmd.get(UPDATE_SEARCH_RESULTS) { |
| 115 | + data.search_results = results.clone(); |
| 116 | + return druid::Handled::Yes; |
| 117 | + } |
| 118 | + druid::Handled::No |
| 119 | + } |
| 120 | +} |
82 | 121 |
|
83 | 122 | fn main() { |
84 | | - let main_window = WindowDesc::new(build_ui()) |
85 | | - .title(LocalizedString::new("File Explorer")); |
86 | | - |
87 | | - let root_path = Arc::new(Mutex::new(std::env::current_dir().unwrap())); |
88 | | - |
89 | | - let app_state = AppState { |
90 | | - root_path: root_path.clone(), |
91 | | - search_term: String::new(), |
92 | | - result: String::new(), |
| 123 | + // Create the main window. |
| 124 | + let main_window = WindowDesc::new(build_ui()).title("macOS File Explorer"); |
| 125 | + |
| 126 | + // Initialize the state with the current directory. |
| 127 | + let initial_state = AppState { |
| 128 | + root_path: std::env::current_dir() |
| 129 | + .unwrap_or_else(|_| PathBuf::from(".")) |
| 130 | + .display() |
| 131 | + .to_string(), |
| 132 | + search_term: "".to_string(), |
| 133 | + search_results: Arc::new(Vec::new()), |
93 | 134 | }; |
94 | 135 |
|
| 136 | + // Launch the application with the delegate to handle background commands. |
95 | 137 | AppLauncher::with_window(main_window) |
96 | | - .log_to_console() |
97 | | - .launch(app_state) |
| 138 | + .delegate(Delegate) |
| 139 | + .launch(initial_state) |
98 | 140 | .expect("Failed to launch application"); |
99 | 141 | } |
0 commit comments