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
|
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
16
Cargo.lock
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
21
Cargo.toml
21
Cargo.toml
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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)?;
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
199
src/lib/gui.rs
199
src/lib/gui.rs
|
|
@ -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(>k_args);
|
app.run_with_args(>k_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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
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()
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
pub mod args;
|
|
||||||
Loading…
Add table
Reference in a new issue