diff --git a/Cargo.lock b/Cargo.lock index 7cabb2d..699f2ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -436,6 +436,12 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "fnv" version = "1.0.7" @@ -1026,9 +1032,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f79496a5651c8d57cd033c5add8ca7ee4e3d5f7587a4777484640d9cb60392d9" dependencies = [ "fnv", - "nom", + "nom 1.2.4", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.7" @@ -1055,6 +1067,16 @@ version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1121,6 +1143,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.9.0", +] + [[package]] name = "phf" version = "0.11.3" @@ -1555,6 +1587,19 @@ dependencies = [ "winnow", ] +[[package]] +name = "tree_magic_mini" +version = "3.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac5e8971f245c3389a5a76e648bfc80803ae066a1243a75db0064d7c1129d63" +dependencies = [ + "fnv", + "memchr", + "nom 7.1.3", + "once_cell", + "petgraph", +] + [[package]] name = "unicode-ident" version = "1.0.18" @@ -1755,6 +1800,7 @@ dependencies = [ "strsim 0.11.1", "thiserror", "toml", + "tree_magic_mini", "which", ] diff --git a/Cargo.toml b/Cargo.toml index 42dfd2b..9fa4efd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,3 +43,4 @@ strsim = "0.11.1" dirs = "6.0.0" which = "7.0.3" meval = "0.2.0" +tree_magic_mini = "3.1.6" diff --git a/README.md b/README.md index 750f593..689ceab 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ -# Worf +# Worf - Wayland Optimized Run Facilitator + +Worf is yet another style launcher, heavily inspired by wofi, rofi and walker. +Worf is written in Rust on top of GTK4. + +It aims to be a drop in replacement for wofi in most part, so it is (almost) compatible with its +configuration and css files. See below for differences + -Worf is yet another dmenu style launcher, heavily inspired by wofi but written in Rust on top of GTK4. -It supports a lot of things the same way wofi does, so migrating to worf is easy, but things I did not -deemed necessary where dropped from worf. See breaking changes section for details. ## Setup @@ -37,7 +41,7 @@ layerrule = blur, worf ## Dropped arguments * `mode`, use show -* `D`, arguments are the same as config in worf, no need to have have this flag. +* `D`, arguments are the same as config in worf, no need to have this flag. ### Dropped configuration options * stylesheet -> use style instead diff --git a/src/lib/config.rs b/src/lib/config.rs index c77310c..8fc6068 100644 --- a/src/lib/config.rs +++ b/src/lib/config.rs @@ -64,6 +64,9 @@ pub enum Mode { /// tries to determine automatically what to do Auto, + + /// use worf as file browser + File, } #[derive(Debug, Error)] @@ -80,6 +83,7 @@ impl FromStr for Mode { "run" => Ok(Mode::Run), "drun" => Ok(Mode::Drun), "dmenu" => Ok(Mode::Dmenu), + "file" => Ok(Mode::File), "auto" => Ok(Mode::Auto), _ => Err(ArgsError::InvalidParameter( format!("{s} is not a valid argument show this, see help for details").to_owned(), @@ -128,21 +132,16 @@ pub struct Config { #[clap(long = "height")] pub height: Option, + /// Defines which prompt is used. Default is selected 'show' #[clap(short = 'p', long = "prompt")] pub prompt: Option, #[clap(short = 'x', long = "xoffset")] pub xoffset: Option, - #[clap(long = "x")] - pub x: Option, - #[clap(short = 'y', long = "yoffset")] pub yoffset: Option, - #[clap(long = "y")] - pub y: Option, - /// If true a normal window instead of a layer shell will be used #[serde(default = "default_normal_window")] #[clap(short = 'n', long = "normal-window")] @@ -315,9 +314,7 @@ impl Default for Config { height: default_height(), prompt: None, xoffset: None, - x: None, yoffset: None, - y: None, normal_window: default_normal_window(), allow_images: None, allow_markup: None, diff --git a/src/lib/gui.rs b/src/lib/gui.rs index 9f36531..15cdae1 100644 --- a/src/lib/gui.rs +++ b/src/lib/gui.rs @@ -135,7 +135,7 @@ fn build_ui( } /// todo make this configurable - window.set_anchor(Edge::Top, true); + //window.set_anchor(Edge::Top, true); let outer_box = gtk4::Box::new(config.orientation.unwrap().into(), 0); outer_box.set_widget_name("outer-box"); @@ -944,7 +944,7 @@ fn percent_or_absolute(value: Option<&String>, base_value: i32) -> Option { // highly unlikely that we are dealing with > i64 items #[allow(clippy::cast_possible_wrap)] -pub fn initialize_sort_scores(items: &mut [MenuItem]) { +pub fn sort_menu_items_alphabetically_honor_initial_score(items: &mut [MenuItem]) { let mut regular_score = items.len() as i64; items.sort_by(|l, r| l.label.cmp(&r.label)); diff --git a/src/lib/mode.rs b/src/lib/mode.rs index 9973c7c..d8b993b 100644 --- a/src/lib/mode.rs +++ b/src/lib/mode.rs @@ -124,7 +124,7 @@ impl DRunProvider { entries.push(entry); } - gui::initialize_sort_scores(&mut entries); + gui::sort_menu_items_alphabetically_honor_initial_score(&mut entries); DRunProvider { items: entries, @@ -134,7 +134,7 @@ impl DRunProvider { } } -impl ItemProvider for DRunProvider { +impl ItemProvider for DRunProvider { fn get_elements(&mut self, _: Option<&str>) -> Vec> { self.items.clone() } @@ -144,50 +144,79 @@ impl ItemProvider for DRunProvider { } } -#[derive(Debug, Clone, PartialEq)] -enum AutoRunType { - Math, - DRun, - File, - Ssh, - WebSearch, - Emoji, - Run, -} - #[derive(Clone)] -struct AutoItemProvider { - drun_provider: DRunProvider, - last_result: Option>>, +struct FileItemProvider { + last_result: Option>>, + menu_item_data: T, } -impl AutoItemProvider { - fn new() -> Self { - AutoItemProvider { - drun_provider: DRunProvider::new(AutoRunType::DRun), - +impl FileItemProvider { + fn new(menu_item_data: T) -> Self { + FileItemProvider { last_result: None, + menu_item_data, } } - fn auto_run_handle_files(&mut self, trimmed_search: &str) -> Vec> { - let folder_icon = "inode-directory"; + fn resolve_icon_for_name(&self, path: PathBuf) -> String { + let result = tree_magic_mini::from_filepath(&path); + if let Some(result) = result { + if result.starts_with("image") { + "image-x-generic".to_owned() + } else if result.starts_with("inode") { + return result.replace("/", "-"); + } else if result.starts_with("text") { + if result.contains("plain") { + "text-x-generic".to_owned() + } else if result.contains("python") { + "text-x-script".to_owned() + } else if result.contains("html") { + return "text-html".to_owned(); + } else { + "text-x-generic".to_owned() + } + } else if result.starts_with("application") { + if result.contains("octet") { + "application-x-executable".to_owned() + } else if result.contains("tar") + || result.contains("lz") + || result.contains("zip") + || result.contains("7z") + || result.contains("xz") + { + "package-x-generic".to_owned() + } else { + return "text-html".to_owned(); + } + } else { + log::debug!("unsupported mime type {result}"); + return "application-x-generic".to_owned(); + } + } else { + "image-not-found".to_string() + } + } +} - let path = config::expand_path(trimmed_search); - let mut items: Vec> = Vec::new(); +impl ItemProvider for FileItemProvider { + fn get_elements(&mut self, search: Option<&str>) -> Vec> { + let default_path = if let Some(home) = dirs::home_dir() { + home.display().to_string() + } else { + "/".to_string() + }; + + let mut trimmed_search = search.unwrap_or(&default_path).to_owned(); + if !trimmed_search.starts_with("/") && !trimmed_search.starts_with("~") { + trimmed_search = format!("{default_path}/{trimmed_search}"); + } + + let path = expand_path(&trimmed_search); + let mut items: Vec> = Vec::new(); if !path.exists() { if let Some(last) = &self.last_result { - if !last.is_empty() - && last.first().is_some_and(|l| { - l.as_ref() - .data - .as_ref() - .is_some_and(|t| t == &AutoRunType::File) - }) - { - return last.clone(); - } + return last.clone(); } return vec![]; @@ -210,17 +239,13 @@ impl AutoItemProvider { items.push({ MenuItem { label: path_str.clone(), - icon_path: if entry.path().is_dir() { - Some(folder_icon.to_owned()) - } else { - Some(resolve_icon_for_name(entry.path())) - }, + icon_path: Some(self.resolve_icon_for_name(entry.path())), action: Some(format!("xdg-open {path_str}")), sub_elements: vec![], working_dir: None, initial_sort_score: 0, search_sort_score: 0.0, - data: Some(AutoRunType::File), + data: Some(self.menu_item_data.clone()), } }); } @@ -229,67 +254,54 @@ impl AutoItemProvider { items.push({ MenuItem { label: trimmed_search.to_owned(), - icon_path: Some(resolve_icon_for_name(PathBuf::from(trimmed_search))), + icon_path: Some(self.resolve_icon_for_name(PathBuf::from(&trimmed_search))), action: Some(format!("xdg-open {trimmed_search}")), sub_elements: vec![], working_dir: None, initial_sort_score: 0, search_sort_score: 0.0, - data: Some(AutoRunType::File), + data: Some(self.menu_item_data.clone()), } }); } + gui::sort_menu_items_alphabetically_honor_initial_score(&mut items); + self.last_result = Some(items.clone()); items } + + fn get_sub_elements(&mut self, _: &MenuItem) -> Option>> { + self.last_result.clone() + } } -fn resolve_icon_for_name(path: PathBuf) -> String { - // todo use https://docs.rs/tree_magic_mini/latest/tree_magic_mini/ instead - if let Ok(metadata) = fs::symlink_metadata(&path) { - if metadata.file_type().is_symlink() { - return "inode-symlink".to_owned(); - } else if metadata.is_dir() { - return "inode-directory".to_owned(); - } else if metadata.permissions().mode() & 0o111 != 0 { - return "application-x-executable".to_owned(); +#[derive(Debug, Clone, PartialEq)] +enum AutoRunType { + Math, + DRun, + File, + Ssh, + WebSearch, + Emoji, + Run, +} + +#[derive(Clone)] +struct AutoItemProvider { + drun_provider: DRunProvider, + file_provider: FileItemProvider, + last_result: Option>>, +} + +impl AutoItemProvider { + fn new() -> Self { + AutoItemProvider { + drun_provider: DRunProvider::new(AutoRunType::DRun), + file_provider: FileItemProvider::new(AutoRunType::File), + last_result: None, } } - - let file_name = path - .file_name() - .and_then(|f| f.to_str()) - .unwrap_or("") - .to_lowercase(); - - let extension = path - .extension() - .and_then(|e| e.to_str()) - .unwrap_or("") - .to_lowercase(); - - match extension.as_str() { - "sh" | "py" | "rb" | "pl" | "bash" => "text-x-script".to_owned(), - "c" | "cpp" | "rs" | "java" | "js" | "h" | "hpp" => "text-x-generic".to_owned(), - "txt" | "md" | "log" => "text-x-generic".to_owned(), - "html" | "htm" => "text-html".to_owned(), - "jpg" | "jpeg" | "png" | "gif" | "svg" | "webp" => "image-x-generic".to_owned(), - "mp3" | "wav" | "ogg" => "audio-x-generic".to_owned(), - "mp4" | "mkv" | "avi" => "video-x-generic".to_owned(), - "ttf" | "otf" | "woff" => "font-x-generic".to_owned(), - "zip" | "tar" | "gz" | "xz" | "7z" | "lz4" => "package-x-generic".to_owned(), - "deb" | "rpm" | "apk" => "x-package-repository".to_owned(), - "odt" => "x-office-document".to_owned(), - "ott" => "x-office-document-template".to_owned(), - "ods" => "x-office-spreadsheet".to_owned(), - "ots" => "x-office-spreadsheet-template".to_owned(), - "odp" => "x-office-presentation".to_owned(), - "otp" => "x-office-presentation-template".to_owned(), - "odg" => "x-office-drawing".to_owned(), - "vcf" => "x-office-addressbook".to_owned(), - _ => "application-x-generic".to_owned(), - } } fn contains_math_functions_or_starts_with_number(input: &str) -> bool { @@ -320,7 +332,7 @@ impl ItemProvider for AutoItemProvider { let item = MenuItem { label: result, icon_path: None, - action: None, + action: Some(trimmed_search.to_owned()), sub_elements: vec![], working_dir: None, initial_sort_score: 0, @@ -333,7 +345,7 @@ impl ItemProvider for AutoItemProvider { || trimmed_search.starts_with("/") || trimmed_search.starts_with("~") { - self.auto_run_handle_files(trimmed_search) + self.file_provider.get_elements(search_opt) } else { return self.drun_provider.get_elements(search_opt); } @@ -414,6 +426,28 @@ pub fn auto(config: &mut Config) -> anyhow::Result<()> { Ok(()) } +pub fn file(config: &mut Config) -> Result<(), String> { + let provider = FileItemProvider::new("".to_owned()); + if config.prompt.is_none() { + config.prompt = Some("file".to_owned()); + } + + // todo ues a arc instead of cloning the config + let selection_result = gui::show(config.clone(), provider); + match selection_result { + Ok(s) => { + if let Some(action) = s.action { + spawn_fork(&action, s.working_dir.as_ref()).map_err(|e| e.to_string())?; + } + } + Err(_) => { + log::error!("No item selected"); + } + } + + Ok(()) +} + fn update_drun_cache_and_run( cache_path: Option, cache: &mut HashMap, diff --git a/src/main.rs b/src/main.rs index 4b7bd37..196264b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,6 +26,9 @@ fn main() -> anyhow::Result<()> { Mode::Dmenu => { todo!("dmenu not implemented") } + Mode::File => { + mode::file(&mut config).map_err(|e| anyhow!(e))?; + } Mode::Auto => { mode::auto(&mut config)?; }