From 29abe29504163d96c0cca68bbb4be99a018b91eb Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 14 Dec 2024 19:13:28 +0100 Subject: [PATCH] split key macro into key_pat and key_expr --- crates/tek/src/api.rs | 2 - crates/tek/src/core/focus.rs | 20 +- crates/tek/src/core/pitch.rs | 2 - crates/tek/src/tui.rs | 2 +- crates/tek/src/tui/app_arranger.rs | 546 ++++++++++++++-------------- crates/tek/src/tui/app_sampler.rs | 12 +- crates/tek/src/tui/app_sequencer.rs | 16 +- crates/tek/src/tui/app_transport.rs | 44 +-- crates/tek/src/tui/file_browser.rs | 44 +-- crates/tek/src/tui/phrase_editor.rs | 56 +-- crates/tek/src/tui/phrase_list.rs | 258 ++++++------- crates/tek/src/tui/phrase_rename.rs | 8 +- crates/tek/src/tui/tui_input.rs | 91 ++--- 13 files changed, 550 insertions(+), 551 deletions(-) diff --git a/crates/tek/src/api.rs b/crates/tek/src/api.rs index 88951c7a..013b4388 100644 --- a/crates/tek/src/api.rs +++ b/crates/tek/src/api.rs @@ -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::*; diff --git a/crates/tek/src/core/focus.rs b/crates/tek/src/core/focus.rs index fe16eeae..0bae0cb6 100644 --- a/crates/tek/src/core/focus.rs +++ b/crates/tek/src/core/focus.rs @@ -259,16 +259,16 @@ pub trait FocusWrap { pub fn to_focus_command (input: &TuiInput) -> Option> { 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 }) } diff --git a/crates/tek/src/core/pitch.rs b/crates/tek/src/core/pitch.rs index 64a2c6d8..e442c60f 100644 --- a/crates/tek/src/core/pitch.rs +++ b/crates/tek/src/core/pitch.rs @@ -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"); diff --git a/crates/tek/src/tui.rs b/crates/tek/src/tui.rs index 3a7325f0..0fdb9d50 100644 --- a/crates/tek/src/tui.rs +++ b/crates/tek/src/tui.rs @@ -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); }, _ => { diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index 1860b848..0c1080fb 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -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>> for ArrangerTui { type Error = Box; @@ -61,6 +57,273 @@ pub struct ArrangerTui { pub perf: PerfModel, } +impl Handle for ArrangerTui { + fn handle (&mut self, i: &TuiInput) -> Perhaps { + ArrangerCommand::execute_with_state(self, i) + } +} + +#[derive(Clone, Debug)] +pub enum ArrangerCommand { + Focus(FocusCommand), + Undo, + Redo, + Clear, + Color(ItemColor), + Clock(ClockCommand), + Scene(ArrangerSceneCommand), + Track(ArrangerTrackCommand), + Clip(ArrangerClipCommand), + Select(ArrangerSelection), + Zoom(usize), + Phrases(PhrasesCommand), + Editor(PhraseCommand), +} + +impl Command for ArrangerCommand { + fn execute (self, state: &mut ArrangerTui) -> Perhaps { + 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 for ArrangerSceneCommand { + fn execute (self, _state: &mut ArrangerTui) -> Perhaps { + //todo!(); + Ok(None) + } +} + +impl Command for ArrangerTrackCommand { + fn execute (self, _state: &mut ArrangerTui) -> Perhaps { + //todo!(); + Ok(None) + } +} + +impl Command for ArrangerClipCommand { + fn execute (self, _state: &mut ArrangerTui) -> Perhaps { + //todo!(); + Ok(None) + } +} + +pub trait ArrangerControl: TransportControl { + fn selected (&self) -> ArrangerSelection; + fn selected_mut (&mut self) -> &mut ArrangerSelection; + fn activate (&mut self) -> Usually<()>; + fn selected_phrase (&self) -> Option>>; + 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>> { + 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 for ArrangerCommand { + fn input_to_command (state: &ArrangerTui, input: &TuiInput) -> Option { + to_arranger_command(state, input) + .or_else(||to_focus_command(input).map(ArrangerCommand::Focus)) + } +} + + +fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option { + 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 { + 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 { + 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 { + 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 { + 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 for ArrangerTui { + fn transport_focused (&self) -> Option { + 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 for ArrangerTui { - fn handle (&mut self, i: &TuiInput) -> Perhaps { - ArrangerCommand::execute_with_state(self, i) - } -} - -#[derive(Clone, Debug)] -pub enum ArrangerCommand { - Focus(FocusCommand), - Undo, - Redo, - Clear, - Color(ItemColor), - Clock(ClockCommand), - Scene(ArrangerSceneCommand), - Track(ArrangerTrackCommand), - Clip(ArrangerClipCommand), - Select(ArrangerSelection), - Zoom(usize), - Phrases(PhrasesCommand), - Editor(PhraseCommand), -} - -impl Command for ArrangerCommand { - fn execute (self, state: &mut ArrangerTui) -> Perhaps { - 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 for ArrangerSceneCommand { - fn execute (self, _state: &mut ArrangerTui) -> Perhaps { - //todo!(); - Ok(None) - } -} - -impl Command for ArrangerTrackCommand { - fn execute (self, _state: &mut ArrangerTui) -> Perhaps { - //todo!(); - Ok(None) - } -} - -impl Command for ArrangerClipCommand { - fn execute (self, _state: &mut ArrangerTui) -> Perhaps { - //todo!(); - Ok(None) - } -} - -pub trait ArrangerControl: TransportControl { - fn selected (&self) -> ArrangerSelection; - fn selected_mut (&mut self) -> &mut ArrangerSelection; - fn activate (&mut self) -> Usually<()>; - fn selected_phrase (&self) -> Option>>; - 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>> { - 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 for ArrangerCommand { - fn input_to_command (state: &ArrangerTui, input: &TuiInput) -> Option { - to_arranger_command(state, input) - .or_else(||to_focus_command(input).map(ArrangerCommand::Focus)) - } -} - - -fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option { - 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 { - 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 { - 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 { - 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 { - 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 for ArrangerTui { - fn transport_focused (&self) -> Option { - match self.focus.inner() { - ArrangerFocus::Transport(focus) => Some(focus), - _ => None - } - } -} diff --git a/crates/tek/src/tui/app_sampler.rs b/crates/tek/src/tui/app_sampler.rs index 728d0b6c..2fa6b9a2 100644 --- a/crates/tek/src/tui/app_sampler.rs +++ b/crates/tek/src/tui/app_sampler.rs @@ -232,26 +232,26 @@ impl Handle 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()); }, _ => { diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 7508c363..ee2a88e4 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -126,21 +126,21 @@ pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option 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 + 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: diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index f618eb48..b67d0e31 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -265,56 +265,56 @@ where U: Into>, { 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 { diff --git a/crates/tek/src/tui/file_browser.rs b/crates/tek/src/tui/file_browser.rs index 26a40c30..5baee32c 100644 --- a/crates/tek/src/tui/file_browser.rs +++ b/crates/tek/src/tui/file_browser.rs @@ -143,30 +143,30 @@ impl InputToCommand for FileBrowserCommand { fn input_to_command (state: &PhraseListModel, from: &TuiInput) -> Option { 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 for PhraseLengthCommand { fn input_to_command (state: &PhraseListModel, from: &TuiInput) -> Option { 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 { diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs index 0af662bd..0f001e24 100644 --- a/crates/tek/src/tui/phrase_editor.rs +++ b/crates/tek/src/tui/phrase_editor.rs @@ -38,39 +38,39 @@ impl InputToCommand 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 }, }) diff --git a/crates/tek/src/tui/phrase_list.rs b/crates/tek/src/tui/phrase_list.rs index 5d862dcd..2967a542 100644 --- a/crates/tek/src/tui/phrase_list.rs +++ b/crates/tek/src/tui/phrase_list.rs @@ -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 for PhrasesCommand { + fn execute (self, state: &mut PhraseListModel) -> Perhaps { + 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 for PhrasesCommand { + fn input_to_command (state: &PhraseListModel, input: &TuiInput) -> Option { + 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 { + 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 for PhrasesCommand { - fn execute (self, state: &mut PhraseListModel) -> Perhaps { - 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 for PhrasesCommand { - fn input_to_command (state: &PhraseListModel, input: &TuiInput) -> Option { - 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 { - 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 - }) -} diff --git a/crates/tek/src/tui/phrase_rename.rs b/crates/tek/src/tui/phrase_rename.rs index c2bab07a..42233cfc 100644 --- a/crates/tek/src/tui/phrase_rename.rs +++ b/crates/tek/src/tui/phrase_rename.rs @@ -38,18 +38,18 @@ impl InputToCommand 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 { diff --git a/crates/tek/src/tui/tui_input.rs b/crates/tek/src/tui/tui_input.rs index 2d6f5686..96b7a88e 100644 --- a/crates/tek/src/tui/tui_input.rs +++ b/crates/tek/src/tui/tui_input.rs @@ -28,51 +28,58 @@ impl Input 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) }; } /*