feat: file search for Linux/GNOME (#884)

This commit implements the file search extension for Linux with the
GNOME desktop environment by employing the engine that powers GNOME's
desktop search - Tracker.

It also fixes an edge case bug that the search and exclude path
configuration entries will not work.  For example, say I set the search path
to ["~/Dcouments"], and I have a file named "Documents_foobarbuzz" under
my home directory, this file is not in the specified search path but
Coco would return it because we verified this by checking string prefix.
Claude Code found this when I asked it to write unit tests.  Thank both
tests and Claude Code.
This commit is contained in:
SteveLauC
2025-08-25 19:29:37 +08:00
committed by GitHub
parent eafa704ca5
commit de3c78a5aa
13 changed files with 755 additions and 111 deletions

View File

@@ -104,7 +104,7 @@ jobs:
if: startsWith(matrix.platform, 'ubuntu-22.04') if: startsWith(matrix.platform, 'ubuntu-22.04')
run: | run: |
sudo apt-get update 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 - name: Add Rust build target
working-directory: src-tauri working-directory: src-tauri

View File

@@ -30,7 +30,7 @@ jobs:
if: startsWith(matrix.platform, 'ubuntu-latest') if: startsWith(matrix.platform, 'ubuntu-latest')
run: | run: |
sudo apt-get update 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 - name: Add pizza engine as a dependency
working-directory: src-tauri working-directory: src-tauri

View File

@@ -22,6 +22,8 @@ Information about release notes of Coco App is provided here.
- feat: impl extension settings 'hide_before_open' #862 - 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: index both en/zh_CN app names and show app name in chosen language #875
- feat: support context menu in debug mode #882 - feat: support context menu in debug mode #882
- feat: file search for Linux/GNOME #884
### 🐛 Bug fix ### 🐛 Bug fix

35
src-tauri/Cargo.lock generated
View File

@@ -858,6 +858,7 @@ dependencies = [
"function_name", "function_name",
"futures", "futures",
"futures-util", "futures-util",
"gio 0.20.12",
"hostname", "hostname",
"http 1.3.1", "http 1.3.1",
"hyper 0.14.32", "hyper 0.14.32",
@@ -910,6 +911,7 @@ dependencies = [
"tokio-stream", "tokio-stream",
"tokio-tungstenite", "tokio-tungstenite",
"tokio-util", "tokio-util",
"tracker-rs",
"tungstenite 0.24.0", "tungstenite 0.24.0",
"url", "url",
"walkdir", "walkdir",
@@ -1730,7 +1732,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adef9ff8c03b1c04bc2c743e0395588001b1c8f5669cab0b300abe873b9c9227" checksum = "adef9ff8c03b1c04bc2c743e0395588001b1c8f5669cab0b300abe873b9c9227"
dependencies = [ dependencies = [
"gio 0.20.9", "gio 0.20.12",
"gtk", "gtk",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-app-kit 0.2.2", "objc2-app-kit 0.2.2",
@@ -2194,9 +2196,9 @@ dependencies = [
[[package]] [[package]]
name = "gio" name = "gio"
version = "0.20.9" version = "0.20.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4f00c70f8029d84ea7572dd0e1aaa79e5329667b4c17f329d79ffb1e6277487" checksum = "8e27e276e7b6b8d50f6376ee7769a71133e80d093bdc363bd0af71664228b831"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
@@ -7017,6 +7019,33 @@ dependencies = [
"once_cell", "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]] [[package]]
name = "tray-icon" name = "tray-icon"
version = "0.20.1" version = "0.20.1"

View File

@@ -113,6 +113,11 @@ tauri-plugin-prevent-default = "1"
[target."cfg(target_os = \"macos\")".dependencies] [target."cfg(target_os = \"macos\")".dependencies]
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" } 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] [target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies]
tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] } tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }

View File

@@ -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<Self, String> {
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<Self::Item> {
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::<f64>()
.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<Vec<(Document, f64)>, 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"
);
}
}

View File

@@ -1,10 +1,11 @@
use super::super::EXTENSION_ID; use super::super::EXTENSION_ID;
use super::super::config::FileSearchConfig; use super::super::config::FileSearchConfig;
use super::super::config::SearchBy; use super::super::config::SearchBy;
use super::should_be_filtered_out;
use crate::common::document::{DataSourceReference, Document}; use crate::common::document::{DataSourceReference, Document};
use crate::extension::LOCAL_QUERY_SOURCE_TYPE; use crate::extension::LOCAL_QUERY_SOURCE_TYPE;
use crate::extension::OnOpened; 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::Stream;
use futures::stream::StreamExt; use futures::stream::StreamExt;
use std::os::fd::OwnedFd; use std::os::fd::OwnedFd;
@@ -32,7 +33,7 @@ pub(crate) async fn hits(
while let Some(res_file_path) = iter.next().await { while let Some(res_file_path) = iter.next().await {
let file_path = res_file_path.map_err(|io_err| io_err.to_string())?; 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 file_path_of_type_path = camino::Utf8Path::new(&file_path);
let r#where = file_path_of_type_path let r#where = file_path_of_type_path
.parent() .parent()
@@ -155,7 +156,7 @@ fn execute_mdfind_query(
.filter(move |res_path| { .filter(move |res_path| {
std::future::ready({ std::future::ready({
match res_path { 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(_) => { Err(_) => {
// Don't filter out Err() values // Don't filter out Err() values
true true
@@ -168,34 +169,3 @@ fn execute_mdfind_query(
Ok((iter, child)) 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
}

View File

@@ -1,10 +1,382 @@
#[cfg(target_os = "linux")]
mod linux_gnome;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
mod macos; mod macos;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
mod windows; mod windows;
// `hits()` function is platform-specific, export the corresponding impl. // `hits()` function is platform-specific, export the corresponding impl.
#[cfg(target_os = "linux")]
pub(crate) use linux_gnome::hits;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub(crate) use macos::hits; pub(crate) use macos::hits;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub(crate) use windows::hits; 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
));
}
}

View File

@@ -9,7 +9,7 @@ use super::super::config::SearchBy;
use crate::common::document::{DataSourceReference, Document}; use crate::common::document::{DataSourceReference, Document};
use crate::extension::LOCAL_QUERY_SOURCE_TYPE; use crate::extension::LOCAL_QUERY_SOURCE_TYPE;
use crate::extension::OnOpened; use crate::extension::OnOpened;
use crate::util::file::get_file_icon; use crate::util::file::sync_get_file_icon;
use windows::{ use windows::{
Win32::System::{ Win32::System::{
Com::{CLSCTX_INPROC_SERVER, CoCreateInstance}, Com::{CLSCTX_INPROC_SERVER, CoCreateInstance},
@@ -420,7 +420,7 @@ pub(crate) async fn hits(
// "file:C:/Users/desktop.ini" => "C:/Users/desktop.ini" // "file:C:/Users/desktop.ini" => "C:/Users/desktop.ini"
let file_path = &item_url[ITEM_URL_PREFIX_LEN..]; 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 file_path_of_type_path = camino::Utf8Path::new(&file_path);
let r#where = file_path_of_type_path let r#where = file_path_of_type_path
.parent() .parent()

View File

@@ -19,7 +19,7 @@ pub(crate) const PLUGIN_JSON_FILE: &str = r#"
{ {
"id": "File Search", "id": "File Search",
"name": "File Search", "name": "File Search",
"platforms": ["macos", "windows"], "platforms": ["macos", "windows", "linux"],
"description": "Search files on your system", "description": "Search files on your system",
"icon": "font_Filesearch", "icon": "font_Filesearch",
"type": "extension" "type": "extension"

View File

@@ -3,7 +3,6 @@
pub mod ai_overview; pub mod ai_overview;
pub mod application; pub mod application;
pub mod calculator; pub mod calculator;
#[cfg(any(target_os = "macos", target_os = "windows"))]
pub mod file_search; pub mod file_search;
pub mod pizza_engine_runtime; pub mod pizza_engine_runtime;
pub mod quick_ai_access; pub mod quick_ai_access;
@@ -173,18 +172,14 @@ pub(crate) async fn list_built_in_extensions(
.await?, .await?,
); );
cfg_if::cfg_if! { built_in_extensions.push(
if #[cfg(any(target_os = "macos", target_os = "windows"))] { load_built_in_extension(
built_in_extensions.push( &dir,
load_built_in_extension( file_search::EXTENSION_ID,
&dir, file_search::PLUGIN_JSON_FILE,
file_search::EXTENSION_ID, )
file_search::PLUGIN_JSON_FILE, .await?,
) );
.await?,
);
}
}
Ok(built_in_extensions) Ok(built_in_extensions)
} }
@@ -212,16 +207,12 @@ pub(super) async fn init_built_in_extension(
log::debug!("built-in extension [{}] initialized", extension.id); log::debug!("built-in extension [{}] initialized", extension.id);
} }
cfg_if::cfg_if! { if extension.id == file_search::EXTENSION_ID {
if #[cfg(any(target_os = "macos", target_os = "windows"))] { let file_system_search = file_search::FileSearchExtensionSearchSource;
if extension.id == file_search::EXTENSION_ID { search_source_registry
let file_system_search = file_search::FileSearchExtensionSearchSource; .register_source(file_system_search)
search_source_registry .await;
.register_source(file_system_search) log::debug!("built-in extension [{}] initialized", extension.id);
.await;
log::debug!("built-in extension [{}] initialized", extension.id);
}
}
} }
Ok(()) Ok(())
@@ -299,21 +290,17 @@ pub(crate) async fn enable_built_in_extension(
return Ok(()); return Ok(());
} }
cfg_if::cfg_if! { if bundle_id.extension_id == file_search::EXTENSION_ID {
if #[cfg(any(target_os = "macos", target_os = "windows"))] { let file_system_search = file_search::FileSearchExtensionSearchSource;
if bundle_id.extension_id == file_search::EXTENSION_ID { search_source_registry_tauri_state
let file_system_search = file_search::FileSearchExtensionSearchSource; .register_source(file_system_search)
search_source_registry_tauri_state .await;
.register_source(file_system_search) alter_extension_json_file(
.await; &get_built_in_extension_directory(tauri_app_handle),
alter_extension_json_file( bundle_id,
&get_built_in_extension_directory(tauri_app_handle), update_extension,
bundle_id, )?;
update_extension, return Ok(());
)?;
return Ok(());
}
}
} }
Ok(()) Ok(())
@@ -387,20 +374,16 @@ pub(crate) async fn disable_built_in_extension(
return Ok(()); return Ok(());
} }
cfg_if::cfg_if! { if bundle_id.extension_id == file_search::EXTENSION_ID {
if #[cfg(any(target_os = "macos", target_os = "windows"))] { search_source_registry_tauri_state
if bundle_id.extension_id == file_search::EXTENSION_ID { .remove_source(bundle_id.extension_id)
search_source_registry_tauri_state .await;
.remove_source(bundle_id.extension_id) alter_extension_json_file(
.await; &get_built_in_extension_directory(tauri_app_handle),
alter_extension_json_file( bundle_id,
&get_built_in_extension_directory(tauri_app_handle), update_extension,
bundle_id, )?;
update_extension, return Ok(());
)?;
return Ok(());
}
}
} }
Ok(()) Ok(())
@@ -524,17 +507,11 @@ pub(crate) async fn is_built_in_extension_enabled(
return Ok(extension.enabled); return Ok(extension.enabled);
} }
cfg_if::cfg_if! { if bundle_id.extension_id == file_search::EXTENSION_ID && bundle_id.sub_extension_id.is_none() {
if #[cfg(any(target_os = "macos", target_os = "windows"))] { return Ok(search_source_registry_tauri_state
if bundle_id.extension_id == file_search::EXTENSION_ID .get_source(bundle_id.extension_id)
&& bundle_id.sub_extension_id.is_none() .await
{ .is_some());
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) unreachable!("extension [{:?}] is not a built-in extension", bundle_id)

View File

@@ -170,9 +170,7 @@ pub fn run() {
settings::get_allow_self_signature, settings::get_allow_self_signature,
assistant::ask_ai, assistant::ask_ai,
crate::common::document::open, crate::common::document::open,
#[cfg(any(target_os = "macos", target_os = "windows"))]
extension::built_in::file_search::config::get_file_system_config, 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, extension::built_in::file_search::config::set_file_system_config,
server::synthesize::synthesize, server::synthesize::synthesize,
util::file::get_file_icon, util::file::get_file_icon,

View File

@@ -50,7 +50,7 @@ pub(crate) enum FileType {
Unknown, Unknown,
} }
async fn get_file_type(path: &str) -> FileType { fn get_file_type(path: &str) -> FileType {
let path = camino::Utf8Path::new(path); let path = camino::Utf8Path::new(path);
// stat() is more precise than file extension, use it if possible. // 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] /// Synchronous version of `get_file_icon()`.
pub(crate) async fn get_file_icon(path: String) -> &'static str { pub(crate) fn sync_get_file_icon(path: &str) -> &'static str {
let ty = get_file_type(path.as_str()).await; let ty = get_file_type(path);
type_to_icon(ty) type_to_icon(ty)
} }
#[tauri::command]
pub(crate) async fn get_file_icon(path: String) -> &'static str {
sync_get_file_icon(&path)
}