mod asking;
mod build;
mod input;
mod metadata;
mod new;
mod package;
mod run;
mod screenshot;
mod signing_key;
mod unreached;
mod waos_app;
mod workspace;
use std::path::Path;
use std::process::ExitCode;
use anyhow::{Context as _, Result};
use clap::{Args, Parser, Subcommand};
use waos_dev_protocol::Input;
use waos_package::ProgramId;
use crate::build::{BuiltProgram, Profile};
use crate::input::Pair;
use crate::metadata::WaosMetadata;
use crate::new::Template;
#[derive(Parser)]
#[command(name = "cargo", bin_name = "cargo")]
enum Cargo {
#[command(
name = "waos",
about = "cargo waos: writes a new program, and builds a program as a component, runs it in WaOS and packages it, and takes screenshots of its windows and gives them input while it runs",
version
)]
Waos(WaosArgs),
}
#[derive(Args)]
struct WaosArgs {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
New(NewArgs),
Build(BuildArgs),
Run(BuildArgs),
Package(Program),
Screenshot(Program),
Tap(TapArgs),
Type(TypeArgs),
Key(KeyArgs),
Scroll(ScrollArgs),
}
#[derive(Args)]
struct NewArgs {
name: String,
#[arg(long, value_enum, default_value_t)]
template: Template,
}
#[derive(Args)]
struct Program {
#[arg(short, long, value_name = "PACKAGE")]
package: Option<String>,
}
#[derive(Args)]
struct BuildArgs {
#[command(flatten)]
program: Program,
#[arg(long)]
release: bool,
}
#[derive(Args)]
struct WindowArgs {
#[command(flatten)]
program: Program,
#[arg(long, default_value_t = 1)]
window: u32,
}
#[derive(Args)]
struct TapArgs {
#[command(flatten)]
window: WindowArgs,
#[arg(long, value_name = "X,Y", allow_hyphen_values = true)]
at: Pair,
}
#[derive(Args)]
struct TypeArgs {
#[command(flatten)]
window: WindowArgs,
#[arg(allow_hyphen_values = true)]
text: String,
}
#[derive(Args)]
struct KeyArgs {
#[command(flatten)]
window: WindowArgs,
#[arg(allow_hyphen_values = true)]
chord: String,
}
#[derive(Args)]
struct ScrollArgs {
#[command(flatten)]
window: WindowArgs,
#[arg(long, value_name = "X,Y", allow_hyphen_values = true)]
at: Pair,
#[arg(long, value_name = "DX,DY", allow_hyphen_values = true)]
by: Pair,
}
impl BuildArgs {
fn profile(&self) -> Profile {
if self.release {
Profile::Release
} else {
Profile::Debug
}
}
}
fn main() -> ExitCode {
let Cargo::Waos(args) = Cargo::parse();
let done = match args.command {
Command::New(new) => new_program(&new),
Command::Build(build) => build_only(&build),
Command::Run(build) => build_and_run(&build),
Command::Package(program) => package(&program),
Command::Screenshot(program) => screenshot(&program),
Command::Tap(tap) => give(
&tap.window,
Input::Tap {
x: tap.at.0,
y: tap.at.1,
},
),
Command::Type(typed) => give(&typed.window, Input::Text(typed.text)),
Command::Key(key) => give(&key.window, Input::Key(key.chord)),
Command::Scroll(scroll) => give(
&scroll.window,
Input::Scroll {
x: scroll.at.0,
y: scroll.at.1,
dx: scroll.by.0,
dy: scroll.by.1,
},
),
};
done.unwrap_or_else(|error| {
eprintln!("cargo waos: {error:#}");
ExitCode::FAILURE
})
}
fn new_program(args: &NewArgs) -> Result<ExitCode> {
let folder = new::create(&args.name, args.template, Path::new("."))?;
println!("created {}", folder.display());
println!(" cd {} && cargo waos run", args.name);
Ok(ExitCode::SUCCESS)
}
fn build_only(args: &BuildArgs) -> Result<ExitCode> {
let built = build_chosen(args)?;
println!("{}", built.component.display());
Ok(ExitCode::SUCCESS)
}
fn build_and_run(args: &BuildArgs) -> Result<ExitCode> {
let metadata = workspace::metadata()?;
let package = workspace::chosen_package(&metadata, args.program.package.as_deref())?;
let id = WaosMetadata::id_of(package)?;
let built = build::build(&metadata, package, args.profile())?;
on_this_thread(run::run(id, &built))
}
fn screenshot(program: &Program) -> Result<ExitCode> {
let metadata = workspace::metadata()?;
let package = workspace::chosen_package(&metadata, program.package.as_deref())?;
let id = WaosMetadata::id_of(package)?;
let folder = workspace::waos_folder(&metadata)
.join("screenshots")
.join(package.name.as_str());
on_this_thread(screenshot::take(id, &folder))
}
fn give(window: &WindowArgs, input: Input) -> Result<ExitCode> {
let id = program_id(&window.program)?;
on_this_thread(input::give(id, window.window, input))
}
fn program_id(program: &Program) -> Result<ProgramId> {
let metadata = workspace::metadata()?;
let package = workspace::chosen_package(&metadata, program.package.as_deref())?;
WaosMetadata::id_of(package)
}
fn on_this_thread(connection: impl Future<Output = Result<ExitCode>>) -> Result<ExitCode> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.context("a runtime for the connection")?;
runtime.block_on(connection)
}
fn package(program: &Program) -> Result<ExitCode> {
let packaged = package::package(program.package.as_deref())?;
eprintln!("cargo waos: listed in {}", packaged.index.display());
println!("{}", packaged.package.display());
Ok(ExitCode::SUCCESS)
}
fn build_chosen(args: &BuildArgs) -> Result<BuiltProgram> {
let metadata = workspace::metadata()?;
let package = workspace::chosen_package(&metadata, args.program.package.as_deref())?;
build::build(&metadata, package, args.profile())
}
#[cfg(test)]
mod tests {
use super::*;
fn parsed(arguments: &[&str]) -> Command {
let Cargo::Waos(args) = Cargo::try_parse_from(["cargo", "waos"].iter().chain(arguments))
.unwrap_or_else(|error| panic!("{arguments:?}: {error}"));
args.command
}
fn refused(arguments: &[&str]) -> bool {
Cargo::try_parse_from(["cargo", "waos"].iter().chain(arguments)).is_err()
}
#[test]
fn a_screenshot_names_its_package_or_none() {
let Command::Screenshot(program) = parsed(&["screenshot"]) else {
panic!("a screenshot");
};
assert_eq!(program.package, None);
let Command::Screenshot(program) = parsed(&["screenshot", "-p", "notes"]) else {
panic!("a screenshot");
};
assert_eq!(program.package.as_deref(), Some("notes"));
}
#[test]
fn a_tap_is_at_a_point_in_the_first_window_unless_another_is_named() {
let Command::Tap(tap) = parsed(&["tap", "--at", "120,48"]) else {
panic!("a tap");
};
assert_eq!((tap.at, tap.window.window), (Pair(120.0, 48.0), 1));
let Command::Tap(tap) = parsed(&["tap", "--at", "1.5,2", "--window", "2", "-p", "notes"])
else {
panic!("a tap");
};
assert_eq!((tap.at, tap.window.window), (Pair(1.5, 2.0), 2));
assert_eq!(tap.window.program.package.as_deref(), Some("notes"));
assert!(refused(&["tap"]), "a tap needs a point");
assert!(refused(&["tap", "--at", "120"]));
}
#[test]
fn a_scroll_is_at_a_point_by_an_amount_either_signed() {
let Command::Scroll(scroll) = parsed(&["scroll", "--at", "10,20", "--by", "-5,-200"])
else {
panic!("a scroll");
};
assert_eq!(
(scroll.at, scroll.by),
(Pair(10.0, 20.0), Pair(-5.0, -200.0))
);
assert!(
refused(&["scroll", "--at", "10,20"]),
"a scroll needs an amount"
);
}
#[test]
fn text_and_a_chord_are_taken_as_written() {
let Command::Type(typed) = parsed(&["type", "-5 eggs, milk"]) else {
panic!("text");
};
assert_eq!(typed.text, "-5 eggs, milk");
let Command::Key(key) = parsed(&["key", "cmd+s", "--window", "3"]) else {
panic!("a key");
};
assert_eq!((key.chord.as_str(), key.window.window), ("cmd+s", 3));
let Command::Key(key) = parsed(&["key", "-"]) else {
panic!("a key");
};
assert_eq!(key.chord, "-");
}
}