use crate::theme::parsing::{parse_key_chord, strip_inline_comment};
use crate::theme::types::{KeyChord, Settings};
fn assign_keybind(chord: Option<KeyChord>, target: &mut Vec<KeyChord>) {
if let Some(ch) = chord {
*target = vec![ch];
}
}
fn assign_keybind_with_duplicate_check(chord: Option<KeyChord>, target: &mut Vec<KeyChord>) {
if let Some(ch) = chord
&& target
.iter()
.all(|c| c.code != ch.code || c.mods != ch.mods)
{
target.push(ch);
}
}
fn apply_global_keybind(key: &str, chord: Option<KeyChord>, settings: &mut Settings) -> bool {
match key {
"keybind_help" | "keybind_help_overlay" => {
assign_keybind(chord, &mut settings.keymap.help_overlay);
true
}
"keybind_toggle_config" | "keybind_config_menu" | "keybind_config_lists" => {
assign_keybind(chord, &mut settings.keymap.config_menu_toggle);
true
}
"keybind_toggle_options" | "keybind_options_menu" => {
assign_keybind(chord, &mut settings.keymap.options_menu_toggle);
true
}
"keybind_toggle_panels" | "keybind_panels_menu" => {
assign_keybind(chord, &mut settings.keymap.panels_menu_toggle);
true
}
"keybind_reload_config" | "keybind_reload_theme" | "keybind_reload" => {
assign_keybind(chord, &mut settings.keymap.reload_config);
true
}
"keybind_exit" | "keybind_quit" => {
assign_keybind(chord, &mut settings.keymap.exit);
true
}
"keybind_show_pkgbuild" | "keybind_pkgbuild" | "keybind_toggle_pkgbuild" => {
assign_keybind(chord, &mut settings.keymap.show_pkgbuild);
true
}
"keybind_comments_toggle" | "keybind_show_comments" | "keybind_toggle_comments" => {
assign_keybind(chord, &mut settings.keymap.comments_toggle);
true
}
"keybind_run_pkgbuild_checks"
| "keybind_pkgbuild_checks"
| "keybind_toggle_pkgbuild_checks" => {
assign_keybind(chord, &mut settings.keymap.run_pkgbuild_checks);
true
}
"keybind_cycle_pkgbuild_sections"
| "keybind_pkgbuild_section_cycle"
| "keybind_pkgbuild_next_section" => {
assign_keybind(chord, &mut settings.keymap.cycle_pkgbuild_sections);
true
}
"keybind_change_sort" | "keybind_sort" => {
assign_keybind(chord, &mut settings.keymap.change_sort);
true
}
"keybind_pane_next" | "keybind_next_pane" | "keybind_switch_pane" => {
assign_keybind(chord, &mut settings.keymap.pane_next);
true
}
"keybind_pane_left" => {
assign_keybind(chord, &mut settings.keymap.pane_left);
true
}
"keybind_pane_right" => {
assign_keybind(chord, &mut settings.keymap.pane_right);
true
}
_ => false,
}
}
fn apply_search_keybind(key: &str, chord: Option<KeyChord>, settings: &mut Settings) -> bool {
match key {
"keybind_search_move_up" => {
assign_keybind(chord, &mut settings.keymap.search_move_up);
true
}
"keybind_search_move_down" => {
assign_keybind(chord, &mut settings.keymap.search_move_down);
true
}
"keybind_search_page_up" => {
assign_keybind(chord, &mut settings.keymap.search_page_up);
true
}
"keybind_search_page_down" => {
assign_keybind(chord, &mut settings.keymap.search_page_down);
true
}
"keybind_search_add" => {
assign_keybind(chord, &mut settings.keymap.search_add);
true
}
"keybind_search_install" => {
assign_keybind(chord, &mut settings.keymap.search_install);
true
}
"keybind_search_focus_left" => {
assign_keybind(chord, &mut settings.keymap.search_focus_left);
true
}
"keybind_search_focus_right" => {
assign_keybind(chord, &mut settings.keymap.search_focus_right);
true
}
"keybind_search_backspace" => {
assign_keybind(chord, &mut settings.keymap.search_backspace);
true
}
"keybind_search_insert_clear" => {
assign_keybind(chord, &mut settings.keymap.search_insert_clear);
true
}
"keybind_search_normal_toggle" => {
assign_keybind(chord, &mut settings.keymap.search_normal_toggle);
true
}
"keybind_search_normal_insert" => {
assign_keybind(chord, &mut settings.keymap.search_normal_insert);
true
}
"keybind_search_normal_select_left" => {
assign_keybind(chord, &mut settings.keymap.search_normal_select_left);
true
}
"keybind_search_normal_select_right" => {
assign_keybind(chord, &mut settings.keymap.search_normal_select_right);
true
}
"keybind_search_normal_delete" => {
assign_keybind(chord, &mut settings.keymap.search_normal_delete);
true
}
"keybind_search_normal_clear" => {
assign_keybind(chord, &mut settings.keymap.search_normal_clear);
true
}
"keybind_search_normal_open_status"
| "keybind_normal_open_status"
| "keybind_open_status" => {
assign_keybind(chord, &mut settings.keymap.search_normal_open_status);
true
}
"keybind_search_normal_import" => {
assign_keybind(chord, &mut settings.keymap.search_normal_import);
true
}
"keybind_search_normal_export" => {
assign_keybind(chord, &mut settings.keymap.search_normal_export);
true
}
"keybind_search_normal_updates" => {
assign_keybind(chord, &mut settings.keymap.search_normal_updates);
true
}
"keybind_toggle_fuzzy" | "keybind_fuzzy_toggle" => {
assign_keybind(chord, &mut settings.keymap.toggle_fuzzy);
true
}
_ => false,
}
}
fn apply_recent_keybind(key: &str, chord: Option<KeyChord>, settings: &mut Settings) -> bool {
match key {
"keybind_recent_move_up" => {
assign_keybind(chord, &mut settings.keymap.recent_move_up);
true
}
"keybind_recent_move_down" => {
assign_keybind(chord, &mut settings.keymap.recent_move_down);
true
}
"keybind_recent_find" => {
assign_keybind(chord, &mut settings.keymap.recent_find);
true
}
"keybind_recent_use" => {
assign_keybind(chord, &mut settings.keymap.recent_use);
true
}
"keybind_recent_add" => {
assign_keybind(chord, &mut settings.keymap.recent_add);
true
}
"keybind_recent_to_search" => {
assign_keybind(chord, &mut settings.keymap.recent_to_search);
true
}
"keybind_recent_focus_right" => {
assign_keybind(chord, &mut settings.keymap.recent_focus_right);
true
}
"keybind_recent_remove" => {
assign_keybind_with_duplicate_check(chord, &mut settings.keymap.recent_remove);
true
}
"keybind_recent_clear" => {
assign_keybind(chord, &mut settings.keymap.recent_clear);
true
}
_ => false,
}
}
fn apply_install_keybind(key: &str, chord: Option<KeyChord>, settings: &mut Settings) -> bool {
match key {
"keybind_install_move_up" => {
assign_keybind(chord, &mut settings.keymap.install_move_up);
true
}
"keybind_install_move_down" => {
assign_keybind(chord, &mut settings.keymap.install_move_down);
true
}
"keybind_install_confirm" => {
assign_keybind(chord, &mut settings.keymap.install_confirm);
true
}
"keybind_install_remove" => {
assign_keybind_with_duplicate_check(chord, &mut settings.keymap.install_remove);
true
}
"keybind_install_clear" => {
assign_keybind(chord, &mut settings.keymap.install_clear);
true
}
"keybind_install_find" => {
assign_keybind(chord, &mut settings.keymap.install_find);
true
}
"keybind_install_to_search" => {
assign_keybind(chord, &mut settings.keymap.install_to_search);
true
}
"keybind_install_focus_left" => {
assign_keybind(chord, &mut settings.keymap.install_focus_left);
true
}
_ => false,
}
}
fn apply_news_keybind(key: &str, chord: Option<KeyChord>, settings: &mut Settings) -> bool {
match key {
"keybind_news_mark_read" => {
if chord.is_none() {
tracing::warn!("Failed to parse keybind_news_mark_read");
}
assign_keybind(chord, &mut settings.keymap.news_mark_read);
true
}
"keybind_news_mark_all_read" => {
if chord.is_none() {
tracing::warn!("Failed to parse keybind_news_mark_all_read");
}
assign_keybind(chord, &mut settings.keymap.news_mark_all_read);
true
}
"keybind_news_feed_mark_read" => {
if chord.is_none() {
tracing::warn!("Failed to parse keybind_news_feed_mark_read");
}
assign_keybind(chord, &mut settings.keymap.news_mark_read_feed);
true
}
"keybind_news_feed_mark_unread" => {
if chord.is_none() {
tracing::warn!("Failed to parse keybind_news_feed_mark_unread");
}
assign_keybind(chord, &mut settings.keymap.news_mark_unread_feed);
true
}
"keybind_news_feed_toggle_read" => {
if chord.is_none() {
tracing::warn!("Failed to parse keybind_news_feed_toggle_read");
}
assign_keybind(chord, &mut settings.keymap.news_toggle_read_feed);
true
}
_ => false,
}
}
fn apply_keybind(key: &str, chord: Option<KeyChord>, settings: &mut Settings) {
if apply_global_keybind(key, chord, settings) {
return;
}
if apply_search_keybind(key, chord, settings) {
return;
}
if apply_recent_keybind(key, chord, settings) {
return;
}
if apply_install_keybind(key, chord, settings) {
return;
}
apply_news_keybind(key, chord, settings);
}
pub fn parse_keybinds(content: &str, settings: &mut Settings) {
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("//") {
continue;
}
if !trimmed.contains('=') {
continue;
}
let mut parts = trimmed.splitn(2, '=');
let raw_key = parts.next().unwrap_or("");
let key = raw_key.trim().to_lowercase().replace(['.', '-', ' '], "_");
let val_raw = parts.next().unwrap_or("").trim();
let val = strip_inline_comment(val_raw);
let chord = parse_key_chord(val);
apply_keybind(&key, chord, settings);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::{KeyCode, KeyModifiers};
fn create_test_chord() -> KeyChord {
KeyChord {
code: KeyCode::Char('r'),
mods: KeyModifiers::CONTROL,
}
}
#[test]
fn test_apply_global_keybind() {
let mut settings = Settings::default();
let chord = create_test_chord();
assert!(apply_global_keybind(
"keybind_help",
Some(chord),
&mut settings
));
assert_eq!(settings.keymap.help_overlay.len(), 1);
assert_eq!(settings.keymap.help_overlay[0], chord);
let mut settings2 = Settings::default();
assert!(apply_global_keybind(
"keybind_help_overlay",
Some(chord),
&mut settings2
));
assert_eq!(settings2.keymap.help_overlay.len(), 1);
let mut settings3 = Settings::default();
assert!(apply_global_keybind(
"keybind_toggle_config",
Some(chord),
&mut settings3
));
assert_eq!(settings3.keymap.config_menu_toggle.len(), 1);
let mut settings4 = Settings::default();
assert!(apply_global_keybind(
"keybind_config_menu",
Some(chord),
&mut settings4
));
assert_eq!(settings4.keymap.config_menu_toggle.len(), 1);
let mut settings5 = Settings::default();
assert!(!apply_global_keybind(
"keybind_invalid",
Some(chord),
&mut settings5
));
let mut settings6 = Settings::default();
let initial_len = settings6.keymap.help_overlay.len();
apply_global_keybind("keybind_help", None, &mut settings6);
assert_eq!(settings6.keymap.help_overlay.len(), initial_len);
}
#[test]
fn test_apply_search_keybind() {
let mut settings = Settings::default();
let chord = create_test_chord();
assert!(apply_search_keybind(
"keybind_search_move_up",
Some(chord),
&mut settings
));
assert_eq!(settings.keymap.search_move_up.len(), 1);
assert!(apply_search_keybind(
"keybind_search_add",
Some(chord),
&mut settings
));
assert_eq!(settings.keymap.search_add.len(), 1);
let mut settings2 = Settings::default();
assert!(apply_search_keybind(
"keybind_search_normal_open_status",
Some(chord),
&mut settings2
));
assert_eq!(settings2.keymap.search_normal_open_status.len(), 1);
let mut settings3 = Settings::default();
assert!(apply_search_keybind(
"keybind_normal_open_status",
Some(chord),
&mut settings3
));
assert_eq!(settings3.keymap.search_normal_open_status.len(), 1);
let mut settings4 = Settings::default();
assert!(apply_search_keybind(
"keybind_open_status",
Some(chord),
&mut settings4
));
assert_eq!(settings4.keymap.search_normal_open_status.len(), 1);
assert!(!apply_search_keybind(
"keybind_invalid",
Some(chord),
&mut settings
));
}
#[test]
fn test_apply_recent_keybind() {
let mut settings = Settings::default();
let chord1 = KeyChord {
code: KeyCode::Char('d'),
mods: KeyModifiers::CONTROL,
};
let chord2 = KeyChord {
code: KeyCode::Char('x'),
mods: KeyModifiers::CONTROL,
};
assert!(apply_recent_keybind(
"keybind_recent_move_up",
Some(chord1),
&mut settings
));
assert_eq!(settings.keymap.recent_move_up.len(), 1);
assert!(apply_recent_keybind(
"keybind_recent_move_up",
Some(chord2),
&mut settings
));
assert_eq!(settings.keymap.recent_move_up.len(), 1);
let mut settings2 = Settings::default();
let initial_len = settings2.keymap.recent_remove.len(); assert!(apply_recent_keybind(
"keybind_recent_remove",
Some(chord1),
&mut settings2
));
assert_eq!(settings2.keymap.recent_remove.len(), initial_len + 1); assert!(apply_recent_keybind(
"keybind_recent_remove",
Some(chord2),
&mut settings2
));
assert_eq!(settings2.keymap.recent_remove.len(), initial_len + 2);
assert!(apply_recent_keybind(
"keybind_recent_remove",
Some(chord1),
&mut settings2
));
assert_eq!(settings2.keymap.recent_remove.len(), initial_len + 2);
assert!(!apply_recent_keybind(
"keybind_invalid",
Some(chord1),
&mut settings
));
}
#[test]
fn test_apply_install_keybind() {
let mut settings = Settings::default();
let chord1 = KeyChord {
code: KeyCode::Char('d'),
mods: KeyModifiers::CONTROL,
};
let chord2 = KeyChord {
code: KeyCode::Char('x'),
mods: KeyModifiers::CONTROL,
};
assert!(apply_install_keybind(
"keybind_install_move_up",
Some(chord1),
&mut settings
));
assert_eq!(settings.keymap.install_move_up.len(), 1);
assert!(apply_install_keybind(
"keybind_install_move_up",
Some(chord2),
&mut settings
));
assert_eq!(settings.keymap.install_move_up.len(), 1);
let mut settings2 = Settings::default();
let initial_len = settings2.keymap.install_remove.len(); assert!(apply_install_keybind(
"keybind_install_remove",
Some(chord1),
&mut settings2
));
assert_eq!(settings2.keymap.install_remove.len(), initial_len + 1); assert!(apply_install_keybind(
"keybind_install_remove",
Some(chord2),
&mut settings2
));
assert_eq!(settings2.keymap.install_remove.len(), initial_len + 2);
assert!(!apply_install_keybind(
"keybind_invalid",
Some(chord1),
&mut settings
));
}
#[test]
fn test_apply_news_keybind() {
let mut settings = Settings::default();
let chord = create_test_chord();
assert!(apply_news_keybind(
"keybind_news_mark_read",
Some(chord),
&mut settings
));
assert_eq!(settings.keymap.news_mark_read.len(), 1);
assert_eq!(settings.keymap.news_mark_read[0], chord);
let mut settings2 = Settings::default();
assert!(apply_news_keybind(
"keybind_news_mark_all_read",
Some(chord),
&mut settings2
));
assert_eq!(settings2.keymap.news_mark_all_read.len(), 1);
assert_eq!(settings2.keymap.news_mark_all_read[0], chord);
let mut settings3 = Settings::default();
assert!(apply_news_keybind(
"keybind_news_feed_mark_read",
Some(chord),
&mut settings3
));
assert_eq!(settings3.keymap.news_mark_read_feed.len(), 1);
assert_eq!(settings3.keymap.news_mark_read_feed[0], chord);
let mut settings4 = Settings::default();
assert!(apply_news_keybind(
"keybind_news_feed_mark_unread",
Some(chord),
&mut settings4
));
assert_eq!(settings4.keymap.news_mark_unread_feed.len(), 1);
assert_eq!(settings4.keymap.news_mark_unread_feed[0], chord);
let mut settings5 = Settings::default();
assert!(apply_news_keybind(
"keybind_news_feed_toggle_read",
Some(chord),
&mut settings5
));
assert_eq!(settings5.keymap.news_toggle_read_feed.len(), 1);
assert_eq!(settings5.keymap.news_toggle_read_feed[0], chord);
assert!(!apply_news_keybind(
"keybind_invalid",
Some(chord),
&mut settings
));
}
#[test]
fn test_apply_keybind_routing() {
let mut settings = Settings::default();
let chord = create_test_chord();
apply_keybind("keybind_help", Some(chord), &mut settings);
assert_eq!(settings.keymap.help_overlay.len(), 1);
let mut settings2 = Settings::default();
apply_keybind("keybind_search_move_up", Some(chord), &mut settings2);
assert_eq!(settings2.keymap.search_move_up.len(), 1);
let mut settings3 = Settings::default();
apply_keybind("keybind_recent_move_up", Some(chord), &mut settings3);
assert_eq!(settings3.keymap.recent_move_up.len(), 1);
let mut settings4 = Settings::default();
apply_keybind("keybind_install_move_up", Some(chord), &mut settings4);
assert_eq!(settings4.keymap.install_move_up.len(), 1);
let mut settings5 = Settings::default();
apply_keybind("keybind_news_mark_all_read", Some(chord), &mut settings5);
assert_eq!(settings5.keymap.news_mark_all_read.len(), 1);
let mut settings6 = Settings::default();
apply_keybind("keybind_unknown", Some(chord), &mut settings6);
}
#[test]
fn test_parse_keybinds() {
let mut settings = Settings::default();
let content = r"
# This is a comment
keybind_help = Ctrl+R
keybind.search.move.up = Ctrl+Up
keybind-search-add = Ctrl+A
keybind search install = Ctrl+I
// Another comment
keybind_recent_remove = Ctrl+D
keybind_recent_remove = Ctrl+X
invalid_line_without_equals
";
parse_keybinds(content, &mut settings);
assert_eq!(settings.keymap.help_overlay.len(), 1);
assert_eq!(settings.keymap.search_move_up.len(), 1);
assert_eq!(settings.keymap.search_add.len(), 1);
assert_eq!(settings.keymap.search_install.len(), 1);
assert_eq!(settings.keymap.recent_remove.len(), 4);
}
#[test]
fn test_parse_news_keybinds() {
let mut settings = Settings::default();
let content = r"
keybind_news_mark_read = r
keybind_news_mark_all_read = CTRL+R
";
parse_keybinds(content, &mut settings);
assert_eq!(settings.keymap.news_mark_read.len(), 1);
assert_eq!(settings.keymap.news_mark_read[0].code, KeyCode::Char('r'));
assert!(settings.keymap.news_mark_read[0].mods.is_empty());
assert_eq!(settings.keymap.news_mark_read[0].label(), "R");
assert_eq!(settings.keymap.news_mark_all_read.len(), 1);
assert_eq!(
settings.keymap.news_mark_all_read[0].code,
KeyCode::Char('r')
);
assert!(
settings.keymap.news_mark_all_read[0]
.mods
.contains(KeyModifiers::CONTROL)
);
assert_eq!(settings.keymap.news_mark_all_read[0].label(), "Ctrl+R");
}
#[test]
fn test_parse_pkgbuild_checks_keybind() {
let mut settings = Settings::default();
let content = "keybind_run_pkgbuild_checks = Ctrl+K";
parse_keybinds(content, &mut settings);
assert_eq!(settings.keymap.run_pkgbuild_checks.len(), 1);
assert_eq!(
settings.keymap.run_pkgbuild_checks[0].label(),
"Ctrl+K".to_string()
);
}
#[test]
fn test_assign_keybind_with_duplicate_check() {
let mut target = Vec::new();
let chord1 = KeyChord {
code: KeyCode::Char('d'),
mods: KeyModifiers::CONTROL,
};
let chord2 = KeyChord {
code: KeyCode::Char('d'),
mods: KeyModifiers::CONTROL, };
let chord3 = KeyChord {
code: KeyCode::Char('d'),
mods: KeyModifiers::ALT, };
let chord4 = KeyChord {
code: KeyCode::Char('x'),
mods: KeyModifiers::CONTROL, };
assign_keybind_with_duplicate_check(Some(chord1), &mut target);
assert_eq!(target.len(), 1);
assign_keybind_with_duplicate_check(Some(chord2), &mut target);
assert_eq!(target.len(), 1);
assign_keybind_with_duplicate_check(Some(chord3), &mut target);
assert_eq!(target.len(), 2);
assign_keybind_with_duplicate_check(Some(chord4), &mut target);
assert_eq!(target.len(), 3);
let len_before = target.len();
assign_keybind_with_duplicate_check(None, &mut target);
assert_eq!(target.len(), len_before);
}
}