495 lines
16 KiB
Rust
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!();
|
|
} |