mirror of
https://github.com/ByteAtATime/raycast-linux.git
synced 2025-09-12 17:06:26 +00:00
feat(file-search): implement file and folder search with indexing
This commit adds a basic file search feature, allowing users to find files and folders within their home directory. The backend consists of a Rust module that builds and maintains a SQLite index of the file system. It performs an initial scan and then uses the `notify` crate to watch for real-time changes. On the frontend, a new Svelte view provides a dedicated UI for searching. It displays results, file details, and an action bar with options to open the file/folder, show it in the native file manager, copy its path, or move it to the trash.
This commit is contained in:
parent
a561534075
commit
e2316cf949
13 changed files with 785 additions and 6 deletions
121
src-tauri/Cargo.lock
generated
121
src-tauri/Cargo.lock
generated
|
@ -1668,6 +1668,27 @@ dependencies = [
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "file-id"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6bc904b9bbefcadbd8e3a9fb0d464a9b979de6324c03b3c663e8994f46a5be36"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filetime"
|
||||||
|
version = "0.2.25"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"libredox",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fixedbitset"
|
name = "fixedbitset"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
|
@ -1772,6 +1793,15 @@ dependencies = [
|
||||||
"xdg",
|
"xdg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fsevent-sys"
|
||||||
|
version = "4.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "funty"
|
name = "funty"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
|
@ -2696,6 +2726,26 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inotify"
|
||||||
|
version = "0.9.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"inotify-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inotify-sys"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "inout"
|
name = "inout"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
|
@ -2884,6 +2934,26 @@ dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kqueue"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
|
||||||
|
dependencies = [
|
||||||
|
"kqueue-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kqueue-sys"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kuchikiki"
|
name = "kuchikiki"
|
||||||
version = "0.8.8-speedreader"
|
version = "0.8.8-speedreader"
|
||||||
|
@ -2986,6 +3056,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"libc",
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -3184,6 +3255,18 @@ dependencies = [
|
||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mio"
|
||||||
|
version = "0.8.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
|
@ -3337,6 +3420,39 @@ version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "notify"
|
||||||
|
version = "6.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.1",
|
||||||
|
"crossbeam-channel",
|
||||||
|
"filetime",
|
||||||
|
"fsevent-sys",
|
||||||
|
"inotify",
|
||||||
|
"kqueue",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"mio 0.8.11",
|
||||||
|
"walkdir",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "notify-debouncer-full"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fb7fd166739789c9ff169e654dc1501373db9d80a4c3f972817c8a4d7cf8f34e"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-channel",
|
||||||
|
"file-id",
|
||||||
|
"log",
|
||||||
|
"notify",
|
||||||
|
"parking_lot",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-bigint"
|
name = "num-bigint"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
|
@ -4474,6 +4590,8 @@ dependencies = [
|
||||||
"image",
|
"image",
|
||||||
"keyring",
|
"keyring",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
"notify",
|
||||||
|
"notify-debouncer-full",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rand 0.9.1",
|
"rand 0.9.1",
|
||||||
"rayon",
|
"rayon",
|
||||||
|
@ -4501,6 +4619,7 @@ dependencies = [
|
||||||
"trash",
|
"trash",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
"walkdir",
|
||||||
"xkbcommon 0.8.0",
|
"xkbcommon 0.8.0",
|
||||||
"zbus",
|
"zbus",
|
||||||
"zip",
|
"zip",
|
||||||
|
@ -6041,7 +6160,7 @@ dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio 1.0.4",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
|
|
|
@ -59,6 +59,9 @@ lazy_static = "1.5.0"
|
||||||
xkbcommon = "0.8.0"
|
xkbcommon = "0.8.0"
|
||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
tauri-plugin-fs = "2"
|
tauri-plugin-fs = "2"
|
||||||
|
walkdir = "2.5.0"
|
||||||
|
notify = "6.1.1"
|
||||||
|
notify-debouncer-full = "0.3.1"
|
||||||
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
tauri-plugin-global-shortcut = "2"
|
tauri-plugin-global-shortcut = "2"
|
||||||
|
|
|
@ -8,7 +8,7 @@ use chrono::Utc;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use rusqlite::{params, Connection, Result as RusqliteResult};
|
use rusqlite::{params, Connection, Result as RusqliteResult};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::AtomicBool;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::{AppHandle, Manager};
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ pub enum AppError {
|
||||||
Keyring(keyring::Error),
|
Keyring(keyring::Error),
|
||||||
ClipboardHistory(String),
|
ClipboardHistory(String),
|
||||||
Frecency(String),
|
Frecency(String),
|
||||||
|
FileSearch(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<io::Error> for AppError {
|
impl From<io::Error> for AppError {
|
||||||
|
@ -51,6 +52,7 @@ impl std::fmt::Display for AppError {
|
||||||
AppError::Keyring(err) => write!(f, "Keychain error: {}", err),
|
AppError::Keyring(err) => write!(f, "Keychain error: {}", err),
|
||||||
AppError::ClipboardHistory(msg) => write!(f, "Clipboard history error: {}", msg),
|
AppError::ClipboardHistory(msg) => write!(f, "Clipboard history error: {}", msg),
|
||||||
AppError::Frecency(msg) => write!(f, "Frecency error: {}", msg),
|
AppError::Frecency(msg) => write!(f, "Frecency error: {}", msg),
|
||||||
|
AppError::FileSearch(msg) => write!(f, "File search error: {}", msg),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
104
src-tauri/src/file_search/indexer.rs
Normal file
104
src-tauri/src/file_search/indexer.rs
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
use super::{manager::FileSearchManager, types::IndexedFile};
|
||||||
|
use std::{env, time::SystemTime};
|
||||||
|
use tauri::{AppHandle, Manager};
|
||||||
|
use walkdir::{DirEntry, WalkDir};
|
||||||
|
|
||||||
|
pub async fn build_initial_index(app_handle: AppHandle) {
|
||||||
|
println!("Starting initial file index build.");
|
||||||
|
let manager = app_handle.state::<FileSearchManager>();
|
||||||
|
let home_dir = match env::var("HOME") {
|
||||||
|
Ok(path) => path,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to get home directory: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let walker = WalkDir::new(home_dir).into_iter();
|
||||||
|
for entry in walker.filter_entry(|e| !is_hidden(e) && !is_excluded(e)) {
|
||||||
|
let entry = match entry {
|
||||||
|
Ok(entry) => entry,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error walking directory: {}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = entry.path();
|
||||||
|
let metadata = match entry.metadata() {
|
||||||
|
Ok(meta) => meta,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
let last_modified_secs = metadata
|
||||||
|
.modified()
|
||||||
|
.unwrap_or(SystemTime::UNIX_EPOCH)
|
||||||
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs() as i64;
|
||||||
|
|
||||||
|
if let Ok(Some(indexed_time)) = manager.get_file_last_modified(&path.to_string_lossy()) {
|
||||||
|
if indexed_time >= last_modified_secs {
|
||||||
|
if path.is_dir() {
|
||||||
|
// continue to walk children
|
||||||
|
} else {
|
||||||
|
// skip this file
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_type = if metadata.is_dir() {
|
||||||
|
"directory".to_string()
|
||||||
|
} else if metadata.is_file() {
|
||||||
|
"file".to_string()
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let indexed_file = IndexedFile {
|
||||||
|
path: path.to_string_lossy().to_string(),
|
||||||
|
name: entry.file_name().to_string_lossy().to_string(),
|
||||||
|
parent_path: path
|
||||||
|
.parent()
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
file_type,
|
||||||
|
last_modified: last_modified_secs,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = manager.add_file(&indexed_file) {
|
||||||
|
eprintln!("Failed to add file to index: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("Finished initial file index build.");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_hidden(entry: &DirEntry) -> bool {
|
||||||
|
entry
|
||||||
|
.file_name()
|
||||||
|
.to_str()
|
||||||
|
.map(|s| s.starts_with('.'))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_excluded(entry: &DirEntry) -> bool {
|
||||||
|
let path = entry.path();
|
||||||
|
let excluded_dirs = [
|
||||||
|
"node_modules",
|
||||||
|
".git",
|
||||||
|
"target",
|
||||||
|
".vscode",
|
||||||
|
".idea",
|
||||||
|
"__pycache__",
|
||||||
|
".cache",
|
||||||
|
"Library",
|
||||||
|
"Application Support",
|
||||||
|
"AppData",
|
||||||
|
];
|
||||||
|
path.components().any(|component| {
|
||||||
|
excluded_dirs
|
||||||
|
.iter()
|
||||||
|
.any(|&excluded| component.as_os_str() == excluded)
|
||||||
|
})
|
||||||
|
}
|
145
src-tauri/src/file_search/manager.rs
Normal file
145
src-tauri/src/file_search/manager.rs
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
use std::fs;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use rusqlite::{params, Connection, OptionalExtension, Result as RusqliteResult};
|
||||||
|
use tauri::{AppHandle, Manager};
|
||||||
|
|
||||||
|
use super::types::IndexedFile;
|
||||||
|
use crate::error::AppError;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct FileSearchManager {
|
||||||
|
db: Arc<Mutex<Connection>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileSearchManager {
|
||||||
|
pub fn new(app_handle: AppHandle) -> Result<Self, AppError> {
|
||||||
|
let data_dir = app_handle
|
||||||
|
.path()
|
||||||
|
.app_local_data_dir()
|
||||||
|
.map_err(|_| AppError::DirectoryNotFound)?;
|
||||||
|
|
||||||
|
if !data_dir.exists() {
|
||||||
|
fs::create_dir_all(&data_dir).map_err(|e| AppError::FileSearch(e.to_string()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let db_path = data_dir.join("file_search.sqlite");
|
||||||
|
let db = Connection::open(db_path)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
db: Arc::new(Mutex::new(db)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_db(&self) -> RusqliteResult<()> {
|
||||||
|
let db = self.db.lock().unwrap();
|
||||||
|
|
||||||
|
db.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS file_index (
|
||||||
|
path TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
parent_path TEXT NOT NULL,
|
||||||
|
file_type TEXT NOT NULL,
|
||||||
|
last_modified INTEGER NOT NULL
|
||||||
|
)",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
db.execute(
|
||||||
|
"CREATE VIRTUAL TABLE IF NOT EXISTS file_index_fts
|
||||||
|
USING fts5(name, content='file_index', content_rowid='rowid', tokenize = 'porter unicode61')",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
db.execute(
|
||||||
|
"CREATE TRIGGER IF NOT EXISTS file_index_after_insert
|
||||||
|
AFTER INSERT ON file_index
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO file_index_fts(rowid, name) VALUES (new.rowid, new.name);
|
||||||
|
END;",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
db.execute(
|
||||||
|
"CREATE TRIGGER IF NOT EXISTS file_index_after_delete
|
||||||
|
AFTER DELETE ON file_index
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO file_index_fts(file_index_fts, rowid, name) VALUES ('delete', old.rowid, old.name);
|
||||||
|
END;",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
db.execute(
|
||||||
|
"CREATE TRIGGER IF NOT EXISTS file_index_after_update
|
||||||
|
AFTER UPDATE ON file_index
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO file_index_fts(file_index_fts, rowid, name) VALUES ('delete', old.rowid, old.name);
|
||||||
|
INSERT INTO file_index_fts(rowid, name) VALUES (new.rowid, new.name);
|
||||||
|
END;",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_file(&self, file: &IndexedFile) -> Result<(), AppError> {
|
||||||
|
let db = self.db.lock().unwrap();
|
||||||
|
db.execute(
|
||||||
|
"INSERT OR REPLACE INTO file_index (path, name, parent_path, file_type, last_modified)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||||
|
params![
|
||||||
|
file.path,
|
||||||
|
file.name,
|
||||||
|
file.parent_path,
|
||||||
|
file.file_type,
|
||||||
|
file.last_modified
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_file(&self, path: &str) -> Result<(), AppError> {
|
||||||
|
let db = self.db.lock().unwrap();
|
||||||
|
db.execute("DELETE FROM file_index WHERE path = ?1", params![path])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_file_last_modified(&self, path: &str) -> Result<Option<i64>, AppError> {
|
||||||
|
let db = self.db.lock().unwrap();
|
||||||
|
let last_modified: Result<Option<i64>, rusqlite::Error> = db
|
||||||
|
.query_row(
|
||||||
|
"SELECT last_modified FROM file_index WHERE path = ?1",
|
||||||
|
params![path],
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
.optional();
|
||||||
|
|
||||||
|
Ok(last_modified?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search_files(&self, term: &str, limit: u32) -> Result<Vec<IndexedFile>, AppError> {
|
||||||
|
let db = self.db.lock().unwrap();
|
||||||
|
let mut stmt = db.prepare(
|
||||||
|
"SELECT t1.path, t1.name, t1.parent_path, t1.file_type, t1.last_modified
|
||||||
|
FROM file_index t1 JOIN file_index_fts t2 ON t1.rowid = t2.rowid
|
||||||
|
WHERE t2.name MATCH ?1
|
||||||
|
ORDER BY t1.last_modified DESC
|
||||||
|
LIMIT ?2",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let search_term = format!("\"{}\"*", term);
|
||||||
|
let files_iter = stmt.query_map(params![search_term, limit], |row| {
|
||||||
|
Ok(IndexedFile {
|
||||||
|
path: row.get(0)?,
|
||||||
|
name: row.get(1)?,
|
||||||
|
parent_path: row.get(2)?,
|
||||||
|
file_type: row.get(3)?,
|
||||||
|
last_modified: row.get(4)?,
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
files_iter
|
||||||
|
.collect::<RusqliteResult<Vec<_>>>()
|
||||||
|
.map_err(|e| e.into())
|
||||||
|
}
|
||||||
|
}
|
44
src-tauri/src/file_search/mod.rs
Normal file
44
src-tauri/src/file_search/mod.rs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
pub mod indexer;
|
||||||
|
pub mod manager;
|
||||||
|
pub mod types;
|
||||||
|
pub mod watcher;
|
||||||
|
|
||||||
|
use tauri::{AppHandle, Manager, State};
|
||||||
|
use manager::FileSearchManager;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn search_files(
|
||||||
|
term: String,
|
||||||
|
manager: State<FileSearchManager>,
|
||||||
|
) -> Result<Vec<types::IndexedFile>, String> {
|
||||||
|
manager.search_files(&term, 100).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(app_handle: AppHandle) {
|
||||||
|
let file_search_manager = match FileSearchManager::new(app_handle.clone()) {
|
||||||
|
Ok(manager) => manager,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to create FileSearchManager: {:?}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = file_search_manager.init_db() {
|
||||||
|
eprintln!("Failed to initialize file search database: {:?}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
app_handle.manage(file_search_manager);
|
||||||
|
|
||||||
|
let indexer_handle = app_handle.clone();
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
indexer::build_initial_index(indexer_handle).await;
|
||||||
|
});
|
||||||
|
|
||||||
|
let watcher_handle = app_handle.clone();
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
if let Err(e) = watcher::start_watching(watcher_handle).await {
|
||||||
|
eprintln!("Failed to start file watcher: {:?}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
11
src-tauri/src/file_search/types.rs
Normal file
11
src-tauri/src/file_search/types.rs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct IndexedFile {
|
||||||
|
pub path: String,
|
||||||
|
pub name: String,
|
||||||
|
pub parent_path: String,
|
||||||
|
pub file_type: String, // "file", "directory"
|
||||||
|
pub last_modified: i64, // unix timestamp
|
||||||
|
}
|
97
src-tauri/src/file_search/watcher.rs
Normal file
97
src-tauri/src/file_search/watcher.rs
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
use super::{manager::FileSearchManager, types::IndexedFile};
|
||||||
|
use crate::error::AppError;
|
||||||
|
use notify::{RecursiveMode, Watcher};
|
||||||
|
use notify_debouncer_full::{new_debouncer, DebounceEventResult, DebouncedEvent};
|
||||||
|
use std::{
|
||||||
|
env,
|
||||||
|
path::PathBuf,
|
||||||
|
time::{Duration, SystemTime},
|
||||||
|
};
|
||||||
|
use tauri::{AppHandle, Manager};
|
||||||
|
|
||||||
|
async fn handle_event(app_handle: AppHandle, debounced_event: DebouncedEvent) {
|
||||||
|
let manager = app_handle.state::<FileSearchManager>();
|
||||||
|
let path = &debounced_event.event.paths[0];
|
||||||
|
|
||||||
|
if path.exists() {
|
||||||
|
if let Ok(metadata) = path.metadata() {
|
||||||
|
let file_type = if metadata.is_dir() {
|
||||||
|
"directory".to_string()
|
||||||
|
} else {
|
||||||
|
"file".to_string()
|
||||||
|
};
|
||||||
|
let last_modified = metadata
|
||||||
|
.modified()
|
||||||
|
.unwrap_or(SystemTime::UNIX_EPOCH)
|
||||||
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs() as i64;
|
||||||
|
|
||||||
|
let indexed_file = IndexedFile {
|
||||||
|
path: path.to_string_lossy().to_string(),
|
||||||
|
name: path
|
||||||
|
.file_name()
|
||||||
|
.map(|s| s.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
parent_path: path
|
||||||
|
.parent()
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
file_type,
|
||||||
|
last_modified,
|
||||||
|
};
|
||||||
|
if let Err(e) = manager.add_file(&indexed_file) {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to add/update file in index: {:?}, path: {}",
|
||||||
|
e,
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Err(e) = manager.remove_file(&path.to_string_lossy()) {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to remove file from index: {:?}, path: {}",
|
||||||
|
e,
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start_watching(app_handle: AppHandle) -> Result<(), AppError> {
|
||||||
|
let home_dir = env::var("HOME").map_err(|e| AppError::FileSearch(e.to_string()))?;
|
||||||
|
let app_handle_clone = app_handle.clone();
|
||||||
|
|
||||||
|
let mut debouncer = new_debouncer(
|
||||||
|
Duration::from_secs(2),
|
||||||
|
None,
|
||||||
|
move |result: DebounceEventResult| {
|
||||||
|
let app_handle_clone2 = app_handle_clone.clone();
|
||||||
|
match result {
|
||||||
|
Ok(events) => {
|
||||||
|
for event in events {
|
||||||
|
tauri::async_runtime::spawn(handle_event(app_handle_clone2.clone(), event));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(errors) => {
|
||||||
|
for error in errors {
|
||||||
|
eprintln!("watch error: {:?}", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|e| AppError::FileSearch(e.to_string()))?;
|
||||||
|
|
||||||
|
debouncer
|
||||||
|
.watcher()
|
||||||
|
.watch(&PathBuf::from(&home_dir), RecursiveMode::Recursive)
|
||||||
|
.map_err(|e| AppError::FileSearch(e.to_string()))?;
|
||||||
|
|
||||||
|
debouncer
|
||||||
|
.cache()
|
||||||
|
.add_root(&PathBuf::from(&home_dir), RecursiveMode::Recursive);
|
||||||
|
|
||||||
|
app_handle.manage(debouncer);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ mod oauth;
|
||||||
mod quicklinks;
|
mod quicklinks;
|
||||||
mod snippets;
|
mod snippets;
|
||||||
mod system;
|
mod system;
|
||||||
|
mod file_search;
|
||||||
|
|
||||||
use crate::snippets::input_manager::{EvdevInputManager, InputManager};
|
use crate::snippets::input_manager::{EvdevInputManager, InputManager};
|
||||||
use crate::{app::App, cache::AppCache};
|
use crate::{app::App, cache::AppCache};
|
||||||
|
@ -140,7 +141,7 @@ fn setup_global_shortcut(app: &mut tauri::App) -> Result<(), Box<dyn std::error:
|
||||||
.with_handler(move |_app, shortcut, event| {
|
.with_handler(move |_app, shortcut, event| {
|
||||||
println!("Shortcut: {:?}, Event: {:?}", shortcut, event);
|
println!("Shortcut: {:?}, Event: {:?}", shortcut, event);
|
||||||
if shortcut == &spotlight_shortcut && event.state() == ShortcutState::Pressed {
|
if shortcut == &spotlight_shortcut && event.state() == ShortcutState::Pressed {
|
||||||
let spotlight_window = handle.get_webview_window("raycast-linux").unwrap();
|
let spotlight_window = handle.get_webview_window("main").unwrap();
|
||||||
println!("Spotlight window: {:?}", spotlight_window);
|
println!("Spotlight window: {:?}", spotlight_window);
|
||||||
if spotlight_window.is_visible().unwrap_or(false) {
|
if spotlight_window.is_visible().unwrap_or(false) {
|
||||||
spotlight_window.hide().unwrap();
|
spotlight_window.hide().unwrap();
|
||||||
|
@ -245,7 +246,8 @@ pub fn run() {
|
||||||
snippets::delete_snippet,
|
snippets::delete_snippet,
|
||||||
snippets::import_snippets,
|
snippets::import_snippets,
|
||||||
snippets::paste_snippet_content,
|
snippets::paste_snippet_content,
|
||||||
snippets::snippet_was_used
|
snippets::snippet_was_used,
|
||||||
|
file_search::search_files
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let app_handle = app.handle().clone();
|
let app_handle = app.handle().clone();
|
||||||
|
@ -265,6 +267,9 @@ pub fn run() {
|
||||||
snippet_manager.init_db()?;
|
snippet_manager.init_db()?;
|
||||||
app.manage(snippet_manager);
|
app.manage(snippet_manager);
|
||||||
|
|
||||||
|
let app_handle_for_file_search = app.handle().clone();
|
||||||
|
file_search::init(app_handle_for_file_search);
|
||||||
|
|
||||||
setup_background_refresh();
|
setup_background_refresh();
|
||||||
setup_global_shortcut(app)?;
|
setup_global_shortcut(app)?;
|
||||||
setup_input_listener(app.handle());
|
setup_input_listener(app.handle());
|
||||||
|
|
225
src/lib/components/FileSearchView.svelte
Normal file
225
src/lib/components/FileSearchView.svelte
Normal file
|
@ -0,0 +1,225 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { untrack } from 'svelte';
|
||||||
|
import { Input } from '$lib/components/ui/input';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { ArrowLeft, Trash, Loader2, Folder, File, Copy, ArrowUpRight, Eye } from '@lucide/svelte';
|
||||||
|
import ListItemBase from './nodes/shared/ListItemBase.svelte';
|
||||||
|
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
||||||
|
import { Kbd } from './ui/kbd';
|
||||||
|
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||||
|
import { shortcutToText } from '$lib/renderKey';
|
||||||
|
import ActionBar from './nodes/shared/ActionBar.svelte';
|
||||||
|
import ActionMenu from './nodes/shared/ActionMenu.svelte';
|
||||||
|
import BaseList from './BaseList.svelte';
|
||||||
|
import { open } from '@tauri-apps/plugin-shell';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onBack: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type IndexedFile = {
|
||||||
|
path: string;
|
||||||
|
name: string;
|
||||||
|
parentPath: string;
|
||||||
|
fileType: 'file' | 'directory';
|
||||||
|
lastModified: number; // unix timestamp
|
||||||
|
};
|
||||||
|
|
||||||
|
let { onBack }: Props = $props();
|
||||||
|
|
||||||
|
let searchResults = $state<IndexedFile[]>([]);
|
||||||
|
let selectedIndex = $state(0);
|
||||||
|
let searchText = $state('');
|
||||||
|
let isFetching = $state(false);
|
||||||
|
|
||||||
|
const selectedItem = $derived(searchResults[selectedIndex]);
|
||||||
|
|
||||||
|
const fetchFiles = async () => {
|
||||||
|
if (isFetching) return;
|
||||||
|
isFetching = true;
|
||||||
|
try {
|
||||||
|
const newItems = await invoke<IndexedFile[]>('search_files', {
|
||||||
|
term: searchText
|
||||||
|
});
|
||||||
|
searchResults = newItems;
|
||||||
|
if (selectedIndex >= newItems.length) {
|
||||||
|
selectedIndex = 0;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to fetch files:', e);
|
||||||
|
} finally {
|
||||||
|
isFetching = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDateTime = (timestamp: number) => {
|
||||||
|
const date = new Date(timestamp * 1000);
|
||||||
|
if (date.getFullYear() < 1971) return 'N/A';
|
||||||
|
return date.toLocaleString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpen = async (item: IndexedFile) => {
|
||||||
|
await open(item.path);
|
||||||
|
onBack();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShow = async (item: IndexedFile) => {
|
||||||
|
await invoke('show_in_finder', { path: item.path });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopyPath = async (item: IndexedFile) => {
|
||||||
|
await writeText(item.path);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (item: IndexedFile) => {
|
||||||
|
await invoke('trash', { paths: [item.path] });
|
||||||
|
fetchFiles();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeydown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
onBack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!selectedItem) return;
|
||||||
|
|
||||||
|
if (e.ctrlKey && e.key.toLowerCase() === 'c') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleCopyPath(selectedItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.ctrlKey && e.key.toLowerCase() === 'x') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleDelete(selectedItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.metaKey && e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleShow(selectedItem);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
const term = searchText;
|
||||||
|
if (!term) {
|
||||||
|
searchResults = [];
|
||||||
|
isFetching = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
untrack(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (term === searchText) {
|
||||||
|
fetchFiles();
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window onkeydown={handleKeydown} />
|
||||||
|
|
||||||
|
<main class="bg-background text-foreground flex h-screen flex-col">
|
||||||
|
<header class="flex h-12 shrink-0 items-center border-b px-2">
|
||||||
|
<Button variant="ghost" size="icon" onclick={onBack}>
|
||||||
|
<ArrowLeft class="size-5" />
|
||||||
|
</Button>
|
||||||
|
<Input
|
||||||
|
class="rounded-none border-none !bg-transparent pr-0"
|
||||||
|
placeholder="Search for files and folders..."
|
||||||
|
bind:value={searchText}
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
</header>
|
||||||
|
<div class="grid grow grid-cols-[minmax(0,_1.5fr)_minmax(0,_2.5fr)] overflow-y-hidden">
|
||||||
|
<div class="flex-grow overflow-y-auto border-r">
|
||||||
|
{#if isFetching && searchResults.length === 0}
|
||||||
|
<div class="text-muted-foreground flex h-full items-center justify-center">
|
||||||
|
<Loader2 class="size-6 animate-spin" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<BaseList
|
||||||
|
items={searchResults.map((item) => ({ ...item, id: item.path }))}
|
||||||
|
bind:selectedIndex
|
||||||
|
onenter={(item) => handleOpen(item)}
|
||||||
|
>
|
||||||
|
{#snippet itemSnippet({ item, isSelected, onclick })}
|
||||||
|
<button class="w-full text-left" {onclick}>
|
||||||
|
<ListItemBase
|
||||||
|
icon={item.fileType === 'directory' ? 'folder-16' : 'blank-document-16'}
|
||||||
|
title={item.name}
|
||||||
|
subtitle={item.parentPath}
|
||||||
|
{isSelected}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{/snippet}
|
||||||
|
</BaseList>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col overflow-y-hidden">
|
||||||
|
{#if selectedItem}
|
||||||
|
<div class="flex h-full flex-col items-center justify-center p-4">
|
||||||
|
<div class="mb-4">
|
||||||
|
{#if selectedItem.fileType === 'directory'}
|
||||||
|
<Folder class="size-24 text-gray-500" />
|
||||||
|
{:else}
|
||||||
|
<File class="size-24 text-gray-500" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<p class="text-xl font-semibold">{selectedItem.name}</p>
|
||||||
|
<p class="text-muted-foreground text-sm">{selectedItem.path}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t p-4">
|
||||||
|
<h3 class="text-muted-foreground mb-2 text-xs font-semibold uppercase">Information</h3>
|
||||||
|
<div class="flex flex-col gap-3 text-sm">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-muted-foreground">Type</span>
|
||||||
|
<span class="capitalize">{selectedItem.fileType}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-muted-foreground">Last Modified</span>
|
||||||
|
<span>{formatDateTime(selectedItem.lastModified)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ActionBar>
|
||||||
|
{#snippet primaryAction({ props })}
|
||||||
|
<Button {...props} onclick={() => handleOpen(selectedItem)}>
|
||||||
|
Open <Kbd>⏎</Kbd>
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet actions()}
|
||||||
|
<ActionMenu>
|
||||||
|
<DropdownMenu.Item onclick={() => handleShow(selectedItem)}>
|
||||||
|
<Eye class="mr-2 size-4" />
|
||||||
|
<span>Show in File Manager</span>
|
||||||
|
<DropdownMenu.Shortcut>
|
||||||
|
{shortcutToText({ key: 'Enter', modifiers: ['cmd'] })}
|
||||||
|
</DropdownMenu.Shortcut>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onclick={() => handleCopyPath(selectedItem)}>
|
||||||
|
<Copy class="mr-2 size-4" />
|
||||||
|
<span>Copy Path</span>
|
||||||
|
<DropdownMenu.Shortcut>
|
||||||
|
{shortcutToText({ key: 'c', modifiers: ['ctrl'] })}
|
||||||
|
</DropdownMenu.Shortcut>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
<DropdownMenu.Item class="text-red-500" onclick={() => handleDelete(selectedItem)}>
|
||||||
|
<Trash class="mr-2 size-4" />
|
||||||
|
<span>Move to Trash</span>
|
||||||
|
<DropdownMenu.Shortcut>
|
||||||
|
{shortcutToText({ key: 'x', modifiers: ['ctrl'] })}
|
||||||
|
</DropdownMenu.Shortcut>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</ActionMenu>
|
||||||
|
{/snippet}
|
||||||
|
</ActionBar>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
|
@ -12,7 +12,8 @@ export type ViewState =
|
||||||
| 'search-snippets'
|
| 'search-snippets'
|
||||||
| 'quicklink-form'
|
| 'quicklink-form'
|
||||||
| 'create-snippet-form'
|
| 'create-snippet-form'
|
||||||
| 'import-snippets';
|
| 'import-snippets'
|
||||||
|
| 'file-search';
|
||||||
|
|
||||||
type OauthState = {
|
type OauthState = {
|
||||||
url: string;
|
url: string;
|
||||||
|
@ -65,6 +66,10 @@ class ViewManager {
|
||||||
this.currentView = 'import-snippets';
|
this.currentView = 'import-snippets';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
showFileSearch = () => {
|
||||||
|
this.currentView = 'file-search';
|
||||||
|
};
|
||||||
|
|
||||||
runPlugin = (plugin: PluginInfo) => {
|
runPlugin = (plugin: PluginInfo) => {
|
||||||
switch (plugin.pluginPath) {
|
switch (plugin.pluginPath) {
|
||||||
case 'builtin:store':
|
case 'builtin:store':
|
||||||
|
@ -85,6 +90,9 @@ class ViewManager {
|
||||||
case 'builtin:import-snippets':
|
case 'builtin:import-snippets':
|
||||||
this.showImportSnippets();
|
this.showImportSnippets();
|
||||||
return;
|
return;
|
||||||
|
case 'builtin:file-search':
|
||||||
|
this.showFileSearch();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
uiStore.setCurrentRunningPlugin(plugin);
|
uiStore.setCurrentRunningPlugin(plugin);
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
import SnippetForm from '$lib/components/SnippetForm.svelte';
|
import SnippetForm from '$lib/components/SnippetForm.svelte';
|
||||||
import ImportSnippets from '$lib/components/ImportSnippets.svelte';
|
import ImportSnippets from '$lib/components/ImportSnippets.svelte';
|
||||||
import SearchSnippets from '$lib/components/SearchSnippets.svelte';
|
import SearchSnippets from '$lib/components/SearchSnippets.svelte';
|
||||||
|
import FileSearchView from '$lib/components/FileSearchView.svelte';
|
||||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||||
|
|
||||||
const storePlugin: PluginInfo = {
|
const storePlugin: PluginInfo = {
|
||||||
|
@ -90,6 +91,18 @@
|
||||||
mode: 'view'
|
mode: 'view'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fileSearchPlugin: PluginInfo = {
|
||||||
|
title: 'Search Files',
|
||||||
|
description: 'Find files and folders on your computer',
|
||||||
|
pluginTitle: 'File Search',
|
||||||
|
pluginName: 'File Search',
|
||||||
|
commandName: 'index',
|
||||||
|
pluginPath: 'builtin:file-search',
|
||||||
|
icon: 'search-16',
|
||||||
|
preferences: [],
|
||||||
|
mode: 'view'
|
||||||
|
};
|
||||||
|
|
||||||
const { pluginList, currentPreferences } = $derived(uiStore);
|
const { pluginList, currentPreferences } = $derived(uiStore);
|
||||||
const allPlugins = $derived([
|
const allPlugins = $derived([
|
||||||
...pluginList,
|
...pluginList,
|
||||||
|
@ -98,7 +111,8 @@
|
||||||
searchSnippetsPlugin,
|
searchSnippetsPlugin,
|
||||||
createQuicklinkPlugin,
|
createQuicklinkPlugin,
|
||||||
createSnippetPlugin,
|
createSnippetPlugin,
|
||||||
importSnippetsPlugin
|
importSnippetsPlugin,
|
||||||
|
fileSearchPlugin
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { currentView, oauthState, oauthStatus, quicklinkToEdit, snippetsForImport } =
|
const { currentView, oauthState, oauthStatus, quicklinkToEdit, snippetsForImport } =
|
||||||
|
@ -222,4 +236,6 @@
|
||||||
<SnippetForm onBack={viewManager.showCommandPalette} onSave={viewManager.showCommandPalette} />
|
<SnippetForm onBack={viewManager.showCommandPalette} onSave={viewManager.showCommandPalette} />
|
||||||
{:else if currentView === 'import-snippets'}
|
{:else if currentView === 'import-snippets'}
|
||||||
<ImportSnippets onBack={viewManager.showCommandPalette} snippetsToImport={snippetsForImport} />
|
<ImportSnippets onBack={viewManager.showCommandPalette} snippetsToImport={snippetsForImport} />
|
||||||
|
{:else if currentView === 'file-search'}
|
||||||
|
<FileSearchView onBack={viewManager.showCommandPalette} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue