Krolyxon
Commit 56c56d4

Branches

Refs →

budctlcratesbudctl-guisrcbuds.rs

268 lines · 8.9 KB · Rust

1//! The buds the window shows: made-up ones for `--demo` (roughly what the owner's T310 reported
2//! at the last checkpoints), or an empty pair until the real buds answer.
3
4use budctl_core::status::{
5 Battery, Charge, EqBand, EqPreset, EqPresets, Gesture, GestureChoice, NoiseControl, Status, Switch, TouchControls,
6};
7
8/// The buds the window shows.
9pub struct Buds {
10 pub name: String,
11 pub connected: bool,
12 pub status: Status,
13 /// The EQ presets, when the buds list them.
14 pub eq: Option<EqPresets>,
15 /// The touch controls, when the buds report them.
16 pub touch: Option<TouchControls>,
17 /// The levels the bass level takes, when the buds have one to set.
18 pub bass_range: Option<(i8, i8)>,
19 /// The bass level last set from this window. The buds do not report it.
20 pub bass: Option<i8>,
21}
22
23impl Buds {
24 /// The name to show for the EQ preset in use.
25 pub fn eq_name(&self) -> String {
26 eq_name(self.status.eq.as_deref(), self.eq.as_ref())
27 }
28
29 /// Records that `now` is the EQ preset in use.
30 pub fn set_eq(&mut self, now: String) {
31 if let Some(eq) = &mut self.eq {
32 eq.current = Some(now.clone());
33 }
34 self.status.eq = Some(now);
35 }
36
37 /// Takes in a custom preset as the buds report it after a write: saved, and in use. A new
38 /// one joins the list.
39 pub fn set_custom(&mut self, preset: EqPreset) {
40 let now = key(&preset);
41 if let Some(eq) = self.eq.as_mut() {
42 match eq.presets.iter_mut().find(|p| p.custom && p.id == preset.id) {
43 Some(shown) => *shown = preset,
44 None => eq.presets.push(preset),
45 }
46 }
47 self.set_eq(now);
48 }
49
50 /// Takes in the whole preset list, as the buds report it.
51 pub fn set_presets(&mut self, eq: EqPresets) {
52 self.status.eq.clone_from(&eq.current);
53 self.eq = Some(eq);
54 }
55
56 /// The custom presets' names, as the buds store them.
57 pub fn custom_names(&self) -> impl Iterator<Item = (u8, &str)> {
58 self.eq
59 .iter()
60 .flat_map(|eq| eq.presets.iter())
61 .filter(|p| p.custom)
62 .map(|p| (p.id, p.name.as_str()))
63 }
64
65 /// The preset with this id.
66 pub fn preset(&self, id: u8) -> Option<&EqPreset> {
67 self.eq.as_ref()?.presets.iter().find(|p| p.id == id)
68 }
69
70 /// What a gesture of a bud (`left`, `right`) does now.
71 pub fn action(&self, side: &str, gesture: &str) -> Option<&str> {
72 self.touch
73 .as_ref()?
74 .gestures
75 .iter()
76 .find(|g| g.side == side && g.gesture == gesture && g.group.is_none())
77 .map(|g| g.action.as_str())
78 }
79}
80
81/// The made-up buds of `--demo`.
82pub fn demo() -> Buds {
83 let charge = |level| Some(Charge { level, charging: false });
84 let switch = |key, label, on| Switch { key, label, on };
85 let builtin = |id, name: &str| EqPreset {
86 name: name.to_owned(),
87 id,
88 custom: false,
89 bands: Vec::new(),
90 range: None,
91 };
92 let band = |hz, gain| EqBand { hz, gain };
93 let gesture = |side: &str, gesture: &str, action: &str| Gesture {
94 side: side.to_owned(),
95 gesture: gesture.to_owned(),
96 action: action.to_owned(),
97 group: None,
98 };
99 let choice = |gesture: &str, actions: &[&str]| GestureChoice {
100 gesture: gesture.to_owned(),
101 actions: actions.iter().map(|&a| a.to_owned()).collect(),
102 };
103 let names = |names: &[&str]| names.iter().map(|&n| n.to_owned()).collect::<Vec<_>>();
104 Buds {
105 name: "realme Buds T310".to_owned(),
106 connected: true,
107 status: Status {
108 battery: Battery {
109 left: charge(80),
110 right: charge(80),
111 case: None,
112 },
113 noise_control: Some(NoiseControl {
114 mode: "anc".to_owned(),
115 level: Some("deep".to_owned()),
116 }),
117 eq: Some("custom-4".to_owned()),
118 bass_level: None,
119 switches: vec![
120 switch("game", "Game mode", false),
121 switch("dual", "Dual connection", true),
122 switch("wind", "Wind-noise reduction", true),
123 switch("spatial", "3D sound", true),
124 switch("bass-boost", "Bass boost", true),
125 ],
126 codec: Some("SBC".to_owned()),
127 firmware: Some("1.1.0.43".to_owned()),
128 warnings: Vec::new(),
129 },
130 eq: Some(EqPresets {
131 current: Some("custom-4".to_owned()),
132 presets: vec![
133 builtin(0, "pure-raw"),
134 builtin(1, "deep-bass"),
135 builtin(2, "pure-vocals"),
136 builtin(3, "pure-bass"),
137 EqPreset {
138 name: "Custom1".to_owned(),
139 id: 4,
140 custom: true,
141 bands: vec![
142 band(50, 3),
143 band(250, 1),
144 band(1000, 0),
145 band(4000, -1),
146 band(8000, 1),
147 band(16000, 2),
148 ],
149 range: Some((-6, 6)),
150 },
151 ],
152 warnings: Vec::new(),
153 }),
154 touch: Some(TouchControls {
155 gestures: vec![
156 gesture("left", "double-tap", "play-pause"),
157 gesture("left", "triple-tap", "previous"),
158 gesture("left", "hold", "noise-cycle"),
159 gesture("right", "double-tap", "play-pause"),
160 gesture("right", "triple-tap", "next"),
161 gesture("right", "hold", "noise-cycle"),
162 ],
163 hold_cycle: Some(names(&["off", "anc"])),
164 choices: vec![
165 choice("double-tap", &["none", "play-pause", "next", "voice-assistant"]),
166 choice(
167 "triple-tap",
168 &[
169 "none",
170 "previous",
171 "next",
172 "voice-assistant",
173 "volume-up",
174 "volume-down",
175 "game-mode",
176 ],
177 ),
178 choice("hold", &["none", "noise-cycle", "voice-assistant", "game-mode"]),
179 ],
180 cycle_modes: names(&["off", "transparency", "anc"]),
181 warnings: Vec::new(),
182 }),
183 bass_range: Some((-5, 5)),
184 bass: None,
185 }
186}
187
188/// What the window shows before the real buds answer.
189pub fn unknown() -> Buds {
190 Buds {
191 name: "Your buds".to_owned(),
192 connected: false,
193 status: Status::default(),
194 eq: None,
195 touch: None,
196 bass_range: None,
197 bass: None,
198 }
199}
200
201/// The name a preset is selected by, as in [`Status::eq`]: a built-in one's own name,
202/// `custom-<id>` for a custom one.
203pub fn key(preset: &EqPreset) -> String {
204 if preset.custom {
205 format!("custom-{}", preset.id)
206 } else {
207 preset.name.clone()
208 }
209}
210
211/// A preset's name to show: a custom one's stored name, a built-in one's in words.
212pub fn shown_name(preset: &EqPreset) -> String {
213 if preset.custom {
214 preset.name.clone()
215 } else {
216 words(&preset.name)
217 }
218}
219
220/// The name to show for the EQ preset in use: a custom preset's own name (`custom-4` →
221/// "Custom1"), else the preset's name in words (`pure-bass` → "Pure bass").
222pub fn eq_name(current: Option<&str>, presets: Option<&EqPresets>) -> String {
223 let Some(current) = current else {
224 return "Unknown".to_owned();
225 };
226 presets
227 .and_then(|eq| eq.presets.iter().find(|p| key(p) == current))
228 .map_or_else(|| words(current), shown_name)
229}
230
231/// `pure-bass` → "Pure bass".
232pub fn words(name: &str) -> String {
233 let words = name.replace('-', " ");
234 let mut letters = words.chars();
235 letters
236 .next()
237 .map(|first| first.to_uppercase().chain(letters).collect())
238 .unwrap_or_default()
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn eq_names() {
247 let buds = demo();
248 assert_eq!(eq_name(Some("custom-4"), buds.eq.as_ref()), "Custom1");
249 assert_eq!(eq_name(Some("custom-4"), None), "Custom 4");
250 assert_eq!(eq_name(Some("pure-bass"), buds.eq.as_ref()), "Pure bass");
251 assert_eq!(eq_name(None, None), "Unknown");
252 assert_eq!(buds.eq_name(), "Custom1");
253 }
254
255 #[test]
256 fn eq_changes_show() {
257 let mut buds = demo();
258 buds.set_eq("deep-bass".to_owned());
259 assert_eq!(buds.eq_name(), "Deep bass");
260 assert_eq!(buds.eq.as_ref().unwrap().current.as_deref(), Some("deep-bass"));
261
262 let mut custom = buds.preset(4).unwrap().clone();
263 custom.bands[0].gain = -6;
264 buds.set_custom(custom);
265 assert_eq!(buds.status.eq.as_deref(), Some("custom-4"));
266 assert_eq!(buds.preset(4).unwrap().bands[0].gain, -6);
267 }
268}