generated from GarandPLG/rust-flake-template
8e01c8c33a
Update flake.lock dependencies to latest revisions
333 lines
9.8 KiB
Rust
333 lines
9.8 KiB
Rust
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,
|
||
/// Human‑readable 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)
|
||
}
|