chore: configure clippy & fix lints

This commit is contained in:
Tal 2025-12-04 20:53:19 +01:00
parent 1c8c788d76
commit a87a8cc59e
19 changed files with 163 additions and 120 deletions

View File

@ -60,3 +60,17 @@ indicatif = { version = "0.18.0", optional = true }
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
libc = "0.2.167" libc = "0.2.167"
[lints.clippy]
all = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
nursery = { level = "warn", priority = -1 }
unwrap_in_result = "warn"
missing_docs_in_private_items = "warn"
missing_errors_doc = "allow"
missing_panics_doc = "allow"
must_use_candidate = "allow"
cast_precision_loss = "allow"
cast_sign_loss = "allow"
cast_possible_truncation = "allow"

View File

@ -25,7 +25,7 @@ pub fn silent_get_output_stream() -> eyre::Result<rodio::OutputStream, crate::Er
// SAFETY: Simple enough to be impossible to fail. Hopefully. // SAFETY: Simple enough to be impossible to fail. Hopefully.
unsafe { unsafe {
freopen(null.as_ptr(), mode.as_ptr(), stderr); freopen(null.as_ptr(), mode.as_ptr(), stderr);
} };
// Make the OutputStream while stderr is still redirected to /dev/null. // Make the OutputStream while stderr is still redirected to /dev/null.
let stream = OutputStreamBuilder::open_default_stream()?; let stream = OutputStreamBuilder::open_default_stream()?;
@ -36,7 +36,7 @@ pub fn silent_get_output_stream() -> eyre::Result<rodio::OutputStream, crate::Er
// SAFETY: See the first call to `freopen`. // SAFETY: See the first call to `freopen`.
unsafe { unsafe {
freopen(tty.as_ptr(), mode.as_ptr(), stderr); freopen(tty.as_ptr(), mode.as_ptr(), stderr);
} };
Ok(stream) Ok(stream)
} }

View File

@ -23,7 +23,7 @@ impl Handle {
let notify = Arc::new(Notify::new()); let notify = Arc::new(Notify::new());
Self { Self {
task: task::spawn(Self::waiter(sink, tx, notify.clone())), task: task::spawn(Self::waiter(sink, tx, Arc::clone(&notify))),
notify, notify,
} }
} }
@ -40,7 +40,7 @@ impl Handle {
time::sleep(Duration::from_millis(8)).await; time::sleep(Duration::from_millis(8)).await;
} }
if let Err(_) = tx.try_send(crate::Message::Next) { if tx.try_send(crate::Message::Next).is_err() {
break; break;
}; };
} }

View File

@ -46,7 +46,7 @@ impl Bookmarks {
if x.is_empty() { if x.is_empty() {
None None
} else { } else {
Some(x.to_string()) Some(x.to_owned())
} }
}) })
.collect(); .collect();
@ -64,7 +64,7 @@ impl Bookmarks {
/// Bookmarks a given track with a full path and optional custom name. /// Bookmarks a given track with a full path and optional custom name.
/// ///
/// Returns whether the track is now bookmarked, or not. /// Returns whether the track is now bookmarked, or not.
pub async fn bookmark(&mut self, track: &tracks::Info) -> Result<bool> { pub fn bookmark(&mut self, track: &tracks::Info) -> Result<bool> {
let entry = track.to_entry(); let entry = track.to_entry();
let idx = self.entries.iter().position(|x| **x == entry); let idx = self.entries.iter().position(|x| **x == entry);

View File

@ -15,6 +15,8 @@ static LOADING: AtomicBool = AtomicBool::new(false);
pub(crate) static PROGRESS: AtomicU8 = AtomicU8::new(0); pub(crate) static PROGRESS: AtomicU8 = AtomicU8::new(0);
pub type Progress = &'static AtomicU8; pub type Progress = &'static AtomicU8;
/// The downloader, which has all of the state necessary
/// to download tracks and add them to the queue.
pub struct Downloader { pub struct Downloader {
queue: Sender<tracks::Queued>, queue: Sender<tracks::Queued>,
tx: Sender<crate::Message>, tx: Sender<crate::Message>,
@ -24,7 +26,10 @@ pub struct Downloader {
} }
impl Downloader { impl Downloader {
pub async fn init(size: usize, tracks: tracks::List, tx: Sender<crate::Message>) -> Handle { /// Initializes the downloader with a track list.
///
/// `tx` specifies the [`Sender`] to be notified with [`crate::Message::Loaded`].
pub fn init(size: usize, tracks: tracks::List, tx: Sender<crate::Message>) -> Handle {
let client = Client::new(); let client = Client::new();
let (qtx, qrx) = mpsc::channel(size - 1); let (qtx, qrx) = mpsc::channel(size - 1);
@ -64,25 +69,30 @@ impl Downloader {
} }
} }
} }
/// Downloader handle, responsible for managing
/// the downloader task and internal buffer.
pub struct Handle { pub struct Handle {
queue: Receiver<tracks::Queued>, queue: Receiver<tracks::Queued>,
handle: JoinHandle<crate::Result<()>>, handle: JoinHandle<crate::Result<()>>,
} }
/// The output when a track is requested from the downloader.
pub enum Output { pub enum Output {
Loading(Option<Progress>), Loading(Option<Progress>),
Queued(tracks::Queued), Queued(tracks::Queued),
} }
impl Handle { impl Handle {
pub async fn track(&mut self) -> Output { /// Gets either a queued track, or a progress report,
match self.queue.try_recv() { /// depending on the state of the internal download buffer.
Ok(queued) => Output::Queued(queued), #[rustfmt::skip]
Err(_) => { pub fn track(&mut self) -> Output {
self.queue.try_recv().map_or_else(|_| {
LOADING.store(true, atomic::Ordering::Relaxed); LOADING.store(true, atomic::Ordering::Relaxed);
Output::Loading(Some(&PROGRESS)) Output::Loading(Some(&PROGRESS))
} }, Output::Queued,
} )
} }
} }

View File

@ -1,6 +1,4 @@
//! An extremely simple lofi player. //! An extremely simple lofi player.
#![warn(clippy::all, clippy::pedantic, clippy::nursery)]
pub mod error; pub mod error;
use std::path::PathBuf; use std::path::PathBuf;
@ -98,23 +96,21 @@ pub fn data_dir() -> crate::Result<PathBuf> {
async fn main() -> eyre::Result<()> { async fn main() -> eyre::Result<()> {
let args = Args::parse(); let args = Args::parse();
if let Some(command) = args.command {
match command {
#[cfg(feature = "scrape")] #[cfg(feature = "scrape")]
if let Some(command) = &args.command {
match command {
Commands::Scrape { source } => match source { Commands::Scrape { source } => match source {
Source::Archive => scrapers::archive::scrape().await?, Source::Archive => scrapers::archive::scrape().await?,
Source::Lofigirl => scrapers::lofigirl::scrape().await?, Source::Lofigirl => scrapers::lofigirl::scrape().await?,
Source::Chillhop => scrapers::chillhop::scrape().await?, Source::Chillhop => scrapers::chillhop::scrape().await?,
}, },
} }
} else { }
let player = Player::init(args).await?; let player = Player::init(args).await?;
let environment = player.environment(); let environment = player.environment();
let result = player.run().await; let result = player.run().await;
environment.cleanup(result.is_ok())?; environment.cleanup(result.is_ok())?;
result?; Ok(result?)
};
Ok(())
} }

View File

@ -23,13 +23,13 @@ pub enum Current {
impl Default for Current { impl Default for Current {
fn default() -> Self { fn default() -> Self {
Current::Loading(None) Self::Loading(None)
} }
} }
impl Current { impl Current {
pub fn loading(&self) -> bool { pub const fn loading(&self) -> bool {
return matches!(self, Current::Loading(_)); matches!(self, Self::Loading(_))
} }
} }
@ -53,25 +53,25 @@ impl Drop for Player {
} }
impl Player { impl Player {
pub fn environment(&self) -> ui::Environment { pub const fn environment(&self) -> ui::Environment {
self.ui.environment self.ui.environment
} }
pub async fn set_current(&mut self, current: Current) -> crate::Result<()> { pub fn set_current(&mut self, current: Current) -> crate::Result<()> {
self.current = current.clone(); self.current = current.clone();
self.update(ui::Update::Track(current)).await?; self.update(ui::Update::Track(current))?;
let Current::Track(track) = &self.current else { let Current::Track(track) = &self.current else {
return Ok(()); return Ok(());
}; };
let bookmarked = self.bookmarks.bookmarked(&track); let bookmarked = self.bookmarks.bookmarked(track);
self.update(ui::Update::Bookmarked(bookmarked)).await?; self.update(ui::Update::Bookmarked(bookmarked))?;
Ok(()) Ok(())
} }
pub async fn update(&mut self, update: ui::Update) -> crate::Result<()> { pub fn update(&mut self, update: ui::Update) -> crate::Result<()> {
self.broadcast.send(update)?; self.broadcast.send(update)?;
Ok(()) Ok(())
} }
@ -87,21 +87,19 @@ impl Player {
let (tx, rx) = mpsc::channel(8); let (tx, rx) = mpsc::channel(8);
tx.send(Message::Init).await?; tx.send(Message::Init).await?;
let (utx, urx) = broadcast::channel(8); let (utx, urx) = broadcast::channel(8);
let current = Current::Loading(None);
let list = List::load(args.track_list.as_ref()).await?; let list = List::load(args.track_list.as_ref()).await?;
let state = let state = ui::State::initial(Arc::clone(&sink), args.width, list.name.clone());
ui::State::initial(sink.clone(), args.width, current.clone(), list.name.clone());
let volume = PersistentVolume::load().await?; let volume = PersistentVolume::load().await?;
sink.set_volume(volume.float()); sink.set_volume(volume.float());
Ok(Self { Ok(Self {
ui: ui::Handle::init(tx.clone(), urx, state, &args).await?, ui: ui::Handle::init(tx.clone(), urx, state, &args).await?,
downloader: Downloader::init(args.buffer_size as usize, list, tx.clone()).await, downloader: Downloader::init(args.buffer_size as usize, list, tx.clone()),
waiter: waiter::Handle::new(sink.clone(), tx.clone()), waiter: waiter::Handle::new(Arc::clone(&sink), tx.clone()),
bookmarks: Bookmarks::load().await?, bookmarks: Bookmarks::load().await?,
current, current: Current::default(),
broadcast: utx, broadcast: utx,
rx, rx,
sink, sink,
@ -112,15 +110,15 @@ impl Player {
pub async fn close(&self) -> crate::Result<()> { pub async fn close(&self) -> crate::Result<()> {
self.bookmarks.save().await?; self.bookmarks.save().await?;
PersistentVolume::save(self.sink.volume() as f32).await?; PersistentVolume::save(self.sink.volume()).await?;
Ok(()) Ok(())
} }
pub async fn play(&mut self, queued: tracks::Queued) -> crate::Result<()> { pub fn play(&mut self, queued: tracks::Queued) -> crate::Result<()> {
let decoded = queued.decode()?; let decoded = queued.decode()?;
self.sink.append(decoded.data); self.sink.append(decoded.data);
self.set_current(Current::Track(decoded.info)).await?; self.set_current(Current::Track(decoded.info))?;
self.waiter.notify(); self.waiter.notify();
Ok(()) Ok(())
@ -135,12 +133,12 @@ impl Player {
} }
self.sink.stop(); self.sink.stop();
match self.downloader.track().await { match self.downloader.track() {
download::Output::Loading(progress) => { download::Output::Loading(progress) => {
self.set_current(Current::Loading(progress)).await?; self.set_current(Current::Loading(progress))?;
} }
download::Output::Queued(queued) => { download::Output::Queued(queued) => {
self.play(queued).await?; self.play(queued)?;
} }
}; };
} }
@ -160,19 +158,19 @@ impl Player {
Message::ChangeVolume(change) => { Message::ChangeVolume(change) => {
self.sink self.sink
.set_volume((self.sink.volume() + change).clamp(0.0, 1.0)); .set_volume((self.sink.volume() + change).clamp(0.0, 1.0));
self.update(ui::Update::Volume).await?; self.update(ui::Update::Volume)?;
} }
Message::SetVolume(set) => { Message::SetVolume(set) => {
self.sink.set_volume(set.clamp(0.0, 1.0)); self.sink.set_volume(set.clamp(0.0, 1.0));
self.update(ui::Update::Volume).await?; self.update(ui::Update::Volume)?;
} }
Message::Bookmark => { Message::Bookmark => {
let Current::Track(current) = &self.current else { let Current::Track(current) = &self.current else {
continue; continue;
}; };
let bookmarked = self.bookmarks.bookmark(current).await?; let bookmarked = self.bookmarks.bookmark(current)?;
self.update(ui::Update::Bookmarked(bookmarked)).await?; self.update(ui::Update::Bookmarked(bookmarked))?;
} }
Message::Quit => break, Message::Quit => break,
} }

View File

@ -11,8 +11,8 @@ mod bookmark {
} }
} }
#[tokio::test] #[test]
async fn toggle_and_check() { fn toggle_and_check() {
let mut bm = Bookmarks { entries: vec![] }; let mut bm = Bookmarks { entries: vec![] };
let info = test_info("p.mp3", "Nice Track"); let info = test_info("p.mp3", "Nice Track");
@ -20,37 +20,37 @@ mod bookmark {
assert!(!bm.bookmarked(&info)); assert!(!bm.bookmarked(&info));
// bookmark it // bookmark it
let added = bm.bookmark(&info).await.unwrap(); let added = bm.bookmark(&info).unwrap();
assert!(added); assert!(added);
assert!(bm.bookmarked(&info)); assert!(bm.bookmarked(&info));
// un-bookmark it // un-bookmark it
let removed = bm.bookmark(&info).await.unwrap(); let removed = bm.bookmark(&info).unwrap();
assert!(!removed); assert!(!removed);
assert!(!bm.bookmarked(&info)); assert!(!bm.bookmarked(&info));
} }
#[tokio::test] #[test]
async fn multiple_bookmarks() { fn multiple_bookmarks() {
let mut bm = Bookmarks { entries: vec![] }; let mut bm = Bookmarks { entries: vec![] };
let info1 = test_info("track1.mp3", "Track One"); let info1 = test_info("track1.mp3", "Track One");
let info2 = test_info("track2.mp3", "Track Two"); let info2 = test_info("track2.mp3", "Track Two");
bm.bookmark(&info1).await.unwrap(); bm.bookmark(&info1).unwrap();
bm.bookmark(&info2).await.unwrap(); bm.bookmark(&info2).unwrap();
assert!(bm.bookmarked(&info1)); assert!(bm.bookmarked(&info1));
assert!(bm.bookmarked(&info2)); assert!(bm.bookmarked(&info2));
assert_eq!(bm.entries.len(), 2); assert_eq!(bm.entries.len(), 2);
} }
#[tokio::test] #[test]
async fn duplicate_bookmark_removes() { fn duplicate_bookmark_removes() {
let mut bm = Bookmarks { entries: vec![] }; let mut bm = Bookmarks { entries: vec![] };
let info = test_info("x.mp3", "X"); let info = test_info("x.mp3", "X");
bm.bookmark(&info).await.unwrap(); bm.bookmark(&info).unwrap();
let is_added = bm.bookmark(&info).await.unwrap(); let is_added = bm.bookmark(&info).unwrap();
assert!(!is_added); assert!(!is_added);
assert!(bm.entries.is_empty()); assert!(bm.entries.is_empty());

View File

@ -79,7 +79,7 @@ mod window {
#[test] #[test]
fn simple() { fn simple() {
let mut w = Window::new(3, false); let w = Window::new(3, false);
let (render, height) = w.render(vec![String::from("abc")], false, true).unwrap(); let (render, height) = w.render(vec![String::from("abc")], false, true).unwrap();
const MIDDLE: &str = "─────"; const MIDDLE: &str = "─────";
@ -89,7 +89,7 @@ mod window {
#[test] #[test]
fn spaced() { fn spaced() {
let mut w = Window::new(3, false); let w = Window::new(3, false);
let (render, height) = w let (render, height) = w
.render( .render(
vec![String::from("abc"), String::from(" b"), String::from("c")], vec![String::from("abc"), String::from(" b"), String::from("c")],
@ -137,7 +137,7 @@ mod interface {
#[test] #[test]
fn loading() { fn loading() {
let sink = Arc::new(rodio::Sink::new().0); let sink = Arc::new(rodio::Sink::new().0);
let mut state = State::initial(sink, 3, Current::Loading(None), String::from("test")); let mut state = State::initial(sink, 3, String::from("test"));
let menu = interface::menu(&mut state, Params::default()); let menu = interface::menu(&mut state, Params::default());
assert_eq!(menu[0], "loading "); assert_eq!(menu[0], "loading ");
@ -157,7 +157,7 @@ mod interface {
fn volume() { fn volume() {
let sink = Arc::new(rodio::Sink::new().0); let sink = Arc::new(rodio::Sink::new().0);
sink.set_volume(0.5); sink.set_volume(0.5);
let mut state = State::initial(sink, 3, Current::Loading(None), String::from("test")); let mut state = State::initial(sink, 3, String::from("test"));
state.timer = Some(Instant::now()); state.timer = Some(Instant::now());
let menu = interface::menu(&mut state, Params::default()); let menu = interface::menu(&mut state, Params::default());
@ -179,12 +179,9 @@ mod interface {
fn progress() { fn progress() {
let sink = Arc::new(rodio::Sink::new().0); let sink = Arc::new(rodio::Sink::new().0);
PROGRESS.store(50, std::sync::atomic::Ordering::Relaxed); PROGRESS.store(50, std::sync::atomic::Ordering::Relaxed);
let mut state = State::initial( let mut state = State::initial(sink, 3, String::from("test"));
sink, state.current = Current::Loading(Some(&PROGRESS));
3,
Current::Loading(Some(&PROGRESS)),
String::from("test"),
);
let menu = interface::menu(&mut state, Params::default()); let menu = interface::menu(&mut state, Params::default());
assert_eq!(menu[0], format!("loading {} ", "50%".bold())); assert_eq!(menu[0], format!("loading {} ", "50%".bold()));
@ -210,8 +207,8 @@ mod interface {
duration: Some(Duration::from_secs(8)), duration: Some(Duration::from_secs(8)),
}; };
let current = Current::Track(track.clone()); let mut state = State::initial(sink, 3, String::from("test"));
let mut state = State::initial(sink, 3, current, String::from("test")); state.current = Current::Track(track.clone());
let menu = interface::menu(&mut state, Params::default()); let menu = interface::menu(&mut state, Params::default());
assert_eq!( assert_eq!(

View File

@ -2,24 +2,18 @@
//! of tracks, as well as downloading them & finding new ones. //! of tracks, as well as downloading them & finding new ones.
//! //!
//! There are several structs which represent the different stages //! There are several structs which represent the different stages
//! that go on in downloading and playing tracks. The proccess for fetching tracks, //! that go on in downloading and playing tracks. When first queued,
//! and what structs are relevant in each step, are as follows. //! the downloader will return a [`Queued`] track.
//! //!
//! First Stage, when a track is initially fetched. //! Then, when it's time to play the track, it is decoded into
//! 1. Raw entry selected from track list. //! a [`Decoded`] track, which includes all the information
//! 2. Raw entry split into path & display name. //! in the form of [`Info`].
//! 3. Track data fetched, and [`QueuedTrack`] is created which includes a [`TrackName`] that may be raw.
//!
//! Second Stage, when a track is played.
//! 1. Track data is decoded.
//! 2. [`Info`] created from decoded data.
//! 3. [`Decoded`] made from [`Info`] and the original decoded data.
use std::{fmt::Debug, io::Cursor, time::Duration}; use std::{fmt::Debug, io::Cursor, time::Duration};
use bytes::Bytes; use bytes::Bytes;
use rodio::{Decoder, Source as _}; use rodio::{Decoder, Source as _};
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation as _;
pub mod list; pub mod list;
pub use list::List; pub use list::List;
@ -27,7 +21,7 @@ pub mod error;
pub mod format; pub mod format;
pub use error::{Error, Result}; pub use error::{Error, Result};
use crate::tracks::error::WithTrackContext; use crate::tracks::error::WithTrackContext as _;
/// Just a shorthand for a decoded [Bytes]. /// Just a shorthand for a decoded [Bytes].
pub type DecodedData = Decoder<Cursor<Bytes>>; pub type DecodedData = Decoder<Cursor<Bytes>>;
@ -35,7 +29,7 @@ pub type DecodedData = Decoder<Cursor<Bytes>>;
/// Tracks which are still waiting in the queue, and can't be played yet. /// Tracks which are still waiting in the queue, and can't be played yet.
/// ///
/// This means that only the data & track name are included. /// This means that only the data & track name are included.
#[derive(PartialEq)] #[derive(PartialEq, Eq)]
pub struct Queued { pub struct Queued {
/// Display name of the track. /// Display name of the track.
pub display: String, pub display: String,
@ -66,6 +60,7 @@ impl Queued {
Decoded::new(self) Decoded::new(self)
} }
/// Creates a new queued track.
pub fn new(path: String, data: Bytes, display: Option<String>) -> Result<Self> { pub fn new(path: String, data: Bytes, display: Option<String>) -> Result<Self> {
let display = match display { let display = match display {
None => self::format::name(&path)?, None => self::format::name(&path)?,
@ -73,8 +68,8 @@ impl Queued {
}; };
Ok(Self { Ok(Self {
path,
display, display,
path,
data, data,
}) })
} }
@ -122,7 +117,7 @@ impl Info {
} }
} }
/// This struct is seperate from [Track] since it is generated lazily from /// This struct is separate from [Track] since it is generated lazily from
/// a track, and not when the track is first downloaded. /// a track, and not when the track is first downloaded.
pub struct Decoded { pub struct Decoded {
/// Has both the formatted name and some information from the decoded data. /// Has both the formatted name and some information from the decoded data.
@ -138,7 +133,7 @@ impl Decoded {
pub fn new(track: Queued) -> Result<Self> { pub fn new(track: Queued) -> Result<Self> {
let (path, display) = (track.path.clone(), track.display.clone()); let (path, display) = (track.path.clone(), track.display.clone());
let data = Decoder::builder() let data = Decoder::builder()
.with_byte_len(track.data.len().try_into().unwrap()) .with_byte_len(track.data.len().try_into()?)
.with_data(Cursor::new(track.data)) .with_data(Cursor::new(track.data))
.build() .build()
.track(track.display)?; .track(track.display)?;

View File

@ -19,6 +19,9 @@ pub enum Kind {
#[error("unable to fetch data: {0}")] #[error("unable to fetch data: {0}")]
Request(#[from] reqwest::Error), Request(#[from] reqwest::Error),
#[error("couldn't handle integer track length: {0}")]
Integer(#[from] std::num::TryFromIntError),
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View File

@ -1,10 +1,10 @@
use convert_case::{Case, Casing}; use convert_case::{Case, Casing as _};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use regex::Regex; use regex::Regex;
use std::path::Path; use std::path::Path;
use url::form_urlencoded; use url::form_urlencoded;
use super::error::WithTrackContext; use super::error::WithTrackContext as _;
lazy_static! { lazy_static! {
static ref MASTER_PATTERNS: [Regex; 5] = [ static ref MASTER_PATTERNS: [Regex; 5] = [
@ -84,7 +84,7 @@ pub fn name(name: &str) -> super::Result<String> {
// If the entire name of the track is a number, then just return it. // If the entire name of the track is a number, then just return it.
if skip == name.len() { if skip == name.len() {
Ok(name.trim().to_string()) Ok(name.trim().to_owned())
} else { } else {
// We've already checked before that the bound is at an ASCII digit. // We've already checked before that the bound is at an ASCII digit.
#[allow(clippy::string_slice)] #[allow(clippy::string_slice)]

View File

@ -6,8 +6,8 @@ use std::{
sync::atomic::{AtomicU8, Ordering}, sync::atomic::{AtomicU8, Ordering},
}; };
use bytes::{BufMut, Bytes, BytesMut}; use bytes::{BufMut as _, Bytes, BytesMut};
use futures::StreamExt; use futures::StreamExt as _;
use reqwest::Client; use reqwest::Client;
use tokio::fs; use tokio::fs;
@ -15,7 +15,7 @@ use crate::{
data_dir, data_dir,
tracks::{ tracks::{
self, self,
error::{self, WithTrackContext}, error::{self, WithTrackContext as _},
}, },
}; };
@ -114,7 +114,7 @@ impl List {
while let Some(item) = stream.next().await { while let Some(item) = stream.next().await {
let chunk = item.track(track)?; let chunk = item.track(track)?;
downloaded = min(downloaded + (chunk.len() as u64), total); downloaded = min(downloaded + (chunk.len() as u64), total);
let rounded = ((downloaded as f32) / (total as f32) * 100.0).round() as u8; let rounded = ((downloaded as f64) / (total as f64) * 100.0).round() as u8;
progress.store(rounded, Ordering::Relaxed); progress.store(rounded, Ordering::Relaxed);
bytes.put(chunk); bytes.put(chunk);

View File

@ -22,8 +22,8 @@ pub mod mpris;
type Result<T> = std::result::Result<T, Error>; type Result<T> = std::result::Result<T, Error>;
/// The error type for the UI, which is used to handle errors that occur /// The error type for the UI, which is used to handle errors
/// while drawing the UI or handling input. /// that occur while drawing the UI or handling input.
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
#[error("unable to convert number")] #[error("unable to convert number")]
@ -36,7 +36,7 @@ pub enum Error {
CrateSend(#[from] tokio::sync::mpsc::error::SendError<crate::Message>), CrateSend(#[from] tokio::sync::mpsc::error::SendError<crate::Message>),
#[error("sharing state between backend and frontend failed")] #[error("sharing state between backend and frontend failed")]
UiSend(#[from] tokio::sync::broadcast::error::SendError<Update>), Send(#[from] tokio::sync::broadcast::error::SendError<Update>),
#[cfg(feature = "mpris")] #[cfg(feature = "mpris")]
#[error("mpris bus error")] #[error("mpris bus error")]
@ -47,6 +47,11 @@ pub enum Error {
Fdo(#[from] mpris_server::zbus::fdo::Error), Fdo(#[from] mpris_server::zbus::fdo::Error),
} }
/// The UI state, which is all of the information that
/// the user interface needs to display to the user.
///
/// It should be noted that this is also used by MPRIS to keep
/// track of state.
#[derive(Clone)] #[derive(Clone)]
pub struct State { pub struct State {
pub sink: Arc<rodio::Sink>, pub sink: Arc<rodio::Sink>,
@ -60,19 +65,26 @@ pub struct State {
} }
impl State { impl State {
pub fn initial(sink: Arc<rodio::Sink>, width: usize, current: Current, list: String) -> Self { /// Creates an initial UI state.
pub fn initial(sink: Arc<rodio::Sink>, width: usize, list: String) -> Self {
let width = 21 + width.min(32) * 2; let width = 21 + width.min(32) * 2;
Self { Self {
width, width,
sink, sink,
current,
list, list,
current: Current::default(),
bookmarked: false, bookmarked: false,
timer: None, timer: None,
} }
} }
} }
/// A UI update sent out by the main player thread, which may
/// not be immediately applied by the UI.
///
/// This corresponds to user actions, like bookmarking a track,
/// skipping, or changing the volume. The difference is that it also
/// contains the new information about the track.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Update { pub enum Update {
Track(Current), Track(Current),
@ -81,12 +93,16 @@ pub enum Update {
Quit, Quit,
} }
/// Just a simple wrapper for the two primary tasks that the UI
/// requires to function.
#[derive(Debug)] #[derive(Debug)]
struct Tasks { struct Tasks {
render: JoinHandle<Result<()>>, render: JoinHandle<Result<()>>,
input: JoinHandle<Result<()>>, input: JoinHandle<Result<()>>,
} }
/// The UI handle for controlling the state of the UI, as well as
/// updating MPRIS information and other small interfacing tasks.
pub struct Handle { pub struct Handle {
tasks: Tasks, tasks: Tasks,
pub environment: Environment, pub environment: Environment,
@ -102,6 +118,14 @@ impl Drop for Handle {
} }
impl Handle { impl Handle {
/// The main UI process, which will both render the UI to the terminal
/// and also update state.
///
/// It does both of these things at a fixed interval, due to things
/// like the track duration changing too frequently.
///
/// `rx` is the receiver for state updates, `state` the initial state,
/// and `params` specifies aesthetic options that are specified by the user.
async fn ui( async fn ui(
mut rx: broadcast::Receiver<Update>, mut rx: broadcast::Receiver<Update>,
mut state: State, mut state: State,
@ -111,8 +135,6 @@ impl Handle {
let mut window = Window::new(state.width, params.borderless); let mut window = Window::new(state.width, params.borderless);
loop { loop {
interface::draw(&mut state, &mut window, params)?;
if let Ok(message) = rx.try_recv() { if let Ok(message) = rx.try_recv() {
match message { match message {
Update::Track(track) => state.current = track, Update::Track(track) => state.current = track,
@ -122,12 +144,15 @@ impl Handle {
} }
}; };
interface::draw(&mut state, &mut window, params)?;
interval.tick().await; interval.tick().await;
} }
Ok(()) Ok(())
} }
/// Initializes the UI itself, along with all of the tasks that are related to it.
#[allow(clippy::unused_async)]
pub async fn init( pub async fn init(
tx: Sender<crate::Message>, tx: Sender<crate::Message>,
updater: broadcast::Receiver<ui::Update>, updater: broadcast::Receiver<ui::Update>,

View File

@ -45,7 +45,7 @@ impl Environment {
panic::set_hook(Box::new(move |info| { panic::set_hook(Box::new(move |info| {
let _ = environment.cleanup(false); let _ = environment.cleanup(false);
eprintln!("panic: {}", info); eprintln!("panic: {info}");
})); }));
Ok(environment) Ok(environment)

View File

@ -26,7 +26,7 @@ impl From<&Args> for Params {
} }
pub(crate) fn menu(state: &mut ui::State, params: Params) -> Vec<String> { pub(crate) fn menu(state: &mut ui::State, params: Params) -> Vec<String> {
let action = components::action(&state, state.width); let action = components::action(state, state.width);
let middle = match state.timer { let middle = match state.timer {
Some(timer) => { Some(timer) => {
@ -38,7 +38,7 @@ pub(crate) fn menu(state: &mut ui::State, params: Params) -> Vec<String> {
components::audio_bar(state.width - 17, volume, &percentage) components::audio_bar(state.width - 17, volume, &percentage)
} }
None => components::progress_bar(&state, state.width - 16), None => components::progress_bar(state, state.width - 16),
}; };
let controls = components::controls(state.width); let controls = components::controls(state.width);

View File

@ -263,8 +263,8 @@ pub struct Server {
/// The inner MPRIS server. /// The inner MPRIS server.
inner: mpris_server::Server<Player>, inner: mpris_server::Server<Player>,
/// Broadcast reciever. /// Broadcast receiver.
reciever: broadcast::Receiver<Update>, receiver: broadcast::Receiver<Update>,
} }
impl Server { impl Server {
@ -273,16 +273,17 @@ impl Server {
&mut self, &mut self,
properties: impl IntoIterator<Item = mpris_server::Property> + Send + Sync, properties: impl IntoIterator<Item = mpris_server::Property> + Send + Sync,
) -> ui::Result<()> { ) -> ui::Result<()> {
while let Ok(update) = self.reciever.try_recv() { while let Ok(update) = self.receiver.try_recv() {
if let Update::Track(current) = update { if let Update::Track(current) = update {
self.player().current.swap(Arc::new(current)); self.player().current.swap(Arc::new(current));
} }
} }
self.inner.properties_changed(properties).await?;
self.inner.properties_changed(properties).await?;
Ok(()) Ok(())
} }
/// Updates the volume with the latest information.
pub async fn update_volume(&mut self) -> ui::Result<()> { pub async fn update_volume(&mut self) -> ui::Result<()> {
self.changed(vec![Property::Volume(self.player().sink.volume().into())]) self.changed(vec![Property::Volume(self.player().sink.volume().into())])
.await?; .await?;
@ -290,7 +291,7 @@ impl Server {
Ok(()) Ok(())
} }
/// Shorthand to emit a `PropertiesChanged` signal, specifically about playback. /// Updates the playback with the latest information.
pub async fn update_playback(&mut self) -> ui::Result<()> { pub async fn update_playback(&mut self) -> ui::Result<()> {
let status = self.player().playback_status().await?; let status = self.player().playback_status().await?;
self.changed(vec![Property::PlaybackStatus(status)]).await?; self.changed(vec![Property::PlaybackStatus(status)]).await?;
@ -298,6 +299,7 @@ impl Server {
Ok(()) Ok(())
} }
/// Updates the current track data with the current information.
pub async fn update_metadata(&mut self) -> ui::Result<()> { pub async fn update_metadata(&mut self) -> ui::Result<()> {
let metadata = self.player().metadata().await?; let metadata = self.player().metadata().await?;
self.changed(vec![Property::Metadata(metadata)]).await?; self.changed(vec![Property::Metadata(metadata)]).await?;
@ -314,7 +316,7 @@ impl Server {
pub async fn new( pub async fn new(
state: ui::State, state: ui::State,
sender: mpsc::Sender<Message>, sender: mpsc::Sender<Message>,
reciever: broadcast::Receiver<Update>, receiver: broadcast::Receiver<Update>,
) -> ui::Result<Server> { ) -> ui::Result<Server> {
let suffix = if env::var("LOWFI_FIXED_MPRIS_NAME").is_ok_and(|x| x == "1") { let suffix = if env::var("LOWFI_FIXED_MPRIS_NAME").is_ok_and(|x| x == "1") {
String::from("lowfi") String::from("lowfi")
@ -335,7 +337,7 @@ impl Server {
Ok(Self { Ok(Self {
inner: server, inner: server,
reciever, receiver,
}) })
} }
} }

View File

@ -2,11 +2,11 @@ use std::io::{stdout, Stdout};
use crossterm::{ use crossterm::{
cursor::{MoveToColumn, MoveUp}, cursor::{MoveToColumn, MoveUp},
style::{Print, Stylize}, style::{Print, Stylize as _},
terminal::{Clear, ClearType}, terminal::{Clear, ClearType},
}; };
use std::fmt::Write; use std::fmt::Write as _;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation as _;
/// Represents an abstraction for drawing the actual lowfi window itself. /// Represents an abstraction for drawing the actual lowfi window itself.
/// ///
@ -52,7 +52,7 @@ impl Window {
} }
pub(crate) fn render( pub(crate) fn render(
&mut self, &self,
content: Vec<String>, content: Vec<String>,
space: bool, space: bool,
testing: bool, testing: bool,

View File

@ -1,8 +1,11 @@
//! Persistent volume management.
use std::{num::ParseIntError, path::PathBuf}; use std::{num::ParseIntError, path::PathBuf};
use tokio::fs; use tokio::fs;
/// Shorthand for a [`Result`] with a persistent volume error.
type Result<T> = std::result::Result<T, Error>; type Result<T> = std::result::Result<T, Error>;
/// Errors which occur when loading/unloading persistent volume.
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
#[error("couldn't find config directory")] #[error("couldn't find config directory")]
@ -27,7 +30,7 @@ impl PersistentVolume {
/// Retrieves the config directory. /// Retrieves the config directory.
async fn config() -> Result<PathBuf> { async fn config() -> Result<PathBuf> {
let config = dirs::config_dir() let config = dirs::config_dir()
.ok_or_else(|| Error::Directory)? .ok_or(Error::Directory)?
.join(PathBuf::from("lowfi")); .join(PathBuf::from("lowfi"));
if !config.exists() { if !config.exists() {