use crate::chord::KeyChord;
use crate::hotkey::Hotkey;
use crate::mode::KeyMode;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConflictEntry {
pub mode: String,
pub hotkey: Hotkey,
pub existing: String,
pub new: String,
}
#[derive(Debug, Clone, Default)]
pub struct ConflictReport {
pub conflicts: Vec<ConflictEntry>,
}
impl ConflictReport {
#[must_use]
pub fn is_clean(&self) -> bool {
self.conflicts.is_empty()
}
}
#[must_use]
pub fn detect_conflicts(modes: &[&KeyMode], chords: &[KeyChord]) -> ConflictReport {
let mut report = ConflictReport::default();
for mode in modes {
for chord in chords {
if mode.bindings.contains_key(&chord.leader) {
report.conflicts.push(ConflictEntry {
mode: mode.name.clone(),
hotkey: chord.leader,
existing: format!(
"binding: {:?}",
mode.bindings[&chord.leader].action
),
new: format!("chord leader (follower: {})", chord.follower),
});
}
}
}
report
}
#[cfg(test)]
mod tests {
use super::*;
use crate::action::Action;
use crate::binding::Binding;
use crate::hotkey::{Key, Modifiers};
fn ctrl_a() -> Hotkey {
Hotkey::new(Modifiers::CTRL, Key::A)
}
#[test]
fn no_conflicts() {
let mode = KeyMode::new("default", true);
let report = detect_conflicts(&[&mode], &[]);
assert!(report.is_clean());
}
#[test]
fn chord_leader_conflicts_with_binding() {
let mut mode = KeyMode::new("default", true);
mode.add_binding(Binding::new(ctrl_a(), Action::command("select_all")));
let chord = KeyChord {
leader: ctrl_a(),
follower: Hotkey::new(Modifiers::NONE, Key::C),
timeout_ms: 1000,
action: Action::command("new_window"),
};
let report = detect_conflicts(&[&mode], &[chord]);
assert!(!report.is_clean());
assert_eq!(report.conflicts.len(), 1);
assert_eq!(report.conflicts[0].mode, "default");
assert_eq!(report.conflicts[0].hotkey, ctrl_a());
}
#[test]
fn chord_no_conflict_when_leader_not_bound() {
let mut mode = KeyMode::new("default", true);
mode.add_binding(Binding::new(
Hotkey::new(Modifiers::CMD, Key::H),
Action::command("focus_west"),
));
let chord = KeyChord {
leader: ctrl_a(),
follower: Hotkey::new(Modifiers::NONE, Key::C),
timeout_ms: 1000,
action: Action::command("new_window"),
};
let report = detect_conflicts(&[&mode], &[chord]);
assert!(report.is_clean());
}
#[test]
fn conflict_in_specific_mode() {
let mut resize = KeyMode::new("resize", false);
resize.add_binding(Binding::new(ctrl_a(), Action::command("shrink")));
let default = KeyMode::new("default", true);
let chord = KeyChord {
leader: ctrl_a(),
follower: Hotkey::new(Modifiers::NONE, Key::C),
timeout_ms: 1000,
action: Action::command("new_window"),
};
let report = detect_conflicts(&[&default, &resize], &[chord]);
assert_eq!(report.conflicts.len(), 1);
assert_eq!(report.conflicts[0].mode, "resize");
}
#[test]
fn no_modes_no_conflicts() {
let report = detect_conflicts(&[], &[]);
assert!(report.is_clean());
}
#[test]
fn chords_only_no_modes_no_conflicts() {
let chord = KeyChord {
leader: ctrl_a(),
follower: Hotkey::new(Modifiers::NONE, Key::C),
timeout_ms: 1000,
action: Action::command("test"),
};
let report = detect_conflicts(&[], &[chord]);
assert!(report.is_clean());
}
#[test]
fn modes_only_no_chords_no_conflicts() {
let mut mode = KeyMode::new("default", true);
mode.add_binding(Binding::new(ctrl_a(), Action::command("test")));
let report = detect_conflicts(&[&mode], &[]);
assert!(report.is_clean());
}
#[test]
fn same_chord_conflicts_in_multiple_modes() {
let mut default = KeyMode::new("default", true);
default.add_binding(Binding::new(ctrl_a(), Action::command("select_all")));
let mut resize = KeyMode::new("resize", false);
resize.add_binding(Binding::new(ctrl_a(), Action::command("shrink")));
let chord = KeyChord {
leader: ctrl_a(),
follower: Hotkey::new(Modifiers::NONE, Key::C),
timeout_ms: 1000,
action: Action::command("new_window"),
};
let report = detect_conflicts(&[&default, &resize], &[chord]);
assert_eq!(report.conflicts.len(), 2);
let modes: Vec<&str> = report.conflicts.iter().map(|c| c.mode.as_str()).collect();
assert!(modes.contains(&"default"));
assert!(modes.contains(&"resize"));
}
#[test]
fn multiple_chords_multiple_conflicts() {
let mut mode = KeyMode::new("default", true);
mode.add_binding(Binding::new(ctrl_a(), Action::command("a")));
mode.add_binding(Binding::new(
Hotkey::new(Modifiers::CTRL, Key::B),
Action::command("b"),
));
let chord1 = KeyChord {
leader: ctrl_a(),
follower: Hotkey::new(Modifiers::NONE, Key::C),
timeout_ms: 1000,
action: Action::command("chord_a"),
};
let chord2 = KeyChord {
leader: Hotkey::new(Modifiers::CTRL, Key::B),
follower: Hotkey::new(Modifiers::NONE, Key::C),
timeout_ms: 1000,
action: Action::command("chord_b"),
};
let report = detect_conflicts(&[&mode], &[chord1, chord2]);
assert_eq!(report.conflicts.len(), 2);
}
#[test]
fn conflict_entry_contains_hotkey() {
let mut mode = KeyMode::new("default", true);
mode.add_binding(Binding::new(ctrl_a(), Action::command("select_all")));
let chord = KeyChord {
leader: ctrl_a(),
follower: Hotkey::new(Modifiers::NONE, Key::C),
timeout_ms: 1000,
action: Action::command("new_window"),
};
let report = detect_conflicts(&[&mode], &[chord]);
assert_eq!(report.conflicts[0].hotkey, ctrl_a());
assert!(report.conflicts[0].existing.contains("select_all"));
assert!(report.conflicts[0].new.contains("chord leader"));
}
#[test]
fn conflict_report_is_clean_true_when_empty() {
let report = ConflictReport::default();
assert!(report.is_clean());
}
#[test]
fn conflict_report_is_clean_false_with_entries() {
let report = ConflictReport {
conflicts: vec![ConflictEntry {
mode: "default".to_string(),
hotkey: ctrl_a(),
existing: "a".to_string(),
new: "b".to_string(),
}],
};
assert!(!report.is_clean());
}
}