#![allow(dead_code)] use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use ratatui::{ layout::{Constraint, Layout, Rect}, style::Style, widgets::{ListState, Paragraph}, Frame, }; use crate::data::{ sample_artists, sample_calendar, sample_history, sample_queue, sample_wanted, Artist, CalendarEntry, HistoryEntry, QueueEntry, WantedEntry, }; use crate::input::{ build_leader_tree, LeaderAction, LeaderNode, LeaderResult, LeaderState, VimState, }; use crate::theme; use crate::ui::cmdline::{render_cmdline, CmdLineState}; use crate::ui::library::{render_library, LibraryFocus, LibraryState}; use crate::ui::modals::{render_help_modal, render_quit_modal, ModalKind}; use crate::ui::notifications::{NotifKind, NotificationManager}; use crate::ui::statusbar::{render_statusbar, StatusHint}; use crate::ui::topbar::render_topbar; use crate::ui::views::{render_calendar, render_history, render_queue, render_settings, render_wanted}; use crate::ui::which_key::render_which_key; const PAGE_SIZE: usize = 12; const HALF_PAGE: usize = 6; #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Tab { #[default] Library, Wanted, Queue, History, Calendar, Settings, } impl Tab { pub const ALL: [Tab; 6] = [ Tab::Library, Tab::Wanted, Tab::Queue, Tab::History, Tab::Calendar, Tab::Settings, ]; pub fn from_number(n: u8) -> Option { match n { 1 => Some(Tab::Library), 2 => Some(Tab::Wanted), 3 => Some(Tab::Queue), 4 => Some(Tab::History), 5 => Some(Tab::Calendar), 6 => Some(Tab::Settings), _ => None, } } pub fn index(&self) -> usize { match self { Tab::Library => 0, Tab::Wanted => 1, Tab::Queue => 2, Tab::History => 3, Tab::Calendar => 4, Tab::Settings => 5, } } pub fn label(&self) -> &'static str { match self { Tab::Library => "Library", Tab::Wanted => "Wanted", Tab::Queue => "Queue", Tab::History => "History", Tab::Calendar => "Calendar", Tab::Settings => "Settings", } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Mode { #[default] Normal, Command, Search, } pub struct App { pub running: bool, pub tab: Tab, pub mode: Mode, pub size: Rect, pub library: LibraryState, pub vim: VimState, pub leader: LeaderState, pub leader_tree: LeaderNode, pub cmd_line: CmdLineState, pub search_filter: String, pub status_message: Option, pub marks: std::collections::HashMap, pub last_position: Option, filtered_artists: Vec, pub modal: Option, pub wanted: Vec, pub wanted_state: ListState, pub queue: Vec, pub queue_state: ListState, pub history: Vec, pub history_state: ListState, pub calendar: Vec, pub notifications: NotificationManager, } #[derive(Debug, Clone)] pub struct MarkPosition { pub tab: Tab, pub focus: LibraryFocus, pub artist_idx: usize, pub album_idx: usize, pub track_idx: usize, } impl Default for App { fn default() -> Self { let artists = sample_artists(); let wanted = sample_wanted(); let queue = sample_queue(); let history = sample_history(); let calendar = sample_calendar(); let mut wanted_state = ListState::default(); if !wanted.is_empty() { wanted_state.select(Some(0)); } let mut queue_state = ListState::default(); if !queue.is_empty() { queue_state.select(Some(0)); } let mut history_state = ListState::default(); if !history.is_empty() { history_state.select(Some(0)); } Self { running: true, tab: Tab::Library, mode: Mode::Normal, size: Rect::default(), library: LibraryState::new(artists.clone()), vim: VimState::new(), leader: LeaderState::new(), leader_tree: build_leader_tree(), cmd_line: CmdLineState::new(), search_filter: String::new(), status_message: None, marks: std::collections::HashMap::new(), last_position: None, filtered_artists: artists, modal: None, wanted, wanted_state, queue, queue_state, history, history_state, calendar, notifications: NotificationManager::new(), } } } impl App { pub fn new() -> Self { Self::default() } fn capture_position(&self) -> MarkPosition { MarkPosition { tab: self.tab, focus: self.library.focus, artist_idx: self.library.artist_state.selected().unwrap_or(0), album_idx: self.library.album_state.selected().unwrap_or(0), track_idx: self.library.track_state.selected().unwrap_or(0), } } fn restore_position(&mut self, pos: &MarkPosition) { self.tab = pos.tab; self.library.focus = pos.focus; self.library.artist_state.select(Some(pos.artist_idx)); self.library.album_state.select(Some(pos.album_idx)); self.library.track_state.select(Some(pos.track_idx)); } fn set_status(&mut self, msg: impl Into) { self.status_message = Some(msg.into()); } pub fn draw(&mut self, frame: &mut Frame) { self.size = frame.area(); let area = frame.area(); frame.render_widget( Paragraph::new("").style(Style::default().bg(theme::BG0)), area, ); let show_cmdline = self.mode != Mode::Normal; let cmdline_height = if show_cmdline { 1 } else { 0 }; let chunks = Layout::vertical([ Constraint::Length(1), Constraint::Min(1), Constraint::Length(cmdline_height), Constraint::Length(1), ]) .split(area); let queue_count = sample_queue().len(); let wanted_count = sample_wanted().len(); render_topbar( frame, chunks[0], self.tab, self.mode, queue_count, wanted_count, ); self.render_main_content(frame, chunks[1]); if show_cmdline { let hint = match self.mode { Mode::Command => "try :help · :theme light · :wanted · :q", Mode::Search => "filter artists by name", Mode::Normal => "", }; render_cmdline(frame, chunks[2], self.mode, &self.cmd_line.text, hint); } let hints = self.get_hints(); let position = self.get_position(); render_statusbar( frame, chunks[3], self.mode, &hints, position, queue_count, wanted_count, self.status_message.as_deref(), ); if self.leader.active { render_which_key(frame, area, &self.leader, &self.leader_tree); } if let Some(modal) = &self.modal { match modal { ModalKind::Help => render_help_modal(frame, area), ModalKind::Quit => render_quit_modal(frame, area, self.queue.len()), } } self.notifications.render(frame, area); } fn render_main_content(&mut self, frame: &mut Frame, area: Rect) { match self.tab { Tab::Library => { render_library(frame, area, &mut self.library); } Tab::Wanted => { render_wanted(frame, area, &self.wanted, &mut self.wanted_state); } Tab::Queue => { render_queue(frame, area, &self.queue, &mut self.queue_state); } Tab::History => { render_history(frame, area, &self.history, &mut self.history_state); } Tab::Calendar => { render_calendar(frame, area, &self.calendar); } Tab::Settings => { render_settings(frame, area); } } } fn get_hints(&self) -> Vec { match self.tab { Tab::Library => vec![ StatusHint { key: "j/k", action: "move" }, StatusHint { key: "h/l", action: "pane" }, StatusHint { key: "SPC", action: "leader" }, StatusHint { key: "a", action: "add" }, StatusHint { key: "/", action: "filter" }, StatusHint { key: "?", action: "help" }, ], Tab::Wanted => vec![ StatusHint { key: "j/k", action: "move" }, StatusHint { key: "s", action: "search" }, StatusHint { key: "m", action: "unmonitor" }, StatusHint { key: "SPC", action: "leader" }, ], Tab::Queue => vec![ StatusHint { key: "j/k", action: "move" }, StatusHint { key: "x", action: "remove" }, StatusHint { key: "p", action: "pause" }, StatusHint { key: "SPC", action: "leader" }, ], Tab::History => vec![ StatusHint { key: "j/k", action: "move" }, StatusHint { key: "d", action: "clear" }, StatusHint { key: "r", action: "retry" }, StatusHint { key: "SPC", action: "leader" }, ], Tab::Calendar => vec![ StatusHint { key: "h/l", action: "month" }, StatusHint { key: "Enter", action: "details" }, StatusHint { key: "SPC", action: "leader" }, ], Tab::Settings => vec![ StatusHint { key: "Tab", action: "next" }, StatusHint { key: "Enter", action: "edit" }, StatusHint { key: ":w", action: "save" }, StatusHint { key: "SPC", action: "leader" }, ], } } fn get_position(&self) -> Option<(usize, usize)> { match self.tab { Tab::Library => { let idx = self.library.selected_artist_index().unwrap_or(0) + 1; let total = self.library.artist_count(); Some((idx, total)) } Tab::Wanted => { let idx = self.wanted_state.selected().unwrap_or(0) + 1; Some((idx, self.wanted.len())) } Tab::Queue => { let idx = self.queue_state.selected().unwrap_or(0) + 1; Some((idx, self.queue.len())) } Tab::History => { let idx = self.history_state.selected().unwrap_or(0) + 1; Some((idx, self.history.len())) } Tab::Calendar | Tab::Settings => None, } } pub fn handle_key(&mut self, key: KeyEvent) { self.status_message = None; if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') { self.running = false; return; } if self.modal.is_some() { self.handle_modal_key(key); return; } match self.mode { Mode::Command | Mode::Search => { self.handle_cmdline_key(key); return; } Mode::Normal => {} } if self.leader.active { self.handle_leader_key(key); return; } self.handle_normal_key(key); } fn handle_modal_key(&mut self, key: KeyEvent) { match key.code { KeyCode::Esc | KeyCode::Char('n') => { self.modal = None; } KeyCode::Char('y') => { if self.modal == Some(ModalKind::Quit) { self.running = false; } } KeyCode::Char('q') => { self.modal = None; } _ => {} } } fn handle_cmdline_key(&mut self, key: KeyEvent) { match key.code { KeyCode::Esc => { self.mode = Mode::Normal; self.cmd_line.clear(); } KeyCode::Enter => { let text = self.cmd_line.take(); if self.mode == Mode::Command { self.execute_command(&text); } else { self.execute_search(&text); } self.mode = Mode::Normal; } KeyCode::Char(c) => { self.cmd_line.push(c); } KeyCode::Backspace => { if self.cmd_line.pop().is_none() || self.cmd_line.is_empty() { self.mode = Mode::Normal; self.cmd_line.clear(); } } _ => {} } } fn handle_leader_key(&mut self, key: KeyEvent) { match key.code { KeyCode::Esc => { self.leader.deactivate(); } KeyCode::Backspace => { self.leader.go_back(); } KeyCode::Char(c) => { let result = self.leader.navigate(c, &self.leader_tree); match result { LeaderResult::Pending => {} LeaderResult::Executed(action) => { self.execute_leader_action(action); } LeaderResult::NotBound => { let path: String = self.leader.path.iter().collect(); self.set_status(format!("SPC {} {} not bound", path, c)); } } } _ => {} } } fn execute_leader_action(&mut self, action: LeaderAction) { match action { LeaderAction::SetTab(tab) => self.tab = tab, LeaderAction::SetMode(mode) => { self.mode = mode; self.cmd_line.clear(); } LeaderAction::NextTab => { let idx = (self.tab.index() + 1) % Tab::ALL.len(); self.tab = Tab::ALL[idx]; } LeaderAction::PrevTab => { let idx = (self.tab.index() + Tab::ALL.len() - 1) % Tab::ALL.len(); self.tab = Tab::ALL[idx]; } LeaderAction::ToggleMonitor => { self.set_status("monitor toggled"); self.notify("monitor toggled", None, NotifKind::Info, "◉"); } LeaderAction::Refresh => { self.set_status("scanning library..."); self.notify("scanning library", Some("18 artists".into()), NotifKind::Info, "↻"); } LeaderAction::SearchRelease => { self.set_status("search release"); } LeaderAction::CycleTheme => { self.set_status("theme cycled"); self.notify("theme", Some("dark".into()), NotifKind::Success, "◐"); } LeaderAction::SetThemeDark => { self.set_status("theme: dark"); self.notify("theme", Some("dark".into()), NotifKind::Success, "●"); } LeaderAction::SetThemeLight => { self.set_status("theme: light"); self.notify("theme", Some("light".into()), NotifKind::Success, "○"); } LeaderAction::ShowNotifications => { self.set_status("notifications"); } LeaderAction::DismissNotifications => { self.set_status("notifications dismissed"); } LeaderAction::ShowHelp => { self.modal = Some(ModalKind::Help); } LeaderAction::ShowAdd => { self.set_status("add artist"); } LeaderAction::ShowQuit => { self.modal = Some(ModalKind::Quit); } LeaderAction::SyncLibrary => { self.set_status("library synced · 0 changes"); self.notify("library synced", Some("0 changes".into()), NotifKind::Success, "✓"); } } } fn handle_normal_key(&mut self, key: KeyEvent) { let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); if self.vim.has_pending() { self.handle_pending_key(key); return; } if let KeyCode::Char(c) = key.code { if self.vim.accumulate_count(c) { return; } } let count = self.vim.take_count(); if ctrl { self.handle_ctrl_key(key, count); return; } match key.code { KeyCode::Char(' ') => { self.leader.activate(); } KeyCode::Char(':') => { self.mode = Mode::Command; self.cmd_line.clear(); } KeyCode::Char('/') => { self.last_position = Some(self.capture_position()); self.mode = Mode::Search; self.cmd_line.clear(); } KeyCode::Char('?') => { self.modal = Some(ModalKind::Help); } KeyCode::Char('g') | KeyCode::Char('z') | KeyCode::Char('m') | KeyCode::Char('\'') | KeyCode::Char('`') | KeyCode::Char('[') | KeyCode::Char(']') => { if let KeyCode::Char(c) = key.code { self.vim.set_pending(c); } } KeyCode::Char('j') | KeyCode::Down => self.move_cursor(count as isize), KeyCode::Char('k') | KeyCode::Up => self.move_cursor(-(count as isize)), KeyCode::Char('h') | KeyCode::Left => { if self.tab == Tab::Library { self.library.focus_left(); } } KeyCode::Char('l') | KeyCode::Right => { if self.tab == Tab::Library { self.library.focus_right(); } } KeyCode::Char('w') => self.move_cursor(3 * count as isize), KeyCode::Char('W') => self.move_cursor(5 * count as isize), KeyCode::Char('b') => self.move_cursor(-3 * (count as isize)), KeyCode::Char('B') => self.move_cursor(-5 * (count as isize)), KeyCode::Char('e') => self.move_cursor(3 * count as isize), KeyCode::Char('E') => self.move_cursor(5 * count as isize), KeyCode::Char('{') => self.move_cursor(-5 * (count as isize)), KeyCode::Char('}') => self.move_cursor(5 * count as isize), KeyCode::Char('(') => self.move_cursor(-3 * (count as isize)), KeyCode::Char(')') => self.move_cursor(3 * count as isize), KeyCode::Char('0') | KeyCode::Char('^') => { if self.tab == Tab::Library { self.library.focus = LibraryFocus::Artists; } } KeyCode::Char('$') => { if self.tab == Tab::Library { self.library.focus = LibraryFocus::Tracks; } } KeyCode::Char('G') => { self.last_position = Some(self.capture_position()); if self.vim.has_count() { let line = self.vim.take_count(); self.move_to(line.saturating_sub(1)); } else { self.move_to_end(); } } KeyCode::Char('H') => { self.last_position = Some(self.capture_position()); self.move_to(0); } KeyCode::Char('M') => { self.last_position = Some(self.capture_position()); let len = self.current_list_len(); self.move_to(len / 2); } KeyCode::Char('L') => { self.last_position = Some(self.capture_position()); self.move_to_end(); } KeyCode::Char('n') => { self.set_status("n: repeat search forward"); } KeyCode::Char('N') => { self.set_status("N: repeat search backward"); } KeyCode::Char('*') => { self.set_status("*: search word under cursor forward"); } KeyCode::Char('#') => { self.set_status("#: search word under cursor backward"); } KeyCode::Char('a') => { self.set_status("add artist"); } KeyCode::Char('s') => { self.set_status("search release"); } KeyCode::Char('r') => { self.set_status("refresh"); } KeyCode::Char('t') => { self.set_status("monitor toggled"); } KeyCode::Char('q') => { self.modal = Some(ModalKind::Quit); } KeyCode::Char('1') => self.tab = Tab::Library, KeyCode::Char('2') => self.tab = Tab::Wanted, KeyCode::Char('3') => self.tab = Tab::Queue, KeyCode::Char('4') => self.tab = Tab::History, KeyCode::Char('5') => self.tab = Tab::Calendar, KeyCode::Char('6') => self.tab = Tab::Settings, KeyCode::Tab => { if self.tab == Tab::Library { self.library.cycle_focus(); } } KeyCode::Enter => { if self.tab == Tab::Library { self.library.focus_right(); } } KeyCode::Esc => { self.vim.clear(); if !self.search_filter.is_empty() { self.search_filter.clear(); self.update_filtered_artists(); self.set_status("filter cleared"); } else if self.library.focus != LibraryFocus::Artists { self.library.focus_left(); } } _ => {} } } fn handle_ctrl_key(&mut self, key: KeyEvent, count: usize) { match key.code { KeyCode::Char('d') => { self.last_position = Some(self.capture_position()); self.move_cursor((HALF_PAGE * count) as isize); } KeyCode::Char('u') => { self.last_position = Some(self.capture_position()); self.move_cursor(-((HALF_PAGE * count) as isize)); } KeyCode::Char('f') => { self.last_position = Some(self.capture_position()); self.move_cursor((PAGE_SIZE * count) as isize); } KeyCode::Char('b') => { self.last_position = Some(self.capture_position()); self.move_cursor(-((PAGE_SIZE * count) as isize)); } KeyCode::Char('e') => { self.move_cursor(count as isize); } KeyCode::Char('y') => { self.move_cursor(-(count as isize)); } KeyCode::Char('o') => { if let Some(pos) = self.last_position.take() { let current = self.capture_position(); self.restore_position(&pos); self.last_position = Some(current); } else { self.set_status("at start of jumplist"); } } KeyCode::Char('i') => { self.set_status("Ctrl-i: forward in jumplist"); } _ => {} } } fn handle_pending_key(&mut self, key: KeyEvent) { let pending = match self.vim.take_pending() { Some(p) => p, None => return, }; let count = self.vim.take_count(); let KeyCode::Char(k) = key.code else { return; }; match pending { 'g' => match k { 'g' => { self.last_position = Some(self.capture_position()); self.move_to(0); } 'e' => self.move_cursor(-3 * (count as isize)), 'E' => self.move_cursor(-5 * (count as isize)), 'j' => self.move_cursor(count as isize), 'k' => self.move_cursor(-(count as isize)), '_' => self.move_to_end(), '0' => { if self.tab == Tab::Library { self.library.focus = LibraryFocus::Artists; } } '$' => { if self.tab == Tab::Library { self.library.focus = LibraryFocus::Tracks; } } 't' => { let idx = (self.tab.index() + 1) % Tab::ALL.len(); self.tab = Tab::ALL[idx]; } 'T' => { let idx = (self.tab.index() + Tab::ALL.len() - 1) % Tab::ALL.len(); self.tab = Tab::ALL[idx]; } _ => {} }, 'z' => match k { 'z' | '.' => self.set_status("zz: cursor centered"), 't' => self.set_status("zt: cursor → viewport top"), 'b' | '-' => self.set_status("zb: cursor → viewport bottom"), _ => {} }, 'm' => { if k.is_ascii_alphabetic() { let pos = self.capture_position(); self.marks.insert(k, pos); self.set_status(format!("mark '{}' set", k)); self.notify("mark set", Some(format!("'{}", k)), NotifKind::Info, "⚑"); } } '\'' | '`' => { if k == '\'' || k == '`' { if let Some(pos) = self.last_position.take() { let current = self.capture_position(); self.restore_position(&pos); self.last_position = Some(current); } else { self.set_status("no previous position"); } } else if k.is_ascii_alphabetic() { if let Some(pos) = self.marks.get(&k).cloned() { self.last_position = Some(self.capture_position()); self.restore_position(&pos); } else { self.set_status(format!("E20: mark not set: '{}'", k)); } } } '[' => match k { '[' => { self.last_position = Some(self.capture_position()); self.move_to(0); } 'c' => self.move_cursor(-(count as isize)), _ => {} }, ']' => match k { ']' => { self.last_position = Some(self.capture_position()); self.move_to_end(); } 'c' => self.move_cursor(count as isize), _ => {} }, _ => {} } } fn current_list_len(&self) -> usize { match self.tab { Tab::Library => match self.library.focus { LibraryFocus::Artists => self.library.artist_count(), LibraryFocus::Albums => self .library .selected_artist() .map(|a| a.albums.len()) .unwrap_or(0), LibraryFocus::Tracks => self.library.selected_album().map(|a| a.total as usize).unwrap_or(0), }, Tab::Wanted => self.wanted.len(), Tab::Queue => self.queue.len(), Tab::History => self.history.len(), Tab::Calendar | Tab::Settings => 0, } } fn move_cursor(&mut self, delta: isize) { let len = self.current_list_len(); if len == 0 { return; } match self.tab { Tab::Library => match self.library.focus { LibraryFocus::Artists => { let current = self.library.artist_state.selected().unwrap_or(0); let new_idx = (current as isize + delta).clamp(0, (len - 1) as isize) as usize; if new_idx != current { self.library.artist_state.select(Some(new_idx)); self.library.album_state.select(Some(0)); self.library.track_state.select(Some(0)); } } LibraryFocus::Albums => { let current = self.library.album_state.selected().unwrap_or(0); let new_idx = (current as isize + delta).clamp(0, (len - 1) as isize) as usize; if new_idx != current { self.library.album_state.select(Some(new_idx)); self.library.track_state.select(Some(0)); } } LibraryFocus::Tracks => { let current = self.library.track_state.selected().unwrap_or(0); let new_idx = (current as isize + delta).clamp(0, (len - 1) as isize) as usize; self.library.track_state.select(Some(new_idx)); } }, Tab::Wanted => { let current = self.wanted_state.selected().unwrap_or(0); let new_idx = (current as isize + delta).clamp(0, (len - 1) as isize) as usize; self.wanted_state.select(Some(new_idx)); } Tab::Queue => { let current = self.queue_state.selected().unwrap_or(0); let new_idx = (current as isize + delta).clamp(0, (len - 1) as isize) as usize; self.queue_state.select(Some(new_idx)); } Tab::History => { let current = self.history_state.selected().unwrap_or(0); let new_idx = (current as isize + delta).clamp(0, (len - 1) as isize) as usize; self.history_state.select(Some(new_idx)); } Tab::Calendar | Tab::Settings => {} } } fn move_to(&mut self, idx: usize) { let len = self.current_list_len(); if len == 0 { return; } let new_idx = idx.min(len - 1); match self.tab { Tab::Library => match self.library.focus { LibraryFocus::Artists => { self.library.artist_state.select(Some(new_idx)); self.library.album_state.select(Some(0)); self.library.track_state.select(Some(0)); } LibraryFocus::Albums => { self.library.album_state.select(Some(new_idx)); self.library.track_state.select(Some(0)); } LibraryFocus::Tracks => { self.library.track_state.select(Some(new_idx)); } }, Tab::Wanted => { self.wanted_state.select(Some(new_idx)); } Tab::Queue => { self.queue_state.select(Some(new_idx)); } Tab::History => { self.history_state.select(Some(new_idx)); } Tab::Calendar | Tab::Settings => {} } } fn move_to_end(&mut self) { let len = self.current_list_len(); if len > 0 { self.move_to(len - 1); } } fn execute_command(&mut self, text: &str) { let text = text.trim(); if text.is_empty() { return; } let parts: Vec<&str> = text.split_whitespace().collect(); let cmd = parts.first().map(|s| s.to_lowercase()).unwrap_or_default(); match cmd.as_str() { "q" | "quit" => self.running = false, "w" | "write" | "sync" => { self.set_status("library synced · 0 changes"); } "wq" => { self.set_status("synced · bye"); self.running = false; } "help" | "h" => { self.set_status("help"); } "theme" => { if let Some(theme) = parts.get(1) { match *theme { "dark" => self.set_status("theme: dark"), "light" => self.set_status("theme: light"), _ => self.set_status("usage: :theme dark | light"), } } else { self.set_status("usage: :theme dark | light"); } } "lib" | "library" => self.tab = Tab::Library, "wanted" => self.tab = Tab::Wanted, "queue" => self.tab = Tab::Queue, "history" => self.tab = Tab::History, "cal" | "calendar" => self.tab = Tab::Calendar, "settings" | "set" => self.tab = Tab::Settings, "refresh" | "rescan" | "r" => { self.set_status("library refreshed"); } _ => { self.set_status(format!("E492: not an editor command: {}", text)); } } } fn execute_search(&mut self, text: &str) { let filter = text.trim().to_string(); self.search_filter = filter.clone(); self.update_filtered_artists(); if filter.is_empty() { self.set_status("filter cleared"); } else { let count = self.filtered_artists.len(); self.set_status(format!("filter: /{} · {} matches", filter, count)); self.notify("filter", Some(format!("/{}", filter)), NotifKind::Info, "⌕"); } } fn update_filtered_artists(&mut self) { if self.search_filter.is_empty() { self.filtered_artists = self.library.artists.clone(); } else { let query = self.search_filter.to_lowercase(); self.filtered_artists = self .library .artists .iter() .filter(|a| a.name.to_lowercase().contains(&query)) .cloned() .collect(); } self.library.artist_state.select(Some(0)); } pub fn handle_tick(&mut self) { if self.status_message.is_some() { self.status_message = None; } self.notifications.tick(); } fn notify(&mut self, title: impl Into, detail: Option, kind: NotifKind, icon: impl Into) { self.notifications.push(title, detail, kind, icon); } }