From e6e52ddb58aac64af544b2e01a2f0027a39f3ddd Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Mon, 3 Jan 2022 21:15:28 -0500 Subject: [PATCH] Add some built-in and code documentation --- Cargo.lock | 6 +- Cargo.toml | 6 +- README.md | 6 +- mps-interpreter/Cargo.toml | 2 +- mps-interpreter/src/interpretor.rs | 3 + mps-interpreter/src/lang/function.rs | 2 +- mps-interpreter/src/runner.rs | 1 + mps-player/Cargo.toml | 4 +- mps-player/README.md | 4 +- mps-player/src/controller.rs | 63 ++++++++++------ mps-player/src/errors.rs | 4 +- mps-player/src/lib.rs | 4 +- mps-player/src/os_controls.rs | 105 ++++++++++++++------------ mps-player/src/player.rs | 55 +++++++------- mps-player/src/player_wrapper.rs | 108 +++++++++++++++------------ src/channel_io.rs | 22 +++--- src/cli.rs | 4 +- src/help.rs | 50 +++++++++++++ src/main.rs | 24 +++--- src/repl.rs | 54 ++++++++++---- 20 files changed, 329 insertions(+), 198 deletions(-) create mode 100644 src/help.rs diff --git a/Cargo.lock b/Cargo.lock index 63d8945..77ac895 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -634,7 +634,7 @@ dependencies = [ [[package]] name = "mps" -version = "0.1.0" +version = "0.2.0" dependencies = [ "clap", "mps-interpreter", @@ -643,7 +643,7 @@ dependencies = [ [[package]] name = "mps-interpreter" -version = "0.1.0" +version = "0.2.0" dependencies = [ "dirs", "regex", @@ -654,7 +654,7 @@ dependencies = [ [[package]] name = "mps-player" -version = "0.1.0" +version = "0.2.0" dependencies = [ "m3u8-rs", "mpris-player", diff --git a/Cargo.toml b/Cargo.toml index cdd9d4c..7a5e83b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mps" -version = "0.1.0" +version = "0.2.0" edition = "2021" authors = ["NGnius (Graham) "] description = "Music Playlist Scripting language (MPS)" @@ -15,7 +15,7 @@ members = [ [dependencies] # local -mps-interpreter = { version = "0.1.0", path = "./mps-interpreter" } -mps-player = { version = "0.1.0", path = "./mps-player" } +mps-interpreter = { version = "0.2.0", path = "./mps-interpreter" } +mps-player = { version = "0.2.0", path = "./mps-player" } # external clap = { version = "3.0", features = ["derive"] } diff --git a/README.md b/README.md index 80d48ff..8c7cbd1 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # mps An MPS program which plays music. -This doesn't do much yet, since mps-interpreter is still under construction. - -Future home of a MPS REPL for playing music ergonomically through a CLI. +This project implements the interpreter (mps-interpreter), music player (mps-player), and CLI interface for MPS (root). +The CLI interface includes a REPL for running scripts. +The REPL interactive mode also provides more details about using MPS through the `?help` command. License: LGPL-2.1-only OR GPL-2.0-or-later diff --git a/mps-interpreter/Cargo.toml b/mps-interpreter/Cargo.toml index 1723fb7..8ea5b75 100644 --- a/mps-interpreter/Cargo.toml +++ b/mps-interpreter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mps-interpreter" -version = "0.1.0" +version = "0.2.0" edition = "2021" license = "LGPL-2.1-only OR GPL-2.0-or-later" readme = "README.md" diff --git a/mps-interpreter/src/interpretor.rs b/mps-interpreter/src/interpretor.rs index 507d444..76726af 100644 --- a/mps-interpreter/src/interpretor.rs +++ b/mps-interpreter/src/interpretor.rs @@ -8,6 +8,8 @@ use super::tokens::MpsToken; use super::MpsContext; use super::MpsMusicItem; +/// The script interpreter. +/// Use MpsRunner for a better interface. pub struct MpsInterpretor where T: crate::tokens::MpsTokenReader, @@ -151,6 +153,7 @@ fn box_error_with_ctx( Box::new(error) as Box } +/// Builder function to add the standard statements of MPS. pub(crate) fn standard_vocab(vocabulary: &mut MpsLanguageDictionary) { vocabulary .add(crate::lang::vocabulary::filters::empty_filter()) diff --git a/mps-interpreter/src/lang/function.rs b/mps-interpreter/src/lang/function.rs index e110f47..6ba6548 100644 --- a/mps-interpreter/src/lang/function.rs +++ b/mps-interpreter/src/lang/function.rs @@ -2,7 +2,7 @@ use std::collections::VecDeque; //use std::fmt::{Debug, Display, Error, Formatter}; use std::marker::PhantomData; -use crate::lang::utility::{assert_token, assert_token_raw, assert_token_raw_back, assert_empty}; +use crate::lang::utility::{assert_empty, assert_token, assert_token_raw, assert_token_raw_back}; use crate::lang::MpsLanguageDictionary; use crate::lang::SyntaxError; use crate::lang::{BoxedMpsOpFactory, MpsOp}; diff --git a/mps-interpreter/src/runner.rs b/mps-interpreter/src/runner.rs index 94cb5aa..fe48cff 100644 --- a/mps-interpreter/src/runner.rs +++ b/mps-interpreter/src/runner.rs @@ -23,6 +23,7 @@ impl MpsRunnerSettings { } } +/// A wrapper around MpsInterpretor which provides a simpler (and more powerful) interface. pub struct MpsRunner { interpretor: MpsInterpretor, new_statement: bool, diff --git a/mps-player/Cargo.toml b/mps-player/Cargo.toml index 3ea2e47..588054c 100644 --- a/mps-player/Cargo.toml +++ b/mps-player/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mps-player" -version = "0.1.0" +version = "0.2.0" edition = "2021" license = "LGPL-2.1-only OR GPL-2.0-or-later" readme = "README.md" @@ -10,7 +10,7 @@ rodio = { version = "^0.14"} m3u8-rs = { version = "^3.0.0" } # local -mps-interpreter = { path = "../mps-interpreter" } +mps-interpreter = { path = "../mps-interpreter", version = "0.2.0" } [target.'cfg(unix)'.dependencies] #dbus = { version = "^0.9" } diff --git a/mps-player/README.md b/mps-player/README.md index b06e0a2..a6d18a7 100644 --- a/mps-player/README.md +++ b/mps-player/README.md @@ -1,6 +1,8 @@ # mps-player -An MPS playback library with support for Linux media controls (D-Bus). +An MPS playback library with support for media controls (Linux & D-Bus only atm). +This handles the output from interpreting a script. +Music playback and m3u8 playlist generation are implemented in this part of the project. License: LGPL-2.1-only OR GPL-2.0-or-later diff --git a/mps-player/src/controller.rs b/mps-player/src/controller.rs index 53730a8..2c9a814 100644 --- a/mps-player/src/controller.rs +++ b/mps-player/src/controller.rs @@ -1,18 +1,20 @@ -use std::sync::mpsc::{Sender, Receiver, channel}; +use std::sync::mpsc::{channel, Receiver, Sender}; use std::thread::JoinHandle; use mps_interpreter::tokens::MpsTokenReader; +use super::os_controls::SystemControlWrapper; +use super::player_wrapper::{ControlAction, MpsPlayerServer, PlayerAction}; use super::MpsPlayer; use super::PlaybackError; -use super::player_wrapper::{ControlAction, PlayerAction, MpsPlayerServer}; -use super::os_controls::SystemControlWrapper; +/// A controller for a MpsPlayer running on another thread. +/// This receives and sends events like media buttons and script errors for the MpsPlayer. pub struct MpsController { control: Sender, event: Receiver, handle: JoinHandle<()>, - sys_ctrl: SystemControlWrapper + sys_ctrl: SystemControlWrapper, } impl MpsController { @@ -23,7 +25,8 @@ impl MpsController { let (event_tx, event_rx) = channel(); let mut sys_ctrl = SystemControlWrapper::new(control_tx.clone()); sys_ctrl.init(); - let handle = MpsPlayerServer::spawn(player_gen, control_tx.clone(), control_rx, event_tx, false); + let handle = + MpsPlayerServer::spawn(player_gen, control_tx.clone(), control_rx, event_tx, false); Self { control: control_tx, event: event_rx, @@ -39,7 +42,8 @@ impl MpsController { let (event_tx, event_rx) = channel(); let mut sys_ctrl = SystemControlWrapper::new(control_tx.clone()); sys_ctrl.init(); - let handle = MpsPlayerServer::spawn(player_gen, control_tx.clone(), control_rx, event_tx, true); + let handle = + MpsPlayerServer::spawn(player_gen, control_tx.clone(), control_rx, event_tx, true); Self { control: control_tx, event: event_rx, @@ -49,7 +53,9 @@ impl MpsController { } fn send_confirm(&self, to_send: ControlAction) -> Result<(), PlaybackError> { - self.control.send(to_send.clone()).map_err(PlaybackError::from_err)?; + self.control + .send(to_send.clone()) + .map_err(PlaybackError::from_err)?; let mut response = self.event.recv().map_err(PlaybackError::from_err)?; while !response.is_acknowledgement() { Self::handle_event(response)?; @@ -60,12 +66,13 @@ impl MpsController { Ok(()) } else { Err(PlaybackError { - msg: "Incorrect acknowledgement received for MpsController control action".into() + msg: "Incorrect acknowledgement received for MpsController control action" + .into(), }) } } else { Err(PlaybackError { - msg: "Invalid acknowledgement received for MpsController control action".into() + msg: "Invalid acknowledgement received for MpsController control action".into(), }) } } @@ -80,41 +87,44 @@ impl MpsController { } pub fn next(&self) -> Result<(), PlaybackError> { - self.send_confirm(ControlAction::Next{ack: true}) + self.send_confirm(ControlAction::Next { ack: true }) } pub fn previous(&self) -> Result<(), PlaybackError> { - self.send_confirm(ControlAction::Previous{ack: true}) + self.send_confirm(ControlAction::Previous { ack: true }) } pub fn play(&self) -> Result<(), PlaybackError> { - self.send_confirm(ControlAction::Play{ack: true}) + self.send_confirm(ControlAction::Play { ack: true }) } pub fn pause(&self) -> Result<(), PlaybackError> { - self.send_confirm(ControlAction::Pause{ack: true}) + self.send_confirm(ControlAction::Pause { ack: true }) } pub fn stop(&self) -> Result<(), PlaybackError> { - self.send_confirm(ControlAction::Stop{ack: true}) + self.send_confirm(ControlAction::Stop { ack: true }) } pub fn enqueue(&self, count: usize) -> Result<(), PlaybackError> { - self.send_confirm(ControlAction::Enqueue{amount: count, ack: true}) + self.send_confirm(ControlAction::Enqueue { + amount: count, + ack: true, + }) } pub fn ping(&self) -> Result<(), PlaybackError> { - self.send_confirm(ControlAction::NoOp{ack: true}) + self.send_confirm(ControlAction::NoOp { ack: true }) } pub fn exit(self) -> Result<(), PlaybackError> { - self.send_confirm(ControlAction::Exit{ack: true})?; + self.send_confirm(ControlAction::Exit { ack: true })?; self.sys_ctrl.exit(); match self.handle.join() { Ok(x) => Ok(x), Err(_) => Err(PlaybackError { - msg: "MpsPlayerServer did not exit correctly".into() - }) + msg: "MpsPlayerServer did not exit correctly".into(), + }), } } @@ -134,7 +144,9 @@ impl MpsController { for msg in self.event.try_iter() { Self::handle_event(msg)?; } - self.control.send(ControlAction::CheckEmpty{ack: true}).map_err(PlaybackError::from_err)?; + self.control + .send(ControlAction::CheckEmpty { ack: true }) + .map_err(PlaybackError::from_err)?; loop { let msg = self.event.recv().map_err(PlaybackError::from_err)?; if let PlayerAction::Empty = msg { @@ -161,8 +173,12 @@ impl MpsController { /// Like check(), but it also waits for an acknowledgement to ensure it gets the latest events. pub fn check_ack(&self) -> Vec { let mut result = Vec::new(); - let to_send = ControlAction::NoOp{ack: true}; - if let Err(e) = self.control.send(to_send.clone()).map_err(PlaybackError::from_err) { + let to_send = ControlAction::NoOp { ack: true }; + if let Err(e) = self + .control + .send(to_send.clone()) + .map_err(PlaybackError::from_err) + { result.push(e); } for msg in self.event.iter() { @@ -171,7 +187,8 @@ impl MpsController { break; } else { result.push(PlaybackError { - msg: "Incorrect acknowledgement received for MpsController control action".into() + msg: "Incorrect acknowledgement received for MpsController control action" + .into(), }); } } else if let Err(e) = Self::handle_event(msg) { diff --git a/mps-player/src/errors.rs b/mps-player/src/errors.rs index 34a97e3..bcc941c 100644 --- a/mps-player/src/errors.rs +++ b/mps-player/src/errors.rs @@ -1,8 +1,8 @@ -use std::fmt::{Debug, Display, Formatter, Error}; +use std::fmt::{Debug, Display, Error, Formatter}; #[derive(Debug, Clone)] pub struct PlaybackError { - pub(crate) msg: String + pub(crate) msg: String, } impl PlaybackError { diff --git a/mps-player/src/lib.rs b/mps-player/src/lib.rs index bf1add1..d3ea138 100644 --- a/mps-player/src/lib.rs +++ b/mps-player/src/lib.rs @@ -1,4 +1,6 @@ -//! An MPS playback library with support for Linux media controls (D-Bus). +//! An MPS playback library with support for media controls (Linux & D-Bus only atm). +//! This handles the output from interpreting a script. +//! Music playback and m3u8 playlist generation are implemented in this part of the project. //! mod controller; diff --git a/mps-player/src/os_controls.rs b/mps-player/src/os_controls.rs index 3022b1d..ce9ede6 100644 --- a/mps-player/src/os_controls.rs +++ b/mps-player/src/os_controls.rs @@ -1,5 +1,5 @@ #[cfg(unix)] -use std::sync::mpsc::{Sender, channel}; +use std::sync::mpsc::{channel, Sender}; #[cfg(unix)] use std::thread::JoinHandle; @@ -9,10 +9,12 @@ use mpris_player::{MprisPlayer, PlaybackStatus}; //use super::MpsController; use super::player_wrapper::ControlAction; +/// OS-specific APIs for media controls. +/// Currently only Linux (dbus) is supported. pub struct SystemControlWrapper { control: Sender, #[cfg(target_os = "linux")] - dbus_handle: Option>,//std::sync::Arc, + dbus_handle: Option>, //std::sync::Arc, #[cfg(target_os = "linux")] dbus_die: Option>, } @@ -22,7 +24,7 @@ impl SystemControlWrapper { pub fn new(control: Sender) -> Self { Self { control: control, - dbus_handle: None,//MprisPlayer::new("mps".into(), "mps".into(), "null".into()) + dbus_handle: None, //MprisPlayer::new("mps".into(), "mps".into(), "null".into()) dbus_die: None, } } @@ -44,58 +46,67 @@ impl SystemControlWrapper { dbus_conn.set_can_go_next(true); let control_clone = control_clone1.clone(); - dbus_conn.connect_next( - move || { - //println!("Got next signal"); - control_clone.send(ControlAction::Next{ack: false}).unwrap_or(()) - } - ); + dbus_conn.connect_next(move || { + //println!("Got next signal"); + control_clone + .send(ControlAction::Next { ack: false }) + .unwrap_or(()) + }); let control_clone = control_clone1.clone(); - dbus_conn.connect_previous( - move || control_clone.send(ControlAction::Previous{ack: false}).unwrap_or(()) - ); + dbus_conn.connect_previous(move || { + control_clone + .send(ControlAction::Previous { ack: false }) + .unwrap_or(()) + }); let control_clone = control_clone1.clone(); let dbus_conn_clone = dbus_conn.clone(); - dbus_conn.connect_pause( - move || { - //println!("Got pause signal"); + dbus_conn.connect_pause(move || { + //println!("Got pause signal"); + dbus_conn_clone.set_playback_status(PlaybackStatus::Paused); + control_clone + .send(ControlAction::Pause { ack: false }) + .unwrap_or(()); + }); + + let control_clone = control_clone1.clone(); + let dbus_conn_clone = dbus_conn.clone(); + dbus_conn.connect_play(move || { + //println!("Got play signal"); + dbus_conn_clone.set_playback_status(PlaybackStatus::Playing); + control_clone + .send(ControlAction::Play { ack: false }) + .unwrap_or(()) + }); + + let control_clone = control_clone1.clone(); + let dbus_conn_clone = dbus_conn.clone(); + dbus_conn.connect_play_pause(move || { + //println!("Got play_pause signal (was playing? {})", is_playing); + if is_playing { dbus_conn_clone.set_playback_status(PlaybackStatus::Paused); - control_clone.send(ControlAction::Pause{ack: false}).unwrap_or(()); - } - ); - - let control_clone = control_clone1.clone(); - let dbus_conn_clone = dbus_conn.clone(); - dbus_conn.connect_play( - move || { - //println!("Got play signal"); + control_clone + .send(ControlAction::Pause { ack: false }) + .unwrap_or(()); + } else { dbus_conn_clone.set_playback_status(PlaybackStatus::Playing); - control_clone.send(ControlAction::Play{ack: false}).unwrap_or(()) + control_clone + .send(ControlAction::Play { ack: false }) + .unwrap_or(()); } - ); + is_playing = !is_playing; + }); let control_clone = control_clone1.clone(); - let dbus_conn_clone = dbus_conn.clone(); - dbus_conn.connect_play_pause( - move || { - //println!("Got play_pause signal (was playing? {})", is_playing); - if is_playing { - dbus_conn_clone.set_playback_status(PlaybackStatus::Paused); - control_clone.send(ControlAction::Pause{ack: false}).unwrap_or(()); - } else { - dbus_conn_clone.set_playback_status(PlaybackStatus::Playing); - control_clone.send(ControlAction::Play{ack: false}).unwrap_or(()); - } - is_playing = !is_playing; - } - ); - - let control_clone = control_clone1.clone(); - dbus_conn.connect_volume( - move |v| control_clone.send(ControlAction::SetVolume{ack: false, volume: (v * (u32::MAX as f64)) as _}).unwrap_or(()) - ); + dbus_conn.connect_volume(move |v| { + control_clone + .send(ControlAction::SetVolume { + ack: false, + volume: (v * (u32::MAX as f64)) as _, + }) + .unwrap_or(()) + }); // poll loop, using my custom mpris lib because original did it wrong loop { @@ -120,9 +131,7 @@ impl SystemControlWrapper { #[cfg(not(any(target_os = "linux")))] impl SystemControlWrapper { pub fn new(control: Sender) -> Self { - Self { - control: control, - } + Self { control: control } } pub fn init(&mut self) {} diff --git a/mps-player/src/player.rs b/mps-player/src/player.rs index 937f9a0..b4fc4f8 100644 --- a/mps-player/src/player.rs +++ b/mps-player/src/player.rs @@ -1,14 +1,16 @@ -use std::io; use std::fs; +use std::io; -use rodio::{decoder::Decoder, OutputStream, Sink, OutputStreamHandle}; +use rodio::{decoder::Decoder, OutputStream, OutputStreamHandle, Sink}; use m3u8_rs::{MediaPlaylist, MediaSegment}; -use mps_interpreter::{MpsRunner, tokens::MpsTokenReader}; +use mps_interpreter::{tokens::MpsTokenReader, MpsRunner}; use super::PlaybackError; +/// Playback functionality for a script. +/// This takes the output of the runner and plays or saves it. pub struct MpsPlayer { runner: MpsRunner, sink: Sink, @@ -19,12 +21,13 @@ pub struct MpsPlayer { impl MpsPlayer { pub fn new(runner: MpsRunner) -> Result { - let (stream, output_handle) = OutputStream::try_default().map_err(PlaybackError::from_err)?; - Ok(Self{ + let (stream, output_handle) = + OutputStream::try_default().map_err(PlaybackError::from_err)?; + Ok(Self { runner: runner, sink: Sink::try_new(&output_handle).map_err(PlaybackError::from_err)?, output_stream: stream, - output_handle: output_handle + output_handle: output_handle, }) } @@ -39,8 +42,8 @@ impl MpsPlayer { self.sink.append(source); //self.sink.play(); // idk if this is necessary Ok(()) - }, - Err(e) => Err(PlaybackError::from_err(e)) + } + Err(e) => Err(PlaybackError::from_err(e)), }?; } self.sink.sleep_until_end(); @@ -57,8 +60,8 @@ impl MpsPlayer { self.sink.append(source); //self.sink.play(); // idk if this is necessary Ok(()) - }, - Err(e) => Err(PlaybackError::from_err(e)) + } + Err(e) => Err(PlaybackError::from_err(e)), }?; } Ok(()) @@ -66,7 +69,9 @@ impl MpsPlayer { pub fn enqueue(&mut self, count: usize) -> Result<(), PlaybackError> { let mut items_left = count; - if items_left == 0 { return Ok(()); } + if items_left == 0 { + return Ok(()); + } for item in &mut self.runner { match item { Ok(music) => { @@ -77,11 +82,13 @@ impl MpsPlayer { self.sink.append(source); //self.sink.play(); // idk if this is necessary Ok(()) - }, - Err(e) => Err(PlaybackError::from_err(e)) + } + Err(e) => Err(PlaybackError::from_err(e)), }?; items_left -= 1; - if items_left == 0 { break; } + if items_left == 0 { + break; + } } //println!("Enqueued {} items", count - items_left); Ok(()) @@ -120,16 +127,14 @@ impl MpsPlayer { for item in &mut self.runner { match item { Ok(music) => { - playlist.segments.push( - MediaSegment { - uri: music.filename, - title: Some(music.title), - ..Default::default() - } - ); + playlist.segments.push(MediaSegment { + uri: music.filename, + title: Some(music.title), + ..Default::default() + }); Ok(()) - }, - Err(e) => Err(PlaybackError::from_err(e)) + } + Err(e) => Err(PlaybackError::from_err(e)), }?; } playlist.write_to(w).map_err(PlaybackError::from_err) @@ -160,9 +165,9 @@ impl MpsPlayer { #[cfg(test)] mod tests { - use std::io; - use mps_interpreter::MpsRunner; use super::*; + use mps_interpreter::MpsRunner; + use std::io; #[allow(dead_code)] #[test] diff --git a/mps-player/src/player_wrapper.rs b/mps-player/src/player_wrapper.rs index e4296a3..89b0a6a 100644 --- a/mps-player/src/player_wrapper.rs +++ b/mps-player/src/player_wrapper.rs @@ -1,4 +1,4 @@ -use std::sync::mpsc::{Sender, Receiver}; +use std::sync::mpsc::{Receiver, Sender}; use std::{thread, thread::JoinHandle}; use mps_interpreter::tokens::MpsTokenReader; @@ -6,6 +6,10 @@ use mps_interpreter::tokens::MpsTokenReader; use super::MpsPlayer; use super::PlaybackError; +/// A wrapper around MpsPlayer so that playback can occur on a different thread. +/// This allows for message passing between the threads. +/// +/// You will probably never directly interact with this, instead using MpsController to communicate. pub struct MpsPlayerServer { player: MpsPlayer, control: Receiver, @@ -14,7 +18,12 @@ pub struct MpsPlayerServer { } impl MpsPlayerServer { - pub fn new(player: MpsPlayer, ctrl: Receiver, event: Sender, keep_alive: bool) -> Self { + pub fn new( + player: MpsPlayer, + ctrl: Receiver, + event: Sender, + keep_alive: bool, + ) -> Self { Self { player: player, control: ctrl, @@ -39,7 +48,7 @@ impl MpsPlayerServer { // process command match command { - ControlAction::Next{..} => { + ControlAction::Next { .. } => { //println!("Executing next command (queue_len: {})", self.player.queue_len()); if let Err(e) = self.player.new_sink() { self.event.send(PlayerAction::Exception(e)).unwrap(); @@ -47,32 +56,32 @@ impl MpsPlayerServer { if !self.player.is_paused() { self.player.enqueue(1).unwrap(); } - }, - ControlAction::Previous{..} => {}, // TODO - ControlAction::Play{..} => self.player.resume(), - ControlAction::Pause{..} => self.player.pause(), - ControlAction::PlayPause{..} => { + } + ControlAction::Previous { .. } => {} // TODO + ControlAction::Play { .. } => self.player.resume(), + ControlAction::Pause { .. } => self.player.pause(), + ControlAction::PlayPause { .. } => { if self.player.is_paused() { self.player.resume(); } else { self.player.pause(); } - }, - ControlAction::Stop{..} => self.player.stop(), - ControlAction::Exit{..} => { + } + ControlAction::Stop { .. } => self.player.stop(), + ControlAction::Exit { .. } => { self.player.stop(); is_exiting = true; - }, - ControlAction::Enqueue{amount,..} => { + } + ControlAction::Enqueue { amount, .. } => { if let Err(e) = self.player.enqueue(amount) { self.event.send(PlayerAction::Exception(e)).unwrap(); } - }, - ControlAction::NoOp{..} => {}, // empty by design - ControlAction::SetVolume{volume,..} => { + } + ControlAction::NoOp { .. } => {} // empty by design + ControlAction::SetVolume { volume, .. } => { self.player.set_volume((volume as f32) / (u32::MAX as f32)); - }, - ControlAction::CheckEmpty{..} => { + } + ControlAction::CheckEmpty { .. } => { check_empty = true; } } @@ -82,7 +91,8 @@ impl MpsPlayerServer { if let Err(e) = self.player.enqueue(1) { self.event.send(PlayerAction::Exception(e)).unwrap(); } - if self.player.queue_len() == 0 { // no more music to add + if self.player.queue_len() == 0 { + // no more music to add is_exiting = !self.keep_alive || is_exiting; } } @@ -92,10 +102,12 @@ impl MpsPlayerServer { } // always check for empty state change - if self.player.queue_len() == 0 && !is_empty { // just became empty + if self.player.queue_len() == 0 && !is_empty { + // just became empty is_empty = true; self.event.send(PlayerAction::Empty).unwrap(); - } else if self.player.queue_len() != 0 && is_empty { // just got filled + } else if self.player.queue_len() != 0 && is_empty { + // just got filled is_empty = false; } @@ -103,7 +115,9 @@ impl MpsPlayerServer { self.event.send(PlayerAction::Empty).unwrap(); } - if is_exiting { break; } + if is_exiting { + break; + } } println!("Exiting playback server"); self.event.send(PlayerAction::End).unwrap(); @@ -114,7 +128,7 @@ impl MpsPlayerServer { ctrl_tx: Sender, ctrl_rx: Receiver, event: Sender, - keep_alive: bool + keep_alive: bool, ) -> JoinHandle<()> { thread::spawn(move || Self::unblocking_timer_loop(ctrl_tx, 50)); thread::spawn(move || { @@ -127,7 +141,7 @@ impl MpsPlayerServer { pub fn unblocking_timer_loop(ctrl_tx: Sender, sleep_ms: u64) { let dur = std::time::Duration::from_millis(sleep_ms); loop { - if let Err(_) = ctrl_tx.send(ControlAction::NoOp{ack: false}) { + if let Err(_) = ctrl_tx.send(ControlAction::NoOp { ack: false }) { break; } thread::sleep(dur); @@ -139,33 +153,33 @@ impl MpsPlayerServer { #[allow(dead_code)] #[derive(Clone, PartialEq, Eq, Debug)] pub enum ControlAction { - Next{ack: bool}, - Previous{ack: bool}, - Play{ack: bool}, - Pause{ack: bool}, - PlayPause{ack: bool}, - Stop{ack: bool}, - Exit{ack: bool}, - Enqueue {amount: usize, ack: bool}, - NoOp{ack: bool}, - SetVolume{ack: bool, volume: u32}, - CheckEmpty{ack: bool}, + Next { ack: bool }, + Previous { ack: bool }, + Play { ack: bool }, + Pause { ack: bool }, + PlayPause { ack: bool }, + Stop { ack: bool }, + Exit { ack: bool }, + Enqueue { amount: usize, ack: bool }, + NoOp { ack: bool }, + SetVolume { ack: bool, volume: u32 }, + CheckEmpty { ack: bool }, } impl ControlAction { fn needs_ack(&self) -> bool { *match self { - Self::Next{ack} => ack, - Self::Previous{ack} => ack, - Self::Play{ack} => ack, - Self::Pause{ack} => ack, - Self::PlayPause{ack} => ack, - Self::Stop{ack} => ack, - Self::Exit{ack} => ack, - Self::Enqueue{ack,..} => ack, - Self::NoOp{ack,..} => ack, - Self::SetVolume{ack,..} => ack, - Self::CheckEmpty{ack} => ack, + Self::Next { ack } => ack, + Self::Previous { ack } => ack, + Self::Play { ack } => ack, + Self::Pause { ack } => ack, + Self::PlayPause { ack } => ack, + Self::Stop { ack } => ack, + Self::Exit { ack } => ack, + Self::Enqueue { ack, .. } => ack, + Self::NoOp { ack, .. } => ack, + Self::SetVolume { ack, .. } => ack, + Self::CheckEmpty { ack } => ack, } } } @@ -183,7 +197,7 @@ impl PlayerAction { pub fn is_acknowledgement(&self) -> bool { match self { Self::Acknowledge(_) => true, - _ => false + _ => false, } } } diff --git a/src/channel_io.rs b/src/channel_io.rs index 130441a..0fc173e 100644 --- a/src/channel_io.rs +++ b/src/channel_io.rs @@ -1,5 +1,5 @@ -use std::sync::mpsc::{channel, Sender, Receiver}; -use std::io::{Read, Write, self}; +use std::io::{self, Read, Write}; +use std::sync::mpsc::{channel, Receiver, Sender}; pub struct ChannelWriter { tx: Sender, @@ -29,34 +29,36 @@ pub struct ChannelReader { } impl Read for ChannelReader { - fn read(&mut self, buf: &mut [u8]) -> io::Result { + fn read(&mut self, buf: &mut [u8]) -> io::Result { let mut count = 0; if self.blocking { for b in self.rx.iter() { buf[count] = b; count += 1; - if count >= buf.len() {break;} + if count >= buf.len() { + break; + } } } else { for b in self.rx.try_iter() { buf[count] = b; count += 1; - if count >= buf.len() {break;} + if count >= buf.len() { + break; + } } } Ok(count) - } + } } pub fn channel_io() -> (ChannelWriter, ChannelReader) { let (sender, receiver) = channel(); ( - ChannelWriter { - tx: sender, - }, + ChannelWriter { tx: sender }, ChannelReader { rx: receiver, blocking: false, - } + }, ) } diff --git a/src/cli.rs b/src/cli.rs index e3d3e04..a26aa8e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,4 +1,4 @@ -use clap::{Parser}; +use clap::Parser; #[derive(Parser)] #[clap(author, version)] @@ -17,7 +17,7 @@ pub struct CliArgs { /// In REPL mode, the prompt to display #[clap(long, default_value_t = String::from("|"))] - pub prompt: String + pub prompt: String, } pub fn parse() -> CliArgs { diff --git a/src/help.rs b/src/help.rs new file mode 100644 index 0000000..b5f32e5 --- /dev/null +++ b/src/help.rs @@ -0,0 +1,50 @@ +/// Standard help string containing usage information for MPS. +pub fn help() -> String { +"This language is all about iteration. Almost everything is an iterator or operates on iterators. By default, any operation which is not an assignment will cause the script runner to handle (play/save) the items which that statement contains. + +To view the currently-supported operations, try ?functions and ?filters".to_string() +} + +pub fn functions() -> String { +"FUNCTIONS (?functions) +Similar to most other languages: function_name(param1, param2, etc.) + + sql_init(generate = true|false, folder = `path/to/music`) + Initialize the SQLite database connection using the provided parameters. This must be performed before any other database operation (otherwise the database will be connected with default settings). + + sql(`SQL query here`) + Perform a raw SQLite query on the database which MPS auto-generates. An iterator of the results is returned. + + song(`something`) + Retrieve all songs in the database with a title like something. + + album(`something`) + Retrieve all songs in the database with an album title like something. + + artist(`something`) + Retrieve all songs in the database with an artist name like something. + + genre(`something`) + Retrieve all songs in the database with a genre title like something. + + repeat(iterable, count) + Repeat the iterable count times, or infinite times if count is omitted. + + files(folder = `path/to/music`, recursive = true|false, regex = `pattern`) + Retrieve all files from a folder, matching a regex pattern.".to_string() +} + +pub fn filters() -> String { +"FILTERS (?filters) +Operations to reduce the items in an iterable: iterable.(filter) + + field == something + field >= something + field > something + field <= something + field < something + Compare all items, keeping only those that match the condition. + + [empty] + Matches all items".to_string() +} diff --git a/src/main.rs b/src/main.rs index fef9898..7f3738b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,19 @@ //! An MPS program which plays music. -//! This doesn't do much yet, since mps-interpreter is still under construction. -//! -//! Future home of a MPS REPL for playing music ergonomically through a CLI. +//! This project implements the interpreter (mps-interpreter), music player (mps-player), and CLI interface for MPS (root). +//! The CLI interface includes a REPL for running scripts. +//! The REPL interactive mode also provides more details about using MPS through the `?help` command. //! mod channel_io; mod cli; +mod help; mod repl; use std::io; use std::path::PathBuf; use mps_interpreter::MpsRunner; -use mps_player::{MpsPlayer, PlaybackError, MpsController}; +use mps_player::{MpsController, MpsPlayer, PlaybackError}; #[allow(dead_code)] fn play_cursor() -> Result<(), PlaybackError> { @@ -36,7 +37,7 @@ fn main() { let player_builder = move || { let script_reader = io::BufReader::new( std::fs::File::open(&script_file2) - .expect(&format!("Abort: Cannot open file `{}`", &script_file2)) + .expect(&format!("Abort: Cannot open file `{}`", &script_file2)), ); let runner = MpsRunner::with_stream(script_reader); let player = MpsPlayer::new(runner).unwrap(); @@ -45,12 +46,14 @@ fn main() { if let Some(playlist_file) = &args.playlist { // generate playlist let mut player = player_builder(); - let mut writer = io::BufWriter::new( - std::fs::File::create(playlist_file) - .expect(&format!("Abort: Cannot create writeable file `{}`", playlist_file)) - ); + let mut writer = io::BufWriter::new(std::fs::File::create(playlist_file).expect( + &format!("Abort: Cannot create writeable file `{}`", playlist_file), + )); match player.save_m3u8(&mut writer) { - Ok(_) => println!("Succes: Finished playlist `{}` from script `{}`", playlist_file, script_file), + Ok(_) => println!( + "Succes: Finished playlist `{}` from script `{}`", + playlist_file, script_file + ), Err(e) => eprintln!("{}", e), } } else { @@ -64,6 +67,7 @@ fn main() { } else { // start REPL println!("Welcome to MPS interactive mode!"); + println!("Run ?help for usage instructions."); println!("End a statement with ; to execute it."); repl::repl(args) } diff --git a/src/repl.rs b/src/repl.rs index 5eee21d..8e1309f 100644 --- a/src/repl.rs +++ b/src/repl.rs @@ -1,12 +1,12 @@ //! Read, Execute, Print Loop functionality -use std::io::{self, Write, Read, Stdin}; +use std::io::{self, Read, Stdin, Write}; use mps_interpreter::MpsRunner; -use mps_player::{MpsPlayer, MpsController}; +use mps_player::{MpsController, MpsPlayer}; -use super::cli::CliArgs; use super::channel_io::{channel_io, ChannelWriter}; +use super::cli::CliArgs; struct ReplState { stdin: Stdin, @@ -37,16 +37,17 @@ pub fn repl(args: CliArgs) { if let Some(playlist_file) = &args.playlist { println!("Playlist mode (output: `{}`)", playlist_file); let mut player = player_builder(); - let mut playlist_writer = io::BufWriter::new( - std::fs::File::create(playlist_file) - .expect(&format!("Abort: Cannot create writeable file `{}`", playlist_file)) - ); + let mut playlist_writer = io::BufWriter::new(std::fs::File::create(playlist_file).expect( + &format!("Abort: Cannot create writeable file `{}`", playlist_file), + )); read_loop(&args, &mut state, || { match player.save_m3u8(&mut playlist_writer) { - Ok(_) => {}, + Ok(_) => {} Err(e) => eprintln!("{}", e.message()), } - playlist_writer.flush().expect("Failed to flush playlist to file"); + playlist_writer + .flush() + .expect("Failed to flush playlist to file"); }); } else { println!("Playback mode (output: audio device)"); @@ -54,7 +55,7 @@ pub fn repl(args: CliArgs) { read_loop(&args, &mut state, || { if args.wait { match ctrl.wait_for_empty() { - Ok(_) => {}, + Ok(_) => {} Err(e) => eprintln!("{}", e.message()), } } else { @@ -67,23 +68,34 @@ pub fn repl(args: CliArgs) { } fn read_loop(args: &CliArgs, state: &mut ReplState, mut execute: F) -> ! { - let mut read_buf: [u8;1] = [0]; + let mut read_buf: [u8; 1] = [0]; prompt(&mut state.line_number, args); loop { read_buf[0] = 0; while read_buf[0] == 0 { // TODO: enable raw mode (char by char) reading of stdin - state.stdin.read(&mut read_buf).expect("Failed to read stdin"); + state + .stdin + .read(&mut read_buf) + .expect("Failed to read stdin"); } match read_buf[0] as char { '\n' => { state.statement_buf.push(read_buf[0]); - state.writer.write(state.statement_buf.as_slice()) - .expect("Failed to write to MPS interpreter"); - execute(); + let statement_result = std::str::from_utf8(state.statement_buf.as_slice()); + if statement_result.is_ok() && statement_result.unwrap().starts_with("?") { + repl_commands(statement_result.unwrap()); + state.writer.write(&[';' as u8]).unwrap_or(0); + } else { + state + .writer + .write(state.statement_buf.as_slice()) + .expect("Failed to write to MPS interpreter"); + execute(); + } state.statement_buf.clear(); prompt(&mut state.line_number, args); - }, + } _ => state.statement_buf.push(read_buf[0]), } } @@ -95,3 +107,13 @@ fn prompt(line: &mut usize, args: &CliArgs) { *line += 1; std::io::stdout().flush().expect("Failed to flush stdout"); } + +fn repl_commands(command_str: &str) { + let words: Vec<&str> = command_str.split(" ").map(|s| s.trim()).collect(); + match words[0] { + "?help" => println!("{}", super::help::help()), + "?function" | "?functions" => println!("{}", super::help::functions()), + "?filter" | "?filters" => println!("{}", super::help::filters()), + _ => println!("Unknown command, try ?help"), + } +}