From 1884d2ebed61953869f4fb835d4ea1710f718fbf Mon Sep 17 00:00:00 2001 From: Tal <83217276+talwat@users.noreply.github.com> Date: Wed, 6 Aug 2025 15:34:44 +0200 Subject: [PATCH] chore: even more error handling improvements --- Cargo.lock | 22 ++++++- Cargo.toml | 3 +- README.md | 4 +- data/micropop.txt | 16 ----- data/sample.txt | 2 +- src/main.rs | 21 +++--- src/messages.rs | 2 +- src/play.rs | 112 ++++++++++---------------------- src/player.rs | 46 +++++++------ src/player/audio.rs | 5 +- src/player/bookmark.rs | 3 +- src/player/downloader.rs | 2 +- src/player/error.rs | 51 +++++++++++++++ src/player/mpris.rs | 25 +++---- src/player/persistent_volume.rs | 70 ++++++++++++++++++++ src/player/queue.rs | 10 +-- src/player/ui.rs | 12 ++-- src/player/ui/components.rs | 2 +- src/player/ui/input.rs | 40 ++++++------ src/tracks.rs | 16 ++--- src/tracks/list.rs | 15 +++-- 21 files changed, 285 insertions(+), 194 deletions(-) delete mode 100644 data/micropop.txt create mode 100644 src/player/error.rs create mode 100644 src/player/persistent_volume.rs diff --git a/Cargo.lock b/Cargo.lock index a51ea09..efb1784 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -433,6 +433,19 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "color-eyre" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" +dependencies = [ + "backtrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", +] + [[package]] name = "colorchoice" version = "1.0.3" @@ -1392,6 +1405,7 @@ dependencies = [ "arc-swap", "bytes", "clap", + "color-eyre", "crossterm", "dirs", "eyre", @@ -1777,6 +1791,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "owo-colors" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" + [[package]] name = "parking" version = "2.2.1" @@ -3113,7 +3133,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9a66386..bb60b07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,8 @@ extra-audio-formats = ["rodio/default"] clap = { version = "4.5.21", features = ["derive", "cargo"] } eyre = { version = "0.6.12" } rand = "0.8.5" +thiserror = "2.0.12" +color-eyre = { version = "0.6.5", default-features = false } # Async tokio = { version = "1.41.1", features = [ @@ -52,4 +54,3 @@ lazy_static = "1.5.0" libc = "0.2.167" url = "2.5.4" unicode-segmentation = "1.12.0" -thiserror = "2.0.12" diff --git a/README.md b/README.md index 17e3609..ff31ca5 100644 --- a/README.md +++ b/README.md @@ -219,13 +219,13 @@ For example, in this list: https://lofigirl.com/wp-content/uploads/ 2023/06/Foudroie-Finding-The-Edge-V2.mp3 2023/04/2-In-Front-Of-Me.mp3 -https://file-examples.com/storage/fea570b16e6703ef79e65b4/2017/11/file_example_MP3_5MG.mp3 +https://file-examples.com/storage/fe85f7a43b689349d9c8f18/2017/11/file_example_MP3_1MG.mp3 ``` lowfi would download these three URLs: - `https://lofigirl.com/wp-content/uploads/2023/06/Foudroie-Finding-The-Edge-V2.mp3` -- `https://file-examples.com/storage/fea570b16e6703ef79e65b4/2017/11/file_example_MP3_5MG.mp3` +- `https://file-examples.com/storage/fe85f7a43b689349d9c8f18/2017/11/file_example_MP3_1MG.mp3` - `https://lofigirl.com/wp-content/uploads/2023/04/2-In-Front-Of-Me.mp3` Additionally, you may also specify a custom display name for the track which is indicated by a `!`. diff --git a/data/micropop.txt b/data/micropop.txt deleted file mode 100644 index a5aaf6c..0000000 --- a/data/micropop.txt +++ /dev/null @@ -1,16 +0,0 @@ -https://archive.org/download/jack-stauber-s-micropop-extended-micropops/Jack%20Stauber-%27s%20Micropop%20-%20 -Al%20Dente.mp3 -Baby%20Hotline.mp3 -Cupid.mp3 -Deploy.mp3 -Dinner%20Is%20Not%20Over.mp3 -Fighter.mp3 -Inchman.mp3 -Keyman.mp3 -Out%20the%20Ox.mp3 -Tea%20Errors.mp3 -The%20Ballad%20of%20Hamantha.mp3 -There%27s%20Something%20Happening.mp3 -Those%20Eggs%20Aren%27t%20Dippy%20.mp3 -Today%20Today.mp3 -Two%20Time.mp3 \ No newline at end of file diff --git a/data/sample.txt b/data/sample.txt index f34ad67..affaf3d 100644 --- a/data/sample.txt +++ b/data/sample.txt @@ -1,4 +1,4 @@ https://lofigirl.com/wp-content/uploads/ 2023/06/Foudroie-Finding-The-Edge-V2.mp3 2023/04/2-In-Front-Of-Me.mp3 -https://file-examples.com/storage/fea570b16e6703ef79e65b4/2017/11/file_example_MP3_5MG.mp3 \ No newline at end of file +https://file-examples.com/storage/fe85f7a43b689349d9c8f18/2017/11/file_example_MP3_1MG.mp3 \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index e1fb2d4..0a0d9e7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,10 +2,8 @@ #![warn(clippy::all, clippy::pedantic, clippy::nursery)] -use std::path::PathBuf; - use clap::{Parser, Subcommand}; -use eyre::OptionExt; +use std::path::PathBuf; mod messages; mod play; @@ -81,9 +79,9 @@ enum Commands { } /// Gets lowfi's data directory. -pub fn data_dir() -> eyre::Result { +pub fn data_dir() -> eyre::Result { let dir = dirs::data_dir() - .ok_or_eyre("data directory not found, are you *really* running this on wasm?")? + .ok_or(player::Error::DataDir)? .join("lowfi"); Ok(dir) @@ -91,8 +89,7 @@ pub fn data_dir() -> eyre::Result { #[tokio::main] async fn main() -> eyre::Result<()> { - #[cfg(target_os = "android")] - compile_error!("Android Audio API not supported due to threading shenanigans"); + color_eyre::install()?; let cli = Args::parse(); @@ -100,12 +97,14 @@ async fn main() -> eyre::Result<()> { match command { // TODO: Actually distinguish between sources. Commands::Scrape { - source, + source: _, extension, include_full, - } => scrapers::lofigirl::scrape(extension, include_full).await, + } => scrapers::lofigirl::scrape(extension, include_full).await?, } } else { - play::play(cli).await - } + play::play(cli).await?; + }; + + Ok(()) } diff --git a/src/messages.rs b/src/messages.rs index b26b555..b33e0c6 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -1,6 +1,6 @@ /// Handles communication between the frontend & audio player. #[derive(PartialEq, Debug, Clone, Copy)] -pub enum Messages { +pub enum Message { /// Notifies the audio server that it should update the track. Next, diff --git a/src/play.rs b/src/play.rs index 98b4903..64ffed5 100644 --- a/src/play.rs +++ b/src/play.rs @@ -1,89 +1,42 @@ //! Responsible for the basic initialization & shutdown of the audio server & frontend. -use std::env; +use crossterm::cursor::Show; +use crossterm::event::PopKeyboardEnhancementFlags; +use crossterm::terminal::{self, Clear, ClearType}; use std::io::{stdout, IsTerminal}; -use std::path::PathBuf; +use std::process::exit; use std::sync::Arc; - -use eyre::eyre; -use tokio::fs; +use std::{env, panic}; use tokio::{sync::mpsc, task}; -use crate::messages::Messages; -use crate::player::ui; +use crate::messages::Message; +use crate::player::persistent_volume::PersistentVolume; use crate::player::Player; +use crate::player::{self, ui}; use crate::Args; -/// This is the representation of the persistent volume, -/// which is loaded at startup and saved on shutdown. -#[derive(Clone, Copy)] -pub struct PersistentVolume { - /// The volume, as a percentage. - inner: u16, -} - -impl PersistentVolume { - /// Retrieves the config directory. - async fn config() -> eyre::Result { - let config = dirs::config_dir() - .ok_or_else(|| eyre!("Couldn't find config directory"))? - .join(PathBuf::from("lowfi")); - - if !config.exists() { - fs::create_dir_all(&config).await?; - } - - Ok(config) - } - - /// Returns the volume as a float from 0 to 1. - pub fn float(self) -> f32 { - f32::from(self.inner) / 100.0 - } - - /// Loads the [`PersistentVolume`] from [`dirs::config_dir()`]. - pub async fn load() -> eyre::Result { - let config = Self::config().await?; - let volume = config.join(PathBuf::from("volume.txt")); - - // Basically just read from the volume file if it exists, otherwise return 100. - let volume = if volume.exists() { - let contents = fs::read_to_string(volume).await?; - let trimmed = contents.trim(); - let stripped = trimmed.strip_suffix("%").unwrap_or(trimmed); - stripped - .parse() - .map_err(|_error| eyre!("volume.txt file is invalid"))? - } else { - fs::write(&volume, "100").await?; - 100u16 - }; - - Ok(Self { inner: volume }) - } - - /// Saves `volume` to `volume.txt`. - pub async fn save(volume: f32) -> eyre::Result<()> { - let config = Self::config().await?; - let path = config.join(PathBuf::from("volume.txt")); - - // Already rounded & absolute, therefore this should be safe. - #[expect( - clippy::as_conversions, - clippy::cast_sign_loss, - clippy::cast_possible_truncation - )] - let percentage = (volume * 100.0).abs().round() as u16; - - fs::write(path, percentage.to_string()).await?; - - Ok(()) - } -} - /// Initializes the audio server, and then safely stops /// it when the frontend quits. -pub async fn play(args: Args) -> eyre::Result<()> { +pub async fn play(args: Args) -> eyre::Result<(), player::Error> { + // TODO: This isn't a great way of doing things, + // but it's better than vanilla behaviour at least. + let eyre_hook = panic::take_hook(); + + panic::set_hook(Box::new(move |x| { + let mut lock = stdout().lock(); + crossterm::execute!( + lock, + Clear(ClearType::FromCursorDown), + Show, + PopKeyboardEnhancementFlags + ) + .unwrap(); + terminal::disable_raw_mode().unwrap(); + + eyre_hook(x); + exit(1) + })); + // Actually initializes the player. // Stream kept here in the master thread to keep it alive. let (player, stream) = Player::new(&args).await?; @@ -102,16 +55,19 @@ pub async fn play(args: Args) -> eyre::Result<()> { }; // Sends the player an "init" signal telling it to start playing a song straight away. - tx.send(Messages::Init).await?; + tx.send(Message::Init).await?; // Actually starts the player. Player::play(Arc::clone(&player), tx.clone(), rx, args.debug).await?; // Save the volume.txt file for the next session. - PersistentVolume::save(player.sink.volume()).await?; + PersistentVolume::save(player.sink.volume()) + .await + .map_err(player::Error::PersistentVolumeSave)?; + drop(stream); player.sink.stop(); - ui.and_then(|x| Some(x.abort())); + ui.map(|x| x.abort()); Ok(()) } diff --git a/src/player.rs b/src/player.rs index d463c0b..9f8336a 100644 --- a/src/player.rs +++ b/src/player.rs @@ -13,7 +13,6 @@ use std::{ use arc_swap::ArcSwapOption; use downloader::Downloader; -use eyre::Context; use reqwest::Client; use rodio::{OutputStream, OutputStreamBuilder, Sink}; use tokio::{ @@ -29,8 +28,8 @@ use tokio::{ use mpris_server::{PlaybackStatus, PlayerInterface, Property}; use crate::{ - messages::Messages, - play::PersistentVolume, + messages::Message, + player::{self, persistent_volume::PersistentVolume}, tracks::{self, list::List}, Args, }; @@ -38,9 +37,13 @@ use crate::{ pub mod audio; pub mod bookmark; pub mod downloader; +pub mod error; +pub mod persistent_volume; pub mod queue; pub mod ui; +pub use error::Error; + #[cfg(feature = "mpris")] pub mod mpris; @@ -104,14 +107,16 @@ impl Player { /// Initializes the entire player, including audio devices & sink. /// /// This also will load the track list & persistent volume. - pub async fn new(args: &Args) -> eyre::Result<(Self, OutputStream)> { + pub async fn new(args: &Args) -> eyre::Result<(Self, OutputStream), player::Error> { // Load the volume file. - let volume = PersistentVolume::load().await?; + let volume = PersistentVolume::load() + .await + .map_err(player::Error::PersistentVolumeLoad)?; // Load the track list. let list = List::load(args.track_list.as_ref()) .await - .wrap_err("unable to load the track list")?; + .map_err(player::Error::TrackListLoad)?; // We should only shut up alsa forcefully on Linux if we really have to. #[cfg(target_os = "linux")] @@ -160,10 +165,10 @@ impl Player { /// The [Downloader]s internal buffer size is determined by `buf_size`. pub async fn play( player: Arc, - tx: Sender, - mut rx: Receiver, + tx: Sender, + mut rx: Receiver, debug: bool, - ) -> eyre::Result<()> { + ) -> eyre::Result<(), player::Error> { // Initialize the mpris player. // // We're initializing here, despite MPRIS being a "user interface", @@ -213,11 +218,11 @@ impl Player { // It's also important to note that the condition is only checked at the // beginning of the loop, not throughout. Ok(()) = task::spawn_blocking(move || clone.sink.sleep_until_end()), - if new => Messages::Next, + if new => Message::Next, }; match msg { - Messages::Next | Messages::Init | Messages::TryAgain => { + Message::Next | Message::Init | Message::TryAgain => { player.bookmarked.swap(false, Ordering::Relaxed); // We manually skipped, so we shouldn't actually wait for the song @@ -225,7 +230,7 @@ impl Player { new = false; // This basically just prevents `Next` while a song is still currently loading. - if msg == Messages::Next && !player.current_exists() { + if msg == Message::Next && !player.current_exists() { continue; } @@ -238,19 +243,19 @@ impl Player { debug, )); } - Messages::Play => { + Message::Play => { player.sink.play(); #[cfg(feature = "mpris")] mpris.playback(PlaybackStatus::Playing).await?; } - Messages::Pause => { + Message::Pause => { player.sink.pause(); #[cfg(feature = "mpris")] mpris.playback(PlaybackStatus::Paused).await?; } - Messages::PlayPause => { + Message::PlayPause => { if player.sink.is_paused() { player.sink.play(); } else { @@ -262,7 +267,7 @@ impl Player { .playback(mpris.player().playback_status().await?) .await?; } - Messages::ChangeVolume(change) => { + Message::ChangeVolume(change) => { player.set_volume(player.sink.volume() + change); #[cfg(feature = "mpris")] @@ -273,7 +278,7 @@ impl Player { // This basically just continues, but more importantly, it'll re-evaluate // the select macro at the beginning of the loop. // See the top section to find out why this matters. - Messages::NewSong => { + Message::NewSong => { // We've recieved `NewSong`, so on the next loop iteration we'll // begin waiting for the song to be over in order to autoplay. new = true; @@ -288,7 +293,7 @@ impl Player { continue; } - Messages::Bookmark => { + Message::Bookmark => { let current = player.current.load(); let current = current.as_ref().unwrap(); @@ -300,11 +305,12 @@ impl Player { None }, ) - .await?; + .await + .map_err(player::Error::Bookmark)?; player.bookmarked.swap(bookmarked, Ordering::Relaxed); } - Messages::Quit => break, + Message::Quit => break, } } diff --git a/src/player/audio.rs b/src/player/audio.rs index 81d62a4..2c5bde4 100644 --- a/src/player/audio.rs +++ b/src/player/audio.rs @@ -1,10 +1,13 @@ #[cfg(target_os = "linux")] use rodio::OutputStream; +#[cfg(target_os = "linux")] +use crate::player; + /// This gets the output stream while also shutting up alsa with [libc]. /// Uses raw libc calls, and therefore is functional only on Linux. #[cfg(target_os = "linux")] -pub fn silent_get_output_stream() -> eyre::Result { +pub fn silent_get_output_stream() -> eyre::Result { use libc::freopen; use rodio::OutputStreamBuilder; use std::ffi::CString; diff --git a/src/player/bookmark.rs b/src/player/bookmark.rs index 393cd0e..1825dcd 100644 --- a/src/player/bookmark.rs +++ b/src/player/bookmark.rs @@ -9,7 +9,7 @@ use crate::data_dir; /// /// Returns whether the track is now bookmarked, or not. pub async fn bookmark(path: String, custom: Option) -> eyre::Result { - let mut entry = format!("{path}"); + let mut entry = path.to_string(); if let Some(custom) = custom { entry.push('!'); entry.push_str(&custom); @@ -25,6 +25,7 @@ pub async fn bookmark(path: String, custom: Option) -> eyre::Result), + + #[error("unable to load track list")] + TrackListLoad(eyre::Error), + + #[error("interfacing with audio failed")] + Stream(#[from] rodio::StreamError), + + #[error("NUL error, if you see this, something has gone VERY wrong")] + Nul(#[from] NulError), + + #[error("unable to send or prepare network request")] + Reqwest(#[from] reqwest::Error), + + #[cfg(feature = "mpris")] + #[error("mpris bus error")] + ZBus(#[from] zbus::Error), + + // TODO: This has a terrible error message, mainly because I barely understand + // what this error even represents. What does fdo mean?!?!? Why, MPRIS!?!? + #[cfg(feature = "mpris")] + #[error("mpris fdo (zbus interface) error")] + Fdo(#[from] fdo::Error), + + #[error("unable to notify downloader")] + DownloaderNotify(#[from] SendError<()>), + + #[error("unable to bookmark track")] + Bookmark(eyre::Error), + + #[error("unable to find data directory")] + DataDir, +} diff --git a/src/player/mpris.rs b/src/player/mpris.rs index 2ab8b6a..d260d72 100644 --- a/src/player/mpris.rs +++ b/src/player/mpris.rs @@ -10,7 +10,7 @@ use mpris_server::{ use tokio::sync::mpsc::Sender; use super::ui; -use super::Messages; +use super::Message; const ERROR: fdo::Error = fdo::Error::Failed(String::new()); @@ -21,7 +21,7 @@ pub struct Player { /// The audio server sender, which is used to communicate with /// the audio sender for skips and a few other inputs. - pub sender: Sender, + pub sender: Sender, } impl RootInterface for Player { @@ -31,7 +31,7 @@ impl RootInterface for Player { async fn quit(&self) -> fdo::Result<()> { self.sender - .send(Messages::Quit) + .send(Message::Quit) .await .map_err(|_error| ERROR) } @@ -80,7 +80,7 @@ impl RootInterface for Player { impl PlayerInterface for Player { async fn next(&self) -> fdo::Result<()> { self.sender - .send(Messages::Next) + .send(Message::Next) .await .map_err(|_error| ERROR) } @@ -91,14 +91,14 @@ impl PlayerInterface for Player { async fn pause(&self) -> fdo::Result<()> { self.sender - .send(Messages::Pause) + .send(Message::Pause) .await .map_err(|_error| ERROR) } async fn play_pause(&self) -> fdo::Result<()> { self.sender - .send(Messages::PlayPause) + .send(Message::PlayPause) .await .map_err(|_error| ERROR) } @@ -109,7 +109,7 @@ impl PlayerInterface for Player { async fn play(&self) -> fdo::Result<()> { self.sender - .send(Messages::Play) + .send(Message::Play) .await .map_err(|_error| ERROR) } @@ -247,10 +247,8 @@ impl Server { pub async fn changed( &self, properties: impl IntoIterator + Send + Sync, - ) -> eyre::Result<()> { - self.inner.properties_changed(properties).await?; - - Ok(()) + ) -> zbus::Result<()> { + self.inner.properties_changed(properties).await } /// Shorthand to emit a `PropertiesChanged` signal, specifically about playback. @@ -266,7 +264,10 @@ impl Server { } /// Creates a new MPRIS server. - pub async fn new(player: Arc, sender: Sender) -> eyre::Result { + pub async fn new( + player: Arc, + sender: Sender, + ) -> eyre::Result { let suffix = if env::var("LOWFI_FIXED_MPRIS_NAME").is_ok_and(|x| x == "1") { String::from("lowfi") } else { diff --git a/src/player/persistent_volume.rs b/src/player/persistent_volume.rs new file mode 100644 index 0000000..6609f8c --- /dev/null +++ b/src/player/persistent_volume.rs @@ -0,0 +1,70 @@ +use eyre::eyre; +use std::path::PathBuf; +use tokio::fs; + +/// This is the representation of the persistent volume, +/// which is loaded at startup and saved on shutdown. +#[derive(Clone, Copy)] +pub struct PersistentVolume { + /// The volume, as a percentage. + inner: u16, +} + +impl PersistentVolume { + /// Retrieves the config directory. + async fn config() -> eyre::Result { + let config = dirs::config_dir() + .ok_or_else(|| eyre!("Couldn't find config directory"))? + .join(PathBuf::from("lowfi")); + + if !config.exists() { + fs::create_dir_all(&config).await?; + } + + Ok(config) + } + + /// Returns the volume as a float from 0 to 1. + pub fn float(self) -> f32 { + f32::from(self.inner) / 100.0 + } + + /// Loads the [`PersistentVolume`] from [`dirs::config_dir()`]. + pub async fn load() -> eyre::Result { + let config = Self::config().await?; + let volume = config.join(PathBuf::from("volume.txt")); + + // Basically just read from the volume file if it exists, otherwise return 100. + let volume = if volume.exists() { + let contents = fs::read_to_string(volume).await?; + let trimmed = contents.trim(); + let stripped = trimmed.strip_suffix("%").unwrap_or(trimmed); + stripped + .parse() + .map_err(|_error| eyre!("volume.txt file is invalid"))? + } else { + fs::write(&volume, "100").await?; + 100u16 + }; + + Ok(Self { inner: volume }) + } + + /// Saves `volume` to `volume.txt`. + pub async fn save(volume: f32) -> eyre::Result<()> { + let config = Self::config().await?; + let path = config.join(PathBuf::from("volume.txt")); + + // Already rounded & absolute, therefore this should be safe. + #[expect( + clippy::as_conversions, + clippy::cast_sign_loss, + clippy::cast_possible_truncation + )] + let percentage = (volume * 100.0).abs().round() as u16; + + fs::write(path, percentage.to_string()).await?; + + Ok(()) + } +} diff --git a/src/player/queue.rs b/src/player/queue.rs index 52d0fc4..d55e50d 100644 --- a/src/player/queue.rs +++ b/src/player/queue.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use tokio::{sync::mpsc::Sender, time::sleep}; use crate::{ - messages::Messages, + messages::Message, player::{downloader::Downloader, Player, TIMEOUT}, tracks, }; @@ -43,7 +43,7 @@ impl Player { pub async fn next( player: Arc, itx: Sender<()>, - tx: Sender, + tx: Sender, debug: bool, ) -> eyre::Result<()> { // Stop the sink. @@ -61,18 +61,18 @@ impl Player { Downloader::notify(&itx).await?; // Notify the audio server that the next song has actually been downloaded. - tx.send(Messages::NewSong).await?; + tx.send(Message::NewSong).await?; } Err(error) => { if !error.is_timeout() { if debug { - panic!("{:?}", error) + panic!("{error:?}") } sleep(TIMEOUT).await; } - tx.send(Messages::TryAgain).await?; + tx.send(Message::TryAgain).await?; } }; diff --git a/src/player/ui.rs b/src/player/ui.rs index 00b6879..13c1789 100644 --- a/src/player/ui.rs +++ b/src/player/ui.rs @@ -32,7 +32,8 @@ use thiserror::Error; use tokio::{sync::mpsc::Sender, task, time::sleep}; use unicode_segmentation::UnicodeSegmentation; -use super::{Messages, Player}; +use super::Player; +use crate::messages::Message; mod components; mod input; @@ -47,8 +48,8 @@ pub enum UIError { #[error("unable to write output")] Write(#[from] std::io::Error), - #[error("sending message to backend failed")] - Communication(#[from] tokio::sync::mpsc::error::SendError), + #[error("sending message to backend from ui failed")] + Communication(#[from] tokio::sync::mpsc::error::SendError), } /// How long the audio bar will be visible for when audio is adjusted. @@ -204,7 +205,7 @@ async fn interface( window.draw(menu, false)?; - let delta = 1.0 / (fps as f32); + let delta = 1.0 / f32::from(fps); sleep(Duration::from_secs_f32(delta)).await; } } @@ -284,10 +285,11 @@ impl Drop for Environment { /// previous terminal history. pub async fn start( player: Arc, - sender: Sender, + sender: Sender, args: Args, ) -> eyre::Result<(), UIError> { let environment = Environment::ready(args.alternate)?; + let interface = task::spawn(interface( Arc::clone(&player), args.minimalist, diff --git a/src/player/ui/components.rs b/src/player/ui/components.rs index aeac244..1dfd417 100644 --- a/src/player/ui/components.rs +++ b/src/player/ui/components.rs @@ -88,7 +88,7 @@ impl ActionBar { |(subject, len)| { ( format!("{} {}{}", word, if star { "*" } else { "" }, subject.bold()), - word.len() + 1 + len + if star { 1 } else { 0 }, + word.len() + 1 + len + usize::from(star), ) }, ) diff --git a/src/player/ui/input.rs b/src/player/ui/input.rs index 6b3d0a9..0177406 100644 --- a/src/player/ui/input.rs +++ b/src/player/ui/input.rs @@ -7,11 +7,11 @@ use tokio::sync::mpsc::Sender; use crate::player::{ ui::{self, UIError}, - Messages, + Message, }; /// Starts the listener to recieve input from the terminal for various events. -pub async fn listen(sender: Sender) -> eyre::Result<(), UIError> { +pub async fn listen(sender: Sender) -> eyre::Result<(), UIError> { let mut reader = EventStream::new(); loop { @@ -25,29 +25,29 @@ pub async fn listen(sender: Sender) -> eyre::Result<(), UIError> { let messages = match event.code { // Arrow key volume controls. - KeyCode::Up => Messages::ChangeVolume(0.1), - KeyCode::Right => Messages::ChangeVolume(0.01), - KeyCode::Down => Messages::ChangeVolume(-0.1), - KeyCode::Left => Messages::ChangeVolume(-0.01), + KeyCode::Up => Message::ChangeVolume(0.1), + KeyCode::Right => Message::ChangeVolume(0.01), + KeyCode::Down => Message::ChangeVolume(-0.1), + KeyCode::Left => Message::ChangeVolume(-0.01), KeyCode::Char(character) => match character.to_ascii_lowercase() { // Ctrl+C - 'c' if event.modifiers == KeyModifiers::CONTROL => Messages::Quit, + 'c' if event.modifiers == KeyModifiers::CONTROL => Message::Quit, // Quit - 'q' => Messages::Quit, + 'q' => Message::Quit, // Skip/Next - 's' | 'n' | 'l' => Messages::Next, + 's' | 'n' | 'l' => Message::Next, // Pause - 'p' | ' ' => Messages::PlayPause, + 'p' | ' ' => Message::PlayPause, // Volume up & down - '+' | '=' | 'k' => Messages::ChangeVolume(0.1), - '-' | '_' | 'j' => Messages::ChangeVolume(-0.1), + '+' | '=' | 'k' => Message::ChangeVolume(0.1), + '-' | '_' | 'j' => Message::ChangeVolume(-0.1), // Bookmark - 'b' => Messages::Bookmark, + 'b' => Message::Bookmark, _ => continue, }, @@ -55,18 +55,18 @@ pub async fn listen(sender: Sender) -> eyre::Result<(), UIError> { KeyCode::Media(media) => match media { event::MediaKeyCode::Pause | event::MediaKeyCode::Play - | event::MediaKeyCode::PlayPause => Messages::PlayPause, - event::MediaKeyCode::Stop => Messages::Pause, - event::MediaKeyCode::TrackNext => Messages::Next, - event::MediaKeyCode::LowerVolume => Messages::ChangeVolume(-0.1), - event::MediaKeyCode::RaiseVolume => Messages::ChangeVolume(0.1), - event::MediaKeyCode::MuteVolume => Messages::ChangeVolume(-1.0), + | event::MediaKeyCode::PlayPause => Message::PlayPause, + event::MediaKeyCode::Stop => Message::Pause, + event::MediaKeyCode::TrackNext => Message::Next, + event::MediaKeyCode::LowerVolume => Message::ChangeVolume(-0.1), + event::MediaKeyCode::RaiseVolume => Message::ChangeVolume(0.1), + event::MediaKeyCode::MuteVolume => Message::ChangeVolume(-1.0), _ => continue, }, _ => continue, }; - if let Messages::ChangeVolume(_) = messages { + if let Message::ChangeVolume(_) = messages { ui::flash_audio(); } diff --git a/src/tracks.rs b/src/tracks.rs index 3768adf..6b6887a 100644 --- a/src/tracks.rs +++ b/src/tracks.rs @@ -51,8 +51,8 @@ pub enum TrackError { } impl TrackError { - pub fn is_timeout(&self) -> bool { - return matches!(self, TrackError::Timeout); + pub const fn is_timeout(&self) -> bool { + matches!(self, Self::Timeout) } } @@ -124,10 +124,8 @@ pub struct Info { impl Info { /// Decodes a URL string into normal UTF-8. fn decode_url(text: &str) -> String { - #[expect( - clippy::tuple_array_conversions, - reason = "the tuple contains smart pointers, so it's not really practical to use `into()`" - )] + // The tuple contains smart pointers, so it's not really practical to use `into()`. + #[allow(clippy::tuple_array_conversions)] form_urlencoded::parse(text.as_bytes()) .map(|(key, val)| [key, val].concat()) .collect() @@ -172,10 +170,8 @@ impl Info { if skip == formatted.len() { Ok(formatted) } else { - #[expect( - clippy::string_slice, - reason = "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)] Ok(String::from(&formatted[skip..])) } } diff --git a/src/tracks/list.rs b/src/tracks/list.rs index d15faf8..d04fe55 100644 --- a/src/tracks/list.rs +++ b/src/tracks/list.rs @@ -17,7 +17,7 @@ use super::QueuedTrack; #[derive(Clone)] pub struct List { /// The "name" of the list, usually derived from a filename. - #[allow(dead_code, reason = "this code may not be dead depending on features")] + #[allow(dead_code)] pub name: String, /// Just the raw file, but seperated by `/n` (newlines). @@ -61,11 +61,11 @@ impl List { }; let data: Bytes = if let Some(x) = full_path.strip_prefix("file://") { - let path = if x.starts_with("~") { + let path = if x.starts_with('~') { let home_path = dirs::home_dir().ok_or(TrackError::InvalidPath)?; let home = home_path.to_str().ok_or(TrackError::InvalidPath)?; - x.replace("~", home) + x.replace('~', home) } else { x.to_owned() }; @@ -97,14 +97,15 @@ impl List { let (path, custom_name) = self.random_path(); let (data, full_path) = self.download(&path, client).await?; - let name = custom_name.map_or(super::TrackName::Raw(path.clone()), |formatted| { - super::TrackName::Formatted(formatted) - }); + let name = custom_name.map_or_else( + || super::TrackName::Raw(path.clone()), + |formatted| super::TrackName::Formatted(formatted), + ); Ok(QueuedTrack { name, - data, full_path, + data, }) }