ability the search and filter the queue

This commit is contained in:
figsoda 2020-11-04 12:19:22 -05:00
parent 7029aaf030
commit 14faf62864
6 changed files with 389 additions and 118 deletions

View file

@ -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("@")),

View file

@ -59,6 +59,7 @@ pub enum Texts {
QueueTitle,
QueueArtist,
QueueAlbum,
Query,
Styled(Vec<AddStyle>, Box<Texts>),
Parts(Vec<Texts>),
If(Condition, Box<Texts>, Option<Box<Texts>>),
@ -103,6 +104,7 @@ pub enum Condition {
AlbumExist,
QueueCurrent,
Selected,
Searching,
Not(Box<Condition>),
And(Box<Condition>, Box<Condition>),
Or(Box<Condition>, Box<Condition>),
@ -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",

View file

@ -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(

View file

@ -17,6 +17,9 @@ pub fn render(
size: Rect,
widget: &Widget,
queue: &[Track],
searching: bool,
query: &str,
filtered: &Option<Vec<usize>>,
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,
)
}
}
}

View file

@ -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?;
}
}
}

View file

@ -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<Vec<Track>> {
pub async fn queue(cl: &mut Client) -> Result<(Vec<Track>, Vec<String>)> {
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<String> = None;
let mut artist: Option<String> = None;
let mut album: Option<String> = None;
let mut title: Option<String> = None;
let mut time = None;
cl.write_all(b"playlistinfo\n").await?;
@ -93,6 +94,20 @@ pub async fn queue(cl: &mut Client) -> Result<Vec<Track>> {
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<Vec<Track>> {
}
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<Vec<Track>> {
});
}
Ok(tracks)
Ok((tracks, track_strings))
}
pub async fn status(cl: &mut Client) -> Result<Status> {