| 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 | |
| 4 | use budctl_core::status::{ |
| 5 | Battery, Charge, EqBand, EqPreset, EqPresets, Gesture, GestureChoice, NoiseControl, Status, Switch, TouchControls, |
| 6 | }; |
| 7 | |
| 8 | /// The buds the window shows. |
| 9 | pub 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 | |
| 19 | impl 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`. |
| 63 | pub 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. |
| 168 | pub 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. |
| 180 | pub 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. |
| 189 | pub 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"). |
| 199 | pub 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". |
| 209 | pub 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)] |
| 219 | mod 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 | } |