From efda18293d08230fca2ba1077079c3e27deb74c4 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 18 Dec 2024 13:28:03 +0100 Subject: [PATCH] apply from_jack! --- crates/tek/src/api/jack.rs | 31 +- crates/tek/src/tui/app_arranger.rs | 569 +++++++++++----------------- crates/tek/src/tui/app_groovebox.rs | 36 +- crates/tek/src/tui/app_sampler.rs | 8 +- crates/tek/src/tui/app_sequencer.rs | 236 ++++-------- crates/tek/src/tui/app_transport.rs | 48 +-- crates/tek/src/tui/status_bar.rs | 176 ++++++++- 7 files changed, 522 insertions(+), 582 deletions(-) diff --git a/crates/tek/src/api/jack.rs b/crates/tek/src/api/jack.rs index 79a80d54..6ce07fcd 100644 --- a/crates/tek/src/api/jack.rs +++ b/crates/tek/src/api/jack.rs @@ -1,6 +1,18 @@ use crate::*; -/// Trait for things that have a JACK process callback. +/// Implement [TryFrom<&Arc>>]: create app state from wrapped JACK handle. +#[macro_export] macro_rules! from_jack { + (|$jack:ident|$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)? $cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? TryFrom<&Arc>> for $Struct $(<$($L),*$($T),*>)? { + type Error = Box; + fn try_from ($jack: &Arc>) -> Usually { + Ok($cb) + } + } + }; +} + +/// Trait for thing that has a JACK process callback. pub trait Audio: Send + Sync { fn process (&mut self, _: &Client, _: &ProcessScope) -> Control { Control::Continue @@ -16,6 +28,7 @@ pub trait Audio: Send + Sync { } } +/// Implement [Audio]: provide JACK callbacks. #[macro_export] macro_rules! audio { (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident|$cb:expr) => { impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? { @@ -24,17 +37,7 @@ pub trait Audio: Send + Sync { } } -#[macro_export] macro_rules! from_jack { - (|$jack:ident|$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)? $cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? TryFrom<&Arc>> for $Struct $(<$($L),*$($T),*>)? { - type Error = Box; - fn try_from ($jack: &Arc>) -> Usually { - Ok($cb) - } - } - }; -} - +/// Trait for thing that may receive MIDI. pub trait HasMidiIns { fn midi_ins (&self) -> &Vec>; fn midi_ins_mut (&mut self) -> &mut Vec>; @@ -43,13 +46,15 @@ pub trait HasMidiIns { } } +/// Trait for thing that may output MIDI. pub trait HasMidiOuts { fn midi_outs (&self) -> &Vec>; fn midi_outs_mut (&mut self) -> &mut Vec>; - fn midi_note (&mut self) -> &mut Vec; fn has_midi_outs (&self) -> bool { self.midi_outs().len() > 0 } + /// Buffer for serializing a MIDI event. FIXME rename + fn midi_note (&mut self) -> &mut Vec; } //////////////////////////////////////////////////////////////////////////////////// diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs index b2c8c6d4..287b659c 100644 --- a/crates/tek/src/tui/app_arranger.rs +++ b/crates/tek/src/tui/app_arranger.rs @@ -2,35 +2,6 @@ use crate::*; use crate::api::ArrangerTrackCommand; use crate::api::ArrangerSceneCommand; -impl TryFrom<&Arc>> for ArrangerTui { - type Error = Box; - fn try_from (jack: &Arc>) -> Usually { - Ok(Self { - jack: jack.clone(), - clock: ClockModel::from(jack), - phrases: PhraseListModel::default(), - editor: PhraseEditorModel::default(), - selected: ArrangerSelection::Clip(0, 0), - scenes: vec![], - tracks: vec![], - color: TuiTheme::bg().into(), - history: vec![], - mode: ArrangerMode::Vertical(2), - name: Arc::new(RwLock::new(String::new())), - size: Measure::new(), - cursor: (0, 0), - splits: [20, 20], - entered: false, - menu_bar: None, - status_bar: None, - midi_buf: vec![vec![];65536], - note_buf: vec![], - perf: PerfModel::default(), - focus: FocusState::Entered(ArrangerFocus::Transport(TransportFocus::PlayPause)), - }) - } -} - /// Root view for standalone `tek_arranger` pub struct ArrangerTui { pub jack: Arc>, @@ -55,7 +26,29 @@ pub struct ArrangerTui { pub focus: FocusState, pub perf: PerfModel, } - +from_jack!(|jack| ArrangerTui Self { + jack: jack.clone(), + clock: ClockModel::from(jack), + phrases: PhraseListModel::default(), + editor: PhraseEditorModel::default(), + selected: ArrangerSelection::Clip(0, 0), + scenes: vec![], + tracks: vec![], + color: TuiTheme::bg().into(), + history: vec![], + mode: ArrangerMode::Vertical(2), + name: Arc::new(RwLock::new(String::new())), + size: Measure::new(), + cursor: (0, 0), + splits: [20, 20], + entered: false, + menu_bar: None, + status_bar: None, + midi_buf: vec![vec![];65536], + note_buf: vec![], + perf: PerfModel::default(), + focus: FocusState::Entered(ArrangerFocus::Transport(TransportFocus::PlayPause)), +}); has_clock!(|self:ArrangerTui|&self.clock); has_phrases!(|self:ArrangerTui|self.phrases.phrases); has_editor!(|self:ArrangerTui|self.editor); @@ -146,7 +139,6 @@ impl HasPhraseList for ArrangerTui { self.phrases.phrase.load(Ordering::Relaxed) } } - #[derive(Clone, Debug)] pub enum ArrangerCommand { Focus(FocusCommand), @@ -163,7 +155,6 @@ pub enum ArrangerCommand { Phrases(PhrasesCommand), Editor(PhraseCommand), } - impl Command for ArrangerCommand { fn execute (self, state: &mut ArrangerTui) -> Perhaps { use ArrangerCommand::*; @@ -184,28 +175,24 @@ impl Command for ArrangerCommand { }) } } - 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; @@ -214,7 +201,6 @@ pub trait ArrangerControl: TransportControl { fn toggle_loop (&mut self); fn randomize_color (&mut self); } - impl ArrangerControl for ArrangerTui { fn selected (&self) -> ArrangerSelection { self.selected @@ -272,8 +258,6 @@ impl InputToCommand for ArrangerCommand { .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; @@ -318,7 +302,6 @@ fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option Option { use KeyCode::{Char, Down, Right, Delete}; use ArrangerCommand as Cmd; @@ -335,7 +318,6 @@ fn to_arranger_mix_command (input: &TuiInput) -> Option { _ => return None }) } - fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option { use KeyCode::{Char, Down, Left, Right, Delete}; use ArrangerCommand as Cmd; @@ -354,7 +336,6 @@ fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option return None }) } - fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option { use KeyCode::{Char, Up, Down, Right, Enter, Delete}; use ArrangerCommand as Cmd; @@ -374,7 +355,6 @@ fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option 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; @@ -396,7 +376,6 @@ fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option return None }) } - impl TransportControl for ArrangerTui { fn transport_focused (&self) -> Option { match self.focus.inner() { @@ -440,7 +419,7 @@ impl From<&ArrangerTui> for Option { } } -impl_focus!(ArrangerTui ArrangerFocus [ +impl_focus!(ArrangerTui ArrangerFocus [ //&[ //Menu, //Menu, @@ -482,116 +461,6 @@ pub enum ArrangerStatus { PhraseEdit, } -impl StatusBar for ArrangerStatus { - type State = (ArrangerFocus, ArrangerSelection, bool); - fn hotkey_fg () -> Color where Self: Sized { - TuiTheme::HOTKEY_FG - } - fn update (&mut self, (focused, selected, entered): &Self::State) { - *self = match focused { - //ArrangerFocus::Menu => { todo!() }, - ArrangerFocus::Transport(_) => ArrangerStatus::Transport, - ArrangerFocus::Arranger => match selected { - ArrangerSelection::Mix => ArrangerStatus::ArrangerMix, - ArrangerSelection::Track(_) => ArrangerStatus::ArrangerTrack, - ArrangerSelection::Scene(_) => ArrangerStatus::ArrangerScene, - ArrangerSelection::Clip(_, _) => ArrangerStatus::ArrangerClip, - }, - ArrangerFocus::Phrases => ArrangerStatus::PhrasePool, - ArrangerFocus::PhraseEditor => match entered { - true => ArrangerStatus::PhraseEdit, - false => ArrangerStatus::PhraseView, - }, - } - } -} - -render!(|self: ArrangerStatus|{ - - let label = match self { - Self::Transport => "TRANSPORT", - Self::ArrangerMix => "PROJECT", - Self::ArrangerTrack => "TRACK", - Self::ArrangerScene => "SCENE", - Self::ArrangerClip => "CLIP", - Self::PhrasePool => "SEQ LIST", - Self::PhraseView => "VIEW SEQ", - Self::PhraseEdit => "EDIT SEQ", - }; - - let status_bar_bg = TuiTheme::status_bar_bg(); - - let mode_bg = TuiTheme::mode_bg(); - let mode_fg = TuiTheme::mode_fg(); - let mode = Tui::fg(mode_fg, Tui::bg(mode_bg, Tui::bold(true, format!(" {label} ")))); - - let commands = match self { - Self::ArrangerMix => Self::command(&[ - ["", "c", "olor"], - ["", "<>", "resize"], - ["", "+-", "zoom"], - ["", "n", "ame/number"], - ["", "Enter", " stop all"], - ]), - Self::ArrangerClip => Self::command(&[ - ["", "g", "et"], - ["", "s", "et"], - ["", "a", "dd"], - ["", "i", "ns"], - ["", "d", "up"], - ["", "e", "dit"], - ["", "c", "olor"], - ["re", "n", "ame"], - ["", ",.", "select"], - ["", "Enter", " launch"], - ]), - Self::ArrangerTrack => Self::command(&[ - ["re", "n", "ame"], - ["", ",.", "resize"], - ["", "<>", "move"], - ["", "i", "nput"], - ["", "o", "utput"], - ["", "m", "ute"], - ["", "s", "olo"], - ["", "Del", "ete"], - ["", "Enter", " stop"], - ]), - Self::ArrangerScene => Self::command(&[ - ["re", "n", "ame"], - ["", "Del", "ete"], - ["", "Enter", " launch"], - ]), - Self::PhrasePool => Self::command(&[ - ["", "a", "ppend"], - ["", "i", "nsert"], - ["", "d", "uplicate"], - ["", "Del", "ete"], - ["", "c", "olor"], - ["re", "n", "ame"], - ["leng", "t", "h"], - ["", ",.", "move"], - ["", "+-", "resize view"], - ]), - Self::PhraseView => Self::command(&[ - ["", "enter", " edit"], - ["", "arrows/pgup/pgdn", " scroll"], - ["", "+=", "zoom"], - ]), - Self::PhraseEdit => Self::command(&[ - ["", "esc", " exit"], - ["", "a", "ppend"], - ["", "s", "et"], - ["", "][", "length"], - ["", "+-", "zoom"], - ]), - _ => Self::command(&[]) - }; - - //let commands = commands.iter().reduce(String::new(), |s, (a, b, c)| format!("{s} {a}{b}{c}")); - Tui::bg(status_bar_bg, Fill::w(row!([mode, commands]))) - -}); - /// Display mode of arranger #[derive(Clone, PartialEq)] pub enum ArrangerMode { @@ -912,201 +781,6 @@ render!(|self: ArrangerVerticalContent<'a>|Fixed::h( }) )); -pub fn arranger_content_horizontal ( - view: &ArrangerTui, -) -> impl Render + use<'_> { - todo!() -} - //let focused = view.arranger_focused(); - //let _tracks = view.tracks(); - //lay!( - //focused.then_some(Background(TuiTheme::border_bg())), - //row!( - //// name - //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ - //todo!() - ////let Self(tracks, selected) = self; - ////let yellow = Some(Style::default().yellow().bold().not_dim()); - ////let white = Some(Style::default().white().bold().not_dim()); - ////let area = to.area(); - ////let area = [area.x(), area.y(), 3 + 5.max(track_name_max_len(tracks)) as u16, area.h()]; - ////let offset = 0; // track scroll offset - ////for y in 0..area.h() { - ////if y == 0 { - ////to.blit(&"Mixer", area.x() + 1, area.y() + y, Some(DIM))?; - ////} else if y % 2 == 0 { - ////let index = (y as usize - 2) / 2 + offset; - ////if let Some(track) = tracks.get(index) { - ////let selected = selected.track() == Some(index); - ////let style = if selected { yellow } else { white }; - ////to.blit(&format!(" {index:>02} "), area.x(), area.y() + y, style)?; - ////to.blit(&*track.name.read().unwrap(), area.x() + 4, area.y() + y, style)?; - ////} - ////} - ////} - ////Ok(Some(area)) - //}), - //// monitor - //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ - //todo!() - ////let Self(tracks) = self; - ////let mut area = to.area(); - ////let on = Some(Style::default().not_dim().green().bold()); - ////let off = Some(DIM); - ////area.x += 1; - ////for y in 0..area.h() { - ////if y == 0 { - //////" MON ".blit(to.buffer, area.x, area.y + y, style2)?; - ////} else if y % 2 == 0 { - ////let index = (y as usize - 2) / 2; - ////if let Some(track) = tracks.get(index) { - ////let style = if track.monitoring { on } else { off }; - ////to.blit(&" MON ", area.x(), area.y() + y, style)?; - ////} else { - ////area.height = y; - ////break - ////} - ////} - ////} - ////area.width = 4; - ////Ok(Some(area)) - //}), - //// record - //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ - //todo!() - ////let Self(tracks) = self; - ////let mut area = to.area(); - ////let on = Some(Style::default().not_dim().red().bold()); - ////let off = Some(Style::default().dim()); - ////area.x += 1; - ////for y in 0..area.h() { - ////if y == 0 { - //////" REC ".blit(to.buffer, area.x, area.y + y, style2)?; - ////} else if y % 2 == 0 { - ////let index = (y as usize - 2) / 2; - ////if let Some(track) = tracks.get(index) { - ////let style = if track.recording { on } else { off }; - ////to.blit(&" REC ", area.x(), area.y() + y, style)?; - ////} else { - ////area.height = y; - ////break - ////} - ////} - ////} - ////area.width = 4; - ////Ok(Some(area)) - //}), - //// overdub - //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ - //todo!() - ////let Self(tracks) = self; - ////let mut area = to.area(); - ////let on = Some(Style::default().not_dim().yellow().bold()); - ////let off = Some(Style::default().dim()); - ////area.x = area.x + 1; - ////for y in 0..area.h() { - ////if y == 0 { - //////" OVR ".blit(to.buffer, area.x, area.y + y, style2)?; - ////} else if y % 2 == 0 { - ////let index = (y as usize - 2) / 2; - ////if let Some(track) = tracks.get(index) { - ////to.blit(&" OVR ", area.x(), area.y() + y, if track.overdub { - ////on - ////} else { - ////off - ////})?; - ////} else { - ////area.height = y; - ////break - ////} - ////} - ////} - ////area.width = 4; - ////Ok(Some(area)) - //}), - //// erase - //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ - //todo!() - ////let Self(tracks) = self; - ////let mut area = to.area(); - ////let off = Some(Style::default().dim()); - ////area.x = area.x + 1; - ////for y in 0..area.h() { - ////if y == 0 { - //////" DEL ".blit(to.buffer, area.x, area.y + y, style2)?; - ////} else if y % 2 == 0 { - ////let index = (y as usize - 2) / 2; - ////if let Some(_) = tracks.get(index) { - ////to.blit(&" DEL ", area.x(), area.y() + y, off)?; - ////} else { - ////area.height = y; - ////break - ////} - ////} - ////} - ////area.width = 4; - ////Ok(Some(area)) - //}), - //// gain - //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ - //todo!() - ////let Self(tracks) = self; - ////let mut area = to.area(); - ////let off = Some(Style::default().dim()); - ////area.x = area.x() + 1; - ////for y in 0..area.h() { - ////if y == 0 { - //////" GAIN ".blit(to.buffer, area.x, area.y + y, style2)?; - ////} else if y % 2 == 0 { - ////let index = (y as usize - 2) / 2; - ////if let Some(_) = tracks.get(index) { - ////to.blit(&" +0.0 ", area.x(), area.y() + y, off)?; - ////} else { - ////area.height = y; - ////break - ////} - ////} - ////} - ////area.width = 7; - ////Ok(Some(area)) - //}), - //// scenes - //Widget::new(|_|{todo!()}, |to: &mut TuiOutput|{ - //let [x, y, _, height] = to.area(); - //let mut x2 = 0; - //Ok(for (scene_index, scene) in view.scenes().iter().enumerate() { - //let active_scene = view.selected.scene() == Some(scene_index); - //let sep = Some(if active_scene { - //Style::default().yellow().not_dim() - //} else { - //Style::default().dim() - //}); - //for y in y+1..y+height { - //to.blit(&"│", x + x2, y, sep); - //} - //let name = scene.name.read().unwrap(); - //let mut x3 = name.len() as u16; - //to.blit(&*name, x + x2, y, sep); - //for (i, clip) in scene.clips.iter().enumerate() { - //let active_track = view.selected.track() == Some(i); - //if let Some(clip) = clip { - //let y2 = y + 2 + i as u16 * 2; - //let label = format!("{}", clip.read().unwrap().name); - //to.blit(&label, x + x2, y2, Some(if active_track && active_scene { - //Style::default().not_dim().yellow().bold() - //} else { - //Style::default().not_dim() - //})); - //x3 = x3.max(label.len() as u16) - //} - //} - //x2 = x2 + x3 + 1; - //}) - //}), - //) - //) -//} - impl HasScenes for ArrangerTui { fn scenes (&self) -> &Vec { &self.scenes @@ -1307,3 +981,198 @@ pub enum ArrangerClipCommand { //Ok(None) //} //} + +pub fn arranger_content_horizontal ( + view: &ArrangerTui, +) -> impl Render + use<'_> { + todo!() +} + //let focused = view.arranger_focused(); + //let _tracks = view.tracks(); + //lay!( + //focused.then_some(Background(TuiTheme::border_bg())), + //row!( + //// name + //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ + //todo!() + ////let Self(tracks, selected) = self; + ////let yellow = Some(Style::default().yellow().bold().not_dim()); + ////let white = Some(Style::default().white().bold().not_dim()); + ////let area = to.area(); + ////let area = [area.x(), area.y(), 3 + 5.max(track_name_max_len(tracks)) as u16, area.h()]; + ////let offset = 0; // track scroll offset + ////for y in 0..area.h() { + ////if y == 0 { + ////to.blit(&"Mixer", area.x() + 1, area.y() + y, Some(DIM))?; + ////} else if y % 2 == 0 { + ////let index = (y as usize - 2) / 2 + offset; + ////if let Some(track) = tracks.get(index) { + ////let selected = selected.track() == Some(index); + ////let style = if selected { yellow } else { white }; + ////to.blit(&format!(" {index:>02} "), area.x(), area.y() + y, style)?; + ////to.blit(&*track.name.read().unwrap(), area.x() + 4, area.y() + y, style)?; + ////} + ////} + ////} + ////Ok(Some(area)) + //}), + //// monitor + //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ + //todo!() + ////let Self(tracks) = self; + ////let mut area = to.area(); + ////let on = Some(Style::default().not_dim().green().bold()); + ////let off = Some(DIM); + ////area.x += 1; + ////for y in 0..area.h() { + ////if y == 0 { + //////" MON ".blit(to.buffer, area.x, area.y + y, style2)?; + ////} else if y % 2 == 0 { + ////let index = (y as usize - 2) / 2; + ////if let Some(track) = tracks.get(index) { + ////let style = if track.monitoring { on } else { off }; + ////to.blit(&" MON ", area.x(), area.y() + y, style)?; + ////} else { + ////area.height = y; + ////break + ////} + ////} + ////} + ////area.width = 4; + ////Ok(Some(area)) + //}), + //// record + //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ + //todo!() + ////let Self(tracks) = self; + ////let mut area = to.area(); + ////let on = Some(Style::default().not_dim().red().bold()); + ////let off = Some(Style::default().dim()); + ////area.x += 1; + ////for y in 0..area.h() { + ////if y == 0 { + //////" REC ".blit(to.buffer, area.x, area.y + y, style2)?; + ////} else if y % 2 == 0 { + ////let index = (y as usize - 2) / 2; + ////if let Some(track) = tracks.get(index) { + ////let style = if track.recording { on } else { off }; + ////to.blit(&" REC ", area.x(), area.y() + y, style)?; + ////} else { + ////area.height = y; + ////break + ////} + ////} + ////} + ////area.width = 4; + ////Ok(Some(area)) + //}), + //// overdub + //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ + //todo!() + ////let Self(tracks) = self; + ////let mut area = to.area(); + ////let on = Some(Style::default().not_dim().yellow().bold()); + ////let off = Some(Style::default().dim()); + ////area.x = area.x + 1; + ////for y in 0..area.h() { + ////if y == 0 { + //////" OVR ".blit(to.buffer, area.x, area.y + y, style2)?; + ////} else if y % 2 == 0 { + ////let index = (y as usize - 2) / 2; + ////if let Some(track) = tracks.get(index) { + ////to.blit(&" OVR ", area.x(), area.y() + y, if track.overdub { + ////on + ////} else { + ////off + ////})?; + ////} else { + ////area.height = y; + ////break + ////} + ////} + ////} + ////area.width = 4; + ////Ok(Some(area)) + //}), + //// erase + //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ + //todo!() + ////let Self(tracks) = self; + ////let mut area = to.area(); + ////let off = Some(Style::default().dim()); + ////area.x = area.x + 1; + ////for y in 0..area.h() { + ////if y == 0 { + //////" DEL ".blit(to.buffer, area.x, area.y + y, style2)?; + ////} else if y % 2 == 0 { + ////let index = (y as usize - 2) / 2; + ////if let Some(_) = tracks.get(index) { + ////to.blit(&" DEL ", area.x(), area.y() + y, off)?; + ////} else { + ////area.height = y; + ////break + ////} + ////} + ////} + ////area.width = 4; + ////Ok(Some(area)) + //}), + //// gain + //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ + //todo!() + ////let Self(tracks) = self; + ////let mut area = to.area(); + ////let off = Some(Style::default().dim()); + ////area.x = area.x() + 1; + ////for y in 0..area.h() { + ////if y == 0 { + //////" GAIN ".blit(to.buffer, area.x, area.y + y, style2)?; + ////} else if y % 2 == 0 { + ////let index = (y as usize - 2) / 2; + ////if let Some(_) = tracks.get(index) { + ////to.blit(&" +0.0 ", area.x(), area.y() + y, off)?; + ////} else { + ////area.height = y; + ////break + ////} + ////} + ////} + ////area.width = 7; + ////Ok(Some(area)) + //}), + //// scenes + //Widget::new(|_|{todo!()}, |to: &mut TuiOutput|{ + //let [x, y, _, height] = to.area(); + //let mut x2 = 0; + //Ok(for (scene_index, scene) in view.scenes().iter().enumerate() { + //let active_scene = view.selected.scene() == Some(scene_index); + //let sep = Some(if active_scene { + //Style::default().yellow().not_dim() + //} else { + //Style::default().dim() + //}); + //for y in y+1..y+height { + //to.blit(&"│", x + x2, y, sep); + //} + //let name = scene.name.read().unwrap(); + //let mut x3 = name.len() as u16; + //to.blit(&*name, x + x2, y, sep); + //for (i, clip) in scene.clips.iter().enumerate() { + //let active_track = view.selected.track() == Some(i); + //if let Some(clip) = clip { + //let y2 = y + 2 + i as u16 * 2; + //let label = format!("{}", clip.read().unwrap().name); + //to.blit(&label, x + x2, y2, Some(if active_track && active_scene { + //Style::default().not_dim().yellow().bold() + //} else { + //Style::default().not_dim() + //})); + //x3 = x3.max(label.len() as u16) + //} + //} + //x2 = x2 + x3 + 1; + //}) + //}), + //) + //) +//} diff --git a/crates/tek/src/tui/app_groovebox.rs b/crates/tek/src/tui/app_groovebox.rs index 72aadb16..fb9cd270 100644 --- a/crates/tek/src/tui/app_groovebox.rs +++ b/crates/tek/src/tui/app_groovebox.rs @@ -1,50 +1,37 @@ use crate::*; use super::*; use KeyCode::Char; - -impl TryFrom<&Arc>> for GrooveboxTui { - type Error = Box; - fn try_from (jack: &Arc>) -> Usually { - let mut sequencer = SequencerTui::try_from(jack)?; - sequencer.status = false; - Ok(Self { - sequencer, - sampler: SamplerTui::try_from(jack)?, - split: 16, - focus: GrooveboxFocus::Sampler, - }) - } -} - pub struct GrooveboxTui { pub sequencer: SequencerTui, pub sampler: SamplerTui, pub split: u16, pub focus: GrooveboxFocus } - +from_jack!(|jack|GrooveboxTui { + let mut sequencer = SequencerTui::try_from(jack)?; + sequencer.status = false; + Self { + sequencer, + sampler: SamplerTui::try_from(jack)?, + split: 16, + focus: GrooveboxFocus::Sampler, + } +}); pub enum GrooveboxFocus { Sequencer, Sampler } - audio!(|self:GrooveboxTui,_client,_process|Control::Continue); - render!(|self:GrooveboxTui|Bsp::n( - Fixed::h(2, SequencerStatusBar::from(&self.sequencer)), + Fixed::h(2, SequencerStatus::from(&self.sequencer)), Fill::h(Bsp::s(&self.sequencer, &self.sampler)), )); - pub enum GrooveboxCommand { Sequencer(SequencerCommand), Sampler(SamplerCommand), } - handle!(|self:GrooveboxTui,input|GrooveboxCommand::execute_with_state(self, input)); - input_to_command!(GrooveboxCommand: |state:GrooveboxTui,input|match input.event() { - // load sample - key_pat!(Char('l')) => GrooveboxCommand::Sampler(SamplerCommand::Import(FileBrowserCommand::Begin)), _ => match state.focus { GrooveboxFocus::Sequencer => GrooveboxCommand::Sequencer(SequencerCommand::input_to_command(&state.sequencer, input)?), @@ -52,7 +39,6 @@ input_to_command!(GrooveboxCommand: |state:GrooveboxTui,input|match input.e GrooveboxCommand::Sampler(SamplerCommand::input_to_command(&state.sampler, input)?), } }); - command!(|self:GrooveboxCommand,state:GrooveboxTui|match self { GrooveboxCommand::Sequencer(command) => command.execute(&mut state.sequencer)?.map(GrooveboxCommand::Sequencer), diff --git a/crates/tek/src/tui/app_sampler.rs b/crates/tek/src/tui/app_sampler.rs index dd551f04..9306db47 100644 --- a/crates/tek/src/tui/app_sampler.rs +++ b/crates/tek/src/tui/app_sampler.rs @@ -1,5 +1,6 @@ use crate::*; use super::*; +use KeyCode::Char; use std::fs::File; use symphonia::core::codecs::CODEC_TYPE_NULL; use symphonia::core::errors::Error; @@ -54,8 +55,11 @@ pub enum SamplerCommand { } input_to_command!(SamplerCommand:|state:SamplerTui,input|match state.mode { Some(SamplerMode::Import(..)) => Self::Import(FileBrowserCommand::input_to_command(state, input)?), - _ => todo!() - //_ => match input.event() { + _ => match input.event() { + // load sample + key_pat!(Char('l')) => Self::Import(FileBrowserCommand::Begin), + _ => return None + } //key_pat!(KeyCode::Up) => state.cursor.0 = if state.cursor.0 == 0 { //mapped.len() + unmapped.len() - 1 //} else { diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs index 44e247f1..aefd456a 100644 --- a/crates/tek/src/tui/app_sequencer.rs +++ b/crates/tek/src/tui/app_sequencer.rs @@ -3,33 +3,6 @@ use KeyCode::{Tab, BackTab, Char}; use SequencerCommand::*; use PhraseCommand::*; use PhrasePoolCommand::*; - -/// Create app state from JACK handle. -impl TryFrom<&Arc>> for SequencerTui { - type Error = Box; - fn try_from (jack: &Arc>) -> Usually { - let clock = ClockModel::from(jack); - let phrase = Arc::new(RwLock::new(Phrase::new( - "New", true, 4 * clock.timebase.ppq.get() as usize, - None, Some(ItemColor::random().into()) - ))); - Ok(Self { - _jack: jack.clone(), - phrases: PhraseListModel::from(&phrase), - editor: PhraseEditorModel::from(&phrase), - player: PhrasePlayerModel::from((&clock, &phrase)), - clock, - size: Measure::new(), - midi_buf: vec![vec![];65536], - note_buf: vec![], - perf: PerfModel::default(), - show_pool: true, - status: true, - }) - - } -} - /// Root view for standalone `tek_sequencer`. pub struct SequencerTui { _jack: Arc>, @@ -44,17 +17,91 @@ pub struct SequencerTui { pub(crate) midi_buf: Vec>>, pub(crate) perf: PerfModel, } - -#[derive(Clone, Debug)] -pub enum SequencerCommand { +from_jack!(|jack|SequencerTui { + let clock = ClockModel::from(jack); + let phrase = Arc::new(RwLock::new(Phrase::new( + "New", true, 4 * clock.timebase.ppq.get() as usize, + None, Some(ItemColor::random().into()) + ))); + Self { + _jack: jack.clone(), + phrases: PhraseListModel::from(&phrase), + editor: PhraseEditorModel::from(&phrase), + player: PhrasePlayerModel::from((&clock, &phrase)), + clock, + size: Measure::new(), + midi_buf: vec![vec![];65536], + note_buf: vec![], + perf: PerfModel::default(), + show_pool: true, + status: true, + } +}); +render!(|self: SequencerTui|{ + let w = self.size.w(); + let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; + let pool_w = if self.show_pool { phrase_w } else { 0 }; + let pool = Fill::h(Align::e(PhraseListView(&self.phrases))); + let with_pool = move|x|Tui::split_w(false, pool_w, pool, x); + let status = SequencerStatus::from(self); + let with_status = |x|Tui::split_n(false, if self.status { 2 } else { 0 }, status, x); + let with_editbar = |x|Tui::split_n(false, 3, PhraseEditStatus(&self.editor), x); + let with_size = |x|lay!([self.size, x]); + let editor = with_editbar(with_pool(Fill::wh(&self.editor))); + let color = self.player.play_phrase().as_ref().map(|(_,p)| + p.as_ref().map(|p|p.read().unwrap().color) + ).flatten().clone(); + let play = Fixed::wh(5, 2, PlayPause(self.clock.is_rolling())); + let transport = Fixed::h(2, TransportView::from((self, color, true))); + let toolbar = row!([play, col!([ + PhraseSelector::play_phrase(&self.player), + PhraseSelector::next_phrase(&self.player), + ]), transport]); + Tui::min_y(15, with_size(with_status(col!([ toolbar, editor, ])))) +}); +audio!(|self:SequencerTui, client, scope|{ + // Start profiling cycle + let t0 = self.perf.get_t0(); + // Update transport clock + if Control::Quit == ClockAudio(self).process(client, scope) { + return Control::Quit + } + // Update MIDI sequencer + if Control::Quit == PlayerAudio( + &mut self.player, &mut self.note_buf, &mut self.midi_buf + ).process(client, scope) { + return Control::Quit + } + // End profiling cycle + self.perf.update(t0, scope); + Control::Continue +}); +impl HasPhraseList for SequencerTui { + fn phrases_focused (&self) -> bool { + true + } + fn phrases_entered (&self) -> bool { + true + } + fn phrases_mode (&self) -> &Option { + &self.phrases.mode + } + fn phrase_index (&self) -> usize { + self.phrases.phrase.load(Ordering::Relaxed) + } +} +has_size!(|self:SequencerTui|&self.size); +has_clock!(|self:SequencerTui|&self.clock); +has_phrases!(|self:SequencerTui|self.phrases.phrases); +has_editor!(|self:SequencerTui|self.editor); +handle!(|self:SequencerTui,input|SequencerCommand::execute_with_state(self, input)); +#[derive(Clone, Debug)] pub enum SequencerCommand { Clock(ClockCommand), Phrases(PhrasesCommand), Editor(PhraseCommand), Enqueue(Option>>), ShowPool(bool), } - -handle!(|self:SequencerTui,input|SequencerCommand::execute_with_state(self, input)); input_to_command!(SequencerCommand: |state:SequencerTui,input|match input.event() { // Transport: Play/pause key_pat!(Char(' ')) => Clock(if state.clock().is_stopped() { @@ -98,7 +145,9 @@ input_to_command!(SequencerCommand: |state:SequencerTui,input|match input.e }); command!(|self: SequencerCommand, state: SequencerTui|match self { Self::Phrases(cmd) => { - let mut default = |cmd: PhrasesCommand|cmd.execute(&mut state.phrases).map(|x|x.map(Phrases)); + let mut default = |cmd: PhrasesCommand|cmd + .execute(&mut state.phrases) + .map(|x|x.map(Phrases)); match cmd { // autoselect: automatically load selected phrase in editor PhrasesCommand::Select(_) => { @@ -131,124 +180,3 @@ command!(|self: SequencerCommand, state: SequencerTui|match self { None } }); - -has_size!(|self:SequencerTui|&self.size); -has_clock!(|self:SequencerTui|&self.clock); -has_phrases!(|self:SequencerTui|self.phrases.phrases); -has_editor!(|self:SequencerTui|self.editor); -render!(|self: SequencerTui|{ - let w = self.size.w(); - let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; - let pool_w = if self.show_pool { phrase_w } else { 0 }; - let pool = Fill::h(Align::e(PhraseListView(&self.phrases))); - let with_pool = move|x|Tui::split_w(false, pool_w, pool, x); - let status = SequencerStatusBar::from(self); - let with_status = |x|Tui::split_n(false, if self.status { 2 } else { 0 }, status, x); - let with_editbar = |x|Tui::split_n(false, 3, PhraseEditStatus(&self.editor), x); - let with_size = |x|lay!([self.size, x]); - let editor = with_editbar(with_pool(Fill::wh(&self.editor))); - let color = self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten().clone(); - let play = Fixed::wh(5, 2, PlayPause(self.clock.is_rolling())); - let transport = Fixed::h(2, TransportView::from((self, color, true))); - let toolbar = row!([play, col!([ - PhraseSelector::play_phrase(&self.player), - PhraseSelector::next_phrase(&self.player), - ]), transport]); - Tui::min_y(15, with_size(with_status(col!([ toolbar, editor, ])))) -}); -audio!(|self:SequencerTui, client, scope|{ - // Start profiling cycle - let t0 = self.perf.get_t0(); - // Update transport clock - if Control::Quit == ClockAudio(self).process(client, scope) { - return Control::Quit - } - // Update MIDI sequencer - if Control::Quit == PlayerAudio( - &mut self.player, &mut self.note_buf, &mut self.midi_buf - ).process(client, scope) { - return Control::Quit - } - // End profiling cycle - self.perf.update(t0, scope); - Control::Continue -}); - -impl HasPhraseList for SequencerTui { - fn phrases_focused (&self) -> bool { - true - } - fn phrases_entered (&self) -> bool { - true - } - fn phrases_mode (&self) -> &Option { - &self.phrases.mode - } - fn phrase_index (&self) -> usize { - self.phrases.phrase.load(Ordering::Relaxed) - } -} - -/// Status bar for sequencer app -#[derive(Clone)] -pub struct SequencerStatusBar { - pub(crate) width: usize, - pub(crate) cpu: Option, - pub(crate) size: String, - pub(crate) res: String, - pub(crate) playing: bool, -} - -impl StatusBar for SequencerStatusBar { - type State = SequencerTui; - fn hotkey_fg () -> Color { - TuiTheme::HOTKEY_FG - } - fn update (&mut self, _: &SequencerTui) { - todo!() - } -} - -impl From<&SequencerTui> for SequencerStatusBar { - fn from (state: &SequencerTui) -> Self { - let samples = state.clock.chunk.load(Ordering::Relaxed); - let rate = state.clock.timebase.sr.get() as f64; - let buffer = samples as f64 / rate; - let width = state.size.w(); - Self { - width, - playing: state.clock.is_rolling(), - cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")), - size: format!("{}x{}│", width, state.size.h()), - res: format!("│{}s│{:.1}kHz│{:.1}ms│", samples, rate / 1000., buffer * 1000.), - } - } -} - -render!(|self: SequencerStatusBar|Fixed::h(2, lay!([ - { - let single = |binding, command|row!([" ", col!([ - Tui::fg(TuiTheme::yellow(), binding), - command - ])]); - let double = |(b1, c1), (b2, c2)|col!([ - row!([" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,]), - row!([" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,]), - ]); - Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!([ - single("SPACE", "play/pause"), - double((" ✣", "cursor"), ("C-✣", "scroll"), ), - double(("a", "append"), ("s", "set note"),), - double((",.", "note"), ("<>", "triplet"), ), - double(("[]", "phrase"), ("{}", "order"), ), - double(("q", "enqueue"), ("e", "edit"), ), - ])) - }, - Fill::wh(Align::se({ - Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), row!([ - &self.cpu, - &self.res, - &self.size, - ])) - })), -]))); diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs index 419c0656..535bad5a 100644 --- a/crates/tek/src/tui/app_transport.rs +++ b/crates/tek/src/tui/app_transport.rs @@ -3,7 +3,6 @@ use crate::api::ClockCommand::{Play, Pause, SetBpm, SetQuant, SetSync}; use TransportCommand::{Focus, Clock}; use FocusCommand::{Next, Prev}; use KeyCode::{Enter, Left, Right, Char}; - /// Transport clock app. pub struct TransportTui { pub jack: Arc>, @@ -12,21 +11,17 @@ pub struct TransportTui { pub cursor: (usize, usize), pub focus: FocusState, } - -/// Create app state from JACK handle. -impl TryFrom<&Arc>> for TransportTui { - type Error = Box; - fn try_from (jack: &Arc>) -> Usually { - Ok(Self { - jack: jack.clone(), - clock: ClockModel::from(jack), - size: Measure::new(), - cursor: (0, 0), - focus: FocusState::Entered(TransportFocus::PlayPause) - }) - } -} - +from_jack!(|jack|TransportTui Self { + jack: jack.clone(), + clock: ClockModel::from(jack), + size: Measure::new(), + cursor: (0, 0), + focus: FocusState::Entered(TransportFocus::PlayPause) +}); +has_clock!(|self:TransportTui|&self.clock); +audio!(|self:TransportTui,client,scope|ClockAudio(self).process(client, scope)); +handle!(|self:TransportTui,from|TransportCommand::execute_with_state(self, from)); +render!(|self: TransportTui|TransportView::from((self, None, true))); impl std::fmt::Debug for TransportTui { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("TransportTui") @@ -36,12 +31,6 @@ impl std::fmt::Debug for TransportTui { .finish() } } - -has_clock!(|self:TransportTui|&self.clock); -audio!(|self:TransportTui,client,scope|ClockAudio(self).process(client, scope)); -handle!(|self:TransportTui,from|TransportCommand::execute_with_state(self, from)); -render!(|self: TransportTui|TransportView::from((self, None, true))); - pub struct TransportView { color: ItemPalette, focused: bool, @@ -187,21 +176,6 @@ impl FocusWrap for Option { } } -#[derive(Copy, Clone)] -pub struct TransportStatusBar; - -impl StatusBar for TransportStatusBar { - type State = (); - fn hotkey_fg () -> Color { - TuiTheme::HOTKEY_FG - } - fn update (&mut self, _: &()) { - todo!() - } -} - -render!(|self: TransportStatusBar|"todo"); - pub trait TransportControl: HasClock + { fn transport_focused (&self) -> Option; } diff --git a/crates/tek/src/tui/status_bar.rs b/crates/tek/src/tui/status_bar.rs index 926abf72..1835f563 100644 --- a/crates/tek/src/tui/status_bar.rs +++ b/crates/tek/src/tui/status_bar.rs @@ -1,6 +1,6 @@ use crate::*; -pub trait StatusBar: Render { +pub trait Bar: Render { type State: Send + Sync; fn hotkey_fg () -> Color where Self: Sized; fn update (&mut self, state: &Self::State) where Self: Sized; @@ -21,3 +21,177 @@ pub trait StatusBar: Render { Bsp::n(state.into(), content) } } + +/// Status bar for sequencer app +#[derive(Clone)] +pub struct SequencerStatus { + pub(crate) width: usize, + pub(crate) cpu: Option, + pub(crate) size: String, + pub(crate) res: String, + pub(crate) playing: bool, +} + +impl Bar for SequencerStatus { + type State = SequencerTui; + fn hotkey_fg () -> Color { + TuiTheme::HOTKEY_FG + } + fn update (&mut self, _: &SequencerTui) { + todo!() + } +} + +impl From<&SequencerTui> for SequencerStatus { + fn from (state: &SequencerTui) -> Self { + let samples = state.clock.chunk.load(Ordering::Relaxed); + let rate = state.clock.timebase.sr.get() as f64; + let buffer = samples as f64 / rate; + let width = state.size.w(); + Self { + width, + playing: state.clock.is_rolling(), + cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")), + size: format!("{}x{}│", width, state.size.h()), + res: format!("│{}s│{:.1}kHz│{:.1}ms│", samples, rate / 1000., buffer * 1000.), + } + } +} + +render!(|self: SequencerStatus|Fixed::h(2, lay!([ + { + let single = |binding, command|row!([" ", col!([ + Tui::fg(TuiTheme::yellow(), binding), + command + ])]); + let double = |(b1, c1), (b2, c2)|col!([ + row!([" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,]), + row!([" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,]), + ]); + Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!([ + single("SPACE", "play/pause"), + double((" ✣", "cursor"), ("C-✣", "scroll"), ), + double(("a", "append"), ("s", "set note"),), + double((",.", "note"), ("<>", "triplet"), ), + double(("[]", "phrase"), ("{}", "order"), ), + double(("q", "enqueue"), ("e", "edit"), ), + ])) + }, + Fill::wh(Align::se({ + Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), row!([ + &self.cpu, + &self.res, + &self.size, + ])) + })), +]))); + +impl Bar for ArrangerStatus { + type State = (ArrangerFocus, ArrangerSelection, bool); + fn hotkey_fg () -> Color where Self: Sized { + TuiTheme::HOTKEY_FG + } + fn update (&mut self, (focused, selected, entered): &Self::State) { + *self = match focused { + //ArrangerFocus::Menu => { todo!() }, + ArrangerFocus::Transport(_) => ArrangerStatus::Transport, + ArrangerFocus::Arranger => match selected { + ArrangerSelection::Mix => ArrangerStatus::ArrangerMix, + ArrangerSelection::Track(_) => ArrangerStatus::ArrangerTrack, + ArrangerSelection::Scene(_) => ArrangerStatus::ArrangerScene, + ArrangerSelection::Clip(_, _) => ArrangerStatus::ArrangerClip, + }, + ArrangerFocus::Phrases => ArrangerStatus::PhrasePool, + ArrangerFocus::PhraseEditor => match entered { + true => ArrangerStatus::PhraseEdit, + false => ArrangerStatus::PhraseView, + }, + } + } +} + +render!(|self: ArrangerStatus|{ + + let label = match self { + Self::Transport => "TRANSPORT", + Self::ArrangerMix => "PROJECT", + Self::ArrangerTrack => "TRACK", + Self::ArrangerScene => "SCENE", + Self::ArrangerClip => "CLIP", + Self::PhrasePool => "SEQ LIST", + Self::PhraseView => "VIEW SEQ", + Self::PhraseEdit => "EDIT SEQ", + }; + + let status_bar_bg = TuiTheme::status_bar_bg(); + + let mode_bg = TuiTheme::mode_bg(); + let mode_fg = TuiTheme::mode_fg(); + let mode = Tui::fg(mode_fg, Tui::bg(mode_bg, Tui::bold(true, format!(" {label} ")))); + + let commands = match self { + Self::ArrangerMix => Self::command(&[ + ["", "c", "olor"], + ["", "<>", "resize"], + ["", "+-", "zoom"], + ["", "n", "ame/number"], + ["", "Enter", " stop all"], + ]), + Self::ArrangerClip => Self::command(&[ + ["", "g", "et"], + ["", "s", "et"], + ["", "a", "dd"], + ["", "i", "ns"], + ["", "d", "up"], + ["", "e", "dit"], + ["", "c", "olor"], + ["re", "n", "ame"], + ["", ",.", "select"], + ["", "Enter", " launch"], + ]), + Self::ArrangerTrack => Self::command(&[ + ["re", "n", "ame"], + ["", ",.", "resize"], + ["", "<>", "move"], + ["", "i", "nput"], + ["", "o", "utput"], + ["", "m", "ute"], + ["", "s", "olo"], + ["", "Del", "ete"], + ["", "Enter", " stop"], + ]), + Self::ArrangerScene => Self::command(&[ + ["re", "n", "ame"], + ["", "Del", "ete"], + ["", "Enter", " launch"], + ]), + Self::PhrasePool => Self::command(&[ + ["", "a", "ppend"], + ["", "i", "nsert"], + ["", "d", "uplicate"], + ["", "Del", "ete"], + ["", "c", "olor"], + ["re", "n", "ame"], + ["leng", "t", "h"], + ["", ",.", "move"], + ["", "+-", "resize view"], + ]), + Self::PhraseView => Self::command(&[ + ["", "enter", " edit"], + ["", "arrows/pgup/pgdn", " scroll"], + ["", "+=", "zoom"], + ]), + Self::PhraseEdit => Self::command(&[ + ["", "esc", " exit"], + ["", "a", "ppend"], + ["", "s", "et"], + ["", "][", "length"], + ["", "+-", "zoom"], + ]), + _ => Self::command(&[]) + }; + + //let commands = commands.iter().reduce(String::new(), |s, (a, b, c)| format!("{s} {a}{b}{c}")); + Tui::bg(status_bar_bg, Fill::w(row!([mode, commands]))) + +});