wip: layered keymaps
Some checks failed
/ build (push) Has been cancelled

This commit is contained in:
🪞👃🪞 2025-04-28 23:43:12 +03:00
parent 5696cbbebb
commit 2fd7d7b39f
18 changed files with 355 additions and 360 deletions

View file

@ -1,5 +1,3 @@
(@space clock toggle)
(@shift-space clock toggle 0)
(@t select :track 0)
(@tab edit :clip)
(@c color)

2
config/keys_clock.edn Normal file
View file

@ -0,0 +1,2 @@
(@space clock toggle)
(@shift-space clock toggle 0)

View file

@ -1,30 +0,0 @@
(@up note/pos :note-pos-next)
(@w note/pos :note-pos-next)
(@down note/pos :note-pos-prev)
(@s note/pos :note-pos-prev)
(@pgup note/pos :note-pos-next-octave)
(@pgdn note/pos :note-pos-prev-octave)
(@comma note/len :note-len-prev)
(@period note/len :note-len-next)
(@lt note/len :note-len-prev)
(@gt note/len :note-len-next)
(@plus note/range :note-range-next)
(@underscore note/range :note-range-prev)
(@left time/pos :time-pos-prev)
(@a time/pos :time-pos-prev)
(@right time/pos :time-pos-next)
(@d time/pos :time-pos-next)
(@equal time/zoom :time-zoom-prev)
(@minus time/zoom :time-zoom-next)
(@z time/lock)
(@enter note/put)
(@shift-enter note/append)
(@del note/del)
(@shift-del note/del)

30
config/keys_editor.edn Normal file
View file

@ -0,0 +1,30 @@
(@up editor note/pos :note-pos-next)
(@w editor note/pos :note-pos-next)
(@down editor note/pos :note-pos-prev)
(@s editor note/pos :note-pos-prev)
(@pgup editor note/pos :note-pos-next-octave)
(@pgdn editor note/pos :note-pos-prev-octave)
(@comma editor note/len :note-len-prev)
(@period editor note/len :note-len-next)
(@lt editor note/len :note-len-prev)
(@gt editor note/len :note-len-next)
(@plus editor note/range :note-range-next)
(@underscore editor note/range :note-range-prev)
(@left editor time/pos :time-pos-prev)
(@a editor time/pos :time-pos-prev)
(@right editor time/pos :time-pos-next)
(@d editor time/pos :time-pos-next)
(@equal editor time/zoom :time-zoom-prev)
(@minus editor time/zoom :time-zoom-next)
(@z editor time/lock)
(@enter editor note/put)
(@shift-enter editor note/append)
(@del editor note/del)
(@shift-del editor note/del)

View file

@ -1,8 +1 @@
(@space clock toggle)
(@shift-space clock toggle 0)
(@c color)
(@q launch)
(@r sampler record/begin :pitch)

View file

@ -1,6 +1,4 @@
(@space clock toggle)
(@shift-space clock toggle 0)
(@c color)
(@q launch)
(@shift-I input add)
(@shift-O output add)
(@c color)
(@q launch)
(@shift-I input add)
(@shift-O output add)

View file

@ -1,7 +1,7 @@
(bsp/a :modal
(bsp/s (fixed/y 1 :transport)
(bsp/n (fixed/y 1 :status)
(bsp/n (fixed/y 5 :sample-viewer)
(bsp/w (fixed/x :w-sidebar :pool)
(bsp/e :samples-keys
(fill/y :editor)))))))
(bsp/a :modal
(bsp/s (fixed/y 1 :transport)
(bsp/n (fixed/y 1 :status)
(bsp/n (fixed/y 5 :sample-viewer)
(bsp/w (fixed/x :w-sidebar :pool)
(bsp/e :samples-keys
(fill/y :editor)))))))

View file

@ -6,19 +6,6 @@ macro_rules! ns { ($C:ty, $s:expr, $a:expr, $W:expr) => { <$C>::try_from_expr($s
macro_rules! cmd { ($cmd:expr) => {{ $cmd; None }}; }
macro_rules! cmd_todo { ($msg:literal) => {{ println!($msg); None }}; }
view!(TuiOut: |self: Tek| self.size.of(View(self, self.view)); {
":nil" => Box::new("nil"),
":modal" => self.view_modal(),
":status" => self.view_status(),
":transport" => self.view_transport(),
":arranger" => self.view_arranger(),
":pool" => self.view_pool(),
":editor" => self.editor().map(|e|Bsp::n(Bsp::e(e.clip_status(), e.edit_status()), e)),
":samples-keys" => self.sampler().map(|s|s.view_list(false, self.editor().unwrap())),
":samples-grid" => self.sampler().map(|s|s.view_grid()),
":sample-viewer" => self.sampler().map(|s|s.view_sample(self.editor().unwrap().note_pos())),
});
expose!([self: Tek]
([bool])
([isize])
@ -45,6 +32,13 @@ expose!([self: Tek]
(":track-next" self.selected.track_next(self.tracks.len()))
(":track-prev" self.selected.track_prev())));
handle!(TuiIn: |self: Tek, input|Ok(if let Some(command) = self.keys.command(self, input) {
let undo = command.execute(self)?;
Some(true)
} else {
None
}));
impose!([app: Tek]
(TekCommand:
("menu" [] Some(Self::ToggleMenu))

View file

@ -1,44 +0,0 @@
use crate::*;
handle!(TuiIn: |self: Tek, input|if let Some(handler) = self.handler {
handler(self, input)
} else {
Ok(None)
});
pub fn handle_arranger (app: &mut Tek, input: &TuiIn) -> Perhaps<bool> {
Ok((app.is_editing() && app.editor.handle(input)? == Some(true) ||
layer(app, input, include_str!("../../../config/keys_global.edn"))? ||
layer(app, input, include_str!("../../../config/keys_arranger.edn"))? ||
app.selected.is_clip() && layer(app, input, include_str!("../../../config/keys_clip.edn"))? ||
app.selected.is_track() && layer(app, input, include_str!("../../../config/keys_track.edn"))? ||
app.selected.is_scene() && layer(app, input, include_str!("../../../config/keys_scene.edn"))? ||
app.selected.is_mix() && layer(app, input, include_str!("../../../config/keys_mix.edn"))?).then_some(true))
}
pub fn handle_sequencer (app: &mut Tek, input: &TuiIn) -> Perhaps<bool> {
Ok((app.editor.handle(input)? == Some(true) ||
layer(app, input, include_str!("../../../config/keys_global.edn"))? ||
layer(app, input, include_str!("../../../config/keys_sequencer.edn"))?).then_some(true))
}
pub fn handle_groovebox (app: &mut Tek, input: &TuiIn) -> Perhaps<bool> {
Ok((app.editor.handle(input)? == Some(true) ||
layer(app, input, include_str!("../../../config/keys_global.edn"))? ||
layer(app, input, include_str!("../../../config/keys_groovebox.edn"))?).then_some(true))
}
pub fn handle_sampler (app: &mut Tek, input: &TuiIn) -> Perhaps<bool> {
Ok((layer(app, input, include_str!("../../../config/keys_global.edn"))? ||
layer(app, input, include_str!("../../../config/keys_sampler.edn"))?).then_some(true))
}
fn layer (app: &mut Tek, input: &TuiIn, keymap: &str) -> Usually<bool> {
if let Some(command) = SourceIter(keymap).command::<_, TekCommand, _>(app, input) {
if let Some(undo) = command.execute(app)? {
app.history.push(undo);
}
return Ok(true)
}
return Ok(false)
}

View file

@ -40,13 +40,13 @@ mod api; pub use self::api::*;
mod audio; pub use self::audio::*;
mod model; pub use self::model::*;
mod view; pub use self::view::*;
mod keys; pub use self::keys::*;
#[cfg(test)] #[test] fn test_model () {
let mut tek = Tek::default();
let _ = tek.clip();
let _ = tek.toggle_loop();
}
#[cfg(test)] #[test] fn test_model_scene () {
let mut app = Tek::default();
let _ = app.scene_longest();
@ -58,6 +58,7 @@ mod keys; pub use self::keys::*;
let _ = scene.pulses();
let _ = scene.is_playing(&[]);
}
#[cfg(test)] #[test] fn test_view_clock () {
let _ = button_play_pause(true);
let mut app = Tek::default();
@ -65,6 +66,7 @@ mod keys; pub use self::keys::*;
let _ = app.view_status();
let _ = app.update_clock();
}
#[cfg(test)] #[test] fn test_view_layout () {
let _ = button_2("", "", true);
let _ = button_2("", "", false);
@ -74,6 +76,7 @@ mod keys; pub use self::keys::*;
let _ = heading("", "", 0, "", false);
let _ = wrap(Reset, Reset, "");
}
#[cfg(test)] mod test_view_meter {
use super::*;
use proptest::prelude::*;
@ -95,6 +98,7 @@ mod keys; pub use self::keys::*;
}
}
}
#[cfg(test)] #[test] fn test_view_iter () {
let mut tek = Tek::default();
tek.editor = Some(Default::default());
@ -105,6 +109,7 @@ mod keys; pub use self::keys::*;
//let _: Vec<_> = tek.scenes_with_colors(true, 10).collect();
//let _: Vec<_> = tek.scenes_with_track_colors(true, 10, 10).collect();
}
#[cfg(test)] #[test] fn test_view_sizes () {
let app = Tek::default();
let _ = app.w();

View file

@ -50,10 +50,10 @@ pub struct Tek {
pub view: SourceIter<'static>,
// Cache of formatted strings
pub view_cache: Arc<RwLock<ViewCache>>,
// Input handler function
pub handler: Option<fn(&mut Self, &TuiIn)->Result<Option<bool>, Box<(dyn std::error::Error + 'static)>>>,
// Modal overlay
pub modal: Option<Modal>
pub modal: Option<Modal>,
// Input keymap
pub keys: InputMap<'static, Self, TekCommand, TuiIn, SourceIter<'static>>
}
impl Tek {

View file

@ -2,6 +2,225 @@ use crate::*;
pub(crate) use std::fmt::Write;
pub(crate) use ::tengri::tui::ratatui::prelude::Position;
view!(TuiOut: |self: Tek| self.size.of(View(self, self.view)); {
":nil" => Box::new("nil"),
":modal" => self.view_modal(),
":status" => self.view_status(),
":transport" => self.view_transport(),
":arranger" => self.view_arranger(),
":pool" => self.view_pool(),
":editor" => self.editor().map(|e|Bsp::n(Bsp::e(e.clip_status(), e.edit_status()), e)),
":samples-keys" => self.sampler().map(|s|s.view_list(false, self.editor().unwrap())),
":samples-grid" => self.sampler().map(|s|s.view_grid()),
":sample-viewer" => self.sampler().map(|s|s.view_sample(self.editor().unwrap().note_pos())),
});
trait ScenesColors<'a> = Iterator<Item=SceneWithColor<'a>>;
type SceneWithColor<'a> = (usize, &'a Scene, usize, usize, Option<ItemTheme>);
impl Tek {
pub fn view_modal (&self) -> impl Content<TuiOut> + use<'_> {
When::new(self.modal.is_some(), Bsp::b(
Fill::xy(Tui::fg_bg(Color::Rgb(64,64,64), Color::Rgb(32,32,32), "")),
Fixed::xy(30, 15, Tui::fg_bg(Color::Rgb(255,255,255), Color::Rgb(16,16,16), Bsp::b(
Repeat(" "),
Outer(true, Style::default().fg(Tui::g(96)))
.enclose(self.modal.map(|modal|match modal {
Modal::Menu => self.view_modal_menu().boxed(),
Modal::Help => self.view_modal_help().boxed(),
}))
)))
))
}
fn view_modal_menu (&self) -> impl Content<TuiOut> {
let options = ||["Projects", "Settings", "Help", "Quit"].iter();
let option = |a,i|Tui::fg(Color::Rgb(255,255,255), format!("{}", a));
Bsp::s(Tui::bold(true, "tek!"), Bsp::s("", Map::south(1, options, option)))
}
fn view_modal_help (&self) -> impl Content<TuiOut> {
let bindings = ||TokenIter::new(include_str!("../../../config/keys_groovebox.edn"))
.filter_map(|x|if let Value::Exp(_, iter)=x.value{
Some(iter)
} else {
None
});
let binding = |mut binding: TokenIter, _|Bsp::e(
Tui::bold(true, Tui::fg(Color::Rgb(255,192,0), if let Some(Token {
value: Value::Sym(key), ..
}) = binding.next() {
Some(key.to_string())
} else {
None
})),
Bsp::e(" ", Tui::fg(Color::Rgb(255,255,255), if let Some(Token {
value: Value::Key(command), ..
}) = binding.next() {
Some(command.to_string())
} else {
None
})),
);
let layer = Map::south(1, bindings, binding);
Bsp::s(Tui::bold(true, "Help"), Bsp::s("", layer))
}
pub fn view_status (&self) -> impl Content<TuiOut> + use<'_> {
self.update_clock();
let cache = self.view_cache.read().unwrap();
view_status(
self.selected.describe(&self.tracks, &self.scenes),
cache.sr.view.clone(),
cache.buf.view.clone(),
cache.lat.view.clone(),
)
}
pub fn view_transport (&self) -> impl Content<TuiOut> + use<'_> {
self.update_clock();
let cache = self.view_cache.read().unwrap();
view_transport(
self.clock.is_rolling(),
cache.bpm.view.clone(),
cache.beat.view.clone(),
cache.time.view.clone(),
)
}
pub fn view_arranger (&self) -> impl Content<TuiOut> + use<'_> {
ArrangerView::new(self)
}
pub fn view_pool (&self) -> impl Content<TuiOut> + use<'_> {
self.pool().map(|p|Fixed::x(self.w_sidebar(), PoolView(self.is_editing(), p)))
}
/// Spacing between tracks.
pub(crate) const TRACK_SPACING: usize = 0;
/// Default scene height.
pub(crate) const H_SCENE: usize = 2;
/// Default editor height.
pub(crate) const H_EDITOR: usize = 15;
/// Width of display
pub(crate) fn w (&self) -> u16 {
self.size.w() as u16
}
pub(crate) fn w_sidebar (&self) -> u16 {
self.w() / if self.is_editing() { 16 } else { 8 } as u16
}
/// Width taken by all tracks.
pub(crate) fn w_tracks (&self) -> u16 {
self.tracks_with_sizes().last().map(|(_, _, _, x)|x as u16).unwrap_or(0)
}
/// Width available to display tracks.
pub(crate) fn w_tracks_area (&self) -> u16 {
self.w().saturating_sub(2 * self.w_sidebar())
}
/// Height of display
pub(crate) fn h (&self) -> u16 {
self.size.h() as u16
}
/// Height available to display track headers.
pub(crate) fn h_tracks_area (&self) -> u16 {
5
//self.h().saturating_sub(self.h_inputs() + self.h_outputs())
}
/// Height available to display tracks.
pub(crate) fn h_scenes_area (&self) -> u16 {
//15
self.h().saturating_sub(self.h_inputs() + self.h_outputs() + 11)
}
/// Height taken by all inputs.
pub(crate) fn h_inputs (&self) -> u16 {
1 + self.inputs_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
}
/// Height taken by all outputs.
pub(crate) fn h_outputs (&self) -> u16 {
1 + self.outputs_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
}
/// Height taken by all scenes.
pub(crate) fn h_scenes (&self) -> u16 {
self.scenes_with_sizes(self.is_editing(), Self::H_SCENE, Self::H_EDITOR).last()
.map(|(_, _, _, y)|y as u16).unwrap_or(0)
}
pub(crate) fn inputs_with_sizes (&self) -> impl PortsSizes<'_> {
let mut y = 0;
self.midi_ins.iter().enumerate().map(move|(i, input)|{
let height = 1 + input.conn().len();
let data = (i, input.name(), input.conn(), y, y + height);
y += height;
data
})
}
pub(crate) fn outputs_with_sizes (&self) -> impl PortsSizes<'_> {
let mut y = 0;
self.midi_outs.iter().enumerate().map(move|(i, output)|{
let height = 1 + output.conn().len();
let data = (i, output.name(), output.conn(), y, y + height);
y += height;
data
})
}
pub(crate) fn tracks_with_sizes (&self) -> impl TracksSizes<'_> {
let mut x = 0;
let editing = self.is_editing();
let active = match self.selected() {
Selection::Track(t) if editing => Some(t),
Selection::Clip(t, _) if editing => Some(t),
_ => None
};
let bigger = self.editor_w();
self.tracks().iter().enumerate().map(move |(index, track)|{
let width = if Some(index) == active.copied() { bigger } else { track.width.max(8) };
let data = (index, track, x, x + width);
x += width + Tek::TRACK_SPACING;
data
})
}
pub(crate) fn scenes_with_sizes (&self, editing: bool, height: usize, larger: usize)
-> impl ScenesSizes<'_>
{
let (selected_track, selected_scene) = match self.selected() {
Selection::Track(t) => (Some(*t), None),
Selection::Scene(s) => (None, Some(*s)),
Selection::Clip(t, s) => (Some(*t), Some(*s)),
_ => (None, None)
};
let mut y = 0;
self.scenes().iter().enumerate().map(move|(s, scene)|{
let active = editing && selected_track.is_some() && selected_scene == Some(s);
let height = if active { larger } else { height };
let data = (s, scene, y, y + height);
y += height;
data
})
}
pub fn update_clock (&self) {
ViewCache::update_clock(&self.view_cache, self.clock(), self.size.w() > 80)
}
}
pub(crate) struct ArrangerView<'a> {
app: &'a Tek,
@ -146,14 +365,8 @@ impl<'a> ArrangerView<'a> {
/// Render output matrix.
pub(crate) fn outputs (&'a self) -> impl Content<TuiOut> + 'a {
Tui::bg(Color::Reset, Align::n(Bsp::s(
Bsp::s(
self.output_nexts(),
self.output_froms(),
),
Bsp::s(
self.output_ports(),
self.output_conns(),
)
Bsp::s(self.output_nexts(), self.output_froms()),
Bsp::s(self.output_ports(), self.output_conns()),
)))
}
@ -369,160 +582,6 @@ impl<'a> ArrangerView<'a> {
}
trait ScenesColors<'a> = Iterator<Item=SceneWithColor<'a>>;
type SceneWithColor<'a> = (usize, &'a Scene, usize, usize, Option<ItemTheme>);
impl Tek {
/// Spacing between tracks.
pub(crate) const TRACK_SPACING: usize = 0;
/// Default scene height.
pub(crate) const H_SCENE: usize = 2;
/// Default editor height.
pub(crate) const H_EDITOR: usize = 15;
/// Width of display
pub(crate) fn w (&self) -> u16 {
self.size.w() as u16
}
pub(crate) fn w_sidebar (&self) -> u16 {
self.w() / if self.is_editing() { 16 } else { 8 } as u16
}
/// Width taken by all tracks.
pub(crate) fn w_tracks (&self) -> u16 {
self.tracks_with_sizes().last().map(|(_, _, _, x)|x as u16).unwrap_or(0)
}
/// Width available to display tracks.
pub(crate) fn w_tracks_area (&self) -> u16 {
self.w().saturating_sub(2 * self.w_sidebar())
}
/// Height of display
pub(crate) fn h (&self) -> u16 {
self.size.h() as u16
}
/// Height available to display track headers.
pub(crate) fn h_tracks_area (&self) -> u16 {
5
//self.h().saturating_sub(self.h_inputs() + self.h_outputs())
}
/// Height available to display tracks.
pub(crate) fn h_scenes_area (&self) -> u16 {
//15
self.h().saturating_sub(self.h_inputs() + self.h_outputs() + 11)
}
/// Height taken by all inputs.
pub(crate) fn h_inputs (&self) -> u16 {
1 + self.inputs_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
}
/// Height taken by all outputs.
pub(crate) fn h_outputs (&self) -> u16 {
1 + self.outputs_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
}
/// Height taken by all scenes.
pub(crate) fn h_scenes (&self) -> u16 {
self.scenes_with_sizes(self.is_editing(), Self::H_SCENE, Self::H_EDITOR).last()
.map(|(_, _, _, y)|y as u16).unwrap_or(0)
}
pub(crate) fn inputs_with_sizes (&self) -> impl PortsSizes<'_> {
let mut y = 0;
self.midi_ins.iter().enumerate().map(move|(i, input)|{
let height = 1 + input.conn().len();
let data = (i, input.name(), input.conn(), y, y + height);
y += height;
data
})
}
pub(crate) fn outputs_with_sizes (&self) -> impl PortsSizes<'_> {
let mut y = 0;
self.midi_outs.iter().enumerate().map(move|(i, output)|{
let height = 1 + output.conn().len();
let data = (i, output.name(), output.conn(), y, y + height);
y += height;
data
})
}
pub(crate) fn tracks_with_sizes (&self) -> impl TracksSizes<'_> {
let mut x = 0;
let editing = self.is_editing();
let active = match self.selected() {
Selection::Track(t) if editing => Some(t),
Selection::Clip(t, _) if editing => Some(t),
_ => None
};
let bigger = self.editor_w();
self.tracks().iter().enumerate().map(move |(index, track)|{
let width = if Some(index) == active.copied() { bigger } else { track.width.max(8) };
let data = (index, track, x, x + width);
x += width + Tek::TRACK_SPACING;
data
})
}
pub(crate) fn scenes_with_sizes (&self, editing: bool, height: usize, larger: usize)
-> impl ScenesSizes<'_>
{
let (selected_track, selected_scene) = match self.selected() {
Selection::Track(t) => (Some(*t), None),
Selection::Scene(s) => (None, Some(*s)),
Selection::Clip(t, s) => (Some(*t), Some(*s)),
_ => (None, None)
};
let mut y = 0;
self.scenes().iter().enumerate().map(move|(s, scene)|{
let active = editing && selected_track.is_some() && selected_scene == Some(s);
let height = if active { larger } else { height };
let data = (s, scene, y, y + height);
y += height;
data
})
}
pub fn update_clock (&self) {
ViewCache::update_clock(&self.view_cache, self.clock(), self.size.w() > 80)
}
pub fn view_transport (&self) -> impl Content<TuiOut> + use<'_> {
self.update_clock();
let cache = self.view_cache.read().unwrap();
view_transport(
self.clock.is_rolling(),
cache.bpm.view.clone(),
cache.beat.view.clone(),
cache.time.view.clone(),
)
}
pub fn view_status (&self) -> impl Content<TuiOut> + use<'_> {
self.update_clock();
let cache = self.view_cache.read().unwrap();
view_status(
self.selected.describe(&self.tracks, &self.scenes),
cache.sr.view.clone(),
cache.buf.view.clone(),
cache.lat.view.clone(),
)
}
pub fn view_modal (&self) -> impl Content<TuiOut> + use<'_> {
When::new(self.modal.is_some(), Bsp::b(
Fill::xy(Tui::fg_bg(Color::Rgb(64,64,64), Color::Rgb(32,32,32), "")),
Fixed::xy(30, 15, Tui::fg_bg(Color::Rgb(255,255,255), Color::Rgb(16,16,16), self.modal))
))
}
pub fn view_arranger (&self) -> impl Content<TuiOut> + use<'_> {
ArrangerView::new(self)
}
pub fn view_pool (&self) -> impl Content<TuiOut> + use<'_> {
self.pool().map(|p|Fixed::x(self.w_sidebar(), PoolView(self.is_editing(), p)))
}
}
/// Define a type alias for iterators of sized items (columns).
macro_rules! def_sizes_iter {
($Type:ident => $($Item:ty),+) => {
@ -577,8 +636,7 @@ fn view_status (
pub(crate) fn button_play_pause (playing: bool) -> impl Content<TuiOut> {
let compact = true;//self.is_editing();
Tui::bg(
if playing{Rgb(0,128,0)}else{Rgb(128,64,0)},
Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) },
Either::new(compact,
Thunk::new(move||Fixed::x(9, Either::new(playing,
Tui::fg(Rgb(0, 255, 0), " PLAYING "),
@ -614,18 +672,15 @@ pub (crate) fn view_meter <'a> (label: &'a str, value: f32) -> impl Content<TuiO
}
pub(crate) fn view_meters (values: &[f32;2]) -> impl Content<TuiOut> + use<'_> {
Bsp::s(
format!("L/{:>+9.3}", values[0]),
format!("R/{:>+9.3}", values[1]),
)
let left = format!("L/{:>+9.3}", values[0]);
let right = format!("R/{:>+9.3}", values[1]);
Bsp::s(left, right)
}
pub(crate) fn wrap (
bg: Color, fg: Color, content: impl Content<TuiOut>
) -> impl Content<TuiOut> {
Bsp::e(Tui::fg_bg(bg, Reset, ""),
Bsp::w(Tui::fg_bg(bg, Reset, ""),
Tui::fg_bg(fg, bg, content)))
pub(crate) fn wrap (bg: Color, fg: Color, content: impl Content<TuiOut>) -> impl Content<TuiOut> {
let left = Tui::fg_bg(bg, Reset, "");
let right = Tui::fg_bg(bg, Reset, "");
Bsp::e(left, Bsp::w(right, Tui::fg_bg(fg, bg, content)))
}
pub(crate) fn button_2 <'a, K, L> (
@ -848,44 +903,3 @@ impl ViewCache {
}
}
}
impl Content<TuiOut> for Modal {
fn content (&self) -> impl Render<TuiOut> {
Bsp::b(
Repeat(" "),
Outer(true, Style::default().fg(Tui::g(96))).enclose(match self {
Self::Menu => self.view_menu().boxed(),
Self::Help => self.view_help().boxed(),
})
)
}
}
impl Modal {
fn view_menu (&self) -> impl Content<TuiOut> {
let options = ||["Projects", "Settings", "Help", "Quit"].iter();
let option = |a,i|Tui::fg(Color::Rgb(255,255,255), format!("{}", a));
Bsp::s(Tui::bold(true, "tek!"), Bsp::s("", Map::south(1, options, option)))
}
fn view_help (&self) -> impl Content<TuiOut> {
let bindings = ||TokenIter::new(include_str!("../../../config/keys_groovebox.edn"))
.filter_map(|x|if let Value::Exp(_, iter)=x.value{
Some(iter)
} else {
None
});
let binding = |mut binding: TokenIter, _|Bsp::e(
Tui::bold(true, Tui::fg(Color::Rgb(255,192,0), if let Some(Token { value: Value::Sym(key), .. }) = binding.next() {
Some(key.to_string())
} else {
None
})),
Bsp::e(" ", Tui::fg(Color::Rgb(255,255,255), if let Some(Token { value: Value::Key(command), .. }) = binding.next() {
Some(command.to_string())
} else {
None
})),
);
Bsp::s(Tui::bold(true, "Help"), Bsp::s("", Map::south(1, bindings, binding)))
}
}

View file

@ -106,13 +106,49 @@ impl Cli {
color: ItemTheme::random(),
clock: Clock::new(jack, self.bpm)?,
view: SourceIter(match mode {
LaunchMode::Clock => include_str!("../../config/view_transport.edn"),
LaunchMode::Sequencer => include_str!("../../config/view_sequencer.edn"),
LaunchMode::Groovebox => include_str!("../../config/view_groovebox.edn"),
LaunchMode::Arranger { .. } => include_str!("../../config/view_arranger.edn"),
LaunchMode::Sampler => include_str!("../../config/view_sampler.edn"),
LaunchMode::Clock =>
include_str!("../../config/view_transport.edn"),
LaunchMode::Sequencer =>
include_str!("../../config/view_sequencer.edn"),
LaunchMode::Groovebox =>
include_str!("../../config/view_groovebox.edn"),
LaunchMode::Arranger { .. } =>
include_str!("../../config/view_arranger.edn"),
LaunchMode::Sampler =>
include_str!("../../config/view_sampler.edn"),
_ => todo!("{mode:?}"),
}),
keys: match mode {
LaunchMode::Sampler => InputMap::default()
.layer(SourceIter(include_str!("../../config/keys_global.edn")))
.layer(SourceIter(include_str!("../../config/keys_sampler.edn"))),
LaunchMode::Clock => InputMap::default()
.layer(SourceIter(include_str!("../../config/keys_global.edn")))
.layer(SourceIter(include_str!("../../config/keys_clock.edn"))),
LaunchMode::Sequencer => InputMap::default()
.layer(SourceIter(include_str!("../../config/keys_global.edn")))
.layer(SourceIter(include_str!("../../config/keys_editor.edn")))
.layer(SourceIter(include_str!("../../config/keys_clock.edn")))
.layer(SourceIter(include_str!("../../config/keys_sequencer.edn"))),
LaunchMode::Groovebox => InputMap::default()
.layer(SourceIter(include_str!("../../config/keys_global.edn")))
.layer(SourceIter(include_str!("../../config/keys_editor.edn")))
.layer(SourceIter(include_str!("../../config/keys_clock.edn")))
.layer(SourceIter(include_str!("../../config/keys_sequencer.edn")))
.layer(SourceIter(include_str!("../../config/keys_groovebox.edn"))),
LaunchMode::Arranger {..} => InputMap::default()
.layer(SourceIter(include_str!("../../config/keys_global.edn")))
.layer_if(
|state: &Tek|state.is_editing(),
SourceIter(include_str!("../../config/keys_editor.edn")))
.layer(SourceIter(include_str!("../../config/keys_clock.edn")))
.layer(SourceIter(include_str!("../../config/keys_arranger.edn")))
.layer(SourceIter(include_str!("../../config/keys_clip.edn")))
.layer(SourceIter(include_str!("../../config/keys_track.edn")))
.layer(SourceIter(include_str!("../../config/keys_scene.edn")))
.layer(SourceIter(include_str!("../../config/keys_mix.edn"))),
_ => todo!("{mode:?}"),
},
pool: match mode {
LaunchMode::Sequencer | LaunchMode::Groovebox => clip.as_ref().map(Into::into),
LaunchMode::Arranger { .. } => Some(Default::default()),
@ -126,16 +162,13 @@ impl Cli {
midi_ins,
midi_outs,
midi_buf: match mode {
LaunchMode::Clock | LaunchMode::Sampler => vec![],
LaunchMode::Sequencer | LaunchMode::Groovebox | LaunchMode::Arranger {..} => vec![vec![];65536],
LaunchMode::Clock
| LaunchMode::Sampler => vec![],
LaunchMode::Sequencer
| LaunchMode::Groovebox
| LaunchMode::Arranger {..} => vec![vec![];65536],
_ => todo!("{mode:?}"),
},
handler: Some(match mode {
LaunchMode::Sequencer => handle_sequencer,
LaunchMode::Groovebox => handle_groovebox,
LaunchMode::Sampler => handle_sampler,
_ => handle_arranger,
}),
tracks: match mode {
LaunchMode::Sequencer => vec![
Track::new_sequencer()

View file

@ -3,9 +3,12 @@ use crate::*;
/// Contains state for viewing and editing a clip
pub struct MidiEditor {
pub mode: PianoHorizontal,
/// Size of editor on screen
pub size: Measure<TuiOut>,
pub keys: InputMap<'static, Self, MidiEditCommand, TuiIn>
/// View mode and state of editor
pub mode: PianoHorizontal,
/// Input keymap
pub keys: InputMap<'static, Self, MidiEditCommand, TuiIn, SourceIter<'static>>
}
impl std::fmt::Debug for MidiEditor {
@ -19,10 +22,9 @@ impl std::fmt::Debug for MidiEditor {
impl Default for MidiEditor {
fn default () -> Self {
Self {
mode: PianoHorizontal::new(None),
size: Measure::new(),
keys: InputMap::new()
.layer(SourceIter(include_str!("../../../../config/keys_edit.edn"))),
mode: PianoHorizontal::new(None),
keys: InputMap::new(SourceIter(include_str!("../../../../config/keys_editor.edn"))),
}
}
}
@ -206,14 +208,12 @@ atom_command!(MidiEditCommand: |state: MidiEditor| {
Show(Option<Arc<RwLock<MidiClip>>>),
}
handle!(TuiIn: |self: MidiEditor, input|{
Ok(if let Some(command) = self.keys.command(self, input) {
let _undo = command.execute(self)?;
Some(true)
} else {
None
})
});
handle!(TuiIn: |self: MidiEditor, input|Ok(if let Some(command) = self.keys.command(self, input) {
command.execute(self)?;
Some(true)
} else {
None
}));
impl Command<MidiEditor> for MidiEditCommand {
fn execute (self, state: &mut MidiEditor) -> Perhaps<Self> {

View file

@ -2,6 +2,7 @@ use crate::*;
use Color::*;
/// A clip, rendered as a horizontal piano roll.
#[derive(Clone)]
pub struct PianoHorizontal {
pub clip: Option<Arc<RwLock<MidiClip>>>,
/// Buffer where the whole clip is rerendered on change

View file

@ -1,6 +1,6 @@
use crate::*;
//#[derive(Debug)]
#[derive(Debug)]
pub struct MidiPool {
pub visible: bool,
/// Collection of clips
@ -10,7 +10,7 @@ pub struct MidiPool {
/// Mode switch
pub mode: Option<PoolMode>,
pub keys: InputMap<'static, Self, PoolCommand, TuiIn>,
pub keys: InputMap<'static, Self, PoolCommand, TuiIn, SourceIter<'static>>,
//pub keys: SourceIter<'static>,
//pub keys_rename: SourceIter<'static>,
//pub keys_length: SourceIter<'static>,
@ -25,9 +25,7 @@ impl Default for MidiPool {
clips: Arc::from(RwLock::from(vec![])),
clip: 0.into(),
mode: None,
keys: InputMap::new()
.layer(
SourceIter(include_str!("../../../../config/keys_edit.edn")))
keys: InputMap::new(SourceIter(include_str!("../../../../config/keys_pool.edn")))
.layer_if(|pool: &Self|matches!(pool.mode, Some(Import(..))|Some(Export(..))),
SourceIter(include_str!("../../../../config/keys_pool_file.edn")))
.layer_if(|pool: &Self|matches!(pool.mode, Some(Rename(..))),

View file

@ -3,10 +3,13 @@ use crate::*;
/// Trait for thing that may output MIDI.
pub trait HasMidiOuts {
fn midi_outs (&self) -> &Vec<JackMidiOut>;
fn midi_outs_mut (&mut self) -> &mut Vec<JackMidiOut>;
fn has_midi_outs (&self) -> bool {
!self.midi_outs().is_empty()
}
/// Buffer for serializing a MIDI event. FIXME rename
fn midi_note (&mut self) -> &mut Vec<u8>;
}

2
deps/tengri vendored

@ -1 +1 @@
Subproject commit 35ad37120554611197c1e30bbed657f310d332c3
Subproject commit 4ec51d5b694c14ccf617ec4538da04089b17ab92