fix warnings

This commit is contained in:
Alexander Mohr 2025-04-19 00:00:43 +02:00
parent f398848dcf
commit efb0c3798e
11 changed files with 375 additions and 362 deletions

View file

@ -15,12 +15,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Format - uses: actions-rust-lang/setup-rust-toolchain@v1
run: cargo fmt --check
- name: Clippy - name: Formatting
run: cargo clippy run: cargo fmt --all -- --check
- name: Build - name: Clippy warnings
run: cargo build --verbose run: cargo clippy -- -D warnings
- name: Run tests - name: Build
run: cargo test --verbose run: cargo build --verbose
- name: Run tests
run: cargo test -- --show-output

16
Cargo.lock generated
View file

@ -277,12 +277,6 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "configparser"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe1d7dcda7d1da79e444bdfba1465f2f849a58b07774e1df473ee77030cb47a7"
[[package]] [[package]]
name = "crossbeam" name = "crossbeam"
version = "0.8.4" version = "0.8.4"
@ -912,15 +906,6 @@ dependencies = [
"hashbrown 0.15.2", "hashbrown 0.15.2",
] ]
[[package]]
name = "ini"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a9271a5dfd4228fa56a78d7508a35c321639cc71f783bb7a5723552add87bce"
dependencies = [
"configparser",
]
[[package]] [[package]]
name = "is_terminal_polyfill" name = "is_terminal_polyfill"
version = "1.70.1" version = "1.70.1"
@ -1686,7 +1671,6 @@ dependencies = [
"gtk4-layer-shell", "gtk4-layer-shell",
"home", "home",
"hyprland", "hyprland",
"ini",
"libc", "libc",
"log", "log",
"regex", "regex",

View file

@ -3,6 +3,24 @@ name = "worf"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[lints.clippy]
# enable pedantic
pedantic = { level = "warn", priority = -1 }
## exclude some too pedantic lints for now
similar_names = "allow"
# additional lints
clone_on_ref_ptr = "warn"
[lib]
name = "worf_lib"
path = "src/lib/mod.rs"
[[bin]]
name = "worf"
path = "src/main.rs"
[dependencies] [dependencies]
gtk4 = { version = "0.9.5", default-features = true, features = ["v4_6"] } gtk4 = { version = "0.9.5", default-features = true, features = ["v4_6"] }
gtk4-layer-shell = "0.5.0" gtk4-layer-shell = "0.5.0"
@ -13,7 +31,6 @@ home = "0.5.11"
log = "0.4.27" log = "0.4.27"
regex = "1.11.1" regex = "1.11.1"
hyprland = "0.4.0-beta.2" hyprland = "0.4.0-beta.2"
ini = "1.3.0"
clap = { version = "4.5.35", features = ["derive"] } clap = { version = "4.5.35", features = ["derive"] }
thiserror = "2.0.12" thiserror = "2.0.12"
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
@ -21,6 +38,6 @@ toml = "0.8.20"
serde_json = "1.0.140" serde_json = "1.0.140"
crossbeam = "0.8.4" crossbeam = "0.8.4"
libc = "0.2.171" libc = "0.2.171"
freedesktop-file-parser = "0.1.0" freedesktop-file-parser = "0.1.3"
strsim = "0.11.1" strsim = "0.11.1"
dirs = "6.0.0" dirs = "6.0.0"

View file

@ -1,15 +1,11 @@
use crate::lib::system;
use anyhow::{anyhow, Context};
use clap::builder::TypedValueParser;
use clap::{Parser, ValueEnum};
use gtk4::prelude::ToValue;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::env::Args;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use std::{env, fs}; use std::{env, fs};
use anyhow::anyhow;
use clap::{Parser, ValueEnum};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use thiserror::Error; use thiserror::Error;
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Serialize, Deserialize)] #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Serialize, Deserialize)]
@ -34,10 +30,10 @@ pub enum Align {
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Mode { pub enum Mode {
/// searches $PATH for executables and allows them to be run by selecting them. /// searches `$PATH` for executables and allows them to be run by selecting them.
Run, Run,
/// searches $XDG_DATA_HOME/applications and $XDG_DATA_DIRS/applications f /// searches `$XDG_DATA_HOME/applications` and `$XDG_DATA_DIRS/applications`
/// or desktop files and allows them to be run by selecting them. /// for desktop files and allows them to be run by selecting them.
Drun, Drun,
/// reads from stdin and displays options which when selected will be output to stdout. /// reads from stdin and displays options which when selected will be output to stdout.
@ -85,8 +81,8 @@ pub struct Config {
pub version: Option<bool>, pub version: Option<bool>,
/// Defines the style sheet to be loaded. /// Defines the style sheet to be loaded.
/// Defaults to $XDG_CONF_DIR/worf/style.css /// Defaults to `$XDG_CONF_DIR/worf/style.css`
/// or $HOME/.config/worf/style.css if XDG_CONF_DIR is not set. /// or `$HOME/.config/worf/style.css` if `$XDG_CONF_DIR` is not set.
#[serde(default = "default_style")] #[serde(default = "default_style")]
#[clap(long = "style")] #[clap(long = "style")]
pub style: Option<String>, pub style: Option<String>,
@ -252,8 +248,6 @@ pub struct Config {
#[serde(default = "default_text_wrap_length")] #[serde(default = "default_text_wrap_length")]
#[clap(long = "text-wrap-length")] #[clap(long = "text-wrap-length")]
pub text_wrap_length: Option<usize>, pub text_wrap_length: Option<usize>,
} }
impl Default for Config { impl Default for Config {
@ -327,28 +321,45 @@ impl Default for Config {
} }
} }
} }
// allowed because option is needed for serde macro
fn default_row_box_orientation() -> Option<Orientation> { #[allow(clippy::unnecessary_wraps)]
#[must_use]
pub fn default_row_box_orientation() -> Option<Orientation> {
Some(Orientation::Horizontal) Some(Orientation::Horizontal)
} }
pub(crate) fn default_orientation() -> Option<Orientation> { // allowed because option is needed for serde macro
#[allow(clippy::unnecessary_wraps)]
#[must_use]
pub fn default_orientation() -> Option<Orientation> {
Some(Orientation::Vertical) Some(Orientation::Vertical)
} }
fn default_halign() -> Option<Align> { // allowed because option is needed for serde macro
#[allow(clippy::unnecessary_wraps)]
#[must_use]
pub fn default_halign() -> Option<Align> {
Some(Align::Fill) Some(Align::Fill)
} }
fn default_content_halign() -> Option<Align> { // allowed because option is needed for serde macro
#[allow(clippy::unnecessary_wraps)]
#[must_use]
pub fn default_content_halign() -> Option<Align> {
Some(Align::Fill) Some(Align::Fill)
} }
fn default_columns() -> Option<u32> { // allowed because option is needed for serde macro
#[allow(clippy::unnecessary_wraps)]
#[must_use]
pub fn default_columns() -> Option<u32> {
Some(1) Some(1)
} }
fn default_normal_window() -> bool { // allowed because option is needed for serde macro
#[allow(clippy::unnecessary_wraps)]
#[must_use]
pub fn default_normal_window() -> bool {
false false
} }
@ -429,77 +440,112 @@ fn default_normal_window() -> bool {
// key_default = "Ctrl-c"; // key_default = "Ctrl-c";
// char* key_copy = (i == 0) ? key_default : config_get(config, "key_copy", key_default); // char* key_copy = (i == 0) ? key_default : config_get(config, "key_copy", key_default);
fn default_style() -> Option<String> { // allowed because option is needed for serde macro
#[allow(clippy::unnecessary_wraps)]
#[must_use]
pub fn default_style() -> Option<String> {
style_path(None) style_path(None)
.ok() .ok()
.and_then(|pb| Some(pb.display().to_string())) .map(|pb| pb.display().to_string())
.or_else(|| { .or_else(|| {
log::error!("no stylesheet found, using system styles"); log::error!("no stylesheet found, using system styles");
None None
}) })
} }
// allowed because option is needed for serde macro
#[allow(clippy::unnecessary_wraps)]
#[must_use]
pub fn default_height() -> Option<String> { pub fn default_height() -> Option<String> {
Some("40%".to_owned()) Some("40%".to_owned())
} }
// allowed because option is needed for serde macro
#[allow(clippy::unnecessary_wraps)]
#[must_use]
pub fn default_width() -> Option<String> { pub fn default_width() -> Option<String> {
Some("50%".to_owned()) Some("50%".to_owned())
} }
// allowed because option is needed for serde macro
#[allow(clippy::unnecessary_wraps)]
#[must_use]
pub fn default_password_char() -> Option<String> { pub fn default_password_char() -> Option<String> {
Some("*".to_owned()) Some("*".to_owned())
} }
// allowed because option is needed for serde macro
#[allow(clippy::unnecessary_wraps)]
#[must_use]
pub fn default_fuzzy_min_length() -> Option<i32> { pub fn default_fuzzy_min_length() -> Option<i32> {
Some(10) Some(10)
} }
// allowed because option is needed for serde macro
#[allow(clippy::unnecessary_wraps)]
#[must_use]
pub fn default_fuzzy_min_score() -> Option<f64> { pub fn default_fuzzy_min_score() -> Option<f64> {
Some(0.1) Some(0.1)
} }
// allowed because option is needed for serde macro
#[allow(clippy::unnecessary_wraps)]
#[must_use]
pub fn default_match_method() -> Option<MatchMethod> { pub fn default_match_method() -> Option<MatchMethod> {
Some(MatchMethod::Contains) Some(MatchMethod::Contains)
} }
// allowed because option is needed for serde macro
#[allow(clippy::unnecessary_wraps)]
#[must_use]
pub fn default_image_size() -> Option<i32> { pub fn default_image_size() -> Option<i32> {
Some(32) Some(32)
} }
// allowed because option is needed for serde macro
#[allow(clippy::unnecessary_wraps)]
#[must_use]
pub fn default_text_wrap_length() -> Option<usize> { pub fn default_text_wrap_length() -> Option<usize> {
Some(15) Some(15)
} }
// allowed because option is needed for serde macro
#[allow(clippy::unnecessary_wraps)]
#[must_use]
pub fn default_text_wrap() -> Option<bool> { pub fn default_text_wrap() -> Option<bool> {
Some(false) Some(false)
} }
#[must_use]
pub fn parse_args() -> Config { pub fn parse_args() -> Config {
Config::parse() Config::parse()
} }
/// # Errors
///
/// Will return Err when it cannot resolve any path or no style is found
pub fn style_path(full_path: Option<String>) -> Result<PathBuf, anyhow::Error> { pub fn style_path(full_path: Option<String>) -> Result<PathBuf, anyhow::Error> {
let alternative_paths = path_alternatives(vec![dirs::config_dir()], PathBuf::from("worf").join("style.css")); let alternative_paths = path_alternatives(
resolve_path( vec![dirs::config_dir()],
full_path, &PathBuf::from("worf").join("style.css"),
alternative_paths );
.into_iter() resolve_path(full_path, alternative_paths.into_iter().collect())
.collect(),
)
} }
pub fn path_alternatives(base_paths: Vec<Option<PathBuf>>, sub_path: PathBuf) -> Vec<PathBuf> { #[must_use]
pub fn path_alternatives(base_paths: Vec<Option<PathBuf>>, sub_path: &PathBuf) -> Vec<PathBuf> {
base_paths base_paths
.into_iter() .into_iter()
.filter_map(|s| s) .flatten()
.map(|pb| pb.join(&sub_path)) .map(|pb| pb.join(sub_path))
.filter_map(|pb| pb.canonicalize().ok()) .filter_map(|pb| pb.canonicalize().ok())
.filter(|c| c.exists()) .filter(|c| c.exists())
.collect() .collect()
} }
/// # Errors
///
/// Will return `Err` if it is not able to find any valid path
pub fn resolve_path( pub fn resolve_path(
full_path: Option<String>, full_path: Option<String>,
alternatives: Vec<PathBuf>, alternatives: Vec<PathBuf>,
@ -513,16 +559,21 @@ pub fn resolve_path(
.filter(|p| p.exists()) .filter(|p| p.exists())
.find_map(|pb| pb.canonicalize().ok().filter(|c| c.exists())) .find_map(|pb| pb.canonicalize().ok().filter(|c| c.exists()))
}) })
.ok_or_else(|| anyhow!("Could not find a valid config file.")) .ok_or_else(|| anyhow!("Could not find a valid file."))
} }
/// # Errors
///
/// Will return Err when it
/// * cannot read the config file
/// * cannot parse the config file
/// * no config file exists
/// * config file and args cannot be merged
pub fn load_config(args_opt: Option<Config>) -> Result<Config, anyhow::Error> { pub fn load_config(args_opt: Option<Config>) -> Result<Config, anyhow::Error> {
let home_dir = env::var("HOME")?; let home_dir = env::var("HOME")?;
let config_path = args_opt.as_ref().map(|c| { let config_path = args_opt.as_ref().map(|c| {
c.config c.config.as_ref().map_or_else(
.as_ref() || {
.and_then(|p| Some(PathBuf::from(p)))
.unwrap_or_else(|| {
env::var("XDG_CONF_HOME") env::var("XDG_CONF_HOME")
.map_or( .map_or(
PathBuf::from(home_dir.clone()).join(".config"), PathBuf::from(home_dir.clone()).join(".config"),
@ -530,7 +581,9 @@ pub fn load_config(args_opt: Option<Config>) -> Result<Config, anyhow::Error> {
) )
.join("worf") .join("worf")
.join("config") .join("config")
}) },
PathBuf::from,
)
}); });
match config_path { match config_path {
@ -549,6 +602,9 @@ pub fn load_config(args_opt: Option<Config>) -> Result<Config, anyhow::Error> {
} }
} }
/// # Errors
///
/// Will return Err when it fails to merge the config with the arguments.
pub fn merge_config_with_args(config: &mut Config, args: &Config) -> anyhow::Result<Config> { pub fn merge_config_with_args(config: &mut Config, args: &Config) -> anyhow::Result<Config> {
let args_json = serde_json::to_value(args)?; let args_json = serde_json::to_value(args)?;
let mut config_json = serde_json::to_value(config)?; let mut config_json = serde_json::to_value(config)?;

View file

@ -1,8 +1,8 @@
use anyhow::anyhow;
use freedesktop_file_parser::DesktopFile; use freedesktop_file_parser::DesktopFile;
use gtk4::prelude::*; use gtk4::prelude::*;
use gtk4::{IconLookupFlags, IconTheme, TextDirection}; use gtk4::{IconLookupFlags, IconTheme, TextDirection};
use home::home_dir; use home::home_dir;
use ini::configparser::ini::Ini;
use log::{debug, info, warn}; use log::{debug, info, warn};
use regex::Regex; use regex::Regex;
use std::collections::HashMap; use std::collections::HashMap;
@ -14,13 +14,21 @@ pub struct IconResolver {
cache: HashMap<String, String>, cache: HashMap<String, String>,
} }
impl Default for IconResolver {
#[must_use]
fn default() -> IconResolver {
Self::new()
}
}
impl IconResolver { impl IconResolver {
#![allow(clippy::single_call_fn)] #[must_use]
pub fn new() -> IconResolver { pub fn new() -> IconResolver {
IconResolver { IconResolver {
cache: HashMap::new(), cache: HashMap::new(),
} }
} }
pub fn icon_path(&mut self, icon_name: &str) -> String { pub fn icon_path(&mut self, icon_name: &str) -> String {
if let Some(icon_path) = self.cache.get(icon_name) { if let Some(icon_path) = self.cache.get(icon_name) {
info!("Fetching {icon_name} from cache"); info!("Fetching {icon_name} from cache");
@ -28,47 +36,55 @@ impl IconResolver {
} }
info!("Loading icon for {icon_name}"); info!("Loading icon for {icon_name}");
let icon = fetch_icon_from_theme(icon_name) let icon = fetch_icon_from_theme(icon_name)
.or_else(|| fetch_icon_from_common_dirs(icon_name)) .or_else(|_| {
.or_else(|| fetch_icon_from_desktop_file(icon_name)) fetch_icon_from_common_dirs(icon_name).map_or_else(
.unwrap_or_else(|| { || Err(anyhow::anyhow!("Missing file")), // Return an error here
Ok,
)
})
.or_else(|_| {
warn!("Missing icon for {icon_name}, using fallback"); warn!("Missing icon for {icon_name}, using fallback");
default_icon() default_icon()
}); });
self.cache.insert(icon_name.to_owned(), icon.clone()); self.cache
self.cache.get(icon_name).unwrap().to_owned() .entry(icon_name.to_owned())
.or_insert_with(|| icon.unwrap_or_default())
.to_owned()
} }
} }
pub fn default_icon() -> String { /// # Errors
fetch_icon_from_theme("image-missing").unwrap() ///
/// Will return `Err` if no icon can be found
pub fn default_icon() -> anyhow::Result<String> {
fetch_icon_from_theme("image-missing")
} }
fn fetch_icon_from_desktop_file(icon_name: &str) -> Option<String> { // fn fetch_icon_from_desktop_file(icon_name: &str) -> Option<String> {
// find_desktop_files().into_iter().find_map(|desktop_file| { // // find_desktop_files().into_iter().find_map(|desktop_file| {
// desktop_file // // desktop_file
// .get("Desktop Entry") // // .get("Desktop Entry")
// .filter(|desktop_entry| { // // .filter(|desktop_entry| {
// desktop_entry // // desktop_entry
// .get("Exec") // // .get("Exec")
// .and_then(|opt| opt.as_ref()) // // .and_then(|opt| opt.as_ref())
// .is_some_and(|exec| exec.to_lowercase().contains(icon_name)) // // .is_some_and(|exec| exec.to_lowercase().contains(icon_name))
// }) // // })
// .map(|desktop_entry| { // // .map(|desktop_entry| {
// desktop_entry // // desktop_entry
// .get("Icon") // // .get("Icon")
// .and_then(|opt| opt.as_ref()) // // .and_then(|opt| opt.as_ref())
// .map(ToOwned::to_owned) // // .map(ToOwned::to_owned)
// .unwrap_or_default() // // .unwrap_or_default()
// }) // // })
// }) // // })
//todo // //todo
None // None
} // }
fn fetch_icon_from_theme(icon_name: &str) -> Option<String> { fn fetch_icon_from_theme(icon_name: &str) -> anyhow::Result<String> {
let display = gtk4::gdk::Display::default(); let display = gtk4::gdk::Display::default();
if display.is_none() { if display.is_none() {
log::error!("Failed to get display"); log::error!("Failed to get display");
@ -85,9 +101,14 @@ fn fetch_icon_from_theme(icon_name: &str) -> Option<String> {
IconLookupFlags::empty(), IconLookupFlags::empty(),
); );
icon.file() match icon
.file()
.and_then(|file| file.path()) .and_then(|file| file.path())
.and_then(|path| path.to_str().map(string::ToString::to_string)) .and_then(|path| path.to_str().map(string::ToString::to_string))
{
None => Err(anyhow!("Cannot find file")),
Some(i) => Ok(i),
}
} }
fn fetch_icon_from_common_dirs(icon_name: &str) -> Option<String> { fn fetch_icon_from_common_dirs(icon_name: &str) -> Option<String> {
@ -125,15 +146,17 @@ fn find_file_case_insensitive(folder: &Path, file_name: &Regex) -> Option<Vec<Pa
.filter(|entry| { .filter(|entry| {
entry entry
.file_name() .file_name()
.and_then(|e| e.to_str()) // Handle the Option here. .and_then(|e| e.to_str())
.map(|name| file_name.is_match(name)) .is_some_and(|name| file_name.is_match(name))
.unwrap_or(false) // Handle the case where the file name is not a valid string.
}) })
.collect() .collect()
}) })
} }
pub(crate) fn find_desktop_files() -> Vec<DesktopFile> { /// # Errors
///
/// Will return Err when it cannot parse the internal regex
pub fn find_desktop_files() -> anyhow::Result<Vec<DesktopFile>> {
let mut paths = vec![ let mut paths = vec![
PathBuf::from("/usr/share/applications"), PathBuf::from("/usr/share/applications"),
PathBuf::from("/usr/local/share/applications"), PathBuf::from("/usr/local/share/applications"),
@ -144,24 +167,33 @@ pub(crate) fn find_desktop_files() -> Vec<DesktopFile> {
paths.push(home.join(".local/share/applications")); paths.push(home.join(".local/share/applications"));
} }
if let Ok(xdg_data_home) = env::var("XDG_DATA_HOME") {
paths.push(PathBuf::from(xdg_data_home).join(".applications"));
}
if let Ok(xdg_data_dir) = env::var("XDG_DATA_DIRS") {
paths.push(PathBuf::from(xdg_data_dir).join(".applications"));
}
let regex = &Regex::new("(?i).*\\.desktop$")?;
let p: Vec<_> = paths let p: Vec<_> = paths
.into_iter() .into_iter()
.filter(|icon_dir| icon_dir.exists()) .filter(|desktop_dir| desktop_dir.exists())
.filter_map(|icon_dir| { .filter_map(|icon_dir| find_file_case_insensitive(&icon_dir, regex))
find_file_case_insensitive(&icon_dir, &Regex::new("(?i).*\\.desktop$").unwrap())
})
.flat_map(|desktop_files| { .flat_map(|desktop_files| {
desktop_files.into_iter().filter_map(|desktop_file| { desktop_files.into_iter().filter_map(|desktop_file| {
debug!("loading desktop file {:?}", desktop_file); debug!("loading desktop file {desktop_file:?}");
fs::read_to_string(desktop_file) fs::read_to_string(desktop_file)
.ok() .ok()
.and_then(|content| freedesktop_file_parser::parse(&content).ok()) .and_then(|content| freedesktop_file_parser::parse(&content).ok())
}) })
}) })
.collect(); .collect();
p Ok(p)
} }
#[must_use]
pub fn get_locale_variants() -> Vec<String> { pub fn get_locale_variants() -> Vec<String> {
let locale = env::var("LC_ALL") let locale = env::var("LC_ALL")
.or_else(|_| env::var("LC_MESSAGES")) .or_else(|_| env::var("LC_MESSAGES"))
@ -172,7 +204,7 @@ pub fn get_locale_variants() -> Vec<String> {
let mut variants = vec![]; let mut variants = vec![];
if let Some((lang_part, region)) = lang.split_once('_') { if let Some((lang_part, region)) = lang.split_once('_') {
variants.push(format!("{}_{region}", lang_part)); // en_us variants.push(format!("{lang_part}_{region}")); // en_us
variants.push(lang_part.to_string()); // en variants.push(lang_part.to_string()); // en
} else { } else {
variants.push(lang.clone()); // e.g. "fr" variants.push(lang.clone()); // e.g. "fr"
@ -181,52 +213,17 @@ pub fn get_locale_variants() -> Vec<String> {
variants variants
} }
pub fn extract_desktop_fields( // implicit hasher does not make sense here, it is only for desktop files
category: &str, #[allow(clippy::implicit_hasher)]
//keys: Vec<String>, #[must_use]
desktop_map: &HashMap<String, HashMap<String, Option<String>>>, pub fn lookup_name_with_locale(
) -> HashMap<String, String> { locale_variants: &[String],
let mut result: HashMap<String, String> = HashMap::new(); variants: &HashMap<String, String>,
let category_map = desktop_map.get(category); fallback: &str,
if category_map.is_none() { ) -> Option<String> {
debug!("No desktop map for category {category}, map data: {desktop_map:?}"); locale_variants
return result; .iter()
} .find_map(|local| variants.get(local))
.map(std::borrow::ToOwned::to_owned)
let keys_needed = ["name", "exec", "icon"]; .or_else(|| Some(fallback.to_owned()))
let locale_variants = get_locale_variants();
for (map_key, map_value) in category_map.unwrap() {
for key in keys_needed {
if result.contains_key(key) || map_value.is_none() {
continue;
}
let (k, v) = locale_variants
.iter()
.find(|locale| {
let localized_key = format!("{}[{}]", key, locale);
key == localized_key
})
.map(|_| (Some(key), map_value))
.unwrap_or_else(|| {
if key == map_key {
(Some(key), map_value)
} else {
(None, &None)
}
});
if let Some(k) = k {
if let Some(v) = v {
result.insert(k.to_owned(), v.clone());
}
}
}
if result.len() == keys_needed.len() {
break;
}
}
result
} }

View file

@ -1,46 +1,43 @@
use crate::lib::config; use std::collections::HashMap;
use crate::lib::config::{Config, MatchMethod}; use std::sync::{Arc, Mutex};
use anyhow::{Context, anyhow};
use anyhow::anyhow;
use crossbeam::channel; use crossbeam::channel;
use crossbeam::channel::Sender; use crossbeam::channel::Sender;
use gdk4::gio::{File, Menu}; use gdk4::gio::File;
use gdk4::glib::{GString, Propagation, Unichar}; use gdk4::glib::Propagation;
use gdk4::prelude::{Cast, DisplayExt, ListModelExtManual, MonitorExt}; use gdk4::prelude::{Cast, DisplayExt, MonitorExt};
use gdk4::{pango, Display, Key}; use gdk4::{Display, Key};
use gtk4::prelude::{ use gtk4::prelude::{
ApplicationExt, ApplicationExtManual, BoxExt, ButtonExt, EditableExt, EntryExt, FileChooserExt, ApplicationExt, ApplicationExtManual, BoxExt, EditableExt, FlowBoxChildExt, GestureSingleExt,
FlowBoxChildExt, GestureSingleExt, GtkWindowExt, ListBoxRowExt, NativeExt, OrientableExt, GtkWindowExt, ListBoxRowExt, NativeExt, WidgetExt,
WidgetExt,
}; };
use gtk4::{ use gtk4::{
Align, Entry, EventControllerKey, Expander, FlowBox, FlowBoxChild, GestureClick, Image, Label, Align, EventControllerKey, Expander, FlowBox, FlowBoxChild, GestureClick, Image, Label,
ListBox, ListBoxRow, Ordering, PolicyType, Revealer, ScrolledWindow, SearchEntry, Widget, gdk, ListBox, ListBoxRow, Ordering, PolicyType, ScrolledWindow, SearchEntry, Widget, gdk,
}; };
use gtk4::{Application, ApplicationWindow, CssProvider, Orientation}; use gtk4::{Application, ApplicationWindow, CssProvider, Orientation};
use gtk4_layer_shell::{KeyboardMode, LayerShell}; use gtk4_layer_shell::{KeyboardMode, LayerShell};
use hyprland::ctl::output::create; use log;
use hyprland::ctl::plugin::list;
use std::collections::HashMap;
use log::{debug, error, info}; use crate::config;
use std::process::exit; use crate::config::{Config, MatchMethod};
use std::sync::{Arc, Mutex, MutexGuard};
type ArcMenuMap<T> = Arc<Mutex<HashMap<FlowBoxChild, MenuItem<T>>>>; type ArcMenuMap<T> = Arc<Mutex<HashMap<FlowBoxChild, MenuItem<T>>>>;
type MenuItemSender<T> = Sender<Result<MenuItem<T>, anyhow::Error>>; type MenuItemSender<T> = Sender<Result<MenuItem<T>, anyhow::Error>>;
impl Into<Orientation> for config::Orientation { impl From<config::Orientation> for Orientation {
fn into(self) -> Orientation { fn from(orientation: config::Orientation) -> Self {
match self { match orientation {
config::Orientation::Vertical => Orientation::Vertical, config::Orientation::Vertical => Orientation::Vertical,
config::Orientation::Horizontal => Orientation::Horizontal, config::Orientation::Horizontal => Orientation::Horizontal,
} }
} }
} }
impl Into<Align> for config::Align { impl From<config::Align> for Align {
fn into(self) -> Align { fn from(align: config::Align) -> Self {
match self { match align {
config::Align::Fill => Align::Fill, config::Align::Fill => Align::Fill,
config::Align::Start => Align::Start, config::Align::Start => Align::Start,
config::Align::Center => Align::Center, config::Align::Center => Align::Center,
@ -62,39 +59,46 @@ pub struct MenuItem<T> {
pub data: Option<T>, pub data: Option<T>,
} }
pub fn show<T>(config: Config, elements: Vec<MenuItem<T>>) -> Result<MenuItem<T>, anyhow::Error> where T: Clone + 'static { /// # Errors
///
/// Will return Err when the channel between the UI and this is broken
pub fn show<T>(config: Config, elements: Vec<MenuItem<T>>) -> Result<MenuItem<T>, anyhow::Error>
where
T: Clone + 'static,
{
if let Some(ref css) = config.style { if let Some(ref css) = config.style {
let provider = CssProvider::new(); let provider = CssProvider::new();
let css_file_path = File::for_path(css); let css_file_path = File::for_path(css);
provider.load_from_file(&css_file_path); provider.load_from_file(&css_file_path);
let display = Display::default().expect("Could not connect to a display"); if let Some(display) = Display::default() {
gtk4::style_context_add_provider_for_display( gtk4::style_context_add_provider_for_display(
&display, &display,
&provider, &provider,
gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION, gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
); );
}
} }
let app = Application::builder().application_id("worf").build(); let app = Application::builder().application_id("worf").build();
let (sender, receiver) = channel::bounded(1); let (sender, receiver) = channel::bounded(1);
app.connect_activate(move |app| { app.connect_activate(move |app| {
build_ui(&config, &elements, sender.clone(), app); build_ui(&config, &elements, &sender, app);
}); });
let gtk_args: [&str; 0] = []; let gtk_args: [&str; 0] = [];
app.run_with_args(&gtk_args); app.run_with_args(&gtk_args);
let selection = receiver.recv()?; receiver.recv()?
selection
} }
fn build_ui<T>( fn build_ui<T>(
config: &Config, config: &Config,
elements: &Vec<MenuItem<T>>, elements: &Vec<MenuItem<T>>,
sender: Sender<Result<MenuItem<T>, anyhow::Error>>, sender: &Sender<Result<MenuItem<T>, anyhow::Error>>,
app: &Application, app: &Application,
) where T: Clone + 'static { ) where
// Create a toplevel undecorated window T: Clone + 'static,
{
let window = ApplicationWindow::builder() let window = ApplicationWindow::builder()
.application(app) .application(app)
.decorated(false) .decorated(false)
@ -130,8 +134,7 @@ fn build_ui<T>(
scroll.set_hexpand(true); scroll.set_hexpand(true);
scroll.set_vexpand(true); scroll.set_vexpand(true);
let hide_scroll = false; // todo if config.hide_scroll.is_some_and(|hs| hs) {
if hide_scroll {
scroll.set_policy(PolicyType::External, PolicyType::External); scroll.set_policy(PolicyType::External, PolicyType::External);
} }
@ -148,37 +151,28 @@ fn build_ui<T>(
if let Some(valign) = config.valign { if let Some(valign) = config.valign {
inner_box.set_valign(valign.into()); inner_box.set_valign(valign.into());
} else if config.orientation.unwrap() == config::Orientation::Horizontal {
inner_box.set_valign(Align::Center);
} else { } else {
if config.orientation.unwrap() == config::Orientation::Horizontal { inner_box.set_valign(Align::Start);
inner_box.set_valign(Align::Center);
} else {
inner_box.set_valign(Align::Start);
}
} }
inner_box.set_selection_mode(gtk4::SelectionMode::Browse); inner_box.set_selection_mode(gtk4::SelectionMode::Browse);
inner_box.set_max_children_per_line(config.columns.unwrap()); inner_box.set_max_children_per_line(config.columns.unwrap());
inner_box.set_activate_on_single_click(true); inner_box.set_activate_on_single_click(true);
let mut list_items: ArcMenuMap<T> = Arc::new(Mutex::new(HashMap::new())); let list_items: ArcMenuMap<T> = Arc::new(Mutex::new(HashMap::new()));
for entry in elements { for entry in elements {
list_items list_items
.lock() .lock()
.unwrap() // panic here ok? deadlock? .unwrap() // panic here ok? deadlock?
.insert( .insert(
add_menu_item( add_menu_item(&inner_box, entry, config, sender, &list_items, app),
&inner_box,
&entry,
&config,
sender.clone(),
list_items.clone(),
app.clone(),
),
entry.clone(), entry.clone(),
); );
} }
let items_clone = list_items.clone(); let items_clone = Arc::<Mutex<HashMap<FlowBoxChild, MenuItem<T>>>>::clone(&list_items);
inner_box.set_sort_func(move |child1, child2| sort_menu_items(child1, child2, &items_clone)); inner_box.set_sort_func(move |child1, child2| sort_menu_items(child1, child2, &items_clone));
// Set focus after everything is realized // Set focus after everything is realized
@ -197,29 +191,28 @@ fn build_ui<T>(
inner_box, inner_box,
app.clone(), app.clone(),
sender.clone(), sender.clone(),
list_items.clone(), Arc::<Mutex<HashMap<FlowBoxChild, MenuItem<T>>>>::clone(&list_items),
config.clone(), config.clone(),
); );
window.show(); window.show();
let display = window.display(); let display = window.display();
window.surface().map(|surface| { if let Some(surface) = window.surface() {
// todo this does not work for multi monitor systems // todo this does not work for multi monitor systems
let monitor = display.monitor_at_surface(&surface); let monitor = display.monitor_at_surface(&surface);
if let Some(monitor) = monitor { if let Some(monitor) = monitor {
let geometry = monitor.geometry(); let geometry = monitor.geometry();
config.width.as_ref().map(|width| { config.width.as_ref().map(|width| {
percent_or_absolute(&width, geometry.width()).map(|w| window.set_width_request(w)) percent_or_absolute(width, geometry.width()).map(|w| window.set_width_request(w))
}); });
config.height.as_ref().map(|height| { config.height.as_ref().map(|height| {
percent_or_absolute(&height, geometry.height()) percent_or_absolute(height, geometry.height()).map(|h| window.set_height_request(h))
.map(|h| window.set_height_request(h))
}); });
} else { } else {
log::error!("failed to get monitor to init window size"); log::error!("failed to get monitor to init window size");
} }
}); }
} }
fn setup_key_event_handler<T: Clone + 'static>( fn setup_key_event_handler<T: Clone + 'static>(
@ -287,12 +280,10 @@ fn sort_menu_items<T>(
} else { } else {
Ordering::Larger Ordering::Larger
} }
} else if menu1.initial_sort_score < menu2.initial_sort_score {
Ordering::Smaller
} else { } else {
if menu1.initial_sort_score < menu2.initial_sort_score { Ordering::Larger
Ordering::Smaller
} else {
Ordering::Larger
}
} }
} }
(Some(_), None) => Ordering::Larger, (Some(_), None) => Ordering::Larger,
@ -306,8 +297,11 @@ fn handle_selected_item<T>(
app: &Application, app: &Application,
inner_box: &FlowBox, inner_box: &FlowBox,
lock_arc: &ArcMenuMap<T>, lock_arc: &ArcMenuMap<T>,
) -> Result<(), String> where T: Clone { ) -> Result<(), String>
for s in inner_box.selected_children() { where
T: Clone,
{
if let Some(s) = inner_box.selected_children().into_iter().next() {
let list_items = lock_arc.lock().unwrap(); let list_items = lock_arc.lock().unwrap();
let item = list_items.get(&s); let item = list_items.get(&s);
if let Some(item) = item { if let Some(item) = item {
@ -325,19 +319,30 @@ fn add_menu_item<T: Clone + 'static>(
inner_box: &FlowBox, inner_box: &FlowBox,
entry_element: &MenuItem<T>, entry_element: &MenuItem<T>,
config: &Config, config: &Config,
sender: MenuItemSender<T>, sender: &MenuItemSender<T>,
lock_arc: ArcMenuMap<T>, lock_arc: &ArcMenuMap<T>,
app: Application, app: &Application,
) -> FlowBoxChild { ) -> FlowBoxChild {
let parent: Widget = if !entry_element.sub_elements.is_empty() { let parent: Widget = if entry_element.sub_elements.is_empty() {
create_menu_row(
entry_element,
config,
Arc::<Mutex<HashMap<FlowBoxChild, MenuItem<T>>>>::clone(lock_arc),
sender.clone(),
app.clone(),
inner_box.clone(),
)
.upcast()
} else {
let expander = Expander::new(None); let expander = Expander::new(None);
expander.set_widget_name("expander-box"); expander.set_widget_name("expander-box");
expander.set_hexpand(true); expander.set_hexpand(true);
// todo deduplicate this snippet
let menu_row = create_menu_row( let menu_row = create_menu_row(
entry_element, entry_element,
config, config,
lock_arc.clone(), Arc::<Mutex<HashMap<FlowBoxChild, MenuItem<T>>>>::clone(lock_arc),
sender.clone(), sender.clone(),
app.clone(), app.clone(),
inner_box.clone(), inner_box.clone(),
@ -352,7 +357,7 @@ fn add_menu_item<T: Clone + 'static>(
let sub_row = create_menu_row( let sub_row = create_menu_row(
sub_item, sub_item,
config, config,
lock_arc.clone(), Arc::<Mutex<HashMap<FlowBoxChild, MenuItem<T>>>>::clone(lock_arc),
sender.clone(), sender.clone(),
app.clone(), app.clone(),
inner_box.clone(), inner_box.clone(),
@ -365,16 +370,6 @@ fn add_menu_item<T: Clone + 'static>(
expander.set_child(Some(&list_box)); expander.set_child(Some(&list_box));
expander.upcast() expander.upcast()
} else {
create_menu_row(
entry_element,
config,
lock_arc.clone(),
sender.clone(),
app.clone(),
inner_box.clone(),
)
.upcast()
}; };
parent.set_halign(Align::Fill); parent.set_halign(Align::Fill);
@ -435,7 +430,7 @@ fn create_menu_row<T: Clone + 'static>(
} }
// todo make max length configurable // todo make max length configurable
let text = if config.text_wrap.is_some_and(|x| x == true) { let text = if config.text_wrap.is_some_and(|x| x) {
&wrap_text(&menu_item.label, config.text_wrap_length) &wrap_text(&menu_item.label, config.text_wrap_length)
} else { } else {
menu_item.label.as_str() menu_item.label.as_str()
@ -462,9 +457,10 @@ fn filter_widgets<T>(
inner_box: &FlowBox, inner_box: &FlowBox,
) { ) {
if items.is_empty() { if items.is_empty() {
items.iter().for_each(|(child, _)| { for (child, _) in items.iter() {
child.set_visible(true); child.set_visible(true);
}); }
if let Some(child) = inner_box.first_child() { if let Some(child) = inner_box.first_child() {
child.grab_focus(); child.grab_focus();
let fb = child.downcast::<FlowBoxChild>(); let fb = child.downcast::<FlowBoxChild>();
@ -476,9 +472,8 @@ fn filter_widgets<T>(
} }
let query = query.to_owned().to_lowercase(); let query = query.to_owned().to_lowercase();
let mut highest_score = -1.0;
let mut fb: Option<&FlowBoxChild> = None; let mut fb: Option<&FlowBoxChild> = None;
items.iter_mut().for_each(|(flowbox_child, mut menu_item)| { for (flowbox_child, menu_item) in items.iter_mut() {
let menu_item_search = format!( let menu_item_search = format!(
"{} {}", "{} {}",
menu_item menu_item
@ -519,12 +514,11 @@ fn filter_widgets<T>(
menu_item.search_sort_score = search_sort_score; menu_item.search_sort_score = search_sort_score;
if visible { if visible {
highest_score = search_sort_score;
fb = Some(flowbox_child); fb = Some(flowbox_child);
} }
flowbox_child.set_visible(visible); flowbox_child.set_visible(visible);
}); }
if let Some(top_item) = fb { if let Some(top_item) = fb {
inner_box.select_child(top_item); inner_box.select_child(top_item);
@ -532,9 +526,12 @@ fn filter_widgets<T>(
} }
} }
fn percent_or_absolute(value: &String, base_value: i32) -> Option<i32> { // allowed because truncating is fine, we do no need the precision
if value.contains("%") { #[allow(clippy::cast_possible_truncation)]
let value = value.replace("%", "").trim().to_string(); #[allow(clippy::cast_precision_loss)]
fn percent_or_absolute(value: &str, base_value: i32) -> Option<i32> {
if value.contains('%') {
let value = value.replace('%', "").trim().to_string();
match value.parse::<i32>() { match value.parse::<i32>() {
Ok(n) => Some(((n as f32 / 100.0) * base_value as f32) as i32), Ok(n) => Some(((n as f32 / 100.0) * base_value as f32) as i32),
Err(_) => None, Err(_) => None,
@ -544,7 +541,9 @@ fn percent_or_absolute(value: &String, base_value: i32) -> Option<i32> {
} }
} }
pub fn initialize_sort_scores<T>(items: &mut Vec<MenuItem<T>>) { // highly unlikely that we are dealing with > i64 items
#[allow(clippy::cast_possible_wrap)]
pub fn initialize_sort_scores<T>(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));
@ -562,19 +561,17 @@ fn wrap_text(text: &str, line_length: Option<usize>) -> String {
let len = line_length.unwrap_or(text.len()); let len = line_length.unwrap_or(text.len());
for word in text.split_whitespace() { for word in text.split_whitespace() {
if line.len() + word.len() + 1 > len { if line.len() + word.len() + 1 > len && !line.is_empty() {
if !line.is_empty() { result.push_str(line.trim_end());
result.push_str(&line.trim_end()); result.push('\n');
result.push('\n'); line.clear();
line.clear();
}
} }
line.push_str(word); line.push_str(word);
line.push(' '); line.push(' ');
} }
if !line.is_empty() { if !line.is_empty() {
result.push_str(&line.trim_end()); result.push_str(line.trim_end());
} }
result result

View file

@ -1,5 +1,4 @@
pub mod config; pub mod config;
pub mod desktop; pub mod desktop;
pub mod gui; pub mod gui;
pub mod system;
pub mod mode; pub mod mode;

View file

@ -1,8 +1,9 @@
use crate::lib::config::Config; use crate::config::Config;
use crate::lib::desktop::{default_icon, find_desktop_files, get_locale_variants}; use crate::desktop::{
use crate::lib::gui; default_icon, find_desktop_files, get_locale_variants, lookup_name_with_locale,
use crate::lib::gui::MenuItem; };
use crate::lookup_name_with_locale; use crate::gui;
use crate::gui::MenuItem;
use anyhow::{Context, anyhow}; use anyhow::{Context, anyhow};
use freedesktop_file_parser::EntryType; use freedesktop_file_parser::EntryType;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -18,9 +19,12 @@ struct DRunCache {
run_count: usize, run_count: usize,
} }
pub fn d_run(mut config: Config) -> anyhow::Result<()> { /// # Errors
///
/// Will return `Err` if it was not able to spawn the process
pub fn d_run(config: &Config) -> anyhow::Result<()> {
let locale_variants = get_locale_variants(); let locale_variants = get_locale_variants();
let default_icon = default_icon(); let default_icon = default_icon().unwrap_or_default();
let cache_path = dirs::cache_dir().map(|x| x.join("worf-drun")); let cache_path = dirs::cache_dir().map(|x| x.join("worf-drun"));
let mut d_run_cache = { let mut d_run_cache = {
@ -30,29 +34,26 @@ pub fn d_run(mut config: Config) -> anyhow::Result<()> {
} }
} }
load_cache_file(&cache_path).unwrap_or_default() load_cache_file(cache_path.as_ref()).unwrap_or_default()
}; };
let mut entries: Vec<MenuItem<String>> = Vec::new(); let mut entries: Vec<MenuItem<String>> = Vec::new();
for file in find_desktop_files().iter().filter(|f| { for file in find_desktop_files().ok().iter().flatten().filter(|f| {
f.entry.hidden.map_or(true, |hidden| !hidden) f.entry.hidden.is_none_or(|hidden| !hidden)
&& f.entry.no_display.map_or(true, |no_display| !no_display) && f.entry.no_display.is_none_or(|no_display| !no_display)
}) { }) {
let (action, working_dir) = match &file.entry.entry_type { let (action, working_dir) = match &file.entry.entry_type {
EntryType::Application(app) => (app.exec.clone(), app.path.clone()), EntryType::Application(app) => (app.exec.clone(), app.path.clone()),
_ => (None, None), _ => (None, None),
}; };
let name = match lookup_name_with_locale( let Some(name) = lookup_name_with_locale(
&locale_variants, &locale_variants,
&file.entry.name.variants, &file.entry.name.variants,
&file.entry.name.default, &file.entry.name.default,
) { ) else {
Some(name) => name, log::debug!("Skipping desktop entry without name {file:?}");
None => { continue;
log::debug!("Skipping desktop entry without name {file:?}");
continue;
}
}; };
let icon = file let icon = file
@ -76,30 +77,31 @@ pub fn d_run(mut config: Config) -> anyhow::Result<()> {
}; };
file.actions.iter().for_each(|(_, action)| { file.actions.iter().for_each(|(_, action)| {
let action_name = lookup_name_with_locale( if let Some(action_name) = lookup_name_with_locale(
&locale_variants, &locale_variants,
&action.name.variants, &action.name.variants,
&action.name.default, &action.name.default,
); ) {
let action_icon = action let action_icon = action
.icon .icon
.as_ref() .as_ref()
.map(|s| s.content.clone()) .map(|s| s.content.clone())
.or(icon.as_ref().map(|s| s.clone())); .or(icon.clone());
log::debug!("sub, action_name={action_name:?}, action_icon={action_icon:?}"); log::debug!("sub, action_name={action_name:?}, action_icon={action_icon:?}");
let sub_entry = MenuItem { let sub_entry = MenuItem {
label: action_name.unwrap().trim().to_owned(), label: action_name,
icon_path: action_icon, icon_path: action_icon,
action: action.exec.clone(), action: action.exec.clone(),
sub_elements: Vec::default(), sub_elements: Vec::default(),
working_dir: working_dir.clone(), working_dir: working_dir.clone(),
initial_sort_score: 0, // subitems are never sorted right now. initial_sort_score: 0, // subitems are never sorted right now.
search_sort_score: 0.0, search_sort_score: 0.0,
data: None, data: None,
}; };
entry.sub_elements.push(sub_entry); entry.sub_elements.push(sub_entry);
}
}); });
entries.push(entry); entries.push(entry);
@ -113,13 +115,13 @@ pub fn d_run(mut config: Config) -> anyhow::Result<()> {
Ok(selected_item) => { Ok(selected_item) => {
if let Some(cache) = cache_path { if let Some(cache) = cache_path {
*d_run_cache.entry(selected_item.label).or_insert(0) += 1; *d_run_cache.entry(selected_item.label).or_insert(0) += 1;
if let Err(e) = save_cache_file(&cache, d_run_cache) { if let Err(e) = save_cache_file(&cache, &d_run_cache) {
log::warn!("cannot save drun cache {e:?}"); log::warn!("cannot save drun cache {e:?}");
} }
} }
if let Some(action) = selected_item.action { if let Some(action) = selected_item.action {
spawn_fork(&action, &selected_item.working_dir)? spawn_fork(&action, selected_item.working_dir.as_ref())?;
} }
} }
Err(e) => { Err(e) => {
@ -130,16 +132,15 @@ pub fn d_run(mut config: Config) -> anyhow::Result<()> {
Ok(()) Ok(())
} }
fn save_cache_file(path: &PathBuf, data: HashMap<String, i64>) -> anyhow::Result<()> { fn save_cache_file(path: &PathBuf, data: &HashMap<String, i64>) -> anyhow::Result<()> {
// Convert the HashMap to TOML string // Convert the HashMap to TOML string
let toml_string = toml::ser::to_string(&data).map_err(|e| anyhow::anyhow!(e))?; let toml_string = toml::ser::to_string(&data).map_err(|e| anyhow::anyhow!(e))?;
fs::write(path, toml_string).map_err(|e| anyhow::anyhow!(e)) fs::write(path, toml_string).map_err(|e| anyhow::anyhow!(e))
} }
fn load_cache_file(cache_path: &Option<PathBuf>) -> anyhow::Result<HashMap<String, i64>> { fn load_cache_file(cache_path: Option<&PathBuf>) -> anyhow::Result<HashMap<String, i64>> {
let path = match cache_path { let Some(path) = cache_path else {
Some(p) => p, return Err(anyhow!("Cache is missing"));
None => return Err(anyhow!("Cache is missing")),
}; };
let toml_content = fs::read_to_string(path)?; let toml_content = fs::read_to_string(path)?;
@ -151,7 +152,7 @@ fn load_cache_file(cache_path: &Option<PathBuf>) -> anyhow::Result<HashMap<Strin
if let toml::Value::Integer(i) = val { if let toml::Value::Integer(i) = val {
result.insert(key, i); result.insert(key, i);
} else { } else {
log::warn!("Skipping key '{}' because it's not an integer", key); log::warn!("Skipping key '{key}' because it's not an integer");
} }
} }
} }
@ -162,7 +163,7 @@ fn create_file_if_not_exists(path: &PathBuf) -> anyhow::Result<()> {
let file = fs::OpenOptions::new() let file = fs::OpenOptions::new()
.write(true) .write(true)
.create_new(true) .create_new(true)
.open(&path); .open(path);
match file { match file {
Ok(_) => Ok(()), Ok(_) => Ok(()),
@ -172,7 +173,7 @@ fn create_file_if_not_exists(path: &PathBuf) -> anyhow::Result<()> {
} }
} }
fn spawn_fork(cmd: &str, working_dir: &Option<String>) -> anyhow::Result<()> { fn spawn_fork(cmd: &str, working_dir: Option<&String>) -> anyhow::Result<()> {
// todo probably remove arguments? // todo probably remove arguments?
// todo support working dir // todo support working dir
// todo fix actions // todo fix actions
@ -192,7 +193,7 @@ fn spawn_fork(cmd: &str, working_dir: &Option<String>) -> anyhow::Result<()> {
let args: Vec<_> = parts let args: Vec<_> = parts
.iter() .iter()
.skip(1) .skip(1)
.filter(|arg| !arg.starts_with("%")) .filter(|arg| !arg.starts_with('%'))
.collect(); .collect();
unsafe { unsafe {

View file

@ -1,3 +0,0 @@
use anyhow::anyhow;
use std::env;
use std::path::PathBuf;

View file

@ -1,33 +1,7 @@
#![warn(clippy::pedantic)] use std::env;
#![allow(clippy::implicit_return)]
// todo resolve paths like ~/
use crate::lib::config::Config;
use crate::lib::desktop::{default_icon, find_desktop_files, get_locale_variants};
use crate::lib::{config, gui, mode};
use crate::lib::gui::MenuItem;
use anyhow::{Error, anyhow};
use clap::Parser;
use freedesktop_file_parser::{DesktopAction, EntryType};
use gdk4::prelude::Cast;
use gtk4::prelude::{
ApplicationExt, ApplicationExtManual, BoxExt, ButtonExt, EditableExt, EntryExt,
FlowBoxChildExt, GtkWindowExt, ListBoxRowExt, NativeExt, ObjectExt, SurfaceExt, WidgetExt,
};
use gtk4_layer_shell::LayerShell;
use log::{debug, info, warn};
use std::collections::HashMap;
use std::ops::Deref;
use std::os::unix::process::CommandExt;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::sync::Arc;
use std::thread::sleep;
use std::{env, fs, time};
mod lib;
use anyhow::anyhow;
use worf_lib::{config, mode};
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
gtk4::init()?; gtk4::init()?;
@ -42,11 +16,15 @@ fn main() -> anyhow::Result<()> {
if let Some(show) = &config.show { if let Some(show) = &config.show {
match show { match show {
config::Mode::Run => {} config::Mode::Run => {
config::Mode::Drun => { todo!("run not implemented")
mode::d_run(config)?; }
config::Mode::Drun => {
mode::d_run(&config)?;
}
config::Mode::Dmenu => {
todo!("dmenu not implemented")
} }
config::Mode::Dmenu => {}
} }
Ok(()) Ok(())
@ -55,20 +33,6 @@ fn main() -> anyhow::Result<()> {
} }
} }
fn lookup_name_with_locale(
locale_variants: &Vec<String>,
variants: &HashMap<String, String>,
fallback: &str,
) -> Option<String> {
locale_variants
.iter()
.filter_map(|local| variants.get(local))
.next()
.map(|name| name.to_owned())
.or_else(|| Some(fallback.to_owned()))
}
// //
// fn main() -> anyhow::Result<()> { // fn main() -> anyhow::Result<()> {
// env_logger::Builder::new() // env_logger::Builder::new()

View file

@ -1 +0,0 @@
pub mod args;