use anyhow::Result;
use clap::{Parser, Subcommand};
pub mod doctor;
pub mod linker_shim;
pub mod manifest;
pub mod new_app;
pub mod new_module;
pub mod platforms;
pub mod probe;
pub mod run;
pub mod rustc_shim;
pub mod tui;
#[derive(Parser, Debug)]
#[command(
name = "whisker",
about = "Whisker — cross-platform mobile UI framework",
version
)]
struct Cli {
#[arg(long, short = 'v', global = true)]
verbose: bool,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug)]
enum Command {
Doctor(doctor::Args),
Run(run::Args),
NewModule(new_module::NewModuleArgs),
New(new_app::NewAppArgs),
}
pub fn run(args: impl IntoIterator<Item = String>) -> Result<()> {
let cli = match Cli::try_parse_from(args) {
Ok(c) => c,
Err(e) => e.exit(),
};
if cli.verbose {
std::env::set_var("WHISKER_VERBOSE", "1");
}
match cli.command {
Command::Doctor(a) => doctor::run(a),
Command::Run(a) => run::run(a),
Command::NewModule(a) => new_module::run(a),
Command::New(a) => new_app::run(a),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse<I, S>(args: I) -> std::result::Result<Cli, clap::Error>
where
I: IntoIterator<Item = S>,
S: Into<std::ffi::OsString> + Clone,
{
Cli::try_parse_from(args)
}
#[test]
fn parses_doctor_with_no_flags() {
let cli = parse(["whisker", "doctor"]).unwrap();
match cli.command {
Command::Doctor(a) => {
assert!(!a.no_ios);
assert!(!a.no_android);
}
other => panic!("expected Doctor, got {other:?}"),
}
}
#[test]
fn parses_run_with_only_target() {
let cli = parse(["whisker", "run", "android"]).unwrap();
match cli.command {
Command::Run(a) => {
assert!(a.manifest_path.is_none());
assert_eq!(a.target, run::CliTarget::Android);
assert_eq!(a.bind.port(), 9876);
assert!(!a.no_hot_patch);
assert!(a.workspace_root.is_none());
}
other => panic!("expected Run, got {other:?}"),
}
}
#[test]
fn parses_run_without_target_fails() {
let res = parse(["whisker", "run"]);
assert!(res.is_err(), "expected clap error, got {res:?}");
}
#[test]
fn parses_run_with_explicit_target_and_flags() {
let cli = parse([
"whisker",
"run",
"--manifest-path",
"/tmp/my-app/Cargo.toml",
"android",
"--bind",
"0.0.0.0:1234",
"--no-hot-patch",
])
.unwrap();
match cli.command {
Command::Run(a) => {
assert_eq!(
a.manifest_path.as_deref(),
Some(std::path::Path::new("/tmp/my-app/Cargo.toml")),
);
assert_eq!(a.target, run::CliTarget::Android);
assert_eq!(a.bind.to_string(), "0.0.0.0:1234");
assert!(a.no_hot_patch);
}
other => panic!("expected Run, got {other:?}"),
}
}
#[test]
fn parses_doctor_skip_flags() {
let cli = parse(["whisker", "doctor", "--no-ios", "--no-android"]).unwrap();
match cli.command {
Command::Doctor(a) => {
assert!(a.no_ios);
assert!(a.no_android);
}
other => panic!("expected Doctor, got {other:?}"),
}
}
#[test]
fn missing_subcommand_is_an_error() {
let e = parse(["whisker"]).unwrap_err();
assert_eq!(
e.kind(),
clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand,
);
}
#[test]
fn unknown_subcommand_is_an_error() {
let e = parse(["whisker", "frobnicate"]).unwrap_err();
assert_eq!(e.kind(), clap::error::ErrorKind::InvalidSubcommand);
}
#[test]
fn help_flag_short_circuits_to_displayhelp() {
let e = parse(["whisker", "--help"]).unwrap_err();
assert_eq!(e.kind(), clap::error::ErrorKind::DisplayHelp);
}
#[test]
fn version_flag_short_circuits_to_displayversion() {
let e = parse(["whisker", "--version"]).unwrap_err();
assert_eq!(e.kind(), clap::error::ErrorKind::DisplayVersion);
}
}