use super::actions::MenuAction;
use super::state::{Check, Requires};
use muda::accelerator::{Accelerator, Code, Modifiers};
use par_term_config::KeyBinding;
use par_term_keybindings::KeybindingRegistry;
pub const HELP_SECTION_TITLE: &str = "Help";
pub const WINDOW_SECTION_TITLE: &str = "Window";
#[derive(Debug, Clone)]
pub struct MenuItemSpec {
pub id: &'static str,
pub label: &'static str,
pub accelerator: Option<Accelerator>,
pub action: MenuAction,
pub requires: Requires,
pub check: Option<Check>,
pub alias: bool,
}
impl MenuItemSpec {
pub(super) fn new(id: &'static str, label: &'static str, action: MenuAction) -> Self {
Self {
id,
label,
accelerator: None,
action,
requires: Requires::Always,
check: None,
alias: false,
}
}
pub(super) fn alias(mut self) -> Self {
self.alias = true;
self
}
pub(super) fn action(id: &'static str, label: &'static str) -> Self {
Self::new(id, label, MenuAction::Action(id))
}
pub(super) fn when(mut self, requires: Requires) -> Self {
self.requires = requires;
self
}
pub(super) fn toggle(mut self, check: Check) -> Self {
self.check = Some(check);
self
}
pub(super) fn accel(mut self, accelerator: Option<Accelerator>) -> Self {
self.accelerator = accelerator;
self
}
}
#[derive(Debug, Clone)]
pub struct SubmenuSpec {
pub id: &'static str,
pub title: &'static str,
pub requires: Requires,
pub entries: Vec<MenuEntry>,
}
#[derive(Debug, Clone)]
pub enum MenuEntry {
Item(MenuItemSpec),
Submenu(SubmenuSpec),
Separator,
Profiles,
Arrangements,
BringAllToFront,
}
impl From<MenuItemSpec> for MenuEntry {
fn from(spec: MenuItemSpec) -> Self {
Self::Item(spec)
}
}
pub(super) fn submenu(
id: &'static str,
title: &'static str,
requires: Requires,
entries: Vec<MenuEntry>,
) -> MenuEntry {
MenuEntry::Submenu(SubmenuSpec {
id,
title,
requires,
entries,
})
}
#[derive(Debug, Clone)]
pub struct MenuSection {
pub title: &'static str,
pub entries: Vec<MenuEntry>,
}
pub fn items_in(entries: &[MenuEntry]) -> Vec<&MenuItemSpec> {
let mut out = Vec::new();
collect_items(entries, &mut out);
out
}
fn collect_items<'a>(entries: &'a [MenuEntry], out: &mut Vec<&'a MenuItemSpec>) {
for entry in entries {
match entry {
MenuEntry::Item(spec) => out.push(spec),
MenuEntry::Submenu(sub) => collect_items(&sub.entries, out),
_ => {}
}
}
}
pub fn all_items(sections: &[MenuSection]) -> Vec<&MenuItemSpec> {
sections
.iter()
.flat_map(|section| items_in(§ion.entries))
.collect()
}
pub const APP_MENU_ACTIONS: &[MenuAction] = &[
MenuAction::About,
MenuAction::OpenSettings,
MenuAction::Quit,
];
pub fn platform_menu_model_with(keybindings: &[KeyBinding]) -> Vec<MenuSection> {
menu_model_with(cfg!(target_os = "macos"), keybindings)
}
pub fn menu_model(has_native_app_menu: bool) -> Vec<MenuSection> {
menu_model_with(
has_native_app_menu,
&par_term_config::Config::default().keybindings,
)
}
pub fn menu_model_with(has_native_app_menu: bool, keybindings: &[KeyBinding]) -> Vec<MenuSection> {
menu_model_with_registry(
has_native_app_menu,
&KeybindingRegistry::from_config(keybindings),
)
}
pub fn menu_model_with_registry(
has_native_app_menu: bool,
registry: &KeybindingRegistry,
) -> Vec<MenuSection> {
let mut sections = structure(has_native_app_menu);
super::registry_accel::apply_registry_accelerators(&mut sections, registry);
sections
}
fn structure(has_native_app_menu: bool) -> Vec<MenuSection> {
use super::{model_sections as s, model_window as w};
vec![
MenuSection {
title: "Shell",
entries: s::shell(has_native_app_menu),
},
MenuSection {
title: "Edit",
entries: s::edit(has_native_app_menu),
},
MenuSection {
title: "View",
entries: s::view(),
},
MenuSection {
title: "Session",
entries: w::session(),
},
MenuSection {
title: "Profiles",
entries: w::profiles(),
},
MenuSection {
title: WINDOW_SECTION_TITLE,
entries: w::window(),
},
MenuSection {
title: HELP_SECTION_TITLE,
entries: w::help(has_native_app_menu),
},
]
}
pub(super) fn primary_modifier() -> Modifiers {
if cfg!(target_os = "macos") {
Modifiers::META
} else {
Modifiers::CONTROL | Modifiers::SHIFT
}
}
pub(super) fn primary(code: Code) -> Option<Accelerator> {
Some(Accelerator::new(primary_modifier(), code))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DynamicEntry {
pub menu_id: String,
pub label: String,
pub accelerator: Option<Accelerator>,
pub action: MenuAction,
}
pub fn profile_entries<'a>(
profiles: impl IntoIterator<Item = &'a crate::profile::Profile>,
registry: &KeybindingRegistry,
) -> Vec<DynamicEntry> {
profiles
.into_iter()
.map(|profile| {
let action = MenuAction::OpenProfile(profile.id);
DynamicEntry {
menu_id: format!("profile_{}", profile.id),
label: profile.display_label(),
accelerator: action
.keybinding_action()
.and_then(|id| registry.chord_for_action(&id))
.and_then(|combo| super::registry_accel::accelerator_from_combo(&combo)),
action,
}
})
.collect()
}
pub fn arrangement_entries_from(
arrangements: &[(crate::arrangements::ArrangementId, String)],
) -> Vec<DynamicEntry> {
arrangements
.iter()
.map(|(id, name)| DynamicEntry {
menu_id: format!("arrangement_{id}"),
label: name.clone(),
accelerator: None,
action: MenuAction::RestoreArrangement(*id),
})
.collect()
}
pub fn accelerator_label(accelerator: &Accelerator) -> String {
let mut label = String::new();
let named: [(Modifiers, &str, &str); 4] = [
(Modifiers::CONTROL, "⌃", "Ctrl"),
(Modifiers::ALT, "⌥", "Alt"),
(Modifiers::SHIFT, "⇧", "Shift"),
(Modifiers::META, "⌘", "Super"),
];
let mods = accelerator.modifiers();
for (flag, symbol, word) in named {
if mods.contains(flag) {
if cfg!(target_os = "macos") {
label.push_str(symbol);
} else {
label.push_str(word);
label.push('+');
}
}
}
label.push_str(&code_label(accelerator.key()));
label
}
fn code_label(code: Code) -> String {
let raw = format!("{code:?}");
match raw.as_str() {
"Comma" => ",".to_string(),
"Period" => ".".to_string(),
"Equal" => "+".to_string(),
"Minus" => "-".to_string(),
"BracketLeft" => "[".to_string(),
"BracketRight" => "]".to_string(),
"Space" => "Space".to_string(),
"ArrowLeft" => "Left".to_string(),
"ArrowRight" => "Right".to_string(),
"ArrowUp" => "Up".to_string(),
"ArrowDown" => "Down".to_string(),
other => other
.strip_prefix("Key")
.or_else(|| other.strip_prefix("Digit"))
.unwrap_or(other)
.to_string(),
}
}
#[cfg(test)]
#[path = "model_tests.rs"]
mod tests;