mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 19:56:42 +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
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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue