Conversation
WalkthroughAdds a file-search feature: new TODO.md, backend file-indexing and Tauri commands, two Cargo dependencies, frontend CSS for the file-search UI, a rewired OpenFilePage that loads/searches files via Tauri, and small navigation label changes (Open File → Open App). Changes
Sequence Diagram(s)sequenceDiagram
participant FE as Frontend (OpenFilePage)
participant BE as Backend (Tauri)
participant FS as Filesystem
rect #f3f4f6
Note over FE,BE: Component mount / user triggers refresh
FE->>BE: refresh_file_index()
BE->>FS: Traverse directories (WalkDir)
BE->>BE: Build FileSearchDatabase (apps & files)
BE-->>FE: Index complete
end
rect #eef2ff
Note over FE,BE: Fetch initial lists
FE->>BE: get_applications(), get_recent_files()
BE-->>FE: Vec<FileItem>
FE->>FE: Render list
end
rect #fff7ed
Note over FE,BE: User search
FE->>BE: search_files(query)
BE->>BE: Query index
BE-->>FE: Filtered Vec<FileItem>
FE->>FE: Update results
end
rect #f0fdf4
Note over FE,BE: Open selection
FE->>BE: open_file(path)
BE->>FS: Launch file/app
FE->>FE: Hide window
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested labels
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/App.css (1)
248-263: Respect prefers-reduced-motion for animations.Add a reduced‑motion fallback to avoid unnecessary motion for sensitive users.
@keyframes slideIn { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: translateY(0); } } .clipboard-item { animation: slideIn 0.2s ease; } + +@media (prefers-reduced-motion: reduce) { + .clipboard-item, + .file-item { + animation: none !important; + transition: none !important; + } +}src-tauri/src/lib.rs (1)
548-569: Consider registering the opener plugin (optional).You depend on tauri-plugin-opener but don’t register it. If you choose to open paths from the frontend, add the plugin.
tauri::Builder::default() - .plugin(tauri_plugin_clipboard_manager::init()) + .plugin(tauri_plugin_clipboard_manager::init()) + .plugin(tauri_plugin_opener::init())
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
⛔ Files ignored due to path filters (1)
src-tauri/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (5)
TODO.md(1 hunks)src-tauri/Cargo.toml(1 hunks)src-tauri/src/lib.rs(8 hunks)src/App.css(1 hunks)src/components/OpenFilePage.jsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src-tauri/src/lib.rs (2)
src/App.jsx (1)
query(10-10)src/components/OpenFilePage.jsx (1)
files(8-8)
src/components/OpenFilePage.jsx (3)
src/components/ClipboardPage.jsx (4)
loading(10-10)error(11-11)handleSelect(40-63)useKeyboardNavigation(122-122)src/App.jsx (1)
query(10-10)src/hooks/useKeyboardNavigation.js (1)
useKeyboardNavigation(3-61)
🪛 Biome (2.1.2)
src/components/OpenFilePage.jsx
[error] 160-161: Provide an explicit type prop for the button element.
The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset
(lint/a11y/useButtonType)
[error] 170-171: Provide an explicit type prop for the button element.
The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset
(lint/a11y/useButtonType)
[error] 182-183: Provide an explicit type prop for the button element.
The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset
(lint/a11y/useButtonType)
🪛 markdownlint-cli2 (0.18.1)
TODO.md
3-3: Trailing spaces
Expected: 0 or 2; Actual: 1
(MD009, no-trailing-spaces)
6-6: Files should end with a single newline character
(MD047, single-trailing-newline)
🔇 Additional comments (1)
src-tauri/Cargo.toml (1)
29-30: Deps look good for indexing support.walkdir and dirs are appropriate for traversal and home resolution. No issues.
| fn get_file_extension(path: &PathBuf) -> String { | ||
| path.extension() | ||
| .and_then(|ext| ext.to_str()) | ||
| .unwrap_or("") | ||
| .to_lowercase() | ||
| } | ||
|
|
||
| fn is_app_file(path: &PathBuf) -> bool { | ||
| let extension = get_file_extension(path); | ||
| match extension.as_str() { | ||
| "app" => true, // macOS | ||
| "exe" | "msi" => true, // Windows | ||
| "deb" | "rpm" | "AppImage" => true, // Linux | ||
| _ => false, | ||
| } | ||
| } |
There was a problem hiding this comment.
Linux app detection and AppImage casing bug.
- "AppImage" won’t match because get_file_extension lowercases to "appimage".
- Linux GUI apps reside as .desktop files in …/share/applications; your matcher excludes them, so Linux will return near‑empty app lists.
fn is_app_file(path: &PathBuf) -> bool {
let extension = get_file_extension(path);
match extension.as_str() {
- "app" => true, // macOS
- "exe" | "msi" => true, // Windows
- "deb" | "rpm" | "AppImage" => true, // Linux
+ "app" => true, // macOS bundles (dirs)
+ "exe" | "msi" => true, // Windows
+ "appimage" => true, // Linux portable apps
+ // NOTE: .desktop are launchers; index differently (parse Name/Exec).
_ => false,
}
}Follow‑up: For Linux, index .desktop entries and extract Name/Exec instead of treating packages (.deb/.rpm) as apps. I can provide a parser if you want.
🤖 Prompt for AI Agents
In src-tauri/src/lib.rs around lines 197–212, the Linux detection logic is
wrong: get_file_extension lowercases extensions so "AppImage" will never match
and .desktop application entries are ignored. Update is_app_file to compare
against lowercase names (add "appimage" and "desktop") and ensure extension
checks use the lowercased string; then stop treating package files (.deb/.rpm)
as GUI apps and instead add a Linux-specific index path that scans
.../share/applications for .desktop files and parses Name and Exec (or use a
.desktop parser) to populate app metadata. Ensure the .desktop path check
handles files ending with ".desktop" and extract human-readable name and
executable command rather than relying on package extensions.
| let app_dirs = if cfg!(target_os = "macos") { | ||
| vec![ | ||
| PathBuf::from("/Applications"), | ||
| PathBuf::from("/System/Applications"), | ||
| PathBuf::from("/System/Library/CoreServices"), | ||
| ] | ||
| } else if cfg!(target_os = "windows") { | ||
| vec![ | ||
| PathBuf::from("C:\\Program Files"), | ||
| PathBuf::from("C:\\Program Files (x86)"), | ||
| PathBuf::from("C:\\Users\\%USERNAME%\\AppData\\Local\\Programs"), | ||
| ] | ||
| } else { | ||
| vec![ | ||
| PathBuf::from("/usr/share/applications"), | ||
| PathBuf::from("/usr/local/share/applications"), | ||
| PathBuf::from("/var/lib/snapd/desktop/applications"), | ||
| ] | ||
| }; |
There was a problem hiding this comment.
Windows path uses literal %USERNAME% (won’t expand).
Use dirs::home_dir or env::var to build the user path; otherwise this directory is skipped.
} else if cfg!(target_os = "windows") {
- vec![
- PathBuf::from("C:\\Program Files"),
- PathBuf::from("C:\\Program Files (x86)"),
- PathBuf::from("C:\\Users\\%USERNAME%\\AppData\\Local\\Programs"),
- ]
+ {
+ let mut v = vec![
+ PathBuf::from("C:\\Program Files"),
+ PathBuf::from("C:\\Program Files (x86)"),
+ ];
+ if let Some(home) = dirs::home_dir() {
+ v.push(home.join("AppData\\Local\\Programs"));
+ }
+ v
+ }Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src-tauri/src/lib.rs around lines 218 to 236, the Windows app_dirs entry uses
a literal "%USERNAME%" which won’t be expanded; replace that entry with a
constructed PathBuf using either env::var("USERNAME") or, better,
dirs::home_dir() to build the user-local programs path (e.g. combine the home
dir with "AppData/Local/Programs"), and handle the case where the env var or
home_dir is missing by skipping or falling back so the vector only contains
valid PathBufs.
| for entry in WalkDir::new(&dir) | ||
| .max_depth(4) | ||
| .into_iter() | ||
| .filter_map(|e| e.ok()) | ||
| { | ||
| let path = entry.path(); | ||
| if path.is_file() && !is_app_file(&path.to_path_buf()) { | ||
| if let (Ok(metadata), Some(name)) = (path.metadata(), path.file_name().and_then(|n| n.to_str())) { | ||
| let modified = metadata | ||
| .modified() | ||
| .unwrap_or(SystemTime::UNIX_EPOCH) | ||
| .duration_since(UNIX_EPOCH) | ||
| .unwrap_or_default() | ||
| .as_secs(); | ||
|
|
||
| files.push(FileItem { | ||
| name: name.to_string(), | ||
| path: path.to_string_lossy().to_string(), | ||
| file_type: get_file_extension(&path.to_path_buf()), | ||
| size: metadata.len(), | ||
| modified, | ||
| is_app: false, | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
User file walk should skip heavy/hidden dirs to avoid stalls.
Add a filter_entry to ignore common heavy folders (node_modules, .git, target, etc.) and hidden items.
for dir in common_dirs {
if dir.exists() {
- for entry in WalkDir::new(&dir)
- .max_depth(4)
- .into_iter()
- .filter_map(|e| e.ok())
+ for entry in WalkDir::new(&dir)
+ .max_depth(4)
+ .follow_links(false)
+ .into_iter()
+ .filter_entry(|e| {
+ let name = e.file_name().to_string_lossy();
+ !name.starts_with('.')
+ && name != "node_modules"
+ && name != "target"
+ && name != "__pycache__"
+ && name != ".git"
+ })
+ .filter_map(|e| e.ok())
{📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for entry in WalkDir::new(&dir) | |
| .max_depth(4) | |
| .into_iter() | |
| .filter_map(|e| e.ok()) | |
| { | |
| let path = entry.path(); | |
| if path.is_file() && !is_app_file(&path.to_path_buf()) { | |
| if let (Ok(metadata), Some(name)) = (path.metadata(), path.file_name().and_then(|n| n.to_str())) { | |
| let modified = metadata | |
| .modified() | |
| .unwrap_or(SystemTime::UNIX_EPOCH) | |
| .duration_since(UNIX_EPOCH) | |
| .unwrap_or_default() | |
| .as_secs(); | |
| files.push(FileItem { | |
| name: name.to_string(), | |
| path: path.to_string_lossy().to_string(), | |
| file_type: get_file_extension(&path.to_path_buf()), | |
| size: metadata.len(), | |
| modified, | |
| is_app: false, | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| for entry in WalkDir::new(&dir) | |
| .max_depth(4) | |
| .follow_links(false) | |
| .into_iter() | |
| .filter_entry(|e| { | |
| let name = e.file_name().to_string_lossy(); | |
| !name.starts_with('.') | |
| && name != "node_modules" | |
| && name != "target" | |
| && name != "__pycache__" | |
| && name != ".git" | |
| }) | |
| .filter_map(|e| e.ok()) | |
| { | |
| let path = entry.path(); | |
| if path.is_file() && !is_app_file(&path.to_path_buf()) { | |
| if let (Ok(metadata), Some(name)) = (path.metadata(), path.file_name().and_then(|n| n.to_str())) { | |
| let modified = metadata | |
| .modified() | |
| .unwrap_or(SystemTime::UNIX_EPOCH) | |
| .duration_since(UNIX_EPOCH) | |
| .unwrap_or_default() | |
| .as_secs(); | |
| files.push(FileItem { | |
| name: name.to_string(), | |
| path: path.to_string_lossy().to_string(), | |
| file_type: get_file_extension(&path.to_path_buf()), | |
| size: metadata.len(), | |
| modified, | |
| is_app: false, | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
In src-tauri/src/lib.rs around lines 286 to 313, the WalkDir iteration should
skip heavy and hidden directories to avoid stalls; update the WalkDir builder to
use filter_entry to immediately skip entries whose file_name starts with '.' or
matches common heavy folders (e.g. "node_modules", ".git", "target", "dist",
"build") so the iterator never descends into them, then keep the existing
filter_map(|e| e.ok()) and file checks unchanged; implement the filter_entry
closure to convert file_name to &str safely (unwrap_or("")), return false for
those names and true otherwise, and ensure directories are the ones being
filtered (i.e., only prevent descending into matching directories).
| #[tauri::command] | ||
| fn open_file( | ||
| _app_handle: tauri::AppHandle, | ||
| path: String, | ||
| ) -> Result<(), String> { | ||
| use std::process::Command; | ||
|
|
||
| #[cfg(target_os = "macos")] | ||
| { | ||
| Command::new("open") | ||
| .arg(&path) | ||
| .spawn() | ||
| .map_err(|e| e.to_string())?; | ||
| } | ||
|
|
||
| #[cfg(target_os = "windows")] | ||
| { | ||
| Command::new("cmd") | ||
| .args(["/C", "start", "", &path]) | ||
| .spawn() | ||
| .map_err(|e| e.to_string())?; | ||
| } | ||
|
|
||
| #[cfg(target_os = "linux")] | ||
| { | ||
| Command::new("xdg-open") | ||
| .arg(&path) | ||
| .spawn() | ||
| .map_err(|e| e.to_string())?; | ||
| } | ||
|
|
||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
Use non-shell open to avoid quoting/injection pitfalls (Windows).
Avoid cmd /C start; use explorer directly (or the opener plugin). Current code can misbehave with spaces/special chars.
#[cfg(target_os = "windows")]
{
- Command::new("cmd")
- .args(["/C", "start", "", &path])
+ // explorer uses file associations and avoids shell parsing
+ Command::new("explorer")
+ .arg(&path)
.spawn()
.map_err(|e| e.to_string())?;
}Optional: add tauri-plugin-opener and call it from the frontend to drop this command entirely.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| #[tauri::command] | |
| fn open_file( | |
| _app_handle: tauri::AppHandle, | |
| path: String, | |
| ) -> Result<(), String> { | |
| use std::process::Command; | |
| #[cfg(target_os = "macos")] | |
| { | |
| Command::new("open") | |
| .arg(&path) | |
| .spawn() | |
| .map_err(|e| e.to_string())?; | |
| } | |
| #[cfg(target_os = "windows")] | |
| { | |
| Command::new("cmd") | |
| .args(["/C", "start", "", &path]) | |
| .spawn() | |
| .map_err(|e| e.to_string())?; | |
| } | |
| #[cfg(target_os = "linux")] | |
| { | |
| Command::new("xdg-open") | |
| .arg(&path) | |
| .spawn() | |
| .map_err(|e| e.to_string())?; | |
| } | |
| Ok(()) | |
| } | |
| #[tauri::command] | |
| fn open_file( | |
| _app_handle: tauri::AppHandle, | |
| path: String, | |
| ) -> Result<(), String> { | |
| use std::process::Command; | |
| #[cfg(target_os = "macos")] | |
| { | |
| Command::new("open") | |
| .arg(&path) | |
| .spawn() | |
| .map_err(|e| e.to_string())?; | |
| } | |
| #[cfg(target_os = "windows")] | |
| { | |
| // explorer uses file associations and avoids shell parsing | |
| Command::new("explorer") | |
| .arg(&path) | |
| .spawn() | |
| .map_err(|e| e.to_string())?; | |
| } | |
| #[cfg(target_os = "linux")] | |
| { | |
| Command::new("xdg-open") | |
| .arg(&path) | |
| .spawn() | |
| .map_err(|e| e.to_string())?; | |
| } | |
| Ok(()) | |
| } |
🤖 Prompt for AI Agents
In src-tauri/src/lib.rs around lines 430 to 462, the Windows branch uses `cmd /C
start "" <path>` which relies on the shell and can mis-handle spaces/special
characters—replace that branch to spawn explorer.exe directly
(Command::new("explorer").arg(path).spawn().map_err(|e| e.to_string())?) so the
path is passed as a single argument without shell parsing; alternatively, remove
the platform logic and integrate tauri-plugin-opener (or call the plugin from
the frontend) to handle opening files safely across platforms.
| let mut db = state.lock().map_err(|e| e.to_string())?; | ||
|
|
||
| // Clear existing data | ||
| db.files.clear(); | ||
| db.apps.clear(); | ||
|
|
||
| // Index applications | ||
| let apps = index_applications(); | ||
| for app in apps { | ||
| db.add_file(app); | ||
| } | ||
|
|
||
| // Index user files | ||
| let files = index_user_files(); | ||
| for file in files { | ||
| db.add_file(file); | ||
| } | ||
|
|
||
| // Update timestamp | ||
| db.last_indexed = SystemTime::now() | ||
| .duration_since(UNIX_EPOCH) | ||
| .unwrap() | ||
| .as_secs(); | ||
|
|
||
| // Save to file | ||
| let db_path = get_file_search_db_path(&app_handle); | ||
| save_file_db(&db, &db_path)?; | ||
|
|
||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Don’t hold the DB lock during long indexing (blocks UI).
Compute index outside the mutex, then swap/update once; reduces contention and keeps reads responsive.
#[tauri::command]
fn refresh_file_index(
state: tauri::State<Arc<Mutex<FileSearchDatabase>>>,
app_handle: tauri::AppHandle,
) -> Result<(), String> {
- let mut db = state.lock().map_err(|e| e.to_string())?;
- // Clear existing data
- db.files.clear();
- db.apps.clear();
-
- // Index applications
- let apps = index_applications();
- for app in apps {
- db.add_file(app);
- }
-
- // Index user files
- let files = index_user_files();
- for file in files {
- db.add_file(file);
- }
-
- // Update timestamp
- db.last_indexed = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .unwrap()
- .as_secs();
-
- // Save to file
- let db_path = get_file_search_db_path(&app_handle);
- save_file_db(&db, &db_path)?;
+ // Build index off-lock
+ let apps = index_applications();
+ let files = index_user_files();
+ let ts = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap()
+ .as_secs();
+
+ // Update under lock
+ let mut db = state.lock().map_err(|e| e.to_string())?;
+ db.files.clear();
+ db.apps.clear();
+ for app in apps { db.add_file(app); }
+ for file in files { db.add_file(file); }
+ db.last_indexed = ts;
+
+ // Persist
+ let db_path = get_file_search_db_path(&app_handle);
+ save_file_db(&db, &db_path)?;
Ok(())
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let mut db = state.lock().map_err(|e| e.to_string())?; | |
| // Clear existing data | |
| db.files.clear(); | |
| db.apps.clear(); | |
| // Index applications | |
| let apps = index_applications(); | |
| for app in apps { | |
| db.add_file(app); | |
| } | |
| // Index user files | |
| let files = index_user_files(); | |
| for file in files { | |
| db.add_file(file); | |
| } | |
| // Update timestamp | |
| db.last_indexed = SystemTime::now() | |
| .duration_since(UNIX_EPOCH) | |
| .unwrap() | |
| .as_secs(); | |
| // Save to file | |
| let db_path = get_file_search_db_path(&app_handle); | |
| save_file_db(&db, &db_path)?; | |
| Ok(()) | |
| } | |
| // Build index off-lock | |
| let apps = index_applications(); | |
| let files = index_user_files(); | |
| let ts = SystemTime::now() | |
| .duration_since(UNIX_EPOCH) | |
| .unwrap() | |
| .as_secs(); | |
| // Update under lock | |
| let mut db = state.lock().map_err(|e| e.to_string())?; | |
| db.files.clear(); | |
| db.apps.clear(); | |
| for app in apps { db.add_file(app); } | |
| for file in files { db.add_file(file); } | |
| db.last_indexed = ts; | |
| // Persist | |
| let db_path = get_file_search_db_path(&app_handle); | |
| save_file_db(&db, &db_path)?; | |
| Ok(()) | |
| } |
🤖 Prompt for AI Agents
In src-tauri/src/lib.rs around lines 469–498, the code holds the state mutex
while doing long-running indexing which blocks the UI; instead, perform
index_applications() and index_user_files() and build a new FileDB (or a
temporary struct) outside the lock, set its last_indexed timestamp, then acquire
the mutex only briefly to swap/replace the in-memory db (e.g., replace
db.files/apps and db.last_indexed or assign the new db), release the lock, and
finally save the new/clone of the db to disk; this minimizes lock duration to a
single quick swap and keeps expensive indexing and file IO off the mutex.
There was a problem hiding this comment.
Actionable comments posted: 2
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (2)
src/App.jsx(1 hunks)src/components/HomeOptions.jsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/App.jsx (1)
src/components/OpenFilePage.jsx (1)
OpenFilePage(7-227)
Summary by CodeRabbit