Krolyxon
Commit 2476af5

Branches

Refs →

budctlcratesbudctl-guisrcbuds.rs

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