use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MenuItem {
pub label: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub icon: Option<String>,
pub action: MenuAction,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub shortcut: Option<String>,
}
impl MenuItem {
pub fn new(label: impl Into<String>, action: MenuAction) -> Self {
Self {
label: label.into(),
icon: None,
action,
shortcut: None,
}
}
pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
self.icon = Some(icon.into());
self
}
pub fn with_shortcut(mut self, shortcut: impl Into<String>) -> Self {
self.shortcut = Some(shortcut.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
pub enum MenuAction {
Command(String),
Remote(String),
SubMenu(Vec<MenuItem>),
}
impl MenuAction {
pub fn is_submenu(&self) -> bool {
matches!(self, MenuAction::SubMenu(_))
}
pub fn is_command(&self) -> bool {
matches!(self, MenuAction::Command(_))
}
pub fn is_remote(&self) -> bool {
matches!(self, MenuAction::Remote(_))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_menu_item_builder() {
let item = MenuItem::new("Test", MenuAction::Command("test".to_string()))
.with_icon("๐งช")
.with_shortcut("Ctrl+T");
assert_eq!(item.label, "Test");
assert_eq!(item.icon, Some("๐งช".to_string()));
assert_eq!(item.shortcut, Some("Ctrl+T".to_string()));
}
#[test]
fn test_menu_action_checks() {
let cmd = MenuAction::Command("ls".to_string());
assert!(cmd.is_command());
assert!(!cmd.is_remote());
assert!(!cmd.is_submenu());
let remote = MenuAction::Remote("action_id".to_string());
assert!(!remote.is_command());
assert!(remote.is_remote());
assert!(!remote.is_submenu());
let submenu = MenuAction::SubMenu(vec![]);
assert!(!submenu.is_command());
assert!(!submenu.is_remote());
assert!(submenu.is_submenu());
}
#[test]
fn test_nested_submenu() {
let nested = MenuItem::new(
"Root",
MenuAction::SubMenu(vec![MenuItem::new(
"Child",
MenuAction::SubMenu(vec![MenuItem::new(
"Grandchild",
MenuAction::Command("echo nested".to_string()),
)]),
)]),
);
if let MenuAction::SubMenu(items) = &nested.action {
assert_eq!(items.len(), 1);
assert_eq!(items[0].label, "Child");
} else {
panic!("Expected submenu");
}
}
#[test]
fn test_menu_item_with_remote_action() {
let item =
MenuItem::new("Test", MenuAction::Remote("test_action".to_string())).with_icon("๐ฏ");
assert_eq!(item.label, "Test");
assert_eq!(item.icon, Some("๐ฏ".to_string()));
assert!(item.action.is_remote());
if let MenuAction::Remote(id) = &item.action {
assert_eq!(id, "test_action");
} else {
panic!("Expected Remote action");
}
}
}