Skip to content

Commit d1ccfff

Browse files
committed
Enhance File Explorer UI: Add directory input box, improve button styles, and implement platform-specific file opening functionality
1 parent 6b65ac9 commit d1ccfff

1 file changed

Lines changed: 87 additions & 43 deletions

File tree

File-Explorer/src/main.rs

Lines changed: 87 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,30 @@
11
use druid::widget::{Button, Flex, Label, List, Scroll, TextBox};
2-
use druid::widget::prelude::*;
32
use druid::{
43
AppDelegate, AppLauncher, Command, Data, DelegateCtx, Env, Lens, Selector, Target,
5-
Widget, WidgetExt, WindowDesc,
4+
Widget, WidgetExt, WindowDesc, commands, FileDialogOptions, theme,
65
};
76
use regex::Regex;
8-
use std::path::PathBuf;
7+
use std::path::{Path, PathBuf}; // added Path
98
use std::sync::Arc;
109
use std::thread;
1110
use walkdir::WalkDir;
11+
use std::fs;
12+
13+
#[cfg(target_os = "macos")]
14+
fn open_path(path: &str) {
15+
std::process::Command::new("open")
16+
.arg(path)
17+
.spawn()
18+
.expect("failed to open file");
19+
}
20+
21+
#[cfg(target_os = "windows")]
22+
fn open_path(path: &str) {
23+
std::process::Command::new("explorer")
24+
.arg(path)
25+
.spawn()
26+
.expect("failed to open file");
27+
}
1228

1329
// A selector for updating search results from a background thread.
1430
// Note: Now the payload is an Arc<Vec<String>>
@@ -25,78 +41,97 @@ struct AppState {
2541

2642
fn build_ui() -> impl Widget<AppState> {
2743
// 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-
});
44+
let choose_dir_btn = Button::new("Choose Directory")
45+
.padding(8.0)
46+
.background(theme::BUTTON_DARK)
47+
.on_click(|ctx, _data, _env| {
48+
ctx.submit_command(Command::new(commands::SHOW_OPEN_PANEL, FileDialogOptions::default(), Target::Auto));
49+
});
3650

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);
51+
// Replace the static label with an editable text box for directory input.
52+
let directory_box = TextBox::new()
53+
.with_placeholder("Enter directory path")
54+
.with_text_size(14.0)
55+
.padding(8.0)
56+
.lens(AppState::root_path);
4257

4358
// Text box for entering the search term.
4459
let search_box = TextBox::new()
4560
.with_placeholder("Enter search term")
61+
.with_text_size(14.0)
62+
.padding(8.0)
4663
.lens(AppState::search_term);
4764

4865
// 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());
66+
let search_btn = Button::new("Search")
67+
.padding(8.0)
68+
.background(theme::BUTTON_DARK)
69+
.on_click(|ctx, data: &mut AppState, _env| {
70+
let root = data.root_path.clone();
71+
let term = data.search_term.clone();
72+
73+
// Clear any previous search results.
74+
data.search_results = Arc::new(Vec::new());
5575

56-
let sink = ctx.get_external_handle();
76+
let sink = ctx.get_external_handle();
5777

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");
78+
thread::spawn(move || {
79+
let results = search_files(&root, &term);
80+
// Send the search results back to the UI thread.
81+
sink.submit_command(UPDATE_SEARCH_RESULTS, results, Target::Auto)
82+
.expect("Failed to submit command");
83+
});
6384
});
64-
});
6585

6686
// Create a list widget to display search results.
6787
let results_list = List::new(|| {
6888
Label::new(|item: &String, _env: &_| format!("{}", item))
69-
.padding(5.0)
89+
.with_text_size(14.0)
90+
.padding(6.0)
91+
.on_click(|_ctx, item: &mut String, _env| {
92+
open_path(item);
93+
})
7094
})
71-
.with_spacing(2.0)
95+
.with_spacing(4.0)
7296
// Lens into the search_results field (which is now an Arc<Vec<String>>)
7397
.lens(AppState::search_results);
7498

7599
// Layout the UI elements vertically.
76100
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)
101+
.with_child(choose_dir_btn.padding(8.0))
102+
.with_child(directory_box.padding(8.0)) // new text box for directory input
103+
.with_child(search_box.padding(8.0))
104+
.with_child(search_btn.padding(8.0))
105+
.with_flex_child(Scroll::new(results_list).expand(), 1.0)
106+
.padding(12.0)
107+
.background(theme::WINDOW_BACKGROUND_COLOR)
82108
}
83109

84-
/// Searches files under the given directory whose names match the search term (case-insensitive)
110+
/// Searches files and directories under the given directory whose names match the search term (case-insensitive)
85111
/// and returns an Arc<Vec<String>>.
86112
fn search_files(root_path: &str, search_term: &str) -> Arc<Vec<String>> {
87113
let regex = Regex::new(&format!(r"(?i){}", search_term)).unwrap();
88114
let root = PathBuf::from(root_path);
115+
let results = search_files_recursive(&root, &regex);
116+
Arc::new(results)
117+
}
118+
119+
fn search_files_recursive(dir: &Path, regex: &Regex) -> Vec<String> {
89120
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());
121+
if dir.is_dir() {
122+
for entry in fs::read_dir(dir).expect("read_dir call failed") {
123+
if let Ok(entry) = entry {
124+
if entry.path().is_file() || entry.path().is_dir() {
125+
if let Some(name) = entry.path().file_name().and_then(|n| n.to_str()) {
126+
if regex.is_match(name) {
127+
results.push(entry.path().display().to_string());
128+
}
129+
}
95130
}
96131
}
97132
}
98133
}
99-
Arc::new(results)
134+
results
100135
}
101136

102137
/// A delegate to handle commands coming from the background thread.
@@ -115,6 +150,15 @@ impl AppDelegate<AppState> for Delegate {
115150
data.search_results = results.clone();
116151
return druid::Handled::Yes;
117152
}
153+
if cmd.is(commands::SHOW_OPEN_PANEL) {
154+
let dialog = rfd::FileDialog::new();
155+
if let Some(folder) = dialog.pick_folder() {
156+
data.root_path = folder.to_string_lossy().to_string();
157+
data.search_results = Arc::new(Vec::new());
158+
return druid::Handled::Yes;
159+
}
160+
// Removed file selection to force folder-only selection.
161+
}
118162
druid::Handled::No
119163
}
120164
}

0 commit comments

Comments
 (0)