fix warnings
This commit is contained in:
parent
f398848dcf
commit
efb0c3798e
11 changed files with 375 additions and 362 deletions
20
.github/workflows/rust.yml
vendored
20
.github/workflows/rust.yml
vendored
|
|
@ -15,12 +15,14 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Format
|
||||
run: cargo fmt --check
|
||||
- name: Clippy
|
||||
run: cargo clippy
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
- uses: actions/checkout@v4
|
||||
- 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 -- --show-output
|
||||
|
|
|
|||
16
Cargo.lock
generated
16
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
21
Cargo.toml
21
Cargo.toml
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)?;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
.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
|
||||
// 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_map(|local| variants.get(local))
|
||||
.map(std::borrow::ToOwned::to_owned)
|
||||
.or_else(|| Some(fallback.to_owned()))
|
||||
}
|
||||
|
|
|
|||
199
src/lib/gui.rs
199
src/lib/gui.rs
|
|
@ -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");
|
||||
gtk4::style_context_add_provider_for_display(
|
||||
&display,
|
||||
&provider,
|
||||
gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||
);
|
||||
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(>k_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 {
|
||||
inner_box.set_valign(Align::Center);
|
||||
} else {
|
||||
if config.orientation.unwrap() == config::Orientation::Horizontal {
|
||||
inner_box.set_valign(Align::Center);
|
||||
} else {
|
||||
inner_box.set_valign(Align::Start);
|
||||
}
|
||||
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,12 +280,10 @@ fn sort_menu_items<T>(
|
|||
} else {
|
||||
Ordering::Larger
|
||||
}
|
||||
} else if menu1.initial_sort_score < menu2.initial_sort_score {
|
||||
Ordering::Smaller
|
||||
} else {
|
||||
if menu1.initial_sort_score < menu2.initial_sort_score {
|
||||
Ordering::Smaller
|
||||
} else {
|
||||
Ordering::Larger
|
||||
}
|
||||
Ordering::Larger
|
||||
}
|
||||
}
|
||||
(Some(_), None) => Ordering::Larger,
|
||||
|
|
@ -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());
|
||||
result.push('\n');
|
||||
line.clear();
|
||||
}
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
pub mod config;
|
||||
pub mod desktop;
|
||||
pub mod gui;
|
||||
pub mod system;
|
||||
pub mod mode;
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
log::debug!("Skipping desktop entry without name {file:?}");
|
||||
continue;
|
||||
}
|
||||
) else {
|
||||
log::debug!("Skipping desktop entry without name {file:?}");
|
||||
continue;
|
||||
};
|
||||
|
||||
let icon = file
|
||||
|
|
@ -76,30 +77,31 @@ 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()));
|
||||
) {
|
||||
let action_icon = action
|
||||
.icon
|
||||
.as_ref()
|
||||
.map(|s| s.content.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 {
|
||||
label: action_name.unwrap().trim().to_owned(),
|
||||
icon_path: action_icon,
|
||||
action: action.exec.clone(),
|
||||
sub_elements: Vec::default(),
|
||||
working_dir: working_dir.clone(),
|
||||
initial_sort_score: 0, // subitems are never sorted right now.
|
||||
search_sort_score: 0.0,
|
||||
data: None,
|
||||
};
|
||||
entry.sub_elements.push(sub_entry);
|
||||
let sub_entry = MenuItem {
|
||||
label: action_name,
|
||||
icon_path: action_icon,
|
||||
action: action.exec.clone(),
|
||||
sub_elements: Vec::default(),
|
||||
working_dir: working_dir.clone(),
|
||||
initial_sort_score: 0, // subitems are never sorted right now.
|
||||
search_sort_score: 0.0,
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
use anyhow::anyhow;
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
58
src/main.rs
58
src/main.rs
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
pub mod args;
|
||||
Loading…
Add table
Reference in a new issue