Refactor keybindings and add validated CLI arguments

Extracted common quit/esc handling into `common_keybindings`. Introduced
`Group::CtrlMovement` for Ctrl‑arrow keys and updated keybinding
definitions.
Removed duplicated quit logic in main_menu and skirmish modules. Bumped
clap to 4.6.0 and updated Cargo.lock. Added range‑checked parsers for
map
size, resources, supply limit, XP modifier and skill points in the CLI.
This commit is contained in:
2026-03-27 20:15:15 +01:00
parent 9300eef906
commit ba0028b7a7
9 changed files with 73 additions and 53 deletions
Generated
+12 -12
View File
@@ -19,9 +19,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "anstream"
version = "0.6.21"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
@@ -40,9 +40,9 @@ checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
@@ -165,9 +165,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "clap"
version = "4.5.60"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
dependencies = [
"clap_builder",
"clap_derive",
@@ -175,9 +175,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.60"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [
"anstream",
"anstyle",
@@ -187,9 +187,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.55"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
dependencies = [
"heck",
"proc-macro2",
@@ -963,9 +963,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.44"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
-1
View File
@@ -12,6 +12,5 @@ strip = true
[dependencies]
clap = { version = "4.5.53", features = ["derive"] }
ratatui = "0.30.0"
log = "0.4.29"
simplelog = "0.12.2"
+5 -7
View File
@@ -69,21 +69,19 @@ pub fn handle_input_events(tx: mpsc::Sender<Event>) {
loop {
match event::read() {
Ok(ev) => match ev {
event::Event::Key(key_event) => {
if tx.send(Event::Input(key_event)).is_err() {
event::Event::Key(key) => {
if tx.send(Event::Input(key)).is_err() {
break;
}
}
event::Event::Resize(cols, rows) => {
if tx.send(Event::Resize(cols, rows)).is_err() {
event::Event::Resize(c, r) => {
if tx.send(Event::Resize(c, r)).is_err() {
break;
}
}
_ => {}
},
Err(_) => {
continue;
}
Err(_) => continue,
}
}
}
+11 -8
View File
@@ -4,13 +4,16 @@ use crate::app::{
};
use ratatui::crossterm::event::KeyEvent;
pub fn default_keybindings(app: &mut App, key_event: &KeyEvent) {
if let Some(action) = event_to_action(&key_event) {
match action {
Action::Quit => app.exit = true,
Action::Quit2 => app.exit = true,
Action::Esc => app.view = View::MainMenu,
_ => (),
}
pub fn common_keybindings(app: &mut App, action: Action) {
match action {
Action::Quit | Action::Quit2 => app.exit = true,
Action::Esc => app.view = View::MainMenu,
_ => (),
}
}
pub fn default_keybindings(app: &mut App, key_event: &KeyEvent) {
if let Some(action) = event_to_action(&key_event) {
common_keybindings(app, action);
}
}
+6 -5
View File
@@ -22,6 +22,7 @@ pub enum Action {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)]
pub enum Group {
CtrlMovement,
Movement,
Scroll,
Select,
@@ -100,7 +101,7 @@ pub static KEYBINDINGS: &[KeyBinding] = &[
code: KeyCode::Up,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::CONTROL,
group: Group::Movement,
group: Group::CtrlMovement,
symbol: "Ctrl + ↑",
description: "Scroll Up ",
},
@@ -109,7 +110,7 @@ pub static KEYBINDINGS: &[KeyBinding] = &[
code: KeyCode::Down,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::CONTROL,
group: Group::Movement,
group: Group::CtrlMovement,
symbol: "Ctrl + ↓",
description: "Scroll Down",
},
@@ -118,7 +119,7 @@ pub static KEYBINDINGS: &[KeyBinding] = &[
code: KeyCode::Left,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::CONTROL,
group: Group::Movement,
group: Group::CtrlMovement,
symbol: "Ctrl + ←",
description: "Scroll Left",
},
@@ -127,7 +128,7 @@ pub static KEYBINDINGS: &[KeyBinding] = &[
code: KeyCode::Right,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::CONTROL,
group: Group::Movement,
group: Group::CtrlMovement,
symbol: "Ctrl + →",
description: "Scroll Right",
},
@@ -156,7 +157,7 @@ pub static KEYBINDINGS: &[KeyBinding] = &[
modifiers: KeyModifiers::NONE,
group: Group::Movement,
symbol: "Esc",
description: "Go back",
description: "Go back to main menu",
},
KeyBinding {
action: Action::Backspace,
+2 -3
View File
@@ -1,14 +1,13 @@
use crate::app::{
App, View,
keybindings::{Action, event_to_action},
keybindings::{Action, common_keybindings, event_to_action},
};
use ratatui::crossterm::event::KeyEvent;
pub fn main_menu_keybindings(app: &mut App, event: &KeyEvent) {
if let Some(action) = event_to_action(&event) {
common_keybindings(app, action);
match action {
Action::Quit => app.exit = true,
Action::Quit2 => app.exit = true,
Action::Up => {
app.states.main_menu.selected_view =
app.states.main_menu.selected_view.saturating_sub(1).max(1);
+1 -1
View File
@@ -3,7 +3,7 @@ pub mod keybindings;
pub mod main_menu;
pub mod skirmish;
pub use default::default_keybindings;
pub use default::{common_keybindings, default_keybindings};
pub use keybindings::{Action, Group, KEYBINDINGS, KeyBinding, binding_for, event_to_action};
pub use main_menu::main_menu_keybindings;
pub use skirmish::skirmish_keybindings;
+3 -5
View File
@@ -1,19 +1,17 @@
use crate::app::{
App, View,
keybindings::{Action, event_to_action},
App,
keybindings::{Action, common_keybindings, event_to_action},
};
use ratatui::crossterm::event::KeyEvent;
pub fn skirmish_keybindings(app: &mut App, key_event: &KeyEvent) {
if let Some(action) = event_to_action(&key_event) {
common_keybindings(app, action);
match action {
Action::ScrollUp => app.states.skirmish.vertical_offset.prev(),
Action::ScrollDown => app.states.skirmish.vertical_offset.next(),
Action::ScrollLeft => app.states.skirmish.horizontal_offset.prev(),
Action::ScrollRight => app.states.skirmish.horizontal_offset.next(),
Action::Quit => app.exit = true,
Action::Quit2 => app.exit = true,
Action::Esc => app.view = View::MainMenu,
_ => (),
}
}
+33 -11
View File
@@ -2,7 +2,8 @@ use crate::app::{
states::{GameMode, PerkDecks},
view::View,
};
use clap::Parser;
use clap::{Error, Parser, error::ErrorKind, value_parser};
use std::num::ParseFloatError;
#[derive(Parser, Debug)]
#[command(version, about = "War in Tunnels", long_about = "War in Tunnels")]
@@ -37,7 +38,8 @@ pub struct Cli {
long,
help = "Map width",
value_name = "Positive integer [20; 100]",
default_value = "50"
default_value = "50",
value_parser = value_parser!(u8).range(20..=100)
)]
pub map_width: u8,
@@ -45,7 +47,8 @@ pub struct Cli {
long,
help = "Map height",
value_name = "Positive integer [11; 50]",
default_value = "25"
default_value = "25",
value_parser = value_parser!(u8).range(25..=50)
)]
pub map_height: u8,
@@ -61,24 +64,27 @@ pub struct Cli {
#[arg(
long,
help = "Starting wood",
value_name = "Positive integer",
default_value = "50"
value_name = "Positive integer [1; 150]",
default_value = "50",
value_parser = value_parser!(u16).range(1..=150)
)]
pub starting_wood: u16,
#[arg(
long,
help = "Starting iron",
value_name = "Positive integer",
default_value = "25"
value_name = "Positive integer [1; 150]",
default_value = "25",
value_parser = value_parser!(u16).range(1..=150)
)]
pub starting_iron: u16,
#[arg(
long,
help = "Supply limit",
value_name = "Positive integer",
default_value = "99"
value_name = "Positive integer [49; 149]",
default_value = "99",
value_parser = value_parser!(u8).range(49..=149)
)]
pub supply_limit: u8,
@@ -86,7 +92,8 @@ pub struct Cli {
long,
help = "XP modifier",
value_name = "Float [0.5; 2.0]",
default_value = "1.0"
default_value = "1.0",
value_parser = parse_xp
)]
pub xp_modifier: f32,
@@ -94,7 +101,8 @@ pub struct Cli {
long,
help = "Skill points limit",
value_name = "Positive integer [120; 690]",
default_value = "120"
default_value = "120",
value_parser = value_parser!(u16).range(120..=690)
)]
pub skill_points_limit: u16,
@@ -109,3 +117,17 @@ pub struct Cli {
pub fn get_args() -> Cli {
Cli::parse()
}
fn parse_xp(s: &str) -> Result<f32, Error> {
let v: f32 = s
.parse()
.map_err(|e: ParseFloatError| Error::raw(ErrorKind::InvalidValue, e.to_string()))?;
if (0.5..=2.0).contains(&v) {
Ok(v)
} else {
Err(Error::raw(
ErrorKind::InvalidValue,
format!("Value {} is out of range 0.5..=2.0", v),
))
}
}