split key macro into key_pat and key_expr

This commit is contained in:
🪞👃🪞 2024-12-14 19:13:28 +01:00
parent d003af85ca
commit 29abe29504
13 changed files with 550 additions and 551 deletions

View file

@ -1,5 +1,3 @@
use crate::*;
mod phrase; pub(crate) use phrase::*;
mod jack; pub(crate) use self::jack::*;
mod clip; pub(crate) use clip::*;

View file

@ -259,16 +259,16 @@ pub trait FocusWrap<T> {
pub fn to_focus_command <T: Send + Sync> (input: &TuiInput) -> Option<FocusCommand<T>> {
use KeyCode::{Tab, BackTab, Up, Down, Left, Right, Enter, Esc};
Some(match input.event() {
key!(Tab) => FocusCommand::Next,
key!(Shift-Tab) => FocusCommand::Prev,
key!(BackTab) => FocusCommand::Prev,
key!(Shift-BackTab) => FocusCommand::Prev,
key!(Up) => FocusCommand::Up,
key!(Down) => FocusCommand::Down,
key!(Left) => FocusCommand::Left,
key!(Right) => FocusCommand::Right,
key!(Enter) => FocusCommand::Enter,
key!(Esc) => FocusCommand::Exit,
key_pat!(Tab) => FocusCommand::Next,
key_pat!(Shift-Tab) => FocusCommand::Prev,
key_pat!(BackTab) => FocusCommand::Prev,
key_pat!(Shift-BackTab) => FocusCommand::Prev,
key_pat!(Up) => FocusCommand::Up,
key_pat!(Down) => FocusCommand::Down,
key_pat!(Left) => FocusCommand::Left,
key_pat!(Right) => FocusCommand::Right,
key_pat!(Enter) => FocusCommand::Enter,
key_pat!(Esc) => FocusCommand::Exit,
_ => return None
})
}

View file

@ -1,5 +1,3 @@
use crate::*;
pub fn to_note_name (n: usize) -> &'static str {
if n > 127 {
panic!("to_note_name({n}): must be 0-127");

View file

@ -114,7 +114,7 @@ impl Tui {
if ::crossterm::event::poll(poll).is_ok() {
let event = TuiEvent::Input(::crossterm::event::read().unwrap());
match event {
key!(Ctrl-KeyCode::Char('c')) => {
key_pat!(Ctrl-KeyCode::Char('c')) => {
exited.store(true, Ordering::Relaxed);
},
_ => {

View file

@ -1,11 +1,7 @@
use crate::{
*,
api::{
ArrangerTrackCommand,
ArrangerSceneCommand,
ArrangerClipCommand
}
};
use crate::*;
use crate::api::ArrangerTrackCommand;
use crate::api::ArrangerSceneCommand;
use crate::api::ArrangerClipCommand;
impl TryFrom<&Arc<RwLock<JackClient>>> for ArrangerTui {
type Error = Box<dyn std::error::Error>;
@ -61,6 +57,273 @@ pub struct ArrangerTui {
pub perf: PerfModel,
}
impl Handle<Tui> for ArrangerTui {
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
ArrangerCommand::execute_with_state(self, i)
}
}
#[derive(Clone, Debug)]
pub enum ArrangerCommand {
Focus(FocusCommand<ArrangerFocus>),
Undo,
Redo,
Clear,
Color(ItemColor),
Clock(ClockCommand),
Scene(ArrangerSceneCommand),
Track(ArrangerTrackCommand),
Clip(ArrangerClipCommand),
Select(ArrangerSelection),
Zoom(usize),
Phrases(PhrasesCommand),
Editor(PhraseCommand),
}
impl Command<ArrangerTui> for ArrangerCommand {
fn execute (self, state: &mut ArrangerTui) -> Perhaps<Self> {
use ArrangerCommand::*;
Ok(match self {
Focus(cmd) => cmd.execute(state)?.map(Focus),
Scene(cmd) => cmd.execute(state)?.map(Scene),
Track(cmd) => cmd.execute(state)?.map(Track),
Clip(cmd) => cmd.execute(state)?.map(Clip),
Phrases(cmd) => cmd.execute(&mut state.phrases)?.map(Phrases),
Editor(cmd) => cmd.execute(&mut state.editor)?.map(Editor),
Clock(cmd) => cmd.execute(state)?.map(Clock),
Zoom(_) => { todo!(); },
Select(selected) => {
*state.selected_mut() = selected;
None
},
_ => { todo!() }
})
}
}
impl Command<ArrangerTui> for ArrangerSceneCommand {
fn execute (self, _state: &mut ArrangerTui) -> Perhaps<Self> {
//todo!();
Ok(None)
}
}
impl Command<ArrangerTui> for ArrangerTrackCommand {
fn execute (self, _state: &mut ArrangerTui) -> Perhaps<Self> {
//todo!();
Ok(None)
}
}
impl Command<ArrangerTui> for ArrangerClipCommand {
fn execute (self, _state: &mut ArrangerTui) -> Perhaps<Self> {
//todo!();
Ok(None)
}
}
pub trait ArrangerControl: TransportControl<ArrangerFocus> {
fn selected (&self) -> ArrangerSelection;
fn selected_mut (&mut self) -> &mut ArrangerSelection;
fn activate (&mut self) -> Usually<()>;
fn selected_phrase (&self) -> Option<Arc<RwLock<Phrase>>>;
fn toggle_loop (&mut self);
fn randomize_color (&mut self);
}
impl ArrangerControl for ArrangerTui {
fn selected (&self) -> ArrangerSelection {
self.selected
}
fn selected_mut (&mut self) -> &mut ArrangerSelection {
&mut self.selected
}
fn activate (&mut self) -> Usually<()> {
if let ArrangerSelection::Scene(s) = self.selected {
for (t, track) in self.tracks.iter_mut().enumerate() {
let phrase = self.scenes[s].clips[t].clone();
if track.player.play_phrase.is_some() || phrase.is_some() {
track.player.enqueue_next(phrase.as_ref());
}
}
if self.clock().is_stopped() {
self.clock().play_from(Some(0))?;
}
} else if let ArrangerSelection::Clip(t, s) = self.selected {
let phrase = self.scenes()[s].clips[t].clone();
self.tracks_mut()[t].player.enqueue_next(phrase.as_ref());
};
Ok(())
}
fn selected_phrase (&self) -> Option<Arc<RwLock<Phrase>>> {
self.selected_scene()?.clips.get(self.selected.track()?)?.clone()
}
fn toggle_loop (&mut self) {
if let Some(phrase) = self.selected_phrase() {
phrase.write().unwrap().toggle_loop()
}
}
fn randomize_color (&mut self) {
match self.selected {
ArrangerSelection::Mix => {
self.color = ItemColor::random_dark()
},
ArrangerSelection::Track(t) => {
self.tracks_mut()[t].color = ItemColor::random()
},
ArrangerSelection::Scene(s) => {
self.scenes_mut()[s].color = ItemColor::random()
},
ArrangerSelection::Clip(t, s) => {
if let Some(phrase) = &self.scenes_mut()[s].clips[t] {
phrase.write().unwrap().color = ItemPalette::random();
}
}
}
}
}
impl InputToCommand<Tui, ArrangerTui> for ArrangerCommand {
fn input_to_command (state: &ArrangerTui, input: &TuiInput) -> Option<Self> {
to_arranger_command(state, input)
.or_else(||to_focus_command(input).map(ArrangerCommand::Focus))
}
}
fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option<ArrangerCommand> {
use ArrangerCommand as Cmd;
use KeyCode::Char;
if !state.entered() {
return None
}
Some(match input.event() {
key_pat!(Char('e')) => Cmd::Editor(PhraseCommand::Show(Some(
state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone()
))),
// WSAD navigation, Q launches, E edits, PgUp/Down pool, Arrows editor
_ => match state.focused() {
ArrangerFocus::Transport(_) => {
match to_transport_command(state, input)? {
TransportCommand::Clock(command) => Cmd::Clock(command),
_ => return None,
}
},
ArrangerFocus::PhraseEditor => {
Cmd::Editor(PhraseCommand::input_to_command(&state.editor, input)?)
},
ArrangerFocus::Phrases => {
Cmd::Phrases(PhrasesCommand::input_to_command(&state.phrases, input)?)
},
ArrangerFocus::Arranger => {
use ArrangerSelection::*;
match input.event() {
key_pat!(Char('l')) => Cmd::Clip(ArrangerClipCommand::SetLoop(false)),
key_pat!(Char('+')) => Cmd::Zoom(0), // TODO
key_pat!(Char('=')) => Cmd::Zoom(0), // TODO
key_pat!(Char('_')) => Cmd::Zoom(0), // TODO
key_pat!(Char('-')) => Cmd::Zoom(0), // TODO
key_pat!(Char('`')) => { todo!("toggle state mode") },
key_pat!(Ctrl-Char('a')) => Cmd::Scene(ArrangerSceneCommand::Add),
key_pat!(Ctrl-Char('t')) => Cmd::Track(ArrangerTrackCommand::Add),
_ => match state.selected() {
Mix => to_arranger_mix_command(input)?,
Track(t) => to_arranger_track_command(input, t)?,
Scene(s) => to_arranger_scene_command(input, s)?,
Clip(t, s) => to_arranger_clip_command(input, t, s)?,
}
}
}
}
})
}
fn to_arranger_mix_command (input: &TuiInput) -> Option<ArrangerCommand> {
use KeyCode::{Char, Down, Right, Delete};
use ArrangerCommand as Cmd;
use ArrangerSelection as Select;
Some(match input.event() {
key_pat!(Down) => Cmd::Select(Select::Scene(0)),
key_pat!(Right) => Cmd::Select(Select::Track(0)),
key_pat!(Char(',')) => Cmd::Zoom(0),
key_pat!(Char('.')) => Cmd::Zoom(0),
key_pat!(Char('<')) => Cmd::Zoom(0),
key_pat!(Char('>')) => Cmd::Zoom(0),
key_pat!(Delete) => Cmd::Clear,
key_pat!(Char('c')) => Cmd::Color(ItemColor::random()),
_ => return None
})
}
fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option<ArrangerCommand> {
use KeyCode::{Char, Down, Left, Right, Delete};
use ArrangerCommand as Cmd;
use ArrangerSelection as Select;
use ArrangerTrackCommand as Track;
Some(match input.event() {
key_pat!(Down) => Cmd::Select(Select::Clip(t, 0)),
key_pat!(Left) => Cmd::Select(if t > 0 { Select::Track(t - 1) } else { Select::Mix }),
key_pat!(Right) => Cmd::Select(Select::Track(t + 1)),
key_pat!(Char(',')) => Cmd::Track(Track::Swap(t, t - 1)),
key_pat!(Char('.')) => Cmd::Track(Track::Swap(t, t + 1)),
key_pat!(Char('<')) => Cmd::Track(Track::Swap(t, t - 1)),
key_pat!(Char('>')) => Cmd::Track(Track::Swap(t, t + 1)),
key_pat!(Delete) => Cmd::Track(Track::Delete(t)),
//key_pat!(Char('c')) => Cmd::Track(Track::Color(t, ItemColor::random())),
_ => return None
})
}
fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option<ArrangerCommand> {
use KeyCode::{Char, Up, Down, Right, Enter, Delete};
use ArrangerCommand as Cmd;
use ArrangerSelection as Select;
use ArrangerSceneCommand as Scene;
Some(match input.event() {
key_pat!(Up) => Cmd::Select(if s > 0 { Select::Scene(s - 1) } else { Select::Mix }),
key_pat!(Down) => Cmd::Select(Select::Scene(s + 1)),
key_pat!(Right) => Cmd::Select(Select::Clip(0, s)),
key_pat!(Char(',')) => Cmd::Scene(Scene::Swap(s, s - 1)),
key_pat!(Char('.')) => Cmd::Scene(Scene::Swap(s, s + 1)),
key_pat!(Char('<')) => Cmd::Scene(Scene::Swap(s, s - 1)),
key_pat!(Char('>')) => Cmd::Scene(Scene::Swap(s, s + 1)),
key_pat!(Enter) => Cmd::Scene(Scene::Play(s)),
key_pat!(Delete) => Cmd::Scene(Scene::Delete(s)),
//key_pat!(Char('c')) => Cmd::Track(Scene::Color(s, ItemColor::random())),
_ => return None
})
}
fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option<ArrangerCommand> {
use KeyCode::{Char, Up, Down, Left, Right, Delete};
use ArrangerCommand as Cmd;
use ArrangerSelection as Select;
use ArrangerClipCommand as Clip;
Some(match input.event() {
key_pat!(Up) => Cmd::Select(if s > 0 { Select::Clip(t, s - 1) } else { Select::Track(t) }),
key_pat!(Down) => Cmd::Select(Select::Clip(t, s + 1)),
key_pat!(Left) => Cmd::Select(if t > 0 { Select::Clip(t - 1, s) } else { Select::Scene(s) }),
key_pat!(Right) => Cmd::Select(Select::Clip(t + 1, s)),
key_pat!(Char(',')) => Cmd::Clip(Clip::Set(t, s, None)),
key_pat!(Char('.')) => Cmd::Clip(Clip::Set(t, s, None)),
key_pat!(Char('<')) => Cmd::Clip(Clip::Set(t, s, None)),
key_pat!(Char('>')) => Cmd::Clip(Clip::Set(t, s, None)),
key_pat!(Delete) => Cmd::Clip(Clip::Set(t, s, None)),
//key_pat!(Char('c')) => Cmd::Clip(Clip::Color(t, s, ItemColor::random())),
//key_pat!(Char('g')) => Cmd::Clip(Clip(Clip::Get(t, s))),
//key_pat!(Char('s')) => Cmd::Clip(Clip(Clip::Set(t, s))),
_ => return None
})
}
impl TransportControl<ArrangerFocus> for ArrangerTui {
fn transport_focused (&self) -> Option<TransportFocus> {
match self.focus.inner() {
ArrangerFocus::Transport(focus) => Some(focus),
_ => None
}
}
}
impl Audio for ArrangerTui {
#[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
// Start profiling cycle
@ -1084,270 +1347,3 @@ impl ArrangerSelection {
}
}
}
impl Handle<Tui> for ArrangerTui {
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
ArrangerCommand::execute_with_state(self, i)
}
}
#[derive(Clone, Debug)]
pub enum ArrangerCommand {
Focus(FocusCommand<ArrangerFocus>),
Undo,
Redo,
Clear,
Color(ItemColor),
Clock(ClockCommand),
Scene(ArrangerSceneCommand),
Track(ArrangerTrackCommand),
Clip(ArrangerClipCommand),
Select(ArrangerSelection),
Zoom(usize),
Phrases(PhrasesCommand),
Editor(PhraseCommand),
}
impl Command<ArrangerTui> for ArrangerCommand {
fn execute (self, state: &mut ArrangerTui) -> Perhaps<Self> {
use ArrangerCommand::*;
Ok(match self {
Focus(cmd) => cmd.execute(state)?.map(Focus),
Scene(cmd) => cmd.execute(state)?.map(Scene),
Track(cmd) => cmd.execute(state)?.map(Track),
Clip(cmd) => cmd.execute(state)?.map(Clip),
Phrases(cmd) => cmd.execute(&mut state.phrases)?.map(Phrases),
Editor(cmd) => cmd.execute(&mut state.editor)?.map(Editor),
Clock(cmd) => cmd.execute(state)?.map(Clock),
Zoom(_) => { todo!(); },
Select(selected) => {
*state.selected_mut() = selected;
None
},
_ => { todo!() }
})
}
}
impl Command<ArrangerTui> for ArrangerSceneCommand {
fn execute (self, _state: &mut ArrangerTui) -> Perhaps<Self> {
//todo!();
Ok(None)
}
}
impl Command<ArrangerTui> for ArrangerTrackCommand {
fn execute (self, _state: &mut ArrangerTui) -> Perhaps<Self> {
//todo!();
Ok(None)
}
}
impl Command<ArrangerTui> for ArrangerClipCommand {
fn execute (self, _state: &mut ArrangerTui) -> Perhaps<Self> {
//todo!();
Ok(None)
}
}
pub trait ArrangerControl: TransportControl<ArrangerFocus> {
fn selected (&self) -> ArrangerSelection;
fn selected_mut (&mut self) -> &mut ArrangerSelection;
fn activate (&mut self) -> Usually<()>;
fn selected_phrase (&self) -> Option<Arc<RwLock<Phrase>>>;
fn toggle_loop (&mut self);
fn randomize_color (&mut self);
}
impl ArrangerControl for ArrangerTui {
fn selected (&self) -> ArrangerSelection {
self.selected
}
fn selected_mut (&mut self) -> &mut ArrangerSelection {
&mut self.selected
}
fn activate (&mut self) -> Usually<()> {
if let ArrangerSelection::Scene(s) = self.selected {
for (t, track) in self.tracks.iter_mut().enumerate() {
let phrase = self.scenes[s].clips[t].clone();
if track.player.play_phrase.is_some() || phrase.is_some() {
track.player.enqueue_next(phrase.as_ref());
}
}
if self.clock().is_stopped() {
self.clock().play_from(Some(0))?;
}
} else if let ArrangerSelection::Clip(t, s) = self.selected {
let phrase = self.scenes()[s].clips[t].clone();
self.tracks_mut()[t].player.enqueue_next(phrase.as_ref());
};
Ok(())
}
fn selected_phrase (&self) -> Option<Arc<RwLock<Phrase>>> {
self.selected_scene()?.clips.get(self.selected.track()?)?.clone()
}
fn toggle_loop (&mut self) {
if let Some(phrase) = self.selected_phrase() {
phrase.write().unwrap().toggle_loop()
}
}
fn randomize_color (&mut self) {
match self.selected {
ArrangerSelection::Mix => {
self.color = ItemColor::random_dark()
},
ArrangerSelection::Track(t) => {
self.tracks_mut()[t].color = ItemColor::random()
},
ArrangerSelection::Scene(s) => {
self.scenes_mut()[s].color = ItemColor::random()
},
ArrangerSelection::Clip(t, s) => {
if let Some(phrase) = &self.scenes_mut()[s].clips[t] {
phrase.write().unwrap().color = ItemPalette::random();
}
}
}
}
}
impl InputToCommand<Tui, ArrangerTui> for ArrangerCommand {
fn input_to_command (state: &ArrangerTui, input: &TuiInput) -> Option<Self> {
to_arranger_command(state, input)
.or_else(||to_focus_command(input).map(ArrangerCommand::Focus))
}
}
fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option<ArrangerCommand> {
use ArrangerCommand as Cmd;
use KeyCode::Char;
if !state.entered() {
return None
}
Some(match input.event() {
key!(Char('e')) => Cmd::Editor(PhraseCommand::Show(Some(
state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone()
))),
// WSAD navigation, Q launches, E edits, PgUp/Down pool, Arrows editor
_ => match state.focused() {
ArrangerFocus::Transport(_) => {
match to_transport_command(state, input)? {
TransportCommand::Clock(command) => Cmd::Clock(command),
_ => return None,
}
},
ArrangerFocus::PhraseEditor => {
Cmd::Editor(PhraseCommand::input_to_command(&state.editor, input)?)
},
ArrangerFocus::Phrases => {
Cmd::Phrases(PhrasesCommand::input_to_command(&state.phrases, input)?)
},
ArrangerFocus::Arranger => {
use ArrangerSelection::*;
match input.event() {
key!(Char('l')) => Cmd::Clip(ArrangerClipCommand::SetLoop(false)),
key!(Char('+')) => Cmd::Zoom(0), // TODO
key!(Char('=')) => Cmd::Zoom(0), // TODO
key!(Char('_')) => Cmd::Zoom(0), // TODO
key!(Char('-')) => Cmd::Zoom(0), // TODO
key!(Char('`')) => { todo!("toggle state mode") },
key!(Ctrl-Char('a')) => Cmd::Scene(ArrangerSceneCommand::Add),
key!(Ctrl-Char('t')) => Cmd::Track(ArrangerTrackCommand::Add),
_ => match state.selected() {
Mix => to_arranger_mix_command(input)?,
Track(t) => to_arranger_track_command(input, t)?,
Scene(s) => to_arranger_scene_command(input, s)?,
Clip(t, s) => to_arranger_clip_command(input, t, s)?,
}
}
}
}
})
}
fn to_arranger_mix_command (input: &TuiInput) -> Option<ArrangerCommand> {
use KeyCode::{Char, Down, Right, Delete};
use ArrangerCommand as Cmd;
use ArrangerSelection as Select;
Some(match input.event() {
key!(Down) => Cmd::Select(Select::Scene(0)),
key!(Right) => Cmd::Select(Select::Track(0)),
key!(Char(',')) => Cmd::Zoom(0),
key!(Char('.')) => Cmd::Zoom(0),
key!(Char('<')) => Cmd::Zoom(0),
key!(Char('>')) => Cmd::Zoom(0),
key!(Delete) => Cmd::Clear,
key!(Char('c')) => Cmd::Color(ItemColor::random()),
_ => return None
})
}
fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option<ArrangerCommand> {
use KeyCode::{Char, Down, Left, Right, Delete};
use ArrangerCommand as Cmd;
use ArrangerSelection as Select;
use ArrangerTrackCommand as Track;
Some(match input.event() {
key!(Down) => Cmd::Select(Select::Clip(t, 0)),
key!(Left) => Cmd::Select(if t > 0 { Select::Track(t - 1) } else { Select::Mix }),
key!(Right) => Cmd::Select(Select::Track(t + 1)),
key!(Char(',')) => Cmd::Track(Track::Swap(t, t - 1)),
key!(Char('.')) => Cmd::Track(Track::Swap(t, t + 1)),
key!(Char('<')) => Cmd::Track(Track::Swap(t, t - 1)),
key!(Char('>')) => Cmd::Track(Track::Swap(t, t + 1)),
key!(Delete) => Cmd::Track(Track::Delete(t)),
//key!(Char('c')) => Cmd::Track(Track::Color(t, ItemColor::random())),
_ => return None
})
}
fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option<ArrangerCommand> {
use KeyCode::{Char, Up, Down, Right, Enter, Delete};
use ArrangerCommand as Cmd;
use ArrangerSelection as Select;
use ArrangerSceneCommand as Scene;
Some(match input.event() {
key!(Up) => Cmd::Select(if s > 0 { Select::Scene(s - 1) } else { Select::Mix }),
key!(Down) => Cmd::Select(Select::Scene(s + 1)),
key!(Right) => Cmd::Select(Select::Clip(0, s)),
key!(Char(',')) => Cmd::Scene(Scene::Swap(s, s - 1)),
key!(Char('.')) => Cmd::Scene(Scene::Swap(s, s + 1)),
key!(Char('<')) => Cmd::Scene(Scene::Swap(s, s - 1)),
key!(Char('>')) => Cmd::Scene(Scene::Swap(s, s + 1)),
key!(Enter) => Cmd::Scene(Scene::Play(s)),
key!(Delete) => Cmd::Scene(Scene::Delete(s)),
//key!(Char('c')) => Cmd::Track(Scene::Color(s, ItemColor::random())),
_ => return None
})
}
fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option<ArrangerCommand> {
use KeyCode::{Char, Up, Down, Left, Right, Delete};
use ArrangerCommand as Cmd;
use ArrangerSelection as Select;
use ArrangerClipCommand as Clip;
Some(match input.event() {
key!(Up) => Cmd::Select(if s > 0 { Select::Clip(t, s - 1) } else { Select::Track(t) }),
key!(Down) => Cmd::Select(Select::Clip(t, s + 1)),
key!(Left) => Cmd::Select(if t > 0 { Select::Clip(t - 1, s) } else { Select::Scene(s) }),
key!(Right) => Cmd::Select(Select::Clip(t + 1, s)),
key!(Char(',')) => Cmd::Clip(Clip::Set(t, s, None)),
key!(Char('.')) => Cmd::Clip(Clip::Set(t, s, None)),
key!(Char('<')) => Cmd::Clip(Clip::Set(t, s, None)),
key!(Char('>')) => Cmd::Clip(Clip::Set(t, s, None)),
key!(Delete) => Cmd::Clip(Clip::Set(t, s, None)),
//key!(Char('c')) => Cmd::Clip(Clip::Color(t, s, ItemColor::random())),
//key!(Char('g')) => Cmd::Clip(Clip(Clip::Get(t, s))),
//key!(Char('s')) => Cmd::Clip(Clip(Clip::Set(t, s))),
_ => return None
})
}
impl TransportControl<ArrangerFocus> for ArrangerTui {
fn transport_focused (&self) -> Option<TransportFocus> {
match self.focus.inner() {
ArrangerFocus::Transport(focus) => Some(focus),
_ => None
}
}
}

View file

@ -232,26 +232,26 @@ impl Handle<Tui> for SamplerTui {
let mapped = &self.state.mapped;
let voices = &self.state.voices;
match from.event() {
key!(KeyCode::Up) => cursor.0 = if cursor.0 == 0 {
key_pat!(KeyCode::Up) => cursor.0 = if cursor.0 == 0 {
mapped.len() + unmapped.len() - 1
} else {
cursor.0 - 1
},
key!(KeyCode::Down) => {
key_pat!(KeyCode::Down) => {
cursor.0 = (cursor.0 + 1) % (mapped.len() + unmapped.len());
},
key!(KeyCode::Char('p')) => if let Some(sample) = self.sample() {
key_pat!(KeyCode::Char('p')) => if let Some(sample) = self.sample() {
voices.write().unwrap().push(Sample::play(sample, 0, &100.into()));
},
key!(KeyCode::Char('a')) => {
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!(KeyCode::Char('r')) => if let Some(sample) = self.sample() {
key_pat!(KeyCode::Char('r')) => if let Some(sample) = self.sample() {
*self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?));
},
key!(KeyCode::Enter) => if let Some(sample) = self.sample() {
key_pat!(KeyCode::Enter) => if let Some(sample) = self.sample() {
self.editing = Some(sample.clone());
},
_ => {

View file

@ -126,21 +126,21 @@ pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option<S
Some(match input.event() {
// Switch between editor and list
key!(Tab) | key!(BackTab) | key!(Shift-Tab) | key!(Shift-BackTab) => match state.focus {
key_pat!(Tab) | key_pat!(BackTab) | key_pat!(Shift-Tab) | key_pat!(Shift-BackTab) => match state.focus {
PhraseEditor => SequencerCommand::Focus(FocusCommand::Set(PhraseList)),
_ => SequencerCommand::Focus(FocusCommand::Set(PhraseEditor)),
}
// Enqueue currently edited phrase
key!(Char('q')) =>
key_pat!(Char('q')) =>
Enqueue(Some(state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone())),
// 0: Enqueue phrase 0 (stop all)
key!(Char('0')) =>
key_pat!(Char('0')) =>
Enqueue(Some(state.phrases.phrases[0].clone())),
// E: Toggle between editing currently playing or other phrase
key!(Char('e')) => if let Some((_, Some(playing_phrase))) = state.player.play_phrase() {
key_pat!(Char('e')) => if let Some((_, Some(playing_phrase))) = state.player.play_phrase() {
let editing_phrase = state.editor.phrase()
.read().unwrap().as_ref()
.map(|p|p.read().unwrap().clone());
@ -155,19 +155,19 @@ pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option<S
},
// Transport: Play/pause
key!(Char(' ')) =>
key_pat!(Char(' ')) =>
Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }),
// Transport: Play from start or rewind to start
key!(Shift-Char(' ')) =>
key_pat!(Shift-Char(' ')) =>
Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }),
// Editor: zoom
key!(Char('z')) | key!(Char('-')) | key!(Char('_'))| key!(Char('=')) | key!(Char('+')) =>
key_pat!(Char('z')) | key_pat!(Char('-')) | key_pat!(Char('_'))| key_pat!(Char('=')) | key_pat!(Char('+')) =>
Editor(PhraseCommand::input_to_command(&state.editor, input)?),
// List: select phrase to edit, change color
key!(Char('[')) | key!(Char(']')) | key!(Char('c')) | key!(Shift-Char('A')) | key!(Shift-Char('D')) =>
key_pat!(Char('[')) | key_pat!(Char(']')) | key_pat!(Char('c')) | key_pat!(Shift-Char('A')) | key_pat!(Shift-Char('D')) =>
Phrases(PhrasesCommand::input_to_command(&state.phrases, input)?),
// Delegate to focused control:

View file

@ -265,56 +265,56 @@ where
U: Into<Option<TransportFocus>>,
{
Some(match input.event() {
key!(Left) => Focus(Prev),
key!(Right) => Focus(Next),
key!(Char(' ')) => Clock(if state.clock().is_stopped() {
key_pat!(Left) => Focus(Prev),
key_pat!(Right) => Focus(Next),
key_pat!(Char(' ')) => Clock(if state.clock().is_stopped() {
Play(None)
} else {
Pause(None)
}),
key!(Shift-Char(' ')) => Clock(if state.clock().is_stopped() {
key_pat!(Shift-Char(' ')) => Clock(if state.clock().is_stopped() {
Play(Some(0))
} else {
Pause(Some(0))
}),
_ => match state.transport_focused().unwrap() {
TransportFocus::Bpm => match input.event() {
key!(Char(',')) => Clock(SetBpm(state.clock().bpm().get() - 1.0)),
key!(Char('.')) => Clock(SetBpm(state.clock().bpm().get() + 1.0)),
key!(Char('<')) => Clock(SetBpm(state.clock().bpm().get() - 0.001)),
key!(Char('>')) => Clock(SetBpm(state.clock().bpm().get() + 0.001)),
key_pat!(Char(',')) => Clock(SetBpm(state.clock().bpm().get() - 1.0)),
key_pat!(Char('.')) => Clock(SetBpm(state.clock().bpm().get() + 1.0)),
key_pat!(Char('<')) => Clock(SetBpm(state.clock().bpm().get() - 0.001)),
key_pat!(Char('>')) => Clock(SetBpm(state.clock().bpm().get() + 0.001)),
_ => return None,
},
TransportFocus::Quant => match input.event() {
key!(Char(',')) => Clock(SetQuant(state.clock().quant.prev())),
key!(Char('.')) => Clock(SetQuant(state.clock().quant.next())),
key!(Char('<')) => Clock(SetQuant(state.clock().quant.prev())),
key!(Char('>')) => Clock(SetQuant(state.clock().quant.next())),
key_pat!(Char(',')) => Clock(SetQuant(state.clock().quant.prev())),
key_pat!(Char('.')) => Clock(SetQuant(state.clock().quant.next())),
key_pat!(Char('<')) => Clock(SetQuant(state.clock().quant.prev())),
key_pat!(Char('>')) => Clock(SetQuant(state.clock().quant.next())),
_ => return None,
},
TransportFocus::Sync => match input.event() {
key!(Char(',')) => Clock(SetSync(state.clock().sync.prev())),
key!(Char('.')) => Clock(SetSync(state.clock().sync.next())),
key!(Char('<')) => Clock(SetSync(state.clock().sync.prev())),
key!(Char('>')) => Clock(SetSync(state.clock().sync.next())),
key_pat!(Char(',')) => Clock(SetSync(state.clock().sync.prev())),
key_pat!(Char('.')) => Clock(SetSync(state.clock().sync.next())),
key_pat!(Char('<')) => Clock(SetSync(state.clock().sync.prev())),
key_pat!(Char('>')) => Clock(SetSync(state.clock().sync.next())),
_ => return None,
},
TransportFocus::Clock => match input.event() {
key!(Char(',')) => todo!("transport seek bar"),
key!(Char('.')) => todo!("transport seek bar"),
key!(Char('<')) => todo!("transport seek beat"),
key!(Char('>')) => todo!("transport seek beat"),
key_pat!(Char(',')) => todo!("transport seek bar"),
key_pat!(Char('.')) => todo!("transport seek bar"),
key_pat!(Char('<')) => todo!("transport seek beat"),
key_pat!(Char('>')) => todo!("transport seek beat"),
_ => return None,
},
TransportFocus::PlayPause => match input.event() {
key!(Enter) => Clock(
key_pat!(Enter) => Clock(
if state.clock().is_stopped() {
Play(None)
} else {
Pause(None)
}
),
key!(Shift-Enter) => Clock(
key_pat!(Shift-Enter) => Clock(
if state.clock().is_stopped() {
Play(Some(0))
} else {

View file

@ -143,30 +143,30 @@ impl InputToCommand<Tui, PhraseListModel> for FileBrowserCommand {
fn input_to_command (state: &PhraseListModel, from: &TuiInput) -> Option<Self> {
if let Some(PhraseListMode::Import(_index, browser)) = state.phrases_mode() {
Some(match from.event() {
key!(Up) => Select(
key_pat!(Up) => Select(
browser.index.overflowing_sub(1).0.min(browser.len().saturating_sub(1))
),
key!(Down) => Select(
key_pat!(Down) => Select(
browser.index.saturating_add(1) % browser.len()
),
key!(Right) => Chdir(browser.cwd.clone()),
key!(Left) => Chdir(browser.cwd.clone()),
key!(Enter) => Confirm,
key!(Char(_)) => { todo!() },
key!(Backspace) => { todo!() },
key!(Esc) => Self::Cancel,
key_pat!(Right) => Chdir(browser.cwd.clone()),
key_pat!(Left) => Chdir(browser.cwd.clone()),
key_pat!(Enter) => Confirm,
key_pat!(Char(_)) => { todo!() },
key_pat!(Backspace) => { todo!() },
key_pat!(Esc) => Self::Cancel,
_ => return None
})
} else if let Some(PhraseListMode::Export(_index, browser)) = state.phrases_mode() {
Some(match from.event() {
key!(Up) => Select(browser.index.overflowing_sub(1).0.min(browser.len())),
key!(Down) => Select(browser.index.saturating_add(1) % browser.len()),
key!(Right) => Chdir(browser.cwd.clone()),
key!(Left) => Chdir(browser.cwd.clone()),
key!(Enter) => Confirm,
key!(Char(_)) => { todo!() },
key!(Backspace) => { todo!() },
key!(Esc) => Self::Cancel,
key_pat!(Up) => Select(browser.index.overflowing_sub(1).0.min(browser.len())),
key_pat!(Down) => Select(browser.index.saturating_add(1) % browser.len()),
key_pat!(Right) => Chdir(browser.cwd.clone()),
key_pat!(Left) => Chdir(browser.cwd.clone()),
key_pat!(Enter) => Confirm,
key_pat!(Char(_)) => { todo!() },
key_pat!(Backspace) => { todo!() },
key_pat!(Esc) => Self::Cancel,
_ => return None
})
} else {
@ -179,12 +179,12 @@ impl InputToCommand<Tui, PhraseListModel> for PhraseLengthCommand {
fn input_to_command (state: &PhraseListModel, from: &TuiInput) -> Option<Self> {
if let Some(PhraseListMode::Length(_, length, _)) = state.phrases_mode() {
Some(match from.event() {
key!(Up) => Self::Inc,
key!(Down) => Self::Dec,
key!(Right) => Self::Next,
key!(Left) => Self::Prev,
key!(Enter) => Self::Set(*length),
key!(Esc) => Self::Cancel,
key_pat!(Up) => Self::Inc,
key_pat!(Down) => Self::Dec,
key_pat!(Right) => Self::Next,
key_pat!(Left) => Self::Prev,
key_pat!(Enter) => Self::Set(*length),
key_pat!(Esc) => Self::Cancel,
_ => return None
})
} else {

View file

@ -38,39 +38,39 @@ impl InputToCommand<Tui, PhraseEditorModel> for PhraseCommand {
let length = state.phrase().read().unwrap().as_ref()
.map(|p|p.read().unwrap().length).unwrap_or(1);
Some(match from.event() {
key!(Char('`')) => ToggleDirection,
key!(Char('z')) => SetTimeZoomLock(!state.range().time_lock()),
key!(Char('-')) => SetTimeZoom(next_note_length(time_zoom)),
key!(Char('_')) => SetTimeZoom(next_note_length(time_zoom)),
key!(Char('=')) => SetTimeZoom(prev_note_length(time_zoom)),
key!(Char('+')) => SetTimeZoom(prev_note_length(time_zoom)),
key!(Char('a')) => AppendNote,
key!(Char('s')) => PutNote,
key_pat!(Char('`')) => ToggleDirection,
key_pat!(Char('z')) => SetTimeZoomLock(!state.range().time_lock()),
key_pat!(Char('-')) => SetTimeZoom(next_note_length(time_zoom)),
key_pat!(Char('_')) => SetTimeZoom(next_note_length(time_zoom)),
key_pat!(Char('=')) => SetTimeZoom(prev_note_length(time_zoom)),
key_pat!(Char('+')) => SetTimeZoom(prev_note_length(time_zoom)),
key_pat!(Char('a')) => AppendNote,
key_pat!(Char('s')) => PutNote,
// TODO: no triplet/dotted
key!(Char(',')) => SetNoteLength(prev_note_length(note_len)),
key!(Char('.')) => SetNoteLength(next_note_length(note_len)),
key_pat!(Char(',')) => SetNoteLength(prev_note_length(note_len)),
key_pat!(Char('.')) => SetNoteLength(next_note_length(note_len)),
// TODO: with triplet/dotted
key!(Char('<')) => SetNoteLength(prev_note_length(note_len)),
key!(Char('>')) => SetNoteLength(next_note_length(note_len)),
key_pat!(Char('<')) => SetNoteLength(prev_note_length(note_len)),
key_pat!(Char('>')) => SetNoteLength(next_note_length(note_len)),
// TODO: '/' set triplet, '?' set dotted
_ => match from.event() {
key!(Up) => SetNoteCursor(note_point + 1),
key!(Down) => SetNoteCursor(note_point.saturating_sub(1)),
key!(Left) => SetTimeCursor(time_point.saturating_sub(note_len)),
key!(Right) => SetTimeCursor((time_point + note_len) % length),
key!(Alt-Up) => SetNoteCursor(note_point + 3),
key!(Alt-Down) => SetNoteCursor(note_point.saturating_sub(3)),
key!(Alt-Left) => SetTimeCursor(time_point.saturating_sub(time_zoom)),
key!(Alt-Right) => SetTimeCursor((time_point + time_zoom) % length),
key_pat!(Up) => SetNoteCursor(note_point + 1),
key_pat!(Down) => SetNoteCursor(note_point.saturating_sub(1)),
key_pat!(Left) => SetTimeCursor(time_point.saturating_sub(note_len)),
key_pat!(Right) => SetTimeCursor((time_point + note_len) % length),
key_pat!(Alt-Up) => SetNoteCursor(note_point + 3),
key_pat!(Alt-Down) => SetNoteCursor(note_point.saturating_sub(3)),
key_pat!(Alt-Left) => SetTimeCursor(time_point.saturating_sub(time_zoom)),
key_pat!(Alt-Right) => SetTimeCursor((time_point + time_zoom) % length),
key!(Ctrl-Up) => SetNoteScroll(note_lo + 1),
key!(Ctrl-Down) => SetNoteScroll(note_lo.saturating_sub(1)),
key!(Ctrl-Left) => SetTimeScroll(time_start.saturating_sub(note_len)),
key!(Ctrl-Right) => SetTimeScroll(time_start + note_len),
key!(Ctrl-Alt-Up) => SetNoteScroll(note_point + 3),
key!(Ctrl-Alt-Down) => SetNoteScroll(note_point.saturating_sub(3)),
key!(Ctrl-Alt-Left) => SetTimeScroll(time_point.saturating_sub(time_zoom)),
key!(Ctrl-Alt-Right) => SetTimeScroll((time_point + time_zoom) % length),
key_pat!(Ctrl-Up) => SetNoteScroll(note_lo + 1),
key_pat!(Ctrl-Down) => SetNoteScroll(note_lo.saturating_sub(1)),
key_pat!(Ctrl-Left) => SetTimeScroll(time_start.saturating_sub(note_len)),
key_pat!(Ctrl-Right) => SetTimeScroll(time_start + note_len),
key_pat!(Ctrl-Alt-Up) => SetNoteScroll(note_point + 3),
key_pat!(Ctrl-Alt-Down) => SetNoteScroll(note_point.saturating_sub(3)),
key_pat!(Ctrl-Alt-Left) => SetTimeScroll(time_point.saturating_sub(time_zoom)),
key_pat!(Ctrl-Alt-Right) => SetTimeScroll((time_point + time_zoom) % length),
_ => return None
},
})

View file

@ -31,6 +31,135 @@ pub enum PhraseListMode {
Export(usize, FileBrowser),
}
#[derive(Clone, PartialEq, Debug)]
pub enum PhrasesCommand {
/// Update the contents of the phrase pool
Phrase(Pool),
/// Select a phrase from the phrase pool
Select(usize),
/// Rename a phrase
Rename(Rename),
/// Change the length of a phrase
Length(Length),
/// Import from file
Import(Browse),
/// Export to file
Export(Browse),
}
impl Command<PhraseListModel> for PhrasesCommand {
fn execute (self, state: &mut PhraseListModel) -> Perhaps<Self> {
use PhrasesCommand::*;
Ok(match self {
Phrase(command) => command.execute(state)?.map(Phrase),
Rename(command) => match command {
PhraseRenameCommand::Begin => {
let length = state.phrases()[state.phrase_index()].read().unwrap().length;
*state.phrases_mode_mut() = Some(
PhraseListMode::Length(state.phrase_index(), length, PhraseLengthFocus::Bar)
);
None
},
_ => command.execute(state)?.map(Rename)
},
Length(command) => match command {
PhraseLengthCommand::Begin => {
let name = state.phrases()[state.phrase_index()].read().unwrap().name.clone();
*state.phrases_mode_mut() = Some(
PhraseListMode::Rename(state.phrase_index(), name)
);
None
},
_ => command.execute(state)?.map(Length)
},
Import(command) => match command {
FileBrowserCommand::Begin => {
*state.phrases_mode_mut() = Some(
PhraseListMode::Import(state.phrase_index(), FileBrowser::new(None)?)
);
None
},
_ => command.execute(state)?.map(Import)
},
Export(command) => match command {
FileBrowserCommand::Begin => {
*state.phrases_mode_mut() = Some(
PhraseListMode::Export(state.phrase_index(), FileBrowser::new(None)?)
);
None
},
_ => command.execute(state)?.map(Export)
},
Select(phrase) => {
state.set_phrase_index(phrase);
None
},
})
}
}
impl InputToCommand<Tui, PhraseListModel> for PhrasesCommand {
fn input_to_command (state: &PhraseListModel, input: &TuiInput) -> Option<Self> {
Some(match state.phrases_mode() {
Some(PhraseListMode::Rename(..)) => Self::Rename(Rename::input_to_command(state, input)?),
Some(PhraseListMode::Length(..)) => Self::Length(Length::input_to_command(state, input)?),
Some(PhraseListMode::Import(..)) => Self::Import(Browse::input_to_command(state, input)?),
Some(PhraseListMode::Export(..)) => Self::Export(Browse::input_to_command(state, input)?),
_ => to_phrases_command(state, input)?
})
}
}
fn to_phrases_command (state: &PhraseListModel, input: &TuiInput) -> Option<PhrasesCommand> {
use KeyCode::{Up, Down, Delete, Char};
use PhrasesCommand as Cmd;
let index = state.phrase_index();
let count = state.phrases().len();
Some(match input.event() {
key_pat!(Char('n')) => Cmd::Rename(Rename::Begin),
key_pat!(Char('t')) => Cmd::Length(Length::Begin),
key_pat!(Char('m')) => Cmd::Import(Browse::Begin),
key_pat!(Char('x')) => Cmd::Export(Browse::Begin),
key_pat!(Char('c')) => Cmd::Phrase(Pool::SetColor(index, ItemColor::random())),
key_pat!(Char('[')) | key_pat!(Up) => Cmd::Select(
index.overflowing_sub(1).0.min(state.phrases().len() - 1)
),
key_pat!(Char(']')) | key_pat!(Down) => Cmd::Select(
index.saturating_add(1) % state.phrases().len()
),
key_pat!(Char('<')) => if index > 1 {
state.set_phrase_index(state.phrase_index().saturating_sub(1));
Cmd::Phrase(Pool::Swap(index - 1, index))
} else {
return None
},
key_pat!(Char('>')) => if index < count.saturating_sub(1) {
state.set_phrase_index(state.phrase_index() + 1);
Cmd::Phrase(Pool::Swap(index + 1, index))
} else {
return None
},
key_pat!(Delete) => if index > 0 {
state.set_phrase_index(index.min(count.saturating_sub(1)));
Cmd::Phrase(Pool::Delete(index))
} else {
return None
},
key_pat!(Char('a')) | key_pat!(Shift-Char('A')) => Cmd::Phrase(Pool::Add(count, Phrase::new(
String::from("(new)"), true, 4 * PPQ, None, Some(ItemPalette::random())
))),
key_pat!(Char('i')) => Cmd::Phrase(Pool::Add(index + 1, Phrase::new(
String::from("(new)"), true, 4 * PPQ, None, Some(ItemPalette::random())
))),
key_pat!(Char('d')) | key_pat!(Shift-Char('D')) => {
let mut phrase = state.phrases()[index].read().unwrap().duplicate();
phrase.color = ItemPalette::random_near(phrase.color, 0.25);
Cmd::Phrase(Pool::Add(index + 1, phrase))
},
_ => return None
})
}
impl Default for PhraseListModel {
fn default () -> Self {
Self {
@ -158,132 +287,3 @@ render!(|self: PhraseListView<'a>|{
add(&Tui::fill_xy(Tui::at_ne(Tui::pull_x(1, Tui::fg(title_color, upper_right.to_string())))))
}))
});
#[derive(Clone, PartialEq, Debug)]
pub enum PhrasesCommand {
/// Update the contents of the phrase pool
Phrase(Pool),
/// Select a phrase from the phrase pool
Select(usize),
/// Rename a phrase
Rename(Rename),
/// Change the length of a phrase
Length(Length),
/// Import from file
Import(Browse),
/// Export to file
Export(Browse),
}
impl Command<PhraseListModel> for PhrasesCommand {
fn execute (self, state: &mut PhraseListModel) -> Perhaps<Self> {
use PhrasesCommand::*;
Ok(match self {
Phrase(command) => command.execute(state)?.map(Phrase),
Rename(command) => match command {
PhraseRenameCommand::Begin => {
let length = state.phrases()[state.phrase_index()].read().unwrap().length;
*state.phrases_mode_mut() = Some(
PhraseListMode::Length(state.phrase_index(), length, PhraseLengthFocus::Bar)
);
None
},
_ => command.execute(state)?.map(Rename)
},
Length(command) => match command {
PhraseLengthCommand::Begin => {
let name = state.phrases()[state.phrase_index()].read().unwrap().name.clone();
*state.phrases_mode_mut() = Some(
PhraseListMode::Rename(state.phrase_index(), name)
);
None
},
_ => command.execute(state)?.map(Length)
},
Import(command) => match command {
FileBrowserCommand::Begin => {
*state.phrases_mode_mut() = Some(
PhraseListMode::Import(state.phrase_index(), FileBrowser::new(None)?)
);
None
},
_ => command.execute(state)?.map(Import)
},
Export(command) => match command {
FileBrowserCommand::Begin => {
*state.phrases_mode_mut() = Some(
PhraseListMode::Export(state.phrase_index(), FileBrowser::new(None)?)
);
None
},
_ => command.execute(state)?.map(Export)
},
Select(phrase) => {
state.set_phrase_index(phrase);
None
},
})
}
}
impl InputToCommand<Tui, PhraseListModel> for PhrasesCommand {
fn input_to_command (state: &PhraseListModel, input: &TuiInput) -> Option<Self> {
Some(match state.phrases_mode() {
Some(PhraseListMode::Rename(..)) => Self::Rename(Rename::input_to_command(state, input)?),
Some(PhraseListMode::Length(..)) => Self::Length(Length::input_to_command(state, input)?),
Some(PhraseListMode::Import(..)) => Self::Import(Browse::input_to_command(state, input)?),
Some(PhraseListMode::Export(..)) => Self::Export(Browse::input_to_command(state, input)?),
_ => to_phrases_command(state, input)?
})
}
}
fn to_phrases_command (state: &PhraseListModel, input: &TuiInput) -> Option<PhrasesCommand> {
use KeyCode::{Up, Down, Delete, Char};
use PhrasesCommand as Cmd;
let index = state.phrase_index();
let count = state.phrases().len();
Some(match input.event() {
key!(Char('n')) => Cmd::Rename(Rename::Begin),
key!(Char('t')) => Cmd::Length(Length::Begin),
key!(Char('m')) => Cmd::Import(Browse::Begin),
key!(Char('x')) => Cmd::Export(Browse::Begin),
key!(Char('c')) => Cmd::Phrase(Pool::SetColor(index, ItemColor::random())),
key!(Char('[')) | key!(Up) => Cmd::Select(
index.overflowing_sub(1).0.min(state.phrases().len() - 1)
),
key!(Char(']')) | key!(Down) => Cmd::Select(
index.saturating_add(1) % state.phrases().len()
),
key!(Char('<')) => if index > 1 {
state.set_phrase_index(state.phrase_index().saturating_sub(1));
Cmd::Phrase(Pool::Swap(index - 1, index))
} else {
return None
},
key!(Char('>')) => if index < count.saturating_sub(1) {
state.set_phrase_index(state.phrase_index() + 1);
Cmd::Phrase(Pool::Swap(index + 1, index))
} else {
return None
},
key!(Delete) => if index > 0 {
state.set_phrase_index(index.min(count.saturating_sub(1)));
Cmd::Phrase(Pool::Delete(index))
} else {
return None
},
key!(Char('a')) | key!(Shift-Char('A')) => Cmd::Phrase(Pool::Add(count, Phrase::new(
String::from("(new)"), true, 4 * PPQ, None, Some(ItemPalette::random())
))),
key!(Char('i')) => Cmd::Phrase(Pool::Add(index + 1, Phrase::new(
String::from("(new)"), true, 4 * PPQ, None, Some(ItemPalette::random())
))),
key!(Char('d')) | key!(Shift-Char('D')) => {
let mut phrase = state.phrases()[index].read().unwrap().duplicate();
phrase.color = ItemPalette::random_near(phrase.color, 0.25);
Cmd::Phrase(Pool::Add(index + 1, phrase))
},
_ => return None
})
}

View file

@ -38,18 +38,18 @@ impl InputToCommand<Tui, PhraseListModel> for PhraseRenameCommand {
use KeyCode::{Char, Backspace, Enter, Esc};
if let Some(PhraseListMode::Rename(_, ref old_name)) = state.phrases_mode() {
Some(match from.event() {
key!(Char(c)) => {
key_pat!(Char(c)) => {
let mut new_name = old_name.clone();
new_name.push(*c);
Self::Set(new_name)
},
key!(Backspace) => {
key_pat!(Backspace) => {
let mut new_name = old_name.clone();
new_name.pop();
Self::Set(new_name)
},
key!(Enter) => Self::Confirm,
key!(Esc) => Self::Cancel,
key_pat!(Enter) => Self::Confirm,
key_pat!(Esc) => Self::Cancel,
_ => return None
})
} else {

View file

@ -28,51 +28,58 @@ impl Input<Tui> for TuiInput {
}
}
//#[macro_export] macro_rules! key_pat {
//}
//#[macro_export] macro_rules! key_expr {
//}
#[macro_export] macro_rules! key_event_pat {
($code:pat, $modifiers: pat) => {
TuiEvent::Input(crossterm::event::Event::Key(KeyEvent {
code: $code,
modifiers: $modifiers,
kind: KeyEventKind::Press,
state: KeyEventState::NONE
}))
};
($code:pat) => {
TuiEvent::Input(crossterm::event::Event::Key(KeyEvent {
code: $code,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
state: KeyEventState::NONE
}))
};
}
/// Define key pattern in key match statement
#[macro_export] macro_rules! key {
(Ctrl-Alt-$code:pat) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code,
modifiers: KeyModifiers::CONTROL | KeyModifiers::ALT,
kind: KeyEventKind::Press,
state: KeyEventState::NONE
})) };
(Ctrl-Alt-$code:expr) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code,
modifiers: KeyModifiers::CONTROL | KeyModifiers::ALT,
kind: KeyEventKind::Press,
state: KeyEventState::NONE
})) };
#[macro_export] macro_rules! key_event_expr {
($code:expr, $modifiers: expr) => {
TuiEvent::Input(crossterm::event::Event::Key(KeyEvent {
code: $code,
modifiers: $modifiers,
kind: KeyEventKind::Press,
state: KeyEventState::NONE
}))
};
($code:expr) => {
TuiEvent::Input(crossterm::event::Event::Key(KeyEvent {
code: $code,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
state: KeyEventState::NONE
}))
};
}
(Ctrl-$code:pat) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code,
modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, state: KeyEventState::NONE
})) };
#[macro_export] macro_rules! key_pat {
(Ctrl-Alt-$code:pat) => { key_event_pat!($code, KeyModifiers::CONTROL | KeyModifiers::ALT) };
(Ctrl-$code:pat) => { key_event_pat!($code, KeyModifiers::CONTROL) };
(Alt-$code:pat) => { key_event_pat!($code, KeyModifiers::ALT) };
(Shift-$code:pat) => { key_event_pat!($code, KeyModifiers::SHIFT) };
($code:pat) => { key_event_pat!($code) };
}
(Ctrl-$code:expr) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code,
modifiers: KeyModifiers::CONTROL, kind: KeyEventKind::Press, state: KeyEventState::NONE
})) };
(Alt-$code:pat) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code,
modifiers: KeyModifiers::ALT, kind: KeyEventKind::Press, state: KeyEventState::NONE
})) };
(Alt-$code:expr) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code,
modifiers: KeyModifiers::ALT, kind: KeyEventKind::Press, state: KeyEventState::NONE
})) };
(Shift-$code:pat) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code,
modifiers: KeyModifiers::SHIFT, kind: KeyEventKind::Press, state: KeyEventState::NONE
})) };
(Shift-$code:expr) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code,
modifiers: KeyModifiers::SHIFT, kind: KeyEventKind::Press, state: KeyEventState::NONE
})) };
($code:pat) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code,
modifiers: KeyModifiers::NONE, kind: KeyEventKind::Press, state: KeyEventState::NONE
})) };
($code:expr) => { TuiEvent::Input(crossterm::event::Event::Key(KeyEvent { code: $code,
modifiers: KeyModifiers::NONE, kind: KeyEventKind::Press, state: KeyEventState::NONE
})) };
#[macro_export] macro_rules! key_expr {
(Ctrl-Alt-$code:expr) => { key_event_expr!($code, KeyModifiers::CONTROL | KeyModifiers::ALT) };
(Ctrl-$code:expr) => { key_event_expr!($code, KeyModifiers::CONTROL) };
(Alt-$code:expr) => { key_event_expr!($code, KeyModifiers::ALT) };
(Shift-$code:expr) => { key_event_expr!($code, KeyModifiers::SHIFT) };
($code:expr) => { key_event_expr!($code) };
}
/*