wip: implementing app command dispatch

This commit is contained in:
🪞👃🪞 2025-01-14 19:03:08 +01:00
parent d393cab2d8
commit 12faadef44
31 changed files with 598 additions and 551 deletions

View file

@ -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))
}
}

View file

@ -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
}
}
};

View file

@ -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
}

View file

@ -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"))
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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
View 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)

View file

@ -10,8 +10,6 @@ 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_edit; pub use midi_edit::*;
mod piano_h; pub use self::piano_h::*;

View file

@ -33,7 +33,6 @@ pub trait HasEditor {
pub struct MidiEditor {
pub mode: PianoHorizontal,
pub size: Measure<TuiOut>,
pub keymap: EdnKeymap,
}
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")
)
}
}
}

View file

@ -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
//})
//}

View file

@ -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!()
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////

View file

@ -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)

View file

@ -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!()
}
}

View file

@ -41,3 +41,4 @@
(:q enqueue :clip)
(:0 enqueue :stop)

View file

@ -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),

View file

@ -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!()
}
}

View file

@ -1,8 +1,13 @@
use crate::*;
impl EdnInput for TuiIn {
fn matches (&self, token: &str) -> bool {
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 }
}