use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::keybinding::{KeyBinding, KeySequence, NamedKey};
#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct KeybindingsConfig {
pub move_down: KeySequence,
pub move_up: KeySequence,
pub move_left: KeySequence,
pub move_right: KeySequence,
pub page_down: KeySequence,
pub page_up: KeySequence,
pub jump_to_first: KeySequence,
pub jump_to_last: KeySequence,
pub jump_back: KeySequence,
pub next_comment: KeySequence,
pub prev_comment: KeySequence,
pub approve: KeySequence,
pub request_changes: KeySequence,
pub comment: KeySequence,
pub suggestion: KeySequence,
pub reply: KeySequence,
pub refresh: KeySequence,
pub submit: KeySequence,
pub quit: KeySequence,
pub help: KeySequence,
pub comment_list: KeySequence,
pub ai_rally: KeySequence,
pub open_panel: KeySequence,
pub go_to_definition: KeySequence,
pub go_to_file: KeySequence,
pub open_in_browser: KeySequence,
pub toggle_local_mode: KeySequence,
pub toggle_auto_focus: KeySequence,
pub toggle_zen_mode: KeySequence,
pub toggle_markdown_rich: KeySequence,
pub filter: KeySequence,
pub multiline_select: KeySequence,
pub pr_description: KeySequence,
pub ci_checks: KeySequence,
pub git_ops: KeySequence,
pub git_ops_stage: KeySequence,
pub git_ops_stage_all: KeySequence,
pub git_ops_discard: KeySequence,
pub git_ops_commit: KeySequence,
pub git_ops_undo: KeySequence,
pub git_ops_reset: KeySequence,
pub git_ops_push: KeySequence,
pub issue_list: KeySequence,
pub tab_switch: KeySequence,
pub mark_viewed: KeySequence,
pub mark_viewed_dir: KeySequence,
pub tree_toggle: KeySequence,
pub shell_command: KeySequence,
pub filter_open: KeySequence,
pub filter_closed: KeySequence,
pub filter_all: KeySequence,
pub tab_prev: KeySequence,
pub tab_next: KeySequence,
pub rally_background: KeySequence,
pub rally_pause: KeySequence,
pub retry: KeySequence,
pub confirm_yes: KeySequence,
pub confirm_no: KeySequence,
}
impl Default for KeybindingsConfig {
fn default() -> Self {
Self {
move_down: KeySequence::single(KeyBinding::char('j'))
.with_alt(vec![KeyBinding::named(crate::keybinding::NamedKey::Down)]),
move_up: KeySequence::single(KeyBinding::char('k'))
.with_alt(vec![KeyBinding::named(crate::keybinding::NamedKey::Up)]),
move_left: KeySequence::single(KeyBinding::char('h'))
.with_alt(vec![KeyBinding::named(crate::keybinding::NamedKey::Left)]),
move_right: KeySequence::single(KeyBinding::char('l'))
.with_alt(vec![KeyBinding::named(crate::keybinding::NamedKey::Right)]),
page_down: KeySequence::single(KeyBinding::ctrl('d')),
page_up: KeySequence::single(KeyBinding::ctrl('u')),
jump_to_first: KeySequence::double(KeyBinding::char('g'), KeyBinding::char('g')),
jump_to_last: KeySequence::single(KeyBinding::char('G')),
jump_back: KeySequence::single(KeyBinding::ctrl('o')),
next_comment: KeySequence::single(KeyBinding::char('n')),
prev_comment: KeySequence::single(KeyBinding::char('N')),
approve: KeySequence::single(KeyBinding::char('a')),
request_changes: KeySequence::single(KeyBinding::char('r')),
comment: KeySequence::single(KeyBinding::char('c')),
suggestion: KeySequence::single(KeyBinding::char('s')),
reply: KeySequence::single(KeyBinding::char('r')),
refresh: KeySequence::single(KeyBinding::char('R')),
submit: KeySequence::single(KeyBinding::ctrl('s')),
quit: KeySequence::single(KeyBinding::char('q'))
.with_alt(vec![KeyBinding::named(crate::keybinding::NamedKey::Esc)]),
help: KeySequence::single(KeyBinding::char('?')),
comment_list: KeySequence::single(KeyBinding::char('C')),
ai_rally: KeySequence::single(KeyBinding::char('A')),
open_panel: KeySequence::single(KeyBinding::named(NamedKey::Enter)),
go_to_definition: KeySequence::double(KeyBinding::char('g'), KeyBinding::char('d')),
go_to_file: KeySequence::double(KeyBinding::char('g'), KeyBinding::char('f')),
open_in_browser: KeySequence::single(KeyBinding::char('O')),
toggle_local_mode: KeySequence::single(KeyBinding::char('L')),
toggle_auto_focus: KeySequence::single(KeyBinding::char('F')),
toggle_zen_mode: KeySequence::single(KeyBinding::char('Z')),
toggle_markdown_rich: KeySequence::single(KeyBinding::char('M')),
filter: KeySequence::double(KeyBinding::char(' '), KeyBinding::char('/')),
multiline_select: KeySequence::single(KeyBinding::char('V')),
pr_description: KeySequence::single(KeyBinding::char('d')),
ci_checks: KeySequence::single(KeyBinding::char('S')),
git_ops: KeySequence::single(KeyBinding::char('G')),
git_ops_stage: KeySequence::single(KeyBinding::char(' ')),
git_ops_stage_all: KeySequence::single(KeyBinding::char('s')),
git_ops_discard: KeySequence::single(KeyBinding::char('d')),
git_ops_commit: KeySequence::single(KeyBinding::char('c')),
git_ops_undo: KeySequence::single(KeyBinding::char('u')),
git_ops_reset: KeySequence::single(KeyBinding::char('r')),
git_ops_push: KeySequence::single(KeyBinding::char('P')),
issue_list: KeySequence::single(KeyBinding::char('I')),
tab_switch: KeySequence::single(KeyBinding::named(crate::keybinding::NamedKey::Tab)),
mark_viewed: KeySequence::single(KeyBinding::char('v')),
mark_viewed_dir: KeySequence::single(KeyBinding::char('V')),
tree_toggle: KeySequence::single(KeyBinding::char('t')),
shell_command: KeySequence::single(KeyBinding::char('!')),
filter_open: KeySequence::single(KeyBinding::char('o')),
filter_closed: KeySequence::single(KeyBinding::char('c')),
filter_all: KeySequence::single(KeyBinding::char('a')),
tab_prev: KeySequence::single(KeyBinding::char('[')),
tab_next: KeySequence::single(KeyBinding::char(']')),
rally_background: KeySequence::single(KeyBinding::char('b')),
rally_pause: KeySequence::single(KeyBinding::char('p')),
retry: KeySequence::single(KeyBinding::char('r')),
confirm_yes: KeySequence::single(KeyBinding::char('y'))
.with_alt(vec![KeyBinding::char('Y')]),
confirm_no: KeySequence::single(KeyBinding::char('n'))
.with_alt(vec![KeyBinding::char('N')]),
}
}
}
impl KeybindingsConfig {
pub fn validate(&self) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
let mut single_keys: HashMap<KeyBinding, &str> = HashMap::new();
let mut sequence_prefixes: HashMap<KeyBinding, &str> = HashMap::new();
let bindings: Vec<(&str, &KeySequence)> = vec![
("move_down", &self.move_down),
("move_up", &self.move_up),
("move_left", &self.move_left),
("move_right", &self.move_right),
("page_down", &self.page_down),
("page_up", &self.page_up),
("jump_to_first", &self.jump_to_first),
("jump_to_last", &self.jump_to_last),
("jump_back", &self.jump_back),
("next_comment", &self.next_comment),
("prev_comment", &self.prev_comment),
("approve", &self.approve),
("request_changes", &self.request_changes),
("comment", &self.comment),
("suggestion", &self.suggestion),
("reply", &self.reply),
("refresh", &self.refresh),
("submit", &self.submit),
("quit", &self.quit),
("help", &self.help),
("comment_list", &self.comment_list),
("ai_rally", &self.ai_rally),
("open_panel", &self.open_panel),
("go_to_definition", &self.go_to_definition),
("go_to_file", &self.go_to_file),
("open_in_browser", &self.open_in_browser),
("toggle_local_mode", &self.toggle_local_mode),
("toggle_auto_focus", &self.toggle_auto_focus),
("toggle_zen_mode", &self.toggle_zen_mode),
("toggle_markdown_rich", &self.toggle_markdown_rich),
("filter", &self.filter),
("multiline_select", &self.multiline_select),
("pr_description", &self.pr_description),
("ci_checks", &self.ci_checks),
("git_ops", &self.git_ops),
("git_ops_stage", &self.git_ops_stage),
("git_ops_stage_all", &self.git_ops_stage_all),
("git_ops_discard", &self.git_ops_discard),
("git_ops_commit", &self.git_ops_commit),
("git_ops_undo", &self.git_ops_undo),
("git_ops_reset", &self.git_ops_reset),
("git_ops_push", &self.git_ops_push),
("issue_list", &self.issue_list),
("tab_switch", &self.tab_switch),
("mark_viewed", &self.mark_viewed),
("mark_viewed_dir", &self.mark_viewed_dir),
("tree_toggle", &self.tree_toggle),
("shell_command", &self.shell_command),
("filter_open", &self.filter_open),
("filter_closed", &self.filter_closed),
("filter_all", &self.filter_all),
("tab_prev", &self.tab_prev),
("tab_next", &self.tab_next),
("rally_background", &self.rally_background),
("rally_pause", &self.rally_pause),
("retry", &self.retry),
("confirm_yes", &self.confirm_yes),
("confirm_no", &self.confirm_no),
];
for (name, seq) in &bindings {
if seq.keys.is_empty() {
errors.push(format!("keybinding '{}' is empty", name));
continue;
}
if seq.is_single() {
let key = seq.keys[0];
if let Some(existing) = single_keys.get(&key) {
if !is_context_compatible(name, existing) {
errors.push(format!(
"duplicate keybinding: '{}' and '{}' both use {}",
name,
existing,
key.display()
));
}
} else {
single_keys.insert(key, name);
}
} else {
if let Some(first) = seq.first() {
sequence_prefixes.insert(*first, name);
}
}
}
for (key, single_name) in &single_keys {
if let Some(seq_name) = sequence_prefixes.get(key) {
if !is_context_compatible(single_name, seq_name) {
errors.push(format!(
"keybinding conflict: '{}' ({}) conflicts with sequence prefix for '{}' ({})",
single_name,
key.display(),
seq_name,
key.display()
));
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
fn is_context_compatible(name1: &str, name2: &str) -> bool {
const SCREEN_SPECIFIC_KEYS: &[&str] = &[
"git_ops_stage",
"git_ops_stage_all",
"git_ops_discard",
"git_ops_commit",
"git_ops_undo",
"git_ops_reset",
"git_ops_push",
"tab_switch",
"mark_viewed",
"mark_viewed_dir",
"tree_toggle",
"shell_command",
"filter_open",
"filter_closed",
"filter_all",
"tab_prev",
"tab_next",
"rally_background",
"rally_pause",
"retry",
"confirm_yes",
"confirm_no",
];
let context_groups: &[&[&str]] = &[
&["reply", "request_changes"],
&["toggle_local_mode", "move_right"], &["toggle_auto_focus", "go_to_file"], &["git_ops", "jump_to_last"], &["filter_closed", "comment"],
&["filter_all", "approve"],
&["retry", "reply", "request_changes"],
&["confirm_no", "next_comment"],
];
if SCREEN_SPECIFIC_KEYS.contains(&name1) || SCREEN_SPECIFIC_KEYS.contains(&name2) {
return true;
}
for group in context_groups {
if group.contains(&name1) && group.contains(&name2) {
return true;
}
}
false
}
impl Serialize for KeybindingsConfig {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(None)?;
fn seq_to_value(seq: &KeySequence) -> toml::Value {
if seq.is_single() && seq.alt.is_empty() {
toml::Value::String(seq.display())
} else if seq.alt.is_empty() {
toml::Value::Array(
seq.keys
.iter()
.map(|k| toml::Value::String(k.display()))
.collect(),
)
} else {
toml::Value::String(seq.display())
}
}
map.serialize_entry("move_down", &seq_to_value(&self.move_down))?;
map.serialize_entry("move_up", &seq_to_value(&self.move_up))?;
map.serialize_entry("move_left", &seq_to_value(&self.move_left))?;
map.serialize_entry("move_right", &seq_to_value(&self.move_right))?;
map.serialize_entry("page_down", &seq_to_value(&self.page_down))?;
map.serialize_entry("page_up", &seq_to_value(&self.page_up))?;
map.serialize_entry("jump_to_first", &seq_to_value(&self.jump_to_first))?;
map.serialize_entry("jump_to_last", &seq_to_value(&self.jump_to_last))?;
map.serialize_entry("jump_back", &seq_to_value(&self.jump_back))?;
map.serialize_entry("next_comment", &seq_to_value(&self.next_comment))?;
map.serialize_entry("prev_comment", &seq_to_value(&self.prev_comment))?;
map.serialize_entry("approve", &seq_to_value(&self.approve))?;
map.serialize_entry("request_changes", &seq_to_value(&self.request_changes))?;
map.serialize_entry("comment", &seq_to_value(&self.comment))?;
map.serialize_entry("suggestion", &seq_to_value(&self.suggestion))?;
map.serialize_entry("reply", &seq_to_value(&self.reply))?;
map.serialize_entry("refresh", &seq_to_value(&self.refresh))?;
map.serialize_entry("submit", &seq_to_value(&self.submit))?;
map.serialize_entry("quit", &seq_to_value(&self.quit))?;
map.serialize_entry("help", &seq_to_value(&self.help))?;
map.serialize_entry("comment_list", &seq_to_value(&self.comment_list))?;
map.serialize_entry("ai_rally", &seq_to_value(&self.ai_rally))?;
map.serialize_entry("open_panel", &seq_to_value(&self.open_panel))?;
map.serialize_entry("go_to_definition", &seq_to_value(&self.go_to_definition))?;
map.serialize_entry("go_to_file", &seq_to_value(&self.go_to_file))?;
map.serialize_entry("open_in_browser", &seq_to_value(&self.open_in_browser))?;
map.serialize_entry("toggle_local_mode", &seq_to_value(&self.toggle_local_mode))?;
map.serialize_entry("toggle_auto_focus", &seq_to_value(&self.toggle_auto_focus))?;
map.serialize_entry("toggle_zen_mode", &seq_to_value(&self.toggle_zen_mode))?;
map.serialize_entry(
"toggle_markdown_rich",
&seq_to_value(&self.toggle_markdown_rich),
)?;
map.serialize_entry("filter", &seq_to_value(&self.filter))?;
map.serialize_entry("multiline_select", &seq_to_value(&self.multiline_select))?;
map.serialize_entry("pr_description", &seq_to_value(&self.pr_description))?;
map.serialize_entry("ci_checks", &seq_to_value(&self.ci_checks))?;
map.serialize_entry("git_ops", &seq_to_value(&self.git_ops))?;
map.serialize_entry("git_ops_stage", &seq_to_value(&self.git_ops_stage))?;
map.serialize_entry("git_ops_stage_all", &seq_to_value(&self.git_ops_stage_all))?;
map.serialize_entry("git_ops_discard", &seq_to_value(&self.git_ops_discard))?;
map.serialize_entry("git_ops_commit", &seq_to_value(&self.git_ops_commit))?;
map.serialize_entry("git_ops_undo", &seq_to_value(&self.git_ops_undo))?;
map.serialize_entry("git_ops_reset", &seq_to_value(&self.git_ops_reset))?;
map.serialize_entry("git_ops_push", &seq_to_value(&self.git_ops_push))?;
map.serialize_entry("issue_list", &seq_to_value(&self.issue_list))?;
map.serialize_entry("tab_switch", &seq_to_value(&self.tab_switch))?;
map.serialize_entry("mark_viewed", &seq_to_value(&self.mark_viewed))?;
map.serialize_entry("mark_viewed_dir", &seq_to_value(&self.mark_viewed_dir))?;
map.serialize_entry("tree_toggle", &seq_to_value(&self.tree_toggle))?;
map.serialize_entry("shell_command", &seq_to_value(&self.shell_command))?;
map.serialize_entry("filter_open", &seq_to_value(&self.filter_open))?;
map.serialize_entry("filter_closed", &seq_to_value(&self.filter_closed))?;
map.serialize_entry("filter_all", &seq_to_value(&self.filter_all))?;
map.serialize_entry("tab_prev", &seq_to_value(&self.tab_prev))?;
map.serialize_entry("tab_next", &seq_to_value(&self.tab_next))?;
map.serialize_entry("rally_background", &seq_to_value(&self.rally_background))?;
map.serialize_entry("rally_pause", &seq_to_value(&self.rally_pause))?;
map.serialize_entry("retry", &seq_to_value(&self.retry))?;
map.serialize_entry("confirm_yes", &seq_to_value(&self.confirm_yes))?;
map.serialize_entry("confirm_no", &seq_to_value(&self.confirm_no))?;
map.end()
}
}