244 lines
7.2 KiB
Rust
244 lines
7.2 KiB
Rust
use directories::UserDirs;
|
|
use gio::prelude::FileExt;
|
|
use glib::object::Cast;
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::Command;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
use tokio::sync::Mutex;
|
|
use walkdir::{DirEntry, WalkDir};
|
|
use waycast_macros::{plugin, launcher_entry};
|
|
|
|
use waycast_core::{LaunchError, LauncherListItem, LauncherPlugin};
|
|
|
|
#[derive(Clone)]
|
|
struct FileEntry {
|
|
path: PathBuf,
|
|
}
|
|
|
|
impl FileEntry {
|
|
fn from(entry: DirEntry) -> Self {
|
|
return FileEntry {
|
|
path: entry.into_path(),
|
|
};
|
|
}
|
|
}
|
|
|
|
impl LauncherListItem for FileEntry {
|
|
launcher_entry! {
|
|
id: self.path.to_string_lossy().to_string(),
|
|
title: String::from(self.path.file_name().unwrap().to_string_lossy()),
|
|
description: Some(self.path.to_string_lossy().to_string()),
|
|
icon: {
|
|
let (content_type, _) = gio::content_type_guess(Some(&self.path), None);
|
|
let icon = gio::content_type_get_icon(&content_type);
|
|
if let Some(themed_icon) = icon.downcast_ref::<gio::ThemedIcon>() {
|
|
if let Some(icon_name) = themed_icon.names().first() {
|
|
return icon_name.to_string();
|
|
}
|
|
}
|
|
String::from("text-x-generic")
|
|
},
|
|
execute: {
|
|
println!("Executing: {}", self.path.display());
|
|
|
|
// Use xdg-open directly since it works properly with music files
|
|
match Command::new("xdg-open").arg(&self.path).spawn() {
|
|
Ok(_) => {
|
|
println!("Successfully launched with xdg-open");
|
|
Ok(())
|
|
}
|
|
Err(e) => {
|
|
println!("xdg-open failed: {}", e);
|
|
// Fallback to GIO method
|
|
let file_gio = gio::File::for_path(&self.path);
|
|
let ctx = gio::AppLaunchContext::new();
|
|
match gio::AppInfo::launch_default_for_uri(file_gio.uri().as_str(), Some(&ctx)) {
|
|
Ok(()) => {
|
|
println!("Successfully launched with GIO fallback");
|
|
Ok(())
|
|
}
|
|
Err(e2) => {
|
|
println!("GIO fallback also failed: {}", e2);
|
|
Err(LaunchError::CouldNotLaunch(format!(
|
|
"Both xdg-open and GIO failed: {} / {}",
|
|
e, e2
|
|
)))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn default_search_list() -> Vec<PathBuf> {
|
|
if let Some(ud) = UserDirs::new() {
|
|
let mut paths: Vec<PathBuf> = Vec::new();
|
|
let user_dirs = [
|
|
ud.document_dir(),
|
|
ud.picture_dir(),
|
|
ud.audio_dir(),
|
|
ud.video_dir(),
|
|
];
|
|
|
|
for d in user_dirs {
|
|
if let Some(path) = d {
|
|
paths.push(path.to_path_buf());
|
|
}
|
|
}
|
|
|
|
return paths;
|
|
}
|
|
|
|
Vec::new()
|
|
}
|
|
|
|
pub struct FileSearchPlugin {
|
|
search_paths: Vec<PathBuf>,
|
|
skip_dirs: Vec<String>,
|
|
// Running list of files in memory
|
|
files: Arc<Mutex<Vec<FileEntry>>>,
|
|
}
|
|
|
|
impl FileSearchPlugin {
|
|
pub fn new() -> Self {
|
|
FileSearchPlugin {
|
|
search_paths: default_search_list(),
|
|
skip_dirs: vec![
|
|
String::from("vendor"),
|
|
String::from("node_modules"),
|
|
String::from("cache"),
|
|
String::from("zig-cache"),
|
|
],
|
|
files: Arc::new(Mutex::new(Vec::new())),
|
|
}
|
|
}
|
|
|
|
pub fn add_search_path<P: AsRef<Path>>(&mut self, path: P) -> Result<(), String> {
|
|
let p = path.as_ref();
|
|
|
|
if !p.exists() {
|
|
return Err(format!("Path does not exist: {}", p.display()));
|
|
}
|
|
|
|
if !p.is_dir() {
|
|
return Err(format!("Path is not a directory: {}", p.display()));
|
|
}
|
|
|
|
self.search_paths.push(p.to_path_buf());
|
|
Ok(())
|
|
}
|
|
|
|
pub fn add_skip_dir(&mut self, directory_name: String) -> Result<(), String> {
|
|
self.skip_dirs.push(directory_name);
|
|
Ok(())
|
|
}
|
|
|
|
async fn init_with_timeout(&self, timeout: Duration) {
|
|
let files_clone = Arc::clone(&self.files);
|
|
let skip_dirs_clone = self.skip_dirs.clone();
|
|
|
|
let scan_task = async move {
|
|
let mut local_files = Vec::new();
|
|
|
|
for path in &self.search_paths {
|
|
let walker = WalkDir::new(path).into_iter();
|
|
for entry in walker
|
|
.filter_entry(|e| !skip_hidden(e) && !skip_dir(e, &skip_dirs_clone))
|
|
.filter_map(|e| e.ok())
|
|
{
|
|
if entry.path().is_file() {
|
|
local_files.push(FileEntry::from(entry));
|
|
}
|
|
|
|
// Yield control periodically to check for timeout
|
|
if local_files.len() % 1000 == 0 {
|
|
tokio::task::yield_now().await;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update the shared files collection
|
|
let mut files_guard = files_clone.lock().await;
|
|
*files_guard = local_files;
|
|
};
|
|
|
|
// Run the scan with a timeout
|
|
if let Err(_) = tokio::time::timeout(timeout, scan_task).await {
|
|
eprintln!("File indexing timed out after {:?}", timeout);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn skip_hidden(entry: &DirEntry) -> bool {
|
|
entry
|
|
.file_name()
|
|
.to_str()
|
|
.map(|s| s.starts_with("."))
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
fn skip_dir(entry: &DirEntry, dirs: &Vec<String>) -> bool {
|
|
entry
|
|
.file_name()
|
|
.to_str()
|
|
.map(|n| dirs.contains(&String::from(n)))
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
impl LauncherPlugin for FileSearchPlugin {
|
|
plugin! {
|
|
name: "Files",
|
|
priority: 500,
|
|
description: "Search and open files",
|
|
prefix: "f"
|
|
}
|
|
|
|
fn init(&self) {
|
|
// Start async file scanning with 2000ms timeout
|
|
let self_clone = FileSearchPlugin {
|
|
search_paths: self.search_paths.clone(),
|
|
skip_dirs: self.skip_dirs.clone(),
|
|
files: Arc::clone(&self.files),
|
|
};
|
|
|
|
std::thread::spawn(move || {
|
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
|
rt.block_on(async {
|
|
self_clone
|
|
.init_with_timeout(Duration::from_millis(2000))
|
|
.await;
|
|
});
|
|
});
|
|
}
|
|
|
|
fn filter(&self, query: &str) -> Vec<Box<dyn LauncherListItem>> {
|
|
if query.is_empty() {
|
|
return self.default_list();
|
|
}
|
|
|
|
let mut entries: Vec<Box<dyn LauncherListItem>> = Vec::new();
|
|
|
|
// Try to get files without blocking - if indexing is still in progress, return empty
|
|
if let Ok(files) = self.files.try_lock() {
|
|
for f in files.iter() {
|
|
if let Some(file_name) = f.path.file_name() {
|
|
let cmp = file_name.to_string_lossy().to_lowercase();
|
|
if cmp.contains(&query.to_lowercase()) {
|
|
entries.push(Box::new(f.clone()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
entries
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn new() -> FileSearchPlugin {
|
|
FileSearchPlugin::new()
|
|
} |