mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 04:06:45 +01:00
wip: implementing app command dispatch
This commit is contained in:
parent
d393cab2d8
commit
12faadef44
31 changed files with 598 additions and 551 deletions
|
|
@ -10,7 +10,9 @@ use crate::*;
|
||||||
}
|
}
|
||||||
pub trait Command<S>: Send + Sync + Sized {
|
pub trait Command<S>: Send + Sync + Sized {
|
||||||
fn execute (self, state: &mut S) -> Perhaps<Self>;
|
fn execute (self, state: &mut S) -> Perhaps<Self>;
|
||||||
fn delegate <T> (self, state: &mut S, wrap: impl Fn(Self)->T) -> Perhaps<T> {
|
fn delegate <T> (self, state: &mut S, wrap: impl Fn(Self)->T) -> Perhaps<T>
|
||||||
|
where Self: Sized
|
||||||
|
{
|
||||||
Ok(self.execute(state)?.map(wrap))
|
Ok(self.execute(state)?.map(wrap))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
/// Turns an EDN item sequence into a command enum variant.
|
/// Turns an EDN item sequence into a command enum variant.
|
||||||
pub trait EdnCommand<C>: Command<C> {
|
pub trait EdnCommand<C>: Command<C> {
|
||||||
fn from_edn <'a> (state: &C, head: &EdnItem<&str>, tail: &'a [EdnItem<String>]) -> Self;
|
fn from_edn <'a> (state: &C, head: &EdnItem<&str>, tail: &'a [EdnItem<&'a str>])
|
||||||
|
-> Option<Self>;
|
||||||
}
|
}
|
||||||
/** Implement `EdnCommand` for given `State` and `Command` */
|
/** Implement `EdnCommand` for given `State` and `Command` */
|
||||||
#[macro_export] macro_rules! edn_command {
|
#[macro_export] macro_rules! edn_command {
|
||||||
|
|
@ -27,7 +28,11 @@ pub trait EdnCommand<C>: Command<C> {
|
||||||
$command:expr
|
$command:expr
|
||||||
))* }) => {
|
))* }) => {
|
||||||
impl EdnCommand<$State> for $Command {
|
impl EdnCommand<$State> for $Command {
|
||||||
fn from_edn <'a> ($state: &$State, head: &EdnItem<&str>, tail: &'a [EdnItem<String>]) -> Self {
|
fn from_edn <'a> (
|
||||||
|
$state: &$State,
|
||||||
|
head: &EdnItem<&str>,
|
||||||
|
tail: &'a [EdnItem<&'a str>]
|
||||||
|
) -> Option<Self> {
|
||||||
$(if let (EdnItem::Key($key), [ // if the identifier matches
|
$(if let (EdnItem::Key($key), [ // if the identifier matches
|
||||||
// bind argument ids
|
// bind argument ids
|
||||||
$($arg),*
|
$($arg),*
|
||||||
|
|
@ -38,9 +43,9 @@ pub trait EdnCommand<C>: Command<C> {
|
||||||
$(let $arg: Option<$type> = EdnProvide::<$type>::get($state, $arg);)?
|
$(let $arg: Option<$type> = EdnProvide::<$type>::get($state, $arg);)?
|
||||||
)*
|
)*
|
||||||
//$(edn_command!(@bind $state => $arg $(?)? : $type);)*
|
//$(edn_command!(@bind $state => $arg $(?)? : $type);)*
|
||||||
return $command
|
return Some($command)
|
||||||
})*
|
})*
|
||||||
panic!("no such command")
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
pub trait EdnInput: Input {
|
pub trait EdnInput: Input {
|
||||||
fn matches (&self, token: &str) -> bool;
|
fn matches_edn (&self, token: &str) -> bool;
|
||||||
fn get_event <S: AsRef<str>> (_: &EdnItem<S>) -> Option<Self::Event> {
|
fn get_event <S: AsRef<str>> (_: &EdnItem<S>) -> Option<Self::Event> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use EdnItem::*;
|
use EdnItem::*;
|
||||||
|
|
||||||
pub struct EdnKeymap(pub Vec<EdnItem<String>>);
|
pub struct EdnKeymap<'a>(pub Vec<EdnItem<&'a str>>);
|
||||||
|
|
||||||
impl EdnKeymap {
|
impl<'a> EdnKeymap<'a> {
|
||||||
pub fn command <C, D: Command<C>, E: EdnCommand<C>, I: EdnInput> (
|
pub fn command <C, D: Command<C>, E: EdnCommand<C>, I: EdnInput> (
|
||||||
&self, state: &C, input: &I
|
&self, state: &C, input: &I
|
||||||
) -> Option<E> {
|
) -> Option<E> {
|
||||||
for item in self.0.iter() {
|
for item in self.0.iter() {
|
||||||
if let Exp(items) = item {
|
if let Exp(items) = item {
|
||||||
match items.as_slice() {
|
match items.as_slice() {
|
||||||
[Sym(a), b, c @ ..] => if input.matches(a.as_str()) {
|
[Sym(a), b, c @ ..] => if input.matches_edn(a) {
|
||||||
return Some(E::from_edn(state, &b.to_ref(), c))
|
return E::from_edn(state, &b.to_ref(), c)
|
||||||
},
|
},
|
||||||
_ => unreachable!()
|
_ => unreachable!()
|
||||||
}
|
}
|
||||||
|
|
@ -23,9 +23,10 @@ impl EdnKeymap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: AsRef<str>> From<T> for EdnKeymap {
|
impl<'a> From<&'a str> for EdnKeymap<'a> {
|
||||||
fn from (source: T) -> Self {
|
fn from (source: &'a str) -> Self {
|
||||||
Self(EdnItem::<String>::read_all(source.as_ref()).expect("failed to load keymap"))
|
let items = EdnItem::<&'a str>::read_all(source.as_ref());
|
||||||
|
Self(items.expect("failed to load keymap"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
use std::sync::{Mutex, Arc, RwLock};
|
|
||||||
|
|
||||||
/// Implement the [Handle] trait.
|
|
||||||
#[macro_export] macro_rules! handle {
|
|
||||||
(|$self:ident:$Struct:ty,$input:ident|$handler:expr) => {
|
|
||||||
impl<E: Engine> Handle<E> for $Struct {
|
|
||||||
fn handle (&mut $self, $input: &E) -> Perhaps<E::Handled> {
|
|
||||||
$handler
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
($E:ty: |$self:ident:$Struct:ty,$input:ident|$handler:expr) => {
|
|
||||||
impl Handle<$E> for $Struct {
|
|
||||||
fn handle (&mut $self, $input: &$E) -> Perhaps<<$E as Input>::Handled> {
|
|
||||||
$handler
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle input
|
|
||||||
pub trait Handle<E: Input>: Send + Sync {
|
|
||||||
fn handle (&mut self, _input: &E) -> Perhaps<E::Handled> {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<E: Input, H: Handle<E>> Handle<E> for &mut H {
|
|
||||||
fn handle (&mut self, context: &E) -> Perhaps<E::Handled> {
|
|
||||||
(*self).handle(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<E: Input, H: Handle<E>> Handle<E> for Option<H> {
|
|
||||||
fn handle (&mut self, context: &E) -> Perhaps<E::Handled> {
|
|
||||||
if let Some(ref mut handle) = self {
|
|
||||||
handle.handle(context)
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<H, E: Input> Handle<E> for Mutex<H> where H: Handle<E> {
|
|
||||||
fn handle (&mut self, context: &E) -> Perhaps<E::Handled> {
|
|
||||||
self.get_mut().unwrap().handle(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<H, E: Input> Handle<E> for Arc<Mutex<H>> where H: Handle<E> {
|
|
||||||
fn handle (&mut self, context: &E) -> Perhaps<E::Handled> {
|
|
||||||
self.lock().unwrap().handle(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<H, E: Input> Handle<E> for RwLock<H> where H: Handle<E> {
|
|
||||||
fn handle (&mut self, context: &E) -> Perhaps<E::Handled> {
|
|
||||||
self.write().unwrap().handle(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<H, E: Input> Handle<E> for Arc<RwLock<H>> where H: Handle<E> {
|
|
||||||
fn handle (&mut self, context: &E) -> Perhaps<E::Handled> {
|
|
||||||
self.write().unwrap().handle(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
|
use crate::*;
|
||||||
|
use std::sync::{Mutex, Arc, RwLock};
|
||||||
|
|
||||||
/// Event source
|
/// Event source
|
||||||
pub trait Input: Send + Sync + Sized {
|
pub trait Input: Send + Sync + Sized {
|
||||||
/// Type of input event
|
/// Type of input event
|
||||||
type Event;
|
type Event;
|
||||||
/// Result of handling input
|
/// Result of handling input
|
||||||
type Handled;
|
type Handled; // TODO: make this an Option<Box dyn Command<Self>> containing the undo
|
||||||
/// Currently handled event
|
/// Currently handled event
|
||||||
fn event (&self) -> &Self::Event;
|
fn event (&self) -> &Self::Event;
|
||||||
/// Whether component should exit
|
/// Whether component should exit
|
||||||
|
|
@ -11,3 +14,62 @@ pub trait Input: Send + Sync + Sized {
|
||||||
/// Mark component as done
|
/// Mark component as done
|
||||||
fn done (&self);
|
fn done (&self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Implement the [Handle] trait.
|
||||||
|
#[macro_export] macro_rules! handle {
|
||||||
|
(|$self:ident:$Struct:ty,$input:ident|$handler:expr) => {
|
||||||
|
impl<E: Engine> Handle<E> for $Struct {
|
||||||
|
fn handle (&mut $self, $input: &E) -> Perhaps<E::Handled> {
|
||||||
|
$handler
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($E:ty: |$self:ident:$Struct:ty,$input:ident|$handler:expr) => {
|
||||||
|
impl Handle<$E> for $Struct {
|
||||||
|
fn handle (&mut $self, $input: &$E) -> Perhaps<<$E as Input>::Handled> {
|
||||||
|
$handler
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle input
|
||||||
|
pub trait Handle<E: Input>: Send + Sync {
|
||||||
|
fn handle (&mut self, _input: &E) -> Perhaps<E::Handled> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<E: Input, H: Handle<E>> Handle<E> for &mut H {
|
||||||
|
fn handle (&mut self, context: &E) -> Perhaps<E::Handled> {
|
||||||
|
(*self).handle(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<E: Input, H: Handle<E>> Handle<E> for Option<H> {
|
||||||
|
fn handle (&mut self, context: &E) -> Perhaps<E::Handled> {
|
||||||
|
if let Some(ref mut handle) = self {
|
||||||
|
handle.handle(context)
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<H, E: Input> Handle<E> for Mutex<H> where H: Handle<E> {
|
||||||
|
fn handle (&mut self, context: &E) -> Perhaps<E::Handled> {
|
||||||
|
self.get_mut().unwrap().handle(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<H, E: Input> Handle<E> for Arc<Mutex<H>> where H: Handle<E> {
|
||||||
|
fn handle (&mut self, context: &E) -> Perhaps<E::Handled> {
|
||||||
|
self.lock().unwrap().handle(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<H, E: Input> Handle<E> for RwLock<H> where H: Handle<E> {
|
||||||
|
fn handle (&mut self, context: &E) -> Perhaps<E::Handled> {
|
||||||
|
self.write().unwrap().handle(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<H, E: Input> Handle<E> for Arc<RwLock<H>> where H: Handle<E> {
|
||||||
|
fn handle (&mut self, context: &E) -> Perhaps<E::Handled> {
|
||||||
|
self.write().unwrap().handle(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
//mod component; pub use self::component::*;
|
//mod component; pub use self::component::*;
|
||||||
mod input; pub use self::input::*;
|
mod input; pub use self::input::*;
|
||||||
mod handle; pub use self::handle::*;
|
|
||||||
mod command; pub use self::command::*;
|
mod command; pub use self::command::*;
|
||||||
mod event_map; pub use self::event_map::*;
|
mod event_map; pub use self::event_map::*;
|
||||||
|
|
||||||
|
|
|
||||||
12
midi/src/keys_pool.edn
Normal file
12
midi/src/keys_pool.edn
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
(:n rename/begin)
|
||||||
|
(:t length/begin)
|
||||||
|
(:m import/begin)
|
||||||
|
(:x export/begin)
|
||||||
|
(:c clip/color :current :random-color)
|
||||||
|
(:openbracket select :previous)
|
||||||
|
(:closebracket select :next)
|
||||||
|
(:lt swap :current :previous)
|
||||||
|
(:gt swap :current :next)
|
||||||
|
(:delete clip/delete :current)
|
||||||
|
(:shift-A clip/add :after :new-clip)
|
||||||
|
(:shift-D clip/add :after :cloned-clip)
|
||||||
|
|
@ -9,9 +9,7 @@ mod midi_range; pub use midi_range::*;
|
||||||
mod midi_point; pub use midi_point::*;
|
mod midi_point; pub use midi_point::*;
|
||||||
mod midi_view; pub use midi_view::*;
|
mod midi_view; pub use midi_view::*;
|
||||||
|
|
||||||
mod midi_pool; pub use midi_pool::*;
|
mod midi_pool; pub use midi_pool::*;
|
||||||
mod midi_pool_cmd; pub use midi_pool_cmd::*;
|
|
||||||
|
|
||||||
mod midi_edit; pub use midi_edit::*;
|
mod midi_edit; pub use midi_edit::*;
|
||||||
mod piano_h; pub use self::piano_h::*;
|
mod piano_h; pub use self::piano_h::*;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,8 @@ pub trait HasEditor {
|
||||||
}
|
}
|
||||||
/// Contains state for viewing and editing a clip
|
/// Contains state for viewing and editing a clip
|
||||||
pub struct MidiEditor {
|
pub struct MidiEditor {
|
||||||
pub mode: PianoHorizontal,
|
pub mode: PianoHorizontal,
|
||||||
pub size: Measure<TuiOut>,
|
pub size: Measure<TuiOut>,
|
||||||
pub keymap: EdnKeymap,
|
|
||||||
}
|
}
|
||||||
impl std::fmt::Debug for MidiEditor {
|
impl std::fmt::Debug for MidiEditor {
|
||||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||||
|
|
@ -47,10 +46,6 @@ impl Default for MidiEditor {
|
||||||
Self {
|
Self {
|
||||||
mode: PianoHorizontal::new(None),
|
mode: PianoHorizontal::new(None),
|
||||||
size: Measure::new(),
|
size: Measure::new(),
|
||||||
keymap: EdnKeymap(
|
|
||||||
EdnItem::<String>::read_all(include_str!("midi_edit_keys.edn"))
|
|
||||||
.expect("failed to load keymap for MidiEditor")
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
use KeyCode::*;
|
||||||
pub type ClipPool = Vec<Arc<RwLock<MidiClip>>>;
|
pub type ClipPool = Vec<Arc<RwLock<MidiClip>>>;
|
||||||
pub trait HasClips {
|
pub trait HasClips {
|
||||||
fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>;
|
fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>;
|
||||||
|
|
@ -204,3 +205,442 @@ content!(TuiOut: |self: ClipLength| {
|
||||||
row!(" ", bars(), ".", beats(), "[", ticks()),
|
row!(" ", bars(), ".", beats(), "[", ticks()),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
impl PoolCommand {
|
||||||
|
const KEYS_POOL: &str = include_str!("keys_pool.edn");
|
||||||
|
const KEYS_FILE: &str = include_str!("keys_pool_file.edn");
|
||||||
|
const KEYS_LENGTH: &str = include_str!("keys_clip_length.edn");
|
||||||
|
const KEYS_RENAME: &str = include_str!("keys_clip_rename.edn");
|
||||||
|
pub fn from_tui_event (state: &MidiPool, input: &impl EdnInput) -> Usually<Option<Self>> {
|
||||||
|
use EdnItem::*;
|
||||||
|
let edns: Vec<EdnItem<&str>> = EdnItem::read_all(match state.mode() {
|
||||||
|
Some(PoolMode::Rename(..)) => Self::KEYS_RENAME,
|
||||||
|
Some(PoolMode::Length(..)) => Self::KEYS_LENGTH,
|
||||||
|
Some(PoolMode::Import(..)) | Some(PoolMode::Export(..)) => Self::KEYS_FILE,
|
||||||
|
_ => Self::KEYS_POOL
|
||||||
|
})?;
|
||||||
|
for item in edns {
|
||||||
|
match item {
|
||||||
|
Exp(e) => match e.as_slice() {
|
||||||
|
[Sym(key), command, args @ ..] if input.matches_edn(key) => {
|
||||||
|
return Ok(PoolCommand::from_edn(state, command, args))
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
_ => panic!("invalid config")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handle!(TuiIn: |self: MidiPool, input|{
|
||||||
|
Ok(if let Some(command) = PoolCommand::from_tui_event(self, input)? {
|
||||||
|
let _undo = command.execute(self)?;
|
||||||
|
Some(true)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
|
});
|
||||||
|
edn_provide!(bool: |self: MidiPool| {});
|
||||||
|
edn_provide!(MidiClip: |self: MidiPool| {
|
||||||
|
":new-clip" => MidiClip::new(
|
||||||
|
"Clip", true, 4 * PPQ, None, Some(ItemPalette::random())
|
||||||
|
),
|
||||||
|
":cloned-clip" => {
|
||||||
|
let index = self.clip_index();
|
||||||
|
let mut clip = self.clips()[index].read().unwrap().duplicate();
|
||||||
|
clip.color = ItemPalette::random_near(clip.color, 0.25);
|
||||||
|
clip
|
||||||
|
}
|
||||||
|
});
|
||||||
|
edn_provide!(PathBuf: |self: MidiPool| {});
|
||||||
|
edn_provide!(Arc<str>: |self: MidiPool| {});
|
||||||
|
edn_provide!(usize: |self: MidiPool| {
|
||||||
|
":current" => 0,
|
||||||
|
":after" => 0,
|
||||||
|
":previous" => 0,
|
||||||
|
":next" => 0
|
||||||
|
});
|
||||||
|
edn_provide!(ItemColor: |self: MidiPool| {
|
||||||
|
":random-color" => ItemColor::random()
|
||||||
|
});
|
||||||
|
#[derive(Clone, PartialEq, Debug)] pub enum PoolCommand {
|
||||||
|
/// Toggle visibility of pool
|
||||||
|
Show(bool),
|
||||||
|
/// Select a clip from the clip pool
|
||||||
|
Select(usize),
|
||||||
|
/// Rename a clip
|
||||||
|
Rename(ClipRenameCommand),
|
||||||
|
/// Change the length of a clip
|
||||||
|
Length(ClipLengthCommand),
|
||||||
|
/// Import from file
|
||||||
|
Import(FileBrowserCommand),
|
||||||
|
/// Export to file
|
||||||
|
Export(FileBrowserCommand),
|
||||||
|
/// Update the contents of the clip pool
|
||||||
|
Clip(PoolClipCommand),
|
||||||
|
}
|
||||||
|
edn_command!(PoolCommand: |state: MidiPool| {
|
||||||
|
("show" [a: bool] Self::Show(a.expect("no flag")))
|
||||||
|
("select" [i: usize] Self::Select(i.expect("no index")))
|
||||||
|
("rename" [a, ..b] ClipRenameCommand::from_edn(state, &a.to_ref(), b).map(Self::Rename).expect("invalid command"))
|
||||||
|
("length" [a, ..b] ClipLengthCommand::from_edn(state, &a.to_ref(), b).map(Self::Length).expect("invalid command"))
|
||||||
|
("import" [a, ..b] FileBrowserCommand::from_edn(state, &a.to_ref(), b).map(Self::Import).expect("invalid command"))
|
||||||
|
("export" [a, ..b] FileBrowserCommand::from_edn(state, &a.to_ref(), b).map(Self::Export).expect("invalid command"))
|
||||||
|
("clip" [a, ..b] PoolClipCommand::from_edn(state, &a.to_ref(), b).map(Self::Clip).expect("invalid command"))
|
||||||
|
});
|
||||||
|
command!(|self: PoolCommand, state: MidiPool|{
|
||||||
|
use PoolCommand::*;
|
||||||
|
match self {
|
||||||
|
Rename(ClipRenameCommand::Begin) => { state.begin_clip_rename(); None }
|
||||||
|
Rename(command) => command.delegate(state, Rename)?,
|
||||||
|
Length(ClipLengthCommand::Begin) => { state.begin_clip_length(); None },
|
||||||
|
Length(command) => command.delegate(state, Length)?,
|
||||||
|
Import(FileBrowserCommand::Begin) => { state.begin_import()?; None },
|
||||||
|
Import(command) => command.delegate(state, Import)?,
|
||||||
|
Export(FileBrowserCommand::Begin) => { state.begin_export()?; None },
|
||||||
|
Export(command) => command.delegate(state, Export)?,
|
||||||
|
Clip(command) => command.execute(state)?.map(Clip),
|
||||||
|
Show(visible) => { state.visible = visible; Some(Self::Show(!visible)) },
|
||||||
|
Select(clip) => { state.set_clip_index(clip); None },
|
||||||
|
}
|
||||||
|
});
|
||||||
|
#[derive(Clone, Debug, PartialEq)] pub enum PoolClipCommand {
|
||||||
|
Add(usize, MidiClip),
|
||||||
|
Delete(usize),
|
||||||
|
Swap(usize, usize),
|
||||||
|
Import(usize, PathBuf),
|
||||||
|
Export(usize, PathBuf),
|
||||||
|
SetName(usize, Arc<str>),
|
||||||
|
SetLength(usize, usize),
|
||||||
|
SetColor(usize, ItemColor),
|
||||||
|
}
|
||||||
|
edn_command!(PoolClipCommand: |state: MidiPool| {
|
||||||
|
("add" [i: usize, c: MidiClip] Self::Add(i.expect("no index"), c.expect("no clip")))
|
||||||
|
("delete" [i: usize] Self::Delete(i.expect("no index")))
|
||||||
|
("swap" [a: usize, b: usize] Self::Swap(a.expect("no index"), b.expect("no index")))
|
||||||
|
("import" [i: usize, p: PathBuf] Self::Import(i.expect("no index"), p.expect("no path")))
|
||||||
|
("export" [i: usize, p: PathBuf] Self::Export(i.expect("no index"), p.expect("no path")))
|
||||||
|
("set-name" [i: usize, n: Arc<str>] Self::SetName(i.expect("no index"), n.expect("no name")))
|
||||||
|
("set-length" [i: usize, l: usize] Self::SetLength(i.expect("no index"), l.expect("no length")))
|
||||||
|
("set-color" [i: usize, c: ItemColor] Self::SetColor(i.expect("no index"), c.expect("no color")))
|
||||||
|
});
|
||||||
|
impl<T: HasClips> Command<T> for PoolClipCommand {
|
||||||
|
fn execute (self, model: &mut T) -> Perhaps<Self> {
|
||||||
|
use PoolClipCommand::*;
|
||||||
|
Ok(match self {
|
||||||
|
Add(mut index, clip) => {
|
||||||
|
let clip = Arc::new(RwLock::new(clip));
|
||||||
|
let mut clips = model.clips_mut();
|
||||||
|
if index >= clips.len() {
|
||||||
|
index = clips.len();
|
||||||
|
clips.push(clip)
|
||||||
|
} else {
|
||||||
|
clips.insert(index, clip);
|
||||||
|
}
|
||||||
|
Some(Self::Delete(index))
|
||||||
|
},
|
||||||
|
Delete(index) => {
|
||||||
|
let clip = model.clips_mut().remove(index).read().unwrap().clone();
|
||||||
|
Some(Self::Add(index, clip))
|
||||||
|
},
|
||||||
|
Swap(index, other) => {
|
||||||
|
model.clips_mut().swap(index, other);
|
||||||
|
Some(Self::Swap(index, other))
|
||||||
|
},
|
||||||
|
Import(index, path) => {
|
||||||
|
let bytes = std::fs::read(&path)?;
|
||||||
|
let smf = Smf::parse(bytes.as_slice())?;
|
||||||
|
let mut t = 0u32;
|
||||||
|
let mut events = vec![];
|
||||||
|
for track in smf.tracks.iter() {
|
||||||
|
for event in track.iter() {
|
||||||
|
t += event.delta.as_int();
|
||||||
|
if let TrackEventKind::Midi { channel, message } = event.kind {
|
||||||
|
events.push((t, channel.as_int(), message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None);
|
||||||
|
for event in events.iter() {
|
||||||
|
clip.notes[event.0 as usize].push(event.2);
|
||||||
|
}
|
||||||
|
Self::Add(index, clip).execute(model)?
|
||||||
|
},
|
||||||
|
Export(_index, _path) => {
|
||||||
|
todo!("export clip to midi file");
|
||||||
|
},
|
||||||
|
SetName(index, name) => {
|
||||||
|
let clip = &mut model.clips_mut()[index];
|
||||||
|
let old_name = clip.read().unwrap().name.clone();
|
||||||
|
clip.write().unwrap().name = name;
|
||||||
|
Some(Self::SetName(index, old_name))
|
||||||
|
},
|
||||||
|
SetLength(index, length) => {
|
||||||
|
let clip = &mut model.clips_mut()[index];
|
||||||
|
let old_len = clip.read().unwrap().length;
|
||||||
|
clip.write().unwrap().length = length;
|
||||||
|
Some(Self::SetLength(index, old_len))
|
||||||
|
},
|
||||||
|
SetColor(index, color) => {
|
||||||
|
let mut color = ItemPalette::from(color);
|
||||||
|
std::mem::swap(&mut color, &mut model.clips()[index].write().unwrap().color);
|
||||||
|
Some(Self::SetColor(index, color.base))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug, PartialEq)] pub enum ClipRenameCommand {
|
||||||
|
Begin,
|
||||||
|
Cancel,
|
||||||
|
Confirm,
|
||||||
|
Set(Arc<str>),
|
||||||
|
}
|
||||||
|
edn_command!(ClipRenameCommand: |state: MidiPool| {
|
||||||
|
("begin" [] Self::Begin)
|
||||||
|
("cancel" [] Self::Cancel)
|
||||||
|
("confirm" [] Self::Confirm)
|
||||||
|
("set" [n: Arc<str>] Self::Set(n.expect("no name")))
|
||||||
|
});
|
||||||
|
command!(|self: ClipRenameCommand, state: MidiPool|{
|
||||||
|
use ClipRenameCommand::*;
|
||||||
|
if let Some(
|
||||||
|
PoolMode::Rename(clip, ref mut old_name)
|
||||||
|
) = state.mode_mut().clone() {
|
||||||
|
match self {
|
||||||
|
Set(s) => {
|
||||||
|
state.clips()[clip].write().unwrap().name = s;
|
||||||
|
return Ok(Some(Self::Set(old_name.clone().into())))
|
||||||
|
},
|
||||||
|
Confirm => {
|
||||||
|
let old_name = old_name.clone();
|
||||||
|
*state.mode_mut() = None;
|
||||||
|
return Ok(Some(Self::Set(old_name)))
|
||||||
|
},
|
||||||
|
Cancel => {
|
||||||
|
state.clips()[clip].write().unwrap().name = old_name.clone().into();
|
||||||
|
return Ok(None)
|
||||||
|
},
|
||||||
|
_ => unreachable!()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq)] pub enum ClipLengthCommand {
|
||||||
|
Begin,
|
||||||
|
Cancel,
|
||||||
|
Set(usize),
|
||||||
|
Next,
|
||||||
|
Prev,
|
||||||
|
Inc,
|
||||||
|
Dec,
|
||||||
|
}
|
||||||
|
edn_command!(ClipLengthCommand: |state: MidiPool| {
|
||||||
|
("begin" [] Self::Begin)
|
||||||
|
("cancel" [] Self::Cancel)
|
||||||
|
("next" [] Self::Next)
|
||||||
|
("prev" [] Self::Prev)
|
||||||
|
("inc" [] Self::Inc)
|
||||||
|
("dec" [] Self::Dec)
|
||||||
|
("set" [l: usize] Self::Set(l.expect("no length")))
|
||||||
|
});
|
||||||
|
command!(|self: ClipLengthCommand, state: MidiPool|{
|
||||||
|
use ClipLengthCommand::*;
|
||||||
|
use ClipLengthFocus::*;
|
||||||
|
if let Some(
|
||||||
|
PoolMode::Length(clip, ref mut length, ref mut focus)
|
||||||
|
) = state.mode_mut().clone() {
|
||||||
|
match self {
|
||||||
|
Cancel => { *state.mode_mut() = None; },
|
||||||
|
Prev => { focus.prev() },
|
||||||
|
Next => { focus.next() },
|
||||||
|
Inc => match focus {
|
||||||
|
Bar => { *length += 4 * PPQ },
|
||||||
|
Beat => { *length += PPQ },
|
||||||
|
Tick => { *length += 1 },
|
||||||
|
},
|
||||||
|
Dec => match focus {
|
||||||
|
Bar => { *length = length.saturating_sub(4 * PPQ) },
|
||||||
|
Beat => { *length = length.saturating_sub(PPQ) },
|
||||||
|
Tick => { *length = length.saturating_sub(1) },
|
||||||
|
},
|
||||||
|
Set(length) => {
|
||||||
|
let mut old_length = None;
|
||||||
|
{
|
||||||
|
let clip = state.clips()[clip].clone();//.write().unwrap();
|
||||||
|
old_length = Some(clip.read().unwrap().length);
|
||||||
|
clip.write().unwrap().length = length;
|
||||||
|
}
|
||||||
|
*state.mode_mut() = None;
|
||||||
|
return Ok(old_length.map(Self::Set))
|
||||||
|
},
|
||||||
|
_ => unreachable!()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
None
|
||||||
|
});
|
||||||
|
edn_command!(FileBrowserCommand: |state: MidiPool| {
|
||||||
|
("begin" [] Self::Begin)
|
||||||
|
("cancel" [] Self::Cancel)
|
||||||
|
("confirm" [] Self::Confirm)
|
||||||
|
("select" [i: usize] Self::Select(i.expect("no index")))
|
||||||
|
("chdir" [p: PathBuf] Self::Chdir(p.expect("no path")))
|
||||||
|
("filter" [f: Arc<str>] Self::Filter(f.expect("no filter")))
|
||||||
|
});
|
||||||
|
command!(|self: FileBrowserCommand, state: MidiPool|{
|
||||||
|
use PoolMode::*;
|
||||||
|
use FileBrowserCommand::*;
|
||||||
|
let mode = &mut state.mode;
|
||||||
|
match mode {
|
||||||
|
Some(Import(index, ref mut browser)) => match self {
|
||||||
|
Cancel => { *mode = None; },
|
||||||
|
Chdir(cwd) => { *mode = Some(Import(*index, FileBrowser::new(Some(cwd))?)); },
|
||||||
|
Select(index) => { browser.index = index; },
|
||||||
|
Confirm => if browser.is_file() {
|
||||||
|
let index = *index;
|
||||||
|
let path = browser.path();
|
||||||
|
*mode = None;
|
||||||
|
PoolClipCommand::Import(index, path).execute(state)?;
|
||||||
|
} else if browser.is_dir() {
|
||||||
|
*mode = Some(Import(*index, browser.chdir()?));
|
||||||
|
},
|
||||||
|
_ => todo!(),
|
||||||
|
},
|
||||||
|
Some(Export(index, ref mut browser)) => match self {
|
||||||
|
Cancel => { *mode = None; },
|
||||||
|
Chdir(cwd) => { *mode = Some(Export(*index, FileBrowser::new(Some(cwd))?)); },
|
||||||
|
Select(index) => { browser.index = index; },
|
||||||
|
_ => unreachable!()
|
||||||
|
},
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
None
|
||||||
|
});
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
input_to_command!(FileBrowserCommand: |state: MidiPool, input: Event|{
|
||||||
|
use FileBrowserCommand::*;
|
||||||
|
use KeyCode::{Up, Down, Left, Right, Enter, Esc, Backspace, Char};
|
||||||
|
if let Some(PoolMode::Import(_index, browser)) = &state.mode {
|
||||||
|
match input {
|
||||||
|
kpat!(Up) => Select(browser.index.overflowing_sub(1).0.min(browser.len().saturating_sub(1))),
|
||||||
|
kpat!(Down) => Select(browser.index.saturating_add(1)% browser.len()),
|
||||||
|
kpat!(Right) => Chdir(browser.cwd.clone()),
|
||||||
|
kpat!(Left) => Chdir(browser.cwd.clone()),
|
||||||
|
|
||||||
|
kpat!(Enter) => Confirm,
|
||||||
|
kpat!(Char(_)) => { todo!() },
|
||||||
|
kpat!(Backspace) => { todo!() },
|
||||||
|
kpat!(Esc) => Cancel,
|
||||||
|
_ => return None
|
||||||
|
}
|
||||||
|
} else if let Some(PoolMode::Export(_index, browser)) = &state.mode {
|
||||||
|
match input {
|
||||||
|
kpat!(Up) => Select(browser.index.overflowing_sub(1).0.min(browser.len())),
|
||||||
|
kpat!(Down) => Select(browser.index.saturating_add(1) % browser.len()),
|
||||||
|
kpat!(Right) => Chdir(browser.cwd.clone()),
|
||||||
|
kpat!(Left) => Chdir(browser.cwd.clone()),
|
||||||
|
|
||||||
|
kpat!(Enter) => Confirm,
|
||||||
|
kpat!(Char(_)) => { todo!() },
|
||||||
|
kpat!(Backspace) => { todo!() },
|
||||||
|
kpat!(Esc) => Cancel,
|
||||||
|
_ => return None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
input_to_command!(ClipLengthCommand: |state: MidiPool, input: Event|{
|
||||||
|
if let Some(PoolMode::Length(_, length, _)) = state.mode() {
|
||||||
|
match input {
|
||||||
|
kpat!(Up) => Self::Inc,
|
||||||
|
kpat!(Down) => Self::Dec,
|
||||||
|
kpat!(Right) => Self::Next,
|
||||||
|
kpat!(Left) => Self::Prev,
|
||||||
|
kpat!(Enter) => Self::Set(*length),
|
||||||
|
kpat!(Esc) => Self::Cancel,
|
||||||
|
_ => return None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
impl InputToCommand<Event, MidiPool> for ClipRenameCommand {
|
||||||
|
fn input_to_command (state: &MidiPool, input: &Event) -> Option<Self> {
|
||||||
|
use KeyCode::{Char, Backspace, Enter, Esc};
|
||||||
|
if let Some(PoolMode::Rename(_, ref old_name)) = state.mode() {
|
||||||
|
Some(match input {
|
||||||
|
kpat!(Char(c)) => {
|
||||||
|
let mut new_name = old_name.clone().to_string();
|
||||||
|
new_name.push(*c);
|
||||||
|
Self::Set(new_name.into())
|
||||||
|
},
|
||||||
|
kpat!(Backspace) => {
|
||||||
|
let mut new_name = old_name.clone().to_string();
|
||||||
|
new_name.pop();
|
||||||
|
Self::Set(new_name.into())
|
||||||
|
},
|
||||||
|
kpat!(Enter) => Self::Confirm,
|
||||||
|
kpat!(Esc) => Self::Cancel,
|
||||||
|
_ => return None
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//fn to_clips_command (state: &MidiPool, input: &Event) -> Option<PoolCommand> {
|
||||||
|
//use KeyCode::{Up, Down, Delete, Char};
|
||||||
|
//use PoolCommand as Cmd;
|
||||||
|
//let index = state.clip_index();
|
||||||
|
//let count = state.clips().len();
|
||||||
|
//Some(match input {
|
||||||
|
//kpat!(Char('n')) => Cmd::Rename(ClipRenameCommand::Begin),
|
||||||
|
//kpat!(Char('t')) => Cmd::Length(ClipLengthCommand::Begin),
|
||||||
|
//kpat!(Char('m')) => Cmd::Import(FileBrowserCommand::Begin),
|
||||||
|
//kpat!(Char('x')) => Cmd::Export(FileBrowserCommand::Begin),
|
||||||
|
//kpat!(Char('c')) => Cmd::Clip(PoolClipCommand::SetColor(index, ItemColor::random())),
|
||||||
|
//kpat!(Char('[')) | kpat!(Up) => Cmd::Select(
|
||||||
|
//index.overflowing_sub(1).0.min(state.clips().len() - 1)
|
||||||
|
//),
|
||||||
|
//kpat!(Char(']')) | kpat!(Down) => Cmd::Select(
|
||||||
|
//index.saturating_add(1) % state.clips().len()
|
||||||
|
//),
|
||||||
|
//kpat!(Char('<')) => if index > 1 {
|
||||||
|
//state.set_clip_index(state.clip_index().saturating_sub(1));
|
||||||
|
//Cmd::Clip(PoolClipCommand::Swap(index - 1, index))
|
||||||
|
//} else {
|
||||||
|
//return None
|
||||||
|
//},
|
||||||
|
//kpat!(Char('>')) => if index < count.saturating_sub(1) {
|
||||||
|
//state.set_clip_index(state.clip_index() + 1);
|
||||||
|
//Cmd::Clip(PoolClipCommand::Swap(index + 1, index))
|
||||||
|
//} else {
|
||||||
|
//return None
|
||||||
|
//},
|
||||||
|
//kpat!(Delete) => if index > 0 {
|
||||||
|
//state.set_clip_index(index.min(count.saturating_sub(1)));
|
||||||
|
//Cmd::Clip(PoolClipCommand::Delete(index))
|
||||||
|
//} else {
|
||||||
|
//return None
|
||||||
|
//},
|
||||||
|
//kpat!(Char('a')) | kpat!(Shift-Char('A')) => Cmd::Clip(PoolClipCommand::Add(count, MidiClip::new(
|
||||||
|
//"Clip", true, 4 * PPQ, None, Some(ItemPalette::random())
|
||||||
|
//))),
|
||||||
|
//kpat!(Char('i')) => Cmd::Clip(PoolClipCommand::Add(index + 1, MidiClip::new(
|
||||||
|
//"Clip", true, 4 * PPQ, None, Some(ItemPalette::random())
|
||||||
|
//))),
|
||||||
|
//kpat!(Char('d')) | kpat!(Shift-Char('D')) => {
|
||||||
|
//let mut clip = state.clips()[index].read().unwrap().duplicate();
|
||||||
|
//clip.color = ItemPalette::random_near(clip.color, 0.25);
|
||||||
|
//Cmd::Clip(PoolClipCommand::Add(index + 1, clip))
|
||||||
|
//},
|
||||||
|
//_ => return None
|
||||||
|
//})
|
||||||
|
//}
|
||||||
|
|
|
||||||
|
|
@ -1,424 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
use KeyCode::*;
|
|
||||||
impl PoolCommand {
|
|
||||||
const KEYS_POOL: &str = include_str!("midi_pool_keys.edn");
|
|
||||||
const KEYS_CLIP: &str = include_str!("midi_pool_keys_clip.edn");
|
|
||||||
const KEYS_RENAME: &str = include_str!("midi_pool_keys_rename.edn");
|
|
||||||
const KEYS_LENGTH: &str = include_str!("midi_pool_keys_length.edn");
|
|
||||||
const KEYS_FILE: &str = include_str!("midi_pool_keys_file.edn");
|
|
||||||
pub fn from_tui_event (state: &MidiPool, input: &Event) -> Usually<Option<Self>> {
|
|
||||||
use EdnItem::*;
|
|
||||||
let edns: Vec<EdnItem<&str>> = EdnItem::read_all(match state.mode() {
|
|
||||||
Some(PoolMode::Rename(..)) => Self::KEYS_RENAME,
|
|
||||||
Some(PoolMode::Length(..)) => Self::KEYS_LENGTH,
|
|
||||||
Some(PoolMode::Import(..)) | Some(PoolMode::Export(..)) => Self::KEYS_FILE,
|
|
||||||
_ => Self::KEYS_CLIP
|
|
||||||
})?;
|
|
||||||
for item in edns {
|
|
||||||
match item {
|
|
||||||
Exp(e) => match e.as_slice() {
|
|
||||||
[Sym(key), Key(command), args @ ..] => {
|
|
||||||
}
|
|
||||||
_ => panic!("invalid config")
|
|
||||||
}
|
|
||||||
_ => panic!("invalid config")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
edn_provide!(bool: |self: MidiPool| {});
|
|
||||||
edn_provide!(usize: |self: MidiPool| {});
|
|
||||||
edn_provide!(MidiClip: |self: MidiPool| {});
|
|
||||||
edn_provide!(PathBuf: |self: MidiPool| {});
|
|
||||||
edn_provide!(Arc<str>: |self: MidiPool| {});
|
|
||||||
edn_provide!(ItemColor: |self: MidiPool| {});
|
|
||||||
#[derive(Clone, PartialEq, Debug)] pub enum PoolCommand {
|
|
||||||
/// Toggle visibility of pool
|
|
||||||
Show(bool),
|
|
||||||
/// Select a clip from the clip pool
|
|
||||||
Select(usize),
|
|
||||||
/// Rename a clip
|
|
||||||
Rename(ClipRenameCommand),
|
|
||||||
/// Change the length of a clip
|
|
||||||
Length(ClipLengthCommand),
|
|
||||||
/// Import from file
|
|
||||||
Import(FileBrowserCommand),
|
|
||||||
/// Export to file
|
|
||||||
Export(FileBrowserCommand),
|
|
||||||
/// Update the contents of the clip pool
|
|
||||||
Clip(PoolClipCommand),
|
|
||||||
}
|
|
||||||
edn_command!(PoolCommand: |state: MidiPool| {
|
|
||||||
("show" [a: bool] Self::Show(a.expect("no flag")))
|
|
||||||
("select" [i: usize] Self::Select(i.expect("no index")))
|
|
||||||
("rename" [a, ..b] Self::Rename(ClipRenameCommand::from_edn(state, &a.to_ref(), b)))
|
|
||||||
("length" [a, ..b] Self::Length(ClipLengthCommand::from_edn(state, &a.to_ref(), b)))
|
|
||||||
("import" [a, ..b] Self::Import(FileBrowserCommand::from_edn(state, &a.to_ref(), b)))
|
|
||||||
("export" [a, ..b] Self::Export(FileBrowserCommand::from_edn(state, &a.to_ref(), b)))
|
|
||||||
("clip" [a, ..b] Self::Clip(PoolClipCommand::from_edn(state, &a.to_ref(), b)))
|
|
||||||
});
|
|
||||||
command!(|self: PoolCommand, state: MidiPool|{
|
|
||||||
use PoolCommand::*;
|
|
||||||
match self {
|
|
||||||
Rename(ClipRenameCommand::Begin) => { state.begin_clip_rename(); None }
|
|
||||||
Rename(command) => command.delegate(state, Rename)?,
|
|
||||||
Length(ClipLengthCommand::Begin) => { state.begin_clip_length(); None },
|
|
||||||
Length(command) => command.delegate(state, Length)?,
|
|
||||||
Import(FileBrowserCommand::Begin) => { state.begin_import()?; None },
|
|
||||||
Import(command) => command.delegate(state, Import)?,
|
|
||||||
Export(FileBrowserCommand::Begin) => { state.begin_export()?; None },
|
|
||||||
Export(command) => command.delegate(state, Export)?,
|
|
||||||
Clip(command) => command.execute(state)?.map(Clip),
|
|
||||||
Show(visible) => { state.visible = visible; Some(Self::Show(!visible)) },
|
|
||||||
Select(clip) => { state.set_clip_index(clip); None },
|
|
||||||
}
|
|
||||||
});
|
|
||||||
#[derive(Clone, Debug, PartialEq)] pub enum PoolClipCommand {
|
|
||||||
Add(usize, MidiClip),
|
|
||||||
Delete(usize),
|
|
||||||
Swap(usize, usize),
|
|
||||||
Import(usize, PathBuf),
|
|
||||||
Export(usize, PathBuf),
|
|
||||||
SetName(usize, Arc<str>),
|
|
||||||
SetLength(usize, usize),
|
|
||||||
SetColor(usize, ItemColor),
|
|
||||||
}
|
|
||||||
edn_command!(PoolClipCommand: |state: MidiPool| {
|
|
||||||
("add" [i: usize, c: MidiClip] Self::Add(i.expect("no index"), c.expect("no clip")))
|
|
||||||
("delete" [i: usize] Self::Delete(i.expect("no index")))
|
|
||||||
("swap" [a: usize, b: usize] Self::Swap(a.expect("no index"), b.expect("no index")))
|
|
||||||
("import" [i: usize, p: PathBuf] Self::Import(i.expect("no index"), p.expect("no path")))
|
|
||||||
("export" [i: usize, p: PathBuf] Self::Export(i.expect("no index"), p.expect("no path")))
|
|
||||||
("set-name" [i: usize, n: Arc<str>] Self::SetName(i.expect("no index"), n.expect("no name")))
|
|
||||||
("set-length" [i: usize, l: usize] Self::SetLength(i.expect("no index"), l.expect("no length")))
|
|
||||||
("set-color" [i: usize, c: ItemColor] Self::SetColor(i.expect("no index"), c.expect("no color")))
|
|
||||||
});
|
|
||||||
impl<T: HasClips> Command<T> for PoolClipCommand {
|
|
||||||
fn execute (self, model: &mut T) -> Perhaps<Self> {
|
|
||||||
use PoolClipCommand::*;
|
|
||||||
Ok(match self {
|
|
||||||
Add(mut index, clip) => {
|
|
||||||
let clip = Arc::new(RwLock::new(clip));
|
|
||||||
let mut clips = model.clips_mut();
|
|
||||||
if index >= clips.len() {
|
|
||||||
index = clips.len();
|
|
||||||
clips.push(clip)
|
|
||||||
} else {
|
|
||||||
clips.insert(index, clip);
|
|
||||||
}
|
|
||||||
Some(Self::Delete(index))
|
|
||||||
},
|
|
||||||
Delete(index) => {
|
|
||||||
let clip = model.clips_mut().remove(index).read().unwrap().clone();
|
|
||||||
Some(Self::Add(index, clip))
|
|
||||||
},
|
|
||||||
Swap(index, other) => {
|
|
||||||
model.clips_mut().swap(index, other);
|
|
||||||
Some(Self::Swap(index, other))
|
|
||||||
},
|
|
||||||
Import(index, path) => {
|
|
||||||
let bytes = std::fs::read(&path)?;
|
|
||||||
let smf = Smf::parse(bytes.as_slice())?;
|
|
||||||
let mut t = 0u32;
|
|
||||||
let mut events = vec![];
|
|
||||||
for track in smf.tracks.iter() {
|
|
||||||
for event in track.iter() {
|
|
||||||
t += event.delta.as_int();
|
|
||||||
if let TrackEventKind::Midi { channel, message } = event.kind {
|
|
||||||
events.push((t, channel.as_int(), message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None);
|
|
||||||
for event in events.iter() {
|
|
||||||
clip.notes[event.0 as usize].push(event.2);
|
|
||||||
}
|
|
||||||
Self::Add(index, clip).execute(model)?
|
|
||||||
},
|
|
||||||
Export(_index, _path) => {
|
|
||||||
todo!("export clip to midi file");
|
|
||||||
},
|
|
||||||
SetName(index, name) => {
|
|
||||||
let clip = &mut model.clips_mut()[index];
|
|
||||||
let old_name = clip.read().unwrap().name.clone();
|
|
||||||
clip.write().unwrap().name = name;
|
|
||||||
Some(Self::SetName(index, old_name))
|
|
||||||
},
|
|
||||||
SetLength(index, length) => {
|
|
||||||
let clip = &mut model.clips_mut()[index];
|
|
||||||
let old_len = clip.read().unwrap().length;
|
|
||||||
clip.write().unwrap().length = length;
|
|
||||||
Some(Self::SetLength(index, old_len))
|
|
||||||
},
|
|
||||||
SetColor(index, color) => {
|
|
||||||
let mut color = ItemPalette::from(color);
|
|
||||||
std::mem::swap(&mut color, &mut model.clips()[index].write().unwrap().color);
|
|
||||||
Some(Self::SetColor(index, color.base))
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[derive(Clone, Debug, PartialEq)] pub enum ClipRenameCommand {
|
|
||||||
Begin,
|
|
||||||
Cancel,
|
|
||||||
Confirm,
|
|
||||||
Set(Arc<str>),
|
|
||||||
}
|
|
||||||
edn_command!(ClipRenameCommand: |state: MidiPool| {
|
|
||||||
("begin" [] Self::Begin)
|
|
||||||
("cancel" [] Self::Cancel)
|
|
||||||
("confirm" [] Self::Confirm)
|
|
||||||
("set" [n: Arc<str>] Self::Set(n.expect("no name")))
|
|
||||||
});
|
|
||||||
command!(|self: ClipRenameCommand, state: MidiPool|{
|
|
||||||
use ClipRenameCommand::*;
|
|
||||||
if let Some(
|
|
||||||
PoolMode::Rename(clip, ref mut old_name)
|
|
||||||
) = state.mode_mut().clone() {
|
|
||||||
match self {
|
|
||||||
Set(s) => {
|
|
||||||
state.clips()[clip].write().unwrap().name = s;
|
|
||||||
return Ok(Some(Self::Set(old_name.clone().into())))
|
|
||||||
},
|
|
||||||
Confirm => {
|
|
||||||
let old_name = old_name.clone();
|
|
||||||
*state.mode_mut() = None;
|
|
||||||
return Ok(Some(Self::Set(old_name)))
|
|
||||||
},
|
|
||||||
Cancel => {
|
|
||||||
state.clips()[clip].write().unwrap().name = old_name.clone().into();
|
|
||||||
return Ok(None)
|
|
||||||
},
|
|
||||||
_ => unreachable!()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
unreachable!()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq)] pub enum ClipLengthCommand {
|
|
||||||
Begin,
|
|
||||||
Cancel,
|
|
||||||
Set(usize),
|
|
||||||
Next,
|
|
||||||
Prev,
|
|
||||||
Inc,
|
|
||||||
Dec,
|
|
||||||
}
|
|
||||||
edn_command!(ClipLengthCommand: |state: MidiPool| {
|
|
||||||
("begin" [] Self::Begin)
|
|
||||||
("cancel" [] Self::Cancel)
|
|
||||||
("next" [] Self::Next)
|
|
||||||
("prev" [] Self::Prev)
|
|
||||||
("inc" [] Self::Inc)
|
|
||||||
("dec" [] Self::Dec)
|
|
||||||
("set" [l: usize] Self::Set(l.expect("no length")))
|
|
||||||
});
|
|
||||||
command!(|self: ClipLengthCommand, state: MidiPool|{
|
|
||||||
use ClipLengthCommand::*;
|
|
||||||
use ClipLengthFocus::*;
|
|
||||||
if let Some(
|
|
||||||
PoolMode::Length(clip, ref mut length, ref mut focus)
|
|
||||||
) = state.mode_mut().clone() {
|
|
||||||
match self {
|
|
||||||
Cancel => { *state.mode_mut() = None; },
|
|
||||||
Prev => { focus.prev() },
|
|
||||||
Next => { focus.next() },
|
|
||||||
Inc => match focus {
|
|
||||||
Bar => { *length += 4 * PPQ },
|
|
||||||
Beat => { *length += PPQ },
|
|
||||||
Tick => { *length += 1 },
|
|
||||||
},
|
|
||||||
Dec => match focus {
|
|
||||||
Bar => { *length = length.saturating_sub(4 * PPQ) },
|
|
||||||
Beat => { *length = length.saturating_sub(PPQ) },
|
|
||||||
Tick => { *length = length.saturating_sub(1) },
|
|
||||||
},
|
|
||||||
Set(length) => {
|
|
||||||
let mut old_length = None;
|
|
||||||
{
|
|
||||||
let clip = state.clips()[clip].clone();//.write().unwrap();
|
|
||||||
old_length = Some(clip.read().unwrap().length);
|
|
||||||
clip.write().unwrap().length = length;
|
|
||||||
}
|
|
||||||
*state.mode_mut() = None;
|
|
||||||
return Ok(old_length.map(Self::Set))
|
|
||||||
},
|
|
||||||
_ => unreachable!()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
unreachable!();
|
|
||||||
}
|
|
||||||
None
|
|
||||||
});
|
|
||||||
edn_command!(FileBrowserCommand: |state: MidiPool| {
|
|
||||||
("begin" [] Self::Begin)
|
|
||||||
("cancel" [] Self::Cancel)
|
|
||||||
("confirm" [] Self::Confirm)
|
|
||||||
("select" [i: usize] Self::Select(i.expect("no index")))
|
|
||||||
("chdir" [p: PathBuf] Self::Chdir(p.expect("no path")))
|
|
||||||
("filter" [f: Arc<str>] Self::Filter(f.expect("no filter")))
|
|
||||||
});
|
|
||||||
command!(|self: FileBrowserCommand, state: MidiPool|{
|
|
||||||
use PoolMode::*;
|
|
||||||
use FileBrowserCommand::*;
|
|
||||||
let mode = &mut state.mode;
|
|
||||||
match mode {
|
|
||||||
Some(Import(index, ref mut browser)) => match self {
|
|
||||||
Cancel => { *mode = None; },
|
|
||||||
Chdir(cwd) => { *mode = Some(Import(*index, FileBrowser::new(Some(cwd))?)); },
|
|
||||||
Select(index) => { browser.index = index; },
|
|
||||||
Confirm => if browser.is_file() {
|
|
||||||
let index = *index;
|
|
||||||
let path = browser.path();
|
|
||||||
*mode = None;
|
|
||||||
PoolClipCommand::Import(index, path).execute(state)?;
|
|
||||||
} else if browser.is_dir() {
|
|
||||||
*mode = Some(Import(*index, browser.chdir()?));
|
|
||||||
},
|
|
||||||
_ => todo!(),
|
|
||||||
},
|
|
||||||
Some(Export(index, ref mut browser)) => match self {
|
|
||||||
Cancel => { *mode = None; },
|
|
||||||
Chdir(cwd) => { *mode = Some(Export(*index, FileBrowser::new(Some(cwd))?)); },
|
|
||||||
Select(index) => { browser.index = index; },
|
|
||||||
_ => unreachable!()
|
|
||||||
},
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
None
|
|
||||||
});
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
input_to_command!(PoolCommand: |state: MidiPool, input: Event|match state.mode() {
|
|
||||||
Some(PoolMode::Rename(..)) => Self::Rename(ClipRenameCommand::input_to_command(state, input)?),
|
|
||||||
Some(PoolMode::Length(..)) => Self::Length(ClipLengthCommand::input_to_command(state, input)?),
|
|
||||||
Some(PoolMode::Import(..)) => Self::Import(FileBrowserCommand::input_to_command(state, input)?),
|
|
||||||
Some(PoolMode::Export(..)) => Self::Export(FileBrowserCommand::input_to_command(state, input)?),
|
|
||||||
_ => to_clips_command(state, input)?
|
|
||||||
});
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
fn to_clips_command (state: &MidiPool, input: &Event) -> Option<PoolCommand> {
|
|
||||||
use KeyCode::{Up, Down, Delete, Char};
|
|
||||||
use PoolCommand as Cmd;
|
|
||||||
let index = state.clip_index();
|
|
||||||
let count = state.clips().len();
|
|
||||||
Some(match input {
|
|
||||||
kpat!(Char('n')) => Cmd::Rename(ClipRenameCommand::Begin),
|
|
||||||
kpat!(Char('t')) => Cmd::Length(ClipLengthCommand::Begin),
|
|
||||||
kpat!(Char('m')) => Cmd::Import(FileBrowserCommand::Begin),
|
|
||||||
kpat!(Char('x')) => Cmd::Export(FileBrowserCommand::Begin),
|
|
||||||
kpat!(Char('c')) => Cmd::Clip(PoolClipCommand::SetColor(index, ItemColor::random())),
|
|
||||||
kpat!(Char('[')) | kpat!(Up) => Cmd::Select(
|
|
||||||
index.overflowing_sub(1).0.min(state.clips().len() - 1)
|
|
||||||
),
|
|
||||||
kpat!(Char(']')) | kpat!(Down) => Cmd::Select(
|
|
||||||
index.saturating_add(1) % state.clips().len()
|
|
||||||
),
|
|
||||||
kpat!(Char('<')) => if index > 1 {
|
|
||||||
state.set_clip_index(state.clip_index().saturating_sub(1));
|
|
||||||
Cmd::Clip(PoolClipCommand::Swap(index - 1, index))
|
|
||||||
} else {
|
|
||||||
return None
|
|
||||||
},
|
|
||||||
kpat!(Char('>')) => if index < count.saturating_sub(1) {
|
|
||||||
state.set_clip_index(state.clip_index() + 1);
|
|
||||||
Cmd::Clip(PoolClipCommand::Swap(index + 1, index))
|
|
||||||
} else {
|
|
||||||
return None
|
|
||||||
},
|
|
||||||
kpat!(Delete) => if index > 0 {
|
|
||||||
state.set_clip_index(index.min(count.saturating_sub(1)));
|
|
||||||
Cmd::Clip(PoolClipCommand::Delete(index))
|
|
||||||
} else {
|
|
||||||
return None
|
|
||||||
},
|
|
||||||
kpat!(Char('a')) | kpat!(Shift-Char('A')) => Cmd::Clip(PoolClipCommand::Add(count, MidiClip::new(
|
|
||||||
"Clip", true, 4 * PPQ, None, Some(ItemPalette::random())
|
|
||||||
))),
|
|
||||||
kpat!(Char('i')) => Cmd::Clip(PoolClipCommand::Add(index + 1, MidiClip::new(
|
|
||||||
"Clip", true, 4 * PPQ, None, Some(ItemPalette::random())
|
|
||||||
))),
|
|
||||||
kpat!(Char('d')) | kpat!(Shift-Char('D')) => {
|
|
||||||
let mut clip = state.clips()[index].read().unwrap().duplicate();
|
|
||||||
clip.color = ItemPalette::random_near(clip.color, 0.25);
|
|
||||||
Cmd::Clip(PoolClipCommand::Add(index + 1, clip))
|
|
||||||
},
|
|
||||||
_ => return None
|
|
||||||
})
|
|
||||||
}
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
input_to_command!(FileBrowserCommand: |state: MidiPool, input: Event|{
|
|
||||||
use FileBrowserCommand::*;
|
|
||||||
use KeyCode::{Up, Down, Left, Right, Enter, Esc, Backspace, Char};
|
|
||||||
if let Some(PoolMode::Import(_index, browser)) = &state.mode {
|
|
||||||
match input {
|
|
||||||
kpat!(Up) => Select(browser.index.overflowing_sub(1).0.min(browser.len().saturating_sub(1))),
|
|
||||||
kpat!(Down) => Select(browser.index.saturating_add(1)% browser.len()),
|
|
||||||
kpat!(Right) => Chdir(browser.cwd.clone()),
|
|
||||||
kpat!(Left) => Chdir(browser.cwd.clone()),
|
|
||||||
|
|
||||||
kpat!(Enter) => Confirm,
|
|
||||||
kpat!(Char(_)) => { todo!() },
|
|
||||||
kpat!(Backspace) => { todo!() },
|
|
||||||
kpat!(Esc) => Cancel,
|
|
||||||
_ => return None
|
|
||||||
}
|
|
||||||
} else if let Some(PoolMode::Export(_index, browser)) = &state.mode {
|
|
||||||
match input {
|
|
||||||
kpat!(Up) => Select(browser.index.overflowing_sub(1).0.min(browser.len())),
|
|
||||||
kpat!(Down) => Select(browser.index.saturating_add(1) % browser.len()),
|
|
||||||
kpat!(Right) => Chdir(browser.cwd.clone()),
|
|
||||||
kpat!(Left) => Chdir(browser.cwd.clone()),
|
|
||||||
|
|
||||||
kpat!(Enter) => Confirm,
|
|
||||||
kpat!(Char(_)) => { todo!() },
|
|
||||||
kpat!(Backspace) => { todo!() },
|
|
||||||
kpat!(Esc) => Cancel,
|
|
||||||
_ => return None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
unreachable!()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
input_to_command!(ClipLengthCommand: |state: MidiPool, input: Event|{
|
|
||||||
if let Some(PoolMode::Length(_, length, _)) = state.mode() {
|
|
||||||
match input {
|
|
||||||
kpat!(Up) => Self::Inc,
|
|
||||||
kpat!(Down) => Self::Dec,
|
|
||||||
kpat!(Right) => Self::Next,
|
|
||||||
kpat!(Left) => Self::Prev,
|
|
||||||
kpat!(Enter) => Self::Set(*length),
|
|
||||||
kpat!(Esc) => Self::Cancel,
|
|
||||||
_ => return None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
unreachable!()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
impl InputToCommand<Event, MidiPool> for ClipRenameCommand {
|
|
||||||
fn input_to_command (state: &MidiPool, input: &Event) -> Option<Self> {
|
|
||||||
use KeyCode::{Char, Backspace, Enter, Esc};
|
|
||||||
if let Some(PoolMode::Rename(_, ref old_name)) = state.mode() {
|
|
||||||
Some(match input {
|
|
||||||
kpat!(Char(c)) => {
|
|
||||||
let mut new_name = old_name.clone().to_string();
|
|
||||||
new_name.push(*c);
|
|
||||||
Self::Set(new_name.into())
|
|
||||||
},
|
|
||||||
kpat!(Backspace) => {
|
|
||||||
let mut new_name = old_name.clone().to_string();
|
|
||||||
new_name.pop();
|
|
||||||
Self::Set(new_name.into())
|
|
||||||
},
|
|
||||||
kpat!(Enter) => Self::Confirm,
|
|
||||||
kpat!(Esc) => Self::Cancel,
|
|
||||||
_ => return None
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
unreachable!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
(:n clip/rename/begin)
|
|
||||||
(:t clip/length/begin)
|
|
||||||
(:m clip/import/begin)
|
|
||||||
(:x clip/export/begin)
|
|
||||||
(:c clip/color/random)
|
|
||||||
(:bracket-open clip/select/prev)
|
|
||||||
(:bracket-close clip/select/next)
|
|
||||||
(:lt clip/move/prev)
|
|
||||||
(:gt clip/move/next)
|
|
||||||
(:del clip/delete)
|
|
||||||
(:shift-a clip/add)
|
|
||||||
(:i clip/insert)
|
|
||||||
(:d clip/duplicate)
|
|
||||||
|
|
@ -8,7 +8,7 @@ handle!(TuiIn: |self: SamplerTui, input|SamplerTuiCommand::execute_with_state(se
|
||||||
Sample(SamplerCommand),
|
Sample(SamplerCommand),
|
||||||
}
|
}
|
||||||
impl EdnCommand<SamplerTui> for SamplerTuiCommand {
|
impl EdnCommand<SamplerTui> for SamplerTuiCommand {
|
||||||
fn from_edn <'a> (state: &SamplerTui, head: &EdnItem<&str>, tail: &'a [EdnItem<String>]) -> Self {
|
fn from_edn <'a> (state: &SamplerTui, head: &EdnItem<&str>, tail: &'a [EdnItem<&str>]) -> Option<Self> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -24,7 +24,7 @@ impl EdnCommand<SamplerTui> for SamplerTuiCommand {
|
||||||
NoteOff(u7),
|
NoteOff(u7),
|
||||||
}
|
}
|
||||||
impl EdnCommand<Sampler> for SamplerCommand {
|
impl EdnCommand<Sampler> for SamplerCommand {
|
||||||
fn from_edn <'a> (state: &Sampler, head: &EdnItem<&str>, tail: &'a [EdnItem<String>]) -> Self {
|
fn from_edn <'a> (state: &Sampler, head: &EdnItem<&str>, tail: &'a [EdnItem<&str>]) -> Option<Self> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,3 +41,4 @@
|
||||||
|
|
||||||
(:q enqueue :clip)
|
(:q enqueue :clip)
|
||||||
(:0 enqueue :stop)
|
(:0 enqueue :stop)
|
||||||
|
|
||||||
|
|
@ -120,7 +120,7 @@ impl App {
|
||||||
//}
|
//}
|
||||||
//};
|
//};
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
edn: include_str!("./transport-view.edn").to_string(),
|
edn: include_str!("./view_transport.edn").to_string(),
|
||||||
jack: jack.clone(),
|
jack: jack.clone(),
|
||||||
color: ItemPalette::random(),
|
color: ItemPalette::random(),
|
||||||
clock: Clock::new(jack, bpm),
|
clock: Clock::new(jack, bpm),
|
||||||
|
|
@ -136,7 +136,7 @@ impl App {
|
||||||
let clip = MidiClip::new("Clip", true, 384usize, None, Some(ItemColor::random().into()));
|
let clip = MidiClip::new("Clip", true, 384usize, None, Some(ItemColor::random().into()));
|
||||||
let clip = Arc::new(RwLock::new(clip));
|
let clip = Arc::new(RwLock::new(clip));
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
edn: include_str!("./sequencer-view.edn").to_string(),
|
edn: include_str!("./view_sequencer.edn").to_string(),
|
||||||
pool: Some((&clip).into()),
|
pool: Some((&clip).into()),
|
||||||
editor: Some((&clip).into()),
|
editor: Some((&clip).into()),
|
||||||
editing: false.into(),
|
editing: false.into(),
|
||||||
|
|
@ -160,7 +160,7 @@ impl App {
|
||||||
audio_tos: &[&[PortConnection];2],
|
audio_tos: &[&[PortConnection];2],
|
||||||
) -> Usually<Self> {
|
) -> Usually<Self> {
|
||||||
let app = Self {
|
let app = Self {
|
||||||
edn: include_str!("./groovebox-view.edn").to_string(),
|
edn: include_str!("./view_groovebox.edn").to_string(),
|
||||||
sampler: Some(Sampler::new(
|
sampler: Some(Sampler::new(
|
||||||
jack,
|
jack,
|
||||||
&"sampler",
|
&"sampler",
|
||||||
|
|
@ -187,7 +187,7 @@ impl App {
|
||||||
track_width: usize,
|
track_width: usize,
|
||||||
) -> Usually<Self> {
|
) -> Usually<Self> {
|
||||||
let mut arranger = Self {
|
let mut arranger = Self {
|
||||||
edn: include_str!("./arranger-view.edn").to_string(),
|
edn: include_str!("./view_arranger.edn").to_string(),
|
||||||
..Self::groovebox(jack, bpm, midi_froms, midi_tos, audio_froms, audio_tos)?
|
..Self::groovebox(jack, bpm, midi_froms, midi_tos, audio_froms, audio_tos)?
|
||||||
};
|
};
|
||||||
arranger.scenes_add(scenes);
|
arranger.scenes_add(scenes);
|
||||||
|
|
@ -373,8 +373,36 @@ impl App {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
const KEYS_APP: &str = include_str!("keys.edn");
|
||||||
|
const KEYS_CLIP: &str = include_str!("keys_clip.edn");
|
||||||
|
const KEYS_TRACK: &str = include_str!("keys_track.edn");
|
||||||
|
const KEYS_SCENE: &str = include_str!("keys_scene.edn");
|
||||||
}
|
}
|
||||||
handle!(TuiIn: |self: App, input| Ok(None));
|
handle!(TuiIn: |self: App, input|{
|
||||||
|
use EdnItem::*;
|
||||||
|
let mut command: Option<AppCommand> = None;
|
||||||
|
let edns: Vec<EdnItem<&str>> = EdnItem::read_all(Self::KEYS_APP)?;
|
||||||
|
for item in edns {
|
||||||
|
match item {
|
||||||
|
Exp(e) => match e.as_slice() {
|
||||||
|
[Sym(key), c, args @ ..] if input.matches_edn(key) => {
|
||||||
|
command = AppCommand::from_edn(self, c, args);
|
||||||
|
if command.is_some() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
_ => panic!("invalid config")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(if let Some(command) = command {
|
||||||
|
let _undo = command.execute(self)?;
|
||||||
|
Some(true)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
|
});
|
||||||
#[derive(Clone, Debug)] pub enum AppCommand {
|
#[derive(Clone, Debug)] pub enum AppCommand {
|
||||||
Clip(ClipCommand),
|
Clip(ClipCommand),
|
||||||
Clock(ClockCommand),
|
Clock(ClockCommand),
|
||||||
|
|
@ -392,20 +420,28 @@ handle!(TuiIn: |self: App, input| Ok(None));
|
||||||
Zoom(Option<usize>),
|
Zoom(Option<usize>),
|
||||||
}
|
}
|
||||||
edn_command!(AppCommand: |state: App| {
|
edn_command!(AppCommand: |state: App| {
|
||||||
("clip" [a, ..b] Self::Clip(ClipCommand::from_edn(state, &a.to_ref(), b)))
|
|
||||||
("clock" [a, ..b] Self::Clock(ClockCommand::from_edn(state, &a.to_ref(), b)))
|
|
||||||
("color" [c: Color] Self::Color(c.map(ItemPalette::from).unwrap_or_default()))
|
("color" [c: Color] Self::Color(c.map(ItemPalette::from).unwrap_or_default()))
|
||||||
("compact" [c: bool ] Self::Compact(c))
|
("compact" [c: bool ] Self::Compact(c))
|
||||||
("editor" [a, ..b] Self::Editor(MidiEditCommand::from_edn(state.editor.as_ref().expect("no editor"), &a.to_ref(), b)))
|
|
||||||
("enqueue" [c: Arc<RwLock<MidiClip>>] Self::Enqueue(c))
|
("enqueue" [c: Arc<RwLock<MidiClip>>] Self::Enqueue(c))
|
||||||
("history" [d: isize] Self::History(d.unwrap_or(0)))
|
("history" [d: isize] Self::History(d.unwrap_or(0)))
|
||||||
("pool" [a, ..b] Self::Pool(PoolCommand::from_edn(state.pool.as_ref().expect("no pool"), &a.to_ref(), b)))
|
|
||||||
("sampler" [a, ..b] Self::Sampler(SamplerCommand::from_edn(state.sampler.as_ref().expect("no sampler"), &a.to_ref(), b)))
|
|
||||||
("scene" [a, ..b] Self::Scene(SceneCommand::from_edn(state, &a.to_ref(), b)))
|
|
||||||
("select" [s: Selection] Self::Select(s.expect("no selection")))
|
("select" [s: Selection] Self::Select(s.expect("no selection")))
|
||||||
("stop-all" [] Self::StopAll)
|
("stop-all" [] Self::StopAll)
|
||||||
("track" [a, ..b] Self::Track(TrackCommand::from_edn(state, &a.to_ref(), b)))
|
|
||||||
("zoom" [z: usize] Self::Zoom(z))
|
("zoom" [z: usize] Self::Zoom(z))
|
||||||
|
|
||||||
|
("clip" [a, ..b] Self::Clip(
|
||||||
|
ClipCommand::from_edn(state, &a.to_ref(), b).expect("invalid command")))
|
||||||
|
("clock" [a, ..b] Self::Clock(
|
||||||
|
ClockCommand::from_edn(state, &a.to_ref(), b).expect("invalid command")))
|
||||||
|
("editor" [a, ..b] Self::Editor(
|
||||||
|
MidiEditCommand::from_edn(state.editor.as_ref().expect("no editor"), &a.to_ref(), b).expect("invalid command")))
|
||||||
|
("pool" [a, ..b] Self::Pool(
|
||||||
|
PoolCommand::from_edn(state.pool.as_ref().expect("no pool"), &a.to_ref(), b).expect("invalid command")))
|
||||||
|
("sampler" [a, ..b] Self::Sampler(
|
||||||
|
SamplerCommand::from_edn(state.sampler.as_ref().expect("no sampler"), &a.to_ref(), b).expect("invalid command")))
|
||||||
|
("scene" [a, ..b] Self::Scene(
|
||||||
|
SceneCommand::from_edn(state, &a.to_ref(), b).expect("invalid command")))
|
||||||
|
("track" [a, ..b] Self::Track(
|
||||||
|
TrackCommand::from_edn(state, &a.to_ref(), b).expect("invalid command")))
|
||||||
});
|
});
|
||||||
command!(|self: AppCommand, state: App|match self {
|
command!(|self: AppCommand, state: App|match self {
|
||||||
Self::Zoom(_) => { todo!(); },
|
Self::Zoom(_) => { todo!(); },
|
||||||
|
|
@ -950,17 +986,6 @@ content!(TuiOut: |self: Meters<'a>| col!(
|
||||||
format!("L/{:>+9.3}", self.0[0]),
|
format!("L/{:>+9.3}", self.0[0]),
|
||||||
format!("R/{:>+9.3}", self.0[1])
|
format!("R/{:>+9.3}", self.0[1])
|
||||||
));
|
));
|
||||||
/// Transport clock app.
|
|
||||||
pub struct ClockTui { pub jack: Arc<RwLock<JackConnection>>, pub clock: Clock, }
|
|
||||||
handle!(TuiIn: |self: ClockTui, input|ClockCommand::execute_with_state(self, input.event()));
|
|
||||||
keymap!(TRANSPORT_KEYS = |state: ClockTui, input: Event| ClockCommand {
|
|
||||||
key(Char(' ')) =>
|
|
||||||
if state.clock().is_stopped() { ClockCommand::Play(None) } else { ClockCommand::Pause(None) },
|
|
||||||
shift(key(Char(' '))) =>
|
|
||||||
if state.clock().is_stopped() { ClockCommand::Play(Some(0)) } else { ClockCommand::Pause(Some(0)) }
|
|
||||||
});
|
|
||||||
has_clock!(|self: ClockTui|&self.clock);
|
|
||||||
content!(TuiOut:|self: ClockTui|ClockView { compact: false, clock: &self.clock });
|
|
||||||
pub struct ClockView<'a> { pub compact: bool, pub clock: &'a Clock }
|
pub struct ClockView<'a> { pub compact: bool, pub clock: &'a Clock }
|
||||||
content!(TuiOut: |self: ClockView<'a>| Outer(Style::default().fg(TuiTheme::g(255))).enclose(row!(
|
content!(TuiOut: |self: ClockView<'a>| Outer(Style::default().fg(TuiTheme::g(255))).enclose(row!(
|
||||||
OutputStats::new(self.compact, self.clock),
|
OutputStats::new(self.compact, self.clock),
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ pub enum ClockCommand {
|
||||||
SetSync(f64),
|
SetSync(f64),
|
||||||
}
|
}
|
||||||
impl<T: HasClock> EdnCommand<T> for ClockCommand {
|
impl<T: HasClock> EdnCommand<T> for ClockCommand {
|
||||||
fn from_edn <'a> (state: &T, head: &EdnItem<&str>, tail: &'a [EdnItem<String>]) -> Self {
|
fn from_edn <'a> (state: &T, head: &EdnItem<&str>, tail: &'a [EdnItem<&str>]) -> Option<Self> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
impl EdnInput for TuiIn {
|
impl EdnInput for TuiIn {
|
||||||
fn matches (&self, token: &str) -> bool {
|
fn matches_edn (&self, token: &str) -> bool {
|
||||||
false
|
if let Some(event) = parse_key_spec(token.to_string(), KeyModifiers::NONE) {
|
||||||
|
&event == self.event()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fn get_event <S: AsRef<str>> (item: &EdnItem<S>) -> Option<Event> {
|
fn get_event <S: AsRef<str>> (item: &EdnItem<S>) -> Option<Event> {
|
||||||
match item { EdnItem::Sym(s) => parse_key_spec(s.as_ref().to_string(), KeyModifiers::NONE), _ => None }
|
match item { EdnItem::Sym(s) => parse_key_spec(s.as_ref().to_string(), KeyModifiers::NONE), _ => None }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue