1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-09-16 11:46:58 +02:00

feat: add menu module

Adds a new Menu module which allows users to create XDG or custom menus that open after clicking on a button.

Resolves #534

Co-authored-by: Jake Stanger <mail@jstanger.dev>
This commit is contained in:
Claire Neveu 2024-04-07 17:23:59 -04:00 committed by Jake Stanger
commit 96e10fe139
No known key found for this signature in database
GPG key ID: C51FC8F9CB0BEA61
14 changed files with 1419 additions and 6 deletions

271
src/modules/menu/config.rs Normal file
View file

@ -0,0 +1,271 @@
use crate::config::{CommonConfig, TruncateMode};
use crate::modules::menu::{MenuEntry, XdgSection};
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>,
}
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()),
}
}
}
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()
}