use bcms::config::Config;
use bcms::devices::WorkingMode;
use bcms::rpc::RpcClient;
use bcms::Result;
use clap::{Parser, Subcommand};
use std::io::Write;
use std::net::SocketAddr;
#[derive(Parser, Debug)]
#[command(name = "bcms")]
#[command(about = "Bluetooth Client Manager Service CLI")]
struct Args {
#[arg(long, default_value = "127.0.0.1:4567")]
server: String,
#[arg(long, short)]
verbose: bool,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
enum Commands {
List {
#[arg(long)]
only_approved: bool,
},
Approve {
#[arg(long)]
address: String,
},
Remove {
#[arg(long)]
address: String,
},
Mode,
SetMode {
#[arg(value_enum)]
mode: CliWorkingMode,
},
Pair {
#[arg(long)]
address: String,
},
Unpair {
#[arg(long)]
address: String,
},
IsPaired {
#[arg(long)]
address: String,
},
}
#[derive(clap::ValueEnum, Clone, Debug)]
enum CliWorkingMode {
Any,
Approved,
}
impl From<CliWorkingMode> for WorkingMode {
fn from(mode: CliWorkingMode) -> Self {
match mode {
CliWorkingMode::Any => WorkingMode::Any,
CliWorkingMode::Approved => WorkingMode::Approved,
}
}
}
impl From<WorkingMode> for CliWorkingMode {
fn from(mode: WorkingMode) -> Self {
match mode {
WorkingMode::Any => CliWorkingMode::Any,
WorkingMode::Approved => CliWorkingMode::Approved,
}
}
}
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
if args.verbose || std::env::var("RUST_LOG").is_ok() {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env()
.add_directive("bcms=debug".parse().unwrap()))
.init();
}
let server_addr: SocketAddr = args.server.parse()
.map_err(|e| bcms::Error::Config(format!("Invalid server address: {}", e)))?;
let client = RpcClient::new(server_addr);
match args.command {
Commands::List { only_approved } => {
let (devices, errors) = client.list_devices(only_approved).await?;
if !errors.is_empty() {
eprintln!("Errors:");
for error in errors {
eprintln!(" {}", error);
}
}
if devices.is_empty() {
println!("No devices found.");
} else {
println!("Found {} device{}:", devices.len(), if devices.len() == 1 { "" } else { "s" });
println!();
let max_name_width = devices.iter().map(|d| d.name.len()).max().unwrap_or(4).max(4);
let max_address_width = devices.iter().map(|d| d.address.len()).max().unwrap_or(7).max(7);
let approved_width = 8; let paired_width = 6;
println!("โโ{}โโฌโ{}โโฌโ{}โโฌโ{}โโ",
"โ".repeat(max_name_width),
"โ".repeat(max_address_width),
"โ".repeat(approved_width),
"โ".repeat(paired_width)
);
println!("โ {:width_name$} โ {:width_addr$} โ {:width_approved$} โ {:width_paired$} โ",
"Name", "Address", "Approved", "Paired",
width_name = max_name_width,
width_addr = max_address_width,
width_approved = approved_width,
width_paired = paired_width
);
println!("โโ{}โโผโ{}โโผโ{}โโผโ{}โโค",
"โ".repeat(max_name_width),
"โ".repeat(max_address_width),
"โ".repeat(approved_width),
"โ".repeat(paired_width)
);
for device in devices {
let approved_symbol = if device.approved { "โ" } else { "โ" };
let paired_symbol = if device.paired { "โ" } else { "โ" };
println!("โ {:width_name$} โ {:width_addr$} โ {:^width_approved$} โ {:^width_paired$} โ",
device.name,
device.address,
approved_symbol,
paired_symbol,
width_name = max_name_width,
width_addr = max_address_width,
width_approved = approved_width,
width_paired = paired_width
);
}
println!("โโ{}โโดโ{}โโดโ{}โโดโ{}โโ",
"โ".repeat(max_name_width),
"โ".repeat(max_address_width),
"โ".repeat(approved_width),
"โ".repeat(paired_width)
);
println!();
println!("Legend: โ Yes โ No");
}
}
Commands::Approve { address } => {
let (success, errors) = client.approve_device(&address).await?;
if !errors.is_empty() {
eprintln!("Errors:");
for error in errors {
eprintln!(" {}", error);
}
}
if success {
println!("โ Device approved: {}", address);
} else {
println!("โ Failed to approve device: {}", address);
}
}
Commands::Remove { address } => {
let (success, errors) = client.remove_device(&address).await?;
if !errors.is_empty() {
eprintln!("Errors:");
for error in errors {
eprintln!(" {}", error);
}
}
if success {
println!("โ Device removed: {}", address);
} else {
println!("โ Failed to remove device: {}", address);
}
}
Commands::Mode => {
let mode = client.get_mode().await?;
let (symbol, mode_str, description) = match mode {
WorkingMode::Any => ("๐", "any", "connects to all discovered devices"),
WorkingMode::Approved => ("๐", "approved", "connects only to approved devices"),
};
println!("Current working mode: {} {} ({})", symbol, mode_str, description);
}
Commands::SetMode { mode } => {
let working_mode = WorkingMode::from(mode.clone());
let (success, errors) = client.set_mode(working_mode).await?;
if !errors.is_empty() {
eprintln!("Errors:");
for error in errors {
eprintln!(" {}", error);
}
}
if success {
let (symbol, mode_str) = match mode {
CliWorkingMode::Any => ("๐", "any"),
CliWorkingMode::Approved => ("๐", "approved"),
};
println!("โ Working mode set to: {} {}", symbol, mode_str);
} else {
println!("โ Failed to set working mode");
}
}
Commands::Pair { address } => {
println!("๐ Attempting to pair with device: {}", address);
let (initial_success, errors) = client.pair_device(&address).await?;
if !errors.is_empty() {
eprintln!("Errors:");
for error in errors {
eprintln!(" {}", error);
}
}
if !initial_success {
println!("โ Failed to initiate pairing with device: {}", address);
return Ok(());
}
println!("โณ Waiting for device to pair...");
let spinner_chars = ['โ ', 'โ ', 'โ น', 'โ ธ', 'โ ผ', 'โ ด', 'โ ฆ', 'โ ง', 'โ ', 'โ '];
let mut spinner_index = 0;
let mut attempts = 0;
const MAX_ATTEMPTS: u32 = 60;
loop {
let (is_paired, _) = client.is_device_paired(&address).await?;
if is_paired {
print!("\rโ Device successfully paired: {} \n", address);
break;
}
attempts += 1;
if attempts >= MAX_ATTEMPTS {
print!("\rโ Pairing timeout after {}s - device may still be pairing in background \n", MAX_ATTEMPTS);
println!(" ๐ก Try running 'bcms list' to check if the device paired successfully");
println!(" ๐ก Some devices require manual confirmation or take longer to pair");
break;
}
let progress_bar = "=".repeat((attempts * 20 / MAX_ATTEMPTS) as usize);
let remaining_bar = " ".repeat(20 - (attempts * 20 / MAX_ATTEMPTS) as usize);
print!("\r{} Pairing in progress... [{}{}] {}s",
spinner_chars[spinner_index], progress_bar, remaining_bar, attempts);
std::io::stdout().flush().unwrap();
spinner_index = (spinner_index + 1) % spinner_chars.len();
tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
}
}
Commands::Unpair { address } => {
let (success, errors) = client.unpair_device(&address).await?;
if !errors.is_empty() {
eprintln!("Errors:");
for error in errors {
eprintln!(" {}", error);
}
}
if success {
println!("โ Device unpaired: {}", address);
} else {
println!("โ Failed to unpair device: {}", address);
}
}
Commands::IsPaired { address } => {
let (is_paired, errors) = client.is_device_paired(&address).await?;
if !errors.is_empty() {
eprintln!("Errors:");
for error in errors {
eprintln!(" {}", error);
}
}
if is_paired {
println!("โ Device {} is paired", address);
} else {
println!("ยท Device {} is not paired", address);
}
}
}
Ok(())
}