diff --git a/Cargo.lock b/Cargo.lock index 56c4ef6..5c467d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,458 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "anyhow" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1fd36ffbb1fb7c834eac128ea8d0e310c5aeb635548f9d58861e1308d46e71c" + +[[package]] +name = "arc-swap" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d25d88fd6b8041580a654f9d0c581a047baee2b3efee13275f2fc392fc75034" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "bytes" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0dcbc35f504eb6fc275a6d20e4ebcda18cf50d40ba6fabff8c711fa16cb3b16" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "cloudabi" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467" +dependencies = [ + "bitflags", +] + +[[package]] +name = "crossterm" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f4919d60f26ae233e14233cc39746c8c8bb8cd7b05840ace83604917b51b6c7" +dependencies = [ + "bitflags", + "crossterm_winapi", + "lazy_static", + "libc", + "mio", + "parking_lot 0.10.2", + "signal-hook", + "winapi", +] + +[[package]] +name = "crossterm" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cef9149b29071d44c9fb98fd9c27fcf74405bbdb761889ad6a03f36be93b0b15" +dependencies = [ + "bitflags", + "crossterm_winapi", + "lazy_static", + "libc", + "mio", + "parking_lot 0.11.0", + "signal-hook", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2265c3f8e080075d9b6417aa72293fc71662f34b4af2612d8d1b074d29510db" +dependencies = [ + "winapi", +] + +[[package]] +name = "expand" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3fc822ad358d15f3cf1127d12f26a0cf0b0dcb26d9f1e33505d80689b7b3f1e" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "hermit-abi" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8" +dependencies = [ + "libc", +] + +[[package]] +name = "instant" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb1fc4429a33e1f80d41dc9fea4d108a88bec1de8053878898ae448a0b52f613" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" + +[[package]] +name = "lock_api" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "lock_api" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28247cc5a5be2f05fbcd76dd0cf2c7d3b5400cb978a28042abcd4fa0b3f8261c" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +dependencies = [ + "cfg-if 0.1.10", +] + +[[package]] +name = "memchr" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" + +[[package]] +name = "mio" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8f1c83949125de4a582aa2da15ae6324d91cf6a58a70ea407643941ff98f558" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07b88fb9795d4d36d62a012dfbf49a8f5cf12751f36d31a9dbe66d528e58979e" +dependencies = [ + "socket2", + "winapi", +] + [[package]] name = "mmtc" version = "0.1.0" +dependencies = [ + "anyhow", + "crossterm 0.18.1", + "expand", + "tokio", + "tui", +] + +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "parking_lot" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" +dependencies = [ + "lock_api 0.3.4", + "parking_lot_core 0.7.2", +] + +[[package]] +name = "parking_lot" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4893845fa2ca272e647da5d0e46660a314ead9c2fdd9a883aabc32e481a8733" +dependencies = [ + "instant", + "lock_api 0.4.1", + "parking_lot_core 0.8.0", +] + +[[package]] +name = "parking_lot_core" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" +dependencies = [ + "cfg-if 0.1.10", + "cloudabi 0.0.3", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c361aa727dd08437f2f1447be8b59a33b0edd15e0fcee698f935613d9efbca9b" +dependencies = [ + "cfg-if 0.1.10", + "cloudabi 0.1.0", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "pin-project-lite" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c917123afa01924fc84bb20c4c03f004d9c38e5127e3c039bbf7f4b9c76a2f6b" + +[[package]] +name = "proc-macro2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "signal-hook" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604508c1418b99dfe1925ca9224829bb2a8a9a04dda655cc01fcad46f4ab05ed" +dependencies = [ + "libc", + "mio", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e12110bc539e657a646068aaf5eb5b63af9d0c1f7b29c97113fad80e15f035" +dependencies = [ + "arc-swap", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" + +[[package]] +name = "smallvec" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbee7696b84bbf3d89a1c2eccff0850e3047ed46bfcd2e92c29a2d074d57e252" + +[[package]] +name = "socket2" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1fa70dc5c8104ec096f4fe7ede7a221d35ae13dcd19ba1ad9a81d2cab9a1c44" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "redox_syscall", + "winapi", +] + +[[package]] +name = "syn" +version = "1.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc371affeffc477f42a221a1e4297aedcea33d47d19b61455588bd9d8f6b19ac" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tokio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71f1b20504fd0aa9dab3ae17e8c4dd9431e5e08fd6921689f9745a4004883a17" +dependencies = [ + "bytes", + "fnv", + "lazy_static", + "libc", + "memchr", + "mio", + "num_cpus", + "pin-project-lite", + "slab", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21d30fdbb5dc2d8f91049691aa1a9d4d4ae422a21c334ce8936e5886d30c5c45" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tui" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2eaeee894a1e9b90f80aa466fe59154fdb471980b5e104d8836fcea309ae17e" +dependencies = [ + "bitflags", + "cassowary", + "crossterm 0.17.7", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-segmentation" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index c329abd..d839445 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,14 @@ repository = "https://github.com/figsoda/mmtc" readme = "README.md" [dependencies] +anyhow = "1.0.33" +crossterm = "0.18.1" +expand = "0.1.2" +tui = { version = "0.12.0", default-features = false, features = ["crossterm"] } + +[dependencies.tokio] +version = "0.3.2" +features = ["io-util", "macros", "net", "rt-multi-thread", "sync", "time"] [profile.release] lto = true diff --git a/src/fail.rs b/src/fail.rs new file mode 100644 index 0000000..44f8672 --- /dev/null +++ b/src/fail.rs @@ -0,0 +1,9 @@ +macro_rules! fail { + ($n:ident $($a:ident)+ = $m:literal) => { + pub fn $n($( $a: impl std::fmt::Display ),+) -> impl FnOnce() -> String { + move || format!($m, $( $a ),+) + } + }; +} + +fail!(connect addr = "Failed to connect to {}"); diff --git a/src/main.rs b/src/main.rs index e7a11a9..193adc5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,200 @@ -fn main() { - println!("Hello, world!"); +#![feature(async_closure)] +#![forbid(unsafe_code)] + +mod fail; +mod mpd; + +use anyhow::{Context, Result}; +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use tokio::{ + sync::Mutex, + time::{sleep_until, Duration, Instant}, +}; +use tui::{ + backend::CrosstermBackend, + layout::{Constraint, Direction, Layout, Rect}, + widgets::{List, ListItem, Paragraph}, + Terminal, +}; + +use std::{ + io::{stdout, Write}, + net::{IpAddr, Ipv4Addr, SocketAddr}, + process::exit, + sync::Arc, +}; + +use crate::mpd::{Song, Status, Track}; + +fn cleanup() -> Result<()> { + disable_raw_mode().context("Failed to clean up terminal")?; + execute!(stdout(), LeaveAlternateScreen, DisableMouseCapture) + .context("Failed to clean up terminal")?; + Ok(()) +} + +fn die(e: impl std::fmt::Display) -> T { + if let Err(e) = cleanup() { + eprintln!("{}", e); + }; + eprintln!("{}", e); + exit(1); +} + +#[tokio::main] +async fn main() -> Result<()> { + let res = run().await; + cleanup()?; + res +} + +async fn run() -> Result<()> { + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 6600); + + let queue_layout = Layout::default() + .direction(Direction::Horizontal) + .constraints(vec![ + Constraint::Ratio(1, 3), + Constraint::Ratio(1, 3), + Constraint::Ratio(1, 3), + ]); + + let mut idle_cl = mpd::init(addr).await.with_context(fail::connect(addr))?; + let mut status_cl = mpd::init(addr).await.with_context(fail::connect(addr))?; + + let queue = Arc::new(Mutex::new(mpd::queue(&mut idle_cl).await?)); + let queue1 = Arc::clone(&queue); + let status = Arc::new(Mutex::new(mpd::status(&mut status_cl).await?)); + let status1 = Arc::clone(&status); + + tokio::spawn(async move { + loop { + mpd::idle_playlist(&mut idle_cl) + .await + .context("Failed to idle") + .unwrap_or_else(die); + *queue1.lock().await = mpd::queue(&mut idle_cl) + .await + .context("Failed to query queue information") + .unwrap_or_else(die); + } + }); + + tokio::spawn(async move { + loop { + let deadline = Instant::now() + Duration::from_millis(250); + *status1.lock().await = mpd::status(&mut status_cl) + .await + .context("Failed to query status") + .unwrap_or_else(die); + sleep_until(deadline).await; + } + }); + + let mut stdout = stdout(); + enable_raw_mode().context("Failed to initialize terminal")?; + execute!(stdout, EnterAlternateScreen, EnableMouseCapture) + .context("Failed to initialize terminal")?; + let mut term = + Terminal::new(CrosstermBackend::new(stdout)).context("Failed to initialize terminal")?; + + loop { + let deadline = Instant::now() + Duration::from_secs_f32(1.0 / 30.0); + + let queue = &*queue.lock().await; + let status = (*status.lock().await).clone(); + + term.draw(|frame| { + let len = queue.len(); + let mut titles = Vec::with_capacity(len); + let mut artists = Vec::with_capacity(len); + let mut albums = Vec::with_capacity(len); + + for Track { + title, + artist, + album, + .. + } in queue + { + titles.push(ListItem::new(title.clone().unwrap_or_default())); + artists.push(ListItem::new(artist.clone().unwrap_or_default())); + albums.push(ListItem::new(album.clone().unwrap_or_default())); + } + + let Rect { + x, + y, + width, + height, + } = frame.size(); + let chunks = queue_layout.split(Rect { + x, + y, + width, + height: y + height - 1, + }); + frame.render_widget(List::new(titles), chunks[0]); + frame.render_widget(List::new(artists), chunks[1]); + frame.render_widget(List::new(albums), chunks[2]); + + if let Status { + song: Some(Song { pos, elapsed }), + .. + } = status + { + if let Some(Track { + file, + artist, + album, + title, + time, + }) = queue.get(pos) + { + frame.render_widget( + Paragraph::new(format!( + "[{:02}:{:02}/{:02}:{:02}] {}", + elapsed / 60, + elapsed % 60, + time / 60, + time % 60, + match (title, artist, album) { + (Some(title), Some(artist), Some(album)) => + format!("{} - {} - {}", title, artist, album), + (Some(title), Some(artist), _) => format!("{} - {}", title, artist), + (Some(title), ..) => title.clone(), + _ => file.clone(), + } + )), + Rect { + x, + y: y + height - 1, + width, + height: 1, + }, + ) + } + } + }) + .context("Failed to draw to terminal")?; + + while event::poll(Duration::new(0, 0)).context("Failed to poll events")? { + match event::read().context("Failed to read events")? { + Event::Key(KeyEvent { code, .. }) => match code { + KeyCode::Char('q') | KeyCode::Esc => { + cleanup()?; + return Ok(()); + } + _ => (), + }, + _ => (), + } + } + + sleep_until(deadline).await; + } } diff --git a/src/mpd.rs b/src/mpd.rs new file mode 100644 index 0000000..6ed19d0 --- /dev/null +++ b/src/mpd.rs @@ -0,0 +1,177 @@ +use anyhow::{bail, Context, Result}; +use expand::expand; +use tokio::{ + io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}, + net::TcpStream, +}; + +use std::net::SocketAddr; + +use crate::fail; + +pub type Client = BufReader; + +#[derive(Clone)] +pub struct Status { + pub repeat: bool, + pub random: bool, + pub single: Option, // None: oneshot + pub consume: bool, + pub song: Option, +} + +#[derive(Clone)] +pub struct Song { + pub pos: usize, + pub elapsed: u16, +} + +#[derive(Clone)] +pub struct Track { + pub file: String, + pub artist: Option, + pub album: Option, + pub title: Option, + pub time: u16, +} + +pub async fn init(addr: SocketAddr) -> Result { + let mut cl = BufReader::new( + TcpStream::connect(&addr) + .await + .with_context(fail::connect(addr))?, + ); + + let mut buf = [0; 7]; + cl.read(&mut buf).await?; + if &buf != b"OK MPD " { + bail!("server did not greet with a success"); + } + cl.read_line(&mut String::new()).await?; + + Ok(cl) +} + +pub async fn idle_playlist(cl: &mut Client) -> Result<()> { + cl.write_all(b"idle playlist\n").await?; + let mut lines = cl.lines(); + + while let Some(line) = lines.next_line().await? { + match line.as_bytes() { + b"OK" => break, + _ => continue, + } + } + + Ok(()) +} + +pub async fn queue(cl: &mut Client) -> Result> { + let mut first = true; + let mut tracks = Vec::new(); + + let mut file = None; + let mut artist = None; + let mut album = None; + let mut title = None; + let mut time = None; + + cl.write_all(b"playlistinfo\n").await?; + let mut lines = cl.lines(); + + while let Some(line) = lines.next_line().await? { + match line.as_bytes() { + b"OK" => break, + expand!([@b"file: ", xs @ ..]) => { + if first { + first = false; + } else { + if let (Some(file), Some(time)) = (file, time) { + tracks.push(Track { + file, + artist, + album, + title, + time, + }); + } else { + bail!("incomplete playlist response"); + } + } + + file = Some(String::from_utf8_lossy(xs).into()); + artist = None; + album = None; + title = None; + time = None; + } + expand!([@b"Artist: ", xs @ ..]) => { + artist = Some(String::from_utf8_lossy(xs).into()); + } + expand!([@b"Album: ", xs @ ..]) => { + album = Some(String::from_utf8_lossy(xs).into()); + } + expand!([@b"Title: ", xs @ ..]) => { + title = Some(String::from_utf8_lossy(xs).into()); + } + expand!([@b"Time: ", xs @ ..]) => { + time = Some(String::from_utf8_lossy(xs).parse()?); + } + _ => continue, + } + } + + Ok(tracks) +} + +pub async fn status(cl: &mut Client) -> Result { + let mut repeat = None; + let mut random = None; + let mut single = None; + let mut consume = None; + let mut pos = None; + let mut elapsed = None; + + cl.write_all(b"status\n").await?; + let mut lines = cl.lines(); + + while let Some(line) = lines.next_line().await? { + match line.as_bytes() { + b"OK" => break, + b"repeat: 0" => repeat = Some(false), + b"repeat: 1" => repeat = Some(true), + b"random: 0" => random = Some(false), + b"random: 1" => random = Some(true), + b"single: 0" => single = Some(Some(false)), + b"single: 1" => single = Some(Some(true)), + b"single: oneshot" => single = Some(None), + b"consume: 0" => consume = Some(false), + b"consume: 1" => consume = Some(true), + expand!([@b"song: ", xs @ ..]) => { + pos = Some(String::from_utf8_lossy(xs).parse()?); + } + expand!([@b"elapsed: ", xs @ ..]) => { + elapsed = Some(String::from_utf8_lossy(xs).parse::()?.round() as u16); + } + _ => continue, + } + } + + if let (Some(repeat), Some(random), Some(single), Some(consume)) = + (repeat, random, single, consume) + { + Ok(Status { + repeat, + random, + single, + consume, + song: if let (Some(pos), Some(elapsed)) = (pos, elapsed) { + Some(Song { pos, elapsed }) + } else { + None + }, + }) + } else { + bail!("incomplete status response"); + } +}