support sub elements, improve desktop file parsing

This commit is contained in:
Alexander Mohr 2025-04-09 22:32:06 +02:00
parent 8ebefeffe6
commit eb2d59070c
3 changed files with 134 additions and 86 deletions

View file

@ -8,7 +8,7 @@ use regex::Regex;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use std::{fs, string};
use std::{env, fs, string};
pub struct IconResolver {
cache: HashMap<String, String>,
@ -165,3 +165,73 @@ pub(crate) fn find_desktop_files() -> Vec<DesktopFile> {
.collect();
p
}
pub fn get_locale_variants() -> Vec<String> {
let locale = env::var("LC_ALL")
.or_else(|_| env::var("LC_MESSAGES"))
.or_else(|_| env::var("LANG"))
.unwrap_or_else(|_| "c".to_string());
let lang = locale.split('.').next().unwrap_or(&locale).to_lowercase();
let mut variants = vec![];
if let Some((lang_part, region)) = lang.split_once('_') {
variants.push(format!("{}_{region}", lang_part)); // en_us
variants.push(lang_part.to_string()); // en
} else {
variants.push(lang.clone()); // e.g. "fr"
}
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
}

View file

@ -20,14 +20,14 @@ use log::{debug, error, info};
use std::process::exit;
#[derive(Clone)]
pub struct EntryElement {
pub struct MenuItem {
pub label: String, // todo support empty label?
pub icon_path: Option<String>,
pub action: Option<String>,
pub sub_elements: Option<Vec<EntryElement>>,
pub sub_elements: Vec<MenuItem>,
}
pub fn show(config: Config, elements: Vec<EntryElement>) -> anyhow::Result<(i32)> {
pub fn show(config: Config, elements: Vec<MenuItem>) -> anyhow::Result<(i32)> {
// Load CSS
let provider = CssProvider::new();
let css_file_path = File::for_path("/home/me/.config/wofi/style.css");
@ -118,7 +118,7 @@ pub fn show(config: Config, elements: Vec<EntryElement>) -> anyhow::Result<(i32)
inner_box.set_activate_on_single_click(true);
for entry in &elements {
add_entry_element(&inner_box, &entry);
add_menu_item(&inner_box, &entry);
}
// Set focus after everything is realized
@ -216,8 +216,8 @@ fn setup_key_event_handler(
window.add_controller(key_controller);
}
fn add_entry_element(inner_box: &FlowBox, entry_element: &EntryElement) {
let parent: Widget = if entry_element.sub_elements.is_some() {
fn add_menu_item(inner_box: &FlowBox, entry_element: &MenuItem) {
let parent: Widget = if !entry_element.sub_elements.is_empty() {
let expander = Expander::new(None);
// Inline label as expander label
@ -228,11 +228,12 @@ fn add_entry_element(inner_box: &FlowBox, entry_element: &EntryElement) {
// todo subelements do not fill full space yet.
// todo multi nesting is not supported yet.
for x in entry_element.sub_elements.iter().flatten() {
for x in entry_element.sub_elements.iter(){
let row = ListBoxRow::new();
row.set_widget_name("entry");
let label = Label::new(Some(&x.label));
label.set_halign(Align::Start);
row.set_child(Some(&label));
list_box.append(&row);
}

View file

@ -3,8 +3,8 @@
use crate::args::{Args, Mode};
use crate::config::{Config, merge_config_with_args};
use crate::desktop::find_desktop_files;
use crate::gui::EntryElement;
use crate::desktop::{default_icon, find_desktop_files, get_locale_variants};
use crate::gui::MenuItem;
use clap::Parser;
use gdk4::prelude::Cast;
use gtk4::prelude::{
@ -12,7 +12,7 @@ use gtk4::prelude::{
FlowBoxChildExt, GtkWindowExt, ListBoxRowExt, NativeExt, ObjectExt, SurfaceExt, WidgetExt,
};
use gtk4_layer_shell::LayerShell;
use log::{debug, warn};
use log::{debug, info, warn};
use merge::Merge;
use std::collections::HashMap;
use std::ops::Deref;
@ -22,6 +22,7 @@ use std::process::{Command, Stdio};
use std::sync::Arc;
use std::thread::sleep;
use std::{env, fs, time};
use freedesktop_file_parser::{DesktopAction, EntryType};
mod args;
mod config;
@ -82,85 +83,61 @@ fn main() -> anyhow::Result<()> {
Ok(())
}
fn get_locale_variants() -> Vec<String> {
let locale = env::var("LC_ALL")
.or_else(|_| env::var("LC_MESSAGES"))
.or_else(|_| env::var("LANG"))
.unwrap_or_else(|_| "c".to_string());
let lang = locale.split('.').next().unwrap_or(&locale).to_lowercase();
let mut variants = vec![];
if let Some((lang_part, region)) = lang.split_once('_') {
variants.push(format!("{}_{region}", lang_part)); // en_us
variants.push(lang_part.to_string()); // en
} else {
variants.push(lang.clone()); // e.g. "fr"
}
variants
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 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
}
fn drun(mut config: Config) -> anyhow::Result<()> {
let mut entries: Vec<EntryElement> = Vec::new();
for file in &find_desktop_files() {
let n = get_locale_variants()
.iter()
.filter_map(|local| file.entry.name.variants.get(local))
.next()
.map(|name| name.deref().clone())
.or_else(|| Some(&file.entry.name.default));
let mut entries: Vec<MenuItem> = Vec::new();
let locale_variants = get_locale_variants();
let default_icon = default_icon();
debug!("{n:?}")
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)
// todo handle not shown in?
}) {
let name = lookup_name_with_locale(
&locale_variants,
&file.entry.name.variants,
&file.entry.name.default,
);
if name.is_none() {
debug!("Skipping desktop entry without name {file:?}")
}
let mut entry = MenuItem {
label: name.unwrap(),
icon_path: None,
action: None,
sub_elements: Vec::default(),
};
file.actions.iter().for_each(|(_, action)| {
let action_name = lookup_name_with_locale(
&locale_variants,
&action.name.variants,
&action.name.default,
);
let sub_entry = MenuItem {
label: action_name.unwrap().trim().to_owned(),
icon_path: None,
action: None,
sub_elements: Vec::default(),
};
entry.sub_elements.push(sub_entry);
});
entries.push(entry);
// let desktop = Some("desktop entry");
// let locale =