Krolyxon
Commit 84c42fc

Branches

Refs →

budctl84c42fc

budctl-gui: create and rename custom EQ presets

The Equalizer page gains "New preset" under the preset list and a
"Rename" button on each custom preset's card. Both open a dialog that
asks for the name; the keyboard goes to its field (Enter confirms, Esc
cancels), and the cursor reaches both buttons. The dialog refuses an
empty name, spaces at either end, control characters and a name
another preset has; what the buds themselves refuse (a name too long,
a fourth preset) comes back as an error.

Creating reads the whole list back, since ids can change, and the new
preset is put to use (W11c). Renaming keeps the preset's id and also
selects it (W11d). The worker gained Command::CreateEq and
Command::RenameEq.

No new dependencies.
Author
krolyxon
Date
2026-10-28 01:35 +0530
Parent
2476af5
Commit
84c42fc92d132cf0c52c5420b29509d5041f7b9b

+360 −18

5 files changed

  1. M crates/budctl-gui/src/buds.rs +22 −7
  2. M crates/budctl-gui/src/equalizer.rs +43 −6
  3. M crates/budctl-gui/src/main.rs +264 −2
  4. M crates/budctl-gui/src/ui.rs +14 −3
  5. M crates/budctl-gui/src/worker.rs +17 −0
M crates/budctl-gui/src/buds.rs View
@@ -34,19 +34,34 @@ impl Buds {
3434 self.status.eq = Some(now);
3535 }
3636
37− /// Takes in a custom preset as the buds report it after a write: saved, and in use.
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.
3839 pub fn set_custom(&mut self, preset: EqPreset) {
3940 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;
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+ }
4646 }
4747 self.set_eq(now);
4848 }
4949
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+
5065 /// The preset with this id.
5166 pub fn preset(&self, id: u8) -> Option<&EqPreset> {
5267 self.eq.as_ref()?.presets.iter().find(|p| p.id == id)
M crates/budctl-gui/src/equalizer.rs View
@@ -14,12 +14,14 @@ use crate::{Draft, Item, Marks, Message};
1414 const SAVING: &str = "Saving…";
1515
1616 /// What the keyboard cursor can rest on, top to bottom, as [`view`] shows them: each preset,
17−/// then each band of each custom preset.
17+/// "New preset", then for each custom preset "Rename" and its bands.
1818 pub fn items(buds: &Buds) -> Vec<Item> {
1919 let mut items = Vec::new();
2020 if let Some(eq) = &buds.eq {
2121 items.extend(eq.presets.iter().map(|p| Item::Preset(p.id)));
22+ items.push(Item::NewPreset);
2223 for preset in eq.presets.iter().filter(|p| p.custom) {
24+ items.push(Item::Rename(preset.id));
2325 items.extend(preset.bands.iter().map(|band| Item::Band(preset.id, band.hz)));
2426 }
2527 }
@@ -67,9 +69,35 @@ pub fn view<'a>(look: Look, buds: &'a Buds, draft: Option<&'a Draft>, marks: Mar
6769 eq.presets
6870 .iter()
6971 .filter(|p| p.custom)
70− .fold(column![title, ui::card(look, rows)].spacing(18), |page, preset| {
71− page.push(bands(look, preset, draft, marks))
72− })
72+ .fold(
73+ column![title, ui::card(look, rows), new_preset(look, marks)].spacing(18),
74+ |page, preset| page.push(bands(look, preset, draft, marks)),
75+ )
76+ .into()
77+}
78+
79+/// The button that creates a custom preset, and what it is doing.
80+fn new_preset<'a>(look: Look, marks: Marks) -> Element<'a, Message> {
81+ let creating = marks
82+ .pending
83+ .contains(&Item::NewPreset)
84+ .then(|| text("Creating…").size(13).color(look.p.icon));
85+ let about = text("A new preset starts flat, with the bands of your other presets, and is put to use.")
86+ .size(13)
87+ .color(look.p.dim)
88+ .width(Length::Fill);
89+ row![about]
90+ .push(creating)
91+ .push(ui::pill_with(
92+ look,
93+ "New preset",
94+ false,
95+ marks.focus == Some(Item::NewPreset),
96+ Message::NewPreset,
97+ ))
98+ .spacing(14)
99+ .align_y(Alignment::Center)
100+ .padding([0, 8])
73101 .into()
74102 }
75103
@@ -94,6 +122,13 @@ fn bands<'a>(look: Look, preset: &'a EqPreset, draft: Option<&'a Draft>, marks:
94122 .width(Length::Fill)
95123 ]
96124 .push(saving)
125+ .push(ui::pill_with(
126+ look,
127+ "Rename",
128+ false,
129+ marks.focus == Some(Item::Rename(id)),
130+ Message::RenamePreset(id),
131+ ))
97132 .spacing(10)
98133 .align_y(Alignment::Center);
99134 let sliders = gains.iter().fold(Row::new().spacing(6), |sliders, &band| {
@@ -168,9 +203,11 @@ mod tests {
168203 #[test]
169204 fn items_follow_the_page() {
170205 let items = items(&buds::demo());
171− assert_eq!(items.len(), 5 + 6);
206+ assert_eq!(items.len(), 5 + 1 + 1 + 6);
172207 assert_eq!(items.first(), Some(&Item::Preset(0)));
173− assert_eq!(items.get(5), Some(&Item::Band(4, 50)));
208+ assert_eq!(items.get(5), Some(&Item::NewPreset));
209+ assert_eq!(items.get(6), Some(&Item::Rename(4)));
210+ assert_eq!(items.get(7), Some(&Item::Band(4, 50)));
174211 assert_eq!(items.last(), Some(&Item::Band(4, 16000)));
175212 assert!(super::items(&buds::unknown()).is_empty());
176213 }
M crates/budctl-gui/src/main.rs View
@@ -24,10 +24,10 @@ use std::time::{Duration, Instant};
2424
2525 use budctl_core::Kind;
2626 use budctl_core::bt::BdAddr;
27−use budctl_core::status::{AncLevel, EqBand, NoiseControl, NoiseMode, Switch};
27+use budctl_core::status::{AncLevel, EqBand, EqPreset, NoiseControl, NoiseMode, Switch};
2828 use iced::keyboard::{self, Key, Modifiers};
2929 use iced::widget::operation::{self, RelativeOffset};
30−use iced::widget::{Column, column, container, mouse_area, opaque, row, scrollable, space, stack, text};
30+use iced::widget::{Column, column, container, mouse_area, opaque, row, scrollable, space, stack, text, text_input};
3131 use iced::{Alignment, Border, Color, Element, Font, Length, Size, Subscription, Task, Theme, event, mouse, window};
3232
3333 use icons::Icon;
@@ -46,6 +46,8 @@ Press ? in the window for the keys.";
4646
4747 /// The page's scrolling area, so the cursor can be kept in view.
4848 const SCROLL: &str = "page";
49+/// The name field of the preset-name dialog, so it can take the keyboard.
50+const NAME_INPUT: &str = "name";
4951
5052 /// When the window gets focus and the last reading is older than this, the buds are read again:
5153 /// the phone may have changed something meanwhile.
@@ -192,6 +194,26 @@ enum Item {
192194 Find,
193195 /// The bass level.
194196 Bass,
197+ /// "New preset", on the Equalizer page.
198+ NewPreset,
199+ /// "Rename", on a custom EQ preset's card.
200+ Rename(u8),
201+}
202+
203+/// The dialog that asks for a preset's name.
204+#[derive(Debug, Clone, PartialEq)]
205+struct NamePrompt {
206+ target: NameTarget,
207+ text: String,
208+ /// What is wrong with the name, after a try.
209+ problem: Option<String>,
210+}
211+
212+#[derive(Debug, Clone, Copy, PartialEq)]
213+enum NameTarget {
214+ New,
215+ /// A custom preset, by its id.
216+ Rename(u8),
195217 }
196218
197219 /// What a page needs besides the buds: the item under the keyboard cursor, if it shows, and the
@@ -268,6 +290,13 @@ enum Message {
268290 save: bool,
269291 },
270292 SaveBass,
293+ /// Ask for a new preset's name.
294+ NewPreset,
295+ /// Ask for a custom preset's (by its id) new name.
296+ RenamePreset(u8),
297+ NameInput(String),
298+ SubmitName,
299+ CancelName,
271300 ShowHelp,
272301 /// Read the buds again.
273302 Refresh,
@@ -317,6 +346,8 @@ struct App {
317346 confirm_find: bool,
318347 /// Why the palette file could not be used, if it could not.
319348 palette_problem: Option<String>,
349+ /// The preset-name dialog, when it is open.
350+ prompt: Option<NamePrompt>,
320351 }
321352
322353 impl App {
@@ -354,6 +385,7 @@ impl App {
354385 ringing: false,
355386 confirm_find: false,
356387 palette_problem: None,
388+ prompt: None,
357389 };
358390 app.refresh_palette();
359391 app
@@ -396,6 +428,19 @@ impl App {
396428 }
397429 }
398430 Message::SaveBass => self.save_bass(),
431+ Message::NewPreset => return self.ask_name(NameTarget::New, String::new()),
432+ Message::RenamePreset(id) => {
433+ let name = self.buds.preset(id).map(|p| p.name.clone()).unwrap_or_default();
434+ return self.ask_name(NameTarget::Rename(id), name);
435+ }
436+ Message::NameInput(text) => {
437+ if let Some(prompt) = &mut self.prompt {
438+ prompt.text = text;
439+ prompt.problem = None;
440+ }
441+ }
442+ Message::SubmitName => self.submit_name(),
443+ Message::CancelName => self.prompt = None,
399444 Message::Refresh => self.refresh(),
400445 Message::Worker(event) => self.answer(event),
401446 Message::Focused => {
@@ -455,6 +500,32 @@ impl App {
455500 self.buds.bass = Some(level);
456501 self.bass_draft = None;
457502 }
503+ Command::CreateEq(name) => {
504+ let customs = || {
505+ self.buds
506+ .eq
507+ .iter()
508+ .flat_map(|eq| eq.presets.iter())
509+ .filter(|p| p.custom)
510+ };
511+ let model = customs().next().cloned();
512+ let id = customs().map(|p| p.id).max().map_or(4, |id| id.saturating_add(1));
513+ if let Some(model) = model {
514+ self.buds.set_custom(EqPreset {
515+ name,
516+ id,
517+ custom: true,
518+ bands: model.bands.iter().map(|b| EqBand { hz: b.hz, gain: 0 }).collect(),
519+ range: model.range,
520+ });
521+ }
522+ }
523+ Command::RenameEq(_, name) => {
524+ if let Some(preset) = self.buds.preset(item_preset(item)) {
525+ let preset = EqPreset { name, ..preset.clone() };
526+ self.buds.set_custom(preset);
527+ }
528+ }
458529 Command::CustomEq(_, bands) => {
459530 if let Some(preset) = self.buds.preset(item_preset(item)) {
460531 let mut preset = preset.clone();
@@ -563,6 +634,11 @@ impl App {
563634 self.save_touch(row);
564635 }
565636 }
637+ Event::Presets(eq) => {
638+ self.in_flight.pop_front();
639+ self.buds.set_presets(eq);
640+ self.link = Link::Live;
641+ }
566642 Event::Bass(level) => {
567643 self.in_flight.pop_front();
568644 self.buds.bass = Some(level);
@@ -738,6 +814,59 @@ impl App {
738814 }
739815 }
740816
817+ /// Opens the preset-name dialog, with the keyboard in its field.
818+ fn ask_name(&mut self, target: NameTarget, text: String) -> Task<Message> {
819+ self.prompt = Some(NamePrompt {
820+ target,
821+ text,
822+ problem: None,
823+ });
824+ operation::focus(NAME_INPUT)
825+ }
826+
827+ /// Creates or renames the preset, if the name will do. What the buds themselves refuse (a
828+ /// name too long for them, a fourth preset) comes back as an error.
829+ fn submit_name(&mut self) {
830+ let Some(prompt) = self.prompt.clone() else {
831+ return;
832+ };
833+ let renaming = match prompt.target {
834+ NameTarget::Rename(id) => Some(id),
835+ NameTarget::New => None,
836+ };
837+ let taken = self
838+ .buds
839+ .custom_names()
840+ .any(|(id, name)| name == prompt.text && Some(id) != renaming);
841+ let problem = if prompt.text.is_empty() || prompt.text.trim() != prompt.text {
842+ Some("A name cannot be empty, or start or end with a space.".to_owned())
843+ } else if prompt.text.chars().any(char::is_control) {
844+ Some("A name cannot hold control characters.".to_owned())
845+ } else if taken {
846+ Some(format!("A preset is called {} already.", prompt.text))
847+ } else {
848+ None
849+ };
850+ if let Some(problem) = problem {
851+ self.prompt = Some(NamePrompt {
852+ problem: Some(problem),
853+ ..prompt
854+ });
855+ return;
856+ }
857+ self.prompt = None;
858+ match prompt.target {
859+ NameTarget::New => self.write(Item::NewPreset, Command::CreateEq(prompt.text)),
860+ NameTarget::Rename(id) => {
861+ let unchanged = self.buds.preset(id).is_some_and(|p| p.name == prompt.text);
862+ if let (false, Some(preset)) = (unchanged, self.buds.preset(id)) {
863+ let command = Command::RenameEq(buds::key(preset), prompt.text);
864+ self.write(Item::Preset(id), command);
865+ }
866+ }
867+ }
868+ }
869+
741870 /// Sets the bass level moved on the screen. If a level is on its way, this one goes out
742871 /// when that is answered: the latest level wins.
743872 fn save_bass(&mut self) {
@@ -764,6 +893,13 @@ impl App {
764893 }
765894
766895 fn key(&mut self, key: &Key, modifiers: Modifiers) -> Task<Message> {
896+ if self.prompt.is_some() {
897+ // The name field takes the keys, and Enter in it; only Esc is ours.
898+ if matches!(key, Key::Named(keyboard::key::Named::Escape)) {
899+ self.prompt = None;
900+ }
901+ return Task::none();
902+ }
767903 let Some(action) = self.keys.press(key, modifiers) else {
768904 return Task::none();
769905 };
@@ -920,6 +1056,8 @@ impl App {
9201056 })
9211057 }
9221058 Item::Find => (action != Action::Left).then_some(Message::Find),
1059+ Item::NewPreset => (action != Action::Left).then_some(Message::NewPreset),
1060+ Item::Rename(id) => (action != Action::Left).then_some(Message::RenamePreset(id)),
9231061 Item::Bass => {
9241062 let (low, high) = self.buds.bass_range?;
9251063 let now = self.bass_draft.or(self.buds.bass);
@@ -1067,6 +1205,8 @@ impl App {
10671205 stack![window, self.help()].into()
10681206 } else if self.confirm_find {
10691207 stack![window, self.confirm_find()].into()
1208+ } else if let Some(prompt) = &self.prompt {
1209+ stack![window, self.name_prompt(prompt)].into()
10701210 } else {
10711211 window.into()
10721212 }
@@ -1217,6 +1357,58 @@ impl App {
12171357 opaque(backdrop)
12181358 }
12191359
1360+ /// The dialog that asks for a preset's name, over a dimmed window.
1361+ fn name_prompt<'a>(&'a self, prompt: &'a NamePrompt) -> Element<'a, Message> {
1362+ let look = self.look;
1363+ let p = look.p;
1364+ let (heading, action) = match prompt.target {
1365+ NameTarget::New => ("New preset".to_owned(), "Create"),
1366+ NameTarget::Rename(id) => {
1367+ let name = self.buds.preset(id).map_or("the preset", |preset| preset.name.as_str());
1368+ (format!("Rename {name}"), "Rename")
1369+ }
1370+ };
1371+ let field = text_input("Name", &prompt.text)
1372+ .id(NAME_INPUT)
1373+ .on_input(Message::NameInput)
1374+ .on_submit(Message::SubmitName)
1375+ .size(16)
1376+ .padding([10, 14])
1377+ .style(move |_, _| text_input::Style {
1378+ background: p.surface_high.into(),
1379+ border: Border {
1380+ color: p.accent,
1381+ width: 1.0,
1382+ radius: 10.0.into(),
1383+ },
1384+ icon: p.dim,
1385+ placeholder: p.dim,
1386+ value: p.fg,
1387+ selection: p.accent_dim,
1388+ });
1389+ let problem = prompt
1390+ .problem
1391+ .as_deref()
1392+ .map(|problem| text(problem).size(13).color(p.err));
1393+ let buttons = row![
1394+ space::horizontal(),
1395+ ui::pill(look, "Cancel", false, Message::CancelName),
1396+ ui::pill(look, action, true, Message::SubmitName),
1397+ ]
1398+ .spacing(10);
1399+ let dialog = column![text(heading).size(22).font(look.bold()).color(p.fg), field]
1400+ .push(problem)
1401+ .push(buttons)
1402+ .spacing(16);
1403+ let backdrop = container(container(ui::panel(look, dialog.into())).width(460))
1404+ .center(Length::Fill)
1405+ .style(|_| container::Style {
1406+ background: Some(Color { a: 0.6, ..Color::BLACK }.into()),
1407+ ..container::Style::default()
1408+ });
1409+ opaque(backdrop)
1410+ }
1411+
12201412 /// The list of keys, over a dimmed window. A key or a click closes it.
12211413 fn help(&self) -> Element<'_, Message> {
12221414 let look = self.look;
@@ -1765,6 +1957,71 @@ mod tests {
17651957 assert!(commands.try_recv().is_err());
17661958 }
17671959
1960+ #[test]
1961+ fn presets_are_created_and_renamed() {
1962+ let mut app = app();
1963+ let _ = app.update(Message::NewPreset);
1964+ assert!(app.prompt.is_some());
1965+ // Typing goes to the field, not to the cursor.
1966+ press(&mut app, "j j");
1967+ assert_eq!(app.focus(), None);
1968+ let _ = app.update(Message::NameInput(" Gym".to_owned()));
1969+ let _ = app.update(Message::SubmitName);
1970+ assert!(app.prompt.as_ref().is_some_and(|p| p.problem.is_some()));
1971+ let _ = app.update(Message::NameInput("Custom1".to_owned()));
1972+ let _ = app.update(Message::SubmitName);
1973+ assert!(app.prompt.as_ref().is_some_and(|p| p.problem.is_some()), "taken");
1974+ let _ = app.update(Message::NameInput("Gym".to_owned()));
1975+ let _ = app.update(Message::SubmitName);
1976+ assert_eq!(app.prompt, None);
1977+ assert_eq!(app.buds.eq_name(), "Gym", "a new preset is put to use");
1978+ let gym = app
1979+ .buds
1980+ .custom_names()
1981+ .find(|(_, name)| *name == "Gym")
1982+ .map(|(id, _)| id)
1983+ .unwrap();
1984+
1985+ let _ = app.update(Message::RenamePreset(gym));
1986+ assert_eq!(app.prompt.as_ref().map(|p| p.text.as_str()), Some("Gym"));
1987+ let _ = app.update(Message::NameInput("Run".to_owned()));
1988+ let _ = app.update(Message::SubmitName);
1989+ assert_eq!(app.buds.preset(gym).map(|p| p.name.as_str()), Some("Run"));
1990+
1991+ let _ = app.update(Message::RenamePreset(gym));
1992+ press(&mut app, "Esc");
1993+ assert_eq!(app.prompt, None);
1994+ }
1995+
1996+ #[test]
1997+ fn live_presets_are_created_and_renamed_by_the_buds() {
1998+ let (mut app, commands) = live();
1999+ let _ = commands.try_recv();
2000+ let _ = app.update(Message::Worker(Event::Read(Box::new(reading()))));
2001+ let _ = app.update(Message::NewPreset);
2002+ let _ = app.update(Message::NameInput("Gym".to_owned()));
2003+ let _ = app.update(Message::SubmitName);
2004+ assert!(matches!(commands.try_recv(), Ok(Command::CreateEq(name)) if name == "Gym"));
2005+ assert_eq!(app.pending(), [Item::NewPreset]);
2006+ let mut presets = buds::demo().eq.unwrap();
2007+ let mut gym = presets.presets.last().unwrap().clone();
2008+ gym.id = 5;
2009+ gym.name = "Gym".to_owned();
2010+ presets.presets.push(gym);
2011+ presets.current = Some("custom-5".to_owned());
2012+ let _ = app.update(Message::Worker(Event::Presets(presets)));
2013+ assert_eq!(app.buds.eq_name(), "Gym");
2014+ assert!(app.pending().is_empty());
2015+
2016+ let _ = app.update(Message::RenamePreset(4));
2017+ let _ = app.update(Message::NameInput("Run".to_owned()));
2018+ let _ = app.update(Message::SubmitName);
2019+ assert!(matches!(
2020+ commands.try_recv(),
2021+ Ok(Command::RenameEq(preset, name)) if preset == "custom-4" && name == "Run"
2022+ ));
2023+ }
2024+
17682025 #[test]
17692026 fn steps() {
17702027 let abc = ['a', 'b', 'c'];
@@ -1824,6 +2081,11 @@ mod tests {
18242081 1340.0,
18252082 on(Page::Overview, "j j j j j j l l"),
18262083 ));
2084+ let mut naming = on(Page::Equalizer, "");
2085+ let _ = naming.update(Message::RenamePreset(4));
2086+ let _ = naming.update(Message::NameInput("Custom1 ".to_owned()));
2087+ let _ = naming.update(Message::SubmitName);
2088+ shots.push(("name-prompt".to_owned(), 800.0, naming));
18272089 let mut connecting = App::new(palette.clone(), font, None, false);
18282090 connecting.link = Link::Connecting;
18292091 shots.push(("connecting".to_owned(), 800.0, connecting));
M crates/budctl-gui/src/ui.rs View
@@ -237,6 +237,17 @@ pub fn nav_item<'a>(look: Look, icon: Icon, label: &'a str, open: bool, on_press
237237
238238 /// A small rounded button: outlined, or `filled` with the accent.
239239 pub fn pill<'a>(look: Look, label: &'a str, filled: bool, on_press: Message) -> Element<'a, Message> {
240+ pill_with(look, label, filled, false, on_press)
241+}
242+
243+/// A [`pill`] that can carry the keyboard cursor.
244+pub fn pill_with<'a>(
245+ look: Look,
246+ label: &'a str,
247+ filled: bool,
248+ focused: bool,
249+ on_press: Message,
250+) -> Element<'a, Message> {
240251 let p = look.p;
241252 let color = if filled { p.accent_fg } else { p.fg };
242253 button(text(label).size(13.5).font(look.medium()).color(color))
@@ -246,12 +257,12 @@ pub fn pill<'a>(look: Look, label: &'a str, filled: bool, on_press: Message) ->
246257 background: if filled {
247258 Some(p.accent.into())
248259 } else {
249− hovered(status).then_some(p.surface_high.into())
260+ (focused || hovered(status)).then_some(p.surface_high.into())
250261 },
251262 text_color: color,
252263 border: Border {
253− color: if filled { p.accent } else { p.line },
254− width: 1.0,
264+ color: if filled || focused { p.accent } else { p.line },
265+ width: if focused { 2.0 } else { 1.0 },
255266 radius: 999.0.into(),
256267 },
257268 ..button::Style::default()
M crates/budctl-gui/src/worker.rs View
@@ -48,6 +48,10 @@ pub enum Command {
4848 Find(bool),
4949 /// Set the bass level.
5050 Bass(i8),
51+ /// Create a custom EQ preset with this name.
52+ CreateEq(String),
53+ /// Give a custom EQ preset (`custom-<id>`) a new name.
54+ RenameEq(String, String),
5155 }
5256
5357 #[derive(Debug, Clone)]
@@ -74,6 +78,8 @@ pub enum Event {
7478 Ringing(bool),
7579 /// The answer to [`Command::Bass`]: the level the buds took.
7680 Bass(i8),
81+ /// The answer to [`Command::CreateEq`]: the whole preset list after it (ids can change).
82+ Presets(EqPresets),
7783 /// The answer to a command that failed. The session is closed; the next command opens a
7884 /// new one.
7985 Failed { kind: Kind, message: String },
@@ -213,6 +219,17 @@ impl Worker {
213219 .session()
214220 .and_then(|session| session.set_touch(&side, &gesture, &action, false))
215221 .map(|change| Event::Touch(change.gestures)),
222+ Command::CreateEq(name) => self
223+ .session()
224+ .and_then(|session| {
225+ session.create_custom_eq(&name)?;
226+ session.eq_presets()
227+ })
228+ .map(Event::Presets),
229+ Command::RenameEq(preset, name) => self
230+ .session()
231+ .and_then(|session| session.rename_custom_eq(&preset, &name))
232+ .map(Event::CustomEq),
216233 Command::Bass(level) => self
217234 .session()
218235 .and_then(|session| session.set_bass_level(level))