Skip to content

Commit 412c8d8

Browse files
authored
feat: file search for Linux/KDE (#886)
This commit implements the file search extension for Linux/KDE using its desktop search engine Baloo.
1 parent de3c78a commit 412c8d8

8 files changed

Lines changed: 239 additions & 7 deletions

File tree

docs/content.en/docs/release-notes/_index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Information about release notes of Coco App is provided here.
2323
- feat: index both en/zh_CN app names and show app name in chosen language #875
2424
- feat: support context menu in debug mode #882
2525
- feat: file search for Linux/GNOME #884
26+
- feat: file search for Linux/KDE #886
2627

2728

2829
### 🐛 Bug fix

src-tauri/Cargo.lock

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2"
117117
[target."cfg(target_os = \"linux\")".dependencies]
118118
gio = "0.20.12"
119119
tracker-rs = "0.6.1"
120+
which = "8.0.0"
120121

121122
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies]
122123
tauri-plugin-single-instance = { version = "2.0.0", features = ["deep-link"] }

src-tauri/src/extension/built_in/file_search/implementation/linux_gnome.rs renamed to src-tauri/src/extension/built_in/file_search/implementation/linux/gnome.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
//! File system powered by GNOME's Tracker engine.
22
3-
use super::super::EXTENSION_ID;
4-
use super::super::config::FileSearchConfig;
5-
use super::should_be_filtered_out;
3+
use super::super::super::EXTENSION_ID;
4+
use super::super::super::config::FileSearchConfig;
5+
use super::super::should_be_filtered_out;
66
use crate::common::document::DataSourceReference;
77
use crate::extension::LOCAL_QUERY_SOURCE_TYPE;
88
use crate::util::file::sync_get_file_icon;
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
//! File search for KDE, powered by its Baloo engine.
2+
3+
use super::super::super::EXTENSION_ID;
4+
use super::super::super::config::FileSearchConfig;
5+
use super::super::super::config::SearchBy;
6+
use super::super::should_be_filtered_out;
7+
use crate::common::document::{DataSourceReference, Document};
8+
use crate::extension::LOCAL_QUERY_SOURCE_TYPE;
9+
use crate::extension::OnOpened;
10+
use crate::util::file::sync_get_file_icon;
11+
use futures::stream::Stream;
12+
use futures::stream::StreamExt;
13+
use std::os::fd::OwnedFd;
14+
use std::path::PathBuf;
15+
use tokio::io::AsyncBufReadExt;
16+
use tokio::io::BufReader;
17+
use tokio::process::Child;
18+
use tokio::process::Command;
19+
use tokio_stream::wrappers::LinesStream;
20+
21+
/// Baloo does not support scoring, use this score for all the documents.
22+
const SCORE: f64 = 1.0;
23+
24+
/// KDE6 updates the binary name to "baloosearch6", but I believe there still have
25+
/// distros using the original name. So we need to check both.
26+
fn cli_tool_lookup() -> Option<PathBuf> {
27+
use which::which;
28+
29+
let res_path = which("baloosearch").or_else(|_| which("baloosearch6"));
30+
res_path.ok()
31+
}
32+
33+
pub(crate) async fn hits(
34+
query_string: &str,
35+
_from: usize,
36+
size: usize,
37+
config: &FileSearchConfig,
38+
) -> Result<Vec<(Document, f64)>, String> {
39+
// Special cases that will make querying faster.
40+
if query_string.is_empty() || size == 0 || config.search_paths.is_empty() {
41+
return Ok(Vec::new());
42+
}
43+
44+
// If the tool is not found, return an empty result as well.
45+
let Some(tool_path) = cli_tool_lookup() else {
46+
return Ok(Vec::new());
47+
};
48+
49+
let (mut iter, _baloosearch_child_process) =
50+
execute_baloosearch_query(tool_path, query_string, size, config)?;
51+
52+
// Convert results to documents
53+
let mut hits: Vec<(Document, f64)> = Vec::new();
54+
while let Some(res_file_path) = iter.next().await {
55+
let file_path = res_file_path.map_err(|io_err| io_err.to_string())?;
56+
57+
let icon = sync_get_file_icon(&file_path);
58+
let file_path_of_type_path = camino::Utf8Path::new(&file_path);
59+
let r#where = file_path_of_type_path
60+
.parent()
61+
.unwrap_or_else(|| {
62+
panic!(
63+
"expect path [{}] to have a parent, but it does not",
64+
file_path
65+
);
66+
})
67+
.to_string();
68+
69+
let file_name = file_path_of_type_path.file_name().unwrap_or_else(|| {
70+
panic!(
71+
"expect path [{}] to have a file name, but it does not",
72+
file_path
73+
);
74+
});
75+
let on_opened = OnOpened::Document {
76+
url: file_path.clone(),
77+
};
78+
79+
let doc = Document {
80+
id: file_path.clone(),
81+
title: Some(file_name.to_string()),
82+
source: Some(DataSourceReference {
83+
r#type: Some(LOCAL_QUERY_SOURCE_TYPE.into()),
84+
name: Some(EXTENSION_ID.into()),
85+
id: Some(EXTENSION_ID.into()),
86+
icon: Some(String::from("font_Filesearch")),
87+
}),
88+
category: Some(r#where),
89+
on_opened: Some(on_opened),
90+
url: Some(file_path),
91+
icon: Some(icon.to_string()),
92+
..Default::default()
93+
};
94+
95+
hits.push((doc, SCORE));
96+
}
97+
98+
Ok(hits)
99+
}
100+
101+
/// Return an array containing the `baloosearch` command and its arguments.
102+
fn build_baloosearch_query(
103+
tool_path: PathBuf,
104+
query_string: &str,
105+
config: &FileSearchConfig,
106+
) -> Vec<String> {
107+
let tool_path = tool_path
108+
.into_os_string()
109+
.into_string()
110+
.expect("binary path should be UTF-8 encoded");
111+
112+
let mut args = vec![tool_path];
113+
114+
match config.search_by {
115+
SearchBy::Name => {
116+
args.push(format!("filename:{query_string}"));
117+
}
118+
SearchBy::NameAndContents => {
119+
args.push(query_string.to_string());
120+
}
121+
}
122+
123+
for search_path in config.search_paths.iter() {
124+
args.extend_from_slice(&["-d".into(), search_path.clone()]);
125+
}
126+
127+
args
128+
}
129+
130+
/// Spawn the `baloosearch` child process and return an async iterator over its output,
131+
/// allowing us to collect the results asynchronously.
132+
///
133+
/// # Return value:
134+
///
135+
/// * impl Stream: an async iterator that will yield the matched files
136+
/// * Child: The handle to the baloosearch process. The child process will be
137+
/// killed when this handle gets dropped so we need to keep it alive util we
138+
/// exhaust the stream.
139+
fn execute_baloosearch_query(
140+
tool_path: PathBuf,
141+
query_string: &str,
142+
size: usize,
143+
config: &FileSearchConfig,
144+
) -> Result<(impl Stream<Item = std::io::Result<String>>, Child), String> {
145+
let args = build_baloosearch_query(tool_path, query_string, config);
146+
147+
let (rx, tx) = std::io::pipe().unwrap();
148+
let rx_owned = OwnedFd::from(rx);
149+
let async_rx = tokio::net::unix::pipe::Receiver::from_owned_fd(rx_owned).unwrap();
150+
let buffered_rx = BufReader::new(async_rx);
151+
let lines = LinesStream::new(buffered_rx.lines());
152+
153+
let child = Command::new(&args[0])
154+
.args(&args[1..])
155+
.stdout(tx)
156+
.stderr(std::process::Stdio::null())
157+
// The child process will be killed when the Child instance gets dropped.
158+
.kill_on_drop(true)
159+
.spawn()
160+
.map_err(|e| format!("Failed to spawn baloosearch: {e}"))?;
161+
let config_clone = config.clone();
162+
let iter = lines
163+
.filter(move |res_path| {
164+
std::future::ready({
165+
match res_path {
166+
Ok(path) => !should_be_filtered_out(&config_clone, path, false, true, true),
167+
Err(_) => {
168+
// Don't filter out Err() values
169+
true
170+
}
171+
}
172+
})
173+
})
174+
.take(size);
175+
176+
Ok((iter, child))
177+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
mod gnome;
2+
mod kde;
3+
4+
use super::super::config::FileSearchConfig;
5+
use crate::common::document::Document;
6+
use crate::util::LinuxDesktopEnvironment;
7+
use crate::util::get_linux_desktop_environment;
8+
9+
/// Dispatch to implementations powered by different backends.
10+
pub(crate) async fn hits(
11+
query_string: &str,
12+
from: usize,
13+
size: usize,
14+
config: &FileSearchConfig,
15+
) -> Result<Vec<(Document, f64)>, String> {
16+
let de = get_linux_desktop_environment();
17+
match de {
18+
Some(LinuxDesktopEnvironment::Gnome) => gnome::hits(query_string, from, size, config).await,
19+
Some(LinuxDesktopEnvironment::Kde) => kde::hits(query_string, from, size, config).await,
20+
Some(LinuxDesktopEnvironment::Unsupported {
21+
xdg_current_desktop: _,
22+
}) => {
23+
return Err("file search is not supported on this desktop environment".into());
24+
}
25+
None => {
26+
return Err("could not determine Linux desktop environment".into());
27+
}
28+
}
29+
}

src-tauri/src/extension/built_in/file_search/implementation/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
#[cfg(target_os = "linux")]
2-
mod linux_gnome;
2+
mod linux;
33
#[cfg(target_os = "macos")]
44
mod macos;
55
#[cfg(target_os = "windows")]
66
mod windows;
77

88
// `hits()` function is platform-specific, export the corresponding impl.
99
#[cfg(target_os = "linux")]
10-
pub(crate) use linux_gnome::hits;
10+
pub(crate) use linux::hits;
1111
#[cfg(target_os = "macos")]
1212
pub(crate) use macos::hits;
1313
#[cfg(target_os = "windows")]

src-tauri/src/util/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use tauri_plugin_shell::ShellExt;
1313
const XDG_CURRENT_DESKTOP: &str = "XDG_CURRENT_DESKTOP";
1414

1515
#[derive(Debug, PartialEq)]
16-
enum LinuxDesktopEnvironment {
16+
pub(crate) enum LinuxDesktopEnvironment {
1717
Gnome,
1818
Kde,
1919
Unsupported { xdg_current_desktop: String },
@@ -65,7 +65,7 @@ impl LinuxDesktopEnvironment {
6565
}
6666

6767
/// None means that it is likely that we do not have a desktop environment.
68-
fn get_linux_desktop_environment() -> Option<LinuxDesktopEnvironment> {
68+
pub(crate) fn get_linux_desktop_environment() -> Option<LinuxDesktopEnvironment> {
6969
let de_os_str = std::env::var_os(XDG_CURRENT_DESKTOP)?;
7070
let de_str = de_os_str.into_string().unwrap_or_else(|_os_string| {
7171
panic!("${} should be UTF-8 encoded", XDG_CURRENT_DESKTOP);

0 commit comments

Comments
 (0)