Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
target
.DS_Store
.vscode
.vscode
.idea
78 changes: 78 additions & 0 deletions cardinal/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions cardinal/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ tauri-plugin-prevent-default = "4"
tauri-plugin-macos-permissions = "2.3.0"
tauri-plugin-window-state = "2"
once_cell = { version = "1.20", features = ["parking_lot"] }
trash = "5.2"

cardinal-sdk.path = "../../cardinal-sdk"
search-cache.path = "../../search-cache"
Expand Down
22 changes: 15 additions & 7 deletions cardinal/src-tauri/src/background.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ pub struct StatusBarUpdate {
pub struct IconPayload {
pub slab_index: SlabIndex,
pub icon: String,
pub width: f64,
pub height: f64,
}

pub struct BackgroundLoopChannels {
Expand All @@ -41,7 +43,7 @@ pub struct BackgroundLoopChannels {
pub search_rx: Receiver<SearchJob>,
pub result_tx: Sender<Result<SearchOutcome>>,
pub node_info_rx: Receiver<NodeInfoRequest>,
pub icon_viewport_rx: Receiver<(u64, Vec<SlabIndex>)>,
pub icon_viewport_rx: Receiver<(u64, Vec<SlabIndex>, f64)>,
pub rescan_rx: Receiver<()>,
pub icon_update_tx: Sender<IconPayload>,
}
Expand Down Expand Up @@ -160,7 +162,7 @@ pub fn run_background_event_loop(
let _ = response_tx.send(node_info_results);
}
recv(icon_viewport_rx) -> update => {
let (_request_id, viewport) = update.expect("Icon viewport channel closed");
let (_request_id, viewport, icon_size) = update.expect("Icon viewport channel closed");

let nodes = cache.expand_file_nodes(&viewport);
let icon_jobs: Vec<_> = viewport
Expand All @@ -180,11 +182,17 @@ pub fn run_background_event_loop(
.for_each(|(slab_index, path)| {
let icon_update_tx = icon_update_tx.clone();
spawn(move || {
if let Some(icon) = fs_icon::icon_of_path_ql(&path).map(|data| format!(
"data:image/png;base64,{}",
general_purpose::STANDARD.encode(&data)
)) {
let _ = icon_update_tx.send(IconPayload { slab_index, icon });
if let Some((data, width, height)) = fs_icon::icon_of_path(&path, icon_size) {
let icon = format!(
"data:image/png;base64,{}",
general_purpose::STANDARD.encode(&data)
);
let _ = icon_update_tx.send(IconPayload {
slab_index,
icon,
width,
height,
});
}
});
});
Expand Down
64 changes: 52 additions & 12 deletions cardinal/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use parking_lot::Mutex;
use search_cache::{SearchOptions, SearchOutcome, SearchResultNode, SlabIndex, SlabNodeMetadata};
use search_cancel::CancellationToken;
use serde::{Deserialize, Serialize};
use std::process::Command;
use std::{fs, path::PathBuf, process::Command};
use tauri::{AppHandle, Manager, State};
use tracing::{error, info, warn};

Expand Down Expand Up @@ -57,7 +57,7 @@ pub struct SearchState {

node_info_tx: Sender<NodeInfoRequest>,

icon_viewport_tx: Sender<(u64, Vec<SlabIndex>)>,
icon_viewport_tx: Sender<(u64, Vec<SlabIndex>, f64)>,
rescan_tx: Sender<()>,
sorted_view_cache: Mutex<Option<SortedViewCache>>,
update_window_state_tx: Sender<()>,
Expand All @@ -68,7 +68,7 @@ impl SearchState {
search_tx: Sender<SearchJob>,
result_rx: Receiver<Result<SearchOutcome>>,
node_info_tx: Sender<NodeInfoRequest>,
icon_viewport_tx: Sender<(u64, Vec<SlabIndex>)>,
icon_viewport_tx: Sender<(u64, Vec<SlabIndex>, f64)>,
rescan_tx: Sender<()>,
update_window_state_tx: Sender<()>,
) -> Self {
Expand Down Expand Up @@ -131,6 +131,8 @@ pub struct NodeInfo {
pub path: String,
pub metadata: Option<NodeInfoMetadata>,
pub icon: Option<String>,
pub icon_width: Option<f64>,
pub icon_height: Option<f64>,
}

#[derive(Serialize, Default)]
Expand Down Expand Up @@ -248,20 +250,28 @@ pub fn get_nodes_info(
.into_iter()
.map(|SearchResultNode { path, metadata }| {
let path = path.to_string_lossy().into_owned();
let icon = if include_icons {
fs_icon::icon_of_path_ns(&path).map(|data| {
format!(
"data:image/png;base64,{}",
general_purpose::STANDARD.encode(data)
let (icon, icon_width, icon_height) = if include_icons {
if let Some((data, width, height)) = fs_icon::icon_of_path_ns(&path, 512.0) {
(
Some(format!(
"data:image/png;base64,{}",
general_purpose::STANDARD.encode(data)
)),
Some(width),
Some(height),
)
})
} else {
(None, None, None)
}
} else {
None
(None, None, None)
};
NodeInfo {
path,
icon,
metadata: metadata.as_ref().map(NodeInfoMetadata::from_metadata),
icon_width,
icon_height,
}
})
.collect()
Expand Down Expand Up @@ -291,8 +301,13 @@ pub fn get_sorted_view(
}

#[tauri::command(async)]
pub fn update_icon_viewport(id: u64, viewport: Vec<SlabIndex>, state: State<'_, SearchState>) {
if let Err(e) = state.icon_viewport_tx.send((id, viewport)) {
pub fn update_icon_viewport(
id: u64,
viewport: Vec<SlabIndex>,
icon_size: f64,
state: State<'_, SearchState>,
) {
if let Err(e) = state.icon_viewport_tx.send((id, viewport, icon_size)) {
error!("Failed to send icon viewport update: {e:?}");
}
}
Expand Down Expand Up @@ -370,3 +385,28 @@ pub async fn toggle_main_window(app: AppHandle) {
warn!("Toggle requested but main window is unavailable");
}
}
#[tauri::command]
pub async fn trash_paths(paths: Vec<String>) -> Result<(), String> {
let path_bufs: Vec<PathBuf> = paths.into_iter().map(PathBuf::from).collect();
trash::delete_all(path_bufs).map_err(|e| {
error!("Failed to trash paths: {e}");
e.to_string()
})
}

#[tauri::command]
pub async fn delete_paths(paths: Vec<String>) -> Result<(), String> {
for path in paths {
let p = PathBuf::from(path);
if p.is_dir() {
if let Err(e) = fs::remove_dir_all(&p) {
error!("Failed to delete directory {p:?}: {e}");
return Err(e.to_string());
}
} else if let Err(e) = fs::remove_file(&p) {
error!("Failed to delete file {p:?}: {e}");
return Err(e.to_string());
}
Comment on lines +397 to +409
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling for file operations. The delete_paths function directly calls fs::remove_dir_all and fs::remove_file without validation. Consider adding checks to prevent deletion of system directories, ensure paths are within allowed boundaries, or confirm files exist before attempting deletion to provide better error messages.

Suggested change
#[tauri::command]
pub async fn delete_paths(paths: Vec<String>) -> Result<(), String> {
for path in paths {
let p = PathBuf::from(path);
if p.is_dir() {
if let Err(e) = fs::remove_dir_all(&p) {
error!("Failed to delete directory {p:?}: {e}");
return Err(e.to_string());
}
} else if let Err(e) = fs::remove_file(&p) {
error!("Failed to delete file {p:?}: {e}");
return Err(e.to_string());
}
/// Returns true if the given path is a protected filesystem location that should never be deleted.
/// This treats any filesystem root (e.g., `/` on Unix or `C:\` on Windows) as protected.
fn is_protected_path(path: &std::path::Path) -> bool {
// Only consider paths with a root component, and that have no parent (i.e., the root itself).
path.has_root() && path.parent().is_none()
}
#[tauri::command]
pub async fn delete_paths(paths: Vec<String>) -> Result<(), String> {
for path in paths {
let p = PathBuf::from(&path);
if !p.exists() {
warn!("Requested to delete non-existent path: {p:?}");
continue;
}
let canonical = match p.canonicalize() {
Ok(c) => c,
Err(e) => {
error!("Failed to canonicalize path {p:?}: {e}");
return Err(e.to_string());
}
};
if is_protected_path(&canonical) {
let msg = format!("Refusing to delete protected path: {canonical:?}");
error!("{msg}");
return Err(msg);
}
if canonical.is_dir() {
if let Err(e) = fs::remove_dir_all(&canonical) {
error!("Failed to delete directory {canonical:?}: {e}");
return Err(e.to_string());
}
} else if canonical.is_file() {
if let Err(e) = fs::remove_file(&canonical) {
error!("Failed to delete file {canonical:?}: {e}");
return Err(e.to_string());
}
} else {
// Path exists but is neither a regular file nor directory (e.g., symlink, special file).
// Let the caller know we didn't delete it.
let msg = format!("Refusing to delete unsupported path type: {canonical:?}");
warn!("{msg}");
return Err(msg);
}

Copilot uses AI. Check for mistakes.
}
Ok(())
}
12 changes: 7 additions & 5 deletions cardinal/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ use background::{
};
use cardinal_sdk::EventWatcher;
use commands::{
NodeInfoRequest, SearchJob, SearchState, activate_main_window, close_quicklook, get_app_status,
get_nodes_info, get_sorted_view, hide_main_window, open_in_finder, open_path, search,
start_logic, toggle_main_window, toggle_quicklook, trigger_rescan, update_icon_viewport,
update_quicklook,
NodeInfoRequest, SearchJob, SearchState, activate_main_window, close_quicklook, delete_paths,
get_app_status, get_nodes_info, get_sorted_view, hide_main_window, open_in_finder, open_path,
search, start_logic, toggle_main_window, toggle_quicklook, trash_paths, trigger_rescan,
update_icon_viewport, update_quicklook,
};
use crossbeam_channel::{Receiver, RecvTimeoutError, Sender, bounded, unbounded};
use lifecycle::{
Expand Down Expand Up @@ -52,7 +52,7 @@ pub fn run() -> Result<()> {
let (search_tx, search_rx) = unbounded::<SearchJob>();
let (result_tx, result_rx) = unbounded::<Result<SearchOutcome>>();
let (node_info_tx, node_info_rx) = unbounded::<NodeInfoRequest>();
let (icon_viewport_tx, icon_viewport_rx) = unbounded::<(u64, Vec<SlabIndex>)>();
let (icon_viewport_tx, icon_viewport_rx) = unbounded::<(u64, Vec<SlabIndex>, f64)>();
let (rescan_tx, rescan_rx) = unbounded::<()>();
let (icon_update_tx, icon_update_rx) = unbounded::<IconPayload>();
let (update_window_state_tx, update_window_state_rx) = bounded::<()>(1);
Expand Down Expand Up @@ -128,6 +128,8 @@ pub fn run() -> Result<()> {
hide_main_window,
activate_main_window,
toggle_main_window,
trash_paths,
delete_paths,
])
.build(tauri::generate_context!())
.expect("error while running tauri application");
Expand Down
Loading