Krolyxon
Commit 9f993c9

Branches

Refs →

budctlcratesbudctl-clisrcmain.rs

139 lines · 4.1 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::status::{AncLevel, NoiseMode};
13use budctl_core::{Error, Kind};
14use clap::{Parser, Subcommand, ValueEnum};
15
16/// Control wireless earbuds from Linux, without the vendor app.
17#[derive(Parser)]
18#[command(name = "budctl", version)]
19struct Cli {
20 /// The buds to use, when more than one supported pair is paired.
21 #[arg(long, global = true, value_name = "MAC")]
22 device: Option<BdAddr>,
23
24 /// Machine-readable output (JSON with "schema": 1).
25 #[arg(long, global = true)]
26 json: bool,
27
28 /// Hex-dump every frame sent and received on stderr.
29 #[arg(long, global = true)]
30 debug: bool,
31
32 #[command(subcommand)]
33 command: Command,
34}
35
36#[derive(Subcommand)]
37enum Command {
38 /// Paired devices and whether budctl recognizes them.
39 List,
40 /// Battery, noise control, EQ and switches.
41 Status,
42 /// Touch controls (gesture table) of each bud.
43 Touch,
44 /// Set noise control, then read it back.
45 Anc {
46 #[arg(value_enum)]
47 mode: AncArg,
48 },
49}
50
51#[derive(Clone, Copy, ValueEnum)]
52enum AncArg {
53 Off,
54 Transparency,
55 /// Noise cancelling at the deep level (the buds use the same value for both).
56 On,
57 Light,
58 Medium,
59 Deep,
60}
61
62impl From<AncArg> for NoiseMode {
63 fn from(arg: AncArg) -> Self {
64 match arg {
65 AncArg::Off => NoiseMode::Off,
66 AncArg::On | AncArg::Deep => NoiseMode::Anc(AncLevel::Deep),
67 AncArg::Light => NoiseMode::Anc(AncLevel::Light),
68 AncArg::Medium => NoiseMode::Anc(AncLevel::Medium),
69 AncArg::Transparency => NoiseMode::Transparency,
70 }
71 }
72}
73
74fn main() -> ExitCode {
75 let cli = match Cli::try_parse() {
76 Ok(cli) => cli,
77 Err(error) => {
78 // Help and version are not errors; everything else is a usage error (exit 1).
79 let code = if error.use_stderr() { 1 } else { 0 };
80 // Nothing useful can be done if printing the usage message fails.
81 let _ = error.print();
82 return ExitCode::from(code);
83 }
84 };
85 if cli.debug {
86 budctl_core::debug::enable();
87 }
88
89 let mut stdout = io::stdout().lock();
90 let result = run(&cli, &mut stdout).and_then(|()| stdout.flush().map_err(output::Failure::Io));
91 match result {
92 Ok(()) => ExitCode::SUCCESS,
93 // `budctl status | head -1` is fine.
94 Err(output::Failure::Io(e)) if e.kind() == io::ErrorKind::BrokenPipe => ExitCode::SUCCESS,
95 Err(output::Failure::Io(e)) => {
96 eprintln!("budctl: cannot write output: {e}");
97 ExitCode::from(1)
98 }
99 Err(output::Failure::Budctl(e)) => {
100 eprintln!("budctl: {e}");
101 ExitCode::from(exit_code(&e))
102 }
103 }
104}
105
106fn run(cli: &Cli, out: &mut impl Write) -> Result<(), output::Failure> {
107 let found = budctl_core::scan()?;
108 if let Command::List = cli.command {
109 return output::list(out, &found, cli.json);
110 }
111
112 let (device, model) = budctl_core::select(found, cli.device)?;
113 let mut session = budctl_core::connect(&device, model)?;
114 match cli.command {
115 Command::List => Ok(()),
116 Command::Status => {
117 let status = session.status()?;
118 output::status(out, &device, model, &status, cli.json)
119 }
120 Command::Touch => {
121 let touch = session.touch_controls()?;
122 output::touch(out, &device, model, &touch, cli.json)
123 }
124 Command::Anc { mode } => {
125 let now = session.set_noise_control(mode.into())?;
126 output::noise_control(out, &device, model, &now, cli.json)
127 }
128 }
129}
130
131/// Exit codes from CLAUDE.md: 1 usage, 2 not found, 3 busy, 4 rejected or timed out.
132fn exit_code(error: &Error) -> u8 {
133 match error.kind() {
134 Kind::Usage => 1,
135 Kind::NotFound => 2,
136 Kind::Busy => 3,
137 Kind::Failed => 4,
138 }
139}