docs: again go into more depth in internal documentation

This commit is contained in:
talwat 2024-10-15 23:46:10 +02:00
parent e3e7c28ab0
commit 6f9dab6aa8
4 changed files with 112 additions and 76 deletions

View File

@ -71,6 +71,13 @@ const TIMEOUT: Duration = Duration::from_secs(5);
const BUFFER_SIZE: usize = 5; const BUFFER_SIZE: usize = 5;
/// Main struct responsible for queuing up & playing tracks. /// Main struct responsible for queuing up & playing tracks.
// TODO: Consider refactoring [Player] from being stored in an [Arc],
// TODO: so `Arc<Player>` 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 { pub struct Player {
/// [rodio]'s [`Sink`] which can control playback. /// [rodio]'s [`Sink`] which can control playback.
pub sink: Sink, pub sink: Sink,
@ -108,11 +115,12 @@ pub struct Player {
_stream: OutputStream, _stream: OutputStream,
} }
/// SAFETY: This is necessary because [OutputStream] does not implement [Send], // SAFETY: This is necessary because [OutputStream] does not implement [Send],
/// SAFETY: even though it is perfectly possible. // 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 {} unsafe impl Send for Player {}
/// SAFETY: See implementation for [Send]. // SAFETY: See implementation for [Send].
unsafe impl Sync for Player {} unsafe impl Sync for Player {}
impl Player { impl Player {

View File

@ -13,21 +13,18 @@ use crate::Args;
use crossterm::{ use crossterm::{
cursor::{Hide, MoveTo, MoveToColumn, MoveUp, Show}, cursor::{Hide, MoveTo, MoveToColumn, MoveUp, Show},
event::{ event::{KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags},
self, EventStream, KeyCode, KeyModifiers, KeyboardEnhancementFlags,
PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
},
style::{Print, Stylize}, style::{Print, Stylize},
terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen}, terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
}; };
use futures::{FutureExt, StreamExt};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use tokio::{sync::mpsc::Sender, task, time::sleep}; use tokio::{sync::mpsc::Sender, task, time::sleep};
use super::{Messages, Player}; use super::{Messages, Player};
mod components; mod components;
mod input;
/// The total width of the UI. /// The total width of the UI.
const WIDTH: usize = 27; const WIDTH: usize = 27;
@ -53,65 +50,6 @@ lazy_static! {
static ref VOLUME_TIMER: AtomicUsize = AtomicUsize::new(0); static ref VOLUME_TIMER: AtomicUsize = AtomicUsize::new(0);
} }
/// Recieves input from the terminal for various events.
async fn input(sender: Sender<Messages>) -> 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. /// The code for the terminal interface itself.
/// ///
/// * `minimalist` - All this does is hide the bottom control bar. /// * `minimalist` - All this does is hide the bottom control bar.
@ -172,6 +110,9 @@ async fn interface(player: Arc<Player>, 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")] #[cfg(feature = "mpris")]
async fn mpris( async fn mpris(
player: Arc<Player>, player: Arc<Player>,
@ -182,17 +123,26 @@ async fn mpris(
.unwrap() .unwrap()
} }
/// Represents the terminal environment, and is used to properly
/// initialize and clean up the terminal.
pub struct Environment { pub struct Environment {
/// Whether keyboard enhancements are enabled.
enhancement: bool, enhancement: bool,
/// Whether the terminal is in an alternate screen or not.
alternate: bool, alternate: bool,
} }
impl Environment { impl Environment {
/// This prepares the terminal, returning an [Environment] helpful
/// for cleaning up afterwards.
pub fn ready(alternate: bool) -> eyre::Result<Self> { pub fn ready(alternate: bool) -> eyre::Result<Self> {
crossterm::execute!(stdout(), Hide)?; let mut lock = stdout().lock();
crossterm::execute!(lock, Hide)?;
if alternate { if alternate {
crossterm::execute!(stdout(), EnterAlternateScreen, MoveTo(0, 0))?; crossterm::execute!(lock, EnterAlternateScreen, MoveTo(0, 0))?;
} }
terminal::enable_raw_mode()?; terminal::enable_raw_mode()?;
@ -200,7 +150,7 @@ impl Environment {
if enhancement { if enhancement {
crossterm::execute!( crossterm::execute!(
stdout(), lock,
PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES) 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<()> { pub fn cleanup(&self) -> eyre::Result<()> {
let mut lock = stdout().lock();
if self.alternate { 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 { if self.enhancement {
crossterm::execute!(stdout(), PopKeyboardEnhancementFlags)?; crossterm::execute!(lock, PopKeyboardEnhancementFlags)?;
} }
terminal::disable_raw_mode()?; terminal::disable_raw_mode()?;
@ -231,6 +185,7 @@ impl Environment {
} }
impl Drop for Environment { impl Drop for Environment {
/// Just a wrapper for [Environment::cleanup] which ignores any errors thrown.
fn drop(&mut self) { fn drop(&mut self) {
// Well, we're dropping it, so it doesn't really matter if there's an error. // Well, we're dropping it, so it doesn't really matter if there's an error.
let _ = self.cleanup(); let _ = self.cleanup();
@ -254,7 +209,7 @@ pub async fn start(player: Arc<Player>, sender: Sender<Messages>, args: Args) ->
let interface = task::spawn(interface(Arc::clone(&player), args.minimalist)); let interface = task::spawn(interface(Arc::clone(&player), args.minimalist));
input(sender.clone()).await?; input::listen(sender.clone()).await?;
interface.abort(); interface.abort();
environment.cleanup()?; environment.cleanup()?;

68
src/player/ui/input.rs Normal file
View File

@ -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<Messages>) -> 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?;
}
}

View File

@ -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. /// See the [README](https://github.com/talwat/lowfi?tab=readme-ov-file#the-format) for more details about the format.
#[derive(Clone)] #[derive(Clone)]
pub struct List { pub struct List {
/// Just the raw file, but seperated by `/n` (newlines).
/// `lines[0]` is the base, with the rest being tracks.
lines: Vec<String>, lines: Vec<String>,
} }
@ -24,8 +26,11 @@ impl List {
/// Gets the name of a random track. /// Gets the name of a random track.
fn random_name(&self) -> String { fn random_name(&self) -> String {
// We're getting from 1 here, since due to how rust vectors work it's // We're getting from 1 here, since the base is at `self.lines[0]`.
// slow to drain only a single element from the start, so we can just keep it in. //
// 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()); let random = rand::thread_rng().gen_range(1..self.lines.len());
self.lines[random].to_owned() self.lines[random].to_owned()
} }