Compare commits
8 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
6089e9082a | |
|
|
2d142e0657 | |
|
|
3f15bad5f3 | |
|
|
4f84ee9785 | |
|
|
8c2874d2ca | |
|
|
cc46fe2fd0 | |
|
|
d8eaea70ca | |
|
|
160c44e6ef |
|
|
@ -7,12 +7,8 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
argh = "0.1.12"
|
argh = "0.1.12"
|
||||||
bincode = "1.3.3"
|
|
||||||
crc32fast = "1.3.2"
|
|
||||||
crossterm = "0.27.0"
|
crossterm = "0.27.0"
|
||||||
interprocess = "1.2.1"
|
|
||||||
opus = "0.3.0"
|
opus = "0.3.0"
|
||||||
ratatui = "0.23.0"
|
ratatui = "0.23.0"
|
||||||
rodio = {version = "0.17.3", features = [ "symphonia-all" ], default-features = false }
|
rodio = { version = "0.17.3", features = [ "symphonia-all" ], default-features = false }
|
||||||
serde = "1.0.196"
|
|
||||||
symphonia = "0.5.4"
|
symphonia = "0.5.4"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
use std::{path::PathBuf, time::Duration};
|
||||||
|
|
||||||
|
use rmp::{DirectoryPlaylist, PlaylistElement, QueuePlaylist, StatefulList, TrackChangeOptions};
|
||||||
|
|
||||||
|
use crate::playback::Playback;
|
||||||
|
|
||||||
|
pub struct AppOptions {
|
||||||
|
pub title: String,
|
||||||
|
pub enhanced_graphics: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct App {
|
||||||
|
pub options: AppOptions,
|
||||||
|
pub should_quit: bool,
|
||||||
|
pub playback: Playback,
|
||||||
|
pub track_change_options: TrackChangeOptions,
|
||||||
|
pub focus: AppWindow,
|
||||||
|
pub directory_playlist: DirectoryPlaylist,
|
||||||
|
pub queue_playlist: QueuePlaylist,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
pub enum AppWindow {
|
||||||
|
#[default]
|
||||||
|
DirectoryPlaylist,
|
||||||
|
QueuePlaylist,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn new(options: AppOptions) -> Self {
|
||||||
|
Self {
|
||||||
|
options,
|
||||||
|
should_quit: false,
|
||||||
|
playback: Playback::new(),
|
||||||
|
track_change_options: Default::default(),
|
||||||
|
focus: Default::default(),
|
||||||
|
directory_playlist: DirectoryPlaylist::read_directory(
|
||||||
|
format!("{}/Music/Ultra Bra", std::env::var("HOME").unwrap()).into(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
queue_playlist: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focused_window(&self) -> &StatefulList<PlaylistElement> {
|
||||||
|
match &self.focus {
|
||||||
|
AppWindow::DirectoryPlaylist => &self.directory_playlist.playlist,
|
||||||
|
AppWindow::QueuePlaylist => &self.queue_playlist.playlist,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focused_window_mut(&mut self) -> &mut StatefulList<PlaylistElement> {
|
||||||
|
match &mut self.focus {
|
||||||
|
AppWindow::DirectoryPlaylist => &mut self.directory_playlist.playlist,
|
||||||
|
AppWindow::QueuePlaylist => &mut self.queue_playlist.playlist,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_shuffle(&mut self) {
|
||||||
|
self.track_change_options.shuffle = !self.track_change_options.shuffle;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_next(&mut self) {
|
||||||
|
self.track_change_options.next = !self.track_change_options.next;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_repeat(&mut self) {
|
||||||
|
self.track_change_options.repeat = !self.track_change_options.repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_pause(&mut self) {
|
||||||
|
self.playback.toggle_pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn play(&mut self, track: PathBuf) {
|
||||||
|
self.playback.play_immediate(track);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_selected_to_queue(&mut self) {
|
||||||
|
// TODO: Fix this mess, add some arguments
|
||||||
|
if let AppWindow::DirectoryPlaylist = self.focus {
|
||||||
|
if let Some(selected) = self.directory_playlist.playlist.current() {
|
||||||
|
self.queue_playlist.playlist.items.push(selected.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_selected_from_queue(&mut self) {
|
||||||
|
// TODO: Fix this mess, add some arguments
|
||||||
|
if let AppWindow::QueuePlaylist = self.focus {
|
||||||
|
if let Some(cursor) = self.queue_playlist.playlist.state.selected() {
|
||||||
|
self.queue_playlist.playlist.items.remove(cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_key(&mut self, key: char) {
|
||||||
|
match key {
|
||||||
|
'S' => self.toggle_shuffle(),
|
||||||
|
'X' => self.toggle_next(),
|
||||||
|
'R' => self.toggle_repeat(),
|
||||||
|
' ' => self.toggle_pause(),
|
||||||
|
'a' => self.add_selected_to_queue(),
|
||||||
|
'd' => self.remove_selected_from_queue(),
|
||||||
|
'q' => self.should_quit = true,
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_left(&mut self) {}
|
||||||
|
|
||||||
|
pub fn on_right(&mut self) {}
|
||||||
|
|
||||||
|
pub fn on_up(&mut self) {
|
||||||
|
self.focused_window_mut().previous();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_down(&mut self) {
|
||||||
|
self.focused_window_mut().next();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_enter(&mut self) {
|
||||||
|
// TODO: Handle directory navigation
|
||||||
|
if let Some(current) = self.focused_window().current() {
|
||||||
|
self.play(current.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_tab(&mut self) {
|
||||||
|
self.focus = match self.focus {
|
||||||
|
AppWindow::QueuePlaylist => AppWindow::DirectoryPlaylist,
|
||||||
|
AppWindow::DirectoryPlaylist => AppWindow::QueuePlaylist,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_tick(&mut self, _duration: Duration) {}
|
||||||
|
}
|
||||||
|
|
@ -1,45 +1,16 @@
|
||||||
use std::{
|
use std::{error::Error, time::Duration};
|
||||||
error::Error,
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::CliArgs;
|
use crate::{
|
||||||
|
|
||||||
use self::{
|
|
||||||
app::{App, AppOptions},
|
app::{App, AppOptions},
|
||||||
request_queue::request_queue_cleaner,
|
crossterm, CliArgs,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod app;
|
|
||||||
pub mod crossterm;
|
|
||||||
pub mod request_queue;
|
|
||||||
pub mod ui;
|
|
||||||
|
|
||||||
pub fn run(args: CliArgs) -> Result<(), Box<dyn Error>> {
|
pub fn run(args: CliArgs) -> Result<(), Box<dyn Error>> {
|
||||||
let message_queue = Arc::new(Mutex::new(vec![]));
|
|
||||||
let server_state = Arc::new(Mutex::new(None));
|
|
||||||
let options = AppOptions {
|
let options = AppOptions {
|
||||||
title: "rmp - Rust Music Player".into(),
|
title: "rmp - Rust Music Player".into(),
|
||||||
enhanced_graphics: args.enhanced_graphics,
|
enhanced_graphics: args.enhanced_graphics,
|
||||||
sync_state: args.sync_state,
|
|
||||||
};
|
};
|
||||||
let app = App {
|
let app = App::new(options);
|
||||||
options,
|
|
||||||
should_quit: false,
|
|
||||||
state: server_state.clone(),
|
|
||||||
message_queue: message_queue.clone(),
|
|
||||||
};
|
|
||||||
let thread_builder = std::thread::Builder::new().name("request_queue".into());
|
|
||||||
thread_builder
|
|
||||||
.spawn(move || {
|
|
||||||
request_queue_cleaner(
|
|
||||||
Duration::from_millis(args.message_rate),
|
|
||||||
message_queue.clone(),
|
|
||||||
server_state.clone(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
crossterm::run(app, Duration::from_millis(args.tick_rate))?;
|
crossterm::run(app, Duration::from_millis(args.tick_rate))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
use std::{
|
|
||||||
path::PathBuf,
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use rmp::{
|
|
||||||
protocol::{Message, MessageType},
|
|
||||||
ServerState,
|
|
||||||
};
|
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
pub struct App {
|
|
||||||
pub options: AppOptions,
|
|
||||||
pub should_quit: bool,
|
|
||||||
pub message_queue: Arc<Mutex<Vec<Message>>>,
|
|
||||||
pub state: Arc<Mutex<Option<ServerState>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct AppOptions {
|
|
||||||
pub title: String,
|
|
||||||
pub sync_state: bool,
|
|
||||||
pub enhanced_graphics: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl App {
|
|
||||||
pub fn new(options: AppOptions) -> Self {
|
|
||||||
Self {
|
|
||||||
options,
|
|
||||||
should_quit: false,
|
|
||||||
message_queue: Arc::new(Mutex::new(vec![])),
|
|
||||||
state: Arc::new(Mutex::new(None)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_message(&mut self, message: Message) {
|
|
||||||
self.message_queue.lock().unwrap().push(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn map_and_serialize<F, T>(&mut self, f: F) -> Option<Vec<u8>>
|
|
||||||
where
|
|
||||||
F: FnOnce(&ServerState) -> Option<T>,
|
|
||||||
T: Serialize,
|
|
||||||
{
|
|
||||||
let data = self.state.lock().unwrap().as_ref().and_then(f);
|
|
||||||
data.map(|data| bincode::serialize(&data).unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn connected(&self) -> bool {
|
|
||||||
self.state.lock().unwrap().is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toggle_shuffle(&mut self) {
|
|
||||||
let body = self.map_and_serialize(|state| Some(!state.playlist_params.shuffle));
|
|
||||||
self.push_message(Message {
|
|
||||||
message_type: MessageType::SetShuffle,
|
|
||||||
body,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toggle_next(&mut self) {
|
|
||||||
let body = self.map_and_serialize(|state| Some(!state.playlist_params.next));
|
|
||||||
self.push_message(Message {
|
|
||||||
message_type: MessageType::SetNext,
|
|
||||||
body,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toggle_repeat(&mut self) {
|
|
||||||
let body = self.map_and_serialize(|state| Some(!state.playlist_params.repeat));
|
|
||||||
self.push_message(Message {
|
|
||||||
message_type: MessageType::SetRepeat,
|
|
||||||
body,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toggle_pause(&mut self) {
|
|
||||||
let body =
|
|
||||||
self.map_and_serialize(|state| state.player.as_ref().map(|player| !player.is_paused));
|
|
||||||
self.push_message(Message {
|
|
||||||
message_type: MessageType::SetPause,
|
|
||||||
body,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fetch_state(&mut self) {
|
|
||||||
self.push_message(Message {
|
|
||||||
message_type: MessageType::StateFetch,
|
|
||||||
body: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn play(&mut self, track: PathBuf) {
|
|
||||||
let body = self.map_and_serialize(|_state| Some(track));
|
|
||||||
self.push_message(Message {
|
|
||||||
message_type: MessageType::PlayTrackFromPath,
|
|
||||||
body,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_key(&mut self, key: char) {
|
|
||||||
match key {
|
|
||||||
'S' => self.toggle_shuffle(),
|
|
||||||
'X' => self.toggle_next(),
|
|
||||||
'R' => self.toggle_repeat(),
|
|
||||||
' ' => self.toggle_pause(),
|
|
||||||
'q' => self.should_quit = true,
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_left(&mut self) {}
|
|
||||||
|
|
||||||
pub fn on_right(&mut self) {}
|
|
||||||
|
|
||||||
pub fn on_up(&mut self) {}
|
|
||||||
|
|
||||||
pub fn on_down(&mut self) {}
|
|
||||||
|
|
||||||
pub fn on_enter(&mut self) {
|
|
||||||
self.play("".into()); // TODO: Remove hardcoding
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_tab(&mut self) {}
|
|
||||||
|
|
||||||
pub fn on_tick(&mut self, _duration: Duration) {
|
|
||||||
if self.options.sync_state {
|
|
||||||
self.push_message(Message {
|
|
||||||
message_type: MessageType::StateFetch,
|
|
||||||
body: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
use std::{
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
time::{Duration, Instant},
|
|
||||||
};
|
|
||||||
|
|
||||||
use interprocess::local_socket::LocalSocketStream;
|
|
||||||
use rmp::{
|
|
||||||
protocol::{self, Message, MessageError, MessageType},
|
|
||||||
ServerState,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn request_queue_cleaner(
|
|
||||||
message_rate: Duration,
|
|
||||||
queue: Arc<Mutex<Vec<Message>>>,
|
|
||||||
state: Arc<Mutex<Option<ServerState>>>,
|
|
||||||
) {
|
|
||||||
let mut last_tick = Instant::now();
|
|
||||||
let mut should_connect = true;
|
|
||||||
let mut stream: Option<LocalSocketStream> = None;
|
|
||||||
loop {
|
|
||||||
if should_connect {
|
|
||||||
*state.lock().unwrap() = None;
|
|
||||||
stream = Some(connect());
|
|
||||||
queue.lock().unwrap().push(Message {
|
|
||||||
message_type: MessageType::StateFetch,
|
|
||||||
body: None,
|
|
||||||
});
|
|
||||||
should_connect = false;
|
|
||||||
}
|
|
||||||
match stream.as_mut() {
|
|
||||||
Some(mut stream) => {
|
|
||||||
for request in queue.lock().unwrap().drain(..) {
|
|
||||||
if let Err(_) = protocol::send(&mut stream, &request) {
|
|
||||||
should_connect = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if let Ok(response) = protocol::receive(&mut stream) {
|
|
||||||
match route_response(&response, &mut state.lock().unwrap()) {
|
|
||||||
Err(error) => {
|
|
||||||
eprintln!("{error:?}")
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let sleep_duration = message_rate
|
|
||||||
.checked_sub(last_tick.elapsed())
|
|
||||||
.unwrap_or_else(|| Duration::from_secs(0));
|
|
||||||
std::thread::sleep(sleep_duration);
|
|
||||||
last_tick = Instant::now();
|
|
||||||
}
|
|
||||||
None => should_connect = true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Blocks thread until connected to socket
|
|
||||||
fn connect() -> LocalSocketStream {
|
|
||||||
let path = rmp::os::server::get_socket_path().unwrap();
|
|
||||||
loop {
|
|
||||||
match LocalSocketStream::connect(path.clone()) {
|
|
||||||
Ok(stream) => return stream,
|
|
||||||
Err(_) => {}
|
|
||||||
}
|
|
||||||
std::thread::sleep(Duration::from_millis(100));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn route_response(response: &Message, state: &mut Option<ServerState>) -> Result<(), String> {
|
|
||||||
match response.message_type {
|
|
||||||
MessageType::StateResponse => {
|
|
||||||
let body = response.body.as_ref().ok_or("Missing response body")?;
|
|
||||||
let response: ServerState =
|
|
||||||
bincode::deserialize(&body).map_err(|err| err.to_string())?;
|
|
||||||
*state = Some(response);
|
|
||||||
}
|
|
||||||
MessageType::NotImplementedAck => {
|
|
||||||
eprintln!("Server doesn't implement message")
|
|
||||||
}
|
|
||||||
MessageType::ProtocolError => {
|
|
||||||
let body = response.body.as_ref().ok_or("Missing response body")?;
|
|
||||||
let response: MessageError =
|
|
||||||
bincode::deserialize(&body).map_err(|err| err.to_string())?;
|
|
||||||
eprintln!("Server claims protocol error: {response:?}");
|
|
||||||
}
|
|
||||||
message_type => {
|
|
||||||
eprintln!("Message handling not implemented for client: {message_type:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
122
src/client/ui.rs
122
src/client/ui.rs
|
|
@ -1,122 +0,0 @@
|
||||||
use ratatui::{
|
|
||||||
prelude::*,
|
|
||||||
widgets::{block::Title, *},
|
|
||||||
};
|
|
||||||
use rmp::ServerState;
|
|
||||||
|
|
||||||
use super::app::App;
|
|
||||||
|
|
||||||
pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
|
||||||
if let Some(state) = app.state.lock().unwrap().as_ref() {
|
|
||||||
let chunks = Layout::default()
|
|
||||||
.constraints([Constraint::Min(3), Constraint::Length(2)].as_ref())
|
|
||||||
.split(f.size());
|
|
||||||
draw_playlist(f, state, chunks[0]);
|
|
||||||
draw_player(f, state, chunks[1]);
|
|
||||||
} else {
|
|
||||||
draw_no_connection(f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static PRIMARY_COLOR: Color = Color::Rgb(200, 150, 70);
|
|
||||||
static SECONDARY_COLOR: Color = Color::Rgb(200, 200, 200);
|
|
||||||
|
|
||||||
static PRIMARY_CONTRAST: Color = Color::Black;
|
|
||||||
static CLEAR_CONTRAST: Color = Color::Rgb(100, 100, 100);
|
|
||||||
|
|
||||||
fn draw_no_connection<B: Backend>(f: &mut Frame<B>) {
|
|
||||||
let message = "Not connected";
|
|
||||||
let width = message.len() as u16 + 4;
|
|
||||||
let height = 3;
|
|
||||||
|
|
||||||
let x = (f.size().width as i16 - width as i16).max(0) as u16 / 2;
|
|
||||||
let y = (f.size().height as i16 - height as i16).max(0) as u16 / 2;
|
|
||||||
|
|
||||||
let area = Rect {
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
width: u16::min(width, f.size().width - x),
|
|
||||||
height: u16::min(height, f.size().height - y),
|
|
||||||
};
|
|
||||||
f.render_widget(
|
|
||||||
Paragraph::new(message)
|
|
||||||
.fg(SECONDARY_COLOR)
|
|
||||||
.block(Block::default().borders(Borders::ALL).fg(PRIMARY_COLOR))
|
|
||||||
.alignment(Alignment::Center),
|
|
||||||
area,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_playlist<B: Backend>(f: &mut Frame<B>, _state: &ServerState, area: Rect) {
|
|
||||||
let playlist = List::new(vec![])
|
|
||||||
.block(
|
|
||||||
Block::default()
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(Style::default().fg(SECONDARY_COLOR)),
|
|
||||||
)
|
|
||||||
.start_corner(Corner::TopLeft);
|
|
||||||
f.render_widget(playlist, area);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_player<B: Backend>(f: &mut Frame<B>, state: &ServerState, area: Rect) {
|
|
||||||
fn decorate_bool(span: Span, value: bool) -> Span {
|
|
||||||
match value {
|
|
||||||
true => return span.bg(PRIMARY_COLOR).fg(PRIMARY_CONTRAST),
|
|
||||||
false => return span.fg(CLEAR_CONTRAST),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let track_title = vec![
|
|
||||||
Span::from("[ "),
|
|
||||||
Span::from("??").fg(PRIMARY_COLOR),
|
|
||||||
Span::from(" ]"),
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut block = Block::default()
|
|
||||||
.borders(Borders::TOP)
|
|
||||||
.border_style(Style::default().fg(SECONDARY_COLOR))
|
|
||||||
.title(track_title);
|
|
||||||
|
|
||||||
// Horrible to look at, worse to write
|
|
||||||
let param_titles: Vec<Title> = vec![
|
|
||||||
(
|
|
||||||
state.playlist_params.next,
|
|
||||||
vec![
|
|
||||||
Span::from("NE"),
|
|
||||||
Span::from("X").underlined(),
|
|
||||||
Span::from("T"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
state.playlist_params.shuffle,
|
|
||||||
vec![Span::from("S").underlined(), Span::from("HUFFLE")],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
state.playlist_params.repeat,
|
|
||||||
vec![Span::from("R").underlined(), Span::from("EPEAT")],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
.iter()
|
|
||||||
.map(|(value, spans)| {
|
|
||||||
spans
|
|
||||||
.iter()
|
|
||||||
.map(|span| decorate_bool(span.clone(), *value))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
})
|
|
||||||
.map(|spans| Title::from([&[Span::from(" ")], spans.as_slice(), &[Span::from(" ")]].concat()))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for title in param_titles {
|
|
||||||
block = block.title(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
let block = block.title_position(block::Position::Top);
|
|
||||||
|
|
||||||
let player = Gauge::default()
|
|
||||||
.block(block)
|
|
||||||
.gauge_style(Style::default().fg(PRIMARY_COLOR))
|
|
||||||
.ratio(0.25)
|
|
||||||
.label("[ PAUSED ]");
|
|
||||||
|
|
||||||
f.render_widget(player, area)
|
|
||||||
}
|
|
||||||
|
|
@ -33,7 +33,7 @@ pub fn run(app: App, tick_rate: Duration) -> Result<(), Box<dyn Error>> {
|
||||||
terminal.show_cursor()?;
|
terminal.show_cursor()?;
|
||||||
|
|
||||||
if let Err(err) = res {
|
if let Err(err) = res {
|
||||||
println!("{err:?}");
|
eprintln!("{err:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -54,27 +54,15 @@ fn run_app<B: Backend>(
|
||||||
if crossterm::event::poll(timeout)? {
|
if crossterm::event::poll(timeout)? {
|
||||||
if let Event::Key(key) = event::read()? {
|
if let Event::Key(key) = event::read()? {
|
||||||
if key.kind == KeyEventKind::Press {
|
if key.kind == KeyEventKind::Press {
|
||||||
if app.connected() {
|
match key.code {
|
||||||
match key.code {
|
KeyCode::Char(c) => app.on_key(c),
|
||||||
KeyCode::Char(c) => app.on_key(c),
|
KeyCode::Left => app.on_left(),
|
||||||
KeyCode::Left => app.on_left(),
|
KeyCode::Up => app.on_up(),
|
||||||
KeyCode::Up => app.on_up(),
|
KeyCode::Right => app.on_right(),
|
||||||
KeyCode::Right => app.on_right(),
|
KeyCode::Down => app.on_down(),
|
||||||
KeyCode::Down => app.on_down(),
|
KeyCode::Enter => app.on_enter(),
|
||||||
KeyCode::Enter => app.on_enter(),
|
KeyCode::Tab => app.on_tab(),
|
||||||
KeyCode::Tab => app.on_tab(),
|
_ => {}
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Allow quitting while in "Not connected" screen
|
|
||||||
match key.code {
|
|
||||||
KeyCode::Char(c) => {
|
|
||||||
if c == 'q' {
|
|
||||||
app.should_quit = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -16,9 +16,9 @@ use symphonia::core::{
|
||||||
probe::Hint,
|
probe::Hint,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod opus;
|
use self::opus::OpusDecoder;
|
||||||
|
|
||||||
use crate::server::decoder::opus::OpusDecoder;
|
mod opus;
|
||||||
|
|
||||||
pub struct Decoder;
|
pub struct Decoder;
|
||||||
|
|
||||||
150
src/lib.rs
150
src/lib.rs
|
|
@ -1,24 +1,102 @@
|
||||||
use std::path::PathBuf;
|
use std::{error::Error, ops::Deref, path::PathBuf};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||||
|
pub struct ListState {
|
||||||
#[cfg(target_family = "unix")]
|
offset: usize,
|
||||||
#[path = "os_unix.rs"]
|
selected: Option<usize>,
|
||||||
pub mod os;
|
|
||||||
pub mod protocol;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
|
||||||
pub struct ServerState {
|
|
||||||
pub playlist_params: PlaylistParams,
|
|
||||||
pub player: Option<PlayerState>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
impl ListState {
|
||||||
pub struct PlayerState {
|
pub fn offset(&self) -> usize {
|
||||||
pub track: PlaylistElement,
|
self.offset
|
||||||
pub is_paused: bool,
|
}
|
||||||
pub duration: Option<f32>,
|
|
||||||
pub current_position: Option<f32>,
|
pub fn offset_mut(&mut self) -> &mut usize {
|
||||||
|
&mut self.offset
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_selected(mut self, selected: Option<usize>) -> Self {
|
||||||
|
self.selected = selected;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_offset(mut self, offset: usize) -> Self {
|
||||||
|
self.offset = offset;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selected(&self) -> Option<usize> {
|
||||||
|
self.selected
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select(&mut self, index: Option<usize>) {
|
||||||
|
self.selected = index;
|
||||||
|
if index.is_none() {
|
||||||
|
self.offset = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct StatefulList<T> {
|
||||||
|
pub state: ListState,
|
||||||
|
pub items: Vec<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> StatefulList<T>
|
||||||
|
where
|
||||||
|
T: Deref + Eq,
|
||||||
|
{
|
||||||
|
pub fn with_items(items: Vec<T>) -> StatefulList<T> {
|
||||||
|
StatefulList {
|
||||||
|
state: ListState::default(),
|
||||||
|
items,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(&mut self) {
|
||||||
|
let i = match self.state.selected() {
|
||||||
|
Some(i) => {
|
||||||
|
if i >= self.items.len() - 1 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => 0,
|
||||||
|
};
|
||||||
|
self.state.select(Some(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn previous(&mut self) {
|
||||||
|
let i = match self.state.selected() {
|
||||||
|
Some(i) => {
|
||||||
|
if i == 0 {
|
||||||
|
self.items.len() - 1
|
||||||
|
} else {
|
||||||
|
i - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => 0,
|
||||||
|
};
|
||||||
|
self.state.select(Some(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn index_of(&self, item: T) -> Option<usize> {
|
||||||
|
self.items.iter().position(|element| *element == item)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current(&self) -> Option<&T> {
|
||||||
|
self.state
|
||||||
|
.selected
|
||||||
|
.map_or(None, |index| self.items.get(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_mut(&mut self) -> Option<&mut T> {
|
||||||
|
self.state
|
||||||
|
.selected
|
||||||
|
.map_or(None, |index| self.items.get_mut(index))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type PlaylistElement = PathBuf;
|
pub type PlaylistElement = PathBuf;
|
||||||
|
|
@ -28,33 +106,39 @@ pub enum PlaylistType {
|
||||||
Queue,
|
Queue,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
#[derive(Debug)]
|
||||||
pub struct Playlist {
|
|
||||||
pub items: Vec<PlaylistElement>,
|
|
||||||
pub current: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Playlist {}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
pub struct DirectoryPlaylist {
|
pub struct DirectoryPlaylist {
|
||||||
pub directory: PathBuf,
|
pub directory: PathBuf,
|
||||||
pub playlist: Playlist,
|
pub playlist: StatefulList<PlaylistElement>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
impl DirectoryPlaylist {
|
||||||
|
pub fn read_directory(path: PathBuf) -> Result<Self, Box<dyn Error>> {
|
||||||
|
let mut playlist: Vec<PlaylistElement> = vec![];
|
||||||
|
for entry in std::fs::read_dir(&path)? {
|
||||||
|
let entry = entry?;
|
||||||
|
playlist.push(entry.path());
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
directory: path,
|
||||||
|
playlist: StatefulList::with_items(playlist),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
pub struct QueuePlaylist {
|
pub struct QueuePlaylist {
|
||||||
pub playlist: Playlist,
|
pub playlist: StatefulList<PlaylistElement>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Debug)]
|
||||||
pub struct PlaylistParams {
|
pub struct TrackChangeOptions {
|
||||||
pub shuffle: bool,
|
pub shuffle: bool,
|
||||||
pub next: bool,
|
pub next: bool,
|
||||||
pub repeat: bool,
|
pub repeat: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for PlaylistParams {
|
impl Default for TrackChangeOptions {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
shuffle: false,
|
shuffle: false,
|
||||||
|
|
|
||||||
57
src/main.rs
57
src/main.rs
|
|
@ -1,47 +1,19 @@
|
||||||
use argh::FromArgs;
|
use argh::FromArgs;
|
||||||
|
|
||||||
|
pub mod app;
|
||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod server;
|
pub mod crossterm;
|
||||||
|
pub mod decoder;
|
||||||
|
pub mod playback;
|
||||||
|
pub mod ui;
|
||||||
|
|
||||||
/// rmp: Rust Music Player
|
/// rmp: Rust Music Player
|
||||||
#[derive(Debug, FromArgs)]
|
#[derive(Debug, FromArgs, Clone)]
|
||||||
pub struct CliArgs {
|
pub struct CliArgs {
|
||||||
/// run the server
|
|
||||||
#[argh(switch, short = 's')]
|
|
||||||
server: bool,
|
|
||||||
|
|
||||||
/// randomize next track?
|
|
||||||
#[argh(option, default = "false")]
|
|
||||||
shuffle: bool,
|
|
||||||
|
|
||||||
/// change track after the current one is finished?
|
|
||||||
#[argh(option, default = "true")]
|
|
||||||
next: bool,
|
|
||||||
|
|
||||||
/// repeat the playlist (or track if 'next' is disabled) after it's finished
|
|
||||||
#[argh(option, default = "true")]
|
|
||||||
repeat: bool,
|
|
||||||
|
|
||||||
/// kill server
|
|
||||||
#[argh(switch, short = 'x')]
|
|
||||||
exit: bool,
|
|
||||||
|
|
||||||
/// don't start server even if it's not running
|
|
||||||
#[argh(switch, short = 'c')]
|
|
||||||
client_only: bool,
|
|
||||||
|
|
||||||
/// time in ms between two ticks.
|
/// time in ms between two ticks.
|
||||||
#[argh(option, default = "100")]
|
#[argh(option, default = "100")]
|
||||||
tick_rate: u64,
|
tick_rate: u64,
|
||||||
|
|
||||||
/// interval in ms for clearing the request queue.
|
|
||||||
#[argh(option, default = "100")]
|
|
||||||
message_rate: u64,
|
|
||||||
|
|
||||||
/// should client automatically sync the server state? (used mainly for debugging)
|
|
||||||
#[argh(option, default = "true")]
|
|
||||||
sync_state: bool,
|
|
||||||
|
|
||||||
/// whether unicode symbols are used to improve the overall look of the app
|
/// whether unicode symbols are used to improve the overall look of the app
|
||||||
#[argh(option, default = "true")]
|
#[argh(option, default = "true")]
|
||||||
enhanced_graphics: bool,
|
enhanced_graphics: bool,
|
||||||
|
|
@ -50,22 +22,5 @@ pub struct CliArgs {
|
||||||
fn main() {
|
fn main() {
|
||||||
let args: CliArgs = argh::from_env();
|
let args: CliArgs = argh::from_env();
|
||||||
|
|
||||||
if args.exit {
|
|
||||||
server::kill();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.server {
|
|
||||||
if server::is_running().unwrap() {
|
|
||||||
println!("Server is already running");
|
|
||||||
} else {
|
|
||||||
server::run(args).unwrap();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !args.client_only && !server::is_running().unwrap() {
|
|
||||||
server::run_in_background();
|
|
||||||
}
|
|
||||||
client::run(args).unwrap();
|
client::run(args).unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
102
src/os_unix.rs
102
src/os_unix.rs
|
|
@ -1,102 +0,0 @@
|
||||||
pub mod client {
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
pub fn get_music_dir() -> PathBuf {
|
|
||||||
PathBuf::from(std::env!("HOME")).join("Music")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod server {
|
|
||||||
use std::{
|
|
||||||
fs,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
process::{id, Command, Stdio},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::server::ServerError;
|
|
||||||
|
|
||||||
pub fn reserve_pid() -> Result<(), ServerError> {
|
|
||||||
let pid_path = get_pid_path()?;
|
|
||||||
is_running()?;
|
|
||||||
|
|
||||||
fs::write(&pid_path, id().to_string()).map_err(|err| ServerError::Io(err))?;
|
|
||||||
Command::new("chmod")
|
|
||||||
.args(&["600", &pid_path.to_string_lossy()])
|
|
||||||
.output()
|
|
||||||
.map_err(|err| ServerError::Io(err))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_running() -> Result<bool, ServerError> {
|
|
||||||
let pid_path = get_pid_path()?;
|
|
||||||
|
|
||||||
match fs::read(&pid_path) {
|
|
||||||
Ok(old_pid) => {
|
|
||||||
let old_pid =
|
|
||||||
String::from_utf8(old_pid).map_err(|err| ServerError::from_debuggable(err))?;
|
|
||||||
let old_pid = old_pid.trim();
|
|
||||||
Ok(Command::new("ps")
|
|
||||||
.args(&["-p", old_pid])
|
|
||||||
.stdout(Stdio::null())
|
|
||||||
.stderr(Stdio::null())
|
|
||||||
.status()
|
|
||||||
.map_err(|err| ServerError::Io(err))?
|
|
||||||
.success())
|
|
||||||
}
|
|
||||||
_ => Ok(false),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run_in_background() -> Result<(), ServerError> {
|
|
||||||
let this = std::env::args().next().unwrap();
|
|
||||||
Command::new(this)
|
|
||||||
.stdout(Stdio::null())
|
|
||||||
.stderr(Stdio::null())
|
|
||||||
.args(&["-s"])
|
|
||||||
.spawn()
|
|
||||||
.map_err(|err| ServerError::Io(err))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn kill() -> Result<(), ServerError> {
|
|
||||||
let pid_path = get_pid_path()?;
|
|
||||||
let socket_path = get_socket_path()?;
|
|
||||||
let pid = fs::read(&pid_path).map_err(|_| ServerError::NotRunning)?;
|
|
||||||
let pid = String::from_utf8(pid).map_err(|err| ServerError::from_debuggable(err))?;
|
|
||||||
let pid = pid.trim();
|
|
||||||
Command::new("kill")
|
|
||||||
.arg(pid)
|
|
||||||
.spawn()
|
|
||||||
.map_err(|err| ServerError::Io(err))?;
|
|
||||||
Command::new("rm")
|
|
||||||
.args(&[
|
|
||||||
"-f",
|
|
||||||
&pid_path.to_string_lossy(),
|
|
||||||
&socket_path.to_string_lossy(),
|
|
||||||
])
|
|
||||||
.spawn()
|
|
||||||
.map_err(|err| ServerError::Io(err))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_socket_path() -> Result<PathBuf, ServerError> {
|
|
||||||
Ok(get_runtime_dir()?.join("rmp.socket"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_runtime_dir() -> Result<PathBuf, ServerError> {
|
|
||||||
let uid = String::from_utf8(
|
|
||||||
Command::new("id")
|
|
||||||
.arg("-u")
|
|
||||||
.output()
|
|
||||||
.map_err(|err| ServerError::Io(err))?
|
|
||||||
.stdout,
|
|
||||||
)
|
|
||||||
.map_err(|err| ServerError::from_debuggable(err))?;
|
|
||||||
let dir = Path::new("/run/user").join(uid.trim().to_string());
|
|
||||||
Ok(dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_pid_path() -> Result<PathBuf, ServerError> {
|
|
||||||
Ok(get_runtime_dir()?.join("rmp.pid"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
use std::{fs::File, path::PathBuf};
|
||||||
|
|
||||||
|
use rmp::PlaylistElement;
|
||||||
|
use rodio::{OutputStream, OutputStreamHandle, Sink};
|
||||||
|
|
||||||
|
use super::decoder::{Decoder, DecoderImpl, SourceWrapper};
|
||||||
|
|
||||||
|
pub struct Playback {
|
||||||
|
/// These must not be dropped before the sink
|
||||||
|
_stream: OutputStream,
|
||||||
|
_stream_handle: OutputStreamHandle,
|
||||||
|
sink: Sink,
|
||||||
|
pub current: Option<Player>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct Player {
|
||||||
|
pub track: PlaylistElement,
|
||||||
|
pub is_paused: bool,
|
||||||
|
pub duration: Option<f32>,
|
||||||
|
pub position: Option<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Playback {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let (_stream, _stream_handle) = OutputStream::try_default().unwrap();
|
||||||
|
let sink: Sink = Sink::try_new(&_stream_handle).unwrap();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
_stream,
|
||||||
|
_stream_handle,
|
||||||
|
sink,
|
||||||
|
current: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear the queue and start playback immediately.
|
||||||
|
pub fn play_immediate(&mut self, path: PathBuf) {
|
||||||
|
let mut source: Option<Box<dyn DecoderImpl<Item = i16>>> = None;
|
||||||
|
|
||||||
|
{
|
||||||
|
let file = File::open(&path).unwrap();
|
||||||
|
if let Ok(decoder) = Decoder::rodio(SourceWrapper::from_file(file)) {
|
||||||
|
source = Some(decoder);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if source.is_none() {
|
||||||
|
let file = File::open(&path).unwrap();
|
||||||
|
if let Ok(decoder) = Decoder::custom(SourceWrapper::from_file(file)) {
|
||||||
|
source = Some(decoder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match source {
|
||||||
|
Some(source) => {
|
||||||
|
self.current = Some(Player {
|
||||||
|
track: path,
|
||||||
|
is_paused: false,
|
||||||
|
duration: source.total_duration().map(|dur| dur.as_secs_f32()),
|
||||||
|
position: None,
|
||||||
|
});
|
||||||
|
self.sink.clear();
|
||||||
|
self.sink.append(source);
|
||||||
|
self.sink.play();
|
||||||
|
}
|
||||||
|
None => eprintln!("No handler found for '{path:?}'"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggle playback pause if possible.
|
||||||
|
pub fn toggle_pause(&mut self) {
|
||||||
|
if let Some(current) = &self.current {
|
||||||
|
match current.is_paused {
|
||||||
|
true => self.sink.play(),
|
||||||
|
false => self.sink.pause(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
137
src/protocol.rs
137
src/protocol.rs
|
|
@ -1,137 +0,0 @@
|
||||||
use std::{
|
|
||||||
fmt::Display,
|
|
||||||
io::{Read, Write},
|
|
||||||
};
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::ServerState;
|
|
||||||
|
|
||||||
/// Prefix messages with this header
|
|
||||||
pub const HEADER_MAGIC: [u8; 4] = [0xCA, 0xFE, 0xBA, 0xBE];
|
|
||||||
/// Maximum allowed body size
|
|
||||||
pub const MAX_BODY_LENGTH: usize = 10 * 1024 * 1024;
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub enum MessageError {
|
|
||||||
HeaderMismatch,
|
|
||||||
BodySizeLimit,
|
|
||||||
ChecksumMismatch,
|
|
||||||
ReadError,
|
|
||||||
DeserializationError,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Deserialize, Serialize)]
|
|
||||||
pub enum MessageType {
|
|
||||||
/// Generic acknowledge
|
|
||||||
Ack = 0,
|
|
||||||
/// Recipient did not know how to handle the request.
|
|
||||||
NotImplementedAck,
|
|
||||||
/// Request was not a valid message. For example if the checksum did not match.
|
|
||||||
ProtocolError,
|
|
||||||
/// Request was a valid message, but it was incorrect. For example the body could be missing if
|
|
||||||
/// it was expected
|
|
||||||
ApplicationError,
|
|
||||||
StateFetch,
|
|
||||||
StateResponse,
|
|
||||||
SetShuffle,
|
|
||||||
SetNext,
|
|
||||||
SetRepeat,
|
|
||||||
SetPause,
|
|
||||||
PlayTrackFromPath,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Message {
|
|
||||||
pub message_type: MessageType,
|
|
||||||
pub body: Option<Vec<u8>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Message {
|
|
||||||
// pub fn new(message_type: MessageType, body: Option<Vec<u8>>) -> Self {
|
|
||||||
// Self { message_type, body }
|
|
||||||
// }
|
|
||||||
|
|
||||||
/// Message format (values are in little-endian):
|
|
||||||
/// offset | size | explanation
|
|
||||||
/// -------+------+-----------
|
|
||||||
/// 0x00 | u32 | HEADER_MAGIC
|
|
||||||
/// 0x04 | u32 | Body checksum
|
|
||||||
/// 0x08 | u32 | Body length
|
|
||||||
/// 0x12 | ? | Body
|
|
||||||
fn as_bytes(&self) -> Vec<u8> {
|
|
||||||
let magic = &HEADER_MAGIC[..];
|
|
||||||
let body = &bincode::serialize(self).unwrap();
|
|
||||||
let checksum = &crc32fast::hash(&body).to_le_bytes();
|
|
||||||
let body_length = &(body.len() as u32).to_le_bytes();
|
|
||||||
[magic, checksum, body_length, body].concat()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn state_response(server_state: &ServerState) -> Result<Self, String> {
|
|
||||||
Ok(Self {
|
|
||||||
message_type: MessageType::StateResponse,
|
|
||||||
body: Some(bincode::serialize(server_state).map_err(|err| err.to_string())?),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Message {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"{:?}\t{}",
|
|
||||||
self.message_type,
|
|
||||||
self.body
|
|
||||||
.as_ref()
|
|
||||||
.map_or("(no body)".into(), |body| format!("{} B", body.len()))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send<T>(stream: &mut T, message: &Message) -> Result<(), std::io::Error>
|
|
||||||
where
|
|
||||||
T: Write,
|
|
||||||
{
|
|
||||||
stream.write_all(&message.as_bytes())?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn receive<T>(stream: &mut T) -> Result<Message, MessageError>
|
|
||||||
where
|
|
||||||
T: Read,
|
|
||||||
{
|
|
||||||
let mut magic_buffer = vec![0; HEADER_MAGIC.len()];
|
|
||||||
if let Err(_) = stream.read_exact(&mut magic_buffer) {
|
|
||||||
return Err(MessageError::ReadError);
|
|
||||||
}
|
|
||||||
if magic_buffer != HEADER_MAGIC {
|
|
||||||
return Err(MessageError::HeaderMismatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut checksum_buffer = [0; 4];
|
|
||||||
if let Err(_) = stream.read_exact(&mut checksum_buffer) {
|
|
||||||
return Err(MessageError::ReadError);
|
|
||||||
}
|
|
||||||
let expected_checksum = u32::from_le_bytes(checksum_buffer);
|
|
||||||
|
|
||||||
let mut body_length_buffer = [0; 4];
|
|
||||||
if let Err(_) = stream.read_exact(&mut body_length_buffer) {
|
|
||||||
return Err(MessageError::ReadError);
|
|
||||||
}
|
|
||||||
let expected_body_length = u32::from_le_bytes(body_length_buffer) as usize;
|
|
||||||
|
|
||||||
if expected_body_length > MAX_BODY_LENGTH {
|
|
||||||
return Err(MessageError::BodySizeLimit);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut body_buffer = vec![0; expected_body_length];
|
|
||||||
if let Err(_) = stream.read_exact(&mut body_buffer) {
|
|
||||||
return Err(MessageError::ReadError);
|
|
||||||
}
|
|
||||||
|
|
||||||
if crc32fast::hash(&body_buffer) != expected_checksum {
|
|
||||||
return Err(MessageError::ChecksumMismatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
bincode::deserialize(&body_buffer).map_err(|_| MessageError::DeserializationError)
|
|
||||||
}
|
|
||||||
253
src/server.rs
253
src/server.rs
|
|
@ -1,253 +0,0 @@
|
||||||
use interprocess::local_socket::{LocalSocketListener, LocalSocketStream};
|
|
||||||
|
|
||||||
use rmp::{
|
|
||||||
os,
|
|
||||||
protocol::{Message, MessageType},
|
|
||||||
server::ServerError,
|
|
||||||
PlaylistParams, ServerState,
|
|
||||||
};
|
|
||||||
use std::{
|
|
||||||
fs,
|
|
||||||
path::PathBuf,
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::CliArgs;
|
|
||||||
|
|
||||||
use self::playback::{playback_manager, Playback};
|
|
||||||
|
|
||||||
pub mod decoder;
|
|
||||||
pub mod playback;
|
|
||||||
|
|
||||||
pub struct Server {
|
|
||||||
pub state: ServerState,
|
|
||||||
pub playback: Playback,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Server {
|
|
||||||
pub fn from_state(state: ServerState) -> Self {
|
|
||||||
Self {
|
|
||||||
state,
|
|
||||||
playback: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run(args: CliArgs) -> Result<(), ServerError> {
|
|
||||||
if let Err(err) = os::server::reserve_pid() {
|
|
||||||
let exit_code = handle_error(err);
|
|
||||||
std::process::exit(exit_code);
|
|
||||||
}
|
|
||||||
|
|
||||||
let server = Arc::new(Mutex::new(Server::from_state(ServerState {
|
|
||||||
playlist_params: PlaylistParams {
|
|
||||||
shuffle: args.shuffle,
|
|
||||||
next: args.next,
|
|
||||||
repeat: args.repeat,
|
|
||||||
},
|
|
||||||
player: None,
|
|
||||||
})));
|
|
||||||
|
|
||||||
let server_clone = server.clone();
|
|
||||||
std::thread::Builder::new()
|
|
||||||
.name("playback_manager".into())
|
|
||||||
.spawn(move || playback_manager(server_clone))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
serve(server)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn kill() {
|
|
||||||
if let Err(err) = os::server::kill() {
|
|
||||||
let exit_code = handle_error(err);
|
|
||||||
std::process::exit(exit_code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_running() -> Result<bool, ServerError> {
|
|
||||||
match os::server::is_running() {
|
|
||||||
Ok(is_running) => Ok(is_running),
|
|
||||||
Err(err) => {
|
|
||||||
let exit_code = handle_error(err);
|
|
||||||
std::process::exit(exit_code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run_in_background() {
|
|
||||||
if let Err(err) = os::server::run_in_background() {
|
|
||||||
let exit_code = handle_error(err);
|
|
||||||
std::process::exit(exit_code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serve(server: Arc<Mutex<Server>>) -> Result<(), ServerError> {
|
|
||||||
let socket_path = os::server::get_socket_path()?;
|
|
||||||
if socket_path.exists() {
|
|
||||||
fs::remove_file(&socket_path).map_err(|err| ServerError::Io(err))?;
|
|
||||||
}
|
|
||||||
let socket = LocalSocketListener::bind(socket_path).map_err(|err| ServerError::Io(err))?;
|
|
||||||
println!("Waiting for connections...");
|
|
||||||
let mut session_counter = 0;
|
|
||||||
for message in socket.incoming() {
|
|
||||||
match message {
|
|
||||||
Ok(stream) => {
|
|
||||||
session_counter += 1;
|
|
||||||
let thread_builder =
|
|
||||||
std::thread::Builder::new().name(format!("session_{session_counter}"));
|
|
||||||
let server = server.clone();
|
|
||||||
thread_builder
|
|
||||||
.spawn(move || session_handler(stream, server))
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
let exit_code = handle_error(ServerError::Io(err));
|
|
||||||
std::process::exit(exit_code);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
println!("Reached the end of an infinite loop?");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn session_handler(mut stream: LocalSocketStream, server: Arc<Mutex<Server>>) {
|
|
||||||
let thread = std::thread::current();
|
|
||||||
let session_id = thread.name().unwrap_or("<unnamed>");
|
|
||||||
println!("[{session_id}] session created");
|
|
||||||
loop {
|
|
||||||
match rmp::protocol::receive(&mut stream) {
|
|
||||||
Ok(message) => {
|
|
||||||
println!("[{session_id}] rx {message}");
|
|
||||||
match route_request(&message, &mut server.lock().unwrap()) {
|
|
||||||
Err(err) => {
|
|
||||||
eprintln!("[{session_id}] rx Error: {err}");
|
|
||||||
}
|
|
||||||
Ok(response) => {
|
|
||||||
println!("[{session_id}] tx {response}");
|
|
||||||
rmp::protocol::send(&mut stream, &response).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(error) => match error {
|
|
||||||
rmp::protocol::MessageError::ReadError => {
|
|
||||||
println!("[{session_id}] session terminated");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
error => {
|
|
||||||
eprintln!("[{session_id}] rx Error {error:?}");
|
|
||||||
let body = bincode::serialize(&error).unwrap();
|
|
||||||
// TODO: Server-side error logging so internal error data is not sent to client
|
|
||||||
let message = Message {
|
|
||||||
message_type: MessageType::ProtocolError,
|
|
||||||
body: Some(body),
|
|
||||||
};
|
|
||||||
eprintln!("[{session_id}] tx {message}");
|
|
||||||
rmp::protocol::send(&mut stream, &message).unwrap();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_error(err: ServerError) -> i32 {
|
|
||||||
match &err {
|
|
||||||
ServerError::Other(msg) => {
|
|
||||||
eprintln!("Unknown error: {msg}");
|
|
||||||
1
|
|
||||||
}
|
|
||||||
ServerError::Io(err) => {
|
|
||||||
eprintln!("IO error: {err}");
|
|
||||||
2
|
|
||||||
}
|
|
||||||
ServerError::AlreadyRunning => {
|
|
||||||
eprintln!("Server already running");
|
|
||||||
100
|
|
||||||
}
|
|
||||||
ServerError::NotRunning => {
|
|
||||||
eprintln!("Server is not running");
|
|
||||||
101
|
|
||||||
}
|
|
||||||
ServerError::MissingRuntimeDir(path) => {
|
|
||||||
eprintln!("Missing runtime directory: {}", path.to_string_lossy());
|
|
||||||
200
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn route_request(request: &Message, server: &mut Server) -> Result<Message, String> {
|
|
||||||
match request.message_type {
|
|
||||||
MessageType::StateFetch => {}
|
|
||||||
MessageType::SetNext => {
|
|
||||||
let body = match request.body.as_ref() {
|
|
||||||
Some(body) => body,
|
|
||||||
None => {
|
|
||||||
return Ok(Message {
|
|
||||||
message_type: MessageType::ApplicationError,
|
|
||||||
body: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let new_state: bool = bincode::deserialize(&body).map_err(|err| err.to_string())?;
|
|
||||||
server.state.playlist_params.next = new_state;
|
|
||||||
}
|
|
||||||
MessageType::SetShuffle => {
|
|
||||||
let body = match request.body.as_ref() {
|
|
||||||
Some(body) => body,
|
|
||||||
None => {
|
|
||||||
return Ok(Message {
|
|
||||||
message_type: MessageType::ApplicationError,
|
|
||||||
body: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let new_state: bool = bincode::deserialize(&body).map_err(|err| err.to_string())?;
|
|
||||||
server.state.playlist_params.shuffle = new_state;
|
|
||||||
}
|
|
||||||
MessageType::SetRepeat => {
|
|
||||||
let body = match request.body.as_ref() {
|
|
||||||
Some(body) => body,
|
|
||||||
None => {
|
|
||||||
return Ok(Message {
|
|
||||||
message_type: MessageType::ApplicationError,
|
|
||||||
body: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let new_state: bool = bincode::deserialize(&body).map_err(|err| err.to_string())?;
|
|
||||||
server.state.playlist_params.repeat = new_state;
|
|
||||||
}
|
|
||||||
MessageType::SetPause => {
|
|
||||||
let body = match request.body.as_ref() {
|
|
||||||
Some(body) => body,
|
|
||||||
None => {
|
|
||||||
return Ok(Message {
|
|
||||||
message_type: MessageType::ApplicationError,
|
|
||||||
body: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let new_state: bool = bincode::deserialize(&body).map_err(|err| err.to_string())?;
|
|
||||||
server.playback.pause(new_state);
|
|
||||||
}
|
|
||||||
MessageType::PlayTrackFromPath => {
|
|
||||||
let body = match request.body.as_ref() {
|
|
||||||
Some(body) => body,
|
|
||||||
None => {
|
|
||||||
return Ok(Message {
|
|
||||||
message_type: MessageType::ApplicationError,
|
|
||||||
body: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let body: PathBuf = bincode::deserialize(&body).map_err(|err| err.to_string())?;
|
|
||||||
server.playback.play(body);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
return Ok(Message {
|
|
||||||
message_type: MessageType::NotImplementedAck,
|
|
||||||
body: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Message::state_response(&server.state)
|
|
||||||
}
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
use std::{
|
|
||||||
borrow::BorrowMut,
|
|
||||||
fs::File,
|
|
||||||
path::PathBuf,
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
};
|
|
||||||
|
|
||||||
use rmp::PlayerState;
|
|
||||||
use rodio::{OutputStream, Sink};
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
decoder::{Decoder, DecoderImpl, SourceWrapper},
|
|
||||||
Server,
|
|
||||||
};
|
|
||||||
|
|
||||||
// HACK: hard-coded path to track
|
|
||||||
static TRACK: &str = "";
|
|
||||||
|
|
||||||
enum PlaybackCommand {
|
|
||||||
PlayPath(PathBuf),
|
|
||||||
SetPause(bool),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Playback {
|
|
||||||
command_queue: Vec<PlaybackCommand>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Playback {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
command_queue: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Playback {
|
|
||||||
pub fn play(&mut self, path: PathBuf) {
|
|
||||||
self.command_queue
|
|
||||||
.push(PlaybackCommand::PlayPath(TRACK.into())) // TODO: Remove hardcoding
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pause(&mut self, new_state: bool) {
|
|
||||||
self.command_queue
|
|
||||||
.push(PlaybackCommand::SetPause(new_state))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn playback_manager(server: Arc<Mutex<Server>>) {
|
|
||||||
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
|
|
||||||
let sink: Sink = Sink::try_new(&stream_handle).unwrap();
|
|
||||||
loop {
|
|
||||||
let mut server = server.lock().unwrap();
|
|
||||||
let commands: Vec<PlaybackCommand> = server.playback.command_queue.drain(..).collect();
|
|
||||||
for command in commands {
|
|
||||||
match command {
|
|
||||||
PlaybackCommand::PlayPath(track) => {
|
|
||||||
let mut source: Option<Box<dyn DecoderImpl<Item = i16>>> = None;
|
|
||||||
|
|
||||||
{
|
|
||||||
let file = File::open(&track).unwrap();
|
|
||||||
if let Ok(decoder) = Decoder::rodio(SourceWrapper::from_file(file)) {
|
|
||||||
println!("playback: rodio\n\tsample_rate: {:?}\n\ttotal_duration: {:?}\n\tchannels: {:?}\n\tcurrent_frame_len: {:?}", decoder.sample_rate(), decoder.total_duration(), decoder.channels(), decoder.current_frame_len());
|
|
||||||
source = Some(decoder);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if source.is_none() {
|
|
||||||
let file = File::open(&track).unwrap();
|
|
||||||
if let Ok(decoder) = Decoder::custom(SourceWrapper::from_file(file)) {
|
|
||||||
println!("playback: custom\n\tsample_rate: {:?}\n\ttotal_duration: {:?}\n\tchannels: {:?}\n\tcurrent_frame_len: {:?}", decoder.sample_rate(), decoder.total_duration(), decoder.channels(), decoder.current_frame_len());
|
|
||||||
source = Some(decoder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match source {
|
|
||||||
Some(source) => {
|
|
||||||
server.state.player = Some(PlayerState {
|
|
||||||
track,
|
|
||||||
is_paused: false,
|
|
||||||
duration: None,
|
|
||||||
current_position: None,
|
|
||||||
});
|
|
||||||
sink.clear();
|
|
||||||
sink.append(source);
|
|
||||||
sink.play();
|
|
||||||
}
|
|
||||||
None => println!("No handler found for '{track:?}'"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
PlaybackCommand::SetPause(new_state) => {
|
|
||||||
if !sink.empty() && new_state != sink.is_paused() {
|
|
||||||
if sink.is_paused() {
|
|
||||||
sink.play();
|
|
||||||
if let Some(player) = server.state.player.borrow_mut() {
|
|
||||||
player.is_paused = false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
sink.pause();
|
|
||||||
if let Some(player) = server.state.player.borrow_mut() {
|
|
||||||
player.is_paused = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,162 @@
|
||||||
|
use ratatui::{
|
||||||
|
prelude::*,
|
||||||
|
widgets::{block::Title, *},
|
||||||
|
};
|
||||||
|
use rmp::{PlaylistElement, StatefulList};
|
||||||
|
|
||||||
|
use super::app::App;
|
||||||
|
|
||||||
|
pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||||
|
let main_chunks = Layout::default()
|
||||||
|
.constraints([Constraint::Min(3), Constraint::Length(2)].as_ref())
|
||||||
|
.split(f.size());
|
||||||
|
draw_playlists(f, app, main_chunks[0]);
|
||||||
|
draw_player(f, app, main_chunks[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static PRIMARY_COLOR: Color = Color::Rgb(200, 150, 70);
|
||||||
|
static SECONDARY_COLOR: Color = Color::Rgb(200, 200, 200);
|
||||||
|
|
||||||
|
static PRIMARY_CONTRAST: Color = Color::Black;
|
||||||
|
static CLEAR_CONTRAST: Color = Color::Rgb(100, 100, 100);
|
||||||
|
|
||||||
|
fn draw_playlists<B: Backend>(f: &mut Frame<B>, app: &App, area: Rect) {
|
||||||
|
let layout = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||||
|
.split(area);
|
||||||
|
draw_playlist(
|
||||||
|
f,
|
||||||
|
app,
|
||||||
|
layout[0],
|
||||||
|
app.directory_playlist.directory.to_string_lossy().into(),
|
||||||
|
&app.directory_playlist.playlist,
|
||||||
|
);
|
||||||
|
draw_playlist(
|
||||||
|
f,
|
||||||
|
app,
|
||||||
|
layout[1],
|
||||||
|
"queue".into(),
|
||||||
|
&app.queue_playlist.playlist,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_playlist<B: Backend>(
|
||||||
|
f: &mut Frame<B>,
|
||||||
|
app: &App,
|
||||||
|
area: Rect,
|
||||||
|
title: String,
|
||||||
|
playlist: &StatefulList<PlaylistElement>,
|
||||||
|
) {
|
||||||
|
let tracks: Vec<_> = playlist
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, path)| {
|
||||||
|
let selected = playlist
|
||||||
|
.state
|
||||||
|
.selected()
|
||||||
|
.map_or(false, |selected| index == selected);
|
||||||
|
let playing = app
|
||||||
|
.playback
|
||||||
|
.current
|
||||||
|
.clone()
|
||||||
|
.map_or(false, |current| current.track == *path);
|
||||||
|
let mut style = Style::default();
|
||||||
|
match (selected, playing) {
|
||||||
|
(true, false) => {
|
||||||
|
style.fg = Some(Color::Black);
|
||||||
|
style.bg = Some(PRIMARY_COLOR);
|
||||||
|
}
|
||||||
|
(false, true) => style.fg = Some(PRIMARY_COLOR),
|
||||||
|
(true, true) => {
|
||||||
|
style.fg = None;
|
||||||
|
style.bg = Some(PRIMARY_COLOR);
|
||||||
|
}
|
||||||
|
(_, _) => (),
|
||||||
|
}
|
||||||
|
let content = Span::from(render_track_name(path));
|
||||||
|
ListItem::new(content).set_style(style)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let playlist = List::new(tracks)
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.title(title)
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(SECONDARY_COLOR)),
|
||||||
|
)
|
||||||
|
.start_corner(Corner::TopLeft);
|
||||||
|
f.render_widget(playlist, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_player<B: Backend>(f: &mut Frame<B>, app: &App, area: Rect) {
|
||||||
|
fn decorate_bool(span: Span, value: bool) -> Span {
|
||||||
|
match value {
|
||||||
|
true => span.bg(PRIMARY_COLOR).fg(PRIMARY_CONTRAST),
|
||||||
|
false => span.fg(CLEAR_CONTRAST),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let track_title = vec![
|
||||||
|
Span::from("[ "),
|
||||||
|
Span::from("??").fg(PRIMARY_COLOR),
|
||||||
|
Span::from(" ]"),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut block = Block::default()
|
||||||
|
.borders(Borders::TOP)
|
||||||
|
.border_style(Style::default().fg(SECONDARY_COLOR))
|
||||||
|
.title(track_title);
|
||||||
|
|
||||||
|
// Horrible to look at, worse to write
|
||||||
|
let param_titles: Vec<Title> = vec![
|
||||||
|
(
|
||||||
|
app.track_change_options.next,
|
||||||
|
vec![
|
||||||
|
Span::from("NE"),
|
||||||
|
Span::from("X").underlined(),
|
||||||
|
Span::from("T"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
app.track_change_options.shuffle,
|
||||||
|
vec![Span::from("S").underlined(), Span::from("HUFFLE")],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
app.track_change_options.repeat,
|
||||||
|
vec![Span::from("R").underlined(), Span::from("EPEAT")],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.map(|(value, spans)| {
|
||||||
|
spans
|
||||||
|
.iter()
|
||||||
|
.map(|span| decorate_bool(span.clone(), *value))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.map(|spans| Title::from([&[Span::from(" ")], spans.as_slice(), &[Span::from(" ")]].concat()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for title in param_titles {
|
||||||
|
block = block.title(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
let block = block.title_position(block::Position::Top);
|
||||||
|
|
||||||
|
let player = Gauge::default()
|
||||||
|
.block(block)
|
||||||
|
.gauge_style(Style::default().fg(PRIMARY_COLOR))
|
||||||
|
.ratio(0.25)
|
||||||
|
.label("[ PAUSED ]");
|
||||||
|
|
||||||
|
f.render_widget(player, area)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_track_name(track: &PlaylistElement) -> String {
|
||||||
|
track
|
||||||
|
.file_name()
|
||||||
|
.map_or("<unspeakable file name>".into(), |os_str| {
|
||||||
|
os_str.to_string_lossy().to_string()
|
||||||
|
})
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue