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 fn bindable(key: KeyCode) -> bool {
#[allow(clippy::enum_glob_use)]
use KeyCode::*;
!GLOBAL_KEYS.contains(&key)
&& matches!(
key,
KeyA | KeyB
| KeyC
| KeyD
| KeyE
| KeyF
| KeyG
| KeyH
| KeyI
| KeyJ
| KeyK
| KeyL
| KeyM
| KeyN
| KeyO
| KeyP
| KeyQ
| KeyR
| KeyS
| KeyT
| KeyU
| KeyV
| KeyW
| KeyX
| KeyY
| KeyZ
| Digit0
| Digit1
| Digit2
| Digit3
| Digit4
| Digit5
| Digit6
| Digit7
| Digit8
| Digit9
| ArrowUp
| ArrowDown
| ArrowLeft
| ArrowRight
| Numpad0
| Numpad1
| Numpad2
| Numpad3
| Numpad4
| Numpad5
| Numpad6
| Numpad7
| Numpad8
| Numpad9
| NumpadEnter
| NumpadAdd
| NumpadSubtract
| NumpadMultiply
| NumpadDivide
| NumpadDecimal
| Space
| Tab
| Backspace
| Insert
| Delete
| Home
| End
| PageUp
| PageDown
| ShiftLeft
| ShiftRight
| ControlLeft
| ControlRight
| AltLeft
| AltRight
| Comma
| Period
| Slash
| Semicolon
| Quote
| BracketLeft
| BracketRight
| Backslash
| Minus
| Equal
| Backquote
)
}
fn all_bindable() -> impl Iterator<Item = KeyCode> {
CANDIDATES.iter().copied().filter(|&key| bindable(key))
}
const CANDIDATES: [KeyCode; 89] = {
#[allow(clippy::enum_glob_use)]
use KeyCode::*;
[
KeyA,
KeyB,
KeyC,
KeyD,
KeyE,
KeyF,
KeyG,
KeyH,
KeyI,
KeyJ,
KeyK,
KeyL,
KeyM,
KeyN,
KeyO,
KeyP,
KeyQ,
KeyR,
KeyS,
KeyT,
KeyU,
KeyV,
KeyW,
KeyX,
KeyY,
KeyZ,
Digit0,
Digit1,
Digit2,
Digit3,
Digit4,
Digit5,
Digit6,
Digit7,
Digit8,
Digit9,
ArrowUp,
ArrowDown,
ArrowLeft,
ArrowRight,
Numpad0,
Numpad1,
Numpad2,
Numpad3,
Numpad4,
Numpad5,
Numpad6,
Numpad7,
Numpad8,
Numpad9,
NumpadEnter,
NumpadAdd,
NumpadSubtract,
NumpadMultiply,
NumpadDivide,
NumpadDecimal,
Space,
Tab,
Backspace,
Insert,
Delete,
Home,
End,
PageUp,
PageDown,
ShiftLeft,
ShiftRight,
ControlLeft,
ControlRight,
AltLeft,
AltRight,
Comma,
Period,
Slash,
Semicolon,
Quote,
BracketLeft,
BracketRight,
Backslash,
Minus,
Equal,
Backquote,
Escape,
Enter,
F1,
F2,
F3,
F4,
F5,
]
};
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");
}
}