file can now be used as standalone mode

This commit is contained in:
Alexander Mohr 2025-04-21 16:23:22 +02:00
parent f558131233
commit d0b526fb9d
7 changed files with 189 additions and 104 deletions

48
Cargo.lock generated
View file

@ -436,6 +436,12 @@ dependencies = [
"rustc_version", "rustc_version",
] ]
[[package]]
name = "fixedbitset"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@ -1026,9 +1032,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f79496a5651c8d57cd033c5add8ca7ee4e3d5f7587a4777484640d9cb60392d9" checksum = "f79496a5651c8d57cd033c5add8ca7ee4e3d5f7587a4777484640d9cb60392d9"
dependencies = [ dependencies = [
"fnv", "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]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.7" version = "0.8.7"
@ -1055,6 +1067,16 @@ version = "1.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce" 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]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@ -1121,6 +1143,16 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 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]] [[package]]
name = "phf" name = "phf"
version = "0.11.3" version = "0.11.3"
@ -1555,6 +1587,19 @@ dependencies = [
"winnow", "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]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.18" version = "1.0.18"
@ -1755,6 +1800,7 @@ dependencies = [
"strsim 0.11.1", "strsim 0.11.1",
"thiserror", "thiserror",
"toml", "toml",
"tree_magic_mini",
"which", "which",
] ]

View file

@ -43,3 +43,4 @@ strsim = "0.11.1"
dirs = "6.0.0" dirs = "6.0.0"
which = "7.0.3" which = "7.0.3"
meval = "0.2.0" meval = "0.2.0"
tree_magic_mini = "3.1.6"

View file

@ -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 ## Setup
@ -37,7 +41,7 @@ layerrule = blur, worf
## Dropped arguments ## Dropped arguments
* `mode`, use show * `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 ### Dropped configuration options
* stylesheet -> use style instead * stylesheet -> use style instead

View file

@ -64,6 +64,9 @@ pub enum Mode {
/// tries to determine automatically what to do /// tries to determine automatically what to do
Auto, Auto,
/// use worf as file browser
File,
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]
@ -80,6 +83,7 @@ impl FromStr for Mode {
"run" => Ok(Mode::Run), "run" => Ok(Mode::Run),
"drun" => Ok(Mode::Drun), "drun" => Ok(Mode::Drun),
"dmenu" => Ok(Mode::Dmenu), "dmenu" => Ok(Mode::Dmenu),
"file" => Ok(Mode::File),
"auto" => Ok(Mode::Auto), "auto" => Ok(Mode::Auto),
_ => Err(ArgsError::InvalidParameter( _ => Err(ArgsError::InvalidParameter(
format!("{s} is not a valid argument show this, see help for details").to_owned(), 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")] #[clap(long = "height")]
pub height: Option<String>, pub height: Option<String>,
/// Defines which prompt is used. Default is selected 'show'
#[clap(short = 'p', long = "prompt")] #[clap(short = 'p', long = "prompt")]
pub prompt: Option<String>, pub prompt: Option<String>,
#[clap(short = 'x', long = "xoffset")] #[clap(short = 'x', long = "xoffset")]
pub xoffset: Option<i32>, pub xoffset: Option<i32>,
#[clap(long = "x")]
pub x: Option<i32>,
#[clap(short = 'y', long = "yoffset")] #[clap(short = 'y', long = "yoffset")]
pub yoffset: Option<i32>, pub yoffset: Option<i32>,
#[clap(long = "y")]
pub y: Option<i32>,
/// If true a normal window instead of a layer shell will be used /// If true a normal window instead of a layer shell will be used
#[serde(default = "default_normal_window")] #[serde(default = "default_normal_window")]
#[clap(short = 'n', long = "normal-window")] #[clap(short = 'n', long = "normal-window")]
@ -315,9 +314,7 @@ impl Default for Config {
height: default_height(), height: default_height(),
prompt: None, prompt: None,
xoffset: None, xoffset: None,
x: None,
yoffset: None, yoffset: None,
y: None,
normal_window: default_normal_window(), normal_window: default_normal_window(),
allow_images: None, allow_images: None,
allow_markup: None, allow_markup: None,

View file

@ -135,7 +135,7 @@ fn build_ui<T, P>(
} }
/// todo make this configurable /// 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); let outer_box = gtk4::Box::new(config.orientation.unwrap().into(), 0);
outer_box.set_widget_name("outer-box"); outer_box.set_widget_name("outer-box");
@ -944,7 +944,7 @@ fn percent_or_absolute(value: Option<&String>, base_value: i32) -> Option<i32> {
// highly unlikely that we are dealing with > i64 items // highly unlikely that we are dealing with > i64 items
#[allow(clippy::cast_possible_wrap)] #[allow(clippy::cast_possible_wrap)]
pub fn initialize_sort_scores<T: std::clone::Clone>(items: &mut [MenuItem<T>]) { pub fn sort_menu_items_alphabetically_honor_initial_score<T: std::clone::Clone>(items: &mut [MenuItem<T>]) {
let mut regular_score = items.len() as i64; let mut regular_score = items.len() as i64;
items.sort_by(|l, r| l.label.cmp(&r.label)); items.sort_by(|l, r| l.label.cmp(&r.label));

View file

@ -124,7 +124,7 @@ impl<T: Clone> DRunProvider<T> {
entries.push(entry); entries.push(entry);
} }
gui::initialize_sort_scores(&mut entries); gui::sort_menu_items_alphabetically_honor_initial_score(&mut entries);
DRunProvider { DRunProvider {
items: entries, items: entries,
@ -134,7 +134,7 @@ impl<T: Clone> DRunProvider<T> {
} }
} }
impl<T: std::clone::Clone> ItemProvider<T> for DRunProvider<T> { impl<T: Clone> ItemProvider<T> for DRunProvider<T> {
fn get_elements(&mut self, _: Option<&str>) -> Vec<MenuItem<T>> { fn get_elements(&mut self, _: Option<&str>) -> Vec<MenuItem<T>> {
self.items.clone() self.items.clone()
} }
@ -144,50 +144,79 @@ impl<T: std::clone::Clone> ItemProvider<T> for DRunProvider<T> {
} }
} }
#[derive(Debug, Clone, PartialEq)]
enum AutoRunType {
Math,
DRun,
File,
Ssh,
WebSearch,
Emoji,
Run,
}
#[derive(Clone)] #[derive(Clone)]
struct AutoItemProvider { struct FileItemProvider<T: std::clone::Clone> {
drun_provider: DRunProvider<AutoRunType>, last_result: Option<Vec<MenuItem<T>>>,
last_result: Option<Vec<MenuItem<AutoRunType>>>, menu_item_data: T,
} }
impl AutoItemProvider { impl<T: Clone> FileItemProvider<T> {
fn new() -> Self { fn new(menu_item_data: T) -> Self {
AutoItemProvider { FileItemProvider {
drun_provider: DRunProvider::new(AutoRunType::DRun),
last_result: None, last_result: None,
menu_item_data,
} }
} }
fn auto_run_handle_files(&mut self, trimmed_search: &str) -> Vec<MenuItem<AutoRunType>> { fn resolve_icon_for_name(&self, path: PathBuf) -> String {
let folder_icon = "inode-directory"; 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); impl<T: Clone> ItemProvider<T> for FileItemProvider<T> {
let mut items: Vec<MenuItem<AutoRunType>> = Vec::new(); fn get_elements(&mut self, search: Option<&str>) -> Vec<MenuItem<T>> {
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<MenuItem<T>> = Vec::new();
if !path.exists() { if !path.exists() {
if let Some(last) = &self.last_result { if let Some(last) = &self.last_result {
if !last.is_empty() return last.clone();
&& last.first().is_some_and(|l| {
l.as_ref()
.data
.as_ref()
.is_some_and(|t| t == &AutoRunType::File)
})
{
return last.clone();
}
} }
return vec![]; return vec![];
@ -210,17 +239,13 @@ impl AutoItemProvider {
items.push({ items.push({
MenuItem { MenuItem {
label: path_str.clone(), label: path_str.clone(),
icon_path: if entry.path().is_dir() { icon_path: Some(self.resolve_icon_for_name(entry.path())),
Some(folder_icon.to_owned())
} else {
Some(resolve_icon_for_name(entry.path()))
},
action: Some(format!("xdg-open {path_str}")), action: Some(format!("xdg-open {path_str}")),
sub_elements: vec![], sub_elements: vec![],
working_dir: None, working_dir: None,
initial_sort_score: 0, initial_sort_score: 0,
search_sort_score: 0.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({ items.push({
MenuItem { MenuItem {
label: trimmed_search.to_owned(), 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}")), action: Some(format!("xdg-open {trimmed_search}")),
sub_elements: vec![], sub_elements: vec![],
working_dir: None, working_dir: None,
initial_sort_score: 0, initial_sort_score: 0,
search_sort_score: 0.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()); self.last_result = Some(items.clone());
items items
} }
fn get_sub_elements(&mut self, _: &MenuItem<T>) -> Option<Vec<MenuItem<T>>> {
self.last_result.clone()
}
} }
fn resolve_icon_for_name(path: PathBuf) -> String { #[derive(Debug, Clone, PartialEq)]
// todo use https://docs.rs/tree_magic_mini/latest/tree_magic_mini/ instead enum AutoRunType {
if let Ok(metadata) = fs::symlink_metadata(&path) { Math,
if metadata.file_type().is_symlink() { DRun,
return "inode-symlink".to_owned(); File,
} else if metadata.is_dir() { Ssh,
return "inode-directory".to_owned(); WebSearch,
} else if metadata.permissions().mode() & 0o111 != 0 { Emoji,
return "application-x-executable".to_owned(); Run,
}
#[derive(Clone)]
struct AutoItemProvider {
drun_provider: DRunProvider<AutoRunType>,
file_provider: FileItemProvider<AutoRunType>,
last_result: Option<Vec<MenuItem<AutoRunType>>>,
}
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 { fn contains_math_functions_or_starts_with_number(input: &str) -> bool {
@ -320,7 +332,7 @@ impl ItemProvider<AutoRunType> for AutoItemProvider {
let item = MenuItem { let item = MenuItem {
label: result, label: result,
icon_path: None, icon_path: None,
action: None, action: Some(trimmed_search.to_owned()),
sub_elements: vec![], sub_elements: vec![],
working_dir: None, working_dir: None,
initial_sort_score: 0, initial_sort_score: 0,
@ -333,7 +345,7 @@ impl ItemProvider<AutoRunType> for AutoItemProvider {
|| trimmed_search.starts_with("/") || trimmed_search.starts_with("/")
|| trimmed_search.starts_with("~") || trimmed_search.starts_with("~")
{ {
self.auto_run_handle_files(trimmed_search) self.file_provider.get_elements(search_opt)
} else { } else {
return self.drun_provider.get_elements(search_opt); return self.drun_provider.get_elements(search_opt);
} }
@ -414,6 +426,28 @@ pub fn auto(config: &mut Config) -> anyhow::Result<()> {
Ok(()) 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<T: Clone>( fn update_drun_cache_and_run<T: Clone>(
cache_path: Option<PathBuf>, cache_path: Option<PathBuf>,
cache: &mut HashMap<String, i64>, cache: &mut HashMap<String, i64>,

View file

@ -26,6 +26,9 @@ fn main() -> anyhow::Result<()> {
Mode::Dmenu => { Mode::Dmenu => {
todo!("dmenu not implemented") todo!("dmenu not implemented")
} }
Mode::File => {
mode::file(&mut config).map_err(|e| anyhow!(e))?;
}
Mode::Auto => { Mode::Auto => {
mode::auto(&mut config)?; mode::auto(&mut config)?;
} }