use clap::Parser; use color_eyre::eyre; use fuzzy_matcher::skim::SkimMatcherV2; use itertools::Itertools; use std::{ collections::HashMap, fs::ReadDir, future::pending, path::PathBuf, process::{Command, Stdio}, }; use zbus::{connection, interface, zvariant::OwnedValue}; type Match = ( String, String, String, i32, f64, HashMap, ); struct Runner { editor_bin: String, project_path: PathBuf, } impl Runner { fn filter_dirs(dirs: ReadDir, query: &str) -> Vec { if query.len() < 3 { return vec![]; } let matcher = SkimMatcherV2::default(); let min_score = (query.len() as i64) * 20; dirs.filter_map(|dir| { dir.map(|dir| (dir.path(), dir.file_name().to_str().unwrap().to_string())) .ok() }) .filter_map(|(_path, name)| { matcher .fuzzy(&name, &query.replace(" ", ""), false) .map(|some| (some, _path, name)) }) .filter(|((score, _), _, _)| score >= &min_score) .sorted_by(|(fuzzy_a, _, _), (fuzzy_b, _, _)| fuzzy_a.0.cmp(&fuzzy_b.0).reverse()) .map(|(_, path, name)| { ( path.to_str().unwrap().to_string(), format!("Open Project {}", Self::to_display(&name)), "zed".to_string(), 100, 0.0, HashMap::new(), ) }) .collect() } fn to_display(name: &str) -> String { let mut words: Vec = Vec::new(); let mut current = String::new(); let mut prev: Option = None; for c in name.chars() { if c == '-' || c == '_' || c.is_whitespace() { if !current.is_empty() { words.push(std::mem::take(&mut current)); } } else { if c.is_uppercase() && prev.is_some_and(|p| p.is_lowercase() || p.is_ascii_digit()) && !current.is_empty() { words.push(std::mem::take(&mut current)); } current.push(c); } prev = Some(c); } if !current.is_empty() { words.push(current); } words .iter() .map(|w| { let mut chars = w.chars(); match chars.next() { Some(first) => { first.to_uppercase().collect::() + &chars.as_str().to_lowercase() } None => String::new(), } }) .collect::>() .join(" ") } } #[interface(name = "org.kde.krunner1")] impl Runner { #[zbus(name = "Actions")] fn do_actions(&self) -> Vec<(String, String, String)> { vec![] } #[zbus(name = "Run")] fn do_run(&self, match_id: &str, _action_id: &str) { println!("starting {} in {}", self.editor_bin, match_id); let res = Command::new("systemd-run") .args(["--user", "--collect", "--quiet", "-p", "KillMode=process"]) .arg(&self.editor_bin) .arg(match_id) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn(); if let Err(e) = res { eprintln!("{}", e); } } #[zbus(name = "Match")] fn do_match(&self, query: &str) -> Vec { self.project_path .read_dir() .map(|dirs| Self::filter_dirs(dirs, query)) .unwrap_or(vec![]) } } #[derive(Parser)] #[command(version, about, long_about = None)] struct Cli { #[arg(long)] editor_bin: Option, #[arg(long)] projects_path: Option, } #[tokio::main] async fn main() -> eyre::Result<()> { let cli = Cli::parse(); let _conn = connection::Builder::session()? .name("dev.tobinio.ProjectRunner")? .serve_at( "/dev/tobinio/ProjectRunner", Runner { editor_bin: cli.editor_bin.unwrap_or("zeditor".to_string()), project_path: cli .projects_path .or_else(|| dirs::home_dir().map(|h| h.join("Projects"))) .ok_or_else(|| eyre::eyre!("could not determine home directory"))?, }, )? .build() .await?; pending::<()>().await; Ok(()) }