From 052ce692f65cf9a79692db67eef83586964b9616 Mon Sep 17 00:00:00 2001 From: Brett Mayson Date: Sat, 9 Nov 2024 20:03:39 +0000 Subject: [PATCH] use clap derive, more table formats --- Cargo.lock | 104 +++++++++-- Cargo.toml | 1 + bin/Cargo.toml | 4 +- bin/src/commands/book.rs | 11 +- bin/src/commands/build.rs | 69 +++---- bin/src/commands/check.rs | 11 +- bin/src/commands/dev.rs | 97 +++++----- bin/src/commands/launch/mod.rs | 120 +++++-------- bin/src/commands/localization/coverage.rs | 120 ++++++++----- bin/src/commands/localization/mod.rs | 45 ++--- bin/src/commands/localization/sort.rs | 14 +- bin/src/commands/mod.rs | 7 + bin/src/commands/new/mod.rs | 25 ++- bin/src/commands/release.rs | 50 +++--- bin/src/commands/script.rs | 25 +-- bin/src/commands/utils.rs | 61 ++++--- bin/src/commands/value.rs | 28 +-- bin/src/commands/wiki.rs | 23 +-- bin/src/lib.rs | 210 +++++++++++----------- bin/src/main.rs | 11 +- bin/src/utils/config/inspect.rs | 6 + bin/src/utils/config/mod.rs | 36 ++-- bin/src/utils/inspect.rs | 29 ++- bin/src/utils/paa/convert.rs | 24 +-- bin/src/utils/paa/inspect.rs | 101 ++++++----- bin/src/utils/paa/mod.rs | 40 ++--- bin/src/utils/pbo/extract.rs | 36 ++-- bin/src/utils/pbo/inspect.rs | 88 ++++----- bin/src/utils/pbo/mod.rs | 47 +++-- bin/src/utils/pbo/unpack.rs | 27 +-- bin/src/utils/sqf/case.rs | 17 +- bin/src/utils/sqf/mod.rs | 28 +-- bin/src/utils/verify.rs | 26 ++- bin/tests/build.rs | 11 +- bin/tests/new.rs | 5 +- libs/paa/Cargo.toml | 7 +- libs/paa/src/mipmap.rs | 6 + 37 files changed, 790 insertions(+), 780 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 077dfbc8..76abd4e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -369,6 +369,12 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytecount" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" + [[package]] name = "bytemuck" version = "1.19.0" @@ -528,6 +534,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -552,6 +559,18 @@ dependencies = [ "clap", ] +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "clap_lex" version = "0.7.2" @@ -1382,6 +1401,12 @@ dependencies = [ "http 0.2.12", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -1422,7 +1447,7 @@ dependencies = [ "serde", "serde_json", "supports-hyperlinks", - "term-table", + "tabled", "terminal-link", "time", "tracing", @@ -2970,6 +2995,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "papergrid" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7419ad52a7de9b60d33e11085a0fe3df1fbd5926aa3f93d3dd53afbc9e86725" +dependencies = [ + "bytecount", + "fnv", + "unicode-width", +] + [[package]] name = "parking_lot" version = "0.12.3" @@ -3265,6 +3301,30 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.89" @@ -4175,7 +4235,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "rustversion", @@ -4264,12 +4324,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" dependencies = [ "cfg-expr", - "heck", + "heck 0.5.0", "pkg-config", "toml 0.8.19", "version-compare", ] +[[package]] +name = "tabled" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c9303ee60b9bedf722012ea29ae3711ba13a67c9b9ae28993838b63057cb1b" +dependencies = [ + "papergrid", + "tabled_derive", +] + +[[package]] +name = "tabled_derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf0fb8bfdc709786c154e24a66777493fb63ae97e3036d914c8666774c477069" +dependencies = [ + "heck 0.4.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "target-lexicon" version = "0.12.16" @@ -4300,17 +4383,6 @@ dependencies = [ "utf-8", ] -[[package]] -name = "term-table" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "210f90191b719267bc8b6307659faf54a77400d06b8033ece26692696fc002be" -dependencies = [ - "lazy_static", - "regex", - "unicode-width", -] - [[package]] name = "termcolor" version = "1.4.1" @@ -4783,9 +4855,9 @@ checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-width" -version = "0.1.14" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "unsafe-libyaml" diff --git a/Cargo.toml b/Cargo.toml index 1dfeef4a..45092d39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ serde_json = "1.0.132" sha-1 = "0.10.1" strsim = "0.11.1" supports-hyperlinks = "3.0.0" +tabled = "0.16.0" terminal-link = "0.1.0" thiserror = "1.0.65" toml = "0.8.19" diff --git a/bin/Cargo.toml b/bin/Cargo.toml index 722d4b69..567c5108 100644 --- a/bin/Cargo.toml +++ b/bin/Cargo.toml @@ -32,7 +32,7 @@ hemtt-stringtable = { path = "../libs/stringtable", version = "1.0.0" } hemtt-workspace = { path = "../libs/workspace", version = "1.0.0" } arma3-wiki = { workspace = true } -clap = { workspace = true } +clap = { workspace = true, features = ["derive"] } dialoguer = "0.11.0" dirs = { workspace = true } fs_extra = "1.3.0" @@ -49,7 +49,7 @@ semver = "1.0.23" serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } supports-hyperlinks = { workspace = true } -term-table = "1.4.0" +tabled = { workspace = true } terminal-link = { workspace = true } time = { version = "0.3.36", features = ["formatting"] } tracing = { workspace = true } diff --git a/bin/src/commands/book.rs b/bin/src/commands/book.rs index d4e2de0a..334657f1 100644 --- a/bin/src/commands/book.rs +++ b/bin/src/commands/book.rs @@ -1,17 +1,14 @@ -use clap::{ArgMatches, Command}; - use crate::{report::Report, Error}; -#[must_use] -pub fn cli() -> Command { - Command::new("book").about("Open The HEMTT book") -} +#[derive(clap::Parser)] +/// Open The HEMTT book +pub struct Command {} /// Execute the book command /// /// # Errors /// Will not return an error -pub fn execute(_: &ArgMatches) -> Result { +pub fn execute(_: &Command) -> Result { if let Err(e) = webbrowser::open("https://hemtt.dev/") { eprintln!("Failed to open the HEMTT book: {e}"); } diff --git a/bin/src/commands/build.rs b/bin/src/commands/build.rs index 61d3eb9c..93a56770 100644 --- a/bin/src/commands/build.rs +++ b/bin/src/commands/build.rs @@ -1,5 +1,3 @@ -use clap::{ArgAction, ArgMatches, Command}; - use crate::{ context::{self, Context}, error::Error, @@ -10,51 +8,38 @@ use crate::{ use super::global_modules; -#[must_use] -pub fn cli() -> Command { - add_just(add_args( - Command::new("build") - .about("Build the project for final testing") - .long_about( - "Build your project in release mode for testing, without signing for full release.", - ), - )) -} +#[derive(clap::Parser)] +#[command( + long_about = "Build your project in release mode for testing, without signing for full release." +)] +/// Build the project for final testing +pub struct Command { + #[clap(flatten)] + build: Args, -#[must_use] -pub fn add_args(cmd: Command) -> Command { - cmd.arg( - clap::Arg::new("no-bin") - .long("no-bin") - .help("Do not binarize the project") - .action(ArgAction::SetTrue), - ) - .arg( - clap::Arg::new("no-rap") - .long("no-rap") - .help("Do not rapify (cpp, rvmat)") - .action(ArgAction::SetTrue), - ) + #[clap(flatten)] + just: super::JustArgs, } -#[must_use] -pub fn add_just(cmd: Command) -> Command { - cmd.arg( - clap::Arg::new("just") - .long("just") - .help("Only build the given addon") - .action(ArgAction::Append), - ) +#[derive(clap::Args)] +pub struct Args { + #[arg(long, action = clap::ArgAction::SetTrue)] + /// Do not binarize the project + no_bin: bool, + #[arg(long, action = clap::ArgAction::SetTrue)] + /// Do not rapify (cpp, rvmat) + no_rap: bool, } /// Execute the build command, build a new executor /// /// # Errors /// [`Error`] depending on the modules -pub fn execute(matches: &ArgMatches) -> Result { - let just = matches - .get_many::("just") - .unwrap_or_default() +pub fn execute(cmd: &Command) -> Result { + let just = cmd + .just + .just + .iter() .map(|s| s.to_lowercase()) .collect::>(); let mut ctx = Context::new( @@ -70,7 +55,7 @@ pub fn execute(matches: &ArgMatches) -> Result { if !just.is_empty() { ctx = ctx.filter(|a, _| just.contains(&a.name().to_lowercase())); } - let mut executor = executor(ctx, matches); + let mut executor = executor(ctx, &cmd.build); if !just.is_empty() { warn!("Use of `--just` is not recommended, only use it if you know what you're doing"); @@ -82,16 +67,16 @@ pub fn execute(matches: &ArgMatches) -> Result { } #[must_use] -pub fn executor(ctx: Context, matches: &ArgMatches) -> Executor { +pub fn executor(ctx: Context, args: &Args) -> Executor { let mut executor = Executor::new(ctx); global_modules(&mut executor); executor.collapse(Collapse::No); - if matches.get_one::("no-rap") != Some(&true) { + if !args.no_rap { executor.add_module(Box::::default()); } - if matches.get_one::("no-bin") != Some(&true) { + if !args.no_bin { executor.add_module(Box::::default()); } executor.add_module(Box::::default()); diff --git a/bin/src/commands/check.rs b/bin/src/commands/check.rs index d091fa20..99555faa 100644 --- a/bin/src/commands/check.rs +++ b/bin/src/commands/check.rs @@ -1,5 +1,3 @@ -use clap::Command; - use crate::{ commands::global_modules, context::Context, @@ -9,16 +7,15 @@ use crate::{ report::Report, }; -#[must_use] -pub fn cli() -> Command { - Command::new("check").about("Check the project for errors") -} +#[derive(clap::Parser)] +/// Check the project for errors +pub struct Command {} /// Execute the dev command /// /// # Errors /// [`Error`] depending on the modules -pub fn execute() -> Result { +pub fn execute(_: &Command) -> Result { let ctx = Context::new( Some("check"), crate::context::PreservePrevious::Remove, diff --git a/bin/src/commands/dev.rs b/bin/src/commands/dev.rs index 7a9837cb..9546dc99 100644 --- a/bin/src/commands/dev.rs +++ b/bin/src/commands/dev.rs @@ -1,4 +1,3 @@ -use clap::{ArgAction, ArgMatches, Command}; use hemtt_workspace::addons::Location; use crate::{ @@ -10,54 +9,49 @@ use crate::{ report::Report, }; -use super::build::add_just; - -#[must_use] -pub fn cli() -> Command { - add_just(add_args( - Command::new("dev") - .about("Build the project for development") - .long_about("Build your project for local development and testing. It is built without binarization of .p3d and .rtm files."), - )) +use super::JustArgs; + +// #[must_use] +// pub fn cli() -> Command { +// add_just(add_args( +// Command::new("dev") +// .about("Build the project for development") +// .long_about("Build your project for local development and testing. It is built without binarization of .p3d and .rtm files."), +// )) +// } + +#[derive(clap::Parser)] +/// Build the project for development +pub struct Command { + #[clap(flatten)] + dev: Args, + + #[clap(flatten)] + just: JustArgs, } -#[must_use] -pub fn add_args(cmd: Command) -> Command { - cmd.arg( - clap::Arg::new("binarize") - .long("binarize") - .short('b') - .help("Use BI's binarize on supported files") - .action(ArgAction::SetTrue), - ) - .arg( - clap::Arg::new("optional") - .long("optional") - .short('o') - .help("Include an optional addon folder") - .action(ArgAction::Append), - ) - .arg( - clap::Arg::new("optionals") - .long("all-optionals") - .short('O') - .help("Include all optional addon folders") - .action(ArgAction::SetTrue), - ) - .arg( - clap::Arg::new("no-rap") - .long("no-rap") - .help("Do not rapify (cpp, rvmat)") - .action(ArgAction::SetTrue), - ) +#[derive(clap::Args)] +pub struct Args { + #[arg(long, short, action = clap::ArgAction::SetTrue)] + /// Use BI's binarize on supported files + binarize: bool, + #[arg(long = "optional", short, action = clap::ArgAction::Append)] + /// Include an optional addon folder + optionals: Vec, + #[arg(long, short = 'O', action = clap::ArgAction::SetTrue)] + /// Include all optional addon folders + all_optionals: bool, + #[arg(long, action = clap::ArgAction::SetTrue)] + /// Do not rapify (cpp, rvmat) + no_rap: bool, } /// Execute the dev command /// /// # Errors /// [`Error`] depending on the modules -pub fn execute(matches: &ArgMatches, launch_optionals: &[String]) -> Result { - let mut executor = context(matches, launch_optionals, false, true)?; +pub fn execute(cmd: &Command, launch_optionals: &[String]) -> Result { + let mut executor = context(&cmd.dev, &cmd.just, launch_optionals, false, true)?; executor.run() } @@ -66,21 +60,22 @@ pub fn execute(matches: &ArgMatches, launch_optionals: &[String]) -> Result Result { - let all_optionals = matches.get_one::("optionals") == Some(&true); - let optionals = matches - .get_many::("optional") - .unwrap_or_default() + let all_optionals = dev.all_optionals; + let optionals = dev + .optionals + .iter() .map(std::string::String::as_str) .collect::>(); - let just = matches - .get_many::("just") - .unwrap_or_default() + let just = just + .just + .iter() .map(|s| s.to_lowercase()) .collect::>(); @@ -127,12 +122,12 @@ pub fn context( executor.collapse(Collapse::Yes); - if rapify && matches.get_one::("no-rap") != Some(&true) { + if rapify && !dev.no_rap { executor.add_module(Box::::default()); } executor.add_module(Box::::default()); executor.add_module(Box::::default()); - if force_binarize || matches.get_one::("binarize") == Some(&true) { + if force_binarize || dev.binarize { executor.add_module(Box::::default()); } diff --git a/bin/src/commands/launch/mod.rs b/bin/src/commands/launch/mod.rs index 9217ed8b..f5c1f431 100644 --- a/bin/src/commands/launch/mod.rs +++ b/bin/src/commands/launch/mod.rs @@ -2,7 +2,6 @@ mod error; use std::path::{Path, PathBuf}; -use clap::{ArgAction, ArgMatches, Command}; use hemtt_common::{ arma::dlc::DLC, config::{LaunchOptions, ProjectConfig}, @@ -24,51 +23,39 @@ use crate::{ report::Report, }; -use super::dev; - -#[must_use] -pub fn cli() -> Command { - dev::add_args( - Command::new("launch") - .about("Test your project") - .long_about("Builds your project in dev mode and launches Arma 3 with file patching enabled, loading your mod and any workshop mods.") - .arg( - clap::Arg::new("config") - .action(ArgAction::Append) - .help("Launches with the specified `[hemtt.launch.]` configurations"), - ) - .arg( - clap::Arg::new("executable") - .short('e') - .help("Executable to launch, defaults to `arma3_x64.exe`"), - ) - .arg( - clap::Arg::new("passthrough") - .raw(true) - .help("Passthrough additional arguments to Arma 3"), - ) - .arg( - clap::Arg::new("instances") - .long("instances") - .short('i') - .help("Launches multiple instances of the game") - .action(ArgAction::Set), - ) - .arg( - clap::Arg::new("no-build") - .long("quick") - .short('Q') - .help("Skips the build step, launching the last built version") - .action(ArgAction::SetTrue), - ) - .arg( - clap::Arg::new("no-filepatching") - .long("no-filepatching") - .short('F') - .help("Disables file patching") - .action(ArgAction::SetTrue), - ) - ) +#[derive(clap::Parser)] +/// Test your project +pub struct Command { + #[clap(flatten)] + launch: Args, + + #[clap(flatten)] + dev: super::dev::Args, + + #[clap(flatten)] + just: super::JustArgs, +} + +#[derive(clap::Args)] +pub struct Args { + #[arg(action = clap::ArgAction::Append)] + /// Launches with the specified `[hemtt.launch.]` configurations + config: Option>, + #[arg(long, short)] + /// Executable to launch, defaults to `arma3_x64.exe` + executable: Option, + #[arg(raw = true)] + /// Passthrough additional arguments to Arma 3 + passthrough: Option>, + #[arg(long, short)] + /// Launches multiple instances of the game + instances: Option, + #[arg(long = "quick", short = 'Q')] + /// Skips the build step, launching the last built version + no_build: bool, + #[arg(long = "no-filepatching", short = 'F')] + /// Disables file patching + no_filepatching: bool, } #[allow(clippy::too_many_lines)] @@ -79,7 +66,7 @@ pub fn cli() -> Command { /// /// # Panics /// Will panic if the regex can not be compiled, which should never be the case in a released version -pub fn execute(matches: &ArgMatches) -> Result { +pub fn execute(cmd: &Command) -> Result { let config = ProjectConfig::from_file(&Path::new(".hemtt").join("project.toml"))?; let mut report = Report::new(); let Some(mainprefix) = config.mainprefix() else { @@ -87,21 +74,19 @@ pub fn execute(matches: &ArgMatches) -> Result { return Ok(report); }; - let launch_config: Vec<&String> = matches - .get_many::("config") - .unwrap_or_default() - .collect(); - let launch = if launch_config.is_empty() { + let configs = cmd.launch.config.clone().unwrap_or_default(); + + let launch = if configs.is_empty() { config .hemtt() .launch() .get("default") .cloned() .unwrap_or_default() - } else if let Some(launch) = launch_config + } else if let Some(launch) = configs .into_iter() .map(|c| { - config.hemtt().launch().get(c).cloned().map_or_else( + config.hemtt().launch().get(&c).cloned().map_or_else( || { report.push(LaunchConfigNotFound::code( c.to_string(), @@ -124,15 +109,7 @@ pub fn execute(matches: &ArgMatches) -> Result { trace!("launch config: {:?}", launch); - let instance_count = matches.get_one::("instances").map_or_else( - || launch.instances() as usize, - |instances| { - instances.parse::().unwrap_or_else(|_| { - error!("Invalid instance count: {}", instances); - std::process::exit(1); - }) - }, - ); + let instance_count = cmd.launch.instances.unwrap_or_else(|| launch.instances()); let Some(arma3dir) = steam::find_app(107_410) else { report.push(ArmaNotFound::code()); @@ -238,13 +215,7 @@ pub fn execute(matches: &ArgMatches) -> Result { .map(std::string::ToString::to_string) .collect(); args.append(&mut launch.parameters().to_vec()); - args.append( - &mut matches - .get_raw("passthrough") - .unwrap_or_default() - .map(|s| s.to_string_lossy().to_string()) - .collect::>(), - ); + args.append(&mut cmd.launch.passthrough.clone().unwrap_or_default()); args.push( mods.iter() .map(|s| format!("-mod=\"{s}\"")) @@ -284,7 +255,7 @@ pub fn execute(matches: &ArgMatches) -> Result { } } - if matches.get_flag("no-build") { + if cmd.launch.no_build { warn!("Using Quick Launch! HEMTT will not rebuild the project"); if !std::env::current_dir()?.join(".hemttout/dev").exists() { report.push(CanNotQuickLaunch::code( @@ -303,7 +274,8 @@ pub fn execute(matches: &ArgMatches) -> Result { } } else { let mut executor = super::dev::context( - matches, + &cmd.dev, + &cmd.just, launch.optionals(), launch.binarize(), launch.rapify(), @@ -342,7 +314,7 @@ pub fn execute(matches: &ArgMatches) -> Result { let mut args = args.clone(); if with_server { args.push("-connect=127.0.0.1".to_string()); - } else if launch.file_patching() && !matches.get_flag("no-filepatching") { + } else if launch.file_patching() && !cmd.launch.no_filepatching { args.push("-filePatching".to_string()); } instances.push(args); @@ -350,7 +322,7 @@ pub fn execute(matches: &ArgMatches) -> Result { if cfg!(target_os = "windows") { let mut path = arma3dir.clone(); - if let Some(exe) = matches.get_one::("executable") { + if let Some(exe) = &cmd.launch.executable { let exe = PathBuf::from(exe); if exe.is_absolute() { path = exe; diff --git a/bin/src/commands/localization/coverage.rs b/bin/src/commands/localization/coverage.rs index a4bef01c..4a89a861 100644 --- a/bin/src/commands/localization/coverage.rs +++ b/bin/src/commands/localization/coverage.rs @@ -1,13 +1,17 @@ use std::{collections::HashMap, io::BufReader}; use hemtt_stringtable::{Project, Totals}; -use term_table::{ - row::Row, - table_cell::{Alignment, TableCell}, - Table, TableStyle, -}; +use serde::Serialize; +use tabled::{settings::Style, Table, Tabled}; -use crate::{context::Context, report::Report, Error}; +use crate::{context::Context, report::Report, Error, TableFormat}; + +#[derive(clap::Args)] +pub struct Args { + #[arg(long, default_value = "ascii")] + /// Output format + format: TableFormat, +} macro_rules! missing { ($totals:ident, $missing:ident, $lang:tt, $addon:expr) => { @@ -19,6 +23,20 @@ macro_rules! missing { }; } +macro_rules! row { + ($table:ident, $global:ident, $missing:ident, $lang:ident) => { + paste::paste! { + if $global.$lang() != 0 { + $table.push(Entry { + language: first_capital(stringify!($lang)), + percent: Percentage(f64::from($global.$lang()) / f64::from($global.total()) * 100.0), + missing: MissingAddons($missing.get(stringify!($lang)).cloned().unwrap_or_default()) + }); + } + } + }; +} + fn first_capital(s: &str) -> String { let mut c = s.chars(); c.next().map_or_else(String::new, |f| { @@ -26,38 +44,48 @@ fn first_capital(s: &str) -> String { }) } -macro_rules! row { - ($table:ident, $global:ident, $missing:ident, $lang:tt) => { - if $global.$lang() != 0 { - $table.add_row(Row::new(vec![ - TableCell::new(first_capital(stringify!($lang))), - TableCell::new(format!( - "{:.2}%", - f64::from($global.$lang()) / f64::from($global.total()) * 100.0 - )), - TableCell::new($global.$lang()), - { - if let Some(missing) = $missing.get(stringify!($lang)) { - if missing.len() < 3 { - TableCell::new(missing.join(", ")) - } else { - TableCell::new(format!("{} and {} more", missing[0], missing.len() - 1)) - } - } else { - TableCell::new("") - } - }, - ])); +#[derive(Serialize)] +pub struct MissingAddons(Vec); + +impl std::fmt::Display for MissingAddons { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.0.is_empty() { + write!(f, "None") + } else if self.0.len() <= 3 { + write!(f, "{}", self.0.join(", ")) + } else { + write!(f, "{} and {} more", self.0[0], self.0.len() - 1) } - }; + } } -pub fn coverage() -> Result { +#[derive(Serialize)] +pub struct Percentage(f64); + +impl std::fmt::Display for Percentage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:.2}%", self.0) + } +} + +#[derive(Tabled, Serialize)] +struct Entry { + #[tabled(rename = "Language")] + language: String, + #[tabled(rename = "Coverage %")] + percent: Percentage, + #[tabled(rename = "Addons")] + missing: MissingAddons, +} + +pub fn coverage(args: &Args) -> Result { let ctx = Context::new(None, crate::context::PreservePrevious::Remove, true)?; let mut global = Totals::default(); let mut missing = HashMap::new(); + let mut table = Vec::new(); + for addon in ctx.addons() { let stringtable_path = ctx .workspace_path() @@ -104,23 +132,6 @@ pub fn coverage() -> Result { } } - let mut table = Table::new(); - table.style = TableStyle::thin(); - table.add_row(Row::new(vec![ - TableCell::builder("Language") - .alignment(Alignment::Center) - .build(), - TableCell::builder("Percent") - .alignment(Alignment::Center) - .build(), - TableCell::builder("Total") - .alignment(Alignment::Center) - .build(), - TableCell::builder("Missing Addons") - .alignment(Alignment::Center) - .build(), - ])); - row!(table, global, missing, original); row!(table, global, missing, english); row!(table, global, missing, czech); @@ -146,7 +157,20 @@ pub fn coverage() -> Result { row!(table, global, missing, finnish); row!(table, global, missing, dutch); - println!("{}", table.render()); + match args.format { + TableFormat::Ascii => { + println!("{}", Table::new(&table).with(Style::modern())); + } + TableFormat::Json => { + println!( + "{}", + serde_json::to_string_pretty(&table).expect("Failed to print json") + ); + } + TableFormat::Markdown => { + println!("{}", Table::new(&table).with(Style::markdown())); + } + } Ok(Report::new()) } diff --git a/bin/src/commands/localization/mod.rs b/bin/src/commands/localization/mod.rs index 09280cb3..de3e63c3 100644 --- a/bin/src/commands/localization/mod.rs +++ b/bin/src/commands/localization/mod.rs @@ -1,40 +1,31 @@ -use clap::{ArgAction, ArgMatches, Command}; - use crate::{report::Report, Error}; mod coverage; mod sort; -#[must_use] -pub fn cli() -> Command { - Command::new("localization") - .visible_alias("ln") - .about("Manage localization stringtables") - .subcommand(Command::new("coverage").about("Check the coverage of localization")) - .subcommand( - Command::new("sort").about("Sort the stringtables").arg( - clap::Arg::new("only-lang") - .long("only-lang") - .help("Sort only the languages") - .action(ArgAction::SetTrue), - ), - ) +#[derive(clap::Parser)] +#[command(arg_required_else_help = true)] +/// Manage localization stringtables +pub struct Command { + #[command(subcommand)] + commands: Subcommands, +} + +#[derive(clap::Subcommand)] +enum Subcommands { + /// Generate a coverage report + Coverage(coverage::Args), + /// Sort the stringtables + Sort(sort::Args), } /// Execute the localization command /// /// # Errors /// [`Error`] depending on the modules -/// -/// # Panics -/// If a name is not provided, but this is usually handled by clap -pub fn execute(matches: &ArgMatches) -> Result { - match matches.subcommand() { - Some(("coverage", _)) => coverage::coverage(), - Some(("sort", matches)) => sort::sort(matches), - _ => { - cli().print_help().expect("Failed to print help"); - Ok(Report::new()) - } +pub fn execute(cmd: &Command) -> Result { + match &cmd.commands { + Subcommands::Coverage(cmd) => coverage::coverage(cmd), + Subcommands::Sort(cmd) => sort::sort(cmd), } } diff --git a/bin/src/commands/localization/sort.rs b/bin/src/commands/localization/sort.rs index 5edfcb20..9a8a9b04 100644 --- a/bin/src/commands/localization/sort.rs +++ b/bin/src/commands/localization/sort.rs @@ -1,14 +1,18 @@ use std::{io::BufReader, path::PathBuf}; -use clap::ArgMatches; use hemtt_stringtable::Project; use crate::{context::Context, report::Report, Error}; -pub fn sort(matches: &ArgMatches) -> Result { - let ctx = Context::new(None, crate::context::PreservePrevious::Remove, true)?; +#[derive(clap::Args)] +pub struct Args { + #[arg(long)] + /// Only sort the languages within keys + only_lang: bool, +} - let only_lang = matches.get_flag("only-lang"); +pub fn sort(args: &Args) -> Result { + let ctx = Context::new(None, crate::context::PreservePrevious::Remove, true)?; for root in ["addons", "optionals"] { if !ctx.project_folder().join(root).exists() { @@ -33,7 +37,7 @@ pub fn sort(matches: &ArgMatches) -> Result { let mut file = std::fs::File::open(&path)?; match Project::from_reader(BufReader::new(&mut file)) { Ok(mut project) => { - if !only_lang { + if !args.only_lang { project.sort(); } let mut writer = String::new(); diff --git a/bin/src/commands/mod.rs b/bin/src/commands/mod.rs index d3734913..d6e2994b 100644 --- a/bin/src/commands/mod.rs +++ b/bin/src/commands/mod.rs @@ -22,3 +22,10 @@ pub fn global_modules(executor: &mut crate::executor::Executor) { executor.add_module(Box::::default()); executor.add_module(Box::::default()); } + +#[derive(clap::Args)] +pub struct JustArgs { + #[arg(long, action = clap::ArgAction::Append)] + /// Only build the given addon + pub(crate) just: Vec, +} diff --git a/bin/src/commands/new/mod.rs b/bin/src/commands/new/mod.rs index 907931a1..5ee80465 100644 --- a/bin/src/commands/new/mod.rs +++ b/bin/src/commands/new/mod.rs @@ -4,7 +4,6 @@ use std::{ path::Path, }; -use clap::{ArgMatches, Command}; use dialoguer::Input; use crate::{ @@ -18,13 +17,12 @@ use crate::{ mod error; -#[must_use] -pub fn cli() -> Command { - Command::new("new").about("Create a new project").arg( - clap::Arg::new("name") - .help("Name of the new mod") - .required(true), - ) +#[derive(clap::Parser)] +#[command(arg_required_else_help = true)] +/// Create a new project +pub struct Command { + #[clap(name = "name")] + name: String, } /// Execute the new command @@ -34,22 +32,19 @@ pub fn cli() -> Command { /// /// # Panics /// If a name is not provided, but this is usually handled by clap -pub fn execute(matches: &ArgMatches) -> Result { +pub fn execute(cmd: &Command, in_test: bool) -> Result { let mut report = Report::new(); - let test_mode = !cfg!(not(debug_assertions)) && matches.get_flag("in-test"); + let test_mode = !cfg!(not(debug_assertions)) && in_test; if !test_mode && !std::io::stdin().is_terminal() { report.push(TerminalNotInput::code()); return Ok(report); } - let name = matches - .get_one::("name") - .expect("name to be set as required"); - let path = Path::new(&name); + let path = Path::new(&cmd.name); if path.exists() { - report.push(FolderExists::code(name.to_string())); + report.push(FolderExists::code(cmd.name.to_string())); return Ok(report); } diff --git a/bin/src/commands/release.rs b/bin/src/commands/release.rs index 965ba85c..178ed8ce 100644 --- a/bin/src/commands/release.rs +++ b/bin/src/commands/release.rs @@ -1,49 +1,45 @@ -use clap::{ArgAction, ArgMatches, Command}; - use crate::{context::Context, error::Error, modules::Sign, report::Report}; use super::build; -#[must_use] -pub fn cli() -> Command { - build::add_args( - Command::new("release") - .about("Build the project for release") - .long_about("Build your project for full release, with signing and archiving."), - ) - .arg( - clap::Arg::new("no-sign") - .long("no-sign") - .help("Do not sign the PBOs") - .action(ArgAction::SetTrue), - ) - .arg( - clap::Arg::new("no-archive") - .long("no-archive") - .help("Do not create an archive of the release") - .action(ArgAction::SetTrue), - ) +#[derive(clap::Parser)] +#[command(long_about = "Build your project for full release, with signing and archiving.")] +/// Build the project for release +pub struct Command { + #[clap(flatten)] + build: build::Args, + + #[clap(flatten)] + release: Args, +} + +#[derive(clap::Args)] +pub struct Args { + #[arg(long, action = clap::ArgAction::SetTrue)] + /// Do not sign the PBOs + no_sign: bool, + #[arg(long, action = clap::ArgAction::SetTrue)] + /// Do not create an archive of the release + no_archive: bool, } /// Execute the release command /// /// # Errors /// [`Error`] depending on the modules -pub fn execute(matches: &ArgMatches) -> Result { +pub fn execute(cmd: &Command) -> Result { let ctx = Context::new( Some("release"), crate::context::PreservePrevious::Remove, true, )?; - let mut executor = build::executor(ctx, matches); + let mut executor = build::executor(ctx, &cmd.build); - if matches.get_one::("no-sign") != Some(&true) - && executor.ctx().config().hemtt().release().sign() - { + if !cmd.release.no_sign && executor.ctx().config().hemtt().release().sign() { executor.add_module(Box::new(Sign::new())); } - let archive = if matches.get_one::("no-archive") == Some(&true) { + let archive = if cmd.release.no_archive { false } else { executor.ctx().config().hemtt().release().archive() diff --git a/bin/src/commands/script.rs b/bin/src/commands/script.rs index 5be50380..116e2e30 100644 --- a/bin/src/commands/script.rs +++ b/bin/src/commands/script.rs @@ -1,17 +1,11 @@ -use clap::{ArgMatches, Command}; - use crate::{context::Context, error::Error, modules::Hooks, report::Report}; -#[must_use] -pub fn cli() -> Command { - Command::new("script") - .about("Run a Rhai script on the project") - .long_about("Run a Rhai script on the project, this is useful for automating tasks in a platform agnostic way, or requiring external dependencies.") - .arg( - clap::Arg::new("name") - .help("Name of the new mod") - .required(true), - ) +#[derive(clap::Parser)] +#[command(arg_required_else_help = true)] +/// Run a Rhai script on the project +pub struct Command { + #[clap(name = "name")] + name: String, } /// Execute the script command @@ -21,14 +15,11 @@ pub fn cli() -> Command { /// /// # Panics /// If a name is not provided, but this is usually handled by clap -pub fn execute(matches: &ArgMatches) -> Result { +pub fn execute(cmd: &Command) -> Result { let ctx = Context::new( Some("script"), crate::context::PreservePrevious::Remove, true, )?; - let name = matches - .get_one::("name") - .expect("name to be set as required"); - Hooks::run_file(&ctx, name).map(|(report, _)| report) + Hooks::run_file(&ctx, &cmd.name).map(|(report, _)| report) } diff --git a/bin/src/commands/utils.rs b/bin/src/commands/utils.rs index e6316342..77764164 100644 --- a/bin/src/commands/utils.rs +++ b/bin/src/commands/utils.rs @@ -1,34 +1,47 @@ -use clap::{ArgMatches, Command}; - use crate::{report::Report, utils, Error}; -#[must_use] -pub fn cli() -> Command { - Command::new("utils") - .about("Use HEMTT standalone utils") - .subcommand_required(false) - .arg_required_else_help(true) - .subcommand(utils::config::cli()) - .subcommand(utils::inspect::cli()) - .subcommand(utils::paa::cli()) - .subcommand(utils::pbo::cli()) - .subcommand(utils::sqf::cli()) - .subcommand(utils::verify::cli()) +#[derive(clap::Parser)] +#[command(arg_required_else_help = true)] +/// Use HEMTT standalone utils +pub struct Command { + #[command(subcommand)] + commands: Subcommands, +} + +#[derive(clap::Subcommand)] +enum Subcommands { + Inspect(utils::inspect::Command), + Config(utils::config::Command), + Paa(utils::paa::Command), + Pbo(utils::pbo::Command), + Sqf(utils::sqf::Command), + Verify(utils::verify::Command), } /// Execute the utils command /// /// # Errors /// [`Error`] depending on the modules -pub fn execute(matches: &ArgMatches) -> Result { - match matches.subcommand() { - Some(("config", matches)) => utils::config::execute(matches), - Some(("inspect", matches)) => utils::inspect::execute(matches), - Some(("paa", matches)) => utils::paa::execute(matches), - Some(("pbo", matches)) => utils::pbo::execute(matches), - Some(("sqf", matches)) => utils::sqf::execute(matches), - Some(("verify", matches)) => utils::verify::execute(matches), - _ => unreachable!(), - }?; +pub fn execute(cmd: &Command) -> Result { + match &cmd.commands { + Subcommands::Inspect(cmd) => { + utils::inspect::execute(cmd)?; + } + Subcommands::Config(cmd) => { + utils::config::execute(cmd)?; + } + Subcommands::Paa(cmd) => { + utils::paa::execute(cmd)?; + } + Subcommands::Pbo(cmd) => { + utils::pbo::execute(cmd)?; + } + Subcommands::Sqf(cmd) => { + utils::sqf::execute(cmd)?; + } + Subcommands::Verify(cmd) => { + utils::verify::execute(cmd)?; + } + } Ok(Report::new()) } diff --git a/bin/src/commands/value.rs b/bin/src/commands/value.rs index 4791ac6f..42e28e4f 100644 --- a/bin/src/commands/value.rs +++ b/bin/src/commands/value.rs @@ -1,18 +1,13 @@ -use clap::{ArgMatches, Command}; use hemtt_common::version::Version; use crate::{context::Context, error::Error, report::Report}; -#[must_use] -pub fn cli() -> Command { - Command::new("value") - .about("Print a value from the project") - .long_about("Print a value from the project, use `list` to see all available values") - .arg( - clap::Arg::new("name") - .help("Name of the new value") - .required(true), - ) +#[derive(clap::Parser)] +#[command(arg_required_else_help = true)] +/// Print a value from the project +pub struct Command { + #[clap(name = "name")] + name: String, } #[allow(clippy::too_many_lines)] @@ -20,17 +15,10 @@ pub fn cli() -> Command { /// /// # Errors /// [`Error`] depending on the modules -/// -/// # Panics -/// If a name is not provided, but this is usually handled by clap -pub fn execute(matches: &ArgMatches) -> Result { +pub fn execute(cmd: &Command) -> Result { let default = String::new(); let ctx = Context::new(None, crate::context::PreservePrevious::Remove, false)?; - match matches - .get_one::("name") - .unwrap_or(&default) - .as_str() - { + match cmd.name.as_str() { "project.name" => { println!("{}", ctx.config().name()); } diff --git a/bin/src/commands/wiki.rs b/bin/src/commands/wiki.rs index 4b2c5616..f7ad52f4 100644 --- a/bin/src/commands/wiki.rs +++ b/bin/src/commands/wiki.rs @@ -1,16 +1,19 @@ -use clap::{ArgMatches, Command}; use hemtt_sqf::parser::database::Database; use crate::{error::Error, report::Report}; -#[must_use] -pub fn cli() -> Command { - Command::new("wiki") - .about("Manage the Arma 3 wiki") - .subcommand( - Command::new("force-pull") - .about("Force pull the wiki, if updates you need have been pushed very recently"), - ) +#[derive(clap::Parser)] +#[command(arg_required_else_help = true)] +/// Manage the Arma 3 wiki +pub struct Command { + #[command(subcommand)] + commands: Subcommands, +} + +#[derive(clap::Subcommand)] +enum Subcommands { + /// Force pull the wiki, regardless of the last pull time + ForcePull, } /// Execute the wiki command @@ -20,7 +23,7 @@ pub fn cli() -> Command { /// /// # Panics /// If a name is not provided, but this is usually handled by clap -pub fn execute(_matches: &ArgMatches) -> Result { +pub fn execute(_cmd: &Command) -> Result { // TODO right now just assumes force-pull since that's the only subcommand let _ = Database::empty(true); Ok(Report::new()) diff --git a/bin/src/lib.rs b/bin/src/lib.rs index 2065771a..56e2af10 100644 --- a/bin/src/lib.rs +++ b/bin/src/lib.rs @@ -1,4 +1,4 @@ -use clap::{ArgAction, ArgMatches, Command}; +use clap::CommandFactory; pub use error::Error; #[macro_use] @@ -16,61 +16,45 @@ pub mod report; pub mod update; pub mod utils; -#[must_use] -pub fn cli() -> Command { - #[allow(unused_mut)] - let mut global = Command::new(env!("CARGO_PKG_NAME")) - .about(env!("CARGO_PKG_DESCRIPTION")) - .version(env!("HEMTT_VERSION")) - .subcommand_required(false) - .arg_required_else_help(true) - .subcommand(commands::book::cli()) - .subcommand(commands::new::cli()) - .subcommand(commands::check::cli()) - .subcommand(commands::localization::cli()) - .subcommand(commands::dev::cli()) - .subcommand(commands::launch::cli()) - .subcommand(commands::build::cli()) - .subcommand(commands::release::cli()) - .subcommand(commands::script::cli()) - .subcommand(commands::utils::cli()) - .subcommand(commands::value::cli()) - .subcommand(commands::wiki::cli()) - .arg( - clap::Arg::new("threads") - .global(true) - .help("Number of threads, defaults to # of CPUs") - .action(ArgAction::Set) - .long("threads") - .short('t'), - ) - .arg( - clap::Arg::new("verbosity") - .global(true) - .help("Verbosity level") - .action(ArgAction::Count) - .short('v'), - ); +#[derive(clap::Parser)] +#[command(arg_required_else_help = true)] +#[command(version = env!("HEMTT_VERSION"), about, long_about = None)] +#[command(propagate_version = true)] +pub struct Cli { + #[command(subcommand)] + command: Option, + + #[arg(long, short)] + /// Number of threads, defaults to # of CPUs + threads: Option, + #[arg(short, action = clap::ArgAction::Count)] + /// Verbosity level + verbosity: u8, #[cfg(debug_assertions)] - { - global = global - .arg( - clap::Arg::new("in-test") - .hide(true) - .global(true) - .help("we are in a test") - .action(ArgAction::SetTrue) - .long("in-test"), - ) - .arg( - clap::Arg::new("dir") - .global(true) - .help("directory to run in") - .action(ArgAction::Set) - .long("dir"), - ); - } - global + #[arg(long, short)] + /// Directory to run in + dir: Option, + #[cfg(debug_assertions)] + #[arg(long, short, hide = true)] + /// we are in a test + in_test: bool, +} + +#[derive(clap::Subcommand)] +enum Commands { + Book(commands::book::Command), + New(commands::new::Command), + Check(commands::check::Command), + Dev(commands::dev::Command), + Launch(commands::launch::Command), + Build(commands::build::Command), + Release(commands::release::Command), + #[clap(alias = "ln")] + Localization(commands::localization::Command), + Script(commands::script::Command), + Utils(commands::utils::Command), + Value(commands::value::Command), + Wiki(commands::wiki::Command), } /// Run the HEMTT CLI @@ -80,20 +64,33 @@ pub fn cli() -> Command { /// /// # Panics /// If the number passed to `--threads` is not a valid number -pub fn execute(matches: &ArgMatches) -> Result<(), Error> { +pub fn execute(cli: &Cli) -> Result<(), Error> { // check for -v with no command and show version - if matches.subcommand().is_none() && matches.get_count("verbosity") > 0 { - println!("{} {}", env!("CARGO_PKG_NAME"), env!("HEMTT_VERSION")); - return Ok(()); + if cli.command.is_none() { + if cli.verbosity > 0 { + println!("{} {}", env!("CARGO_PKG_NAME"), env!("HEMTT_VERSION")); + return Ok(()); + } + Cli::command() + .print_long_help() + .expect("Failed to print help"); + std::process::exit(1); } - if cfg!(not(debug_assertions)) || !matches.get_flag("in-test") { + if !matches!(cli.command, Some(Commands::Value(_))) { logging::init( - matches.get_count("verbosity"), - matches.subcommand_name() != Some("utils"), + cli.verbosity, + !matches!(cli.command, Some(Commands::Utils(_))), ); } - if let Some(dir) = matches.get_one::("dir") { + + #[cfg(debug_assertions)] + let in_test = cli.in_test; + #[cfg(not(debug_assertions))] + let in_test = false; + + #[cfg(debug_assertions)] + if let Some(dir) = &cli.dir { std::env::set_current_dir(dir).expect("Failed to set current directory"); } @@ -122,11 +119,7 @@ pub fn execute(matches: &ArgMatches) -> Result<(), Error> { trace!("args: {:#?}", std::env::args().collect::>()); - if let Some(threads) = matches.get_one::("threads") { - let Ok(threads) = threads.parse::() else { - error!("Invalid thread count: {threads}"); - std::process::exit(1); - }; + if let Some(threads) = cli.threads { debug!("Using custom thread count: {threads}"); if let Err(e) = rayon::ThreadPoolBuilder::new() .num_threads(threads) @@ -135,48 +128,37 @@ pub fn execute(matches: &ArgMatches) -> Result<(), Error> { error!("Failed to initialize thread pool: {e}"); } } - let report = match matches.subcommand() { - Some(("book", matches)) => commands::book::execute(matches).map(Some), - Some(("new", matches)) => commands::new::execute(matches).map(Some), - Some(("dev", matches)) => commands::dev::execute(matches, &[]).map(Some), - Some(("check", _matches)) => commands::check::execute().map(Some), - Some(("localization", matches)) => commands::localization::execute(matches) - .map_err(std::convert::Into::into) - .map(Some), - Some(("build", matches)) => commands::build::execute(matches) - .map_err(std::convert::Into::into) - .map(Some), - Some(("release", matches)) => commands::release::execute(matches) - .map_err(std::convert::Into::into) - .map(Some), - Some(("launch", matches)) => commands::launch::execute(matches) - .map_err(std::convert::Into::into) - .map(Some), - Some(("script", matches)) => commands::script::execute(matches) - .map_err(std::convert::Into::into) - .map(Some), - Some(("utils", matches)) => commands::utils::execute(matches) - .map_err(std::convert::Into::into) - .map(Some), - Some(("value", matches)) => commands::value::execute(matches) - .map_err(std::convert::Into::into) - .map(Some), - Some(("wiki", matches)) => commands::wiki::execute(matches) - .map_err(std::convert::Into::into) - .map(Some), - _ => { - cli().print_help().expect("Failed to print help"); - Ok(None) - } + + let report = match cli.command.as_ref().expect("Handled above") { + Commands::Book(ref cmd) => commands::book::execute(cmd), + Commands::New(ref cmd) => commands::new::execute(cmd, in_test), + Commands::Check(ref cmd) => commands::check::execute(cmd), + Commands::Dev(ref cmd) => commands::dev::execute(cmd, &[]), + Commands::Launch(ref cmd) => commands::launch::execute(cmd), + Commands::Build(ref cmd) => commands::build::execute(cmd), + Commands::Release(ref cmd) => commands::release::execute(cmd), + Commands::Localization(ref cmd) => commands::localization::execute(cmd), + Commands::Script(ref cmd) => commands::script::execute(cmd), + Commands::Utils(ref cmd) => commands::utils::execute(cmd), + Commands::Value(ref cmd) => commands::value::execute(cmd), + Commands::Wiki(ref cmd) => commands::wiki::execute(cmd), }; - if let Some(report) = report? { - report.write_to_stdout(); - if !matches.subcommand_name().is_some_and(|s| { - s == "new" || s == "utils" || s == "wiki" || s == "book" || s == "localization" - }) { - report.write_ci_annotations()?; + + match report { + Ok(report) => { + report.write_to_stdout(); + if !matches!( + cli.command, + Some(Commands::New(_) | Commands::Utils(_) | Commands::Wiki(_)) + ) { + report.write_ci_annotations()?; + } + if report.failed() { + std::process::exit(1); + } } - if report.failed() { + Err(e) => { + error!("Failed to execute command:\n{e}"); std::process::exit(1); } } @@ -222,3 +204,15 @@ pub fn is_ci() -> bool { } false } + +#[derive(clap::ValueEnum, Clone, Default, Debug, serde::Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum TableFormat { + /// ascii table + #[default] + Ascii, + /// json + Json, + /// markdown table + Markdown, +} diff --git a/bin/src/main.rs b/bin/src/main.rs index b29b937f..60561a5e 100644 --- a/bin/src/main.rs +++ b/bin/src/main.rs @@ -1,3 +1,4 @@ +use clap::Parser; use hemtt_workspace::reporting::WorkspaceFiles; use tracing::error; @@ -27,7 +28,15 @@ It is always best to the include the log and a link to your project when reporti tracing::warn!("Failed to enable ANSI support, colored output will not work"); } - if let Err(e) = hemtt::execute(&hemtt::cli().get_matches()) { + let cli = match hemtt::Cli::try_parse() { + Ok(cli) => cli, + Err(e) => { + eprintln!("{e}"); + std::process::exit(1); + } + }; + + if let Err(e) = hemtt::execute(&cli) { if let hemtt::Error::Preprocessor(e) = &e { if let Some(code) = e.get_code() { if let Some(diag) = code.diagnostic() { diff --git a/bin/src/utils/config/inspect.rs b/bin/src/utils/config/inspect.rs index 83f537cd..a405c5e3 100644 --- a/bin/src/utils/config/inspect.rs +++ b/bin/src/utils/config/inspect.rs @@ -8,6 +8,12 @@ use hemtt_workspace::{ LayerType, }; +#[derive(clap::Args)] +pub struct Args { + /// Config to inspect + pub(crate) config: String, +} + use crate::Error; /// Prints information about a config to stdout diff --git a/bin/src/utils/config/mod.rs b/bin/src/utils/config/mod.rs index a77070b2..0c5997d9 100644 --- a/bin/src/utils/config/mod.rs +++ b/bin/src/utils/config/mod.rs @@ -1,25 +1,23 @@ use std::path::PathBuf; -use clap::{ArgMatches, Command}; - use crate::Error; mod inspect; pub use inspect::inspect; -#[must_use] -pub fn cli() -> Command { - Command::new("paa") - .about("Commands for PAA files") - .arg_required_else_help(true) - .subcommand( - Command::new("inspect").about("Inspect a config file").arg( - clap::Arg::new("config") - .help("Config to inspect") - .required(true), - ), - ) +#[derive(clap::Parser)] +#[command(arg_required_else_help = true)] +/// Commands for config files +pub struct Command { + #[command(subcommand)] + commands: Subcommands, +} + +#[derive(clap::Subcommand)] +enum Subcommands { + /// Inspect a config file + Inspect(inspect::Args), } /// Execute the config command @@ -29,12 +27,8 @@ pub fn cli() -> Command { /// /// # Panics /// If the args are not present from clap -pub fn execute(matches: &ArgMatches) -> Result<(), Error> { - match matches.subcommand() { - Some(("inspect", matches)) => inspect::inspect(&PathBuf::from( - matches.get_one::("config").expect("required"), - )), - - _ => unreachable!(), +pub fn execute(cmd: &Command) -> Result<(), Error> { + match &cmd.commands { + Subcommands::Inspect(args) => inspect::inspect(&PathBuf::from(&args.config)), } } diff --git a/bin/src/utils/inspect.rs b/bin/src/utils/inspect.rs index b398b4f2..b98c9a19 100644 --- a/bin/src/utils/inspect.rs +++ b/bin/src/utils/inspect.rs @@ -4,21 +4,16 @@ use std::{ path::PathBuf, }; -use clap::{ArgMatches, Command}; use hemtt_signing::{BIPublicKey, BISign}; use crate::Error; -#[must_use] -pub fn cli() -> Command { - Command::new("inspect") - .about("Inspect an Arma file") - .long_about("Provides information about supported files. Supported: pbo, bikey, bisign") - .arg( - clap::Arg::new("file") - .help("File to inspect") - .required(true), - ) +#[derive(clap::Parser)] +#[command(arg_required_else_help = true)] +/// Inspect an Arma file +pub struct Command { + /// File to inspect + pub(crate) file: String, } /// Execute the inspect command @@ -28,8 +23,8 @@ pub fn cli() -> Command { /// /// # Panics /// If the args are not present from clap -pub fn execute(matches: &ArgMatches) -> Result<(), Error> { - let path = PathBuf::from(matches.get_one::("file").expect("required")); +pub fn execute(cmd: &Command) -> Result<(), Error> { + let path = PathBuf::from(&cmd.file); match path .extension() .unwrap_or_default() @@ -37,10 +32,10 @@ pub fn execute(matches: &ArgMatches) -> Result<(), Error> { .unwrap_or_default() { "paa" => { - super::paa::inspect(File::open(&path)?)?; + super::paa::inspect(File::open(&path)?, &crate::TableFormat::Ascii)?; } "pbo" => { - super::pbo::inspect(File::open(&path)?)?; + super::pbo::inspect(File::open(&path)?, &crate::TableFormat::Ascii)?; } "bikey" => { bikey(File::open(&path)?, &path)?; @@ -59,14 +54,14 @@ pub fn execute(matches: &ArgMatches) -> Result<(), Error> { // PBO if buf == b"\x00sreV\x00" { warn!("The file appears to be a PBO but does not have the .pbo extension."); - super::pbo::inspect(file)?; + super::pbo::inspect(file, &crate::TableFormat::Ascii)?; return Ok(()); } // PAA (skip first two bytes) if &buf[2..] == b"GGAT" { warn!("The file appears to be a PAA but does not have the .paa extension."); file.seek(std::io::SeekFrom::Start(0))?; - super::paa::inspect(file)?; + super::paa::inspect(file, &crate::TableFormat::Ascii)?; return Ok(()); } // BiSign diff --git a/bin/src/utils/paa/convert.rs b/bin/src/utils/paa/convert.rs index b3b378e6..1aa9ddb8 100644 --- a/bin/src/utils/paa/convert.rs +++ b/bin/src/utils/paa/convert.rs @@ -1,28 +1,22 @@ use std::path::PathBuf; -use clap::{ArgMatches, Command}; - use crate::Error; -#[must_use] -pub fn cli() -> Command { - Command::new("convert") - .about("Convert a PAA to another format") - .arg(clap::Arg::new("paa").help("PAA to convert").required(true)) - .arg( - clap::Arg::new("output") - .help("Where to save the file") - .required(true), - ) +#[derive(clap::Args)] +pub struct Args { + /// PAA to convert + paa: String, + /// Where to save the file + output: String, } /// Execute the convert command /// /// # Errors /// [`Error`] depending on the modules -pub fn execute(matches: &ArgMatches) -> Result<(), Error> { - let paa = PathBuf::from(matches.get_one::("paa").expect("required")); - let output = PathBuf::from(matches.get_one::("output").expect("required")); +pub fn execute(args: &Args) -> Result<(), Error> { + let paa = PathBuf::from(&args.paa); + let output = PathBuf::from(&args.output); if output.exists() { error!("Output file already exists"); return Ok(()); diff --git a/bin/src/utils/paa/inspect.rs b/bin/src/utils/paa/inspect.rs index fd4e97b2..b9081a9e 100644 --- a/bin/src/utils/paa/inspect.rs +++ b/bin/src/utils/paa/inspect.rs @@ -1,63 +1,72 @@ use std::fs::File; -use term_table::{ - row::Row, - table_cell::{Alignment, TableCell}, - Table, TableStyle, +use serde::Serialize; +use tabled::{ + settings::{Alignment, Style}, + Table, Tabled, }; -use crate::Error; +use crate::{Error, TableFormat}; + +#[derive(clap::Args)] +pub struct Args { + /// PAA to inspect + pub(crate) paa: String, + #[clap(long, default_value = "ascii")] + /// Output format + pub(crate) format: TableFormat, +} + +#[derive(Tabled, Serialize)] +pub struct MipMapInfo { + #[tabled(rename = "Width")] + width: u16, + #[tabled(rename = "Height")] + height: u16, + #[tabled(rename = "Size")] + size: usize, + #[tabled(rename = "Format")] + format: String, + #[tabled(rename = "Compressed")] + compressed: bool, +} /// Prints information about a PAA to stdout /// /// # Errors /// [`Error::Io`] if the file is not a valid [`hemtt_paa::Paa`] -pub fn inspect(mut file: File) -> Result<(), Error> { +pub fn inspect(mut file: File, format: &TableFormat) -> Result<(), Error> { let paa = hemtt_paa::Paa::read(&mut file)?; println!("PAA"); println!(" - Format: {}", paa.format()); let maps = paa.maps(); println!("Maps: {}", maps.len()); - let mut table = Table::new(); - table.style = TableStyle::thin(); - table.add_row(Row::new(vec![ - TableCell::builder("Width") - .alignment(Alignment::Center) - .build(), - TableCell::builder("Height") - .alignment(Alignment::Center) - .build(), - TableCell::builder("Size") - .alignment(Alignment::Center) - .build(), - TableCell::builder("Format") - .alignment(Alignment::Center) - .build(), - TableCell::builder("Compressed") - .alignment(Alignment::Center) - .build(), - ])); - for map in maps { - let mut row = Row::new(vec![ - TableCell::builder(map.width().to_string()) - .alignment(Alignment::Right) - .build(), - TableCell::builder(map.height().to_string()) - .alignment(Alignment::Right) - .build(), - TableCell::builder(map.data().len().to_string()) - .alignment(Alignment::Right) - .build(), - TableCell::builder(format!("{:?}", map.format())) - .alignment(Alignment::Right) - .build(), - TableCell::builder(map.is_compressed().to_string()) - .alignment(Alignment::Right) - .build(), - ]); - row.has_separator = table.rows.len() == 1; - table.add_row(row); + let data = maps + .iter() + .map(|map| MipMapInfo { + width: map.width(), + height: map.height(), + size: map.data().len(), + format: format!("{:?}", map.format()), + compressed: map.is_compressed(), + }) + .collect::>(); + + match format { + TableFormat::Ascii => println!( + "{}", + Table::new(data) + .with(Style::modern()) + .with(Alignment::right()) + ), + TableFormat::Json => println!("{}", serde_json::to_string_pretty(&data)?), + TableFormat::Markdown => println!( + "{}", + Table::new(data) + .with(Style::markdown()) + .with(Alignment::right()) + ), } - println!("{}", table.render()); + Ok(()) } diff --git a/bin/src/utils/paa/mod.rs b/bin/src/utils/paa/mod.rs index 1b12b36b..79a71971 100644 --- a/bin/src/utils/paa/mod.rs +++ b/bin/src/utils/paa/mod.rs @@ -1,7 +1,5 @@ use std::{fs::File, path::PathBuf}; -use clap::{ArgMatches, Command}; - use crate::Error; mod convert; @@ -9,17 +7,18 @@ mod inspect; pub use inspect::inspect; -#[must_use] -pub fn cli() -> Command { - Command::new("paa") - .about("Commands for PAA files") - .arg_required_else_help(true) - .subcommand(convert::cli()) - .subcommand( - Command::new("inspect") - .about("Inspect a PAA") - .arg(clap::Arg::new("paa").help("PAA to inspect").required(true)), - ) +#[derive(clap::Parser)] +#[command(arg_required_else_help = true)] +/// Commands for PAA files +pub struct Command { + #[command(subcommand)] + commands: Subcommands, +} + +#[derive(clap::Subcommand)] +enum Subcommands { + Convert(convert::Args), + Inspect(inspect::Args), } /// Execute the paa command @@ -29,14 +28,11 @@ pub fn cli() -> Command { /// /// # Panics /// If the args are not present from clap -pub fn execute(matches: &ArgMatches) -> Result<(), Error> { - match matches.subcommand() { - Some(("convert", matches)) => convert::execute(matches), - - Some(("inspect", matches)) => inspect::inspect(File::open(PathBuf::from( - matches.get_one::("paa").expect("required"), - ))?), - - _ => unreachable!(), +pub fn execute(cmd: &Command) -> Result<(), Error> { + match &cmd.commands { + Subcommands::Convert(args) => convert::execute(args), + Subcommands::Inspect(args) => { + inspect::inspect(File::open(PathBuf::from(&args.paa))?, &args.format) + } } } diff --git a/bin/src/utils/pbo/extract.rs b/bin/src/utils/pbo/extract.rs index 25685c6a..fb80ce78 100644 --- a/bin/src/utils/pbo/extract.rs +++ b/bin/src/utils/pbo/extract.rs @@ -1,40 +1,32 @@ use std::{fs::File, path::PathBuf}; -use clap::{ArgMatches, Command}; use hemtt_pbo::ReadablePbo; use crate::Error; -#[must_use] -pub fn cli() -> Command { - Command::new("extract") - .about("Extract a file from a PBO") - .arg( - clap::Arg::new("pbo") - .help("PBO file to extract from") - .required(true), - ) - .arg( - clap::Arg::new("file") - .help("File to extract") - .required(true), - ) - .arg(clap::Arg::new("output").help("Where to save the extracted file")) +#[derive(clap::Args)] +/// Arguments for the extract command +pub struct Args { + /// PBO file to extract from + pbo: String, + /// File to extract + file: String, + /// Where to save the extracted file + output: Option, } /// Execute the extract command /// /// # Errors /// [`Error`] depending on the modules -pub fn execute(matches: &ArgMatches) -> Result<(), Error> { - let path = PathBuf::from(matches.get_one::("pbo").expect("required")); +pub fn execute(args: &Args) -> Result<(), Error> { + let path = PathBuf::from(&args.pbo); let mut pbo = ReadablePbo::from(File::open(path)?)?; - let file = matches.get_one::("file").expect("required"); - let Some(mut file) = pbo.file(file)? else { - error!("File `{file}` not found in PBO"); + let Some(mut file) = pbo.file(&args.file)? else { + error!("File `{}` not found in PBO", args.file); return Ok(()); }; - let output = matches.get_one::("output").map(PathBuf::from); + let output = args.output.as_ref().map(PathBuf::from); if let Some(output) = output { if output.exists() { error!("Output file already exists"); diff --git a/bin/src/utils/pbo/inspect.rs b/bin/src/utils/pbo/inspect.rs index 81701321..5c073ce6 100644 --- a/bin/src/utils/pbo/inspect.rs +++ b/bin/src/utils/pbo/inspect.rs @@ -1,13 +1,31 @@ use std::fs::File; use hemtt_pbo::ReadablePbo; -use term_table::{ - row::Row, - table_cell::{Alignment, TableCell}, - Table, TableStyle, +use serde::Serialize; +use tabled::{ + settings::{object::Columns, Alignment, Style}, + Table, Tabled, }; -use crate::Error; +use crate::{Error, TableFormat}; + +#[derive(clap::Args)] +pub struct Args { + /// PBO to inspect + pub(crate) pbo: String, + #[clap(long, default_value = "ascii")] + /// Output format + pub(crate) format: TableFormat, +} + +#[derive(Tabled, Serialize)] +pub struct FileInfo { + filename: String, + mime: String, + size: u32, + original: u32, + timestamp: u32, +} /// Prints information about a [`ReadablePbo`] to stdout /// @@ -16,7 +34,7 @@ use crate::Error; /// /// # Panics /// If the file is not a valid [`ReadablePbo`] -pub fn inspect(file: File) -> Result<(), Error> { +pub fn inspect(file: File, format: &TableFormat) -> Result<(), Error> { let mut pbo = ReadablePbo::from(file)?; println!("Properties"); for (key, value) in pbo.properties() { @@ -36,45 +54,23 @@ pub fn inspect(file: File) -> Result<(), Error> { println!(" - Sorted: false !!!"); } println!(" - Count: {}", files.len()); - let mut table = Table::new(); - table.style = TableStyle::thin(); - table.add_row(Row::new(vec![ - TableCell::builder("Filename") - .alignment(Alignment::Center) - .build(), - TableCell::builder("Method") - .alignment(Alignment::Center) - .build(), - TableCell::builder("Size") - .alignment(Alignment::Center) - .build(), - TableCell::builder("Original") - .alignment(Alignment::Center) - .build(), - TableCell::builder("Timestamp") - .alignment(Alignment::Center) - .build(), - ])); - for file in files { - let mut row = Row::new(vec![ - TableCell::new(file.filename()), - TableCell::builder(file.mime().to_string()) - .alignment(Alignment::Right) - .build(), - TableCell::builder(file.size().to_string()) - .alignment(Alignment::Right) - .build(), - TableCell::builder(file.original().to_string()) - .alignment(Alignment::Right) - .build(), - TableCell::builder(file.timestamp().to_string()) - .alignment(Alignment::Right) - .build(), - ]); - row.has_separator = table.rows.len() == 1; - table.add_row(row); + let data = files + .iter() + .map(|file| FileInfo { + filename: file.filename().to_string(), + mime: file.mime().to_string(), + size: file.size(), + original: file.original(), + timestamp: file.timestamp(), + }) + .collect::>(); + + match format { + TableFormat::Ascii => println!("{}", modify(Table::new(data).with(Style::modern()))), + TableFormat::Json => println!("{}", serde_json::to_string_pretty(&data)?), + TableFormat::Markdown => println!("{}", modify(Table::new(data).with(Style::markdown()))), } - println!("{}", table.render()); + if pbo.is_sorted().is_err() { warn!("The PBO is not sorted, signatures may be invalid"); } @@ -83,3 +79,7 @@ pub fn inspect(file: File) -> Result<(), Error> { } Ok(()) } + +fn modify(table: &mut Table) -> &mut Table { + table.modify(Columns::new(1..), Alignment::right()) +} diff --git a/bin/src/utils/pbo/mod.rs b/bin/src/utils/pbo/mod.rs index 72ccd5fb..a3b117b5 100644 --- a/bin/src/utils/pbo/mod.rs +++ b/bin/src/utils/pbo/mod.rs @@ -1,7 +1,5 @@ use std::{fs::File, path::PathBuf}; -use clap::{ArgMatches, Command}; - use crate::Error; mod extract; @@ -10,18 +8,22 @@ mod unpack; pub use inspect::inspect; -#[must_use] -pub fn cli() -> Command { - Command::new("pbo") - .about("Commands for PBO files") - .arg_required_else_help(true) - .subcommand(extract::cli()) - .subcommand(unpack::cli()) - .subcommand( - Command::new("inspect") - .about("Inspect a PBO") - .arg(clap::Arg::new("pbo").help("PBO to inspect").required(true)), - ) +#[derive(clap::Parser)] +#[command(arg_required_else_help = true)] +/// Commands for PBO files +pub struct Command { + #[command(subcommand)] + commands: Subcommands, +} + +#[derive(clap::Subcommand)] +enum Subcommands { + /// Extract a file from a PBO + Extract(extract::Args), + /// Inspect a PBO file + Inspect(inspect::Args), + /// Unpack a PBO file + Unpack(unpack::Args), } /// Execute the pbo command @@ -31,15 +33,12 @@ pub fn cli() -> Command { /// /// # Panics /// If the args are not present from clap -pub fn execute(matches: &ArgMatches) -> Result<(), Error> { - match matches.subcommand() { - Some(("extract", matches)) => extract::execute(matches), - Some(("unpack", matches)) => unpack::execute(matches), - - Some(("inspect", matches)) => inspect::inspect(File::open(PathBuf::from( - matches.get_one::("pbo").expect("required"), - ))?), - - _ => unreachable!(), +pub fn execute(cmd: &Command) -> Result<(), Error> { + match &cmd.commands { + Subcommands::Extract(args) => extract::execute(args), + Subcommands::Inspect(args) => { + inspect::inspect(File::open(PathBuf::from(&args.pbo))?, &args.format) + } + Subcommands::Unpack(args) => unpack::execute(args), } } diff --git a/bin/src/utils/pbo/unpack.rs b/bin/src/utils/pbo/unpack.rs index 0d7ee3cf..49e63a57 100644 --- a/bin/src/utils/pbo/unpack.rs +++ b/bin/src/utils/pbo/unpack.rs @@ -4,35 +4,26 @@ use std::{ path::PathBuf, }; -use clap::{ArgMatches, Command}; use hemtt_pbo::ReadablePbo; use crate::Error; -#[must_use] -pub fn cli() -> Command { - Command::new("unpack") - .about("Unpack a PBO") - .arg( - clap::Arg::new("pbo") - .help("PBO file to unpack") - .required(true), - ) - .arg( - clap::Arg::new("output") - .help("Directory to unpack to") - .required(true), - ) +#[derive(clap::Args)] +pub struct Args { + /// PBO file to unpack + pbo: String, + /// Directory to unpack to + output: String, } /// Execute the unpack command /// /// # Errors /// [`Error`] depending on the modules -pub fn execute(matches: &ArgMatches) -> Result<(), Error> { - let path = PathBuf::from(matches.get_one::("pbo").expect("required")); +pub fn execute(args: &Args) -> Result<(), Error> { + let path = PathBuf::from(&args.pbo); let mut pbo = ReadablePbo::from(File::open(path)?)?; - let output = PathBuf::from(matches.get_one::("output").expect("required")); + let output = PathBuf::from(&args.output); if output.exists() { error!("Output directory already exists"); return Ok(()); diff --git a/bin/src/utils/sqf/case.rs b/bin/src/utils/sqf/case.rs index 9c185aca..c73db329 100644 --- a/bin/src/utils/sqf/case.rs +++ b/bin/src/utils/sqf/case.rs @@ -3,28 +3,21 @@ use std::{ sync::{atomic::AtomicUsize, Arc}, }; -use clap::{ArgMatches, Command}; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use crate::Error; -#[must_use] -pub fn cli() -> Command { - Command::new("case") - .about("Fix the case of commands in your SQF files") - .arg( - clap::Arg::new("path") - .help("Path to the SQF file or a folder to recursively fix") - .required(true), - ) +#[derive(clap::Args)] +pub struct Args { + path: String, } /// Execute the convert command /// /// # Errors /// [`Error`] depending on the modules -pub fn execute(matches: &ArgMatches) -> Result<(), Error> { - let path = PathBuf::from(matches.get_one::("path").expect("required")); +pub fn execute(args: &Args) -> Result<(), Error> { + let path = PathBuf::from(&args.path); if path.is_dir() { let count = Arc::new(AtomicUsize::new(0)); let entries = walkdir::WalkDir::new(&path) diff --git a/bin/src/utils/sqf/mod.rs b/bin/src/utils/sqf/mod.rs index c6bd55ef..c9cff3f2 100644 --- a/bin/src/utils/sqf/mod.rs +++ b/bin/src/utils/sqf/mod.rs @@ -1,15 +1,19 @@ mod case; -use clap::{ArgMatches, Command}; - use crate::Error; -#[must_use] -pub fn cli() -> Command { - Command::new("sqf") - .about("Commands for SQF files") - .arg_required_else_help(true) - .subcommand(case::cli()) +#[derive(clap::Parser)] +#[command(arg_required_else_help = true)] +/// Commands for SQF files +pub struct Command { + #[command(subcommand)] + commands: Subcommands, +} + +#[derive(clap::Subcommand)] +enum Subcommands { + /// Convert case + Case(case::Args), } /// Execute the paa command @@ -19,10 +23,8 @@ pub fn cli() -> Command { /// /// # Panics /// If the args are not present from clap -pub fn execute(matches: &ArgMatches) -> Result<(), Error> { - match matches.subcommand() { - Some(("case", matches)) => case::execute(matches), - - _ => unreachable!(), +pub fn execute(cmd: &Command) -> Result<(), Error> { + match &cmd.commands { + Subcommands::Case(args) => case::execute(args), } } diff --git a/bin/src/utils/verify.rs b/bin/src/utils/verify.rs index 4848a2ba..2bf576b9 100644 --- a/bin/src/utils/verify.rs +++ b/bin/src/utils/verify.rs @@ -1,6 +1,5 @@ use std::path::PathBuf; -use clap::{ArgMatches, Command}; use hemtt_pbo::ReadablePbo; use crate::{ @@ -8,17 +7,14 @@ use crate::{ Error, }; -#[must_use] -pub fn cli() -> Command { - Command::new("verify") - .about("Verify a signed PBO") - .long_about("Check a .bisign file against a public key and PBO") - .arg(clap::Arg::new("pbo").help("PBO to verify").required(true)) - .arg( - clap::Arg::new("bikey") - .help("BIKey to verify against") - .required(true), - ) +#[derive(clap::Parser)] +#[command(arg_required_else_help = true)] +/// Verify a signed PBO +pub struct Command { + /// PBO to verify + pbo: String, + /// `BIKey` to verify against + bikey: String, } /// Execute the verify command @@ -28,9 +24,9 @@ pub fn cli() -> Command { /// /// # Panics /// If the args are not present from clap -pub fn execute(matches: &ArgMatches) -> Result<(), Error> { - let pbo_path = PathBuf::from(matches.get_one::("pbo").expect("required")); - let bikey_path = PathBuf::from(matches.get_one::("bikey").expect("required")); +pub fn execute(cmd: &Command) -> Result<(), Error> { + let pbo_path = PathBuf::from(&cmd.pbo); + let bikey_path = PathBuf::from(&cmd.bikey); debug!("Reading PBO: {:?}", &pbo_path); let mut pbo = ReadablePbo::from(std::fs::File::open(&pbo_path)?)?; diff --git a/bin/tests/build.rs b/bin/tests/build.rs index a6e81587..56353e09 100644 --- a/bin/tests/build.rs +++ b/bin/tests/build.rs @@ -1,19 +1,20 @@ #![allow(clippy::unwrap_used)] +use clap::Parser; use sealed_test::prelude::*; -use hemtt::cli; +use hemtt::Cli; #[sealed_test] fn build_alpha() { std::env::set_current_dir(format!("{}/tests/alpha", env!("CARGO_MANIFEST_DIR"))).unwrap(); - hemtt::execute(&cli().get_matches_from(vec!["hemtt", "dev", "--in-test"])).unwrap(); - hemtt::execute(&cli().get_matches_from(vec!["hemtt", "build", "--in-test"])).unwrap(); + hemtt::execute(&Cli::parse_from(vec!["hemtt", "dev", "--in-test"])).unwrap(); + hemtt::execute(&Cli::parse_from(vec!["hemtt", "build", "--in-test"])).unwrap(); } #[sealed_test] fn build_bravo() { std::env::set_current_dir(format!("{}/tests/bravo", env!("CARGO_MANIFEST_DIR"))).unwrap(); - hemtt::execute(&cli().get_matches_from(vec!["hemtt", "script", "test"])).unwrap(); - hemtt::execute(&cli().get_matches_from(vec!["hemtt", "release", "--in-test"])).unwrap(); + hemtt::execute(&Cli::parse_from(vec!["hemtt", "script", "test"])).unwrap(); + hemtt::execute(&Cli::parse_from(vec!["hemtt", "release", "--in-test"])).unwrap(); } diff --git a/bin/tests/new.rs b/bin/tests/new.rs index f4813e52..fbc59a87 100644 --- a/bin/tests/new.rs +++ b/bin/tests/new.rs @@ -1,10 +1,11 @@ #![allow(clippy::unwrap_used)] -use hemtt::cli; +use clap::Parser; +use hemtt::Cli; use sealed_test::prelude::*; #[sealed_test] fn new() { - hemtt::execute(&cli().get_matches_from(vec!["hemtt", "new", "test", "--in-test"])).unwrap(); + hemtt::execute(&Cli::parse_from(vec!["hemtt", "new", "test", "--in-test"])).unwrap(); } diff --git a/libs/paa/Cargo.toml b/libs/paa/Cargo.toml index dca66121..4df640c7 100644 --- a/libs/paa/Cargo.toml +++ b/libs/paa/Cargo.toml @@ -14,12 +14,13 @@ crate-type = ["cdylib", "rlib"] [dependencies] hemtt-lzo = { path = "../lzo", version = "1.0.0", features = ["decompress"], default-features = false } -js-sys = { version = "0.3.72", optional = true } -wasm-bindgen = { version = "0.2.95", optional = true } - byteorder = { workspace = true } texpresso = "2.0.1" image = "0.25.5" +# WASM +js-sys = { version = "0.3.72", optional = true } +wasm-bindgen = { version = "0.2.95", optional = true } + [features] wasm = ["wasm-bindgen", "js-sys"] diff --git a/libs/paa/src/mipmap.rs b/libs/paa/src/mipmap.rs index 09426d3b..6c468735 100644 --- a/libs/paa/src/mipmap.rs +++ b/libs/paa/src/mipmap.rs @@ -64,6 +64,12 @@ impl MipMap { &self.format } + #[must_use] + /// Get the format of the `MipMap` as a string + pub fn format_display(&self) -> String { + format!("{:?}", self.format) + } + #[must_use] /// Get the image from the `MipMap` ///