Files
war-in-tunnels/src/app/keybindings/keybindings.rs
T
GarandPLG 8e01c8c33a Add audio subsystem with mute and volume controls
Update flake.lock dependencies to latest revisions
2026-04-07 01:05:33 +02:00

333 lines
9.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use clap::ValueEnum;
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use std::collections::HashMap;
/// Represents the set of actions that the UI can perform.
///
/// Each variant corresponds to a concrete operation that can be triggered by a
/// key binding. The `WildCard` variant is a special case that matches any
/// character key and carries the actual character pressed.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Action {
/// Request the application to quit.
Quit,
/// Alternate quit action.
Quit2,
/// Move the selection cursor up.
Up,
/// Move the selection cursor down.
Down,
/// Move the selection cursor left.
Left,
/// Move the selection cursor right.
Right,
/// Scroll up without moving the cursor.
ScrollUp,
/// Scroll down without moving the cursor.
ScrollDown,
/// Scroll left without moving the cursor.
ScrollLeft,
/// Scroll right without moving the cursor.
ScrollRight,
/// Select the current item.
Space,
/// Submit or confirm the current choice.
Enter,
/// Return to the main menu or previous screen.
Esc,
/// Delete the character before the cursor.
Backspace,
/// Zoom the view in.
ZoomIn,
/// Zoom the view out.
ZoomOut,
/// Mute music.
Mute,
/// Volume up.
VolumeUp,
/// Volume down.
VolumeDown,
/// Matches any character key; the inner `char` is the actual key pressed.
WildCard(char),
}
/// Logical groups of key bindings used for organising help screens or
/// configuration sections.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)]
pub enum Group {
/// Bindings that combine `Ctrl` with movement keys.
CtrlMovement,
/// Plain movement bindings.
Movement,
/// Scroll bindings.
Scroll,
/// Selection bindings.
Select,
/// Text input related bindings.
Input,
/// Zoom related bindings.
Zoom,
/// Music related bindings.
Music,
/// Quit related bindings.
Quit,
}
/// Description of a single key binding.
///
/// This struct stores the mapping from a physical key event to an `Action`,
/// together with metadata used for displaying help information.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct KeyBinding {
/// The logical action performed when this binding is triggered.
pub action: Action,
/// The raw `crossterm::event::KeyCode` that identifies the key.
pub code: KeyCode,
/// The kind of key event (crossterm::event::KeyEventKind).
pub kind: KeyEventKind,
/// Any modifier keys that must be held (crossterm::event::KeyModifiers).
pub modifiers: KeyModifiers,
/// The group this binding belongs to, useful for categorising help.
pub group: Group,
/// Humanreadable symbol shown in UI/help screens.
pub symbol: &'static str,
/// Short description of what the binding does.
pub description: &'static str,
}
/// Complete list of all key bindings used by the application.
///
/// The slice is ordered primarily for readability; the runtime lookup scans
/// this slice linearly, which is acceptable given the modest size of the list.
pub static KEYBINDINGS: &[KeyBinding] = &[
KeyBinding {
action: Action::Quit,
code: KeyCode::Char('q'),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Quit,
symbol: "q",
description: "Quit",
},
KeyBinding {
action: Action::Quit2,
code: KeyCode::Char('c'),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::CONTROL,
group: Group::Quit,
symbol: "Ctrl + c",
description: "Quit",
},
KeyBinding {
action: Action::Up,
code: KeyCode::Up,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Movement,
symbol: "",
description: "Up",
},
KeyBinding {
action: Action::Down,
code: KeyCode::Down,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Movement,
symbol: "",
description: "Down",
},
KeyBinding {
action: Action::Left,
code: KeyCode::Left,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Movement,
symbol: "",
description: "Left",
},
KeyBinding {
action: Action::Right,
code: KeyCode::Right,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Movement,
symbol: "",
description: "Right",
},
KeyBinding {
action: Action::ScrollUp,
code: KeyCode::Up,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::CONTROL,
group: Group::CtrlMovement,
symbol: "Ctrl + ↑",
description: "Scroll Up ",
},
KeyBinding {
action: Action::ScrollDown,
code: KeyCode::Down,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::CONTROL,
group: Group::CtrlMovement,
symbol: "Ctrl + ↓",
description: "Scroll Down",
},
KeyBinding {
action: Action::ScrollLeft,
code: KeyCode::Left,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::CONTROL,
group: Group::CtrlMovement,
symbol: "Ctrl + ←",
description: "Scroll Left",
},
KeyBinding {
action: Action::ScrollRight,
code: KeyCode::Right,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::CONTROL,
group: Group::CtrlMovement,
symbol: "Ctrl + →",
description: "Scroll Right",
},
KeyBinding {
action: Action::Space,
code: KeyCode::Char(' '),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Select,
symbol: "Space",
description: "Select",
},
KeyBinding {
action: Action::Enter,
code: KeyCode::Enter,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Select,
symbol: "Enter",
description: "Submit",
},
KeyBinding {
action: Action::Esc,
code: KeyCode::Esc,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Quit,
symbol: "Esc",
description: "Main menu",
},
KeyBinding {
action: Action::Backspace,
code: KeyCode::Backspace,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Input,
symbol: "Backspace",
description: "Delete character",
},
KeyBinding {
action: Action::ZoomIn,
code: KeyCode::Char(','),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Zoom,
symbol: ",",
description: "Zoom in",
},
KeyBinding {
action: Action::ZoomOut,
code: KeyCode::Char('.'),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Zoom,
symbol: ".",
description: "Zoom out",
},
KeyBinding {
action: Action::Mute,
code: KeyCode::Char('m'),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Music,
symbol: "m",
description: "Mute music",
},
KeyBinding {
action: Action::VolumeUp,
code: KeyCode::Char('b'),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Music,
symbol: "b",
description: "Increase music volume",
},
KeyBinding {
action: Action::VolumeDown,
code: KeyCode::Char('n'),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Music,
symbol: "n",
description: "Decrease music volume",
},
KeyBinding {
action: Action::WildCard('_'),
code: KeyCode::Char('_'),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Input,
symbol: "[All]",
description: "All keyboard characters",
},
];
/// Returns the `KeyBinding` associated with the given `Action`, if one exists.
pub fn binding_for(action: Action) -> Option<&'static KeyBinding> {
KEYBINDINGS.iter().find(|b| b.action == action)
}
/// Converts a raw `KeyEvent` into an `Action` if a matching binding is found.
///
/// The function first looks for an exact match on code, kind, and modifiers.
/// If no exact match exists but a wildcard binding is present for the same
/// event kind, the function returns `Action::WildCard` with the character that
/// was pressed. Returns `None` when the event does not correspond to any known
/// action.
pub fn event_to_action(event: &KeyEvent) -> Option<Action> {
if let Some(b) = KEYBINDINGS
.iter()
.find(|b| b.code == event.code && b.kind == event.kind && b.modifiers == event.modifiers)
{
return Some(b.action);
}
if KEYBINDINGS
.iter()
.any(|b| matches!(b.action, Action::WildCard(_)) && b.kind == event.kind)
{
if let KeyCode::Char(c) = event.code {
return Some(Action::WildCard(c));
}
}
None
}
/// Determines the size of the largest group among a slice of actions.
///
/// It counts how many times each group appears in the supplied actions and
/// returns the highest count. This can be used, for example, to size UI
/// elements that display groups of bindings.
pub fn count_largest_group(actions: &[Action]) -> u16 {
let mut group_counts: HashMap<Group, u16> = HashMap::new();
for action in actions {
if let Some(binding) = binding_for(*action) {
*group_counts.entry(binding.group).or_insert(0) += 1;
}
}
group_counts.values().copied().max().map_or(0, |v| v)
}