diff --git a/mmtc.default.ron b/mmtc.default.ron index 78f5560..865cc01 100644 --- a/mmtc.default.ron +++ b/mmtc.default.ron @@ -46,29 +46,35 @@ Config( ), ])), Fixed(1, Columns([ - Min(0, Textbox(If(Not(Stopped), Styled([Bold], Parts([ - Styled([Fg(Indexed(113))], Parts([ - If(Playing, Text("[playing: "), Text("[paused: ")), - CurrentElapsed, - Text("/"), - CurrentDuration, - Text("] "), - ])), - If(TitleExist, - Parts([ - Styled([Fg(Indexed(149))], CurrentTitle), - If(ArtistExist, Parts([ - Styled([Fg(Indexed(216))], Text(" ◆ ")), - Styled([Fg(Indexed(185))], CurrentArtist), - If(AlbumExist, Parts([ + Min(0, Textbox(Styled([Bold], If(Searching, + Parts([ + Styled([Fg(Indexed(113))], Text("Searching: ")), + Styled([Fg(Indexed(185))], Query), + ]), + If(Not(Stopped), Parts([ + Styled([Fg(Indexed(113))], Parts([ + If(Playing, Text("[playing: "), Text("[paused: ")), + CurrentElapsed, + Text("/"), + CurrentDuration, + Text("] "), + ])), + If(TitleExist, + Parts([ + Styled([Fg(Indexed(149))], CurrentTitle), + If(ArtistExist, Parts([ Styled([Fg(Indexed(216))], Text(" ◆ ")), - Styled([Fg(Indexed(221))], CurrentAlbum), + Styled([Fg(Indexed(185))], CurrentArtist), + If(AlbumExist, Parts([ + Styled([Fg(Indexed(216))], Text(" ◆ ")), + Styled([Fg(Indexed(221))], CurrentAlbum), + ])), ])), - ])), - ]), - Styled([Fg(Indexed(185))], CurrentFile), - ), - ]))))), + ]), + Styled([Fg(Indexed(185))], CurrentFile), + ), + ])), + )))), Fixed(12, TextboxR(Styled([Fg(Indexed(81))], Parts([ Text("["), If(Repeat, Text("@")), diff --git a/src/config.rs b/src/config.rs index af2b41c..64ceb7d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -59,6 +59,7 @@ pub enum Texts { QueueTitle, QueueArtist, QueueAlbum, + Query, Styled(Vec, Box), Parts(Vec), If(Condition, Box, Option>), @@ -103,6 +104,7 @@ pub enum Condition { AlbumExist, QueueCurrent, Selected, + Searching, Not(Box), And(Box, Box), Or(Box, Box), @@ -147,6 +149,7 @@ impl<'de> Deserialize<'de> for Texts { QueueTitle, QueueArtist, QueueAlbum, + Query, Styled, Parts, If, @@ -219,6 +222,7 @@ impl<'de> Deserialize<'de> for Texts { Variant::QueueTitle => unit_variant!(QueueTitle), Variant::QueueArtist => unit_variant!(QueueArtist), Variant::QueueAlbum => unit_variant!(QueueAlbum), + Variant::Query => unit_variant!(Query), Variant::Styled => va.tuple_variant(2, StyledVisitor), Variant::Parts => Ok(Texts::Parts(va.newtype_variant()?)), Variant::If => va.tuple_variant(3, IfVisitor), @@ -241,6 +245,7 @@ impl<'de> Deserialize<'de> for Texts { "QueueTitle", "QueueArtist", "QueueAlbum", + "Query", "Styled", "Parts", "If", diff --git a/src/defaults.rs b/src/defaults.rs index 1263da5..b3bb269 100644 --- a/src/defaults.rs +++ b/src/defaults.rs @@ -152,71 +152,90 @@ pub fn layout() -> Widget { Widget::Columns(vec![ Constrained::Min( 0, - Widget::Textbox(Texts::If( - Condition::Not(Box::new(Condition::Stopped)), - Box::new(Texts::Styled( - vec![AddStyle::Bold], + Widget::Textbox(Texts::Styled( + vec![AddStyle::Bold], + Box::new(Texts::If( + Condition::Searching, Box::new(Texts::Parts(vec![ Texts::Styled( vec![AddStyle::Fg(Color::Indexed(113))], - Box::new(Texts::Parts(vec![ - Texts::If( - Condition::Playing, - Box::new(Texts::Text(String::from("[playing: "))), - Some(Box::new(Texts::Text(String::from("[paused: ")))), - ), - Texts::CurrentElapsed, - Texts::Text(String::from("/")), - Texts::CurrentDuration, - Texts::Text(String::from("] ")), - ])), + Box::new(Texts::Text(String::from("Searching: "))), ), - Texts::If( - Condition::TitleExist, - Box::new(Texts::Parts(vec![ - Texts::Styled( - vec![AddStyle::Fg(Color::Indexed(149))], - Box::new(Texts::CurrentTitle), - ), - Texts::If( - Condition::ArtistExist, - Box::new(Texts::Parts(vec![ - Texts::Styled( - vec![AddStyle::Fg(Color::Indexed(216))], - Box::new(Texts::Text(String::from(" ◆ "))), - ), - Texts::Styled( - vec![AddStyle::Fg(Color::Indexed(185))], - Box::new(Texts::CurrentArtist), - ), - Texts::If( - Condition::AlbumExist, - Box::new(Texts::Parts(vec![ - Texts::Styled( - vec![AddStyle::Fg(Color::Indexed(216))], - Box::new(Texts::Text(String::from( - " ◆ ", - ))), - ), - Texts::Styled( - vec![AddStyle::Fg(Color::Indexed(221))], - Box::new(Texts::CurrentAlbum), - ), - ])), - None, - ), - ])), - None, - ), - ])), - Some(Box::new(Texts::Styled( - vec![AddStyle::Fg(Color::Indexed(185))], - Box::new(Texts::CurrentFile), - ))), + Texts::Styled( + vec![AddStyle::Fg(Color::Indexed(185))], + Box::new(Texts::Query), ), ])), + Some(Box::new(Texts::If( + Condition::Not(Box::new(Condition::Stopped)), + Box::new(Texts::Parts(vec![ + Texts::Styled( + vec![AddStyle::Fg(Color::Indexed(113))], + Box::new(Texts::Parts(vec![ + Texts::If( + Condition::Playing, + Box::new(Texts::Text(String::from("[playing: "))), + Some(Box::new(Texts::Text(String::from( + "[paused: ", + )))), + ), + Texts::CurrentElapsed, + Texts::Text(String::from("/")), + Texts::CurrentDuration, + Texts::Text(String::from("] ")), + ])), + ), + Texts::If( + Condition::TitleExist, + Box::new(Texts::Parts(vec![ + Texts::Styled( + vec![AddStyle::Fg(Color::Indexed(149))], + Box::new(Texts::CurrentTitle), + ), + Texts::If( + Condition::ArtistExist, + Box::new(Texts::Parts(vec![ + Texts::Styled( + vec![AddStyle::Fg(Color::Indexed(216))], + Box::new(Texts::Text(String::from(" ◆ "))), + ), + Texts::Styled( + vec![AddStyle::Fg(Color::Indexed(185))], + Box::new(Texts::CurrentArtist), + ), + Texts::If( + Condition::AlbumExist, + Box::new(Texts::Parts(vec![ + Texts::Styled( + vec![AddStyle::Fg(Color::Indexed( + 216, + ))], + Box::new(Texts::Text( + String::from(" ◆ "), + )), + ), + Texts::Styled( + vec![AddStyle::Fg(Color::Indexed( + 221, + ))], + Box::new(Texts::CurrentAlbum), + ), + ])), + None, + ), + ])), + None, + ), + ])), + Some(Box::new(Texts::Styled( + vec![AddStyle::Fg(Color::Indexed(185))], + Box::new(Texts::CurrentFile), + ))), + ), + ])), + None, + ))), )), - None, )), ), Constrained::Fixed( diff --git a/src/layout.rs b/src/layout.rs index afbf779..cfa2e91 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -17,6 +17,9 @@ pub fn render( size: Rect, widget: &Widget, queue: &[Track], + searching: bool, + query: &str, + filtered: &Option>, status: &Status, liststate: &mut ListState, ) { @@ -53,7 +56,9 @@ pub fn render( let mut ws = ws.into_iter(); while let (Some(chunk), Some(w)) = (chunks.next(), ws.next()) { - render(frame, chunk, w, queue, status, liststate); + render( + frame, chunk, w, queue, searching, query, filtered, status, liststate, + ); } } Widget::Columns(xs) => { @@ -88,7 +93,9 @@ pub fn render( let mut ws = ws.into_iter(); while let (Some(chunk), Some(w)) = (chunks.next(), ws.next()) { - render(frame, chunk, w, queue, status, liststate); + render( + frame, chunk, w, queue, searching, query, filtered, status, liststate, + ); } } Widget::Textbox(xs) => { @@ -105,6 +112,8 @@ pub fn render( None, false, false, + searching, + query, Style::default(), ); frame.render_widget(Paragraph::new(Spans::from(spans)), size); @@ -123,6 +132,8 @@ pub fn render( None, false, false, + searching, + query, Style::default(), ); frame.render_widget( @@ -144,6 +155,8 @@ pub fn render( None, false, false, + searching, + query, Style::default(), ); frame.render_widget( @@ -184,19 +197,40 @@ pub fn render( }; let mut items = Vec::with_capacity(len); - for (i, track) in queue.iter().enumerate() { - let mut spans = Vec::new(); - flatten( - &mut spans, - txts, - status, - current_track, - Some(track), - pos == Some(i), - liststate.selected() == Some(i), - Style::default(), - ); - items.push(ListItem::new(Spans::from(spans))); + if let Some(filtered) = filtered { + for &i in filtered { + let mut spans = Vec::new(); + flatten( + &mut spans, + txts, + status, + current_track, + Some(&queue[i]), + pos == Some(i), + liststate.selected() == Some(i), + searching, + query, + Style::default(), + ); + items.push(ListItem::new(Spans::from(spans))); + } + } else { + for (i, track) in queue.iter().enumerate() { + let mut spans = Vec::new(); + flatten( + &mut spans, + txts, + status, + current_track, + Some(track), + pos == Some(i), + liststate.selected() == Some(i), + searching, + query, + Style::default(), + ); + items.push(ListItem::new(Spans::from(spans))); + } } ws.push( List::new(items) @@ -231,6 +265,8 @@ fn flatten( queue_track: Option<&Track>, queue_current: bool, selected: bool, + searching: bool, + query: &str, style: Style, ) { match xs { @@ -319,6 +355,9 @@ fn flatten( spans.push(Span::styled(album.clone(), style)); } } + Texts::Query => { + spans.push(Span::styled(String::from(query), style)); + } Texts::Styled(styles, box xs) => { flatten( spans, @@ -328,6 +367,8 @@ fn flatten( queue_track, queue_current, selected, + searching, + query, patch_style(style, styles), ); } @@ -341,6 +382,8 @@ fn flatten( queue_track, queue_current, selected, + searching, + query, style, ); } @@ -348,7 +391,15 @@ fn flatten( Texts::If(cond, box yes, Some(box no)) => { flatten( spans, - if eval_cond(cond, status, current_track, queue_current, selected) { + if eval_cond( + cond, + status, + current_track, + queue_current, + selected, + searching, + query, + ) { yes } else { no @@ -358,11 +409,21 @@ fn flatten( queue_track, queue_current, selected, + searching, + query, style, ); } Texts::If(cond, box xs, None) => { - if eval_cond(cond, status, current_track, queue_current, selected) { + if eval_cond( + cond, + status, + current_track, + queue_current, + selected, + searching, + query, + ) { flatten( spans, xs, @@ -371,6 +432,8 @@ fn flatten( queue_track, queue_current, selected, + searching, + query, style, ); } @@ -453,6 +516,8 @@ fn eval_cond( current_track: Option<&Track>, queue_current: bool, selected: bool, + searching: bool, + query: &str, ) -> bool { match cond { Condition::Repeat => status.repeat, @@ -473,18 +538,72 @@ fn eval_cond( Condition::AlbumExist => matches!(current_track, Some(Track { album: Some(_), .. })), Condition::QueueCurrent => queue_current, Condition::Selected => selected, - Condition::Not(box x) => !eval_cond(x, status, current_track, queue_current, selected), + Condition::Searching => searching, + Condition::Not(box x) => !eval_cond( + x, + status, + current_track, + queue_current, + selected, + searching, + query, + ), Condition::And(box x, box y) => { - eval_cond(x, status, current_track, queue_current, selected) - && eval_cond(y, status, current_track, queue_current, selected) + eval_cond( + x, + status, + current_track, + queue_current, + selected, + searching, + query, + ) && eval_cond( + y, + status, + current_track, + queue_current, + selected, + searching, + query, + ) } Condition::Or(box x, box y) => { - eval_cond(x, status, current_track, queue_current, selected) - || eval_cond(y, status, current_track, queue_current, selected) + eval_cond( + x, + status, + current_track, + queue_current, + selected, + searching, + query, + ) || eval_cond( + y, + status, + current_track, + queue_current, + selected, + searching, + query, + ) } Condition::Xor(box x, box y) => { - eval_cond(x, status, current_track, queue_current, selected) - ^ eval_cond(y, status, current_track, queue_current, selected) + eval_cond( + x, + status, + current_track, + queue_current, + selected, + searching, + query, + ) ^ eval_cond( + y, + status, + current_track, + queue_current, + selected, + searching, + query, + ) } } } diff --git a/src/main.rs b/src/main.rs index b873e5a..a08b867 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,6 +27,10 @@ use std::{ fs, io::{stdout, Write}, process::exit, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, }; use crate::config::Config; @@ -91,6 +95,10 @@ enum Command { Up, JumpDown, JumpUp, + InputSearch(char), + BackspaceSearch, + UpdateSearch, + QuitSearch, } fn cleanup() -> Result<()> { @@ -148,11 +156,15 @@ async fn run() -> Result<()> { let mut idle_cl = mpd::init(addr).await?; let mut cl = mpd::init(addr).await?; - let mut queue = mpd::queue(&mut idle_cl).await?; + let (mut queue, mut queue_strings) = mpd::queue(&mut idle_cl).await?; let mut status = mpd::status(&mut cl).await?; let mut selected = status.song.map_or(0, |song| song.pos); let mut liststate = ListState::default(); liststate.select(Some(selected)); + let searching = Arc::new(AtomicBool::new(false)); + let searching1 = Arc::clone(&searching); + let mut query = String::with_capacity(32); + let mut filtered = None; let seek_backwards = format!("seekcur -{}\n", seek_secs); let seek_backwards = seek_backwards.as_bytes(); @@ -200,9 +212,29 @@ async fn run() -> Result<()> { Terminal::new(CrosstermBackend::new(stdout)).context("Failed to initialize terminal")?; tokio::spawn(async move { + let searching = searching1; let tx = tx3; while let Ok(ev) = event::read() { - if let Some(cmd) = match ev { + if let Event::Key(KeyEvent { + code: KeyCode::Esc, .. + }) = ev + { + tx.send(Command::QuitSearch).await.unwrap_or_else(die); + } else if searching.load(Ordering::Acquire) { + if let Event::Key(KeyEvent { code, .. }) = ev { + if let Some(cmd) = match code { + KeyCode::Char(c) => Some(Command::InputSearch(c)), + KeyCode::Backspace => Some(Command::BackspaceSearch), + KeyCode::Enter => { + searching.store(false, Ordering::Release); + Some(Command::UpdateFrame) + } + _ => None, + } { + tx.send(cmd).await.unwrap_or_else(die); + } + } + } else if let Some(cmd) = match ev { Event::Key(KeyEvent { code, .. }) => match code { KeyCode::Char('q') => Some(Command::Quit), KeyCode::Char('r') => Some(Command::ToggleRepeat), @@ -222,6 +254,10 @@ async fn run() -> Result<()> { KeyCode::Char('k') | KeyCode::Up => Some(Command::Up), KeyCode::Char('J') | KeyCode::PageDown => Some(Command::JumpDown), KeyCode::Char('K') | KeyCode::PageUp => Some(Command::JumpUp), + KeyCode::Char('/') => { + searching.store(true, Ordering::Release); + Some(Command::UpdateFrame) + } _ => None, }, Event::Mouse(MouseEvent::ScrollDown(..)) => Some(Command::Down), @@ -244,16 +280,24 @@ async fn run() -> Result<()> { frame.size(), &cfg.layout, &queue, + searching.load(Ordering::Acquire), + &query, + &filtered, &status, &mut liststate, ); }) .context("Failed to draw to terminal")?, Command::UpdateQueue => { - queue = mpd::queue(&mut cl).await.context("Failed to query queue")?; + let res = mpd::queue(&mut cl).await.context("Failed to query queue")?; + queue = res.0; + queue_strings = res.1; selected = status.song.map_or(0, |song| song.pos); liststate = ListState::default(); liststate.select(Some(selected)); + if searching.load(Ordering::Acquire) { + tx.send(Command::UpdateSearch).await?; + } } Command::UpdateStatus => { status = mpd::status(&mut cl) @@ -373,11 +417,17 @@ async fn run() -> Result<()> { tx.send(Command::UpdateFrame).await?; } Command::Play => { - if selected < queue.len() { + if let Some(filtered) = &filtered { + if selected < filtered.len() { + mpd::play(&mut cl, filtered[selected]) + .await + .context("Failed to play the selected song")?; + } + } else if selected < queue.len() { mpd::play(&mut cl, selected) .await .context("Failed to play the selected song")?; - } + }; tx.send(Command::UpdateStatus).await?; tx.send(Command::UpdateFrame).await?; } @@ -387,7 +437,11 @@ async fn run() -> Result<()> { tx.send(Command::UpdateFrame).await?; } Command::Down => { - let len = queue.len(); + let len = if let Some(filtered) = &filtered { + filtered.len() + } else { + query.len() + }; if selected >= len { selected = status.song.map_or(0, |song| song.pos); } else if selected == len - 1 { @@ -401,7 +455,11 @@ async fn run() -> Result<()> { tx.send(Command::UpdateFrame).await?; } Command::Up => { - let len = queue.len(); + let len = if let Some(filtered) = &filtered { + filtered.len() + } else { + query.len() + }; if selected >= len { selected = status.song.map_or(0, |song| song.pos); } else if selected == 0 { @@ -415,7 +473,11 @@ async fn run() -> Result<()> { tx.send(Command::UpdateFrame).await?; } Command::JumpDown => { - let len = queue.len(); + let len = if let Some(filtered) = &filtered { + filtered.len() + } else { + query.len() + }; if selected >= len { selected = status.song.map_or(0, |song| song.pos); } else { @@ -429,7 +491,11 @@ async fn run() -> Result<()> { tx.send(Command::UpdateFrame).await?; } Command::JumpUp => { - let len = queue.len(); + let len = if let Some(filtered) = &filtered { + filtered.len() + } else { + query.len() + }; if selected >= len { selected = status.song.map_or(0, |song| song.pos); } else { @@ -442,6 +508,33 @@ async fn run() -> Result<()> { liststate.select(Some(selected)); tx.send(Command::UpdateFrame).await?; } + Command::InputSearch(c) => { + query.push(c); + tx.send(Command::UpdateSearch).await?; + } + Command::BackspaceSearch => { + query.pop(); + tx.send(Command::UpdateSearch).await?; + } + Command::UpdateSearch => { + let query = query.to_lowercase(); + let mut xs = Vec::new(); + for (i, track) in queue_strings.iter().enumerate() { + if track.contains(&query) { + xs.push(i); + } + } + filtered = Some(xs); + selected = 0; + liststate.select(Some(selected)); + tx.send(Command::UpdateFrame).await?; + } + Command::QuitSearch => { + searching.store(false, Ordering::Release); + filtered = None; + query.clear(); + tx.send(Command::UpdateFrame).await?; + } } } diff --git a/src/mpd.rs b/src/mpd.rs index de0c76a..40eee45 100644 --- a/src/mpd.rs +++ b/src/mpd.rs @@ -73,14 +73,15 @@ pub async fn idle(cl: &mut Client) -> Result<(bool, bool)> { Ok((queue, status)) } -pub async fn queue(cl: &mut Client) -> Result> { +pub async fn queue(cl: &mut Client) -> Result<(Vec, Vec)> { let mut first = true; let mut tracks = Vec::new(); + let mut track_strings = Vec::new(); - let mut file = None; - let mut artist = None; - let mut album = None; - let mut title = None; + let mut file: Option = None; + let mut artist: Option = None; + let mut album: Option = None; + let mut title: Option = None; let mut time = None; cl.write_all(b"playlistinfo\n").await?; @@ -93,6 +94,20 @@ pub async fn queue(cl: &mut Client) -> Result> { if first { first = false; } else if let (Some(file), Some(time)) = (file, time) { + let mut track_string = String::from(file.to_lowercase()); + if let Some(artist) = &artist { + track_string.push('\n'); + track_string.push_str(&artist.to_lowercase()); + } + if let Some(album) = &album { + track_string.push('\n'); + track_string.push_str(&album.to_lowercase()); + } + if let Some(title) = &title { + track_string.push('\n'); + track_string.push_str(&title.to_lowercase()); + } + track_strings.push(track_string); tracks.push(Track { file, artist, @@ -127,6 +142,20 @@ pub async fn queue(cl: &mut Client) -> Result> { } if let (Some(file), Some(time)) = (file, time) { + let mut track_string = String::from(file.to_lowercase()); + if let Some(artist) = &artist { + track_string.push('\n'); + track_string.push_str(&artist.to_lowercase()); + } + if let Some(album) = &album { + track_string.push('\n'); + track_string.push_str(&album.to_lowercase()); + } + if let Some(title) = &title { + track_string.push('\n'); + track_string.push_str(&title.to_lowercase()); + } + track_strings.push(track_string); tracks.push(Track { file, artist, @@ -136,7 +165,7 @@ pub async fn queue(cl: &mut Client) -> Result> { }); } - Ok(tracks) + Ok((tracks, track_strings)) } pub async fn status(cl: &mut Client) -> Result {