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::collections::HashMap;
use std::path::Path; use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use std::{fs, string}; use std::{env, fs, string};
pub struct IconResolver { pub struct IconResolver {
cache: HashMap<String, String>, cache: HashMap<String, String>,
@ -165,3 +165,73 @@ pub(crate) fn find_desktop_files() -> Vec<DesktopFile> {
.collect(); .collect();
p 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; use std::process::exit;
#[derive(Clone)] #[derive(Clone)]
pub struct EntryElement { pub struct MenuItem {
pub label: String, // todo support empty label? pub label: String, // todo support empty label?
pub icon_path: Option<String>, pub icon_path: Option<String>,
pub action: 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 // Load CSS
let provider = CssProvider::new(); let provider = CssProvider::new();
let css_file_path = File::for_path("/home/me/.config/wofi/style.css"); 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); inner_box.set_activate_on_single_click(true);
for entry in &elements { for entry in &elements {
add_entry_element(&inner_box, &entry); add_menu_item(&inner_box, &entry);
} }
// Set focus after everything is realized // Set focus after everything is realized
@ -216,8 +216,8 @@ fn setup_key_event_handler(
window.add_controller(key_controller); window.add_controller(key_controller);
} }
fn add_entry_element(inner_box: &FlowBox, entry_element: &EntryElement) { fn add_menu_item(inner_box: &FlowBox, entry_element: &MenuItem) {
let parent: Widget = if entry_element.sub_elements.is_some() { let parent: Widget = if !entry_element.sub_elements.is_empty() {
let expander = Expander::new(None); let expander = Expander::new(None);
// Inline label as expander label // 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 subelements do not fill full space yet.
// todo multi nesting is not supported 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(); let row = ListBoxRow::new();
row.set_widget_name("entry"); row.set_widget_name("entry");
let label = Label::new(Some(&x.label)); let label = Label::new(Some(&x.label));
label.set_halign(Align::Start);
row.set_child(Some(&label)); row.set_child(Some(&label));
list_box.append(&row); list_box.append(&row);
} }

View file

@ -3,8 +3,8 @@
use crate::args::{Args, Mode}; use crate::args::{Args, Mode};
use crate::config::{Config, merge_config_with_args}; use crate::config::{Config, merge_config_with_args};
use crate::desktop::find_desktop_files; use crate::desktop::{default_icon, find_desktop_files, get_locale_variants};
use crate::gui::EntryElement; use crate::gui::MenuItem;
use clap::Parser; use clap::Parser;
use gdk4::prelude::Cast; use gdk4::prelude::Cast;
use gtk4::prelude::{ use gtk4::prelude::{
@ -12,7 +12,7 @@ use gtk4::prelude::{
FlowBoxChildExt, GtkWindowExt, ListBoxRowExt, NativeExt, ObjectExt, SurfaceExt, WidgetExt, FlowBoxChildExt, GtkWindowExt, ListBoxRowExt, NativeExt, ObjectExt, SurfaceExt, WidgetExt,
}; };
use gtk4_layer_shell::LayerShell; use gtk4_layer_shell::LayerShell;
use log::{debug, warn}; use log::{debug, info, warn};
use merge::Merge; use merge::Merge;
use std::collections::HashMap; use std::collections::HashMap;
use std::ops::Deref; use std::ops::Deref;
@ -22,6 +22,7 @@ use std::process::{Command, Stdio};
use std::sync::Arc; use std::sync::Arc;
use std::thread::sleep; use std::thread::sleep;
use std::{env, fs, time}; use std::{env, fs, time};
use freedesktop_file_parser::{DesktopAction, EntryType};
mod args; mod args;
mod config; mod config;
@ -82,85 +83,61 @@ fn main() -> anyhow::Result<()> {
Ok(()) Ok(())
} }
fn get_locale_variants() -> Vec<String> { fn lookup_name_with_locale(
let locale = env::var("LC_ALL") locale_variants: &Vec<String>,
.or_else(|_| env::var("LC_MESSAGES")) variants: &HashMap<String, String>,
.or_else(|_| env::var("LANG")) fallback: &str,
.unwrap_or_else(|_| "c".to_string()); ) -> Option<String> {
locale_variants
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 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() .iter()
.find(|locale| { .filter_map(|local| variants.get(local))
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() .next()
.map(|name| name.deref().clone()) .map(|name| name.to_owned())
.or_else(|| Some(&file.entry.name.default)); .or_else(|| Some(fallback.to_owned()))
}
debug!("{n:?}") fn drun(mut config: Config) -> anyhow::Result<()> {
let mut entries: Vec<MenuItem> = Vec::new();
let locale_variants = get_locale_variants();
let default_icon = default_icon();
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 desktop = Some("desktop entry");
// let locale = // let locale =