//! SPC leader key tree (Doom Emacs / which-key style). //! //! Builds a tree of keybindings accessible via the SPC leader key. use crate::app::{Mode, Tab}; /// Action to execute when a leader key sequence is completed. #[derive(Debug, Clone, PartialEq, Eq)] pub enum LeaderAction { /// Switch to a tab SetTab(Tab), /// Set input mode SetMode(Mode), /// Navigate to next/prev tab NextTab, PrevTab, /// Toggle monitor on selected item ToggleMonitor, /// Refresh/rescan library Refresh, /// Search for release SearchRelease, /// Cycle theme CycleTheme, /// Set specific theme SetThemeDark, SetThemeLight, /// Show notifications ShowNotifications, /// Dismiss all notifications DismissNotifications, /// Show help ShowHelp, /// Show add modal ShowAdd, /// Show quit confirm ShowQuit, /// Sync/save library SyncLibrary, } /// A node in the leader key tree. #[derive(Debug, Clone)] pub struct LeaderNode { /// Display name for this node pub name: String, /// Child nodes (None if this is a leaf action) pub children: Option>, /// Action to execute (None if this is a group) pub action: Option, } impl LeaderNode { /// Create a new group node. pub fn group(name: impl Into, children: Vec<(char, LeaderNode)>) -> Self { Self { name: name.into(), children: Some(children), action: None, } } /// Create a new leaf action node. pub fn action(name: impl Into, action: LeaderAction) -> Self { Self { name: name.into(), children: None, action: Some(action), } } /// Get a child node by key. pub fn get_child(&self, key: char) -> Option<&LeaderNode> { self.children .as_ref() .and_then(|children| children.iter().find(|(k, _)| *k == key).map(|(_, n)| n)) } /// Check if this is a group (has children). pub fn is_group(&self) -> bool { self.children.is_some() } } /// Result of navigating the leader tree. #[derive(Debug, Clone, PartialEq, Eq)] pub enum LeaderResult { /// Navigation is pending (entered a group) Pending, /// An action was executed Executed(LeaderAction), /// Key was not bound NotBound, } /// State for the leader key system. #[derive(Debug, Default)] pub struct LeaderState { /// Current path in the tree (e.g., ['b'] for SPC b ...) pub path: Vec, /// Whether the leader menu is currently active pub active: bool, } impl LeaderState { /// Create a new leader state. pub fn new() -> Self { Self::default() } /// Activate the leader menu. pub fn activate(&mut self) { self.active = true; self.path.clear(); } /// Deactivate the leader menu. pub fn deactivate(&mut self) { self.active = false; self.path.clear(); } /// Go back one level (Backspace behavior). pub fn go_back(&mut self) { if self.path.is_empty() { self.deactivate(); } else { self.path.pop(); } } /// Navigate to a key in the tree. pub fn navigate(&mut self, key: char, tree: &LeaderNode) -> LeaderResult { let current = self.current_node(tree); let Some(current) = current else { self.deactivate(); return LeaderResult::NotBound; }; match current.get_child(key) { Some(child) => { if child.is_group() { self.path.push(key); LeaderResult::Pending } else if let Some(action) = &child.action { let action = action.clone(); self.deactivate(); LeaderResult::Executed(action) } else { self.deactivate(); LeaderResult::NotBound } } None => { self.deactivate(); LeaderResult::NotBound } } } /// Get the current node based on the path. pub fn current_node<'a>(&self, tree: &'a LeaderNode) -> Option<&'a LeaderNode> { let mut node = tree; for &key in &self.path { node = node.get_child(key)?; } Some(node) } /// Get the breadcrumb string for display (e.g., "SPC › b"). pub fn breadcrumb(&self) -> String { if self.path.is_empty() { "SPC".to_string() } else { let path_str: String = self.path.iter().map(|c| format!(" › {}", c)).collect(); format!("SPC{}", path_str) } } } /// Build the leader key tree matching the app.jsx specification. pub fn build_leader_tree() -> LeaderNode { LeaderNode::group( "leader", vec![ // SPC SPC / SPC : → command mode (' ', LeaderNode::action("M-x command", LeaderAction::SetMode(Mode::Command))), (':', LeaderNode::action("command", LeaderAction::SetMode(Mode::Command))), // SPC / → search mode ('/', LeaderNode::action("filter", LeaderAction::SetMode(Mode::Search))), // SPC ? → help ('?', LeaderNode::action("help", LeaderAction::ShowHelp)), // SPC b → +buffer (tabs) ( 'b', LeaderNode::group( "+buffer", vec![ ('l', LeaderNode::action("library", LeaderAction::SetTab(Tab::Library))), ('w', LeaderNode::action("wanted", LeaderAction::SetTab(Tab::Wanted))), ('q', LeaderNode::action("queue", LeaderAction::SetTab(Tab::Queue))), ('h', LeaderNode::action("history", LeaderAction::SetTab(Tab::History))), ('c', LeaderNode::action("calendar", LeaderAction::SetTab(Tab::Calendar))), ('s', LeaderNode::action("settings", LeaderAction::SetTab(Tab::Settings))), ('n', LeaderNode::action("next tab", LeaderAction::NextTab)), ('p', LeaderNode::action("prev tab", LeaderAction::PrevTab)), ], ), ), // Quick-jump shortcuts ('l', LeaderNode::action("→ library", LeaderAction::SetTab(Tab::Library))), ('w', LeaderNode::action("→ wanted", LeaderAction::SetTab(Tab::Wanted))), ('h', LeaderNode::action("→ history", LeaderAction::SetTab(Tab::History))), ('c', LeaderNode::action("→ calendar", LeaderAction::SetTab(Tab::Calendar))), // SPC a → +actions ( 'a', LeaderNode::group( "+actions / artist", vec![ ('a', LeaderNode::action("add artist", LeaderAction::ShowAdd)), ('m', LeaderNode::action("toggle monitor", LeaderAction::ToggleMonitor)), ('r', LeaderNode::action("refresh / rescan", LeaderAction::Refresh)), ], ), ), // SPC s → +search ( 's', LeaderNode::group( "+search", vec![ ('s', LeaderNode::action("release for selected", LeaderAction::SearchRelease)), ('f', LeaderNode::action("filter library", LeaderAction::SetMode(Mode::Search))), ('a', LeaderNode::action("add new artist", LeaderAction::ShowAdd)), ], ), ), // SPC t → +toggle/theme ( 't', LeaderNode::group( "+toggle / theme", vec![ ('t', LeaderNode::action("cycle theme", LeaderAction::CycleTheme)), ('d', LeaderNode::action("dark theme", LeaderAction::SetThemeDark)), ('l', LeaderNode::action("light theme", LeaderAction::SetThemeLight)), ], ), ), // SPC n → +notifications ( 'n', LeaderNode::group( "+notifications", vec![ ('n', LeaderNode::action("open center", LeaderAction::ShowNotifications)), ('d', LeaderNode::action("dismiss all", LeaderAction::DismissNotifications)), ], ), ), // SPC f → +file ( 'f', LeaderNode::group( "+file / library", vec![ ('s', LeaderNode::action("save (sync)", LeaderAction::SyncLibrary)), ('r', LeaderNode::action("refresh / rescan", LeaderAction::Refresh)), ], ), ), // SPC q → +quit ( 'q', LeaderNode::group( "+queue / quit", vec![ ('q', LeaderNode::action("queue tab", LeaderAction::SetTab(Tab::Queue))), ('Q', LeaderNode::action("quit harmony", LeaderAction::ShowQuit)), ], ), ), ], ) }