use crate::action_types::ActionType; use std::collections::HashSet; use zellij_tile::prelude::actions::Action; use zellij_tile::prelude::*; pub struct KeybindProcessor; impl KeybindProcessor { /// Find predetermined actions based on predicates while maintaining order pub fn find_predetermined_actions( mode_info: &ModeInfo, mode: InputMode, predicates: Vec, ) -> Vec<(String, String)> where F: Fn(&Action) -> bool, { let mut result = Vec::new(); let keybinds = mode_info.get_keybinds_for_mode(mode); let mut processed_action_types = HashSet::new(); // Iterate through predicates in order to maintain the desired sequence for predicate in predicates { // Find the first matching action for this predicate let mut found_match = false; for (_key, actions) in &keybinds { if let Some(first_action) = actions.first() { if predicate(first_action) { let action_type = ActionType::from_action(first_action); // Skip if we've already processed this action type if processed_action_types.contains(&action_type) { found_match = true; break; } let mut matching_keys = Vec::new(); // Find all keys that match this action type (including different directions) for (inner_key, inner_actions) in &keybinds { if let Some(inner_first_action) = inner_actions.first() { if ActionType::from_action(inner_first_action) == action_type { matching_keys.push(format!("{}", inner_key)); } } } if !matching_keys.is_empty() { let description = action_type.description(); let should_add_brackets_to_keys = mode != InputMode::Normal; // Check if this is switching to normal mode let is_switching_to_locked = matches!( first_action, Action::SwitchToMode { input_mode: InputMode::Locked } ); let grouped_keys = Self::group_key_sets( &matching_keys, should_add_brackets_to_keys, is_switching_to_locked, ); result.push((grouped_keys, description)); processed_action_types.insert(action_type); } found_match = true; break; } } } // If we found a match for this predicate, we've processed it if found_match { continue; } } result } /// Group keys into sets and separate different key types with '|' fn group_key_sets( keys: &[String], should_add_brackets_to_keys: bool, is_switching_to_locked: bool, ) -> String { if keys.is_empty() { return String::new(); } // Filter out Esc and Enter keys when switching to normal mode, but only if other keys exist let filtered_keys: Vec = if is_switching_to_locked { let non_esc_enter_keys: Vec = keys .iter() .filter(|k| k.as_str() != "ESC" && k.as_str() != "ENTER") .cloned() .collect(); if non_esc_enter_keys.is_empty() { // If no other keys exist, keep the original keys keys.to_vec() } else { // Use filtered keys (without Esc/Enter) non_esc_enter_keys } } else { keys.to_vec() }; if filtered_keys.len() == 1 { return if should_add_brackets_to_keys { format!("<{}>", filtered_keys[0]) } else { filtered_keys[0].clone() }; } // Group keys by type let mut arrow_keys = Vec::new(); let mut hjkl_lower = Vec::new(); let mut hjkl_upper = Vec::new(); let mut square_bracket_keys = Vec::new(); let mut plus_minus_keys = Vec::new(); let mut pgup_pgdown = Vec::new(); let mut other_keys = Vec::new(); for key in &filtered_keys { match key.as_str() { "Left" | "←" => arrow_keys.push("←"), "Down" | "↓" => arrow_keys.push("↓"), "Up" | "↑" => arrow_keys.push("↑"), "Right" | "→" => arrow_keys.push("→"), "h" => hjkl_lower.push("h"), "j" => hjkl_lower.push("j"), "k" => hjkl_lower.push("k"), "l" => hjkl_lower.push("l"), "H" => hjkl_upper.push("H"), "J" => hjkl_upper.push("J"), "K" => hjkl_upper.push("K"), "L" => hjkl_upper.push("L"), "[" => square_bracket_keys.push("["), "]" => square_bracket_keys.push("]"), "+" => plus_minus_keys.push("+"), "-" => plus_minus_keys.push("-"), "=" => plus_minus_keys.push("="), "PgUp" => pgup_pgdown.push("PgUp"), "PgDn" => pgup_pgdown.push("PgDn"), _ => { if should_add_brackets_to_keys { other_keys.push(format!("<{}>", key)); } else { other_keys.push(key.clone()); } }, } } let mut groups = Vec::new(); // Add hjkl group if present (prioritize hjkl over arrows) if !hjkl_lower.is_empty() { Self::sort_hjkl(&mut hjkl_lower); groups.push(Self::format_key_group( &hjkl_lower, should_add_brackets_to_keys, false, )); } // Add HJKL group if present if !hjkl_upper.is_empty() { Self::sort_hjkl_upper(&mut hjkl_upper); groups.push(Self::format_key_group( &hjkl_upper, should_add_brackets_to_keys, false, )); } // Add arrow keys group if present if !arrow_keys.is_empty() { Self::sort_arrows(&mut arrow_keys); groups.push(Self::format_key_group( &arrow_keys, should_add_brackets_to_keys, false, )); } if !square_bracket_keys.is_empty() { Self::sort_square_brackets(&mut square_bracket_keys); groups.push(Self::format_key_group( &square_bracket_keys, should_add_brackets_to_keys, false, )); } if !plus_minus_keys.is_empty() { Self::sort_plus_minus(&mut plus_minus_keys); groups.push(Self::format_key_group( &plus_minus_keys, should_add_brackets_to_keys, false, )); } if !pgup_pgdown.is_empty() { Self::sort_pgup_pgdown(&mut pgup_pgdown); groups.push(Self::format_key_group( &pgup_pgdown, should_add_brackets_to_keys, true, )); } // Add other keys with / separator if !other_keys.is_empty() { groups.push(other_keys.join("/")); } groups.join("/") } fn sort_hjkl(keys: &mut Vec<&str>) { keys.sort_by(|a, b| { let order = ["h", "j", "k", "l"]; let pos_a = order.iter().position(|&x| &x == a).unwrap_or(usize::MAX); let pos_b = order.iter().position(|&x| &x == b).unwrap_or(usize::MAX); pos_a.cmp(&pos_b) }); } fn sort_hjkl_upper(keys: &mut Vec<&str>) { keys.sort_by(|a, b| { let order = ["H", "J", "K", "L"]; let pos_a = order.iter().position(|&x| &x == a).unwrap_or(usize::MAX); let pos_b = order.iter().position(|&x| &x == b).unwrap_or(usize::MAX); pos_a.cmp(&pos_b) }); } fn sort_arrows(keys: &mut Vec<&str>) { keys.sort(); keys.dedup(); keys.sort_by(|a, b| { let order = ["←", "↓", "↑", "→"]; let pos_a = order.iter().position(|&x| &x == a).unwrap_or(usize::MAX); let pos_b = order.iter().position(|&x| &x == b).unwrap_or(usize::MAX); pos_a.cmp(&pos_b) }); } fn sort_square_brackets(keys: &mut Vec<&str>) { keys.sort_by(|a, b| { let order = ["[", "]"]; let pos_a = order.iter().position(|&x| &x == a).unwrap_or(usize::MAX); let pos_b = order.iter().position(|&x| &x == b).unwrap_or(usize::MAX); pos_a.cmp(&pos_b) }); } fn sort_plus_minus(keys: &mut Vec<&str>) { keys.sort_by(|a, b| { let order = ["+", "-"]; let pos_a = order.iter().position(|&x| &x == a).unwrap_or(usize::MAX); let pos_b = order.iter().position(|&x| &x == b).unwrap_or(usize::MAX); pos_a.cmp(&pos_b) }); // Remove "=" if both "+" and "=" are present if keys.contains(&"+") && keys.contains(&"=") { keys.retain(|k| k != &"="); } } fn sort_pgup_pgdown(keys: &mut Vec<&str>) { keys.sort_by(|a, b| { let order = ["PgUp", "PgDn"]; let pos_a = order.iter().position(|&x| &x == a).unwrap_or(usize::MAX); let pos_b = order.iter().position(|&x| &x == b).unwrap_or(usize::MAX); pos_a.cmp(&pos_b) }); } fn format_key_group( keys: &[&str], should_add_brackets: bool, use_pipe_separator: bool, ) -> String { let separator = if use_pipe_separator { "|" } else { "" }; let joined = keys.join(separator); if should_add_brackets { format!("<{}>", joined) } else { joined } } /// Get predetermined actions for a specific mode pub fn get_predetermined_actions( mode_info: &ModeInfo, mode: InputMode, ) -> Vec<(String, String)> { match mode { InputMode::Locked => { let ordered_predicates = vec![|action: &Action| { matches!( action, Action::SwitchToMode { input_mode: InputMode::Normal } ) }]; Self::find_predetermined_actions(mode_info, mode, ordered_predicates) }, InputMode::Normal => { let ordered_predicates = vec![ |action: &Action| { matches!( action, Action::SwitchToMode { input_mode: InputMode::Locked } ) }, |action: &Action| { matches!( action, Action::SwitchToMode { input_mode: InputMode::Pane } ) }, |action: &Action| { matches!( action, Action::SwitchToMode { input_mode: InputMode::Tab } ) }, |action: &Action| { matches!( action, Action::SwitchToMode { input_mode: InputMode::Resize } ) }, |action: &Action| { matches!( action, Action::SwitchToMode { input_mode: InputMode::Move } ) }, |action: &Action| { matches!( action, Action::SwitchToMode { input_mode: InputMode::Scroll } ) }, |action: &Action| { matches!( action, Action::SwitchToMode { input_mode: InputMode::Session } ) }, |action: &Action| matches!(action, Action::Quit), ]; Self::find_predetermined_actions(mode_info, mode, ordered_predicates) }, InputMode::Pane => { let ordered_predicates = vec![ |action: &Action| { matches!( action, Action::NewPane { direction: None, pane_name: None, start_suppressed: false } ) }, |action: &Action| { matches!( action, Action::MoveFocus { direction: Direction::Left } ) }, |action: &Action| { matches!( action, Action::MoveFocus { direction: Direction::Down } ) }, |action: &Action| { matches!( action, Action::MoveFocus { direction: Direction::Up } ) }, |action: &Action| { matches!( action, Action::MoveFocus { direction: Direction::Right } ) }, |action: &Action| matches!(action, Action::CloseFocus), |action: &Action| { matches!( action, Action::SwitchToMode { input_mode: InputMode::RenamePane } ) }, |action: &Action| matches!(action, Action::ToggleFocusFullscreen), |action: &Action| matches!(action, Action::ToggleFloatingPanes), |action: &Action| matches!(action, Action::TogglePaneEmbedOrFloating), |action: &Action| { matches!( action, Action::NewStackedPane { command: None, pane_name: None, near_current_pane: false, .. } ) }, |action: &Action| { matches!( action, Action::NewPane { direction: Some(Direction::Right), pane_name: None, start_suppressed: false } ) }, |action: &Action| { matches!( action, Action::NewPane { direction: Some(Direction::Down), pane_name: None, start_suppressed: false } ) }, ]; Self::find_predetermined_actions(mode_info, mode, ordered_predicates) }, InputMode::Tab => { let ordered_predicates = vec![ |action: &Action| matches!(action, Action::GoToPreviousTab), |action: &Action| matches!(action, Action::GoToNextTab), |action: &Action| { matches!( action, Action::NewTab { tiled_layout: None, floating_layouts: _, swap_tiled_layouts: None, swap_floating_layouts: None, tab_name: None, should_change_focus_to_new_tab: true, cwd: None, initial_panes: _, first_pane_unblock_condition: _, } ) }, |action: &Action| matches!(action, Action::CloseTab), |action: &Action| { matches!( action, Action::SwitchToMode { input_mode: InputMode::RenameTab } ) }, |action: &Action| matches!(action, Action::TabNameInput { .. }), |action: &Action| matches!(action, Action::ToggleActiveSyncTab), |action: &Action| matches!(action, Action::BreakPane), |action: &Action| matches!(action, Action::BreakPaneLeft), |action: &Action| matches!(action, Action::BreakPaneRight), |action: &Action| matches!(action, Action::ToggleTab), ]; Self::find_predetermined_actions(mode_info, mode, ordered_predicates) }, InputMode::Resize => { let ordered_predicates = vec![ |action: &Action| { matches!( action, Action::Resize { resize: Resize::Increase, direction: None } ) }, |action: &Action| { matches!( action, Action::Resize { resize: Resize::Decrease, direction: None } ) }, |action: &Action| { matches!( action, Action::Resize { resize: Resize::Increase, direction: Some(Direction::Left) } ) }, |action: &Action| { matches!( action, Action::Resize { resize: Resize::Increase, direction: Some(Direction::Down) } ) }, |action: &Action| { matches!( action, Action::Resize { resize: Resize::Increase, direction: Some(Direction::Up) } ) }, |action: &Action| { matches!( action, Action::Resize { resize: Resize::Increase, direction: Some(Direction::Right) } ) }, |action: &Action| { matches!( action, Action::Resize { resize: Resize::Decrease, direction: Some(Direction::Left) } ) }, |action: &Action| { matches!( action, Action::Resize { resize: Resize::Decrease, direction: Some(Direction::Down) } ) }, |action: &Action| { matches!( action, Action::Resize { resize: Resize::Decrease, direction: Some(Direction::Up) } ) }, |action: &Action| { matches!( action, Action::Resize { resize: Resize::Decrease, direction: Some(Direction::Right) } ) }, ]; Self::find_predetermined_actions(mode_info, mode, ordered_predicates) }, InputMode::Move => { let ordered_predicates = vec![ |action: &Action| { matches!( action, Action::MovePane { direction: Some(Direction::Left) } ) }, |action: &Action| { matches!( action, Action::MovePane { direction: Some(Direction::Down) } ) }, |action: &Action| { matches!( action, Action::MovePane { direction: Some(Direction::Up) } ) }, |action: &Action| { matches!( action, Action::MovePane { direction: Some(Direction::Right) } ) }, ]; Self::find_predetermined_actions(mode_info, mode, ordered_predicates) }, InputMode::Scroll => { let ordered_predicates = vec![ |action: &Action| matches!(action, Action::ScrollDown), |action: &Action| matches!(action, Action::ScrollUp), |action: &Action| matches!(action, Action::HalfPageScrollDown), |action: &Action| matches!(action, Action::HalfPageScrollUp), |action: &Action| matches!(action, Action::PageScrollDown), |action: &Action| matches!(action, Action::PageScrollUp), |action: &Action| { matches!( action, Action::SwitchToMode { input_mode: InputMode::EnterSearch } ) }, |action: &Action| matches!(action, Action::EditScrollback { .. }), ]; Self::find_predetermined_actions(mode_info, mode, ordered_predicates) }, InputMode::Search => { let ordered_predicates = vec![ |action: &Action| { matches!( action, Action::SwitchToMode { input_mode: InputMode::EnterSearch } ) }, |action: &Action| matches!(action, Action::SearchInput { .. }), |action: &Action| matches!(action, Action::ScrollDown), |action: &Action| matches!(action, Action::ScrollUp), |action: &Action| matches!(action, Action::PageScrollDown), |action: &Action| matches!(action, Action::PageScrollUp), |action: &Action| matches!(action, Action::HalfPageScrollDown), |action: &Action| matches!(action, Action::HalfPageScrollUp), |action: &Action| { matches!( action, Action::Search { direction: actions::SearchDirection::Down } ) }, |action: &Action| { matches!( action, Action::Search { direction: actions::SearchDirection::Up } ) }, |action: &Action| { matches!( action, Action::SearchToggleOption { option: actions::SearchOption::CaseSensitivity } ) }, |action: &Action| { matches!( action, Action::SearchToggleOption { option: actions::SearchOption::Wrap } ) }, |action: &Action| { matches!( action, Action::SearchToggleOption { option: actions::SearchOption::WholeWord } ) }, ]; Self::find_predetermined_actions(mode_info, mode, ordered_predicates) }, InputMode::Session => { let ordered_predicates = vec![ |action: &Action| matches!(action, Action::Detach), |action: &Action| action.launches_plugin("session-manager"), |action: &Action| action.launches_plugin("plugin-manager"), |action: &Action| action.launches_plugin("configuration"), |action: &Action| action.launches_plugin("zellij:about"), ]; Self::find_predetermined_actions(mode_info, mode, ordered_predicates) }, InputMode::EnterSearch | InputMode::RenameTab | InputMode::RenamePane | InputMode::Prompt | InputMode::Tmux => Vec::new(), } } }