mirror of
https://github.com/talwat/lowfi
synced 2024-12-26 19:21:54 +00:00
docs: again go into more depth in internal documentation
This commit is contained in:
parent
e3e7c28ab0
commit
6f9dab6aa8
@ -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 {
|
||||||
|
@ -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
68
src/player/ui/input.rs
Normal 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?;
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user