Krolyxon
Commit 90075f3

Branches

Refs →

budctlcratesbudctl-clisrcmain.rs

106 lines · 3.2 KB · Rust

1//! `budctl`: control wireless earbuds from Linux.
2//!
3//! A thin layer over budctl-core: parse the command line, run one command, print the result,
4//! turn errors into the exit codes listed in CLAUDE.md.
5
6mod output;
7
8use std::io::{self, Write};
9use std::process::ExitCode;
10
11use budctl_core::bt::BdAddr;
12use budctl_core::{Error, Kind};
13use clap::{Parser, Subcommand};
14
15/// Control wireless earbuds from Linux, without the vendor app.
16#[derive(Parser)]
17#[command(name = "budctl", version)]
18struct Cli {
19 /// The buds to use, when more than one supported pair is paired.
20 #[arg(long, global = true, value_name = "MAC")]
21 device: Option<BdAddr>,
22
23 /// Machine-readable output (JSON with "schema": 1).
24 #[arg(long, global = true)]
25 json: bool,
26
27 /// Hex-dump every frame sent and received on stderr.
28 #[arg(long, global = true)]
29 debug: bool,
30
31 #[command(subcommand)]
32 command: Command,
33}
34
35#[derive(Subcommand)]
36enum Command {
37 /// Paired devices and whether budctl recognizes them.
38 List,
39 /// Battery, noise control, EQ and switches.
40 Status,
41 /// Touch controls (gesture table) of each bud.
42 Touch,
43}
44
45fn main() -> ExitCode {
46 let cli = match Cli::try_parse() {
47 Ok(cli) => cli,
48 Err(error) => {
49 // Help and version are not errors; everything else is a usage error (exit 1).
50 let code = if error.use_stderr() { 1 } else { 0 };
51 // Nothing useful can be done if printing the usage message fails.
52 let _ = error.print();
53 return ExitCode::from(code);
54 }
55 };
56 if cli.debug {
57 budctl_core::debug::enable();
58 }
59
60 let mut stdout = io::stdout().lock();
61 let result = run(&cli, &mut stdout).and_then(|()| stdout.flush().map_err(output::Failure::Io));
62 match result {
63 Ok(()) => ExitCode::SUCCESS,
64 // `budctl status | head -1` is fine.
65 Err(output::Failure::Io(e)) if e.kind() == io::ErrorKind::BrokenPipe => ExitCode::SUCCESS,
66 Err(output::Failure::Io(e)) => {
67 eprintln!("budctl: cannot write output: {e}");
68 ExitCode::from(1)
69 }
70 Err(output::Failure::Budctl(e)) => {
71 eprintln!("budctl: {e}");
72 ExitCode::from(exit_code(&e))
73 }
74 }
75}
76
77fn run(cli: &Cli, out: &mut impl Write) -> Result<(), output::Failure> {
78 let found = budctl_core::scan()?;
79 if let Command::List = cli.command {
80 return output::list(out, &found, cli.json);
81 }
82
83 let (device, model) = budctl_core::select(found, cli.device)?;
84 let mut session = budctl_core::connect(&device, model)?;
85 match cli.command {
86 Command::List => Ok(()),
87 Command::Status => {
88 let status = session.status()?;
89 output::status(out, &device, model, &status, cli.json)
90 }
91 Command::Touch => {
92 let touch = session.touch_controls()?;
93 output::touch(out, &device, model, &touch, cli.json)
94 }
95 }
96}
97
98/// Exit codes from CLAUDE.md: 1 usage, 2 not found, 3 busy, 4 rejected or timed out.
99fn exit_code(error: &Error) -> u8 {
100 match error.kind() {
101 Kind::Usage => 1,
102 Kind::NotFound => 2,
103 Kind::Busy => 3,
104 Kind::Failed => 4,
105 }
106}