diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a98bafcb..f79d1df1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -104,7 +104,7 @@ jobs: if: startsWith(matrix.platform, 'ubuntu-22.04') run: | sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils libtracker-sparql-3.0-dev - name: Add Rust build target working-directory: src-tauri diff --git a/.github/workflows/rust_code_check.yml b/.github/workflows/rust_code_check.yml index 45eecd5e..e6040679 100644 --- a/.github/workflows/rust_code_check.yml +++ b/.github/workflows/rust_code_check.yml @@ -30,7 +30,7 @@ jobs: if: startsWith(matrix.platform, 'ubuntu-latest') run: | sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils libtracker-sparql-3.0-dev - name: Add pizza engine as a dependency working-directory: src-tauri diff --git a/docs/content.en/docs/release-notes/_index.md b/docs/content.en/docs/release-notes/_index.md index a7a13134..6da90d27 100644 --- a/docs/content.en/docs/release-notes/_index.md +++ b/docs/content.en/docs/release-notes/_index.md @@ -22,6 +22,8 @@ Information about release notes of Coco App is provided here. - feat: impl extension settings 'hide_before_open' #862 - feat: index both en/zh_CN app names and show app name in chosen language #875 - feat: support context menu in debug mode #882 +- feat: file search for Linux/GNOME #884 + ### 🐛 Bug fix diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 76811241..da5782d4 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -858,6 +858,7 @@ dependencies = [ "function_name", "futures", "futures-util", + "gio 0.20.12", "hostname", "http 1.3.1", "hyper 0.14.32", @@ -910,6 +911,7 @@ dependencies = [ "tokio-stream", "tokio-tungstenite", "tokio-util", + "tracker-rs", "tungstenite 0.24.0", "url", "walkdir", @@ -1730,7 +1732,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adef9ff8c03b1c04bc2c743e0395588001b1c8f5669cab0b300abe873b9c9227" dependencies = [ - "gio 0.20.9", + "gio 0.20.12", "gtk", "objc2 0.5.2", "objc2-app-kit 0.2.2", @@ -2194,9 +2196,9 @@ dependencies = [ [[package]] name = "gio" -version = "0.20.9" +version = "0.20.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4f00c70f8029d84ea7572dd0e1aaa79e5329667b4c17f329d79ffb1e6277487" +checksum = "8e27e276e7b6b8d50f6376ee7769a71133e80d093bdc363bd0af71664228b831" dependencies = [ "futures-channel", "futures-core", @@ -7017,6 +7019,33 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tracker-rs" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc48e3b6e7a94b6c90b6905f157f2a875dfb5866cf19429476fd803c9c84eafc" +dependencies = [ + "bitflags 2.9.0", + "gio 0.20.12", + "glib 0.20.9", + "glib-sys 0.20.9", + "libc", + "tracker-sys", +] + +[[package]] +name = "tracker-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8814f2bd279c6fa3eb22921a5394df6109c6ec18f805e5973fa633a4e9a57785" +dependencies = [ + "gio-sys 0.20.9", + "glib-sys 0.20.9", + "gobject-sys 0.20.9", + "libc", + "system-deps 7.0.3", +] + [[package]] name = "tray-icon" version = "0.20.1" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 17def2c8..bef5288d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -113,6 +113,11 @@ tauri-plugin-prevent-default = "1" [target."cfg(target_os = \"macos\")".dependencies] tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" } + +[target."cfg(target_os = \"linux\")".dependencies] +gio = "0.20.12" +tracker-rs = "0.6.1" + [target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies] tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] } diff --git a/src-tauri/src/extension/built_in/file_search/implementation/linux_gnome.rs b/src-tauri/src/extension/built_in/file_search/implementation/linux_gnome.rs new file mode 100644 index 00000000..0673d475 --- /dev/null +++ b/src-tauri/src/extension/built_in/file_search/implementation/linux_gnome.rs @@ -0,0 +1,286 @@ +//! File system powered by GNOME's Tracker engine. + +use super::super::EXTENSION_ID; +use super::super::config::FileSearchConfig; +use super::should_be_filtered_out; +use crate::common::document::DataSourceReference; +use crate::extension::LOCAL_QUERY_SOURCE_TYPE; +use crate::util::file::sync_get_file_icon; +use crate::{ + common::document::{Document, OnOpened}, + extension::built_in::file_search::config::SearchBy, +}; +use gio::Cancellable; +use tracker::{SparqlConnection, SparqlCursor, prelude::SparqlCursorExtManual}; + +/// The service that we will connect to. +const SERVICE_NAME: &str = "org.freedesktop.Tracker3.Miner.Files"; + +/// Tracker won't return scores when we are not using full-text seach. In that +/// case, we use this score. +const SCORE: f64 = 1.0; + +/// Helper function to return different SPARQL queries depending on the different configurations. +fn query_sparql(query_string: &str, config: &FileSearchConfig) -> String { + match config.search_by { + SearchBy::Name => { + // Cannot use the inverted index as that searches for all the attributes, + // but we only want to search the filename. + format!( + "SELECT nie:url(?file_item) WHERE {{ ?file_item nfo:fileName ?fileName . FILTER(regex(?fileName, '{query_string}', 'i')) }}" + ) + } + SearchBy::NameAndContents => { + // Full-text search against all attributes + // OR + // filename search + format!( + "SELECT nie:url(?file_item) fts:rank(?file_item) WHERE {{ {{ ?file_item fts:match '{query_string}' }} UNION {{ ?file_item nfo:fileName ?fileName . FILTER(regex(?fileName, '{query_string}', 'i')) }} }} ORDER BY DESC fts:rank(?file_item)" + ) + } + } +} + +/// Helper function to replace unsupported characters with whitespace. +/// +/// Tracker will error out if it encounters these characters. +/// +/// The complete list of unsupported characters is unknown and we don't know how +/// to escape them, so let's replace them. +fn query_string_cleanup(old: &str) -> String { + const UNSUPPORTED_CHAR: [char; 3] = ['\'', '\n', '\\']; + + // Using len in bytes is ok + let mut chars = Vec::with_capacity(old.len()); + for char in old.chars() { + if UNSUPPORTED_CHAR.contains(&char) { + chars.push(' '); + } else { + chars.push(char); + } + } + + chars.into_iter().collect() +} + +struct Query { + conn: SparqlConnection, + cursor: SparqlCursor, +} + +impl Query { + fn new(query_string: &str, config: &FileSearchConfig) -> Result { + let query_string = query_string_cleanup(query_string); + let sparql = query_sparql(&query_string, config); + let conn = + SparqlConnection::bus_new(SERVICE_NAME, None, None).map_err(|e| e.to_string())?; + let cursor = conn + .query(&sparql, Cancellable::NONE) + .map_err(|e| e.to_string())?; + + Ok(Self { conn, cursor }) + } +} + +impl Drop for Query { + fn drop(&mut self) { + self.cursor.close(); + self.conn.close(); + } +} + +impl Iterator for Query { + /// It yields a tuple `(file path, score)` + type Item = Result<(String, f64), String>; + + fn next(&mut self) -> Option { + loop { + let has_next = match self + .cursor + .next(Cancellable::NONE) + .map_err(|e| e.to_string()) + { + Ok(has_next) => has_next, + Err(err_str) => return Some(Err(err_str)), + }; + + if !has_next { + return None; + } + + // The first column is the URL + let file_url_column = self.cursor.string(0); + // It could be None (or NULL ptr if you use C), I have no clue why. + let opt_str = file_url_column.as_ref().map(|gstr| gstr.as_str()); + + match opt_str { + Some(url) => { + // The returned URL has a prefix that we need to trim + const PREFIX: &str = "file://"; + const PREFIX_LEN: usize = PREFIX.len(); + + let file_path = url[PREFIX_LEN..].to_string(); + assert!(!file_path.is_empty()); + assert_ne!(file_path, "/", "file search should not hit the root path"); + + let score = { + // The second column is the score, this column may not + // exist. We use SCORE if the real value is absent. + let score_column = self.cursor.string(1); + let opt_score_str = score_column.as_ref().map(|g_str| g_str.as_str()); + let opt_score = opt_score_str.map(|str| { + str.parse::() + .expect("score should be valid for type f64") + }); + + opt_score.unwrap_or(SCORE) + }; + + return Some(Ok((file_path, score))); + } + None => { + // another try + continue; + } + } + } + } +} + +pub(crate) async fn hits( + query_string: &str, + from: usize, + size: usize, + config: &FileSearchConfig, +) -> Result, String> { + // Special cases that will make querying faster. + if query_string.is_empty() || size == 0 || config.search_paths.is_empty() { + return Ok(Vec::new()); + } + + let mut result_hits = Vec::with_capacity(size); + + let need_to_skip = { + if matches!(config.search_by, SearchBy::Name) { + // We don't use full-text search in this case, the returned documents + // won't be scored, the query hits won't be sorted, so processing the + // from parameter is meaningless. + false + } else { + from > 0 + } + }; + let mut num_skipped = 0; + let should_skip = from; + + let query = Query::new(query_string, config)?; + for res_entry in query { + let (file_path, score) = res_entry?; + + // This should be called before processing the `from` parameter. + if should_be_filtered_out(config, &file_path, true, true, true) { + continue; + } + + // Process the `from` parameter. + if need_to_skip && num_skipped < should_skip { + // Skip this + num_skipped += 1; + continue; + } + + let icon = sync_get_file_icon(&file_path); + let file_path_of_type_path = camino::Utf8Path::new(&file_path); + let r#where = file_path_of_type_path + .parent() + .unwrap_or_else(|| { + panic!( + "expect path [{}] to have a parent, but it does not", + file_path + ); + }) + .to_string(); + + let file_name = file_path_of_type_path.file_name().unwrap_or_else(|| { + panic!( + "expect path [{}] to have a file name, but it does not", + file_path + ); + }); + let on_opened = OnOpened::Document { + url: file_path.to_string(), + }; + + let doc = Document { + id: file_path.to_string(), + title: Some(file_name.to_string()), + source: Some(DataSourceReference { + r#type: Some(LOCAL_QUERY_SOURCE_TYPE.into()), + name: Some(EXTENSION_ID.into()), + id: Some(EXTENSION_ID.into()), + icon: Some(String::from("font_Filesearch")), + }), + category: Some(r#where), + on_opened: Some(on_opened), + url: Some(file_path), + icon: Some(icon.to_string()), + ..Default::default() + }; + + result_hits.push((doc, score)); + + // Collected enough documents, return + if result_hits.len() >= size { + break; + } + } + + Ok(result_hits) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_query_string_cleanup_basic() { + assert_eq!(query_string_cleanup("test"), "test"); + assert_eq!(query_string_cleanup("hello world"), "hello world"); + assert_eq!(query_string_cleanup("file.txt"), "file.txt"); + } + + #[test] + fn test_query_string_cleanup_unsupported_chars() { + assert_eq!(query_string_cleanup("test'file"), "test file"); + assert_eq!(query_string_cleanup("test\nfile"), "test file"); + assert_eq!(query_string_cleanup("test\\file"), "test file"); + } + + #[test] + fn test_query_string_cleanup_multiple_unsupported() { + assert_eq!(query_string_cleanup("test'file\nname"), "test file name"); + assert_eq!(query_string_cleanup("test\'file"), "test file"); + assert_eq!(query_string_cleanup("\n'test"), " test"); + } + + #[test] + fn test_query_string_cleanup_edge_cases() { + assert_eq!(query_string_cleanup(""), ""); + assert_eq!(query_string_cleanup("'"), " "); + assert_eq!(query_string_cleanup("\n"), " "); + assert_eq!(query_string_cleanup("\\"), " "); + assert_eq!(query_string_cleanup(" '\n\\ "), " "); + } + + #[test] + fn test_query_string_cleanup_mixed_content() { + assert_eq!( + query_string_cleanup("document's content\nwith\\backslash"), + "document s content with backslash" + ); + assert_eq!( + query_string_cleanup("path/to'file\nextension\\test"), + "path/to file extension test" + ); + } +} diff --git a/src-tauri/src/extension/built_in/file_search/implementation/macos.rs b/src-tauri/src/extension/built_in/file_search/implementation/macos.rs index 2d39d527..8f2d117d 100644 --- a/src-tauri/src/extension/built_in/file_search/implementation/macos.rs +++ b/src-tauri/src/extension/built_in/file_search/implementation/macos.rs @@ -1,10 +1,11 @@ use super::super::EXTENSION_ID; use super::super::config::FileSearchConfig; use super::super::config::SearchBy; +use super::should_be_filtered_out; use crate::common::document::{DataSourceReference, Document}; use crate::extension::LOCAL_QUERY_SOURCE_TYPE; use crate::extension::OnOpened; -use crate::util::file::get_file_icon; +use crate::util::file::sync_get_file_icon; use futures::stream::Stream; use futures::stream::StreamExt; use std::os::fd::OwnedFd; @@ -32,7 +33,7 @@ pub(crate) async fn hits( while let Some(res_file_path) = iter.next().await { let file_path = res_file_path.map_err(|io_err| io_err.to_string())?; - let icon = get_file_icon(file_path.clone()).await; + let icon = sync_get_file_icon(&file_path); let file_path_of_type_path = camino::Utf8Path::new(&file_path); let r#where = file_path_of_type_path .parent() @@ -155,7 +156,7 @@ fn execute_mdfind_query( .filter(move |res_path| { std::future::ready({ match res_path { - Ok(path) => !should_be_filtered_out(&config_clone, path), + Ok(path) => !should_be_filtered_out(&config_clone, path, false, true, true), Err(_) => { // Don't filter out Err() values true @@ -168,34 +169,3 @@ fn execute_mdfind_query( Ok((iter, child)) } - -/// If `file_path` should be removed from the search results given the filter -/// conditions specified in `config`. -fn should_be_filtered_out(config: &FileSearchConfig, file_path: &str) -> bool { - let is_excluded = config - .exclude_paths - .iter() - .any(|exclude_path| file_path.starts_with(exclude_path)); - - if is_excluded { - return true; - } - - let matches_file_type = if config.file_types.is_empty() { - true - } else { - let path_obj = camino::Utf8Path::new(&file_path); - if let Some(extension) = path_obj.extension() { - config - .file_types - .iter() - .any(|file_type| file_type == extension) - } else { - // `config.file_types` is not empty, then the search results - // should have extensions. - false - } - }; - - !matches_file_type -} diff --git a/src-tauri/src/extension/built_in/file_search/implementation/mod.rs b/src-tauri/src/extension/built_in/file_search/implementation/mod.rs index 71662433..c1d4585d 100644 --- a/src-tauri/src/extension/built_in/file_search/implementation/mod.rs +++ b/src-tauri/src/extension/built_in/file_search/implementation/mod.rs @@ -1,10 +1,382 @@ +#[cfg(target_os = "linux")] +mod linux_gnome; #[cfg(target_os = "macos")] mod macos; #[cfg(target_os = "windows")] mod windows; // `hits()` function is platform-specific, export the corresponding impl. +#[cfg(target_os = "linux")] +pub(crate) use linux_gnome::hits; #[cfg(target_os = "macos")] pub(crate) use macos::hits; #[cfg(target_os = "windows")] pub(crate) use windows::hits; + +use super::config::FileSearchConfig; +use camino::Utf8Path; + +/// If `file_path` should be removed from the search results given the filter +/// conditions specified in `config`. +pub(crate) fn should_be_filtered_out( + config: &FileSearchConfig, + file_path: &str, + check_search_paths: bool, + check_exclude_paths: bool, + check_file_type: bool, +) -> bool { + let file_path = Utf8Path::new(file_path); + + if check_search_paths { + // search path + let in_search_paths = config.search_paths.iter().any(|search_path| { + let search_path = Utf8Path::new(search_path); + file_path.starts_with(search_path) + }); + if !in_search_paths { + return true; + } + } + + if check_exclude_paths { + // exclude path + let is_excluded = config + .exclude_paths + .iter() + .any(|exclude_path| file_path.starts_with(exclude_path)); + if is_excluded { + return true; + } + } + + if check_file_type { + // file type + let matches_file_type = if config.file_types.is_empty() { + true + } else { + let path_obj = camino::Utf8Path::new(&file_path); + if let Some(extension) = path_obj.extension() { + config + .file_types + .iter() + .any(|file_type| file_type == extension) + } else { + // `config.file_types` is not empty, the hit files should have extensions. + false + } + }; + + if !matches_file_type { + return true; + } + } + + false +} + +#[cfg(test)] +mod tests { + use super::super::config::SearchBy; + use super::*; + + #[test] + fn test_should_be_filtered_out_with_no_check() { + let config = FileSearchConfig { + search_paths: vec!["/home/user/Documents".to_string()], + exclude_paths: vec![], + file_types: vec!["fffffff".into()], + search_by: SearchBy::Name, + }; + + assert!(!should_be_filtered_out( + &config, "abbc", false, false, false + )); + } + + #[test] + fn test_should_be_filtered_out_search_paths() { + let config = FileSearchConfig { + search_paths: vec![ + "/home/user/Documents".to_string(), + "/home/user/Downloads".to_string(), + ], + exclude_paths: vec![], + file_types: vec![], + search_by: SearchBy::Name, + }; + + // Files in search paths should not be filtered + assert!(!should_be_filtered_out( + &config, + "/home/user/Documents/file.txt", + true, + true, + true + )); + assert!(!should_be_filtered_out( + &config, + "/home/user/Downloads/image.jpg", + true, + true, + true + )); + assert!(!should_be_filtered_out( + &config, + "/home/user/Documents/folder/file.txt", + true, + true, + true + )); + + // Files not in search paths should be filtered + assert!(should_be_filtered_out( + &config, + "/home/user/Pictures/photo.jpg", + true, + true, + true + )); + assert!(should_be_filtered_out( + &config, + "/tmp/tempfile", + true, + true, + true + )); + assert!(should_be_filtered_out( + &config, + "/usr/bin/ls", + true, + true, + true + )); + } + + #[test] + fn test_should_be_filtered_out_exclude_paths() { + let config = FileSearchConfig { + search_paths: vec!["/home/user".to_string()], + exclude_paths: vec![ + "/home/user/Trash".to_string(), + "/home/user/.cache".to_string(), + ], + file_types: vec![], + search_by: SearchBy::Name, + }; + + // Files in search paths but not excluded should not be filtered + assert!(!should_be_filtered_out( + &config, + "/home/user/Documents/file.txt", + true, + true, + true + )); + assert!(!should_be_filtered_out( + &config, + "/home/user/Downloads/image.jpg", + true, + true, + true + )); + + // Files in excluded paths should be filtered + assert!(should_be_filtered_out( + &config, + "/home/user/Trash/deleted_file", + true, + true, + true + )); + assert!(should_be_filtered_out( + &config, + "/home/user/.cache/temp", + true, + true, + true + )); + assert!(should_be_filtered_out( + &config, + "/home/user/Trash/folder/file.txt", + true, + true, + true + )); + } + + #[test] + fn test_should_be_filtered_out_file_types() { + let config = FileSearchConfig { + search_paths: vec!["/home/user/Documents".to_string()], + exclude_paths: vec![], + file_types: vec!["txt".to_string(), "md".to_string()], + search_by: SearchBy::Name, + }; + + // Files with allowed extensions should not be filtered + assert!(!should_be_filtered_out( + &config, + "/home/user/Documents/notes.txt", + true, + true, + true + )); + assert!(!should_be_filtered_out( + &config, + "/home/user/Documents/readme.md", + true, + true, + true + )); + + // Files with disallowed extensions should be filtered + assert!(should_be_filtered_out( + &config, + "/home/user/Documents/image.jpg", + true, + true, + true + )); + assert!(should_be_filtered_out( + &config, + "/home/user/Documents/document.pdf", + true, + true, + true + )); + + // Files without extensions should be filtered when file_types is not empty + assert!(should_be_filtered_out( + &config, + "/home/user/Documents/file", + true, + true, + true + )); + assert!(should_be_filtered_out( + &config, + "/home/user/Documents/folder", + true, + true, + true + )); + } + + #[test] + fn test_should_be_filtered_out_empty_file_types() { + let config = FileSearchConfig { + search_paths: vec!["/home/user/Documents".to_string()], + exclude_paths: vec![], + file_types: vec![], + search_by: SearchBy::Name, + }; + + // When file_types is empty, all file types should be allowed + assert!(!should_be_filtered_out( + &config, + "/home/user/Documents/file.txt", + true, + true, + true + )); + assert!(!should_be_filtered_out( + &config, + "/home/user/Documents/image.jpg", + true, + true, + true + )); + assert!(!should_be_filtered_out( + &config, + "/home/user/Documents/document", + true, + true, + true + )); + assert!(!should_be_filtered_out( + &config, + "/home/user/Documents/folder/", + true, + true, + true + )); + } + + #[test] + fn test_should_be_filtered_out_combined_filters() { + let config = FileSearchConfig { + search_paths: vec!["/home/user".to_string()], + exclude_paths: vec!["/home/user/Trash".to_string()], + file_types: vec!["txt".to_string()], + search_by: SearchBy::Name, + }; + + // Should pass all filters: in search path, not excluded, and correct file type + assert!(!should_be_filtered_out( + &config, + "/home/user/Documents/notes.txt", + true, + true, + true + )); + + // Fails file type filter + assert!(should_be_filtered_out( + &config, + "/home/user/Documents/image.jpg", + true, + true, + true + )); + + // Fails exclude path filter + assert!(should_be_filtered_out( + &config, + "/home/user/Trash/deleted.txt", + true, + true, + true + )); + + // Fails search path filter + assert!(should_be_filtered_out( + &config, + "/tmp/temp.txt", + true, + true, + true + )); + } + + #[test] + fn test_should_be_filtered_out_edge_cases() { + let config = FileSearchConfig { + search_paths: vec!["/home/user".to_string()], + exclude_paths: vec![], + file_types: vec!["txt".to_string()], + search_by: SearchBy::Name, + }; + + // Empty path + assert!(should_be_filtered_out(&config, "", true, true, true)); + + // Root path + assert!(should_be_filtered_out(&config, "/", true, true, true)); + + // Path that starts with search path but continues differently + assert!(!should_be_filtered_out( + &config, + "/home/user/document.txt", + true, + true, + true + )); + assert!(should_be_filtered_out( + &config, + "/home/user_other/file.txt", + true, + true, + true + )); + } +} diff --git a/src-tauri/src/extension/built_in/file_search/implementation/windows.rs b/src-tauri/src/extension/built_in/file_search/implementation/windows.rs index 53f686a4..485d7d0a 100644 --- a/src-tauri/src/extension/built_in/file_search/implementation/windows.rs +++ b/src-tauri/src/extension/built_in/file_search/implementation/windows.rs @@ -9,7 +9,7 @@ use super::super::config::SearchBy; use crate::common::document::{DataSourceReference, Document}; use crate::extension::LOCAL_QUERY_SOURCE_TYPE; use crate::extension::OnOpened; -use crate::util::file::get_file_icon; +use crate::util::file::sync_get_file_icon; use windows::{ Win32::System::{ Com::{CLSCTX_INPROC_SERVER, CoCreateInstance}, @@ -420,7 +420,7 @@ pub(crate) async fn hits( // "file:C:/Users/desktop.ini" => "C:/Users/desktop.ini" let file_path = &item_url[ITEM_URL_PREFIX_LEN..]; - let icon = get_file_icon(file_path.to_string()).await; + let icon = sync_get_file_icon(file_path); let file_path_of_type_path = camino::Utf8Path::new(&file_path); let r#where = file_path_of_type_path .parent() diff --git a/src-tauri/src/extension/built_in/file_search/mod.rs b/src-tauri/src/extension/built_in/file_search/mod.rs index 11d810d7..14fea860 100644 --- a/src-tauri/src/extension/built_in/file_search/mod.rs +++ b/src-tauri/src/extension/built_in/file_search/mod.rs @@ -19,7 +19,7 @@ pub(crate) const PLUGIN_JSON_FILE: &str = r#" { "id": "File Search", "name": "File Search", - "platforms": ["macos", "windows"], + "platforms": ["macos", "windows", "linux"], "description": "Search files on your system", "icon": "font_Filesearch", "type": "extension" diff --git a/src-tauri/src/extension/built_in/mod.rs b/src-tauri/src/extension/built_in/mod.rs index 388e2bfe..a07e0536 100644 --- a/src-tauri/src/extension/built_in/mod.rs +++ b/src-tauri/src/extension/built_in/mod.rs @@ -3,7 +3,6 @@ pub mod ai_overview; pub mod application; pub mod calculator; -#[cfg(any(target_os = "macos", target_os = "windows"))] pub mod file_search; pub mod pizza_engine_runtime; pub mod quick_ai_access; @@ -173,18 +172,14 @@ pub(crate) async fn list_built_in_extensions( .await?, ); - cfg_if::cfg_if! { - if #[cfg(any(target_os = "macos", target_os = "windows"))] { - built_in_extensions.push( - load_built_in_extension( - &dir, - file_search::EXTENSION_ID, - file_search::PLUGIN_JSON_FILE, - ) - .await?, - ); - } - } + built_in_extensions.push( + load_built_in_extension( + &dir, + file_search::EXTENSION_ID, + file_search::PLUGIN_JSON_FILE, + ) + .await?, + ); Ok(built_in_extensions) } @@ -212,16 +207,12 @@ pub(super) async fn init_built_in_extension( log::debug!("built-in extension [{}] initialized", extension.id); } - cfg_if::cfg_if! { - if #[cfg(any(target_os = "macos", target_os = "windows"))] { - if extension.id == file_search::EXTENSION_ID { - let file_system_search = file_search::FileSearchExtensionSearchSource; - search_source_registry - .register_source(file_system_search) - .await; - log::debug!("built-in extension [{}] initialized", extension.id); - } - } + if extension.id == file_search::EXTENSION_ID { + let file_system_search = file_search::FileSearchExtensionSearchSource; + search_source_registry + .register_source(file_system_search) + .await; + log::debug!("built-in extension [{}] initialized", extension.id); } Ok(()) @@ -299,21 +290,17 @@ pub(crate) async fn enable_built_in_extension( return Ok(()); } - cfg_if::cfg_if! { - if #[cfg(any(target_os = "macos", target_os = "windows"))] { - if bundle_id.extension_id == file_search::EXTENSION_ID { - let file_system_search = file_search::FileSearchExtensionSearchSource; - search_source_registry_tauri_state - .register_source(file_system_search) - .await; - alter_extension_json_file( - &get_built_in_extension_directory(tauri_app_handle), - bundle_id, - update_extension, - )?; - return Ok(()); - } - } + if bundle_id.extension_id == file_search::EXTENSION_ID { + let file_system_search = file_search::FileSearchExtensionSearchSource; + search_source_registry_tauri_state + .register_source(file_system_search) + .await; + alter_extension_json_file( + &get_built_in_extension_directory(tauri_app_handle), + bundle_id, + update_extension, + )?; + return Ok(()); } Ok(()) @@ -387,20 +374,16 @@ pub(crate) async fn disable_built_in_extension( return Ok(()); } - cfg_if::cfg_if! { - if #[cfg(any(target_os = "macos", target_os = "windows"))] { - if bundle_id.extension_id == file_search::EXTENSION_ID { - search_source_registry_tauri_state - .remove_source(bundle_id.extension_id) - .await; - alter_extension_json_file( - &get_built_in_extension_directory(tauri_app_handle), - bundle_id, - update_extension, - )?; - return Ok(()); - } - } + if bundle_id.extension_id == file_search::EXTENSION_ID { + search_source_registry_tauri_state + .remove_source(bundle_id.extension_id) + .await; + alter_extension_json_file( + &get_built_in_extension_directory(tauri_app_handle), + bundle_id, + update_extension, + )?; + return Ok(()); } Ok(()) @@ -524,17 +507,11 @@ pub(crate) async fn is_built_in_extension_enabled( return Ok(extension.enabled); } - cfg_if::cfg_if! { - if #[cfg(any(target_os = "macos", target_os = "windows"))] { - if bundle_id.extension_id == file_search::EXTENSION_ID - && bundle_id.sub_extension_id.is_none() - { - return Ok(search_source_registry_tauri_state - .get_source(bundle_id.extension_id) - .await - .is_some()); - } - } + if bundle_id.extension_id == file_search::EXTENSION_ID && bundle_id.sub_extension_id.is_none() { + return Ok(search_source_registry_tauri_state + .get_source(bundle_id.extension_id) + .await + .is_some()); } unreachable!("extension [{:?}] is not a built-in extension", bundle_id) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 70046b89..1bfb8d10 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -170,9 +170,7 @@ pub fn run() { settings::get_allow_self_signature, assistant::ask_ai, crate::common::document::open, - #[cfg(any(target_os = "macos", target_os = "windows"))] extension::built_in::file_search::config::get_file_system_config, - #[cfg(any(target_os = "macos", target_os = "windows"))] extension::built_in::file_search::config::set_file_system_config, server::synthesize::synthesize, util::file::get_file_icon, diff --git a/src-tauri/src/util/file.rs b/src-tauri/src/util/file.rs index b65f1e5a..0622f452 100644 --- a/src-tauri/src/util/file.rs +++ b/src-tauri/src/util/file.rs @@ -50,7 +50,7 @@ pub(crate) enum FileType { Unknown, } -async fn get_file_type(path: &str) -> FileType { +fn get_file_type(path: &str) -> FileType { let path = camino::Utf8Path::new(path); // stat() is more precise than file extension, use it if possible. @@ -167,8 +167,13 @@ fn type_to_icon(ty: FileType) -> &'static str { } } -#[tauri::command] -pub(crate) async fn get_file_icon(path: String) -> &'static str { - let ty = get_file_type(path.as_str()).await; +/// Synchronous version of `get_file_icon()`. +pub(crate) fn sync_get_file_icon(path: &str) -> &'static str { + let ty = get_file_type(path); type_to_icon(ty) } + +#[tauri::command] +pub(crate) async fn get_file_icon(path: String) -> &'static str { + sync_get_file_icon(&path) +}