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",
]
[[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",
]

View file

@ -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"

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
@ -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

View file

@ -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<String>,
/// Defines which prompt is used. Default is selected 'show'
#[clap(short = 'p', long = "prompt")]
pub prompt: Option<String>,
#[clap(short = 'x', long = "xoffset")]
pub xoffset: Option<i32>,
#[clap(long = "x")]
pub x: Option<i32>,
#[clap(short = 'y', long = "yoffset")]
pub yoffset: Option<i32>,
#[clap(long = "y")]
pub y: Option<i32>,
/// 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,

View file

@ -135,7 +135,7 @@ fn build_ui<T, P>(
}
/// 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<i32> {
// highly unlikely that we are dealing with > i64 items
#[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;
items.sort_by(|l, r| l.label.cmp(&r.label));

View file

@ -124,7 +124,7 @@ impl<T: Clone> DRunProvider<T> {
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<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>> {
self.items.clone()
}
@ -144,51 +144,80 @@ 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)]
struct AutoItemProvider {
drun_provider: DRunProvider<AutoRunType>,
last_result: Option<Vec<MenuItem<AutoRunType>>>,
struct FileItemProvider<T: std::clone::Clone> {
last_result: Option<Vec<MenuItem<T>>>,
menu_item_data: T,
}
impl AutoItemProvider {
fn new() -> Self {
AutoItemProvider {
drun_provider: DRunProvider::new(AutoRunType::DRun),
impl<T: Clone> FileItemProvider<T> {
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<MenuItem<AutoRunType>> {
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<MenuItem<AutoRunType>> = Vec::new();
impl<T: Clone> ItemProvider<T> for FileItemProvider<T> {
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 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 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<T>) -> Option<Vec<MenuItem<T>>> {
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<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 {
@ -320,7 +332,7 @@ impl ItemProvider<AutoRunType> 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<AutoRunType> 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<T: Clone>(
cache_path: Option<PathBuf>,
cache: &mut HashMap<String, i64>,

View file

@ -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)?;
}