Krolyxon
Commit 8ff50e2

Branches

Refs →

budctlcratesbudctl-clisrcoutput.rs

247 lines · 7.3 KB · Rust

1//! Printing results, as text for people or as JSON (`--json`) for scripts and status bars.
2
3use std::io::{self, Write};
4
5use budctl_core::bluez::PairedDevice;
6use budctl_core::devices::Device;
7use budctl_core::status::{Charge, JSON_SCHEMA, NoiseControl, Status, Switch, TouchControls};
8use budctl_core::{Error, Found};
9use serde_json::Value;
10
11/// Why a command did not finish: budctl failed, or stdout did.
12pub enum Failure {
13 Budctl(Error),
14 Io(io::Error),
15}
16
17impl From<Error> for Failure {
18 fn from(error: Error) -> Self {
19 Failure::Budctl(error)
20 }
21}
22
23impl From<io::Error> for Failure {
24 fn from(error: io::Error) -> Self {
25 Failure::Io(error)
26 }
27}
28
29impl From<serde_json::Error> for Failure {
30 fn from(error: serde_json::Error) -> Self {
31 Failure::Io(io::Error::from(error))
32 }
33}
34
35/// Print `body` inside the JSON envelope every result shares: `schema`, plus `device` and
36/// `address` when the result is about one pair of buds.
37fn json(out: &mut impl Write, target: Option<(&PairedDevice, &dyn Device)>, body: Value) -> Result<(), Failure> {
38 let mut envelope = serde_json::Map::new();
39 envelope.insert("schema".to_owned(), JSON_SCHEMA.into());
40 if let Some((device, model)) = target {
41 envelope.insert("device".to_owned(), model.name().into());
42 envelope.insert("address".to_owned(), device.address.to_string().into());
43 }
44 if let Value::Object(fields) = body {
45 envelope.extend(fields);
46 }
47 serde_json::to_writer(&mut *out, &envelope)?;
48 writeln!(out)?;
49 Ok(())
50}
51
52pub fn list(out: &mut impl Write, found: &[Found], as_json: bool) -> Result<(), Failure> {
53 if as_json {
54 let devices: Vec<Value> = found
55 .iter()
56 .map(|f| {
57 serde_json::json!({
58 "address": f.device.address,
59 "name": f.device.name,
60 "alias": f.device.alias,
61 "connected": f.device.connected,
62 // The model budctl recognized, or null.
63 "model": f.model.map(|m| m.name()),
64 })
65 })
66 .collect();
67 return json(out, None, serde_json::json!({ "devices": devices }));
68 }
69
70 if found.is_empty() {
71 writeln!(out, "No paired Bluetooth devices.")?;
72 return Ok(());
73 }
74 for f in found {
75 let state = if f.device.connected {
76 "connected"
77 } else {
78 "not connected"
79 };
80 let model = match f.model {
81 Some(model) => format!("supported: {}", model.name()),
82 None => "not supported".to_owned(),
83 };
84 writeln!(out, "{} {} ({state}, {model})", f.device.address, f.device.alias)?;
85 }
86 Ok(())
87}
88
89fn charge(charge: Option<Charge>) -> String {
90 match charge {
91 Some(Charge { level, charging: true }) => format!("{level}% (charging)"),
92 Some(Charge { level, charging: false }) => format!("{level}%"),
93 None => "—".to_owned(),
94 }
95}
96
97fn header(out: &mut impl Write, device: &PairedDevice, model: &dyn Device) -> io::Result<()> {
98 writeln!(out, "{} ({})", model.name(), device.address)
99}
100
101fn warnings(out: &mut impl Write, warnings: &[String]) -> io::Result<()> {
102 for warning in warnings {
103 writeln!(out, " warning: {warning}")?;
104 }
105 Ok(())
106}
107
108pub fn status(
109 out: &mut impl Write,
110 device: &PairedDevice,
111 model: &dyn Device,
112 status: &Status,
113 as_json: bool,
114) -> Result<(), Failure> {
115 if as_json {
116 return json(out, Some((device, model)), serde_json::to_value(status)?);
117 }
118
119 header(out, device, model)?;
120 let battery = &status.battery;
121 let mut rows: Vec<(&str, String)> = vec![(
122 "Battery",
123 format!(
124 "L {} R {} case {}",
125 charge(battery.left),
126 charge(battery.right),
127 charge(battery.case)
128 ),
129 )];
130 if let Some(noise) = &status.noise_control {
131 let value = match &noise.level {
132 Some(level) => format!("{} ({level})", noise.mode),
133 None => noise.mode.clone(),
134 };
135 rows.push(("Noise control", value));
136 }
137 if let Some(eq) = &status.eq {
138 rows.push(("EQ", eq.clone()));
139 }
140 if let Some(bass) = status.bass_level {
141 rows.push(("Bass level", bass.to_string()));
142 }
143 for switch in &status.switches {
144 rows.push((switch.label, if switch.on { "on" } else { "off" }.to_owned()));
145 }
146 if let Some(codec) = &status.codec {
147 rows.push(("Codec", codec.clone()));
148 }
149 if let Some(firmware) = &status.firmware {
150 rows.push(("Firmware", firmware.clone()));
151 }
152 // Labels are as wide as the longest one, so values line up whatever the device names.
153 let width = rows.iter().map(|(label, _)| label.chars().count()).max().unwrap_or(0);
154 for (label, value) in &rows {
155 writeln!(out, " {label:<width$} {value}")?;
156 }
157 warnings(out, &status.warnings)?;
158 Ok(())
159}
160
161pub fn touch(
162 out: &mut impl Write,
163 device: &PairedDevice,
164 model: &dyn Device,
165 touch: &TouchControls,
166 as_json: bool,
167) -> Result<(), Failure> {
168 if as_json {
169 return json(out, Some((device, model)), serde_json::to_value(touch)?);
170 }
171
172 header(out, device, model)?;
173 if touch.gestures.is_empty() {
174 writeln!(out, " (no gestures reported)")?;
175 }
176 for g in &touch.gestures {
177 match g.group {
178 Some(group) => writeln!(out, " {:<6} {:<12} {} [group {group}]", g.side, g.gesture, g.action)?,
179 None => writeln!(out, " {:<6} {:<12} {}", g.side, g.gesture, g.action)?,
180 }
181 }
182 if let Some(cycle) = &touch.hold_cycle {
183 writeln!(out, " Hold cycles through: {}", cycle.join(" → "))?;
184 }
185 warnings(out, &touch.warnings)?;
186 Ok(())
187}
188
189/// The noise-control mode after a change, as the buds report it on reading back.
190pub fn noise_control(
191 out: &mut impl Write,
192 device: &PairedDevice,
193 model: &dyn Device,
194 now: &NoiseControl,
195 as_json: bool,
196) -> Result<(), Failure> {
197 if as_json {
198 return json(
199 out,
200 Some((device, model)),
201 serde_json::json!({ "noise_control": serde_json::to_value(now)? }),
202 );
203 }
204 header(out, device, model)?;
205 match &now.level {
206 Some(level) => writeln!(out, " Noise control {} ({level})", now.mode)?,
207 None => writeln!(out, " Noise control {}", now.mode)?,
208 }
209 Ok(())
210}
211
212/// One switch after a change, as the buds report it on reading back. JSON uses the same
213/// `switches` map as `status`.
214pub fn switch(
215 out: &mut impl Write,
216 device: &PairedDevice,
217 model: &dyn Device,
218 now: &Switch,
219 as_json: bool,
220) -> Result<(), Failure> {
221 if as_json {
222 return json(
223 out,
224 Some((device, model)),
225 serde_json::json!({ "switches": { now.key: now.on } }),
226 );
227 }
228 header(out, device, model)?;
229 writeln!(out, " {} {}", now.label, if now.on { "on" } else { "off" })?;
230 Ok(())
231}
232
233/// The EQ preset after a change, as the buds report it on reading back.
234pub fn eq(
235 out: &mut impl Write,
236 device: &PairedDevice,
237 model: &dyn Device,
238 now: &str,
239 as_json: bool,
240) -> Result<(), Failure> {
241 if as_json {
242 return json(out, Some((device, model)), serde_json::json!({ "eq": now }));
243 }
244 header(out, device, model)?;
245 writeln!(out, " EQ {now}")?;
246 Ok(())
247}