use anyhow::Result;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
use thermark::config::{Config, ConnPref, MAX_SCAN_SECS};
use thermark::label::{TextAlign, TextSide};
use thermark::print_task::PrintTask;
use thermark::protocol::Model;
#[cfg(feature = "ble")]
use thermark::transport::BleMatchMode;
use thermark::types::{Density, Rotation, Threshold};
use thermark::wifi::WifiSecurity;
#[derive(Parser, Debug)]
#[command(
name = "thermark",
version,
about = "Local sticker printing over BLE/USB — guest Wi‑Fi, QR, inventory (no vendor app)"
)]
pub struct Cli {
#[arg(short, long, global = true)]
pub verbose: bool,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Debug, Clone, clap::Args)]
pub struct ConnArgs {
#[arg(short = 'c', long, value_enum)]
pub conn: Option<ConnPref>,
#[arg(short, long)]
pub addr: Option<String>,
#[arg(long, value_parser = parse_scan_seconds)]
pub scan_secs: Option<u64>,
#[arg(long, default_value_t = false)]
pub fuzzy: bool,
}
#[derive(Debug, Clone, clap::Args)]
pub struct TaskArgs {
#[arg(long, value_enum)]
pub task: Option<PrintTask>,
#[arg(long, default_value_t = false)]
pub allow_experimental: bool,
}
#[derive(Debug, Clone, clap::Args)]
pub struct FontArgs {
#[arg(long)]
pub font: Option<PathBuf>,
#[arg(long)]
pub font_name: Option<String>,
#[arg(long)]
pub font_size: Option<f32>,
}
#[cfg_attr(not(any(feature = "ble", feature = "serial")), allow(dead_code))]
pub struct ResolvedConn {
pub conn: ConnPref,
pub addr: String,
#[cfg(feature = "ble")]
pub scan_secs: u64,
#[cfg(feature = "ble")]
pub match_mode: BleMatchMode,
}
impl ConnArgs {
pub fn resolve(&self, cfg: &Config) -> Result<ResolvedConn> {
Ok(ResolvedConn {
conn: cfg.resolve_connection(self.conn),
addr: cfg.resolve_addr(self.addr.as_deref())?,
#[cfg(feature = "ble")]
scan_secs: cfg.resolve_scan_secs(self.scan_secs),
#[cfg(feature = "ble")]
match_mode: BleMatchMode::from_fuzzy(self.fuzzy),
})
}
}
pub fn parse_density(s: &str) -> std::result::Result<Density, String> {
s.parse::<Density>().map_err(|e| e.to_string())
}
pub fn parse_rotation(s: &str) -> std::result::Result<Rotation, String> {
s.parse::<Rotation>().map_err(|e| e.to_string())
}
pub fn parse_threshold(s: &str) -> std::result::Result<Threshold, String> {
s.parse::<Threshold>().map_err(|e| e.to_string())
}
pub fn parse_scan_seconds(s: &str) -> std::result::Result<u64, String> {
let seconds = s
.parse::<u64>()
.map_err(|_| "scan time must be an integer number of seconds".to_string())?;
if !(1..=MAX_SCAN_SECS).contains(&seconds) {
return Err(format!(
"scan time must be between 1 and {MAX_SCAN_SECS} seconds"
));
}
Ok(seconds)
}
#[derive(Debug, clap::Args)]
pub struct PrintCommand {
#[command(flatten)]
pub conn: ConnArgs,
#[command(flatten)]
pub task: TaskArgs,
#[arg(short, long)]
pub image: PathBuf,
#[arg(short, long, value_enum)]
pub model: Option<Model>,
#[arg(short, long, default_value = "3", value_parser = parse_density)]
pub density: Density,
#[arg(short, long, default_value = "0", value_parser = parse_rotation)]
pub rotate: Rotation,
#[arg(long, default_value = "127", value_parser = parse_threshold)]
pub threshold: Threshold,
#[arg(long, default_value_t = false)]
pub fit: bool,
#[arg(long)]
pub label: Option<String>,
#[arg(long, default_value_t = true)]
pub fill: bool,
#[arg(long, default_value_t = false)]
pub no_fill: bool,
#[arg(long, default_value_t = 0)]
pub margin: u32,
#[arg(long, default_value_t = false)]
pub dither: bool,
#[arg(long, default_value_t = false)]
pub no_trim: bool,
#[arg(long, default_value_t = false)]
pub full_bleed: bool,
#[arg(long)]
pub preview: Option<PathBuf>,
}
#[derive(Debug, clap::Args)]
pub struct CalibrateCommand {
#[command(flatten)]
pub conn: ConnArgs,
#[command(flatten)]
pub task: TaskArgs,
#[arg(short, long, value_enum)]
pub model: Option<Model>,
#[arg(long)]
pub label: Option<String>,
#[arg(short, long, default_value = "4", value_parser = parse_density)]
pub density: Density,
#[arg(long, default_value_t = false)]
pub boundary: bool,
}
#[derive(Debug, clap::Args)]
pub struct TextCommand {
#[command(flatten)]
pub conn: ConnArgs,
#[command(flatten)]
pub task: TaskArgs,
#[command(flatten)]
pub font: FontArgs,
#[arg(short, long, value_enum)]
pub model: Option<Model>,
#[arg(long)]
pub text: String,
#[arg(long, value_enum, default_value_t = TextAlign::Center)]
pub align: TextAlign,
#[arg(long)]
pub label: Option<String>,
#[arg(long, default_value_t = false)]
pub border: bool,
#[arg(short, long, default_value = "4", value_parser = parse_density)]
pub density: Density,
#[arg(long)]
pub save: Option<PathBuf>,
#[arg(long, default_value_t = false)]
pub no_print: bool,
}
#[derive(Debug, clap::Args)]
pub struct QrCommand {
#[command(flatten)]
pub conn: ConnArgs,
#[command(flatten)]
pub task: TaskArgs,
#[command(flatten)]
pub font: FontArgs,
#[arg(short, long, value_enum)]
pub model: Option<Model>,
#[arg(long, default_value = "https://example.com")]
pub url: String,
#[arg(long, default_value = "ABC\nHELLO")]
pub text: String,
#[arg(long, value_enum, default_value_t = TextSide::Right)]
pub text_side: TextSide,
#[arg(long)]
pub label: Option<String>,
#[arg(long, default_value_t = false)]
pub border: bool,
#[arg(short, long, default_value = "4", value_parser = parse_density)]
pub density: Density,
#[arg(long)]
pub save: Option<PathBuf>,
#[arg(long, default_value_t = false)]
pub no_print: bool,
}
#[derive(Debug, clap::Args)]
pub struct WifiCommand {
#[command(flatten)]
pub conn: ConnArgs,
#[command(flatten)]
pub task: TaskArgs,
#[command(flatten)]
pub font: FontArgs,
#[arg(short, long, value_enum)]
pub model: Option<Model>,
#[arg(long)]
pub ssid: String,
#[arg(long, default_value = "")]
pub password: String,
#[arg(long, value_enum, default_value_t = WifiSecurity::Wpa)]
pub security: WifiSecurity,
#[arg(long, default_value_t = false)]
pub hidden: bool,
#[arg(long, default_value_t = false)]
pub show_password: bool,
#[arg(long, value_enum, default_value_t = TextSide::Right)]
pub text_side: TextSide,
#[arg(long)]
pub label: Option<String>,
#[arg(long, default_value_t = false)]
pub border: bool,
#[arg(short, long, default_value = "4", value_parser = parse_density)]
pub density: Density,
#[arg(long)]
pub save: Option<PathBuf>,
#[arg(long, default_value_t = false)]
pub no_print: bool,
}
#[derive(Debug, clap::Args)]
pub struct DoctorCommand {
#[arg(short, long)]
pub addr: Option<String>,
#[arg(short = 'c', long, value_enum)]
pub conn: Option<ConnPref>,
#[arg(short, long, value_enum)]
pub model: Option<Model>,
#[arg(long, value_enum)]
pub task: Option<PrintTask>,
#[arg(short, long, default_value_t = 5, value_parser = parse_scan_seconds)]
pub seconds: u64,
#[arg(long, default_value_t = false)]
pub use_config: bool,
#[arg(long, default_value_t = false)]
pub fuzzy: bool,
#[arg(long, default_value_t = false)]
pub json: bool,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
Scan {
#[arg(short, long, default_value_t = 5, value_parser = parse_scan_seconds)]
seconds: u64,
#[arg(long, default_value_t = false)]
save: bool,
#[arg(long)]
name: Option<String>,
},
Ports,
Info {
#[command(flatten)]
conn: ConnArgs,
},
Identify {
#[command(flatten)]
conn: ConnArgs,
#[arg(long, default_value_t = false)]
json: bool,
},
Print(PrintCommand),
Calibrate(CalibrateCommand),
Text(TextCommand),
Qr(QrCommand),
Wifi(WifiCommand),
Fonts,
Tasks,
Config {
#[command(subcommand)]
action: ConfigCmd,
},
Doctor(DoctorCommand),
Encode {
cmd: String,
#[arg(default_value = "")]
data: String,
},
}
#[derive(Subcommand, Debug)]
pub enum ConfigCmd {
Show {
#[arg(long, default_value_t = false)]
json: bool,
},
Path,
Set {
#[arg(short, long)]
addr: Option<String>,
#[arg(short = 'c', long, value_enum)]
conn: Option<ConnPref>,
#[arg(short, long, value_enum)]
model: Option<Model>,
#[arg(long, value_parser = parse_scan_seconds)]
scan_secs: Option<u64>,
#[arg(short, long)]
label: Option<String>,
},
SafeArea {
#[arg(long, conflicts_with = "bottom")]
last_tick: Option<f64>,
#[arg(long)]
label: Option<String>,
#[arg(long)]
top: Option<f64>,
#[arg(long)]
bottom: Option<f64>,
#[arg(long)]
left: Option<f64>,
#[arg(long)]
right: Option<f64>,
#[arg(long, default_value_t = false)]
reset: bool,
},
Clear,
}