Add some built-in and code documentation
This commit is contained in:
parent
5ef1b4a2b8
commit
e6e52ddb58
20 changed files with 329 additions and 198 deletions
6
Cargo.lock
generated
6
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "mps"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
authors = ["NGnius (Graham) <ngniusness@gmail.com>"]
|
||||
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"] }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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<T>
|
||||
where
|
||||
T: crate::tokens::MpsTokenReader,
|
||||
|
@ -151,6 +153,7 @@ fn box_error_with_ctx<E: MpsLanguageError + 'static>(
|
|||
Box::new(error) as Box<dyn MpsLanguageError>
|
||||
}
|
||||
|
||||
/// 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())
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -23,6 +23,7 @@ impl<T: MpsTokenReader> MpsRunnerSettings<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/// A wrapper around MpsInterpretor which provides a simpler (and more powerful) interface.
|
||||
pub struct MpsRunner<T: MpsTokenReader> {
|
||||
interpretor: MpsInterpretor<T>,
|
||||
new_statement: bool,
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<ControlAction>,
|
||||
event: Receiver<PlayerAction>,
|
||||
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<PlaybackError> {
|
||||
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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<ControlAction>,
|
||||
#[cfg(target_os = "linux")]
|
||||
dbus_handle: Option<JoinHandle<()>>,//std::sync::Arc<MprisPlayer>,
|
||||
dbus_handle: Option<JoinHandle<()>>, //std::sync::Arc<MprisPlayer>,
|
||||
#[cfg(target_os = "linux")]
|
||||
dbus_die: Option<Sender<()>>,
|
||||
}
|
||||
|
@ -22,7 +24,7 @@ impl SystemControlWrapper {
|
|||
pub fn new(control: Sender<ControlAction>) -> 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<ControlAction>) -> Self {
|
||||
Self {
|
||||
control: control,
|
||||
}
|
||||
Self { control: control }
|
||||
}
|
||||
|
||||
pub fn init(&mut self) {}
|
||||
|
|
|
@ -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<T: MpsTokenReader> {
|
||||
runner: MpsRunner<T>,
|
||||
sink: Sink,
|
||||
|
@ -19,12 +21,13 @@ pub struct MpsPlayer<T: MpsTokenReader> {
|
|||
|
||||
impl<T: MpsTokenReader> MpsPlayer<T> {
|
||||
pub fn new(runner: MpsRunner<T>) -> Result<Self, PlaybackError> {
|
||||
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<T: MpsTokenReader> MpsPlayer<T> {
|
|||
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<T: MpsTokenReader> MpsPlayer<T> {
|
|||
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<T: MpsTokenReader> MpsPlayer<T> {
|
|||
|
||||
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<T: MpsTokenReader> MpsPlayer<T> {
|
|||
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<T: MpsTokenReader> MpsPlayer<T> {
|
|||
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<T: MpsTokenReader> MpsPlayer<T> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io;
|
||||
use mps_interpreter::MpsRunner;
|
||||
use super::*;
|
||||
use mps_interpreter::MpsRunner;
|
||||
use std::io;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[test]
|
||||
|
|
|
@ -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<T: MpsTokenReader> {
|
||||
player: MpsPlayer<T>,
|
||||
control: Receiver<ControlAction>,
|
||||
|
@ -14,7 +18,12 @@ pub struct MpsPlayerServer<T: MpsTokenReader> {
|
|||
}
|
||||
|
||||
impl<T: MpsTokenReader> MpsPlayerServer<T> {
|
||||
pub fn new(player: MpsPlayer<T>, ctrl: Receiver<ControlAction>, event: Sender<PlayerAction>, keep_alive: bool) -> Self {
|
||||
pub fn new(
|
||||
player: MpsPlayer<T>,
|
||||
ctrl: Receiver<ControlAction>,
|
||||
event: Sender<PlayerAction>,
|
||||
keep_alive: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
player: player,
|
||||
control: ctrl,
|
||||
|
@ -39,7 +48,7 @@ impl<T: MpsTokenReader> MpsPlayerServer<T> {
|
|||
|
||||
// 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<T: MpsTokenReader> MpsPlayerServer<T> {
|
|||
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<T: MpsTokenReader> MpsPlayerServer<T> {
|
|||
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<T: MpsTokenReader> MpsPlayerServer<T> {
|
|||
}
|
||||
|
||||
// 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<T: MpsTokenReader> MpsPlayerServer<T> {
|
|||
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<T: MpsTokenReader> MpsPlayerServer<T> {
|
|||
ctrl_tx: Sender<ControlAction>,
|
||||
ctrl_rx: Receiver<ControlAction>,
|
||||
event: Sender<PlayerAction>,
|
||||
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<T: MpsTokenReader> MpsPlayerServer<T> {
|
|||
pub fn unblocking_timer_loop(ctrl_tx: Sender<ControlAction>, 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<T: MpsTokenReader> MpsPlayerServer<T> {
|
|||
#[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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<u8>,
|
||||
|
@ -29,34 +29,36 @@ pub struct ChannelReader {
|
|||
}
|
||||
|
||||
impl Read for ChannelReader {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
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,
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
50
src/help.rs
Normal file
50
src/help.rs
Normal file
|
@ -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()
|
||||
}
|
24
src/main.rs
24
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)
|
||||
}
|
||||
|
|
54
src/repl.rs
54
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<F: FnMut()>(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"),
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue