Files
anvil/src/main.rs
Eric Ratliff ba402cc187
Some checks failed
CI / Test (Linux) (push) Has been cancelled
CI / Test (Windows MSVC) (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Format (push) Has been cancelled
Supporting auto complete and a build-release script
2026-02-22 08:22:22 -06:00

495 lines
16 KiB
Rust

use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::{generate, Shell};
use colored::*;
use anyhow::Result;
use anvil::version::ANVIL_VERSION;
mod commands {
pub use anvil::commands::*;
}
#[derive(Parser)]
#[command(name = "anvil")]
#[command(author = "Eric Ratliff <eric@nxlearn.net>")]
#[command(version = ANVIL_VERSION)]
#[command(
about = "Arduino project generator and build tool - forges clean embedded projects",
long_about = None
)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Create a new Arduino project
New {
/// Project name
name: Option<String>,
/// Template to use (basic)
#[arg(long, short = 't', value_name = "TEMPLATE")]
template: Option<String>,
/// Board preset (uno, mega, nano, leonardo, micro)
#[arg(long, short = 'b', value_name = "BOARD")]
board: Option<String>,
/// List available templates
#[arg(long, conflicts_with = "name")]
list_templates: bool,
/// List available board presets
#[arg(long, conflicts_with = "name")]
list_boards: bool,
},
/// Check system health and diagnose issues
Doctor {
/// Automatically install missing tools
#[arg(long)]
fix: bool,
},
/// Install arduino-cli and required cores
Setup,
/// List connected boards and serial ports
Devices {
/// Save a port to .anvil.local for this project
#[arg(long, conflicts_with_all = ["get", "clear"])]
set: bool,
/// Show the saved port for this project
#[arg(long, conflicts_with_all = ["set", "clear"])]
get: bool,
/// Remove .anvil.local (revert to auto-detect)
#[arg(long, conflicts_with_all = ["set", "get"])]
clear: bool,
/// Port name (e.g. COM3, /dev/ttyUSB0). Auto-detects if omitted with --set.
port_or_dir: Option<String>,
/// Path to project directory (defaults to current directory)
#[arg(long, short = 'd', value_name = "DIR")]
dir: Option<String>,
},
/// Update project scripts to the latest version
Refresh {
/// Path to project directory (defaults to current directory)
dir: Option<String>,
/// Overwrite managed files even if they have been modified
#[arg(long)]
force: bool,
/// Override .anvilignore for a specific file (use with --force)
#[arg(long, value_name = "PATH")]
file: Option<String>,
/// Add a pattern to .anvilignore (protect a file from refresh)
#[arg(long, value_name = "PATTERN", conflicts_with_all = ["unignore", "force"])]
ignore: Option<String>,
/// Remove a pattern from .anvilignore (allow refresh to update it)
#[arg(long, value_name = "PATTERN", conflicts_with_all = ["ignore", "force"])]
unignore: Option<String>,
},
/// Manage board profiles in .anvil.toml
Board {
/// Board name (e.g. mega, nano)
name: Option<String>,
/// Add a board to the project
#[arg(long, conflicts_with_all = ["remove", "listall", "default"])]
add: bool,
/// Remove a board from the project
#[arg(long, conflicts_with_all = ["add", "listall", "default"])]
remove: bool,
/// Set the default board
#[arg(long, conflicts_with_all = ["add", "remove", "listall"])]
default: bool,
/// Browse all available boards
#[arg(long, conflicts_with_all = ["add", "remove", "default"])]
listall: bool,
/// Board identifier (from anvil board --listall)
#[arg(long, value_name = "ID")]
id: Option<String>,
/// Baud rate override
#[arg(long, value_name = "RATE")]
baud: Option<u32>,
/// Path to project directory (defaults to current directory)
#[arg(long, short = 'd', value_name = "DIR")]
dir: Option<String>,
},
/// Add a device library to the project
Add {
/// Library name (e.g. tmp36)
name: String,
/// Assign a pin during add (e.g. --pin A0)
#[arg(long, value_name = "PIN")]
pin: Option<String>,
/// Path to project directory (defaults to current directory)
#[arg(long, short = 'd', value_name = "DIR")]
dir: Option<String>,
},
/// Remove a device library from the project
Remove {
/// Library name (e.g. tmp36)
name: String,
/// Path to project directory (defaults to current directory)
#[arg(long, short = 'd', value_name = "DIR")]
dir: Option<String>,
},
/// List installed or available device libraries
Lib {
/// Show all available libraries (not just installed)
#[arg(long)]
available: bool,
/// Path to project directory (defaults to current directory)
#[arg(long, short = 'd', value_name = "DIR")]
dir: Option<String>,
},
/// Generate shell completion scripts
Completions {
/// Shell to generate completions for (bash, zsh, fish, powershell)
shell: Shell,
},
/// View pin maps, assign pins, and audit wiring
Pin {
/// Capability filter (pwm, analog, spi, i2c, uart, interrupt)
/// or pin/bus name for --assign / --remove
name: Option<String>,
/// Pin number or alias for --assign (e.g. 13, A0, SDA)
pin: Option<String>,
/// Assign a pin or bus group
#[arg(long, conflicts_with_all = ["remove", "audit", "generate", "capabilities", "init_from"])]
assign: bool,
/// Remove a pin or bus assignment
#[arg(long, conflicts_with_all = ["assign", "audit", "generate", "capabilities", "init_from"])]
remove: bool,
/// Show wiring audit report
#[arg(long, conflicts_with_all = ["assign", "remove", "generate", "capabilities", "init_from"])]
audit: bool,
/// Show only the wiring checklist (with --audit)
#[arg(long, requires = "audit")]
brief: bool,
/// Generate lib/hal/pins.h
#[arg(long, conflicts_with_all = ["assign", "remove", "audit", "capabilities", "init_from"])]
generate: bool,
/// List capabilities supported by the board
#[arg(long, conflicts_with_all = ["assign", "remove", "audit", "generate", "init_from"])]
capabilities: bool,
/// Copy pin assignments from another board
#[arg(long, value_name = "BOARD", conflicts_with_all = ["assign", "remove", "audit", "generate", "capabilities"])]
init_from: Option<String>,
/// Pin mode (input, output, input_pullup, pwm, analog)
#[arg(long, value_name = "MODE")]
mode: Option<String>,
/// SPI chip-select pin (with --assign spi)
#[arg(long, value_name = "PIN")]
cs: Option<String>,
/// Target board (defaults to project default)
#[arg(long, short = 'b', value_name = "BOARD")]
board: Option<String>,
/// Path to project directory (defaults to current directory)
#[arg(long, short = 'd', value_name = "DIR")]
dir: Option<String>,
},
}
fn main() -> Result<()> {
#[cfg(windows)]
colored::control::set_virtual_terminal(true).ok();
let cli = Cli::parse();
// Completions output must be clean -- no banner
let is_completions = matches!(cli.command, Commands::Completions { .. });
if !is_completions {
print_banner();
}
match cli.command {
Commands::New { name, template, board, list_templates, list_boards } => {
if list_boards {
commands::new::list_boards()
} else if list_templates {
commands::new::list_templates()
} else if let Some(project_name) = name {
commands::new::create_project(
&project_name,
template.as_deref(),
board.as_deref(),
)
} else {
anyhow::bail!(
"Project name required.\n\
Usage: anvil new <name>\n\
Usage: anvil new <name> --board mega\n\
List boards: anvil new --list-boards\n\
List templates: anvil new --list-templates"
);
}
}
Commands::Doctor { fix } => {
commands::doctor::run_diagnostics(fix)
}
Commands::Setup => {
commands::setup::run_setup()
}
Commands::Devices { set, get, clear, port_or_dir, dir } => {
if set {
commands::devices::set_port(
port_or_dir.as_deref(),
dir.as_deref(),
)
} else if get {
commands::devices::get_port(
dir.as_deref().or(port_or_dir.as_deref()),
)
} else if clear {
commands::devices::clear_port(
dir.as_deref().or(port_or_dir.as_deref()),
)
} else {
commands::devices::scan_devices()
}
}
Commands::Refresh { dir, force, file, ignore, unignore } => {
if let Some(pattern) = ignore {
commands::refresh::add_ignore(
dir.as_deref(),
&pattern,
)
} else if let Some(pattern) = unignore {
commands::refresh::remove_ignore(
dir.as_deref(),
&pattern,
)
} else {
commands::refresh::run_refresh(
dir.as_deref(),
force,
file.as_deref(),
)
}
}
Commands::Board { name, add, remove, default, listall, id, baud, dir } => {
if listall {
commands::board::listall_boards(name.as_deref())
} else if add {
let board_name = name.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"Board name required.\n\
Usage: anvil board --add mega\n\
Browse available boards: anvil board --listall"
)
})?;
commands::board::add_board(
board_name,
id.as_deref(),
baud,
dir.as_deref(),
)
} else if remove {
let board_name = name.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"Board name required.\n\
Usage: anvil board --remove mega"
)
})?;
commands::board::remove_board(
board_name,
dir.as_deref(),
)
} else if default {
let board_name = name.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"Board name required.\n\
Usage: anvil board --default uno\n\
List boards: anvil board"
)
})?;
commands::board::set_default_board(
board_name,
dir.as_deref(),
)
} else {
commands::board::list_boards(dir.as_deref())
}
}
Commands::Add { name, pin, dir } => {
commands::lib::add_library(&name, pin.as_deref(), dir.as_deref())
}
Commands::Remove { name, dir } => {
commands::lib::remove_library(&name, dir.as_deref())
}
Commands::Lib { available, dir } => {
if available {
commands::lib::list_available()
} else {
commands::lib::list_libraries(dir.as_deref())
}
}
Commands::Completions { shell } => {
let mut cmd = Cli::command();
generate(shell, &mut cmd, "anvil", &mut std::io::stdout());
Ok(())
}
Commands::Pin {
name, pin, assign, remove, audit, brief,
generate, capabilities, init_from, mode, cs, board, dir,
} => {
if capabilities {
commands::pin::show_capabilities(
board.as_deref(),
dir.as_deref(),
)
} else if audit {
commands::pin::audit_pins(
board.as_deref(),
brief,
dir.as_deref(),
)
} else if generate {
commands::pin::generate_pins_header(
board.as_deref(),
dir.as_deref(),
)
} else if remove {
let pin_name = name.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"Name required.\n\
Usage: anvil pin --remove red_led\n\
Audit current: anvil pin --audit"
)
})?;
commands::pin::remove_assignment(
pin_name,
board.as_deref(),
dir.as_deref(),
)
} else if let Some(ref source) = init_from {
let target = board.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"Target board required.\n\
Usage: anvil pin --init-from uno --board mega"
)
})?;
commands::pin::init_from(
source,
target,
dir.as_deref(),
)
} else if assign {
let assign_name = name.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"Name required.\n\
Usage: anvil pin --assign red_led 13\n\
Usage: anvil pin --assign spi --cs 10\n\
See pins: anvil pin"
)
})?;
// Check if this is a bus group assignment
let is_bus = {
let board_name = board.as_deref().unwrap_or("uno");
anvil::board::pinmap::find_pinmap_fuzzy(board_name)
.map(|pm| pm.groups.iter().any(|g| {
g.name == assign_name.to_lowercase()
}))
.unwrap_or(false)
};
if is_bus {
let mut user_pins: Vec<(&str, &str)> = Vec::new();
if let Some(ref cs_pin) = cs {
user_pins.push(("cs", cs_pin.as_str()));
}
commands::pin::assign_bus(
assign_name,
&user_pins,
board.as_deref(),
dir.as_deref(),
)
} else {
let pin_str = pin.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"Pin number required.\n\
Usage: anvil pin --assign {} 13\n\
See pins: anvil pin",
assign_name
)
})?;
commands::pin::assign_pin(
assign_name,
pin_str,
mode.as_deref(),
board.as_deref(),
dir.as_deref(),
)
}
} else {
// Default: show pin map, optionally filtered
commands::pin::show_pin_map(
board.as_deref(),
name.as_deref(),
dir.as_deref(),
)
}
}
}
}
fn print_banner() {
println!(
"{}",
"================================================================"
.bright_cyan()
);
println!(
"{}",
format!(" Anvil - Arduino Build Tool v{}", ANVIL_VERSION)
.bright_cyan()
.bold()
);
println!("{}", " Nexus Workshops LLC".bright_cyan());
println!(
"{}",
"================================================================"
.bright_cyan()
);
println!();
}