From 6f9dab6aa8d19d3f80301dde82602ec65e8317ac Mon Sep 17 00:00:00 2001 From: talwat <83217276+talwat@users.noreply.github.com> Date: Tue, 15 Oct 2024 23:46:10 +0200 Subject: [PATCH] docs: again go into more depth in internal documentation --- src/player.rs | 14 ++++-- src/player/ui.rs | 97 +++++++++++------------------------------- src/player/ui/input.rs | 68 +++++++++++++++++++++++++++++ src/tracks/list.rs | 9 +++- 4 files changed, 112 insertions(+), 76 deletions(-) create mode 100644 src/player/ui/input.rs diff --git a/src/player.rs b/src/player.rs index 986de47..b555759 100644 --- a/src/player.rs +++ b/src/player.rs @@ -71,6 +71,13 @@ const TIMEOUT: Duration = Duration::from_secs(5); const BUFFER_SIZE: usize = 5; /// Main struct responsible for queuing up & playing tracks. +// TODO: Consider refactoring [Player] from being stored in an [Arc], +// TODO: so `Arc` into containing many smaller [Arc]s, being just +// TODO: `Player` as the type. +// TODO: +// TODO: This is conflicting, since then it'd clone ~10 smaller [Arc]s +// TODO: every single time, which could be even worse than having an +// TODO: [Arc] of an [Arc] in some cases (Like with [Sink] & [Client]). pub struct Player { /// [rodio]'s [`Sink`] which can control playback. pub sink: Sink, @@ -108,11 +115,12 @@ pub struct Player { _stream: OutputStream, } -/// SAFETY: This is necessary because [OutputStream] does not implement [Send], -/// SAFETY: even though it is perfectly possible. +// SAFETY: This is necessary because [OutputStream] does not implement [Send], +// due to some limitation with Android's Audio API. +// I'm pretty sure nobody will use lowfi with android, so this is safe. unsafe impl Send for Player {} -/// SAFETY: See implementation for [Send]. +// SAFETY: See implementation for [Send]. unsafe impl Sync for Player {} impl Player { diff --git a/src/player/ui.rs b/src/player/ui.rs index a8ef832..ec89027 100644 --- a/src/player/ui.rs +++ b/src/player/ui.rs @@ -13,21 +13,18 @@ use crate::Args; use crossterm::{ cursor::{Hide, MoveTo, MoveToColumn, MoveUp, Show}, - event::{ - self, EventStream, KeyCode, KeyModifiers, KeyboardEnhancementFlags, - PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, - }, + event::{KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags}, style::{Print, Stylize}, terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen}, }; -use futures::{FutureExt, StreamExt}; use lazy_static::lazy_static; use tokio::{sync::mpsc::Sender, task, time::sleep}; use super::{Messages, Player}; mod components; +mod input; /// The total width of the UI. const WIDTH: usize = 27; @@ -53,65 +50,6 @@ lazy_static! { static ref VOLUME_TIMER: AtomicUsize = AtomicUsize::new(0); } -/// Recieves input from the terminal for various events. -async fn input(sender: Sender) -> eyre::Result<()> { - let mut reader = EventStream::new(); - - loop { - let Some(Ok(event::Event::Key(event))) = reader.next().fuse().await else { - continue; - }; - - 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::Char(character) => match character.to_ascii_lowercase() { - // Ctrl+C - 'c' if event.modifiers == KeyModifiers::CONTROL => Messages::Quit, - - // Quit - 'q' => Messages::Quit, - - // Skip/Next - 's' | 'n' => Messages::Next, - - // Pause - 'p' => Messages::PlayPause, - - // Volume up & down - '+' | '=' => Messages::ChangeVolume(0.1), - '-' | '_' => Messages::ChangeVolume(-0.1), - - _ => continue, - }, - // Media keys - KeyCode::Media(media) => match media { - event::MediaKeyCode::Play => Messages::PlayPause, - event::MediaKeyCode::Pause => Messages::PlayPause, - 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), - _ => continue, - }, - _ => continue, - }; - - // If it's modifying the volume, then we'll set the `VOLUME_TIMER` to 1 - // so that the UI thread will know that it should show the audio bar. - if let Messages::ChangeVolume(_) = messages { - VOLUME_TIMER.store(1, Ordering::Relaxed); - } - - sender.send(messages).await?; - } -} - /// The code for the terminal interface itself. /// /// * `minimalist` - All this does is hide the bottom control bar. @@ -172,6 +110,9 @@ async fn interface(player: Arc, minimalist: bool) -> eyre::Result<()> { } } +/// The mpris server additionally needs a reference to the player, +/// since it frequently accesses the sink directly as well as +/// the current track. #[cfg(feature = "mpris")] async fn mpris( player: Arc, @@ -182,17 +123,26 @@ async fn mpris( .unwrap() } +/// Represents the terminal environment, and is used to properly +/// initialize and clean up the terminal. pub struct Environment { + /// Whether keyboard enhancements are enabled. enhancement: bool, + + /// Whether the terminal is in an alternate screen or not. alternate: bool, } impl Environment { + /// This prepares the terminal, returning an [Environment] helpful + /// for cleaning up afterwards. pub fn ready(alternate: bool) -> eyre::Result { - crossterm::execute!(stdout(), Hide)?; + let mut lock = stdout().lock(); + + crossterm::execute!(lock, Hide)?; if alternate { - crossterm::execute!(stdout(), EnterAlternateScreen, MoveTo(0, 0))?; + crossterm::execute!(lock, EnterAlternateScreen, MoveTo(0, 0))?; } terminal::enable_raw_mode()?; @@ -200,7 +150,7 @@ impl Environment { if enhancement { crossterm::execute!( - stdout(), + lock, PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES) )?; } @@ -211,15 +161,19 @@ impl Environment { }) } + /// Uses the information collected from initialization to safely close down + /// the terminal & restore it to it's previous state. pub fn cleanup(&self) -> eyre::Result<()> { + let mut lock = stdout().lock(); + if self.alternate { - crossterm::execute!(stdout(), LeaveAlternateScreen)?; + crossterm::execute!(lock, LeaveAlternateScreen)?; } - crossterm::execute!(stdout(), Clear(ClearType::FromCursorDown), Show)?; + crossterm::execute!(lock, Clear(ClearType::FromCursorDown), Show)?; if self.enhancement { - crossterm::execute!(stdout(), PopKeyboardEnhancementFlags)?; + crossterm::execute!(lock, PopKeyboardEnhancementFlags)?; } terminal::disable_raw_mode()?; @@ -231,6 +185,7 @@ impl Environment { } impl Drop for Environment { + /// Just a wrapper for [Environment::cleanup] which ignores any errors thrown. fn drop(&mut self) { // Well, we're dropping it, so it doesn't really matter if there's an error. let _ = self.cleanup(); @@ -254,7 +209,7 @@ pub async fn start(player: Arc, sender: Sender, args: Args) -> let interface = task::spawn(interface(Arc::clone(&player), args.minimalist)); - input(sender.clone()).await?; + input::listen(sender.clone()).await?; interface.abort(); environment.cleanup()?; diff --git a/src/player/ui/input.rs b/src/player/ui/input.rs new file mode 100644 index 0000000..c6882d0 --- /dev/null +++ b/src/player/ui/input.rs @@ -0,0 +1,68 @@ +use std::sync::atomic::Ordering; + +use crossterm::event::{self, EventStream, KeyCode, KeyModifiers}; +use futures::{FutureExt, StreamExt}; +use tokio::sync::mpsc::Sender; + +use crate::player::Messages; + +use super::VOLUME_TIMER; + +/// Starts the listener to recieve input from the terminal for various events. +pub async fn listen(sender: Sender) -> eyre::Result<()> { + let mut reader = EventStream::new(); + + loop { + let Some(Ok(event::Event::Key(event))) = reader.next().fuse().await else { + continue; + }; + + 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::Char(character) => match character.to_ascii_lowercase() { + // Ctrl+C + 'c' if event.modifiers == KeyModifiers::CONTROL => Messages::Quit, + + // Quit + 'q' => Messages::Quit, + + // Skip/Next + 's' | 'n' => Messages::Next, + + // Pause + 'p' => Messages::PlayPause, + + // Volume up & down + '+' | '=' => Messages::ChangeVolume(0.1), + '-' | '_' => Messages::ChangeVolume(-0.1), + + _ => continue, + }, + // Media keys + KeyCode::Media(media) => match media { + event::MediaKeyCode::Play => Messages::PlayPause, + event::MediaKeyCode::Pause => Messages::PlayPause, + 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), + _ => continue, + }, + _ => continue, + }; + + // If it's modifying the volume, then we'll set the `VOLUME_TIMER` to 1 + // so that the UI thread will know that it should show the audio bar. + if let Messages::ChangeVolume(_) = messages { + VOLUME_TIMER.store(1, Ordering::Relaxed); + } + + sender.send(messages).await?; + } +} diff --git a/src/tracks/list.rs b/src/tracks/list.rs index e00f9c5..1f641eb 100644 --- a/src/tracks/list.rs +++ b/src/tracks/list.rs @@ -13,6 +13,8 @@ use super::Track; /// See the [README](https://github.com/talwat/lowfi?tab=readme-ov-file#the-format) for more details about the format. #[derive(Clone)] pub struct List { + /// Just the raw file, but seperated by `/n` (newlines). + /// `lines[0]` is the base, with the rest being tracks. lines: Vec, } @@ -24,8 +26,11 @@ impl List { /// Gets the name of a random track. fn random_name(&self) -> String { - // We're getting from 1 here, since due to how rust vectors work it's - // slow to drain only a single element from the start, so we can just keep it in. + // We're getting from 1 here, since the base is at `self.lines[0]`. + // + // We're also not pre-trimming `self.lines` into `base` & `tracks` due to + // how rust vectors work, sinceslow to drain only a single element from + // the start, so it's faster to just keep it in & work around it. let random = rand::thread_rng().gen_range(1..self.lines.len()); self.lines[random].to_owned() }