2024-04-07 17:23:59 -04:00
|
|
|
use crate::config::{CommonConfig, TruncateMode};
|
|
|
|
use crate::modules::menu::{MenuEntry, XdgSection};
|
2025-06-21 14:21:54 +02:00
|
|
|
use crate::modules::launcher::default_launch_command;
|
2024-04-07 17:23:59 -04:00
|
|
|
use indexmap::IndexMap;
|
|
|
|
use serde::Deserialize;
|
|
|
|
|
|
|
|
/// An individual entry in the main menu section.
|
|
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
|
|
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
|
|
|
#[serde(tag = "type", rename_all = "snake_case")]
|
|
|
|
pub enum MenuConfig {
|
|
|
|
/// Contains all applications matching the configured `categories`.
|
|
|
|
XdgEntry(XdgEntry),
|
|
|
|
/// Contains all applications not covered by `xdg_entry` categories.
|
|
|
|
XdgOther,
|
|
|
|
/// Individual shell command entry.
|
|
|
|
Custom(CustomEntry),
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
|
|
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
|
|
|
pub struct XdgEntry {
|
|
|
|
/// Text to display on the button.
|
|
|
|
#[serde(default)]
|
|
|
|
pub label: String,
|
|
|
|
|
|
|
|
/// Name of the image icon to show next to the label.
|
|
|
|
#[serde(default)]
|
|
|
|
pub icon: Option<String>,
|
|
|
|
|
|
|
|
/// XDG categories the associated submenu should contain.
|
|
|
|
#[serde(default)]
|
|
|
|
pub categories: Vec<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Individual shell command entry.
|
|
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
|
|
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
|
|
|
pub struct CustomEntry {
|
|
|
|
/// Text to display on the button.
|
|
|
|
#[serde(default)]
|
|
|
|
pub label: String,
|
|
|
|
|
|
|
|
/// Name of the image icon to show next to the label.
|
|
|
|
///
|
|
|
|
/// **Default**: `null`
|
|
|
|
pub icon: Option<String>,
|
|
|
|
|
|
|
|
/// Shell command to execute when the button is clicked.
|
|
|
|
/// This is run using `sh -c`.
|
|
|
|
#[serde(default)]
|
|
|
|
pub on_click: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
|
|
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
|
|
|
pub struct MenuModule {
|
|
|
|
/// Items to add to the start of the main menu.
|
|
|
|
///
|
|
|
|
/// **Default**: `[]`
|
|
|
|
#[serde(default)]
|
|
|
|
pub(super) start: Vec<MenuConfig>,
|
|
|
|
|
|
|
|
/// Items to add to the start of the main menu.
|
|
|
|
///
|
|
|
|
/// By default, this shows a number of XDG entries
|
|
|
|
/// that should cover all common applications.
|
|
|
|
///
|
|
|
|
/// **Default**: See `examples/menu/default`
|
|
|
|
#[serde(default = "default_menu")]
|
|
|
|
pub(super) center: Vec<MenuConfig>,
|
|
|
|
|
|
|
|
/// Items to add to the end of the main menu.
|
|
|
|
///
|
|
|
|
/// **Default**: `[]`
|
|
|
|
#[serde(default)]
|
|
|
|
pub(super) end: Vec<MenuConfig>,
|
|
|
|
|
|
|
|
/// Fixed height of the menu.
|
|
|
|
///
|
|
|
|
/// When set, if the number of (sub)menu entries exceeds this value,
|
|
|
|
/// a scrollbar will be shown.
|
|
|
|
///
|
|
|
|
/// Leave null to resize dynamically.
|
|
|
|
///
|
|
|
|
/// **Default**: `null`
|
|
|
|
#[serde(default)]
|
|
|
|
pub(super) height: Option<i32>,
|
|
|
|
|
|
|
|
/// Fixed width of the menu.
|
|
|
|
///
|
|
|
|
/// Can be used with `truncate` options
|
|
|
|
/// to customise how item labels are truncated.
|
|
|
|
///
|
|
|
|
/// **Default**: `null`
|
|
|
|
#[serde(default)]
|
|
|
|
pub(super) width: Option<i32>,
|
|
|
|
|
|
|
|
/// Label to show on the menu button on the bar.
|
|
|
|
///
|
|
|
|
/// **Default**: `≡`
|
|
|
|
#[serde(default = "default_menu_popup_label")]
|
|
|
|
pub(super) label: Option<String>,
|
|
|
|
|
|
|
|
/// Icon to show on the menu button on the bar.
|
|
|
|
///
|
|
|
|
/// **Default**: `null`
|
|
|
|
#[serde(default)]
|
|
|
|
pub(super) label_icon: Option<String>,
|
|
|
|
|
|
|
|
/// Size of the `label_icon` image.
|
|
|
|
#[serde(default = "default_menu_popup_icon_size")]
|
|
|
|
pub(super) label_icon_size: i32,
|
|
|
|
|
|
|
|
// -- common --
|
|
|
|
/// Truncate options to apply to (sub)menu item labels.
|
|
|
|
///
|
|
|
|
/// See [truncate options](module-level-options#truncate-mode).
|
|
|
|
///
|
|
|
|
/// **Default**: `Auto (end)`
|
|
|
|
#[serde(default)]
|
|
|
|
pub(super) truncate: TruncateMode,
|
|
|
|
|
|
|
|
/// See [common options](module-level-options#common-options).
|
|
|
|
#[serde(flatten)]
|
|
|
|
pub common: Option<CommonConfig>,
|
2025-06-21 14:21:54 +02:00
|
|
|
|
2025-06-21 14:47:53 +02:00
|
|
|
/// Command used to launch applications.
|
|
|
|
///
|
|
|
|
/// **Default**: `gtk-launch`
|
2025-06-21 14:21:54 +02:00
|
|
|
#[serde(default = "default_launch_command")]
|
|
|
|
pub launch_command: String,
|
2024-04-07 17:23:59 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for MenuModule {
|
|
|
|
fn default() -> Self {
|
|
|
|
MenuModule {
|
|
|
|
start: vec![],
|
|
|
|
center: default_menu(),
|
|
|
|
end: vec![],
|
|
|
|
height: None,
|
|
|
|
width: None,
|
|
|
|
truncate: TruncateMode::default(),
|
|
|
|
// max_label_length: default_length(),
|
|
|
|
label: default_menu_popup_label(),
|
|
|
|
label_icon: None,
|
|
|
|
label_icon_size: default_menu_popup_icon_size(),
|
|
|
|
common: Some(CommonConfig::default()),
|
2025-06-21 14:21:54 +02:00
|
|
|
launch_command: default_launch_command(),
|
2024-04-07 17:23:59 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn default_menu() -> Vec<MenuConfig> {
|
|
|
|
vec![
|
|
|
|
MenuConfig::XdgEntry(XdgEntry {
|
|
|
|
label: "Accessories".to_string(),
|
|
|
|
icon: Some("accessories".to_string()),
|
|
|
|
categories: vec![
|
|
|
|
"Accessibility".to_string(),
|
|
|
|
"Core".to_string(),
|
|
|
|
"Legacy".to_string(),
|
|
|
|
"Utility".to_string(),
|
|
|
|
],
|
|
|
|
}),
|
|
|
|
MenuConfig::XdgEntry(XdgEntry {
|
|
|
|
label: "Development".to_string(),
|
|
|
|
icon: Some("applications-development".to_string()),
|
|
|
|
categories: vec!["Development".to_string()],
|
|
|
|
}),
|
|
|
|
MenuConfig::XdgEntry(XdgEntry {
|
|
|
|
label: "Education".to_string(),
|
|
|
|
icon: Some("applications-education".to_string()),
|
|
|
|
categories: vec!["Education".to_string()],
|
|
|
|
}),
|
|
|
|
MenuConfig::XdgEntry(XdgEntry {
|
|
|
|
label: "Games".to_string(),
|
|
|
|
icon: Some("applications-games".to_string()),
|
|
|
|
categories: vec!["Game".to_string()],
|
|
|
|
}),
|
|
|
|
MenuConfig::XdgEntry(XdgEntry {
|
|
|
|
label: "Graphics".to_string(),
|
|
|
|
icon: Some("applications-graphics".to_string()),
|
|
|
|
categories: vec!["Graphics".to_string()],
|
|
|
|
}),
|
|
|
|
MenuConfig::XdgEntry(XdgEntry {
|
|
|
|
label: "Multimedia".to_string(),
|
|
|
|
icon: Some("applications-multimedia".to_string()),
|
|
|
|
categories: vec![
|
|
|
|
"Audio".to_string(),
|
|
|
|
"Video".to_string(),
|
|
|
|
"AudioVideo".to_string(),
|
|
|
|
],
|
|
|
|
}),
|
|
|
|
MenuConfig::XdgEntry(XdgEntry {
|
|
|
|
label: "Network".to_string(),
|
|
|
|
icon: Some("applications-internet".to_string()),
|
|
|
|
categories: vec!["Network".to_string()],
|
|
|
|
}),
|
|
|
|
MenuConfig::XdgEntry(XdgEntry {
|
|
|
|
label: "Office".to_string(),
|
|
|
|
icon: Some("applications-office".to_string()),
|
|
|
|
categories: vec!["Office".to_string()],
|
|
|
|
}),
|
|
|
|
MenuConfig::XdgEntry(XdgEntry {
|
|
|
|
label: "Science".to_string(),
|
|
|
|
icon: Some("applications-science".to_string()),
|
|
|
|
categories: vec!["Science".to_string()],
|
|
|
|
}),
|
|
|
|
MenuConfig::XdgEntry(XdgEntry {
|
|
|
|
label: "System".to_string(),
|
|
|
|
icon: Some("applications-system".to_string()),
|
|
|
|
categories: vec!["Emulator".to_string(), "System".to_string()],
|
|
|
|
}),
|
|
|
|
MenuConfig::XdgOther,
|
|
|
|
MenuConfig::XdgEntry(XdgEntry {
|
|
|
|
label: "Settings".to_string(),
|
|
|
|
icon: Some("preferences-system".to_string()),
|
|
|
|
categories: vec!["Settings".to_string(), "Screensaver".to_string()],
|
|
|
|
}),
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
fn default_menu_popup_label() -> Option<String> {
|
|
|
|
Some("≡".to_string())
|
|
|
|
}
|
|
|
|
|
|
|
|
const fn default_menu_popup_icon_size() -> i32 {
|
|
|
|
16
|
|
|
|
}
|
|
|
|
|
|
|
|
pub const OTHER_LABEL: &str = "Other";
|
|
|
|
|
|
|
|
pub fn parse_config(
|
|
|
|
section_config: Vec<MenuConfig>,
|
|
|
|
sections_by_cat: &mut IndexMap<String, Vec<String>>,
|
|
|
|
) -> IndexMap<String, MenuEntry> {
|
|
|
|
section_config
|
|
|
|
.into_iter()
|
|
|
|
.map(|entry_config| match entry_config {
|
|
|
|
MenuConfig::XdgEntry(entry) => {
|
|
|
|
entry.categories.into_iter().for_each(|cat| {
|
|
|
|
let existing = sections_by_cat.get_mut(&cat);
|
|
|
|
|
|
|
|
if let Some(existing) = existing {
|
|
|
|
existing.push(entry.label.clone());
|
|
|
|
} else {
|
|
|
|
sections_by_cat.insert(cat, vec![entry.label.clone()]);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
(
|
|
|
|
entry.label.clone(),
|
|
|
|
MenuEntry::Xdg(XdgSection {
|
|
|
|
label: entry.label,
|
|
|
|
icon: entry.icon,
|
|
|
|
applications: IndexMap::new(),
|
|
|
|
}),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
MenuConfig::XdgOther => (
|
|
|
|
OTHER_LABEL.to_string(),
|
|
|
|
MenuEntry::Xdg(XdgSection {
|
|
|
|
label: OTHER_LABEL.to_string(),
|
|
|
|
icon: Some("applications-other".to_string()),
|
|
|
|
applications: IndexMap::new(),
|
|
|
|
}),
|
|
|
|
),
|
|
|
|
MenuConfig::Custom(entry) => (
|
|
|
|
entry.label.clone(),
|
|
|
|
MenuEntry::Custom(CustomEntry {
|
|
|
|
icon: entry.icon,
|
|
|
|
label: entry.label,
|
|
|
|
on_click: entry.on_click,
|
|
|
|
}),
|
|
|
|
),
|
|
|
|
})
|
|
|
|
.collect()
|
|
|
|
}
|