use crossbeam_channel::{self as channel, Receiver, Sender, TryRecvError};
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
use rodio::{Decoder, OutputStream, OutputStreamHandle, Sink};
#[cfg(debug_assertions)]
use std::fmt::{self, Debug, Formatter};
use std::{
io::{Read, Seek},
thread::{Builder, JoinHandle},
};
use super::{ChannelError, Error};
const DISCONNECTED: &str = "DISCONNECTED CHANNEL";
pub struct IOHandle {
sound_out: (OutputStream, OutputStreamHandle), controls: Controls,
playback: Sink,
}
#[cfg_attr(any(debug_assertions, feature = "debug"), derive(Debug))]
pub struct Controls {
control_thread: JoinHandle<()>,
exit_notifier: Sender<()>,
signal_receiver: Receiver<Signal>,
}
#[cfg_attr(any(debug_assertions, feature = "debug"), derive(Debug))]
#[cfg_attr(
any(debug_assertions, feature = "traits"),
derive(PartialEq, Eq, PartialOrd, Ord),
derive(Hash)
)]
#[repr(u8)]
pub enum Signal {
PlaylistNext = 0b0101, PlaylistBack = 0b0110, Exit = 0b0111, PlaylistReset = 0b0100,
TrackNext = 0b1001, TrackBack = 0b1010, Play = 0b1011, TrackReset = 0b1000,
VolumeIncrease = 0b1101, VolumeDecrease = 0b1110, Mute = 0b1111, VolumeReset = 0b1100, }
impl IOHandle {
#[inline(always)]
pub fn controls_get(&self) -> &Controls {
&self.controls
}
#[inline(always)]
pub fn controls_take(self) -> Controls {
self.controls
}
#[inline(always)]
pub fn sound_out_handle_get(&self) -> &OutputStreamHandle {
&self.sound_out
.1
}
#[inline(always)]
pub fn signal_receive(&self) -> Result<Signal, Error> {
self.controls_get()
.signal_receive()
.map_err(ChannelError::from)
.map_err(Error::Channel)
}
#[inline(always)]
pub fn playback_get(&self) -> &Sink {
&self.playback
}
#[inline(always)]
pub fn stream_play(
&self,
source: impl Read + Seek + Send + Sync + 'static,
) -> Result<(), Error> {
let decoder = Decoder::new(source)?;
self.playback
.append(decoder);
Ok(())
}
pub fn try_new() -> Result<Self, Error> {
let sound_out = rodio::OutputStream::try_default()?;
let (signal_sender, signal_receiver) = channel::unbounded();
let (exit_notifier, exit_receiver) = channel::unbounded();
let key_handler = move || loop {
if !exit_receiver.is_empty() { return }
let signal = match event::read().unwrap_or_else(|why| panic!("read an event from the current terminal {why}")) {
Event::Key(KeyEvent { code: KeyCode::Char('l' | 'L'), modifiers, .. }) if modifiers.contains(KeyModifiers::CONTROL) => Signal::PlaylistNext,
Event::Key(KeyEvent { code: KeyCode::Char('j' | 'J'), modifiers, .. }) if modifiers.contains(KeyModifiers::CONTROL) => Signal::PlaylistBack,
Event::Key(KeyEvent { code: KeyCode::Char('k' | 'K'), modifiers, .. }) if modifiers.contains(KeyModifiers::CONTROL) => Signal::Exit,
Event::Key(KeyEvent { code: KeyCode::Char('h' | 'H'), modifiers, .. }) if modifiers.contains(KeyModifiers::CONTROL) => Signal::PlaylistReset,
Event::Key(KeyEvent { code: KeyCode::Char('l'), ..}) => Signal::TrackNext,
Event::Key(KeyEvent { code: KeyCode::Char('j'), ..}) => Signal::TrackBack,
Event::Key(KeyEvent { code: KeyCode::Char('k'), ..}) => Signal::Play,
Event::Key(KeyEvent { code: KeyCode::Char('h'), ..}) => Signal::TrackReset,
Event::Key(KeyEvent { code: KeyCode::Char('L'), .. }) => Signal::VolumeIncrease,
Event::Key(KeyEvent { code: KeyCode::Char('J'), .. }) => Signal::VolumeDecrease,
Event::Key(KeyEvent { code: KeyCode::Char('K'), .. }) => Signal::Mute,
Event::Key(KeyEvent { code: KeyCode::Char('H'), .. }) => Signal::VolumeReset,
_ => continue,
};
if signal_sender
.send(signal)
.is_err()
{ panic!("send a signal to the playback {DISCONNECTED}") }
};
let control_thread = Builder::new()
.name(String::from("Controls"))
.stack_size(8)
.spawn(key_handler)?;
let controls = Controls {
control_thread,
exit_notifier,
signal_receiver,
};
let playback = Sink::try_new(&sound_out.1)?;
playback.pause();
Ok(Self {
sound_out,
controls,
playback,
})
}
}
#[cfg(any(debug_assertions, feature = "debug"))]
impl Debug for IOHandle {
fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
formatter
.debug_struct("IOHandle")
.field("controls", &self.controls)
.finish_non_exhaustive()
}
}
impl Controls {
#[inline(always)]
pub fn cleanly_exit(self) {
self.exit_notify();
self.clean_up()
}
#[inline(always)]
pub fn clean_up(self) {
let _ = self
.control_thread
.join();
}
#[inline(always)]
pub fn exit_notify(&self) {
let _ = self
.exit_notifier
.send(());
}
#[inline]
pub fn signal_receive(&self) -> Result<Signal, TryRecvError> {
self.signal_receiver
.try_recv()
}
}
macro_rules! pat {
($this: expr => $($name: ident)|+) => {
if let $(Self::$name)|+ = $this { true } else { false }
}
}
impl Signal {
#[inline(always)]
pub fn is_track_skip(&self) -> bool {
pat!(self => TrackNext | TrackBack)
}
#[inline(always)]
pub fn is_playlist_skip(&self) -> bool {
pat!(self => PlaylistNext | PlaylistBack)
}
#[inline(always)]
pub fn is_next_skip(&self) -> bool {
pat!(self => TrackNext | PlaylistNext)
}
#[inline(always)]
pub fn is_back_skip(&self) -> bool {
pat!(self => TrackBack | PlaylistBack)
}
#[inline(always)]
pub fn is_skip(&self) -> bool {
pat!(self => TrackNext | TrackBack | PlaylistNext | PlaylistBack)
}
#[inline(always)]
pub fn is_reset(&self) -> bool {
pat!(self => PlaylistReset | TrackReset | VolumeReset)
}
#[inline(always)]
pub fn is_playlist(&self) -> bool {
pat!(self => PlaylistNext | PlaylistBack | PlaylistReset)
}
#[inline(always)]
pub fn is_track(&self) -> bool {
pat!(self => TrackNext | TrackBack | TrackReset)
}
#[inline(always)]
pub fn is_volume(&self) -> bool {
pat!(self => VolumeIncrease | VolumeDecrease | Mute | VolumeReset)
}
}