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;
/// 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 {
/// [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 {

View File

@ -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<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.
///
/// * `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")]
async fn mpris(
player: Arc<Player>,
@ -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<Self> {
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<Player>, sender: Sender<Messages>, 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()?;

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.
#[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<String>,
}
@ -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()
}