Krolyxon
Commit 47dc78a

Branches

Refs →

budctlcratesbudctl-guisrcbuds.rs

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