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

@ -16,11 +16,13 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Format
run: cargo fmt --check
- name: Clippy
run: cargo clippy
- uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Formatting
run: cargo fmt --all -- --check
- name: Clippy warnings
run: cargo clippy -- -D warnings
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
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"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "configparser"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe1d7dcda7d1da79e444bdfba1465f2f849a58b07774e1df473ee77030cb47a7"
[[package]]
name = "crossbeam"
version = "0.8.4"
@ -912,15 +906,6 @@ dependencies = [
"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]]
name = "is_terminal_polyfill"
version = "1.70.1"
@ -1686,7 +1671,6 @@ dependencies = [
"gtk4-layer-shell",
"home",
"hyprland",
"ini",
"libc",
"log",
"regex",

View file

@ -3,6 +3,24 @@ name = "worf"
version = "0.1.0"
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]
gtk4 = { version = "0.9.5", default-features = true, features = ["v4_6"] }
gtk4-layer-shell = "0.5.0"
@ -13,7 +31,6 @@ home = "0.5.11"
log = "0.4.27"
regex = "1.11.1"
hyprland = "0.4.0-beta.2"
ini = "1.3.0"
clap = { version = "4.5.35", features = ["derive"] }
thiserror = "2.0.12"
serde = { version = "1.0.219", features = ["derive"] }
@ -21,6 +38,6 @@ toml = "0.8.20"
serde_json = "1.0.140"
crossbeam = "0.8.4"
libc = "0.2.171"
freedesktop-file-parser = "0.1.0"
freedesktop-file-parser = "0.1.3"
strsim = "0.11.1"
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::str::FromStr;
use std::{env, fs};
use anyhow::anyhow;
use clap::{Parser, ValueEnum};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use thiserror::Error;
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug, Serialize, Deserialize)]
@ -34,10 +30,10 @@ pub enum Align {
#[derive(Clone, Debug, Serialize, Deserialize)]
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,
/// searches $XDG_DATA_HOME/applications and $XDG_DATA_DIRS/applications f
/// or desktop files and allows them to be run by selecting them.
/// searches `$XDG_DATA_HOME/applications` and `$XDG_DATA_DIRS/applications`
/// for desktop files and allows them to be run by selecting them.
Drun,
/// 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>,
/// Defines the style sheet to be loaded.
/// Defaults to $XDG_CONF_DIR/worf/style.css
/// or $HOME/.config/worf/style.css if XDG_CONF_DIR is not set.
/// Defaults to `$XDG_CONF_DIR/worf/style.css`
/// or `$HOME/.config/worf/style.css` if `$XDG_CONF_DIR` is not set.
#[serde(default = "default_style")]
#[clap(long = "style")]
pub style: Option<String>,
@ -252,8 +248,6 @@ pub struct Config {
#[serde(default = "default_text_wrap_length")]
#[clap(long = "text-wrap-length")]
pub text_wrap_length: Option<usize>,
}
impl Default for Config {
@ -327,28 +321,45 @@ impl Default for Config {
}
}
}
fn default_row_box_orientation() -> Option<Orientation> {
// allowed because option is needed for serde macro
#[allow(clippy::unnecessary_wraps)]
#[must_use]
pub fn default_row_box_orientation() -> Option<Orientation> {
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)
}
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)
}
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)
}
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)
}
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
}
@ -429,77 +440,112 @@ fn default_normal_window() -> bool {
// key_default = "Ctrl-c";
// 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)
.ok()
.and_then(|pb| Some(pb.display().to_string()))
.map(|pb| pb.display().to_string())
.or_else(|| {
log::error!("no stylesheet found, using system styles");
None
})
}
// allowed because option is needed for serde macro
#[allow(clippy::unnecessary_wraps)]
#[must_use]
pub fn default_height() -> Option<String> {
Some("40%".to_owned())
}
// allowed because option is needed for serde macro
#[allow(clippy::unnecessary_wraps)]
#[must_use]
pub fn default_width() -> Option<String> {
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> {
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> {
Some(10)
}
// allowed because option is needed for serde macro
#[allow(clippy::unnecessary_wraps)]
#[must_use]
pub fn default_fuzzy_min_score() -> Option<f64> {
Some(0.1)
}
// allowed because option is needed for serde macro
#[allow(clippy::unnecessary_wraps)]
#[must_use]
pub fn default_match_method() -> Option<MatchMethod> {
Some(MatchMethod::Contains)
}
// allowed because option is needed for serde macro
#[allow(clippy::unnecessary_wraps)]
#[must_use]
pub fn default_image_size() -> Option<i32> {
Some(32)
}
// allowed because option is needed for serde macro
#[allow(clippy::unnecessary_wraps)]
#[must_use]
pub fn default_text_wrap_length() -> Option<usize> {
Some(15)
}
// allowed because option is needed for serde macro
#[allow(clippy::unnecessary_wraps)]
#[must_use]
pub fn default_text_wrap() -> Option<bool> {
Some(false)
}
#[must_use]
pub fn parse_args() -> Config {
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> {
let alternative_paths = path_alternatives(vec![dirs::config_dir()], PathBuf::from("worf").join("style.css"));
resolve_path(
full_path,
alternative_paths
.into_iter()
.collect(),
)
let alternative_paths = path_alternatives(
vec![dirs::config_dir()],
&PathBuf::from("worf").join("style.css"),
);
resolve_path(full_path, alternative_paths.into_iter().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
.into_iter()
.filter_map(|s| s)
.map(|pb| pb.join(&sub_path))
.flatten()
.map(|pb| pb.join(sub_path))
.filter_map(|pb| pb.canonicalize().ok())
.filter(|c| c.exists())
.collect()
}
/// # Errors
///
/// Will return `Err` if it is not able to find any valid path
pub fn resolve_path(
full_path: Option<String>,
alternatives: Vec<PathBuf>,
@ -513,16 +559,21 @@ pub fn resolve_path(
.filter(|p| p.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> {
let home_dir = env::var("HOME")?;
let config_path = args_opt.as_ref().map(|c| {
c.config
.as_ref()
.and_then(|p| Some(PathBuf::from(p)))
.unwrap_or_else(|| {
c.config.as_ref().map_or_else(
|| {
env::var("XDG_CONF_HOME")
.map_or(
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("config")
})
},
PathBuf::from,
)
});
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> {
let args_json = serde_json::to_value(args)?;
let mut config_json = serde_json::to_value(config)?;

View file

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

View file

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

View file

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

View file

@ -1,8 +1,9 @@
use crate::lib::config::Config;
use crate::lib::desktop::{default_icon, find_desktop_files, get_locale_variants};
use crate::lib::gui;
use crate::lib::gui::MenuItem;
use crate::lookup_name_with_locale;
use crate::config::Config;
use crate::desktop::{
default_icon, find_desktop_files, get_locale_variants, lookup_name_with_locale,
};
use crate::gui;
use crate::gui::MenuItem;
use anyhow::{Context, anyhow};
use freedesktop_file_parser::EntryType;
use serde::{Deserialize, Serialize};
@ -18,9 +19,12 @@ struct DRunCache {
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 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 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();
for file in find_desktop_files().iter().filter(|f| {
f.entry.hidden.map_or(true, |hidden| !hidden)
&& f.entry.no_display.map_or(true, |no_display| !no_display)
for file in find_desktop_files().ok().iter().flatten().filter(|f| {
f.entry.hidden.is_none_or(|hidden| !hidden)
&& f.entry.no_display.is_none_or(|no_display| !no_display)
}) {
let (action, working_dir) = match &file.entry.entry_type {
EntryType::Application(app) => (app.exec.clone(), app.path.clone()),
_ => (None, None),
};
let name = match lookup_name_with_locale(
let Some(name) = lookup_name_with_locale(
&locale_variants,
&file.entry.name.variants,
&file.entry.name.default,
) {
Some(name) => name,
None => {
) else {
log::debug!("Skipping desktop entry without name {file:?}");
continue;
}
};
let icon = file
@ -76,21 +77,21 @@ pub fn d_run(mut config: Config) -> anyhow::Result<()> {
};
file.actions.iter().for_each(|(_, action)| {
let action_name = lookup_name_with_locale(
if let Some(action_name) = lookup_name_with_locale(
&locale_variants,
&action.name.variants,
&action.name.default,
);
) {
let action_icon = action
.icon
.as_ref()
.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:?}");
let sub_entry = MenuItem {
label: action_name.unwrap().trim().to_owned(),
label: action_name,
icon_path: action_icon,
action: action.exec.clone(),
sub_elements: Vec::default(),
@ -100,6 +101,7 @@ pub fn d_run(mut config: Config) -> anyhow::Result<()> {
data: None,
};
entry.sub_elements.push(sub_entry);
}
});
entries.push(entry);
@ -113,13 +115,13 @@ pub fn d_run(mut config: Config) -> anyhow::Result<()> {
Ok(selected_item) => {
if let Some(cache) = cache_path {
*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:?}");
}
}
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) => {
@ -130,16 +132,15 @@ pub fn d_run(mut config: Config) -> anyhow::Result<()> {
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
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))
}
fn load_cache_file(cache_path: &Option<PathBuf>) -> anyhow::Result<HashMap<String, i64>> {
let path = match cache_path {
Some(p) => p,
None => return Err(anyhow!("Cache is missing")),
fn load_cache_file(cache_path: Option<&PathBuf>) -> anyhow::Result<HashMap<String, i64>> {
let Some(path) = cache_path else {
return Err(anyhow!("Cache is missing"));
};
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 {
result.insert(key, i);
} 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()
.write(true)
.create_new(true)
.open(&path);
.open(path);
match file {
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 support working dir
// todo fix actions
@ -192,7 +193,7 @@ fn spawn_fork(cmd: &str, working_dir: &Option<String>) -> anyhow::Result<()> {
let args: Vec<_> = parts
.iter()
.skip(1)
.filter(|arg| !arg.starts_with("%"))
.filter(|arg| !arg.starts_with('%'))
.collect();
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)]
#![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 std::env;
use anyhow::anyhow;
use worf_lib::{config, mode};
fn main() -> anyhow::Result<()> {
gtk4::init()?;
@ -42,11 +16,15 @@ fn main() -> anyhow::Result<()> {
if let Some(show) = &config.show {
match show {
config::Mode::Run => {}
config::Mode::Drun => {
mode::d_run(config)?;
config::Mode::Run => {
todo!("run not implemented")
}
config::Mode::Drun => {
mode::d_run(&config)?;
}
config::Mode::Dmenu => {
todo!("dmenu not implemented")
}
config::Mode::Dmenu => {}
}
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<()> {
// env_logger::Builder::new()

View file

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