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 {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
use crate::*;
|
||||
/// Turns an EDN item sequence into a command enum variant.
|
||||
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` */
|
||||
#[macro_export] macro_rules! edn_command {
|
||||
|
|
@ -27,7 +28,11 @@ pub trait EdnCommand<C>: Command<C> {
|
|||
$command:expr
|
||||
))* }) => {
|
||||
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
|
||||
// bind argument ids
|
||||
$($arg),*
|
||||
|
|
@ -38,9 +43,9 @@ pub trait EdnCommand<C>: Command<C> {
|
|||
$(let $arg: Option<$type> = EdnProvide::<$type>::get($state, $arg);)?
|
||||
)*
|
||||
//$(edn_command!(@bind $state => $arg $(?)? : $type);)*
|
||||
return $command
|
||||
return Some($command)
|
||||
})*
|
||||
panic!("no such command")
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::*;
|
||||
|
||||
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> {
|
||||
None
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
use crate::*;
|
||||
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> (
|
||||
&self, state: &C, input: &I
|
||||
) -> Option<E> {
|
||||
for item in self.0.iter() {
|
||||
if let Exp(items) = item {
|
||||
match items.as_slice() {
|
||||
[Sym(a), b, c @ ..] => if input.matches(a.as_str()) {
|
||||
return Some(E::from_edn(state, &b.to_ref(), c))
|
||||
[Sym(a), b, c @ ..] => if input.matches_edn(a) {
|
||||
return E::from_edn(state, &b.to_ref(), c)
|
||||
},
|
||||
_ => unreachable!()
|
||||
}
|
||||
|
|
@ -23,9 +23,10 @@ impl EdnKeymap {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> From<T> for EdnKeymap {
|
||||
fn from (source: T) -> Self {
|
||||
Self(EdnItem::<String>::read_all(source.as_ref()).expect("failed to load keymap"))
|
||||
impl<'a> From<&'a str> for EdnKeymap<'a> {
|
||||
fn from (source: &'a str) -> Self {
|
||||
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
|
||||
pub trait Input: Send + Sync + Sized {
|
||||
/// Type of input event
|
||||
type Event;
|
||||
/// Result of handling input
|
||||
type Handled;
|
||||
type Handled; // TODO: make this an Option<Box dyn Command<Self>> containing the undo
|
||||
/// Currently handled event
|
||||
fn event (&self) -> &Self::Event;
|
||||
/// Whether component should exit
|
||||
|
|
@ -11,3 +14,62 @@ pub trait Input: Send + Sync + Sized {
|
|||
/// Mark component as done
|
||||
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 input; pub use self::input::*;
|
||||
mod handle; pub use self::handle::*;
|
||||
mod command; pub use self::command::*;
|
||||
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_view; pub use midi_view::*;
|
||||
|
||||
mod midi_pool; pub use midi_pool::*;
|
||||
mod midi_pool_cmd; pub use midi_pool_cmd::*;
|
||||
|
||||
mod midi_pool; pub use midi_pool::*;
|
||||
mod midi_edit; pub use midi_edit::*;
|
||||
mod piano_h; pub use self::piano_h::*;
|
||||
|
||||
|
|
|
|||
|
|
@ -31,9 +31,8 @@ pub trait HasEditor {
|
|||
}
|
||||
/// Contains state for viewing and editing a clip
|
||||
pub struct MidiEditor {
|
||||
pub mode: PianoHorizontal,
|
||||
pub size: Measure<TuiOut>,
|
||||
pub keymap: EdnKeymap,
|
||||
pub mode: PianoHorizontal,
|
||||
pub size: Measure<TuiOut>,
|
||||
}
|
||||
impl std::fmt::Debug for MidiEditor {
|
||||
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
|
|
@ -47,10 +46,6 @@ impl Default for MidiEditor {
|
|||
Self {
|
||||
mode: PianoHorizontal::new(None),
|
||||
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 KeyCode::*;
|
||||
pub type ClipPool = Vec<Arc<RwLock<MidiClip>>>;
|
||||
pub trait HasClips {
|
||||
fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>;
|
||||
|
|
@ -204,3 +205,442 @@ content!(TuiOut: |self: ClipLength| {
|
|||
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),
|
||||
}
|
||||
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!()
|
||||
}
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ impl EdnCommand<SamplerTui> for SamplerTuiCommand {
|
|||
NoteOff(u7),
|
||||
}
|
||||
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!()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,3 +41,4 @@
|
|||
|
||||
(:q enqueue :clip)
|
||||
(:0 enqueue :stop)
|
||||
|
||||
|
|
@ -120,7 +120,7 @@ impl App {
|
|||
//}
|
||||
//};
|
||||
Ok(Self {
|
||||
edn: include_str!("./transport-view.edn").to_string(),
|
||||
edn: include_str!("./view_transport.edn").to_string(),
|
||||
jack: jack.clone(),
|
||||
color: ItemPalette::random(),
|
||||
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 = Arc::new(RwLock::new(clip));
|
||||
Ok(Self {
|
||||
edn: include_str!("./sequencer-view.edn").to_string(),
|
||||
edn: include_str!("./view_sequencer.edn").to_string(),
|
||||
pool: Some((&clip).into()),
|
||||
editor: Some((&clip).into()),
|
||||
editing: false.into(),
|
||||
|
|
@ -160,7 +160,7 @@ impl App {
|
|||
audio_tos: &[&[PortConnection];2],
|
||||
) -> Usually<Self> {
|
||||
let app = Self {
|
||||
edn: include_str!("./groovebox-view.edn").to_string(),
|
||||
edn: include_str!("./view_groovebox.edn").to_string(),
|
||||
sampler: Some(Sampler::new(
|
||||
jack,
|
||||
&"sampler",
|
||||
|
|
@ -187,7 +187,7 @@ impl App {
|
|||
track_width: usize,
|
||||
) -> Usually<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)?
|
||||
};
|
||||
arranger.scenes_add(scenes);
|
||||
|
|
@ -373,8 +373,36 @@ impl App {
|
|||
}
|
||||
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 {
|
||||
Clip(ClipCommand),
|
||||
Clock(ClockCommand),
|
||||
|
|
@ -392,20 +420,28 @@ handle!(TuiIn: |self: App, input| Ok(None));
|
|||
Zoom(Option<usize>),
|
||||
}
|
||||
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()))
|
||||
("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))
|
||||
("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")))
|
||||
("stop-all" [] Self::StopAll)
|
||||
("track" [a, ..b] Self::Track(TrackCommand::from_edn(state, &a.to_ref(), b)))
|
||||
("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 {
|
||||
Self::Zoom(_) => { todo!(); },
|
||||
|
|
@ -950,17 +986,6 @@ content!(TuiOut: |self: Meters<'a>| col!(
|
|||
format!("L/{:>+9.3}", self.0[0]),
|
||||
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 }
|
||||
content!(TuiOut: |self: ClockView<'a>| Outer(Style::default().fg(TuiTheme::g(255))).enclose(row!(
|
||||
OutputStats::new(self.compact, self.clock),
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ pub enum ClockCommand {
|
|||
SetSync(f64),
|
||||
}
|
||||
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!()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
use crate::*;
|
||||
|
||||
impl EdnInput for TuiIn {
|
||||
fn matches (&self, token: &str) -> bool {
|
||||
false
|
||||
fn matches_edn (&self, token: &str) -> bool {
|
||||
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> {
|
||||
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