From 96e10fe1398125124331939b8ca0f8dfd612c956 Mon Sep 17 00:00:00 2001 From: Claire Neveu Date: Sun, 7 Apr 2024 17:23:59 -0400 Subject: [PATCH] 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 --- .github/workflows/build.yml | 1 + Cargo.toml | 3 + docs/modules/Menu.md | 157 ++++++++++++++++++ examples/menu/default.corn | 102 ++++++++++++ examples/menu/default.json | 107 ++++++++++++ examples/menu/default.toml | 87 ++++++++++ examples/menu/default.yml | 68 ++++++++ src/config/mod.rs | 6 + src/desktop_file.rs | 45 ++++- src/modules/menu/config.rs | 271 ++++++++++++++++++++++++++++++ src/modules/menu/mod.rs | 321 ++++++++++++++++++++++++++++++++++++ src/modules/menu/ui.rs | 221 +++++++++++++++++++++++++ src/modules/mod.rs | 2 + test-configs/menu.corn | 34 ++++ 14 files changed, 1419 insertions(+), 6 deletions(-) create mode 100644 docs/modules/Menu.md create mode 100644 examples/menu/default.corn create mode 100644 examples/menu/default.json create mode 100644 examples/menu/default.toml create mode 100644 examples/menu/default.yml create mode 100644 src/modules/menu/config.rs create mode 100644 src/modules/menu/mod.rs create mode 100644 src/modules/menu/ui.rs create mode 100644 test-configs/menu.corn diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 38abaf9..9f5e386 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -113,6 +113,7 @@ jobs: - keyboard+hyprland - label - launcher + - menu - music+all - music+mpris - music+mpd diff --git a/Cargo.toml b/Cargo.toml index f94d8c3..bdb119d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ default = [ "keyboard+all", "launcher", "label", + "menu", "music+all", "network_manager", "notifications", @@ -76,6 +77,8 @@ label = [] launcher = [] +menu = [] + music = ["dep:regex"] "music+all" = ["music", "music+mpris", "music+mpd"] "music+mpris" = ["music", "mpris"] diff --git a/docs/modules/Menu.md b/docs/modules/Menu.md new file mode 100644 index 0000000..793f4d0 --- /dev/null +++ b/docs/modules/Menu.md @@ -0,0 +1,157 @@ +Application menu that shows installed programs and optionally custom entries. +This works by reading all `.desktop` files on the system. + +Clicking the menu button will open the main menu. +Clicking on any application category will open a sub-menu with any installed applications that match. + +It is also possible to add custom categories and actions into the menu. + +![Screenshot of open menu showing applications inside Office category](https://f.jstanger.dev/github/ironbar/menu.png) + +## Configuration + +| | Type | Default | Description | +|-----------------------|------------------------------------------------------|------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `start` | `MenuEntry[]` | `[]` | Items to add to the start of the main menu. | +| `center` | `MenuEntry[]` | Default XDG menu | Items to add to the centre of the main menu. By default this shows a number of XDG entries that should cover all common applications. | +| `end` | `MenuEntry[]` | `[]` | Items to add to the end of the main menu. | +| `height` | `integer` | `null` | Height of the menu. Leave null to resize dynamically. | +| `width` | `integer` | `null` | Width of the menu. Leave null to resize dynamically. | +| `label` | `string` | `≡` | Label to show on the menu button on the bar. | +| `label_icon` | `string` | `null` | Icon to show on the menu button on the bar. | +| `label_icon_size` | `integer` | `16` | Size of the label_icon image. | +| `truncate` | `'start'` or `'middle'` or `'end'` or `off` or `Map` | `off` | Applies to popup. The location of the ellipses and where to truncate text from. Leave null to avoid truncating. Use the long-hand `Map` version if specifying a length. | +| `truncate.mode` | `'start'` or `'middle'` or `'end'` or `off` | `off` | Applies to popup. The location of the ellipses and where to truncate text from. Leave null to avoid truncating. | +| `truncate.length` | `integer` | `null` | Applies to popup. The fixed width (in chars) of the widget. Leave blank to let GTK automatically handle. | +| `truncate.max_length` | `integer` | `null` | Applies to popup. The maximum number of characters before truncating. Leave blank to let GTK automatically handle. | + + +### `MenuEntry` + +Each entry can be one of three types: + +- `xdg_entry` - Contains all applications matching the configured `categories`. +- `xdg_other` - Contains all applications not covered by `xdg_entry` categories. +- `custom` - Individual shell command entry. + +| | Type | Default | Description | +|--------------|----------------------------------------|---------|----------------------------------------------------------------------------------------| +| `type` | `xdg_entry` or `xdg_other` or `custom` | | Type of the entry. | +| `label` | `string` | `''` | Label of the entry's button. | +| `icon` | `string` | `null` | Icon for the entry's button. | +| `categories` | `string[]` | `[]` | [`xfg_entry`] List of freedesktop.org categories to include in this entry's sub menu . | +| `on_click` | `string` | `''` | [`custom`] Shell command to execute when the entry's button is clicked | + +### Default XDG Menu + +Setting the `center` menu entries will override the default menu. + +The default menu can be found in the `default` example files [here](https://github.com/jakestanger/ironbar/blob/examples/menu/). + +
+ +JSON + +```json +{ + "start": [ + { + "type": "menu", + "start": [ + { + "type": "custom", + "label": "Terminal", + "on_click": "xterm" + } + ], + "height": 440, + "width": 200, + "icon": "archlinux", + "label": null + } + ] +} + + +``` + +
+ +
+TOML + +```toml +[[start]] +type = "memu" +height = 400 +width = 200 +icon = "archlinux" + +[[start.start]] +type = "custom" +label = "Terminal" +on_click = "xterm" +``` + +
+ +
+YAML + +```yaml +start: + - type: "menu" + start: + - type: custom + label: Terminal + on_click: xterm + height: 440 + width: 200 + icon: archlinux + label: null +``` + +
+ +
+Corn + +```corn +{ + start = [ + { + type = "menu" + start = [ + { + type = "custom" + label = "Terminal" + on_click = "xterm" + } + ] + height = 440 + width = 200 + icon = "archlinux" + label = null + } + ] +} +``` + +
+ +## Styling + +| Selector | Description | +|--------------------------------------|-----------------------------------| +| `.menu` | Menu button | +| `.popup-menu` | Main container of the popup | +| `.popup-menu .main` | Main menu of the menu | +| `.popup-menu .main .category` | Category button | +| `.popup-menu .main .category.open` | Open category button | +| `.popup-menu .main .main-start` | Container for `start` entries | +| `.popup-menu .main .main-center` | Container for `center` entries | +| `.popup-menu .main .main-end` | Container for `end` entries | +| `.popup-menu .sub-menu` | All sub-menus | +| `.popup-menu .sub-menu .application` | Application button within submenu | + +For more information on styling, please see the [styling guide](styling-guide). \ No newline at end of file diff --git a/examples/menu/default.corn b/examples/menu/default.corn new file mode 100644 index 0000000..fc0a20c --- /dev/null +++ b/examples/menu/default.corn @@ -0,0 +1,102 @@ +let { + $menu = [ + { + type = "xdg_entry" + label = "Accessories" + icon = "accessories" + categories = [ + "Accessibility" + "Core" + "Legacy" + "Utility" + ] + } + { + type = "xdg_entry" + label = "Development" + icon = "applications-development" + categories = [ + "Development" + ] + } + { + type = "xdg_entry" + label = "Education" + icon = "applications-education" + categories = [ + "Education" + ] + } + { + type = "xdg_entry" + label = "Games" + icon = "applications-games" + categories = [ + "Games" + ] + } + { + type = "xdg_entry" + label = "Graphics" + icon = "applications-graphics" + categories = [ + "Graphics" + ] + } + { + type = "xdg_entry" + label = "Multimedia" + icon = "applications-multimedia" + categories = [ + "Audio" + "Video" + "AudioVideo" + ] + } + { + type = "xdg_entry" + label = "Network" + icon = "applications-internet" + categories = [ + "Network" + ] + } + { + type = "xdg_entry" + label = "Office" + icon = "applications-office" + categories = [ + "Office" + ] + } + { + type = "xdg_entry" + label = "Science" + icon = "applications-science" + categories = [ + "Science" + ] + } + { + type = "xdg_entry" + label = "System" + icon = "applications-system" + categories = [ + "Emulator" + "System" + ] + } + { type = "xdg_other" } + { + type = "xdg_entry" + label = "Settings" + icon = "preferences-system" + categories = [ + "Settings" + "Screensaver" + ] + } + ] +} in { + start = [ { type = "menu" center = $menu } ] +} \ No newline at end of file diff --git a/examples/menu/default.json b/examples/menu/default.json new file mode 100644 index 0000000..92396fc --- /dev/null +++ b/examples/menu/default.json @@ -0,0 +1,107 @@ +{ + "start": [ + { + "type": "menu", + "center": [ + { + "type": "xdg_entry", + "label": "Accessories", + "icon": "accessories", + "categories": [ + "Accessibility", + "Core", + "Legacy", + "Utility" + ] + }, + { + "type": "xdg_entry", + "label": "Development", + "icon": "applications-development", + "categories": [ + "Development" + ] + }, + { + "type": "xdg_entry", + "label": "Education", + "icon": "applications-education", + "categories": [ + "Education" + ] + }, + { + "type": "xdg_entry", + "label": "Games", + "icon": "applications-games", + "categories": [ + "Games" + ] + }, + { + "type": "xdg_entry", + "label": "Graphics", + "icon": "applications-graphics", + "categories": [ + "Graphics" + ] + }, + { + "type": "xdg_entry", + "label": "Multimedia", + "icon": "applications-multimedia", + "categories": [ + "Audio", + "Video", + "AudioVideo" + ] + }, + { + "type": "xdg_entry", + "label": "Network", + "icon": "applications-internet", + "categories": [ + "Network" + ] + }, + { + "type": "xdg_entry", + "label": "Office", + "icon": "applications-office", + "categories": [ + "Office" + ] + }, + { + "type": "xdg_entry", + "label": "Science", + "icon": "applications-science", + "categories": [ + "Science" + ] + }, + { + "type": "xdg_entry", + "label": "System", + "icon": "applications-system", + "categories": [ + "Emulator", + "System" + ] + }, + { + "type": "xdg_other" + }, + { + "type": "xdg_entry", + "label": "Settings", + "icon": "preferences-system", + "categories": [ + "Settings", + "Screensaver" + ] + } + ] + } + ] +} diff --git a/examples/menu/default.toml b/examples/menu/default.toml new file mode 100644 index 0000000..48e62d0 --- /dev/null +++ b/examples/menu/default.toml @@ -0,0 +1,87 @@ +[[start]] +type = "menu" + +[[start.center]] +type = "xdg_entry" +label = "Accessories" +icon = "accessories" +categories = [ + "Accessibility", + "Core", + "Legacy", + "Utility", +] + +[[start.center]] +type = "xdg_entry" +label = "Development" +icon = "applications-development" +categories = ["Development"] + +[[start.center]] +type = "xdg_entry" +label = "Education" +icon = "applications-education" +categories = ["Education"] + +[[start.center]] +type = "xdg_entry" +label = "Games" +icon = "applications-games" +categories = ["Games"] + +[[start.center]] +type = "xdg_entry" +label = "Graphics" +icon = "applications-graphics" +categories = ["Graphics"] + +[[start.center]] +type = "xdg_entry" +label = "Multimedia" +icon = "applications-multimedia" +categories = [ + "Audio", + "Video", + "AudioVideo", +] + +[[start.center]] +type = "xdg_entry" +label = "Network" +icon = "applications-internet" +categories = ["Network"] + +[[start.center]] +type = "xdg_entry" +label = "Office" +icon = "applications-office" +categories = ["Office"] + +[[start.center]] +type = "xdg_entry" +label = "Science" +icon = "applications-science" +categories = ["Science"] + +[[start.center]] +type = "xdg_entry" +label = "System" +icon = "applications-system" +categories = [ + "Emulator", + "System", +] + +[[start.center]] +type = "xdg_other" + +[[start.center]] +type = "xdg_entry" +label = "Settings" +icon = "preferences-system" +categories = [ + "Settings", + "Screensaver", +] + diff --git a/examples/menu/default.yml b/examples/menu/default.yml new file mode 100644 index 0000000..c593d04 --- /dev/null +++ b/examples/menu/default.yml @@ -0,0 +1,68 @@ +start: + - type: menu + center: + - type: xdg_entry + label: Accessories + icon: accessories + categories: + - Accessibility + - Core + - Legacy + - Utility + - type: xdg_entry + label: Development + icon: applications-development + categories: + - Development + - type: xdg_entry + label: Education + icon: applications-education + categories: + - Education + - type: xdg_entry + label: Games + icon: applications-games + categories: + - Games + - type: xdg_entry + label: Graphics + icon: applications-graphics + categories: + - Graphics + - type: xdg_entry + label: Multimedia + icon: applications-multimedia + categories: + - Audio + - Video + - AudioVideo + - type: xdg_entry + label: Network + icon: applications-internet + categories: + - Network + - type: xdg_entry + label: Office + icon: applications-office + categories: + - Office + - type: xdg_entry + label: Science + icon: applications-science + categories: + - Science + - type: xdg_entry + label: System + icon: applications-system + categories: + - Emulator + - System + - type: xdg_other + - type: xdg_entry + label: Settings + icon: preferences-system + categories: + - Settings + - Screensaver + + diff --git a/src/config/mod.rs b/src/config/mod.rs index 088fdb3..0fb7875 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -21,6 +21,8 @@ use crate::modules::keyboard::KeyboardModule; use crate::modules::label::LabelModule; #[cfg(feature = "launcher")] use crate::modules::launcher::LauncherModule; +#[cfg(feature = "menu")] +use crate::modules::menu::MenuModule; #[cfg(feature = "music")] use crate::modules::music::MusicModule; #[cfg(feature = "network_manager")] @@ -75,6 +77,8 @@ pub enum ModuleConfig { Label(Box), #[cfg(feature = "launcher")] Launcher(Box), + #[cfg(feature = "menu")] + Menu(Box), #[cfg(feature = "music")] Music(Box), #[cfg(feature = "network_manager")] @@ -127,6 +131,8 @@ impl ModuleConfig { Self::Label(module) => create!(module), #[cfg(feature = "launcher")] Self::Launcher(module) => create!(module), + #[cfg(feature = "menu")] + Self::Menu(module) => create!(module), #[cfg(feature = "music")] Self::Music(module) => create!(module), #[cfg(feature = "network_manager")] diff --git a/src/desktop_file.rs b/src/desktop_file.rs index 2d788df..3fdf346 100644 --- a/src/desktop_file.rs +++ b/src/desktop_file.rs @@ -53,6 +53,8 @@ impl DesktopFileRef { let mut has_wm_class = false; let mut has_exec = false; let mut has_icon = false; + let mut has_categories = false; + let mut has_no_display = false; while let Ok(Some(line)) = lines.next_line().await { let Some((key, value)) = line.split_once('=') else { @@ -60,31 +62,46 @@ impl DesktopFileRef { }; match key { - "Name" => { + "Name" if !has_name => { desktop_file.name = Some(value.to_string()); has_name = true; } - "Type" => { + "Type" if !has_type => { desktop_file.app_type = Some(value.to_string()); has_type = true; } - "StartupWMClass" => { + "StartupWMClass" if !has_wm_class => { desktop_file.startup_wm_class = Some(value.to_string()); has_wm_class = true; } - "Exec" => { + "Exec" if !has_exec => { desktop_file.exec = Some(value.to_string()); has_exec = true; } - "Icon" => { + "Icon" if !has_icon => { desktop_file.icon = Some(value.to_string()); has_icon = true; } + "Categories" if !has_categories => { + desktop_file.categories = value.split(';').map(|s| s.to_string()).collect(); + has_categories = true; + } + "NoDisplay" if !has_no_display => { + desktop_file.no_display = Some(value.parse()?); + has_no_display = true; + } _ => {} } // parsing complete - don't bother with the rest of the lines - if has_name && has_type && has_wm_class && has_exec && has_icon { + if has_name + && has_type + && has_wm_class + && has_exec + && has_icon + && has_categories + && has_no_display + { break; } } @@ -101,6 +118,8 @@ pub struct DesktopFile { pub startup_wm_class: Option, pub exec: Option, pub icon: Option, + pub categories: Vec, + pub no_display: Option, } impl DesktopFile { @@ -112,6 +131,8 @@ impl DesktopFile { startup_wm_class: None, exec: None, icon: None, + categories: vec![], + no_display: None, } } } @@ -159,6 +180,18 @@ impl DesktopFiles { } } + pub async fn get_all(&self) -> Result> { + let mut files = self.files.lock().await; + + let mut res = Vec::with_capacity(files.len()); + for file in files.values_mut() { + let file = file.get().await?; + res.push(file); + } + + Ok(res) + } + /// Attempts to locate a applications file by file name or contents. /// /// Input should typically be the app id, app name or icon. diff --git a/src/modules/menu/config.rs b/src/modules/menu/config.rs new file mode 100644 index 0000000..4575880 --- /dev/null +++ b/src/modules/menu/config.rs @@ -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, + + /// XDG categories the associated submenu should contain. + #[serde(default)] + pub categories: Vec, +} + +/// 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, + + /// 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, + + /// 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, + + /// Items to add to the end of the main menu. + /// + /// **Default**: `[]` + #[serde(default)] + pub(super) end: Vec, + + /// 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, + + /// 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, + + /// Label to show on the menu button on the bar. + /// + /// **Default**: `≡` + #[serde(default = "default_menu_popup_label")] + pub(super) label: Option, + + /// Icon to show on the menu button on the bar. + /// + /// **Default**: `null` + #[serde(default)] + pub(super) label_icon: Option, + + /// 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, +} + +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 { + 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 { + 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, + sections_by_cat: &mut IndexMap>, +) -> IndexMap { + 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() +} diff --git a/src/modules/menu/mod.rs b/src/modules/menu/mod.rs new file mode 100644 index 0000000..e9f70ef --- /dev/null +++ b/src/modules/menu/mod.rs @@ -0,0 +1,321 @@ +mod config; +mod ui; + +use color_eyre::Result; +use color_eyre::eyre::Report; +use config::*; +use gtk::prelude::*; +use gtk::{Align, Button, Orientation}; +use indexmap::IndexMap; +use serde::Deserialize; +use tokio::sync::mpsc; + +use super::{ModuleLocation, PopupButton}; +use crate::channels::{AsyncSenderExt, BroadcastReceiverExt}; +use crate::config::BarPosition; +use crate::gtk_helpers::IronbarGtkExt; +use crate::modules::{ + Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext, +}; +use crate::{module_impl, spawn}; + +pub use config::MenuModule; + +/// XDG button and menu from parsed config. +#[derive(Debug, Clone)] +pub struct XdgSection { + pub label: String, + pub icon: Option, + pub applications: IndexMap, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct MenuApplication { + pub label: String, + pub file_name: String, + pub categories: Vec, +} + +#[derive(Debug)] +pub enum MenuEntry { + Xdg(XdgSection), + Custom(CustomEntry), +} + +impl MenuEntry { + pub fn label(&self) -> String { + match self { + Self::Xdg(entry) => entry.label.clone(), + Self::Custom(entry) => entry.label.clone(), + } + } + + pub fn icon(&self) -> Option { + match self { + Self::Xdg(entry) => entry.icon.clone(), + Self::Custom(entry) => entry.icon.clone(), + } + } +} + +impl Module