use anyhow::Result;
use clap::{Parser, Subcommand};
pub mod build_dispatch;
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),
#[command(name = "build-ios", hide = true)]
BuildIos(build_dispatch::IosArgs),
#[command(name = "build-android", hide = true)]
BuildAndroid(build_dispatch::AndroidArgs),
#[command(name = "modules", hide = true)]
Modules(build_dispatch::ModulesArgs),
}
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),
Command::BuildIos(a) => build_dispatch::run_ios(a),
Command::BuildAndroid(a) => build_dispatch::run_android(a),
Command::Modules(a) => build_dispatch::run_modules(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 internal_build_subcommands_parse() {
match parse([
"whisker",
"build-ios",
"--workspace=/ws",
"--package=app",
"--configuration=Debug",
"--platform=iphonesimulator",
"--archs=arm64",
"--built-products-dir=/out",
])
.unwrap()
.command
{
Command::BuildIos(_) => {}
other => panic!("expected BuildIos, got {other:?}"),
}
match parse([
"whisker",
"build-android",
"--workspace=/ws",
"--package=app",
"--profile=debug",
"--abi=arm64-v8a",
"--jni-libs-dir=/jni",
])
.unwrap()
.command
{
Command::BuildAndroid(_) => {}
other => panic!("expected BuildAndroid, got {other:?}"),
}
match parse(["whisker", "modules", "--workspace=/ws", "--package=app"])
.unwrap()
.command
{
Command::Modules(_) => {}
other => panic!("expected Modules, got {other:?}"),
}
}
#[test]
fn version_flag_short_circuits_to_displayversion() {
let e = parse(["whisker", "--version"]).unwrap_err();
assert_eq!(e.kind(), clap::error::ErrorKind::DisplayVersion);
}
}