mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 11:46:41 +01:00
add command! and input_to_command! macros
This commit is contained in:
parent
93413ae303
commit
fdafd15a01
4 changed files with 146 additions and 162 deletions
|
|
@ -1,5 +1,25 @@
|
|||
use crate::*;
|
||||
|
||||
#[macro_export] macro_rules! command {
|
||||
(|$self:ident:$Command:ty,$state:ident:$State:ty|$handler:expr) => {
|
||||
impl Command<$State> for $Command {
|
||||
fn execute ($self, $state: &mut $State) -> Perhaps<Self> {
|
||||
Ok($handler)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export] macro_rules! input_to_command {
|
||||
($Command:ty: <$Engine:ty>|$state:ident:$State:ty,$input:ident|$handler:expr) => {
|
||||
impl InputToCommand<$Engine, $State> for $Command {
|
||||
fn input_to_command ($state: &$State, $input: &<$Engine as Engine>::Input) -> Option<Self> {
|
||||
$handler
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum NextPrev {
|
||||
Next,
|
||||
|
|
@ -65,7 +85,7 @@ impl<E: Engine, S, C: Command<S>> Menu<E, S, C> {
|
|||
self
|
||||
}
|
||||
pub fn cmd (mut self, hotkey: &'static str, text: &'static str, command: C) -> Self {
|
||||
self.items.push(MenuItem::cmd(hotkey, text, command));
|
||||
self.items.push(MenuItem::cmd(hotkey, text, command));
|
||||
self
|
||||
}
|
||||
pub fn off (mut self, hotkey: &'static str, text: &'static str) -> Self {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,13 @@ pub struct GrooveboxTui {
|
|||
pub split: u16,
|
||||
}
|
||||
|
||||
pub enum GrooveboxCommand {
|
||||
Sequencer(SequencerCommand),
|
||||
Sampler(SamplerCommand),
|
||||
}
|
||||
|
||||
render!(|self:GrooveboxTui|Bsp::s(Tui::fixed_y(self.split, &self.sequencer), &self.sampler));
|
||||
audio!(|self:GrooveboxTui,_client,_process|Control::Continue);
|
||||
handle!(<Tui>|self:GrooveboxTui,input|Ok(None));
|
||||
handle!(<Tui>|self:GrooveboxTui,input|GrooveboxCommand::execute_with_state(self, input));
|
||||
command!(|self:GrooveboxCommand,state:GrooveboxTui|todo!());
|
||||
input_to_command!(GrooveboxCommand: <Tui>|state:GrooveboxTui,input|todo!());
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ use symphonia::core::probe::Hint;
|
|||
use symphonia::core::audio::SampleBuffer;
|
||||
use symphonia::default::get_codecs;
|
||||
|
||||
pub enum SamplerCommand {
|
||||
}
|
||||
|
||||
impl TryFrom<&Arc<RwLock<JackClient>>> for SamplerTui {
|
||||
type Error = Box<dyn std::error::Error>;
|
||||
fn try_from (jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
|
||||
|
|
@ -65,54 +68,55 @@ render!(|self: SamplerTui|{
|
|||
})),
|
||||
|
||||
col!([
|
||||
"",
|
||||
row!([
|
||||
" ", Tui::bold(true, "Sampler."), " Voices: ",
|
||||
Tui::push_x(2, row!([
|
||||
Tui::bold(true, "Sampler"), "|Voices: ",
|
||||
&format!("{}", self.state.voices.read().unwrap().len()),
|
||||
]),
|
||||
" ",
|
||||
])),
|
||||
Tui::either(self.state.unmapped.len() > 0,
|
||||
col!((index, sample) in self.state.unmapped.iter().enumerate() =>
|
||||
{ &format!("| Unmapped #{index}") }), " · No unmapped samples."),
|
||||
" ",
|
||||
{ &format!("| Unmapped #{index}") }), "· No unmapped samples."),
|
||||
Tui::either(self.state.mapped.len() > 0,
|
||||
col!((index, sample) in self.state.unmapped.iter().enumerate() =>
|
||||
{ &format!("| Mapped #{index}") }), " · No mapped samples."),
|
||||
{ &format!("| Mapped #{index}") }), "· No mapped samples."),
|
||||
])
|
||||
|
||||
//render(|to|{
|
||||
//let [x, y, w, h] = to.area();
|
||||
//let style = Style::default().gray();
|
||||
//let voice = self.state.voices.read().unwrap().len();
|
||||
//let title = format!(" {} ({voice} voice(s) playing now)", self.state.name);
|
||||
//to.blit(&title, x+1, y, Some(style.white().bold().not_dim()));
|
||||
|
||||
//let mut width = title.len() + 2;
|
||||
//let mut y1 = 1;
|
||||
//let mut j = 0;
|
||||
//for (note, sample) in self.state.mapped.iter()
|
||||
//.map(|(note, sample)|(Some(note), sample))
|
||||
//.chain(self.state.unmapped.iter().map(|sample|(None, sample)))
|
||||
//{
|
||||
//if y1 >= h {
|
||||
//break
|
||||
//}
|
||||
//let active = j == self.cursor.0;
|
||||
//width = width.max(
|
||||
//draw_sample(to, x, y + y1, note, &*sample.read().unwrap(), active)?
|
||||
//);
|
||||
//y1 = y1 + 1;
|
||||
//j = j + 1;
|
||||
//}
|
||||
//let h = ((2 + y1) as u16).min(h);
|
||||
////Ok(Some([x, y, (width as u16).min(to.area().w()), h]))
|
||||
//Ok(())
|
||||
//}),
|
||||
//])
|
||||
|
||||
]))
|
||||
});
|
||||
|
||||
handle!(<Tui>|self:SamplerTui,from|{
|
||||
let cursor = &mut self.cursor;
|
||||
let unmapped = &mut self.state.unmapped;
|
||||
let mapped = &self.state.mapped;
|
||||
let voices = &self.state.voices;
|
||||
match from.event() {
|
||||
key_pat!(KeyCode::Up) => cursor.0 = if cursor.0 == 0 {
|
||||
mapped.len() + unmapped.len() - 1
|
||||
} else {
|
||||
cursor.0 - 1
|
||||
},
|
||||
key_pat!(KeyCode::Down) => {
|
||||
cursor.0 = (cursor.0 + 1) % (mapped.len() + unmapped.len());
|
||||
},
|
||||
key_pat!(KeyCode::Char('p')) => if let Some(sample) = self.sample() {
|
||||
voices.write().unwrap().push(Sample::play(sample, 0, &100.into()));
|
||||
},
|
||||
key_pat!(KeyCode::Char('a')) => {
|
||||
let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![])));
|
||||
*self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?));
|
||||
unmapped.push(sample);
|
||||
},
|
||||
key_pat!(KeyCode::Char('r')) => if let Some(sample) = self.sample() {
|
||||
*self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?));
|
||||
},
|
||||
key_pat!(KeyCode::Enter) => if let Some(sample) = self.sample() {
|
||||
self.editing = Some(sample.clone());
|
||||
},
|
||||
_ => {
|
||||
return Ok(None)
|
||||
}
|
||||
}
|
||||
Ok(Some(true))
|
||||
});
|
||||
|
||||
impl SamplerTui {
|
||||
/// Immutable reference to sample at cursor.
|
||||
pub fn sample (&self) -> Option<&Arc<RwLock<Sample>>> {
|
||||
|
|
@ -256,41 +260,6 @@ fn read_sample_data (_: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
|
|||
todo!();
|
||||
}
|
||||
|
||||
handle!(<Tui>|self:SamplerTui,from|{
|
||||
let cursor = &mut self.cursor;
|
||||
let unmapped = &mut self.state.unmapped;
|
||||
let mapped = &self.state.mapped;
|
||||
let voices = &self.state.voices;
|
||||
match from.event() {
|
||||
key_pat!(KeyCode::Up) => cursor.0 = if cursor.0 == 0 {
|
||||
mapped.len() + unmapped.len() - 1
|
||||
} else {
|
||||
cursor.0 - 1
|
||||
},
|
||||
key_pat!(KeyCode::Down) => {
|
||||
cursor.0 = (cursor.0 + 1) % (mapped.len() + unmapped.len());
|
||||
},
|
||||
key_pat!(KeyCode::Char('p')) => if let Some(sample) = self.sample() {
|
||||
voices.write().unwrap().push(Sample::play(sample, 0, &100.into()));
|
||||
},
|
||||
key_pat!(KeyCode::Char('a')) => {
|
||||
let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![])));
|
||||
*self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?));
|
||||
unmapped.push(sample);
|
||||
},
|
||||
key_pat!(KeyCode::Char('r')) => if let Some(sample) = self.sample() {
|
||||
*self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?));
|
||||
},
|
||||
key_pat!(KeyCode::Enter) => if let Some(sample) = self.sample() {
|
||||
self.editing = Some(sample.clone());
|
||||
},
|
||||
_ => {
|
||||
return Ok(None)
|
||||
}
|
||||
}
|
||||
Ok(Some(true))
|
||||
});
|
||||
|
||||
fn scan (dir: &PathBuf) -> Usually<(Vec<OsString>, Vec<OsString>)> {
|
||||
let (mut subdirs, mut files) = std::fs::read_dir(dir)?
|
||||
.fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{
|
||||
|
|
|
|||
|
|
@ -54,100 +54,88 @@ pub enum SequencerCommand {
|
|||
ShowPool(bool),
|
||||
}
|
||||
|
||||
impl Command<SequencerTui> for SequencerCommand {
|
||||
fn execute (self, state: &mut SequencerTui) -> Perhaps<Self> {
|
||||
Ok(match self {
|
||||
Self::Phrases(cmd) => {
|
||||
let mut default = |cmd: PhrasesCommand|cmd.execute(&mut state.phrases).map(|x|x.map(Phrases));
|
||||
match cmd {
|
||||
// autoselect: automatically load selected phrase in editor
|
||||
PhrasesCommand::Select(_) => {
|
||||
let undo = default(cmd)?;
|
||||
state.editor.set_phrase(Some(state.phrases.phrase()));
|
||||
undo
|
||||
},
|
||||
// update color in all places simultaneously
|
||||
PhrasesCommand::Phrase(SetColor(index, _)) => {
|
||||
let undo = default(cmd)?;
|
||||
state.editor.set_phrase(Some(state.phrases.phrase()));
|
||||
undo
|
||||
},
|
||||
_ => default(cmd)?
|
||||
}
|
||||
},
|
||||
Self::Editor(cmd) => {
|
||||
let default = ||cmd.execute(&mut state.editor).map(|x|x.map(Editor));
|
||||
match cmd {
|
||||
_ => default()?
|
||||
}
|
||||
},
|
||||
Self::Clock(cmd) => cmd.execute(state)?.map(Clock),
|
||||
Self::Enqueue(phrase) => {
|
||||
state.player.enqueue_next(phrase.as_ref());
|
||||
None
|
||||
},
|
||||
Self::ShowPool(value) => {
|
||||
state.show_pool = value;
|
||||
None
|
||||
}
|
||||
})
|
||||
handle!(<Tui>|self:SequencerTui,input|SequencerCommand::execute_with_state(self, input));
|
||||
input_to_command!(SequencerCommand: <Tui>|state:SequencerTui,input|Some(match input.event() {
|
||||
// Transport: Play/pause
|
||||
key_pat!(Char(' ')) => Clock(if state.clock().is_stopped() {
|
||||
Play(None) } else { Pause(None) }),
|
||||
// Transport: Play from start or rewind to start
|
||||
key_pat!(Shift-Char(' ')) => Clock(if state.clock().is_stopped() {
|
||||
Play(Some(0)) } else { Pause(Some(0)) }),
|
||||
// TODO: u: undo
|
||||
key_pat!(Char('u')) => { todo!("undo") },
|
||||
// TODO: Shift-U: redo
|
||||
key_pat!(Char('U')) => { todo!("redo") },
|
||||
// TODO: k: toggle on-screen keyboard
|
||||
key_pat!(Ctrl-Char('k')) => { todo!("keyboard") },
|
||||
// Tab: Toggle visibility of phrase pool column
|
||||
key_pat!(Tab) => ShowPool(!state.show_pool),
|
||||
// q: Enqueue currently edited phrase
|
||||
key_pat!(Char('q')) => Enqueue(Some(state.phrases.phrase().clone())),
|
||||
// 0: Enqueue phrase 0 (stop all)
|
||||
key_pat!(Char('0')) => Enqueue(Some(state.phrases.phrases()[0].clone())),
|
||||
// e: Toggle between editing currently playing or other phrase
|
||||
key_pat!(Char('e')) => if let Some((_, Some(playing))) = state.player.play_phrase() {
|
||||
let editing = state.editor.phrase().as_ref().map(|p|p.read().unwrap().clone());
|
||||
let selected = state.phrases.phrase().clone();
|
||||
Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing {
|
||||
selected
|
||||
} else {
|
||||
playing.clone()
|
||||
})))
|
||||
} else {
|
||||
return None
|
||||
},
|
||||
// For the rest, use the default keybindings of the components.
|
||||
// The ones defined above supersede them.
|
||||
_ => if let Some(command) = PhraseCommand::input_to_command(&state.editor, input) {
|
||||
Editor(command)
|
||||
} else if let Some(command) = PhrasesCommand::input_to_command(&state.phrases, input) {
|
||||
Phrases(command)
|
||||
} else {
|
||||
return None
|
||||
}
|
||||
}
|
||||
|
||||
impl InputToCommand<Tui, SequencerTui> for SequencerCommand {
|
||||
fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option<Self> {
|
||||
Some(match input.event() {
|
||||
// TODO: u: undo
|
||||
key_pat!(Char('u')) => { todo!("undo") },
|
||||
// TODO: Shift-U: redo
|
||||
key_pat!(Char('U')) => { todo!("redo") },
|
||||
// TODO: k: toggle on-screen keyboard
|
||||
key_pat!(Ctrl-Char('k')) => { todo!("keyboard") },
|
||||
// Tab: Toggle visibility of phrase pool column
|
||||
key_pat!(Tab) => ShowPool(!state.show_pool),
|
||||
// q: Enqueue currently edited phrase
|
||||
key_pat!(Char('q')) => Enqueue(Some(state.phrases.phrase().clone())),
|
||||
// 0: Enqueue phrase 0 (stop all)
|
||||
key_pat!(Char('0')) => Enqueue(Some(state.phrases.phrases()[0].clone())),
|
||||
// e: Toggle between editing currently playing or other phrase
|
||||
key_pat!(Char('e')) => if let Some((_, Some(playing))) = state.player.play_phrase() {
|
||||
let editing = state.editor.phrase().as_ref().map(|p|p.read().unwrap().clone());
|
||||
let selected = state.phrases.phrase().clone();
|
||||
Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing {
|
||||
selected
|
||||
} else {
|
||||
playing.clone()
|
||||
})))
|
||||
} else {
|
||||
return None
|
||||
}));
|
||||
command!(|self: SequencerCommand, state: SequencerTui|match self {
|
||||
Self::Phrases(cmd) => {
|
||||
let mut default = |cmd: PhrasesCommand|cmd.execute(&mut state.phrases).map(|x|x.map(Phrases));
|
||||
match cmd {
|
||||
// autoselect: automatically load selected phrase in editor
|
||||
PhrasesCommand::Select(_) => {
|
||||
let undo = default(cmd)?;
|
||||
state.editor.set_phrase(Some(state.phrases.phrase()));
|
||||
undo
|
||||
},
|
||||
|
||||
// Transport: Play/pause
|
||||
key_pat!(Char(' ')) => Clock(if state.clock().is_stopped() {
|
||||
Play(None) } else { Pause(None) }),
|
||||
|
||||
// Transport: Play from start or rewind to start
|
||||
key_pat!(Shift-Char(' ')) => Clock(if state.clock().is_stopped() {
|
||||
Play(Some(0)) } else { Pause(Some(0)) }),
|
||||
|
||||
// For the rest, use the default keybindings of the components.
|
||||
// The ones defined above supersede them.
|
||||
_ => if let Some(command) = PhraseCommand::input_to_command(&state.editor, input) {
|
||||
Editor(command)
|
||||
} else if let Some(command) = PhrasesCommand::input_to_command(&state.phrases, input) {
|
||||
Phrases(command)
|
||||
} else {
|
||||
return None
|
||||
}
|
||||
})
|
||||
// update color in all places simultaneously
|
||||
PhrasesCommand::Phrase(SetColor(index, _)) => {
|
||||
let undo = default(cmd)?;
|
||||
state.editor.set_phrase(Some(state.phrases.phrase()));
|
||||
undo
|
||||
},
|
||||
_ => default(cmd)?
|
||||
}
|
||||
},
|
||||
Self::Editor(cmd) => {
|
||||
let default = ||cmd.execute(&mut state.editor).map(|x|x.map(Editor));
|
||||
match cmd {
|
||||
_ => default()?
|
||||
}
|
||||
},
|
||||
Self::Clock(cmd) => cmd.execute(state)?.map(Clock),
|
||||
Self::Enqueue(phrase) => {
|
||||
state.player.enqueue_next(phrase.as_ref());
|
||||
None
|
||||
},
|
||||
Self::ShowPool(value) => {
|
||||
state.show_pool = value;
|
||||
None
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
has_size!(<Tui>|self:SequencerTui|&self.size);
|
||||
has_clock!(|self:SequencerTui|&self.clock);
|
||||
has_phrases!(|self:SequencerTui|self.phrases.phrases);
|
||||
has_editor!(|self:SequencerTui|self.editor);
|
||||
handle!(<Tui>|self:SequencerTui,i|SequencerCommand::execute_with_state(self, i));
|
||||
render!(|self: SequencerTui|{
|
||||
let w = self.size.w();
|
||||
let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue