From 0a1a9bdd4d959acf58d0e01eda7314b4c41a6224 Mon Sep 17 00:00:00 2001 From: Thomas Frans Date: Fri, 27 Oct 2023 17:03:06 +0200 Subject: [PATCH] refactor: tabs rewrite to clean up API Tabs relied heavily on `ViewExt`'s `title()` function, while also requiring a separate `id`. The `id`, if used, should be an internal part of the struct and not part of its API. This also removes the hashmap as it will never be faster than sequentially looking up all the names, since there will most likely never be that many tabs in a `TabbedView`. --- src/traits.rs | 76 ++++++++++++++ src/ui/album.rs | 33 +++--- src/ui/artist.rs | 29 ++---- src/ui/library.rs | 32 +++--- src/ui/mod.rs | 2 +- src/ui/search.rs | 2 +- src/ui/search_results.rs | 44 ++++---- src/ui/tabbedview.rs | 217 +++++++++++++++++++++++++++++++++++++++ src/ui/tabview.rs | 169 ------------------------------ 9 files changed, 358 insertions(+), 246 deletions(-) create mode 100644 src/ui/tabbedview.rs delete mode 100644 src/ui/tabview.rs diff --git a/src/traits.rs b/src/traits.rs index 7d6a808..ba4c1cf 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -104,3 +104,79 @@ impl IntoBoxedViewExt for V { Box::new(self) } } + +pub struct BoxedViewExt { + boxed_view: Box, +} + +impl BoxedViewExt { + pub fn new(view: Box) -> Self { + Self { boxed_view: view } + } +} + +impl View for BoxedViewExt { + fn draw(&self, printer: &cursive::Printer) { + self.boxed_view.draw(printer); + } + + fn layout(&mut self, xy: cursive::Vec2) { + self.boxed_view.layout(xy); + } + + fn needs_relayout(&self) -> bool { + self.boxed_view.needs_relayout() + } + + fn required_size(&mut self, constraint: cursive::Vec2) -> cursive::Vec2 { + self.boxed_view.required_size(constraint) + } + + fn on_event(&mut self, event: cursive::event::Event) -> cursive::event::EventResult { + self.boxed_view.on_event(event) + } + + fn call_on_any(&mut self, selector: &cursive::view::Selector, callback: cursive::event::AnyCb) { + self.boxed_view.call_on_any(selector, callback); + } + + fn focus_view( + &mut self, + selector: &cursive::view::Selector, + ) -> Result { + self.boxed_view.focus_view(selector) + } + + fn take_focus( + &mut self, + source: cursive::direction::Direction, + ) -> Result { + self.boxed_view.take_focus(source) + } + + fn important_area(&self, view_size: cursive::Vec2) -> cursive::Rect { + self.boxed_view.important_area(view_size) + } + + fn type_name(&self) -> &'static str { + std::any::type_name::() + } +} + +impl ViewExt for BoxedViewExt { + fn title(&self) -> String { + self.boxed_view.title() + } + + fn title_sub(&self) -> String { + self.boxed_view.title_sub() + } + + fn on_leave(&self) { + self.boxed_view.on_leave(); + } + + fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result { + self.boxed_view.on_command(s, cmd) + } +} diff --git a/src/ui/album.rs b/src/ui/album.rs index ac884f4..8e8480d 100644 --- a/src/ui/album.rs +++ b/src/ui/album.rs @@ -11,11 +11,11 @@ use crate::model::artist::Artist; use crate::queue::Queue; use crate::traits::ViewExt; use crate::ui::listview::ListView; -use crate::ui::tabview::TabView; +use crate::ui::tabbedview::TabbedView; pub struct AlbumView { album: Album, - tabs: TabView, + tabs: TabbedView, } impl AlbumView { @@ -37,27 +37,26 @@ impl AlbumView { .map(|(id, name)| Artist::new(id.clone(), name.clone())) .collect(); - let tabs = TabView::new() - .tab( - "tracks", - ListView::new( - Arc::new(RwLock::new(tracks)), - queue.clone(), - library.clone(), - ) - .with_title("Tracks"), - ) - .tab( - "artists", - ListView::new(Arc::new(RwLock::new(artists)), queue, library).with_title("Artists"), - ); + let mut tabs = TabbedView::new(); + tabs.add_tab( + "Tracks", + ListView::new( + Arc::new(RwLock::new(tracks)), + queue.clone(), + library.clone(), + ), + ); + tabs.add_tab( + "Artists", + ListView::new(Arc::new(RwLock::new(artists)), queue, library), + ); Self { album, tabs } } } impl ViewWrapper for AlbumView { - wrap_impl!(self.tabs: TabView); + wrap_impl!(self.tabs: TabbedView); } impl ViewExt for AlbumView { diff --git a/src/ui/artist.rs b/src/ui/artist.rs index 95c3574..8b1d537 100644 --- a/src/ui/artist.rs +++ b/src/ui/artist.rs @@ -14,11 +14,11 @@ use crate::model::track::Track; use crate::queue::Queue; use crate::traits::ViewExt; use crate::ui::listview::ListView; -use crate::ui::tabview::TabView; +use crate::ui::tabbedview::TabbedView; pub struct ArtistView { artist: Artist, - tabs: TabView, + tabs: TabbedView, } impl ArtistView { @@ -61,34 +61,27 @@ impl ArtistView { }); } - let mut tabs = TabView::new(); + let mut tabs = TabbedView::new(); if let Some(tracks) = artist.tracks.as_ref() { let tracks = tracks.clone(); tabs.add_tab( - "tracks", + "Saved Tracks", ListView::new( Arc::new(RwLock::new(tracks)), queue.clone(), library.clone(), - ) - .with_title("Saved Tracks"), + ), ); } - tabs.add_tab( - "top_tracks", - ListView::new(top_tracks, queue.clone(), library.clone()).with_title("Top 10"), - ); - - tabs.add_tab("albums", albums_view.with_title("Albums")); - tabs.add_tab("singles", singles_view.with_title("Singles")); - - tabs.add_tab( - "related", - ListView::new(related, queue, library).with_title("Related Artists"), + "Top 10", + ListView::new(top_tracks, queue.clone(), library.clone()), ); + tabs.add_tab("Albums", albums_view); + tabs.add_tab("Singles", singles_view); + tabs.add_tab("Related Artists", ListView::new(related, queue, library)); Self { artist: artist.clone(), @@ -116,7 +109,7 @@ impl ArtistView { } impl ViewWrapper for ArtistView { - wrap_impl!(self.tabs: TabView); + wrap_impl!(self.tabs: TabbedView); } impl ViewExt for ArtistView { diff --git a/src/ui/library.rs b/src/ui/library.rs index cb3b49e..3a63a6f 100644 --- a/src/ui/library.rs +++ b/src/ui/library.rs @@ -13,16 +13,16 @@ use crate::traits::ViewExt; use crate::ui::browse::BrowseView; use crate::ui::listview::ListView; use crate::ui::playlists::PlaylistsView; -use crate::ui::tabview::TabView; +use crate::ui::tabbedview::TabbedView; pub struct LibraryView { - tabs: TabView, + tabs: TabbedView, display_name: Option, } impl LibraryView { pub fn new(queue: Arc, library: Arc) -> Self { - let mut tabview = TabView::new(); + let mut tabview = TabbedView::new(); let selected_tabs = library .cfg .values() @@ -33,31 +33,27 @@ impl LibraryView { for tab in selected_tabs { match tab { LibraryTab::Tracks => tabview.add_tab( - "tracks", - ListView::new(library.tracks.clone(), queue.clone(), library.clone()) - .with_title("Tracks"), + "Tracks", + ListView::new(library.tracks.clone(), queue.clone(), library.clone()), ), LibraryTab::Albums => tabview.add_tab( - "albums", - ListView::new(library.albums.clone(), queue.clone(), library.clone()) - .with_title("Albums"), + "Albums", + ListView::new(library.albums.clone(), queue.clone(), library.clone()), ), LibraryTab::Artists => tabview.add_tab( - "artists", - ListView::new(library.artists.clone(), queue.clone(), library.clone()) - .with_title("Artists"), + "Artists", + ListView::new(library.artists.clone(), queue.clone(), library.clone()), ), LibraryTab::Playlists => tabview.add_tab( - "playlists", + "Playlists", PlaylistsView::new(queue.clone(), library.clone()), ), LibraryTab::Podcasts => tabview.add_tab( - "podcasts", - ListView::new(library.shows.clone(), queue.clone(), library.clone()) - .with_title("Podcasts"), + "Podcasts", + ListView::new(library.shows.clone(), queue.clone(), library.clone()), ), LibraryTab::Browse => { - tabview.add_tab("browse", BrowseView::new(queue.clone(), library.clone())) + tabview.add_tab("Browse", BrowseView::new(queue.clone(), library.clone())) } } } @@ -77,7 +73,7 @@ impl LibraryView { } impl ViewWrapper for LibraryView { - wrap_impl!(self.tabs: TabView); + wrap_impl!(self.tabs: TabbedView); } impl ViewExt for LibraryView { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index e8783f7..903fb2d 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -18,7 +18,7 @@ pub mod search; pub mod search_results; pub mod show; pub mod statusbar; -pub mod tabview; +pub mod tabbedview; #[cfg(feature = "cover")] pub mod cover; diff --git a/src/ui/search.rs b/src/ui/search.rs index 09e4a6b..8f8c53b 100644 --- a/src/ui/search.rs +++ b/src/ui/search.rs @@ -26,7 +26,7 @@ use crate::ui::layout::Layout; use crate::ui::listview::ListView; use crate::ui::pagination::Pagination; use crate::ui::search_results::SearchResultsView; -use crate::ui::tabview::TabView; +use crate::ui::tabbedview::TabbedView; use rspotify::model::search::SearchResult; pub struct SearchView { diff --git a/src/ui/search_results.rs b/src/ui/search_results.rs index 02fe446..60f2c80 100644 --- a/src/ui/search_results.rs +++ b/src/ui/search_results.rs @@ -14,7 +14,7 @@ use crate::spotify_url::SpotifyUrl; use crate::traits::{ListItem, ViewExt}; use crate::ui::listview::ListView; use crate::ui::pagination::Pagination; -use crate::ui::tabview::TabView; +use crate::ui::tabbedview::TabbedView; use cursive::view::ViewWrapper; use cursive::Cursive; use rspotify::model::search::SearchResult; @@ -35,7 +35,7 @@ pub struct SearchResultsView { pagination_shows: Pagination, results_episodes: Arc>>, pagination_episodes: Pagination, - tabs: TabView, + tabs: TabbedView, spotify: Spotify, events: EventManager, } @@ -71,13 +71,13 @@ impl SearchResultsView { let list_episodes = ListView::new(results_episodes.clone(), queue.clone(), library); let pagination_episodes = list_episodes.get_pagination().clone(); - let tabs = TabView::new() - .tab("tracks", list_tracks.with_title("Tracks")) - .tab("albums", list_albums.with_title("Albums")) - .tab("artists", list_artists.with_title("Artists")) - .tab("playlists", list_playlists.with_title("Playlists")) - .tab("shows", list_shows.with_title("Podcasts")) - .tab("episodes", list_episodes.with_title("Podcast Episodes")); + let mut tabs = TabbedView::new(); + tabs.add_tab("Tracks", list_tracks); + tabs.add_tab("Albums", list_albums); + tabs.add_tab("Artists", list_artists); + tabs.add_tab("Playlists", list_playlists); + tabs.add_tab("Shows", list_shows); + tabs.add_tab("Episodes", list_episodes); let mut view = Self { search_term, @@ -403,7 +403,7 @@ impl SearchResultsView { &query, None, ); - self.tabs.move_focus_to(0); + self.tabs.set_selected(0); } UriType::Album => { self.perform_search( @@ -412,7 +412,7 @@ impl SearchResultsView { &query, None, ); - self.tabs.move_focus_to(1); + self.tabs.set_selected(1); } UriType::Artist => { self.perform_search( @@ -421,7 +421,7 @@ impl SearchResultsView { &query, None, ); - self.tabs.move_focus_to(2); + self.tabs.set_selected(2); } UriType::Playlist => { self.perform_search( @@ -430,7 +430,7 @@ impl SearchResultsView { &query, None, ); - self.tabs.move_focus_to(3); + self.tabs.set_selected(3); } UriType::Show => { self.perform_search( @@ -439,7 +439,7 @@ impl SearchResultsView { &query, None, ); - self.tabs.move_focus_to(4); + self.tabs.set_selected(4); } UriType::Episode => { self.perform_search( @@ -448,7 +448,7 @@ impl SearchResultsView { &query, None, ); - self.tabs.move_focus_to(5); + self.tabs.set_selected(5); } } // Is the query a spotify URL? @@ -462,7 +462,7 @@ impl SearchResultsView { &url.id, None, ); - self.tabs.move_focus_to(0); + self.tabs.set_selected(0); } UriType::Album => { self.perform_search( @@ -471,7 +471,7 @@ impl SearchResultsView { &url.id, None, ); - self.tabs.move_focus_to(1); + self.tabs.set_selected(1); } UriType::Artist => { self.perform_search( @@ -480,7 +480,7 @@ impl SearchResultsView { &url.id, None, ); - self.tabs.move_focus_to(2); + self.tabs.set_selected(2); } UriType::Playlist => { self.perform_search( @@ -489,7 +489,7 @@ impl SearchResultsView { &url.id, None, ); - self.tabs.move_focus_to(3); + self.tabs.set_selected(3); } UriType::Show => { self.perform_search( @@ -498,7 +498,7 @@ impl SearchResultsView { &url.id, None, ); - self.tabs.move_focus_to(4); + self.tabs.set_selected(4); } UriType::Episode => { self.perform_search( @@ -507,7 +507,7 @@ impl SearchResultsView { &url.id, None, ); - self.tabs.move_focus_to(5); + self.tabs.set_selected(5); } } } else { @@ -552,7 +552,7 @@ impl SearchResultsView { } impl ViewWrapper for SearchResultsView { - wrap_impl!(self.tabs: TabView); + wrap_impl!(self.tabs: TabbedView); } impl ViewExt for SearchResultsView { diff --git a/src/ui/tabbedview.rs b/src/ui/tabbedview.rs new file mode 100644 index 0000000..9367453 --- /dev/null +++ b/src/ui/tabbedview.rs @@ -0,0 +1,217 @@ +use std::cmp::min; + +use cursive::{ + align::HAlign, + event::{Event, EventResult, MouseButton, MouseEvent}, + theme::ColorStyle, + view::Nameable, + views::NamedView, + Cursive, Printer, Vec2, View, +}; +use unicode_width::UnicodeWidthStr; + +use crate::{ + command::{Command, MoveAmount, MoveMode}, + commands::CommandResult, + traits::{BoxedViewExt, IntoBoxedViewExt, ViewExt}, +}; + +/// A view that displays other views in a tab layout. +#[derive(Default)] +pub struct TabbedView { + /// The list of tabs + tabs: Vec>, + /// The index of the currently visible tab from `tabs` + selected: usize, + /// The size given to the last call to `layout()` + last_layout_size: Vec2, +} + +impl TabbedView { + pub fn new() -> Self { + Default::default() + } + + /// Add `view` as a new tab to the end of this [TabsView]. + pub fn add_tab(&mut self, title: impl Into, view: impl IntoBoxedViewExt) { + let tab = BoxedViewExt::new(view.into_boxed_view_ext()).with_name(title); + self.tabs.push(tab); + } + + /// Return a mutable reference to the tab at `index`, or None if there is no tab at `index`. + pub fn tab_mut(&mut self, index: usize) -> Option<&mut NamedView> { + self.tabs.get_mut(index) + } + + /// Return a mutable reference to the selected tab, or None if there is no selected tab + /// currently. + pub fn selected_tab_mut(&mut self) -> Option<&mut NamedView> { + self.tab_mut(self.selected) + } + + /// Return the amount of tabs in this view. + pub fn len(&self) -> usize { + self.tabs.len() + } + + /// Check whether there are tabs in this [TabsView]. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Set the tab at `index` as currently visible. + pub fn set_selected(&mut self, index: usize) { + self.selected = min(self.len().saturating_sub(1), index); + } + + /// Move the focus by `amount`, clipping at the edges. + pub fn move_selected(&mut self, amount: isize) { + self.selected = min( + self.selected.saturating_add_signed(amount), + self.len().saturating_sub(1), + ); + } + + pub fn move_left(&mut self) { + self.move_selected(-1); + } + + pub fn move_right(&mut self) { + self.move_selected(1); + } + + /// Move the focus to the first tab. + pub fn select_first(&mut self) { + self.selected = 0; + } + + /// Move the focus to the last tab. + pub fn select_last(&mut self) { + self.selected = self.len() - 1; + } + + /// Return whether we are on the first tab. + pub fn on_first_tab(&mut self) -> bool { + self.selected == 0 + } + + /// Return whether we are on the last tab. + pub fn on_last_tab(&mut self) -> bool { + self.selected == self.len() - 1 + } + + /// Return the width of a single tab. + /// + /// Keep in mind that this is an average. It's only provided to make sure all functions use the + /// same calculation for tab width to prevent off-by-one errors. + pub fn tab_width(&self) -> usize { + self.last_layout_size.x / self.len() + } +} + +impl View for TabbedView { + fn draw(&self, printer: &Printer<'_, '_>) { + if self.is_empty() { + return; + } + + let tabwidth = self.tab_width(); + for (i, tab) in self.tabs.iter().enumerate() { + let style = if self.selected == i { + ColorStyle::highlight() + } else { + ColorStyle::primary() + }; + + let mut width = tabwidth; + if i == self.tabs.len() - 1 { + width += printer.size.x % self.tabs.len(); + } + + let title = tab.name(); + let offset = HAlign::Center.get_offset(title.width(), width); + + printer.with_color(style, |printer| { + printer.print_hline((i * tabwidth, 0), width, " "); + printer.print((i * tabwidth + offset, 0), title); + }); + } + + if let Some(tab) = self.tabs.get(self.selected) { + let printer = printer + .offset((0, 1)) + .cropped((printer.size.x, printer.size.y - 1)); + + tab.draw(&printer); + } + } + + fn layout(&mut self, size: Vec2) { + self.last_layout_size = size; + if let Some(tab) = self.tab_mut(self.selected) { + tab.layout((size.x, size.y - 1).into()) + } + } + + fn on_event(&mut self, event: Event) -> EventResult { + if let Event::Mouse { + offset, + position, + event, + } = event + { + let position = position.checked_sub(offset); + if let Some(0) = position.map(|p| p.y) { + match event { + MouseEvent::WheelUp => self.move_left(), + MouseEvent::WheelDown => self.move_right(), + MouseEvent::Press(MouseButton::Left) => { + let tabwidth = self.tab_width(); + if let Some(selected_tab) = position.and_then(|p| p.x.checked_div(tabwidth)) + { + self.set_selected(selected_tab); + } + } + _ => {} + }; + return EventResult::consumed(); + } + } + + if let Some(tab) = self.tab_mut(self.selected) { + tab.on_event(event.relativized((0, 1))) + } else { + EventResult::Ignored + } + } +} + +impl ViewExt for TabbedView { + fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result { + match cmd { + Command::Move(mode, amount) if matches!(mode, MoveMode::Left | MoveMode::Right) => { + if matches!(mode, MoveMode::Left) && !self.on_first_tab() { + match amount { + MoveAmount::Extreme => self.select_first(), + MoveAmount::Integer(amount) => self.move_selected(-(*amount) as isize), + _ => (), + } + } else if matches!(mode, MoveMode::Right) && !self.on_last_tab() { + match amount { + MoveAmount::Extreme => self.select_last(), + MoveAmount::Integer(amount) => self.move_selected(*amount as isize), + _ => (), + } + } + Ok(CommandResult::Consumed(None)) + } + _ => { + if let Some(tab) = self.selected_tab_mut() { + tab.on_command(s, cmd) + } else { + Ok(CommandResult::Ignored) + } + } + } + } +} diff --git a/src/ui/tabview.rs b/src/ui/tabview.rs deleted file mode 100644 index 1431308..0000000 --- a/src/ui/tabview.rs +++ /dev/null @@ -1,169 +0,0 @@ -use std::cmp::{max, min}; -use std::collections::HashMap; - -use cursive::align::HAlign; -use cursive::event::{Event, EventResult, MouseButton, MouseEvent}; -use cursive::theme::ColorStyle; -use cursive::traits::View; -use cursive::{Cursive, Printer, Vec2}; -use unicode_width::UnicodeWidthStr; - -use crate::command::{Command, MoveAmount, MoveMode}; -use crate::commands::CommandResult; -use crate::traits::{IntoBoxedViewExt, ViewExt}; - -pub struct Tab { - view: Box, -} - -pub struct TabView { - tabs: Vec, - ids: HashMap, - selected: usize, - size: Vec2, -} - -impl TabView { - pub fn new() -> Self { - Self { - tabs: Vec::new(), - ids: HashMap::new(), - selected: 0, - size: Vec2::default(), - } - } - - pub fn add_tab, V: IntoBoxedViewExt>(&mut self, id: S, view: V) { - let tab = Tab { - view: view.into_boxed_view_ext(), - }; - self.tabs.push(tab); - self.ids.insert(id.into(), self.tabs.len() - 1); - } - - pub fn tab, V: IntoBoxedViewExt>(mut self, id: S, view: V) -> Self { - self.add_tab(id, view); - self - } - - pub fn move_focus_to(&mut self, target: usize) { - let len = self.tabs.len().saturating_sub(1); - self.selected = min(target, len); - } - - pub fn move_focus(&mut self, delta: i32) { - let new = self.selected as i32 + delta; - self.move_focus_to(max(new, 0) as usize); - } -} - -impl View for TabView { - fn draw(&self, printer: &Printer<'_, '_>) { - if self.tabs.is_empty() { - return; - } - - let tabwidth = printer.size.x / self.tabs.len(); - for (i, tab) in self.tabs.iter().enumerate() { - let style = if self.selected == i { - ColorStyle::highlight() - } else { - ColorStyle::primary() - }; - - let mut width = tabwidth; - if i == self.tabs.len() - 1 { - width += printer.size.x % self.tabs.len(); - } - - let title = tab.view.title(); - let offset = HAlign::Center.get_offset(title.width(), width); - - printer.with_color(style, |printer| { - printer.print_hline((i * tabwidth, 0), width, " "); - printer.print((i * tabwidth + offset, 0), &title); - }); - } - - if let Some(tab) = self.tabs.get(self.selected) { - let printer = printer - .offset((0, 1)) - .cropped((printer.size.x, printer.size.y - 1)); - - tab.view.draw(&printer); - } - } - - fn layout(&mut self, size: Vec2) { - self.size = size; - if let Some(tab) = self.tabs.get_mut(self.selected) { - tab.view.layout(Vec2::new(size.x, size.y - 1)); - } - } - - fn on_event(&mut self, event: Event) -> EventResult { - if let Event::Mouse { - offset, - position, - event, - } = event - { - let position = position.checked_sub(offset); - if let Some(0) = position.map(|p| p.y) { - match event { - MouseEvent::WheelUp => self.move_focus(-1), - MouseEvent::WheelDown => self.move_focus(1), - MouseEvent::Press(MouseButton::Left) => { - let tabwidth = self.size.x / self.tabs.len(); - if let Some(selected_tab) = position.and_then(|p| p.x.checked_div(tabwidth)) - { - self.move_focus_to(selected_tab); - } - } - _ => {} - }; - return EventResult::consumed(); - } - } - - if let Some(tab) = self.tabs.get_mut(self.selected) { - tab.view.on_event(event.relativized((0, 1))) - } else { - EventResult::Ignored - } - } -} - -impl ViewExt for TabView { - fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result { - if let Command::Move(mode, amount) = cmd { - let last_idx = self.tabs.len() - 1; - - match mode { - MoveMode::Left if self.selected > 0 => { - match amount { - MoveAmount::Extreme => self.move_focus_to(0), - MoveAmount::Integer(amount) => self.move_focus(-(*amount)), - _ => (), - } - return Ok(CommandResult::Consumed(None)); - } - MoveMode::Right if self.selected < last_idx => { - match amount { - MoveAmount::Extreme => self.move_focus_to(last_idx), - MoveAmount::Integer(amount) => self.move_focus(*amount), - _ => (), - } - return Ok(CommandResult::Consumed(None)); - } - _ => {} - } - } - - if let Some(tab) = self.tabs.get_mut(self.selected) { - tab.view.on_command(s, cmd) - } else { - Ok(CommandResult::Ignored) - } - } -}