use bevy::prelude::KeyCode;
pub const BOUND_SEATS: usize = 2;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Action {
MoveUp,
MoveDown,
MoveLeft,
MoveRight,
PlaceUp,
PlaceDown,
PlaceLeft,
PlaceRight,
Remove,
ClearAll,
}
impl Action {
pub const ALL: [Action; 10] = [
Action::MoveUp,
Action::MoveDown,
Action::MoveLeft,
Action::MoveRight,
Action::PlaceUp,
Action::PlaceDown,
Action::PlaceLeft,
Action::PlaceRight,
Action::Remove,
Action::ClearAll,
];
pub fn index(self) -> usize {
Action::ALL
.iter()
.position(|&a| a == self)
.expect("every action is in ALL")
}
}
pub const ACTIONS: usize = Action::ALL.len();
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct SeatBinds(pub [KeyCode; ACTIONS]);
impl SeatBinds {
pub fn key(&self, action: Action) -> KeyCode {
self.0[action.index()]
}
pub fn set(&mut self, action: Action, key: KeyCode) {
self.0[action.index()] = key;
}
pub fn default_for(seat: usize) -> SeatBinds {
if seat == 0 {
SeatBinds([
KeyCode::KeyW,
KeyCode::KeyS,
KeyCode::KeyA,
KeyCode::KeyD,
KeyCode::ArrowUp,
KeyCode::ArrowDown,
KeyCode::ArrowLeft,
KeyCode::ArrowRight,
KeyCode::Space,
KeyCode::ShiftLeft,
])
} else {
SeatBinds([
KeyCode::KeyI,
KeyCode::KeyK,
KeyCode::KeyJ,
KeyCode::KeyL,
KeyCode::Numpad8,
KeyCode::Numpad5,
KeyCode::Numpad4,
KeyCode::Numpad6,
KeyCode::Numpad0,
KeyCode::NumpadEnter,
])
}
}
}
pub fn default_binds() -> [SeatBinds; BOUND_SEATS] {
std::array::from_fn(SeatBinds::default_for)
}
pub fn conflict(binds: &[SeatBinds; BOUND_SEATS], key: KeyCode) -> Option<(usize, Action)> {
for (seat, seat_binds) in binds.iter().enumerate() {
for action in Action::ALL {
if seat_binds.key(action) == key {
return Some((seat, action));
}
}
}
None
}
pub fn all_distinct(binds: &[SeatBinds; BOUND_SEATS]) -> bool {
let mut names: Vec<String> = binds
.iter()
.flat_map(|seat| seat.0.iter().copied().map(key_name))
.collect();
let total = names.len();
names.sort();
names.dedup();
names.len() == total
}
pub const GLOBAL_KEYS: [KeyCode; 6] = [
KeyCode::KeyM,
KeyCode::KeyH,
KeyCode::KeyN,
KeyCode::KeyP,
KeyCode::KeyR,
KeyCode::KeyC,
];
pub const GLOBAL_LETTERS: [char; 6] = ['M', 'H', 'N', 'P', 'R', 'C'];
pub fn global_key_from_name(name: &str) -> Option<KeyCode> {
GLOBAL_KEYS.into_iter().find(|&key| key_name(key) == name)
}
pub fn bindable(key: KeyCode) -> bool {
!GLOBAL_KEYS.contains(&key) && BINDABLE.contains(&key)
}
pub(crate) fn all_bindable() -> impl Iterator<Item = KeyCode> {
BINDABLE
.iter()
.copied()
.filter(|key| !GLOBAL_KEYS.contains(key))
}
const BINDABLE: [KeyCode; 82] = [
KeyCode::KeyA,
KeyCode::KeyB,
KeyCode::KeyC,
KeyCode::KeyD,
KeyCode::KeyE,
KeyCode::KeyF,
KeyCode::KeyG,
KeyCode::KeyH,
KeyCode::KeyI,
KeyCode::KeyJ,
KeyCode::KeyK,
KeyCode::KeyL,
KeyCode::KeyM,
KeyCode::KeyN,
KeyCode::KeyO,
KeyCode::KeyP,
KeyCode::KeyQ,
KeyCode::KeyR,
KeyCode::KeyS,
KeyCode::KeyT,
KeyCode::KeyU,
KeyCode::KeyV,
KeyCode::KeyW,
KeyCode::KeyX,
KeyCode::KeyY,
KeyCode::KeyZ,
KeyCode::Digit0,
KeyCode::Digit1,
KeyCode::Digit2,
KeyCode::Digit3,
KeyCode::Digit4,
KeyCode::Digit5,
KeyCode::Digit6,
KeyCode::Digit7,
KeyCode::Digit8,
KeyCode::Digit9,
KeyCode::ArrowUp,
KeyCode::ArrowDown,
KeyCode::ArrowLeft,
KeyCode::ArrowRight,
KeyCode::Numpad0,
KeyCode::Numpad1,
KeyCode::Numpad2,
KeyCode::Numpad3,
KeyCode::Numpad4,
KeyCode::Numpad5,
KeyCode::Numpad6,
KeyCode::Numpad7,
KeyCode::Numpad8,
KeyCode::Numpad9,
KeyCode::NumpadEnter,
KeyCode::NumpadAdd,
KeyCode::NumpadSubtract,
KeyCode::NumpadMultiply,
KeyCode::NumpadDivide,
KeyCode::NumpadDecimal,
KeyCode::Space,
KeyCode::Tab,
KeyCode::Backspace,
KeyCode::Insert,
KeyCode::Delete,
KeyCode::Home,
KeyCode::End,
KeyCode::PageUp,
KeyCode::PageDown,
KeyCode::ShiftLeft,
KeyCode::ShiftRight,
KeyCode::ControlLeft,
KeyCode::ControlRight,
KeyCode::AltLeft,
KeyCode::AltRight,
KeyCode::Comma,
KeyCode::Period,
KeyCode::Slash,
KeyCode::Semicolon,
KeyCode::Quote,
KeyCode::BracketLeft,
KeyCode::BracketRight,
KeyCode::Backslash,
KeyCode::Minus,
KeyCode::Equal,
KeyCode::Backquote,
];
pub fn key_name(key: KeyCode) -> String {
format!("{key:?}")
}
pub fn key_from_name(name: &str) -> Option<KeyCode> {
all_bindable().find(|&key| key_name(key) == name)
}
pub fn key_label(key: KeyCode) -> String {
let name = key_name(key);
for (spelling, cap) in [
("Comma", ","),
("Period", "."),
("Slash", "/"),
("Backslash", "\\"),
("Semicolon", ";"),
("Quote", "'"),
("BracketLeft", "["),
("BracketRight", "]"),
("Minus", "-"),
("Equal", "="),
("Backquote", "`"),
] {
if name == spelling {
return cap.to_string();
}
}
for (prefix, cap) in [
("Key", ""),
("Digit", ""),
("Arrow", ""),
("Numpad", "Num "),
] {
if let Some(rest) = name.strip_prefix(prefix) {
return format!("{cap}{rest}");
}
}
for side in ["Left", "Right"] {
if let Some(rest) = name.strip_suffix(side) {
return format!("{side} {rest}");
}
}
for (run_on, spaced) in [("PageUp", "Page Up"), ("PageDown", "Page Down")] {
if name == run_on {
return spaced.to_string();
}
}
name
}
pub fn to_text(binds: &[SeatBinds; BOUND_SEATS]) -> String {
let mut out = String::new();
for (seat, seat_binds) in binds.iter().enumerate() {
let names: Vec<String> = seat_binds.0.iter().copied().map(key_name).collect();
out.push_str(&format!("keys_p{}: {}\n", seat + 1, names.join(" ")));
}
out
}
pub fn parse_seat(value: &str) -> Option<SeatBinds> {
let names: Vec<&str> = value.split_whitespace().collect();
if names.len() != ACTIONS {
return None;
}
let mut keys = [KeyCode::Space; ACTIONS];
for (slot, name) in keys.iter_mut().zip(&names) {
*slot = key_from_name(name)?;
}
let mut seen: Vec<String> = keys.iter().copied().map(key_name).collect();
seen.sort();
seen.dedup();
(seen.len() == ACTIONS).then_some(SeatBinds(keys))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn key_names_round_trip() {
for key in all_bindable() {
assert_eq!(key_from_name(&key_name(key)), Some(key), "{key:?}");
}
assert_eq!(key_from_name("NoSuchKey"), None);
assert!(!bindable(KeyCode::Escape));
assert_eq!(key_from_name("Escape"), None);
}
#[test]
fn global_keys_are_not_bindable() {
for key in GLOBAL_KEYS {
assert!(!bindable(key), "{key:?} is read whatever the bindings say");
assert_eq!(key_from_name(&key_name(key)), None, "{key:?}");
}
assert!(all_bindable().all(|key| !GLOBAL_KEYS.contains(&key)));
for seat in default_binds() {
for action in Action::ALL {
assert!(bindable(seat.key(action)), "{action:?}");
}
}
}
#[test]
fn labels_read_like_keycaps() {
assert_eq!(key_label(KeyCode::KeyW), "W");
assert_eq!(key_label(KeyCode::Digit4), "4");
assert_eq!(key_label(KeyCode::ArrowUp), "Up");
assert_eq!(key_label(KeyCode::Numpad8), "Num 8");
assert_eq!(key_label(KeyCode::ShiftLeft), "Left Shift");
assert_eq!(key_label(KeyCode::ControlRight), "Right Control");
assert_eq!(key_label(KeyCode::Backquote), "`");
assert_eq!(key_label(KeyCode::PageUp), "Page Up");
}
#[test]
fn the_default_layout_has_no_clashes() {
let binds = default_binds();
for seat in 0..BOUND_SEATS {
for action in Action::ALL {
assert_eq!(
conflict(&binds, binds[seat].key(action)),
Some((seat, action)),
"{seat} {action:?} is claimed by someone else"
);
}
}
assert_eq!(conflict(&binds, KeyCode::F5), None);
}
#[test]
fn bindings_round_trip_through_text() {
let mut binds = default_binds();
binds[0].set(Action::Remove, KeyCode::Backquote);
binds[1].set(Action::MoveUp, KeyCode::Home);
let text = to_text(&binds);
for (seat, line) in text.lines().enumerate() {
let (_, value) = line.split_once(':').expect("key: value");
assert_eq!(parse_seat(value), Some(binds[seat]));
}
}
#[test]
fn broken_binding_lines_are_refused() {
assert_eq!(parse_seat("KeyW KeyS"), None, "too few keys");
assert_eq!(parse_seat("Bogus ".repeat(10).trim()), None, "unknown key");
let doubled = "KeyW KeyW KeyA KeyD ArrowUp ArrowDown ArrowLeft ArrowRight Space ShiftLeft";
assert_eq!(parse_seat(doubled), None, "one key, two jobs");
}
}