mirror of
https://github.com/talwat/lowfi
synced 2025-12-07 23:47:46 +00:00
chore: configure clippy & fix lints
This commit is contained in:
parent
1c8c788d76
commit
a87a8cc59e
14
Cargo.toml
14
Cargo.toml
@ -60,3 +60,17 @@ indicatif = { version = "0.18.0", optional = true }
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
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"
|
||||
@ -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.
|
||||
unsafe {
|
||||
freopen(null.as_ptr(), mode.as_ptr(), stderr);
|
||||
}
|
||||
};
|
||||
|
||||
// Make the OutputStream while stderr is still redirected to /dev/null.
|
||||
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`.
|
||||
unsafe {
|
||||
freopen(tty.as_ptr(), mode.as_ptr(), stderr);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ impl Handle {
|
||||
let notify = Arc::new(Notify::new());
|
||||
|
||||
Self {
|
||||
task: task::spawn(Self::waiter(sink, tx, notify.clone())),
|
||||
task: task::spawn(Self::waiter(sink, tx, Arc::clone(¬ify))),
|
||||
notify,
|
||||
}
|
||||
}
|
||||
@ -40,7 +40,7 @@ impl Handle {
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
@ -46,7 +46,7 @@ impl Bookmarks {
|
||||
if x.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(x.to_string())
|
||||
Some(x.to_owned())
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
@ -64,7 +64,7 @@ impl Bookmarks {
|
||||
/// Bookmarks a given track with a full path and optional custom name.
|
||||
///
|
||||
/// 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 idx = self.entries.iter().position(|x| **x == entry);
|
||||
|
||||
|
||||
@ -15,6 +15,8 @@ static LOADING: AtomicBool = AtomicBool::new(false);
|
||||
pub(crate) static PROGRESS: AtomicU8 = AtomicU8::new(0);
|
||||
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 {
|
||||
queue: Sender<tracks::Queued>,
|
||||
tx: Sender<crate::Message>,
|
||||
@ -24,7 +26,10 @@ pub struct 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 (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 {
|
||||
queue: Receiver<tracks::Queued>,
|
||||
handle: JoinHandle<crate::Result<()>>,
|
||||
}
|
||||
|
||||
/// The output when a track is requested from the downloader.
|
||||
pub enum Output {
|
||||
Loading(Option<Progress>),
|
||||
Queued(tracks::Queued),
|
||||
}
|
||||
|
||||
impl Handle {
|
||||
pub async fn track(&mut self) -> Output {
|
||||
match self.queue.try_recv() {
|
||||
Ok(queued) => Output::Queued(queued),
|
||||
Err(_) => {
|
||||
/// Gets either a queued track, or a progress report,
|
||||
/// depending on the state of the internal download buffer.
|
||||
#[rustfmt::skip]
|
||||
pub fn track(&mut self) -> Output {
|
||||
self.queue.try_recv().map_or_else(|_| {
|
||||
LOADING.store(true, atomic::Ordering::Relaxed);
|
||||
Output::Loading(Some(&PROGRESS))
|
||||
}
|
||||
}
|
||||
}, Output::Queued,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
20
src/main.rs
20
src/main.rs
@ -1,6 +1,4 @@
|
||||
//! An extremely simple lofi player.
|
||||
#![warn(clippy::all, clippy::pedantic, clippy::nursery)]
|
||||
|
||||
pub mod error;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@ -98,23 +96,21 @@ pub fn data_dir() -> crate::Result<PathBuf> {
|
||||
async fn main() -> eyre::Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
if let Some(command) = args.command {
|
||||
#[cfg(feature = "scrape")]
|
||||
if let Some(command) = &args.command {
|
||||
match command {
|
||||
#[cfg(feature = "scrape")]
|
||||
Commands::Scrape { source } => match source {
|
||||
Source::Archive => scrapers::archive::scrape().await?,
|
||||
Source::Lofigirl => scrapers::lofigirl::scrape().await?,
|
||||
Source::Chillhop => scrapers::chillhop::scrape().await?,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
let player = Player::init(args).await?;
|
||||
let environment = player.environment();
|
||||
let result = player.run().await;
|
||||
}
|
||||
|
||||
environment.cleanup(result.is_ok())?;
|
||||
result?;
|
||||
};
|
||||
let player = Player::init(args).await?;
|
||||
let environment = player.environment();
|
||||
let result = player.run().await;
|
||||
|
||||
Ok(())
|
||||
environment.cleanup(result.is_ok())?;
|
||||
Ok(result?)
|
||||
}
|
||||
|
||||
@ -23,13 +23,13 @@ pub enum Current {
|
||||
|
||||
impl Default for Current {
|
||||
fn default() -> Self {
|
||||
Current::Loading(None)
|
||||
Self::Loading(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Current {
|
||||
pub fn loading(&self) -> bool {
|
||||
return matches!(self, Current::Loading(_));
|
||||
pub const fn loading(&self) -> bool {
|
||||
matches!(self, Self::Loading(_))
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,25 +53,25 @@ impl Drop for Player {
|
||||
}
|
||||
|
||||
impl Player {
|
||||
pub fn environment(&self) -> ui::Environment {
|
||||
pub const fn 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.update(ui::Update::Track(current)).await?;
|
||||
self.update(ui::Update::Track(current))?;
|
||||
|
||||
let Current::Track(track) = &self.current else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let bookmarked = self.bookmarks.bookmarked(&track);
|
||||
self.update(ui::Update::Bookmarked(bookmarked)).await?;
|
||||
let bookmarked = self.bookmarks.bookmarked(track);
|
||||
self.update(ui::Update::Bookmarked(bookmarked))?;
|
||||
|
||||
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)?;
|
||||
Ok(())
|
||||
}
|
||||
@ -87,21 +87,19 @@ impl Player {
|
||||
let (tx, rx) = mpsc::channel(8);
|
||||
tx.send(Message::Init).await?;
|
||||
let (utx, urx) = broadcast::channel(8);
|
||||
let current = Current::Loading(None);
|
||||
|
||||
let list = List::load(args.track_list.as_ref()).await?;
|
||||
let state =
|
||||
ui::State::initial(sink.clone(), args.width, current.clone(), list.name.clone());
|
||||
let state = ui::State::initial(Arc::clone(&sink), args.width, list.name.clone());
|
||||
|
||||
let volume = PersistentVolume::load().await?;
|
||||
sink.set_volume(volume.float());
|
||||
|
||||
Ok(Self {
|
||||
ui: ui::Handle::init(tx.clone(), urx, state, &args).await?,
|
||||
downloader: Downloader::init(args.buffer_size as usize, list, tx.clone()).await,
|
||||
waiter: waiter::Handle::new(sink.clone(), tx.clone()),
|
||||
downloader: Downloader::init(args.buffer_size as usize, list, tx.clone()),
|
||||
waiter: waiter::Handle::new(Arc::clone(&sink), tx.clone()),
|
||||
bookmarks: Bookmarks::load().await?,
|
||||
current,
|
||||
current: Current::default(),
|
||||
broadcast: utx,
|
||||
rx,
|
||||
sink,
|
||||
@ -112,15 +110,15 @@ impl Player {
|
||||
|
||||
pub async fn close(&self) -> crate::Result<()> {
|
||||
self.bookmarks.save().await?;
|
||||
PersistentVolume::save(self.sink.volume() as f32).await?;
|
||||
PersistentVolume::save(self.sink.volume()).await?;
|
||||
|
||||
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()?;
|
||||
self.sink.append(decoded.data);
|
||||
self.set_current(Current::Track(decoded.info)).await?;
|
||||
self.set_current(Current::Track(decoded.info))?;
|
||||
self.waiter.notify();
|
||||
|
||||
Ok(())
|
||||
@ -135,12 +133,12 @@ impl Player {
|
||||
}
|
||||
|
||||
self.sink.stop();
|
||||
match self.downloader.track().await {
|
||||
match self.downloader.track() {
|
||||
download::Output::Loading(progress) => {
|
||||
self.set_current(Current::Loading(progress)).await?;
|
||||
self.set_current(Current::Loading(progress))?;
|
||||
}
|
||||
download::Output::Queued(queued) => {
|
||||
self.play(queued).await?;
|
||||
self.play(queued)?;
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -160,19 +158,19 @@ impl Player {
|
||||
Message::ChangeVolume(change) => {
|
||||
self.sink
|
||||
.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) => {
|
||||
self.sink.set_volume(set.clamp(0.0, 1.0));
|
||||
self.update(ui::Update::Volume).await?;
|
||||
self.update(ui::Update::Volume)?;
|
||||
}
|
||||
Message::Bookmark => {
|
||||
let Current::Track(current) = &self.current else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let bookmarked = self.bookmarks.bookmark(current).await?;
|
||||
self.update(ui::Update::Bookmarked(bookmarked)).await?;
|
||||
let bookmarked = self.bookmarks.bookmark(current)?;
|
||||
self.update(ui::Update::Bookmarked(bookmarked))?;
|
||||
}
|
||||
Message::Quit => break,
|
||||
}
|
||||
|
||||
@ -11,8 +11,8 @@ mod bookmark {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn toggle_and_check() {
|
||||
#[test]
|
||||
fn toggle_and_check() {
|
||||
let mut bm = Bookmarks { entries: vec![] };
|
||||
let info = test_info("p.mp3", "Nice Track");
|
||||
|
||||
@ -20,37 +20,37 @@ mod bookmark {
|
||||
assert!(!bm.bookmarked(&info));
|
||||
|
||||
// bookmark it
|
||||
let added = bm.bookmark(&info).await.unwrap();
|
||||
let added = bm.bookmark(&info).unwrap();
|
||||
assert!(added);
|
||||
assert!(bm.bookmarked(&info));
|
||||
|
||||
// un-bookmark it
|
||||
let removed = bm.bookmark(&info).await.unwrap();
|
||||
let removed = bm.bookmark(&info).unwrap();
|
||||
assert!(!removed);
|
||||
assert!(!bm.bookmarked(&info));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multiple_bookmarks() {
|
||||
#[test]
|
||||
fn multiple_bookmarks() {
|
||||
let mut bm = Bookmarks { entries: vec![] };
|
||||
let info1 = test_info("track1.mp3", "Track One");
|
||||
let info2 = test_info("track2.mp3", "Track Two");
|
||||
|
||||
bm.bookmark(&info1).await.unwrap();
|
||||
bm.bookmark(&info2).await.unwrap();
|
||||
bm.bookmark(&info1).unwrap();
|
||||
bm.bookmark(&info2).unwrap();
|
||||
|
||||
assert!(bm.bookmarked(&info1));
|
||||
assert!(bm.bookmarked(&info2));
|
||||
assert_eq!(bm.entries.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn duplicate_bookmark_removes() {
|
||||
#[test]
|
||||
fn duplicate_bookmark_removes() {
|
||||
let mut bm = Bookmarks { entries: vec![] };
|
||||
let info = test_info("x.mp3", "X");
|
||||
|
||||
bm.bookmark(&info).await.unwrap();
|
||||
let is_added = bm.bookmark(&info).await.unwrap();
|
||||
bm.bookmark(&info).unwrap();
|
||||
let is_added = bm.bookmark(&info).unwrap();
|
||||
|
||||
assert!(!is_added);
|
||||
assert!(bm.entries.is_empty());
|
||||
|
||||
@ -79,7 +79,7 @@ mod window {
|
||||
|
||||
#[test]
|
||||
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();
|
||||
|
||||
const MIDDLE: &str = "─────";
|
||||
@ -89,7 +89,7 @@ mod window {
|
||||
|
||||
#[test]
|
||||
fn spaced() {
|
||||
let mut w = Window::new(3, false);
|
||||
let w = Window::new(3, false);
|
||||
let (render, height) = w
|
||||
.render(
|
||||
vec![String::from("abc"), String::from(" b"), String::from("c")],
|
||||
@ -137,7 +137,7 @@ mod interface {
|
||||
#[test]
|
||||
fn loading() {
|
||||
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());
|
||||
|
||||
assert_eq!(menu[0], "loading ");
|
||||
@ -157,7 +157,7 @@ mod interface {
|
||||
fn volume() {
|
||||
let sink = Arc::new(rodio::Sink::new().0);
|
||||
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());
|
||||
|
||||
let menu = interface::menu(&mut state, Params::default());
|
||||
@ -179,12 +179,9 @@ mod interface {
|
||||
fn progress() {
|
||||
let sink = Arc::new(rodio::Sink::new().0);
|
||||
PROGRESS.store(50, std::sync::atomic::Ordering::Relaxed);
|
||||
let mut state = State::initial(
|
||||
sink,
|
||||
3,
|
||||
Current::Loading(Some(&PROGRESS)),
|
||||
String::from("test"),
|
||||
);
|
||||
let mut state = State::initial(sink, 3, String::from("test"));
|
||||
state.current = Current::Loading(Some(&PROGRESS));
|
||||
|
||||
let menu = interface::menu(&mut state, Params::default());
|
||||
|
||||
assert_eq!(menu[0], format!("loading {} ", "50%".bold()));
|
||||
@ -210,8 +207,8 @@ mod interface {
|
||||
duration: Some(Duration::from_secs(8)),
|
||||
};
|
||||
|
||||
let current = Current::Track(track.clone());
|
||||
let mut state = State::initial(sink, 3, current, String::from("test"));
|
||||
let mut state = State::initial(sink, 3, String::from("test"));
|
||||
state.current = Current::Track(track.clone());
|
||||
let menu = interface::menu(&mut state, Params::default());
|
||||
|
||||
assert_eq!(
|
||||
|
||||
@ -2,24 +2,18 @@
|
||||
//! of tracks, as well as downloading them & finding new ones.
|
||||
//!
|
||||
//! There are several structs which represent the different stages
|
||||
//! that go on in downloading and playing tracks. The proccess for fetching tracks,
|
||||
//! and what structs are relevant in each step, are as follows.
|
||||
//! that go on in downloading and playing tracks. When first queued,
|
||||
//! the downloader will return a [`Queued`] track.
|
||||
//!
|
||||
//! First Stage, when a track is initially fetched.
|
||||
//! 1. Raw entry selected from track list.
|
||||
//! 2. Raw entry split into path & display name.
|
||||
//! 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.
|
||||
//! Then, when it's time to play the track, it is decoded into
|
||||
//! a [`Decoded`] track, which includes all the information
|
||||
//! in the form of [`Info`].
|
||||
|
||||
use std::{fmt::Debug, io::Cursor, time::Duration};
|
||||
|
||||
use bytes::Bytes;
|
||||
use rodio::{Decoder, Source as _};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_segmentation::UnicodeSegmentation as _;
|
||||
|
||||
pub mod list;
|
||||
pub use list::List;
|
||||
@ -27,7 +21,7 @@ pub mod error;
|
||||
pub mod format;
|
||||
pub use error::{Error, Result};
|
||||
|
||||
use crate::tracks::error::WithTrackContext;
|
||||
use crate::tracks::error::WithTrackContext as _;
|
||||
|
||||
/// Just a shorthand for a decoded [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.
|
||||
///
|
||||
/// This means that only the data & track name are included.
|
||||
#[derive(PartialEq)]
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub struct Queued {
|
||||
/// Display name of the track.
|
||||
pub display: String,
|
||||
@ -66,6 +60,7 @@ impl Queued {
|
||||
Decoded::new(self)
|
||||
}
|
||||
|
||||
/// Creates a new queued track.
|
||||
pub fn new(path: String, data: Bytes, display: Option<String>) -> Result<Self> {
|
||||
let display = match display {
|
||||
None => self::format::name(&path)?,
|
||||
@ -73,8 +68,8 @@ impl Queued {
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
path,
|
||||
display,
|
||||
path,
|
||||
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.
|
||||
pub struct Decoded {
|
||||
/// 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> {
|
||||
let (path, display) = (track.path.clone(), track.display.clone());
|
||||
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))
|
||||
.build()
|
||||
.track(track.display)?;
|
||||
|
||||
@ -19,6 +19,9 @@ pub enum Kind {
|
||||
|
||||
#[error("unable to fetch data: {0}")]
|
||||
Request(#[from] reqwest::Error),
|
||||
|
||||
#[error("couldn't handle integer track length: {0}")]
|
||||
Integer(#[from] std::num::TryFromIntError),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
use convert_case::{Case, Casing};
|
||||
use convert_case::{Case, Casing as _};
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use std::path::Path;
|
||||
use url::form_urlencoded;
|
||||
|
||||
use super::error::WithTrackContext;
|
||||
use super::error::WithTrackContext as _;
|
||||
|
||||
lazy_static! {
|
||||
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 skip == name.len() {
|
||||
Ok(name.trim().to_string())
|
||||
Ok(name.trim().to_owned())
|
||||
} else {
|
||||
// We've already checked before that the bound is at an ASCII digit.
|
||||
#[allow(clippy::string_slice)]
|
||||
|
||||
@ -6,8 +6,8 @@ use std::{
|
||||
sync::atomic::{AtomicU8, Ordering},
|
||||
};
|
||||
|
||||
use bytes::{BufMut, Bytes, BytesMut};
|
||||
use futures::StreamExt;
|
||||
use bytes::{BufMut as _, Bytes, BytesMut};
|
||||
use futures::StreamExt as _;
|
||||
use reqwest::Client;
|
||||
use tokio::fs;
|
||||
|
||||
@ -15,7 +15,7 @@ use crate::{
|
||||
data_dir,
|
||||
tracks::{
|
||||
self,
|
||||
error::{self, WithTrackContext},
|
||||
error::{self, WithTrackContext as _},
|
||||
},
|
||||
};
|
||||
|
||||
@ -114,7 +114,7 @@ impl List {
|
||||
while let Some(item) = stream.next().await {
|
||||
let chunk = item.track(track)?;
|
||||
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);
|
||||
|
||||
bytes.put(chunk);
|
||||
|
||||
39
src/ui.rs
39
src/ui.rs
@ -22,8 +22,8 @@ pub mod mpris;
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// The error type for the UI, which is used to handle errors that occur
|
||||
/// while drawing the UI or handling input.
|
||||
/// The error type for the UI, which is used to handle errors
|
||||
/// that occur while drawing the UI or handling input.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("unable to convert number")]
|
||||
@ -36,7 +36,7 @@ pub enum Error {
|
||||
CrateSend(#[from] tokio::sync::mpsc::error::SendError<crate::Message>),
|
||||
|
||||
#[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")]
|
||||
#[error("mpris bus error")]
|
||||
@ -47,6 +47,11 @@ pub enum 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)]
|
||||
pub struct State {
|
||||
pub sink: Arc<rodio::Sink>,
|
||||
@ -60,19 +65,26 @@ pub struct 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;
|
||||
Self {
|
||||
width,
|
||||
sink,
|
||||
current,
|
||||
list,
|
||||
current: Current::default(),
|
||||
bookmarked: false,
|
||||
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)]
|
||||
pub enum Update {
|
||||
Track(Current),
|
||||
@ -81,12 +93,16 @@ pub enum Update {
|
||||
Quit,
|
||||
}
|
||||
|
||||
/// Just a simple wrapper for the two primary tasks that the UI
|
||||
/// requires to function.
|
||||
#[derive(Debug)]
|
||||
struct Tasks {
|
||||
render: 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 {
|
||||
tasks: Tasks,
|
||||
pub environment: Environment,
|
||||
@ -102,6 +118,14 @@ impl Drop for 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(
|
||||
mut rx: broadcast::Receiver<Update>,
|
||||
mut state: State,
|
||||
@ -111,8 +135,6 @@ impl Handle {
|
||||
let mut window = Window::new(state.width, params.borderless);
|
||||
|
||||
loop {
|
||||
interface::draw(&mut state, &mut window, params)?;
|
||||
|
||||
if let Ok(message) = rx.try_recv() {
|
||||
match message {
|
||||
Update::Track(track) => state.current = track,
|
||||
@ -122,12 +144,15 @@ impl Handle {
|
||||
}
|
||||
};
|
||||
|
||||
interface::draw(&mut state, &mut window, params)?;
|
||||
interval.tick().await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initializes the UI itself, along with all of the tasks that are related to it.
|
||||
#[allow(clippy::unused_async)]
|
||||
pub async fn init(
|
||||
tx: Sender<crate::Message>,
|
||||
updater: broadcast::Receiver<ui::Update>,
|
||||
|
||||
@ -45,7 +45,7 @@ impl Environment {
|
||||
|
||||
panic::set_hook(Box::new(move |info| {
|
||||
let _ = environment.cleanup(false);
|
||||
eprintln!("panic: {}", info);
|
||||
eprintln!("panic: {info}");
|
||||
}));
|
||||
|
||||
Ok(environment)
|
||||
|
||||
@ -26,7 +26,7 @@ impl From<&Args> for Params {
|
||||
}
|
||||
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
None => components::progress_bar(&state, state.width - 16),
|
||||
None => components::progress_bar(state, state.width - 16),
|
||||
};
|
||||
|
||||
let controls = components::controls(state.width);
|
||||
|
||||
@ -263,8 +263,8 @@ pub struct Server {
|
||||
/// The inner MPRIS server.
|
||||
inner: mpris_server::Server<Player>,
|
||||
|
||||
/// Broadcast reciever.
|
||||
reciever: broadcast::Receiver<Update>,
|
||||
/// Broadcast receiver.
|
||||
receiver: broadcast::Receiver<Update>,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
@ -273,16 +273,17 @@ impl Server {
|
||||
&mut self,
|
||||
properties: impl IntoIterator<Item = mpris_server::Property> + Send + Sync,
|
||||
) -> ui::Result<()> {
|
||||
while let Ok(update) = self.reciever.try_recv() {
|
||||
while let Ok(update) = self.receiver.try_recv() {
|
||||
if let Update::Track(current) = update {
|
||||
self.player().current.swap(Arc::new(current));
|
||||
}
|
||||
}
|
||||
self.inner.properties_changed(properties).await?;
|
||||
|
||||
self.inner.properties_changed(properties).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates the volume with the latest information.
|
||||
pub async fn update_volume(&mut self) -> ui::Result<()> {
|
||||
self.changed(vec![Property::Volume(self.player().sink.volume().into())])
|
||||
.await?;
|
||||
@ -290,7 +291,7 @@ impl Server {
|
||||
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<()> {
|
||||
let status = self.player().playback_status().await?;
|
||||
self.changed(vec![Property::PlaybackStatus(status)]).await?;
|
||||
@ -298,6 +299,7 @@ impl Server {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates the current track data with the current information.
|
||||
pub async fn update_metadata(&mut self) -> ui::Result<()> {
|
||||
let metadata = self.player().metadata().await?;
|
||||
self.changed(vec![Property::Metadata(metadata)]).await?;
|
||||
@ -314,7 +316,7 @@ impl Server {
|
||||
pub async fn new(
|
||||
state: ui::State,
|
||||
sender: mpsc::Sender<Message>,
|
||||
reciever: broadcast::Receiver<Update>,
|
||||
receiver: broadcast::Receiver<Update>,
|
||||
) -> ui::Result<Server> {
|
||||
let suffix = if env::var("LOWFI_FIXED_MPRIS_NAME").is_ok_and(|x| x == "1") {
|
||||
String::from("lowfi")
|
||||
@ -335,7 +337,7 @@ impl Server {
|
||||
|
||||
Ok(Self {
|
||||
inner: server,
|
||||
reciever,
|
||||
receiver,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,11 +2,11 @@ use std::io::{stdout, Stdout};
|
||||
|
||||
use crossterm::{
|
||||
cursor::{MoveToColumn, MoveUp},
|
||||
style::{Print, Stylize},
|
||||
style::{Print, Stylize as _},
|
||||
terminal::{Clear, ClearType},
|
||||
};
|
||||
use std::fmt::Write;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use std::fmt::Write as _;
|
||||
use unicode_segmentation::UnicodeSegmentation as _;
|
||||
|
||||
/// Represents an abstraction for drawing the actual lowfi window itself.
|
||||
///
|
||||
@ -52,7 +52,7 @@ impl Window {
|
||||
}
|
||||
|
||||
pub(crate) fn render(
|
||||
&mut self,
|
||||
&self,
|
||||
content: Vec<String>,
|
||||
space: bool,
|
||||
testing: bool,
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
//! Persistent volume management.
|
||||
use std::{num::ParseIntError, path::PathBuf};
|
||||
use tokio::fs;
|
||||
|
||||
/// Shorthand for a [`Result`] with a persistent volume error.
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Errors which occur when loading/unloading persistent volume.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("couldn't find config directory")]
|
||||
@ -27,7 +30,7 @@ impl PersistentVolume {
|
||||
/// Retrieves the config directory.
|
||||
async fn config() -> Result<PathBuf> {
|
||||
let config = dirs::config_dir()
|
||||
.ok_or_else(|| Error::Directory)?
|
||||
.ok_or(Error::Directory)?
|
||||
.join(PathBuf::from("lowfi"));
|
||||
|
||||
if !config.exists() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user