use super::actions::MenuAction;
use super::model::{MenuEntry, MenuItemSpec, MenuSection};
use crate::arrangements::ArrangementId;
use muda::accelerator::{Accelerator, Code};
use par_term_keybindings::KeybindingRegistry;
use std::borrow::Cow;
use std::collections::BTreeSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Requires {
Always,
MultiplePanes,
MultipleTabs,
MultipleWindows,
MuxAvailable,
SessionAttached,
MuxAttached,
TabMovable,
TabMovableAway,
Tab(usize),
Window(usize),
Arrangements,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Check {
Fullscreen,
PaneZoom,
ProfileDrawer,
AssistantPanel,
AgentUsage,
FpsOverlay,
BackgroundShader,
ShaderAnimation,
ShaderReadability,
CursorShader,
Throughput,
AlwaysOnTop,
BroadcastTab,
PaneBroadcastExcluded,
OutputRecording,
CopyMode,
TabProfilePinned,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MoveBlock {
Attached,
Gateway,
}
const MAX_TITLE_CHARS: usize = 40;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct MenuState {
pub modal_open: bool,
pub multiple_panes: bool,
pub tab_count: usize,
pub tab_titles: Vec<String>,
pub windows: Vec<(usize, String)>,
pub mux_available: bool,
pub session_attached: bool,
pub mux_attached: bool,
pub move_block: Option<MoveBlock>,
pub arrangements: Vec<(ArrangementId, String)>,
pub checks: BTreeSet<Check>,
pub open_toggles: BTreeSet<&'static str>,
}
impl MenuState {
pub fn satisfies(&self, requires: Requires) -> bool {
match requires {
Requires::Always => true,
Requires::MultiplePanes => self.multiple_panes,
Requires::MultipleTabs => self.tab_count > 1,
Requires::MultipleWindows => self.windows.len() > 1,
Requires::MuxAvailable => self.mux_available,
Requires::SessionAttached => self.session_attached,
Requires::MuxAttached => self.mux_attached,
Requires::TabMovable => self.move_block.is_none(),
Requires::TabMovableAway => self.move_block.is_none() && self.tab_count > 1,
Requires::Tab(n) => n >= 1 && n <= self.tab_count,
Requires::Window(n) => self.windows.iter().any(|(number, _)| *number == n),
Requires::Arrangements => !self.arrangements.is_empty(),
}
}
pub fn enabled(&self, spec: &MenuItemSpec) -> bool {
if self.modal_open && !self.passes(&spec.action, spec.accelerator.as_ref()) {
return false;
}
self.satisfies(spec.requires)
}
pub fn passes(&self, action: &MenuAction, accelerator: Option<&Accelerator>) -> bool {
passes_key_block(action, accelerator) || self.closes_open_panel(action)
}
pub fn passes_with(&self, action: &MenuAction, registry: &KeybindingRegistry) -> bool {
let accelerator = action
.keybinding_action()
.and_then(|id| registry.chord_for_action(&id))
.and_then(|combo| super::registry_accel::accelerator_from_combo(&combo));
self.passes(action, accelerator.as_ref())
}
fn closes_open_panel(&self, action: &MenuAction) -> bool {
matches!(action, MenuAction::Action(id) if self.open_toggles.contains(id))
}
pub fn dynamic_enabled(&self) -> bool {
!self.modal_open
}
pub fn checked(&self, check: Check) -> bool {
self.checks.contains(&check)
}
pub fn label(&self, spec: &MenuItemSpec) -> Cow<'static, str> {
match spec.requires {
Requires::Tab(n) => match self.tab_titles.get(n.wrapping_sub(1)) {
Some(title) if !title.trim().is_empty() => {
Cow::Owned(format!("Tab {n}: {}", shorten(title)))
}
_ => Cow::Borrowed(spec.label),
},
Requires::Window(n) => match self.windows.iter().find(|(number, _)| *number == n) {
Some((_, title)) if !title.trim().is_empty() => {
Cow::Owned(format!("Window {n}: {}", shorten(title)))
}
_ => Cow::Borrowed(spec.label),
},
Requires::TabMovable | Requires::TabMovableAway => match self.move_block {
Some(MoveBlock::Attached) => Cow::Owned(format!("{} (attached tab)", spec.label)),
Some(MoveBlock::Gateway) => Cow::Owned(format!("{} (tmux tab)", spec.label)),
None => Cow::Borrowed(spec.label),
},
_ => Cow::Borrowed(spec.label),
}
}
}
pub fn passes_key_block(action: &MenuAction, accelerator: Option<&Accelerator>) -> bool {
matches!(
action,
MenuAction::Copy
| MenuAction::Paste
| MenuAction::SelectAll
| MenuAction::Quit
| MenuAction::OpenSettings
| MenuAction::About
| MenuAction::OpenDocs
| MenuAction::Minimize
| MenuAction::Zoom
) || accelerator
.is_some_and(|a| matches!(a.key(), Code::F1 | Code::F2 | Code::F3 | Code::Escape))
}
pub type Capture = Option<BTreeSet<&'static str>>;
pub fn release_captured_accelerators(sections: &mut [MenuSection], open: &BTreeSet<&'static str>) {
fn walk(entries: &mut [MenuEntry], open: &BTreeSet<&'static str>) {
for entry in entries {
match entry {
MenuEntry::Item(spec) => {
let keeps = passes_key_block(&spec.action, spec.accelerator.as_ref())
|| matches!(spec.action, MenuAction::Action(id) if open.contains(id));
if !keeps {
spec.accelerator = None;
}
}
MenuEntry::Submenu(sub) => walk(&mut sub.entries, open),
_ => {}
}
}
}
for section in sections {
walk(&mut section.entries, open);
}
}
pub fn label_varies(requires: Requires) -> bool {
matches!(
requires,
Requires::Tab(_) | Requires::Window(_) | Requires::TabMovable | Requires::TabMovableAway
)
}
fn shorten(title: &str) -> String {
let title = title.trim();
if title.chars().count() <= MAX_TITLE_CHARS {
return title.to_string();
}
let mut cut: String = title.chars().take(MAX_TITLE_CHARS - 1).collect();
cut.push('…');
cut
}
#[cfg(test)]
#[path = "state_tests.rs"]
mod tests;