mirror of
https://github.com/talwat/lowfi
synced 2024-12-25 18:51: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;
|
||||
|
||||
/// 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 {
|
||||
|
@ -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
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.
|
||||
#[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()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user