diff --git a/Justfile b/Justfile index e91a5c36..0647fb44 100644 --- a/Justfile +++ b/Justfile @@ -13,6 +13,8 @@ grcov-ignore := "--ignore-not-existing --ignore '../*' --ignore \"/*\" --ignore default: just -l +bacon: + bacon -s tui: cargo run --example tui cloc: diff --git a/crates/app/app.rs b/crates/app/app.rs index f0202b96..5b4c74d4 100644 --- a/crates/app/app.rs +++ b/crates/app/app.rs @@ -40,9 +40,6 @@ use std::collections::BTreeMap; use std::fmt::Write; use ::tengri::tui::ratatui::prelude::Position; use xdg::BaseDirectories; -mod app_dsl; pub use self::app_dsl::*; -mod app_view; pub use self::app_view::*; -mod app_ctrl; pub use self::app_ctrl::*; mod app_jack; pub use self::app_jack::*; #[cfg(test)] mod app_test; /// Total state @@ -85,6 +82,17 @@ pub struct Mode { pub keys: Vec, pub modes: BTreeMap>, } +/// Various possible dialog modes. +#[derive(Debug, Clone, Default)] +pub enum Dialog { + #[default] None, + Help(usize), + Menu(usize), + Device(usize), + Message(Arc), + Browser(BrowserTarget, Arc), + Options, +} impl Config { const CONFIG: &'static str = "tek.edn"; const DEFAULTS: &'static str = include_str!("../../tek.edn"); @@ -111,7 +119,6 @@ impl Config { } pub fn load_defs (&mut self, dsl: impl Dsl) -> Usually<()> { dsl.each(|item|{ - println!("{item:?}"); match item.exp().head() { Ok(Some("keys")) if let Some(id) = item.exp().tail().head()? => self.load_bind(id.into(), item), @@ -170,29 +177,6 @@ impl Config { }) } } -fn load_modules (dsl: D, cb: impl Fn(&str, &str)->Usually) -> Usually<()> { - //dsl!(dsl|module :id ..body|dsl!(body|@bind #info? ..commands|) - if let Some(exp) = dsl.exp()? - && Some("module") == exp.head().key()? - && let Some(tail) = exp.tail()? - && let Some(id) = tail.head().sym()? - && let Some(body) = tail.tail()? - { - let _ = cb(id, body)?; - Ok(()) - } else { - return Err("unexpected: {exp:?}".into()); - } -} -/// Various possible dialog modes. -impl App { - pub fn update_clock (&self) { - ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) - } - pub fn focused_editor (&self) -> bool { - false - } -} has!(Jack<'static>: |self: App|self.jack); has!(Pool: |self: App|self.pool); has!(Dialog: |self: App|self.dialog); @@ -212,38 +196,711 @@ maybe_has!(Scene: |self: App| { MaybeHas::::get(&self.project) }; impl HasClipsSize for App { fn clips_size (&self) -> &Measure { &self.project.inner_size } } impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() } } impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } } - -#[derive(Debug, Clone, Default)] -pub enum Dialog { - #[default] None, - Help(usize), - Menu(usize), - Device(usize), - Message(Arc), - Browser(BrowserTarget, Arc), - Options, +pub fn view_nil (_: &App) -> Box> { + Box::new(Fill::xy("ยท")) +} +content!(TuiOut:|self: App|Fill::xy(Stack::above(|add|{ + for dsl in self.mode.view.iter() { add(&Fill::xy(self.view(dsl.as_ref()))); } +}))); +impl App { + fn view (&self, index: D) -> Box> { + match index.src() { + Ok(Some(src)) => render_dsl(self, src), + Ok(None) => Box::new(Tui::fg(Color::Rgb(192, 192, 192), "empty view")), + Err(e) => Box::new(format!("{e}")), + } + } + pub fn update_clock (&self) { + ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) + } } +fn render_dsl <'t, S> (state: &'t S, src: &str) -> Box> +where S: DslSymNs<'t, Box>> + DslExpNs<'t, Box>> +{ + if let Ok(Some(value)) = state.from_sym(src) { return value } + if let Ok(Some(value)) = state.from_exp(src) { return value } + Box::new(Bsp::s( + Fill::x(Align::w(Tui::bold(true, Tui::fg_bg(Color::Rgb(240,160,100), Color::Rgb(48,00,00), + "Could not render:")))), + Max::x(40, Tui::fg_bg(Color::Rgb(250,200,120), Color::Rgb(32,00,00), + format!("{src}"))) + )) +} + +dsl_exp!(|app: App, tail| -> Box> { + ("bold", value: bool, x: Box>) => Box::new(Tui::bold(value, x)), + ("fg", color: Color, x: Box>) => Box::new(Tui::fg(color, x)), + ("bg", color: Color, x: Box>) => Box::new(Tui::bg(color, x)), + ("fg/bg", fg: Color, bg: Color, x: Box>) => Box::new(Tui::fg_bg(fg, bg, x)), + ("bsp/n", a: Box>, b: Box>) => Box::new(Bsp::n(a, b)), + ("bsp/s", a: Box>, b: Box>) => Box::new(Bsp::s(a, b)), + ("bsp/e", a: Box>, b: Box>) => Box::new(Bsp::e(a, b)), + ("bsp/w", a: Box>, b: Box>) => Box::new(Bsp::w(a, b)), + ("bsp/a", a: Box>, b: Box>) => Box::new(Bsp::a(a, b)), + ("bsp/b", a: Box>, b: Box>) => Box::new(Bsp::b(a, b)), + ("align/n", x: Box>) => Box::new(Align::n(a, b)), + ("align/s", x: Box>) => Box::new(Align::s(a, b)), + ("align/e", x: Box>) => Box::new(Align::e(a, b)), + ("align/w", x: Box>) => Box::new(Align::w(a, b)), + ("align/x", x: Box>) => Box::new(Align::x(a, b)), + ("align/y", x: Box>) => Box::new(Align::y(a, b)), + ("align/c", x: Box>) => Box::new(Align::c(a, b)), + ("fill/x", x: Box>) => Box::new(Fill::x(x)), + ("fill/y", x: Box>) => Box::new(Fill::y(x)), + ("fill/xy", x: Box>) => Box::new(Fill::xy(x)), +}); + +dsl_exp!(|app: App, tail| -> Color { + ("rgb", r: u8, g: u8, b: u8) => Color::Rgb(r, g, b), + ("g", n: u8) => Color::Rgb(n. n. n), +}); + +dsl_sym!(|app: App| -> Box> { + + ":view/menu" => app.view(stringify!((bg (rgb 0 0 0) + (bsp/s :view/ports/outs (bsp/s (bg (rgb 33 33 33) (bold :true "tek 0.3.0-rc.0"))) + (bsp/n :view/ports/ins (bsp/n (bg (rgb 33 33 33) (bsp/e (fg (rgb 255 192 48) "[Enter]") " new session")) + (align/n (fill/xy :view/modes)))))))), + + ":view/ports/outs" => app.view(stringify!((fill/x (fixed/y 3 + (bsp/a (fill/x (align/w "L AUDIO OUT") + (bsp/a "MIDI OUT" (fill/x (align/e "AUDIO OUT R"))))))))), + + ":view/ports/ins" => app.view(stringify!(fill/x (fixed/y 3 + (bsp/a (fill/x (align/w "L AUDIO IN ") + (bsp/a "MIDI IN " (fill/x (align/e "AUDIO IN R")))))))), + + ":view/browse" => app.view(stringify!(bsp/s + (padding/xy 3 1 :view/browse-title) (enclose (fg (g 96)) :view/browser))), + + ":view/modes" => Box::new({ + let modes = app.config.modes.clone(); + Stack::south(move|add: &mut dyn FnMut(&dyn Render)|{ + for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { + let bg = if index == 0 { Rgb(64,64,64) } else { Rgb(32,32,32) }; + let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); + let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); + //add(&app.view(stringify!((fixed/y 2 (bg #bg (bsp/s + //(fill/x (bsp/a (fill/x (align/w (fg (rgb 224 192 128) #name))) + //(fill/x (align/e (fg (rgb 224 128 32) #id))))) + //(fill/x (align/w #info)))))))); + } + }) }), + + ":view/browse/title" => { + let title = match app.dialog.browser_target().unwrap() { + BrowserTarget::SaveProject => "Save project:", + BrowserTarget::LoadProject => "Load project:", + BrowserTarget::ImportSample(_) => "Import sample:", + BrowserTarget::ExportSample(_) => "Export sample:", + BrowserTarget::ImportClip(_) => "Import clip:", + BrowserTarget::ExportClip(_) => "Export clip:", + }; + Box::new(Fill::x(Align::w(FieldV(Default::default(), title, Shrink::x(3, Fixed::y(1, Tui::fg(Tui::g(96), RepeatH("๐Ÿญป")))))))) + }, + + ":view/device" => { + let selected = app.dialog.device_kind().unwrap(); + Box::new(Bsp::s(Tui::bold(true, "Add device"), Map::south(1, + move||device_kinds().iter(), + move|label: &&'static str, i|{ + let bg = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; + let lb = if i == selected { "[ " } else { " " }; + let rb = if i == selected { " ]" } else { " " }; + Fill::x(Tui::bg(bg, Bsp::e(lb, Bsp::w(rb, "FIXME device name")))) }))) }, + + //(":view/options", view_options), +}); +fn wrap_dialog (dialog: impl Content) -> impl Content { + Fixed::xy(70, 23, Tui::fg_bg(Rgb(255,255,255), Rgb(16,16,16), Bsp::b( + Repeat(" "), Outer(true, Style::default().fg(Tui::g(96))).enclose(dialog)))) +} +impl ScenesView for App { + fn h_scenes (&self) -> u16 { + (self.height() as u16).saturating_sub(20) + } + fn w_side (&self) -> u16 { + 20 + } + fn w_mid (&self) -> u16 { + (self.width() as u16).saturating_sub(self.w_side()) + } +} + + //Bsp::s("", + //Map::south(1, + //move||app.config.binds.layers.iter() + //.filter_map(|a|(a.0)(app).then_some(a.1)) + //.flat_map(|a|a) + //.filter_map(|x|if let Value::Exp(_, iter)=x.value{ Some(iter) } else { None }) + //.skip(offset) + //.take(20), + //|mut b,i|Fixed::x(60, Align::w(Bsp::e("(", Bsp::e( + //b.next().map(|t|Fixed::x(16, Align::w(Tui::fg(Rgb(64,224,0), format!("{}", t.value))))), + //Bsp::e(" ", Align::w(format!("{}", b.0.0.trim()))))))))))), + + //Dialog::Browser(BrowserTarget::Load, browser) => { + //"bobcat".boxed() + ////Bsp::s( + ////Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( + ////Tui::bold(true, " Load project: "), + ////Shrink::x(3, Fixed::y(1, RepeatH("๐Ÿญป"))))))), + ////Outer(true, Style::default().fg(Tui::g(96))) + ////.enclose(Fill::xy(browser))) + //}, + //Dialog::Browser(BrowserTarget::Export, browser) => { + //"bobcat".boxed() + ////Bsp::s( + ////Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( + ////Tui::bold(true, " Export: "), + ////Shrink::x(3, Fixed::y(1, RepeatH("๐Ÿญป"))))))), + ////Outer(true, Style::default().fg(Tui::g(96))) + ////.enclose(Fill::xy(browser))) + //}, + //Dialog::Browser(BrowserTarget::Import, browser) => { + //"bobcat".boxed() + ////Bsp::s( + ////Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( + ////Tui::bold(true, " Import: "), + ////Shrink::x(3, Fixed::y(1, RepeatH("๐Ÿญป"))))))), + ////Outer(true, Style::default().fg(Tui::g(96))) + ////.enclose(Fill::xy(browser))) + //}, +// + //pub fn view_meters_input (&self) -> impl Content + use<'_> { + //self.project.sampler().map(|s| + //s.view_meters_input()) + //} + //pub fn view_meters_output (&self) -> impl Content + use<'_> { + //self.project.sampler().map(|s| + //s.view_meters_output()) + //} + //pub fn view_history (&self) -> impl Content { + //Fixed::y(1, Fill::x(Align::w(FieldH(self.color, + //format!("History ({})", self.history.len()), + //self.history.last().map(|last|Fill::x(Align::w(format!("{:?}", last.0)))))))) + //} + //pub fn view_status_h2 (&self) -> impl Content { + //self.update_clock(); + //let theme = self.color; + //let clock = self.clock(); + //let playing = clock.is_rolling(); + //let cache = clock.view_cache.clone(); + ////let selection = self.selection().describe(self.tracks(), self.scenes()); + //let hist_len = self.history.len(); + //let hist_last = self.history.last(); + //Fixed::y(2, Stack::east(move|add: &mut dyn FnMut(&dyn Render)|{ + //add(&Fixed::x(5, Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, + //Either::new(false, // TODO + //Thunk::new(move||Fixed::x(9, Either::new(playing, + //Tui::fg(Rgb(0, 255, 0), " PLAYING "), + //Tui::fg(Rgb(255, 128, 0), " STOPPED "))) + //), + //Thunk::new(move||Fixed::x(5, Either::new(playing, + //Tui::fg(Rgb(0, 255, 0), Bsp::s(" ๐Ÿญ๐Ÿญ‘๐Ÿฌฝ ", " ๐Ÿญž๐Ÿญœ๐Ÿญ˜ ",)), + //Tui::fg(Rgb(255, 128, 0), Bsp::s(" โ–—โ–„โ–– ", " โ–โ–€โ–˜ ",)))) + //) + //) + //))); + //add(&" "); + //{ + //let cache = cache.read().unwrap(); + //add(&Fixed::x(15, Align::w(Bsp::s( + //FieldH(theme, "Beat", cache.beat.view.clone()), + //FieldH(theme, "Time", cache.time.view.clone()), + //)))); + //add(&Fixed::x(13, Align::w(Bsp::s( + //Fill::x(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), + //Fill::x(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), + //)))); + //add(&Fixed::x(12, Align::w(Bsp::s( + //Fill::x(Align::w(FieldH(theme, "Buf", cache.buf.view.clone()))), + //Fill::x(Align::w(FieldH(theme, "Lat", cache.lat.view.clone()))), + //)))); + ////add(&Bsp::s( + //////Fill::x(Align::w(FieldH(theme, "Selected", Align::w(selection)))), + ////Fill::x(Align::w(FieldH(theme, format!("History ({})", hist_len), + ////hist_last.map(|last|Fill::x(Align::w(format!("{:?}", last.0))))))), + ////"" + ////)); + //////if let Some(last) = self.history.last() { + //////add(&FieldV(theme, format!("History ({})", self.history.len()), + //////Fill::x(Align::w(format!("{:?}", last.0))))); + //////} + //} + //})) + //} + //pub fn view_status_v (&self) -> impl Content + use<'_> { + //self.update_clock(); + //let cache = self.project.clock.view_cache.read().unwrap(); + //let theme = self.color; + //let playing = self.clock().is_rolling(); + //Tui::bg(theme.darker.rgb, Fixed::xy(20, 5, Outer(true, Style::default().fg(Tui::g(96))).enclose( + //col!( + //Fill::x(Align::w(Bsp::e( + //Align::w(Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, + //Either::new(false, // TODO + //Thunk::new(move||Fixed::x(9, Either::new(playing, + //Tui::fg(Rgb(0, 255, 0), " PLAYING "), + //Tui::fg(Rgb(255, 128, 0), " STOPPED "))) + //), + //Thunk::new(move||Fixed::x(5, Either::new(playing, + //Tui::fg(Rgb(0, 255, 0), Bsp::s(" ๐Ÿญ๐Ÿญ‘๐Ÿฌฝ ", " ๐Ÿญž๐Ÿญœ๐Ÿญ˜ ",)), + //Tui::fg(Rgb(255, 128, 0), Bsp::s(" โ–—โ–„โ–– ", " โ–โ–€โ–˜ ",)))) + //) + //) + //)), + //Bsp::s( + //FieldH(theme, "Beat", cache.beat.view.clone()), + //FieldH(theme, "Time", cache.time.view.clone()), + //), + //))), + //Fill::x(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), + //Fill::x(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), + //Fill::x(Align::w(FieldH(theme, "Buf", Bsp::e(cache.buf.view.clone(), Bsp::e(" = ", cache.lat.view.clone()))))), + //)))) + //} + //pub fn view_status (&self) -> impl Content + use<'_> { + //self.update_clock(); + //let cache = self.project.clock.view_cache.read().unwrap(); + //view_status(Some(self.project.selection.describe(self.tracks(), self.scenes())), + //cache.sr.view.clone(), cache.buf.view.clone(), cache.lat.view.clone()) + //} + //pub fn view_transport (&self) -> impl Content + use<'_> { + //self.update_clock(); + //let cache = self.project.clock.view_cache.read().unwrap(); + //view_transport(self.project.clock.is_rolling(), + //cache.bpm.view.clone(), cache.beat.view.clone(), cache.time.view.clone()) + //} + //pub fn view_editor (&self) -> impl Content + use<'_> { + //let bg = self.editor() + //.and_then(|editor|editor.clip().clone()) + //.map(|clip|clip.read().unwrap().color.darker) + //.unwrap_or(self.color.darker); + //Fill::xy(Tui::bg(bg.rgb, self.editor())) + //} + //pub fn view_editor_status (&self) -> impl Content + use<'_> { + //self.editor().map(|e|Fixed::x(20, Outer(true, Style::default().fg(Tui::g(96))).enclose( + //Fill::y(Align::n(Bsp::s(e.clip_status(), e.edit_status())))))) + //} + //pub fn view_midi_ins_status (&self) -> impl Content + use<'_> { + //self.project.view_midi_ins_status(self.color) + //} + //pub fn view_midi_outs_status (&self) -> impl Content + use<'_> { + //self.project.view_midi_outs_status(self.color) + //} + //pub fn view_audio_ins_status (&self) -> impl Content + use<'_> { + //self.project.view_audio_ins_status(self.color) + //} + //pub fn view_audio_outs_status (&self) -> impl Content + use<'_> { + //self.project.view_audio_outs_status(self.color) + //} + //pub fn view_scenes (&self) -> impl Content + use<'_> { + //Bsp::e( + //Fixed::x(20, Align::nw(self.project.view_scenes_names())), + //self.project.view_scenes_clips(), + //) + //} + //pub fn view_scenes_names (&self) -> impl Content + use<'_> { + //self.project.view_scenes_names() + //} + //pub fn view_scenes_clips (&self) -> impl Content + use<'_> { + //self.project.view_scenes_clips() + //} + //pub fn view_tracks_inputs <'a> (&'a self) -> impl Content + use<'a> { + //Fixed::y(1 + self.project.midi_ins.len() as u16, + //self.project.view_inputs(self.color)) + //} + //pub fn view_tracks_outputs <'a> (&'a self) -> impl Content + use<'a> { + //self.project.view_outputs(self.color) + //} + //pub fn view_tracks_devices <'a> (&'a self) -> impl Content + use<'a> { + //Fixed::y(4, self.project.view_track_devices(self.color)) + //} + //pub fn view_tracks_names <'a> (&'a self) -> impl Content + use<'a> { + //Fixed::y(2, self.project.view_track_names(self.color)) + //} + //pub fn view_pool (&self) -> impl Content + use<'_> { + //Fixed::x(20, Bsp::s( + //Fill::x(Align::w(FieldH(self.color, "Clip pool:", ""))), + //Fill::y(Align::n(Tui::bg(Rgb(0, 0, 0), Outer(true, Style::default().fg(Tui::g(96))) + //.enclose(PoolView(&self.pool))))))) + //} + //pub fn view_samples_keys (&self) -> impl Content + use<'_> { + //self.project.sampler().map(|s|s.view_list(true, self.editor().unwrap())) + //} + //pub fn view_samples_grid (&self) -> impl Content + use<'_> { + //self.project.sampler().map(|s|s.view_grid()) + //} + //pub fn view_sample_viewer (&self) -> impl Content + use<'_> { + //self.project.sampler().map(|s|s.view_sample(self.editor().unwrap().get_note_pos())) + //} + //pub fn view_sample_info (&self) -> impl Content + use<'_> { + //self.project.sampler().map(|s|s.view_sample_info(self.editor().unwrap().get_note_pos())) + //} + //pub fn view_sample_status (&self) -> impl Content + use<'_> { + //self.project.sampler().map(|s|Outer(true, Style::default().fg(Tui::g(96))).enclose( + //Fill::y(Align::n(s.view_sample_status(self.editor().unwrap().get_note_pos()))))) + //} + ////let options = ||["Projects", "Settings", "Help", "Quit"].iter(); + ////let option = |a,i|Tui::fg(Rgb(255,255,255), format!("{}", a)); + ////Bsp::s(Tui::bold(true, "tek!"), Bsp::s("", Map::south(1, options, option))) +handle!(TuiIn:|self: App, input|{ + panic!("wat: {:?}", self.mode); + for keys in self.mode.keys.iter() { + panic!("{keys} {:?}", self.config.binds.read().unwrap()); + if let Some(binding) = self.config.binds.read().unwrap().get(keys.as_ref()) { + panic!("{binding:?}"); + } + } + Ok(None) + //Ok(if let Some(binding) = self.mode.as_ref() + //.map(|c|c.keys.dispatch(input.event())).flatten() + //{ + //let binding = binding.clone(); + //let undo = binding.command.clone().execute(self)?; + //// FIXME failed commands are not persisted in undo history + ////self.history.push((binding.command.clone(), undo)); + //Some(true) + //} else { + //None + //}) +}); +dsl_sym!(|app: App| -> isize { + ":_isize_stub" => -1 +}); +dsl_sym!(|app: App| -> ItemTheme { + ":_theme_stub" => Default::default() +}); +dsl_sym!(|app: App| -> u16{ + ":w/sidebar" => app.project.w_sidebar(app.editor().is_some()), + ":h/sample-detail" => 6.max(app.height() as u16 * 3 / 9), +}); +dsl_sym!(|app: App| -> usize { + ":scene-count" => app.scenes().len(), + ":track-count" => app.tracks().len(), + ":device-kind" => app.dialog.device_kind().unwrap_or(0), + ":device-kind/next" => app.dialog.device_kind_next().unwrap_or(0), + ":device-kind/prev" => app.dialog.device_kind_prev().unwrap_or(0), +}); +dsl_sym!(|app: App| -> bool { + ":focused/editor" => app.project.editor.is_some(), + ":focused/dialog" => !matches!(app.dialog, Dialog::None), + ":focused/message" => matches!(app.dialog, Dialog::Message(..)), + ":focused/add_device" => matches!(app.dialog, Dialog::Device(..)), + ":focused/browser" => app.dialog.browser().is_some(), + ":focused/clip" => !app.focused_editor() && matches!(app.selection(), Selection::TrackClip{..}), + ":focused/track" => !app.focused_editor() && matches!(app.selection(), Selection::Track(..)), + ":focused/scene" => !app.focused_editor() && matches!(app.selection(), Selection::Scene(..)), + ":focused/mix" => !app.focused_editor() && matches!(app.selection(), Selection::Mix), + ":focused/pool/import" => matches!(app.pool.mode, Some(PoolMode::Import(..))), + ":focused/pool/export" => matches!(app.pool.mode, Some(PoolMode::Export(..))), + ":focused/pool/rename" => matches!(app.pool.mode, Some(PoolMode::Rename(..))), + ":focused/pool/length" => matches!(app.pool.mode, Some(PoolMode::Length(..))), +}); +dsl_sym!(|app: App| -> Dialog { + ":dialog/none" => Dialog::None, + ":dialog/options" => Dialog::Options, + ":dialog/device" => Dialog::Device(0), + ":dialog/device/prev" => Dialog::Device(0), + ":dialog/device/next" => Dialog::Device(0), + ":dialog/help" => Dialog::Help(0), + ":dialog/menu" => Dialog::Menu(0), + ":dialog/save" => Dialog::Browser(BrowserTarget::SaveProject, Browser::new(None).unwrap().into()), + ":dialog/load" => Dialog::Browser(BrowserTarget::LoadProject, Browser::new(None).unwrap().into()), + ":dialog/import/clip" => Dialog::Browser(BrowserTarget::ImportClip(Default::default()), Browser::new(None).unwrap().into()), + ":dialog/export/clip" => Dialog::Browser(BrowserTarget::ExportClip(Default::default()), Browser::new(None).unwrap().into()), + ":dialog/import/sample" => Dialog::Browser(BrowserTarget::ImportSample(Default::default()), Browser::new(None).unwrap().into()), + ":dialog/export/sample" => Dialog::Browser(BrowserTarget::ExportSample(Default::default()), Browser::new(None).unwrap().into()), +}); +dsl_sym!(|app: App| -> Selection { + ":select/scene" => app.selection().select_scene(app.tracks().len()), + ":select/scene/next" => app.selection().select_scene_next(app.scenes().len()), + ":select/scene/prev" => app.selection().select_scene_prev(), + ":select/track" => app.selection().select_track(app.tracks().len()), + ":select/track/next" => app.selection().select_track_next(app.tracks().len()), + ":select/track/prev" => app.selection().select_track_prev(), +}); +dsl_sym!(|app: App| -> Option { + ":editor/pitch" => Some((app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into()) +}); +dsl_sym!(|app: App| -> Option { + ":selected/scene" => app.selection().scene(), + ":selected/track" => app.selection().track(), +}); +dsl_sym!(|app: App| -> Option>> { + ":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() { + app.scenes()[*scene].clips[*track].clone() + } else { + None + } +}); +#[derive(Debug)] +pub enum AppCommand { +} +dsl_exp!(|app: App| -> AppCommand { + ("stop-all") => app.project.stop_all(), + ("enqueue", clip?: Option>>) => todo!(), + ("history", delta: isize) => todo!(), + ("zoom", zoom: usize) => todo!(), + ("select", selection: Selection) => todo!(), + ("dialog" / command: DialogCommand) => todo!(), + ("project" / command: ArrangementCommand) => todo!(), + ("clock" / command: ClockCommand) => todo!(), + ("sampler" / command: SamplerCommand) => todo!(), + ("pool" / command: PoolCommand) => todo!(), + ("pool" / editor: MidiEditCommand) => todo!(), +}); +dsl_exp!(|app: App| -> DialogCommand {}); +dsl_exp!(|app: App| -> ArrangementCommand {}); +dsl_exp!(|app: App| -> ClockCommand {}); +dsl_exp!(|app: App| -> SamplerCommand {}); +dsl_exp!(|app: App| -> PoolCommand {}); +dsl_exp!(|app: App| -> MidiEditCommand {}); +impl App { + pub fn focused_editor (&self) -> bool { + false + } +} impl Dialog { - fn menu_selected (&self) -> Option { + pub fn menu_selected (&self) -> Option { if let Self::Menu(selected) = self { Some(*selected) } else { None } } - fn device_kind (&self) -> Option { + pub fn device_kind (&self) -> Option { if let Self::Device(index) = self { Some(*index) } else { None } } - fn device_kind_next (&self) -> Option { + pub fn device_kind_next (&self) -> Option { self.device_kind().map(|index|(index + 1) % device_kinds().len()) } - fn device_kind_prev (&self) -> Option { + pub fn device_kind_prev (&self) -> Option { self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1))) } - fn message (&self) -> Option<&str> { + pub fn message (&self) -> Option<&str> { todo!() } - fn browser (&self) -> Option<&Arc> { + pub fn browser (&self) -> Option<&Arc> { todo!() } - fn browser_target (&self) -> Option<&BrowserTarget> { + pub fn browser_target (&self) -> Option<&BrowserTarget> { todo!() } } +//dsl_bind!(AppCommand: App { + //enqueue = |app, clip: Option>>| { todo!() }; + //history = |app, delta: isize| { todo!() }; + //zoom = |app, zoom: usize| { todo!() }; + //stop_all = |app| { app.tracks_stop_all(); Ok(None) }; + ////dialog = |app, command: DialogCommand| + ////Ok(command.delegate(&mut app.dialog, |c|Self::Dialog{command: c})?); + //project = |app, command: ArrangementCommand| + //Ok(command.delegate(&mut app.project, |c|Self::Project{command: c})?); + //clock = |app, command: ClockCommand| + //Ok(command.execute(app.clock_mut())?.map(|c|Self::Clock{command: c})); + //sampler = |app, command: SamplerCommand| + //Ok(app.project.sampler_mut().map(|s|command.delegate(s, |command|Self::Sampler{command})) + //.transpose()?.flatten()); + //pool = |app, command: PoolCommand| { + //let undo = command.clone().delegate(&mut app.pool, |command|AppCommand::Pool{command})?; + //// update linked editor after pool action + //match command { + //// autoselect: automatically load selected clip in editor + //PoolCommand::Select { .. } | + //// autocolor: update color in all places simultaneously + //PoolCommand::Clip { command: PoolClipCommand::SetColor { .. } } => { + //let clip = app.pool.clip().clone(); + //app.editor_mut().map(|editor|editor.set_clip(clip.as_ref())) + //}, + //_ => None + //}; + //Ok(undo) + //}; + //select = |app, selection: Selection| { + //*app.project.selection_mut() = selection; + ////todo! + ////if let Some(ref mut editor) = app.editor_mut() { + ////editor.set_clip(match selection { + ////Selection::TrackClip { track, scene } if let Some(Some(Some(clip))) = app + ////.project + ////.scenes.get(scene) + ////.map(|s|s.clips.get(track)) + ////=> + ////Some(clip), + ////_ => + ////None + ////}); + ////} + //Ok(None) + ////("select" [t: usize, s: usize] Some(match (t.expect("no track"), s.expect("no scene")) { + ////(0, 0) => Self::Select(Selection::Mix), + ////(t, 0) => Self::Select(Selection::Track(t)), + ////(0, s) => Self::Select(Selection::Scene(s)), + ////(t, s) => Self::Select(Selection::TrackClip { track: t, scene: s }) }))) + //// autoedit: load focused clip in editor. + //}; + ////fn color (app: &mut App, theme: ItemTheme) -> Perhaps { + ////Ok(app.set_color(Some(theme)).map(|theme|Self::Color{theme})) + ////} + ////fn launch (app: &mut App) -> Perhaps { + ////app.project.launch(); + ////Ok(None) + ////} + //toggle_editor = |app, value: bool|{ app.toggle_editor(Some(value)); Ok(None) }; + //editor = |app, command: MidiEditCommand| Ok(if let Some(editor) = app.editor_mut() { + //let undo = command.clone().delegate(editor, |command|AppCommand::Editor{command})?; + //// update linked sampler after editor action + //app.project.sampler_mut().map(|sampler|match command { + //// autoselect: automatically select sample in sampler + //MidiEditCommand::SetNotePos { pos } => { sampler.set_note_pos(pos); }, + //_ => {} + //}); + //undo + //} else { + //None + //}); +//}); +//take!(ClockCommand |state: App, iter|Take::take(state.clock(), iter)); +//take!(MidiEditCommand |state: App, iter|Ok(state.editor().map(|x|Take::take(x, iter)).transpose()?.flatten())); +//take!(PoolCommand |state: App, iter|Take::take(&state.pool, iter)); +//take!(SamplerCommand |state: App, iter|Ok(state.project.sampler().map(|x|Take::take(x, iter)).transpose()?.flatten())); +//take!(ArrangementCommand |state: App, iter|Take::take(&state.project, iter)); +//take!(DialogCommand |state: App, iter|Take::take(&state.dialog, iter)); +//has_editor!(|self: App|{ + //editor = self.editor; + //editor_w = { + //let size = self.size.w(); + //let editor = self.editor.as_ref().expect("missing editor"); + //let time_len = editor.time_len().get(); + //let time_zoom = editor.time_zoom().get().max(1); + //(5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16) + //}; + //editor_h = 15; + //is_editing = self.editor.is_some(); +//}); +#[tengri_proc::command(Option)] impl DialogCommand { + fn open (dialog: &mut Option, new: Dialog) -> Perhaps { + *dialog = Some(new); + Ok(None) + } + fn close (dialog: &mut Option) -> Perhaps { + *dialog = None; + Ok(None) + } +} + +impl App { + pub fn toggle_dialog (&mut self, mut dialog: Dialog) -> Dialog { + std::mem::swap(&mut self.dialog, &mut dialog); + dialog + } + pub fn toggle_editor (&mut self, value: Option) { + //FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed); + let value = value.unwrap_or_else(||!self.editor().is_some()); + if value { + // Create new clip in pool when entering empty cell + if let Selection::TrackClip { track, scene } = *self.selection() + && let Some(scene) = self.project.scenes.get_mut(scene) + && let Some(slot) = scene.clips.get_mut(track) + && slot.is_none() + && let Some(track) = self.project.tracks.get_mut(track) + { + let (index, mut clip) = self.pool.add_new_clip(); + // autocolor: new clip colors from scene and track color + let color = track.color.base.mix(scene.color.base, 0.5); + clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); + if let Some(ref mut editor) = &mut self.project.editor { + editor.set_clip(Some(&clip)); + } + *slot = Some(clip.clone()); + //Some(clip) + } else { + //None + } + } else if let Selection::TrackClip { track, scene } = *self.selection() + && let Some(scene) = self.project.scenes.get_mut(scene) + && let Some(slot) = scene.clips.get_mut(track) + && let Some(clip) = slot.as_mut() + { + // Remove clip from arrangement when exiting empty clip editor + let mut swapped = None; + if clip.read().unwrap().count_midi_messages() == 0 { + std::mem::swap(&mut swapped, slot); + } + if let Some(clip) = swapped { + self.pool.delete_clip(&clip.read().unwrap()); + } + } + } + pub fn browser (&self) -> Option<&Browser> { + if let Dialog::Browser(_, ref b) = self.dialog { Some(b) } else { None } + } + pub fn device_pick (&mut self, index: usize) { + self.dialog = Dialog::Device(index); + } + pub fn add_device (&mut self, index: usize) -> Usually<()> { + match index { + 0 => { + let name = self.jack.with_client(|c|c.name().to_string()); + let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name(); + let track = self.track().expect("no active track"); + let port = format!("{}/Sampler", &track.name); + let connect = Connect::exact(format!("{name}:{midi}")); + let sampler = if let Ok(sampler) = Sampler::new( + &self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]] + ) { + self.dialog = Dialog::None; + Device::Sampler(sampler) + } else { + self.dialog = Dialog::Message("Failed to add device.".into()); + return Err("failed to add device".into()) + }; + let track = self.track_mut().expect("no active track"); + track.devices.push(sampler); + Ok(()) + }, + 1 => { + todo!(); + Ok(()) + }, + _ => unreachable!(), + } + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +//#[derive(Clone, Debug)] +//pub enum DialogCommand { + //Open { dialog: Dialog }, + //Close +//} + +//impl Command> for DialogCommand { + //fn execute (self, state: &mut Option) -> Perhaps { + //match self { + //Self::Open { dialog } => { + //*state = Some(dialog); + //}, + //Self::Close => { + //*state = None; + //} + //}; + //Ok(None) + //} +//} + +//dsl!(DialogCommand: |self: Dialog, iter|todo!()); +//Dsl::take(&mut self.dialog, iter)); + +//#[tengri_proc::command(Option)]//Nope. +//impl DialogCommand { + //fn open (dialog: &mut Option, new: Dialog) -> Perhaps { + //*dialog = Some(new); + //Ok(None) + //} + //fn close (dialog: &mut Option) -> Perhaps { + //*dialog = None; + //Ok(None) + //} +//} +// diff --git a/crates/app/app_ctrl.rs b/crates/app/app_ctrl.rs deleted file mode 100644 index 95d6738b..00000000 --- a/crates/app/app_ctrl.rs +++ /dev/null @@ -1,331 +0,0 @@ -use crate::*; -handle!(TuiIn:|self: App, input|{ - panic!("wat: {:?}", self.mode); - for keys in self.mode.keys.iter() { - panic!("{keys} {:?}", self.config.binds.read().unwrap()); - if let Some(binding) = self.config.binds.read().unwrap().get(keys.as_ref()) { - panic!("{binding:?}"); - } - } - Ok(None) - //Ok(if let Some(binding) = self.mode.as_ref() - //.map(|c|c.keys.dispatch(input.event())).flatten() - //{ - //let binding = binding.clone(); - //let undo = binding.command.clone().execute(self)?; - //// FIXME failed commands are not persisted in undo history - ////self.history.push((binding.command.clone(), undo)); - //Some(true) - //} else { - //None - //}) -}); -dsl_sym!(|app: App| -> isize { - ":_isize_stub" => -1 -}); -dsl_sym!(|app: App| -> ItemTheme { - ":_theme_stub" => Default::default() -}); -dsl_sym!(|app: App| -> u16{ - ":w/sidebar" => app.project.w_sidebar(app.editor().is_some()), - ":h/sample-detail" => 6.max(app.height() as u16 * 3 / 9), -}); -dsl_sym!(|app: App| -> usize { - ":scene-count" => app.scenes().len(), - ":track-count" => app.tracks().len(), - ":device-kind" => app.dialog.device_kind().unwrap_or(0), - ":device-kind/next" => app.dialog.device_kind_next().unwrap_or(0), - ":device-kind/prev" => app.dialog.device_kind_prev().unwrap_or(0), -}); -dsl_sym!(|app: App| -> bool { - ":focused/editor" => app.project.editor.is_some(), - ":focused/dialog" => !matches!(app.dialog, Dialog::None), - ":focused/message" => matches!(app.dialog, Dialog::Message(..)), - ":focused/add_device" => matches!(app.dialog, Dialog::Device(..)), - ":focused/browser" => app.dialog.browser().is_some(), - ":focused/clip" => !app.focused_editor() && matches!(app.selection(), Selection::TrackClip{..}), - ":focused/track" => !app.focused_editor() && matches!(app.selection(), Selection::Track(..)), - ":focused/scene" => !app.focused_editor() && matches!(app.selection(), Selection::Scene(..)), - ":focused/mix" => !app.focused_editor() && matches!(app.selection(), Selection::Mix), - ":focused/pool/import" => matches!(app.pool.mode, Some(PoolMode::Import(..))), - ":focused/pool/export" => matches!(app.pool.mode, Some(PoolMode::Export(..))), - ":focused/pool/rename" => matches!(app.pool.mode, Some(PoolMode::Rename(..))), - ":focused/pool/length" => matches!(app.pool.mode, Some(PoolMode::Length(..))), -}); -dsl_sym!(|app: App| -> Dialog { - ":dialog/none" => Dialog::None, - ":dialog/options" => Dialog::Options, - ":dialog/device" => Dialog::Device(0), - ":dialog/device/prev" => Dialog::Device(0), - ":dialog/device/next" => Dialog::Device(0), - ":dialog/help" => Dialog::Help(0), - ":dialog/menu" => Dialog::Menu(0), - ":dialog/save" => Dialog::Browser(BrowserTarget::SaveProject, Browser::new(None).unwrap().into()), - ":dialog/load" => Dialog::Browser(BrowserTarget::LoadProject, Browser::new(None).unwrap().into()), - ":dialog/import/clip" => Dialog::Browser(BrowserTarget::ImportClip(Default::default()), Browser::new(None).unwrap().into()), - ":dialog/export/clip" => Dialog::Browser(BrowserTarget::ExportClip(Default::default()), Browser::new(None).unwrap().into()), - ":dialog/import/sample" => Dialog::Browser(BrowserTarget::ImportSample(Default::default()), Browser::new(None).unwrap().into()), - ":dialog/export/sample" => Dialog::Browser(BrowserTarget::ExportSample(Default::default()), Browser::new(None).unwrap().into()), -}); -dsl_sym!(|app: App| -> Selection { - ":select/scene" => app.selection().select_scene(app.tracks().len()), - ":select/scene/next" => app.selection().select_scene_next(app.scenes().len()), - ":select/scene/prev" => app.selection().select_scene_prev(), - ":select/track" => app.selection().select_track(app.tracks().len()), - ":select/track/next" => app.selection().select_track_next(app.tracks().len()), - ":select/track/prev" => app.selection().select_track_prev(), -}); -dsl_sym!(|app: App| -> Option { - ":editor/pitch" => Some((app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into()) -}); -dsl_sym!(|app: App| -> Option { - ":selected/scene" => app.selection().scene(), - ":selected/track" => app.selection().track(), -}); -dsl_sym!(|app: App| -> Option>> { - ":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() { - app.scenes()[*scene].clips[*track].clone() - } else { - None - } -}); -#[derive(Debug)] -pub enum AppCommand { -} -dsl_exp!(|app: App| -> AppCommand { - ["stop-all"] => app.project.stop_all(), - ["enqueue", clip?: Option>>] => todo!(), - ["history", delta: isize] => todo!(), - ["zoom", zoom: usize] => todo!(), - ["select", selection: Selection] => todo!(), - ["dialog" / command: DialogCommand] => todo!(), - ["project" / command: ArrangementCommand] => todo!(), - ["clock" / command: ClockCommand] => todo!(), - ["sampler" / command: SamplerCommand] => todo!(), - ["pool" / command: PoolCommand] => todo!(), - ["pool" / editor: MidiEditCommand] => todo!(), -}); -dsl_exp!(|app: App| -> DialogCommand {}); -dsl_exp!(|app: App| -> ArrangementCommand {}); -dsl_exp!(|app: App| -> ClockCommand {}); -dsl_exp!(|app: App| -> SamplerCommand {}); -dsl_exp!(|app: App| -> PoolCommand {}); -dsl_exp!(|app: App| -> MidiEditCommand {}); -//dsl_bind!(AppCommand: App { - //enqueue = |app, clip: Option>>| { todo!() }; - //history = |app, delta: isize| { todo!() }; - //zoom = |app, zoom: usize| { todo!() }; - //stop_all = |app| { app.tracks_stop_all(); Ok(None) }; - ////dialog = |app, command: DialogCommand| - ////Ok(command.delegate(&mut app.dialog, |c|Self::Dialog{command: c})?); - //project = |app, command: ArrangementCommand| - //Ok(command.delegate(&mut app.project, |c|Self::Project{command: c})?); - //clock = |app, command: ClockCommand| - //Ok(command.execute(app.clock_mut())?.map(|c|Self::Clock{command: c})); - //sampler = |app, command: SamplerCommand| - //Ok(app.project.sampler_mut().map(|s|command.delegate(s, |command|Self::Sampler{command})) - //.transpose()?.flatten()); - //pool = |app, command: PoolCommand| { - //let undo = command.clone().delegate(&mut app.pool, |command|AppCommand::Pool{command})?; - //// update linked editor after pool action - //match command { - //// autoselect: automatically load selected clip in editor - //PoolCommand::Select { .. } | - //// autocolor: update color in all places simultaneously - //PoolCommand::Clip { command: PoolClipCommand::SetColor { .. } } => { - //let clip = app.pool.clip().clone(); - //app.editor_mut().map(|editor|editor.set_clip(clip.as_ref())) - //}, - //_ => None - //}; - //Ok(undo) - //}; - //select = |app, selection: Selection| { - //*app.project.selection_mut() = selection; - ////todo! - ////if let Some(ref mut editor) = app.editor_mut() { - ////editor.set_clip(match selection { - ////Selection::TrackClip { track, scene } if let Some(Some(Some(clip))) = app - ////.project - ////.scenes.get(scene) - ////.map(|s|s.clips.get(track)) - ////=> - ////Some(clip), - ////_ => - ////None - ////}); - ////} - //Ok(None) - ////("select" [t: usize, s: usize] Some(match (t.expect("no track"), s.expect("no scene")) { - ////(0, 0) => Self::Select(Selection::Mix), - ////(t, 0) => Self::Select(Selection::Track(t)), - ////(0, s) => Self::Select(Selection::Scene(s)), - ////(t, s) => Self::Select(Selection::TrackClip { track: t, scene: s }) }))) - //// autoedit: load focused clip in editor. - //}; - ////fn color (app: &mut App, theme: ItemTheme) -> Perhaps { - ////Ok(app.set_color(Some(theme)).map(|theme|Self::Color{theme})) - ////} - ////fn launch (app: &mut App) -> Perhaps { - ////app.project.launch(); - ////Ok(None) - ////} - //toggle_editor = |app, value: bool|{ app.toggle_editor(Some(value)); Ok(None) }; - //editor = |app, command: MidiEditCommand| Ok(if let Some(editor) = app.editor_mut() { - //let undo = command.clone().delegate(editor, |command|AppCommand::Editor{command})?; - //// update linked sampler after editor action - //app.project.sampler_mut().map(|sampler|match command { - //// autoselect: automatically select sample in sampler - //MidiEditCommand::SetNotePos { pos } => { sampler.set_note_pos(pos); }, - //_ => {} - //}); - //undo - //} else { - //None - //}); -//}); -//take!(ClockCommand |state: App, iter|Take::take(state.clock(), iter)); -//take!(MidiEditCommand |state: App, iter|Ok(state.editor().map(|x|Take::take(x, iter)).transpose()?.flatten())); -//take!(PoolCommand |state: App, iter|Take::take(&state.pool, iter)); -//take!(SamplerCommand |state: App, iter|Ok(state.project.sampler().map(|x|Take::take(x, iter)).transpose()?.flatten())); -//take!(ArrangementCommand |state: App, iter|Take::take(&state.project, iter)); -//take!(DialogCommand |state: App, iter|Take::take(&state.dialog, iter)); -//has_editor!(|self: App|{ - //editor = self.editor; - //editor_w = { - //let size = self.size.w(); - //let editor = self.editor.as_ref().expect("missing editor"); - //let time_len = editor.time_len().get(); - //let time_zoom = editor.time_zoom().get().max(1); - //(5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16) - //}; - //editor_h = 15; - //is_editing = self.editor.is_some(); -//}); -#[tengri_proc::command(Option)] impl DialogCommand { - fn open (dialog: &mut Option, new: Dialog) -> Perhaps { - *dialog = Some(new); - Ok(None) - } - fn close (dialog: &mut Option) -> Perhaps { - *dialog = None; - Ok(None) - } -} - -impl App { - pub fn toggle_dialog (&mut self, mut dialog: Dialog) -> Dialog { - std::mem::swap(&mut self.dialog, &mut dialog); - dialog - } - pub fn toggle_editor (&mut self, value: Option) { - //FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed); - let value = value.unwrap_or_else(||!self.editor().is_some()); - if value { - // Create new clip in pool when entering empty cell - if let Selection::TrackClip { track, scene } = *self.selection() - && let Some(scene) = self.project.scenes.get_mut(scene) - && let Some(slot) = scene.clips.get_mut(track) - && slot.is_none() - && let Some(track) = self.project.tracks.get_mut(track) - { - let (index, mut clip) = self.pool.add_new_clip(); - // autocolor: new clip colors from scene and track color - let color = track.color.base.mix(scene.color.base, 0.5); - clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); - if let Some(ref mut editor) = &mut self.project.editor { - editor.set_clip(Some(&clip)); - } - *slot = Some(clip.clone()); - //Some(clip) - } else { - //None - } - } else if let Selection::TrackClip { track, scene } = *self.selection() - && let Some(scene) = self.project.scenes.get_mut(scene) - && let Some(slot) = scene.clips.get_mut(track) - && let Some(clip) = slot.as_mut() - { - // Remove clip from arrangement when exiting empty clip editor - let mut swapped = None; - if clip.read().unwrap().count_midi_messages() == 0 { - std::mem::swap(&mut swapped, slot); - } - if let Some(clip) = swapped { - self.pool.delete_clip(&clip.read().unwrap()); - } - } - } - pub fn browser (&self) -> Option<&Browser> { - if let Dialog::Browser(_, ref b) = self.dialog { Some(b) } else { None } - } - pub fn device_pick (&mut self, index: usize) { - self.dialog = Dialog::Device(index); - } - pub fn add_device (&mut self, index: usize) -> Usually<()> { - match index { - 0 => { - let name = self.jack.with_client(|c|c.name().to_string()); - let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name(); - let track = self.track().expect("no active track"); - let port = format!("{}/Sampler", &track.name); - let connect = Connect::exact(format!("{name}:{midi}")); - let sampler = if let Ok(sampler) = Sampler::new( - &self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]] - ) { - self.dialog = Dialog::None; - Device::Sampler(sampler) - } else { - self.dialog = Dialog::Message("Failed to add device.".into()); - return Err("failed to add device".into()) - }; - let track = self.track_mut().expect("no active track"); - track.devices.push(sampler); - Ok(()) - }, - 1 => { - todo!(); - Ok(()) - }, - _ => unreachable!(), - } - } -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -//#[derive(Clone, Debug)] -//pub enum DialogCommand { - //Open { dialog: Dialog }, - //Close -//} - -//impl Command> for DialogCommand { - //fn execute (self, state: &mut Option) -> Perhaps { - //match self { - //Self::Open { dialog } => { - //*state = Some(dialog); - //}, - //Self::Close => { - //*state = None; - //} - //}; - //Ok(None) - //} -//} - -//dsl!(DialogCommand: |self: Dialog, iter|todo!()); -//Dsl::take(&mut self.dialog, iter)); - -//#[tengri_proc::command(Option)]//Nope. -//impl DialogCommand { - //fn open (dialog: &mut Option, new: Dialog) -> Perhaps { - //*dialog = Some(new); - //Ok(None) - //} - //fn close (dialog: &mut Option) -> Perhaps { - //*dialog = None; - //Ok(None) - //} -//} -// diff --git a/crates/app/app_dsl.rs b/crates/app/app_dsl.rs deleted file mode 100644 index ae27e8d7..00000000 --- a/crates/app/app_dsl.rs +++ /dev/null @@ -1,56 +0,0 @@ -use crate::*; - -/// Namespace. Currently linearly searched. -pub struct DslNs<'t, T: 't>(pub &'t [(&'t str, T)]); - -/// Namespace where keys are symbols. -pub trait DslSymNs<'t, T: 't>: 't { - const SYMS: DslNs<'t, fn (&'t Self)->T>; - fn from_sym (&'t self, dsl: D) -> Usually { - if let Some(dsl) = dsl.sym()? { - for (sym, get) in Self::SYMS.0 { - if dsl == *sym { - return Ok(get(self)) - } - } - } - return Err(format!("not found: sym: {dsl:?}").into()) - } -} - -#[macro_export] macro_rules! dsl_sym ( - (|$state:ident:$State:ty| -> $type:ty {$($lit:literal => $exp:expr),* $(,)?})=>{ - impl<'t> DslSymNs<'t, $type> for $State { - const SYMS: DslNs<'t, fn (&'t $State)->$type> = - DslNs(&[$(($lit, |$state: &$State|$exp)),*]); } }); - -pub trait DslExpNs<'t, T: 't>: 't { - const EXPS: DslNs<'t, fn (&'t Self, &str)->T>; -} -#[macro_export] macro_rules! dsl_exp ( - (|$state:ident:$State:ty|->$type:ty { $( - [$key:literal $(/ $sub:ident: $Sub:ty)? $(, $arg:ident $(?)? :$argtype:ty)*] => $body:expr - ),* $(,)? }) => { - impl<'t> DslExpNs<'t, $type> for $State { - const EXPS: DslNs<'t, fn (&'t $State, &str)->$type> = - DslNs(&[]); } }); - -pub type DslCb = fn (&App) -> Box>; - -impl<'t, D: Dsl> std::ops::Index for DslNs<'t, DslCb> { - type Output = DslCb; - fn index (&self, index: D) -> &Self::Output { - if let Ok(Some(symbol)) = index.src() { - for (key, value) in self.0.iter() { - if symbol == *key { - return value - } - } - } - &(view_nil as DslCb) - } -} - -pub fn view_nil (_: &App) -> Box> { - Box::new(Fill::xy("ยท")) -} diff --git a/crates/app/app_view.rs b/crates/app/app_view.rs deleted file mode 100644 index 8179d583..00000000 --- a/crates/app/app_view.rs +++ /dev/null @@ -1,304 +0,0 @@ -use crate::*; -content!(TuiOut:|self: App|Stack::above(|add|{ - for dsl in self.mode.view.iter() { add(&self.view(dsl.as_ref())); } -})); -impl App { - fn view (&self, index: D) -> Box> { - if let Ok(Some(symbol)) = index.src() { - for (key, value) in Self::SYMS.0.iter() { - if symbol == *key { - return value(self) - } - } - } - view_nil(self) - } -} -dsl_sym!(|app: App| -> Box> { - ":view" => app.view(":view/menu"), - ":view/menu" => { - let selected = app.dialog.menu_selected(); - let outputs = app.view(":view/ports/outs"); - let inputs = app.view(":view/ports/ins"); - Box::new(Tui::bg(Rgb(0,0,0), Bsp::s(outputs, Bsp::s( - Fill::x(Fixed::y(3, Tui::bg(Rgb(33,33,33), Tui::bold(true, "tek 0.3.0-rc0")))), - Bsp::n(inputs, Bsp::n( - Fill::x(Fixed::y(3, Tui::bg(Rgb(33,33,33), Bsp::e(Tui::fg(Rgb(255,192,48), "[Enter]"), " new session")))), - Fill::y(Align::n(Fill::x(app.view(":view/profiles")))))))))) - }, - ":view/ports/outs" => Box::new(Fill::x(Fixed::y(3, - Bsp::a(Fill::x(Align::w(" L AUDIO OUTS")), Bsp::a("MIDI OUT", Fill::x(Align::e("AUDIO OUTS R "))))))), - ":view/ports/ins" => Box::new(Fill::x(Fixed::y(3, - Bsp::a(Fill::x(Align::w(" L AUDIO ISYMS")), Bsp::a("MIDI ISYMS", Fill::x(Align::e("AUDIO ISYMS R "))))))), - ":view/profiles" => Box::new({ - let modes = app.config.modes.clone(); - Stack::south(move|add: &mut dyn FnMut(&dyn Render)|{ - for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { - let bg = if index == 0 { Rgb(64,64,64) } else { Rgb(32,32,32) }; - let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); - let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); - add(&Fixed::y(3, Tui::bg(bg, Bsp::s( - Fill::x(Bsp::a( - Fill::x(Align::w(Tui::fg(Rgb(224,192,128), name))), - Fill::x(Align::e(Tui::fg(Rgb(224,128,32), &id))))), - Fill::x(Align::w(info)))))); } })}), - ":view/browse" => { - let browser = app.dialog.browser().cloned().unwrap(); - Box::new(Bsp::s(Padding::xy(3, 1, app.view(":view/browse-title")), - Outer(true, Style::default().fg(Tui::g(96))) - .enclose(Fill::xy(browser)))) }, - ":view/browse/title" => { - let target = app.dialog.browser_target().unwrap(); - Box::new(Fill::x(Align::w(FieldV(Default::default(), match target { - BrowserTarget::SaveProject => "Save project:", - BrowserTarget::LoadProject => "Load project:", - BrowserTarget::ImportSample(_) => "Import sample:", - BrowserTarget::ExportSample(_) => "Export sample:", - BrowserTarget::ImportClip(_) => "Import clip:", - BrowserTarget::ExportClip(_) => "Export clip:", - }, Shrink::x(3, Fixed::y(1, Tui::fg(Tui::g(96), RepeatH("๐Ÿญป"))))))))}, - ":view/device" => { - let selected = app.dialog.device_kind().unwrap(); - Box::new(Bsp::s(Tui::bold(true, "Add device"), Map::south(1, - move||device_kinds().iter(), - move|label: &&'static str, i|{ - let bg = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; - let lb = if i == selected { "[ " } else { " " }; - let rb = if i == selected { " ]" } else { " " }; - Fill::x(Tui::bg(bg, Bsp::e(lb, Bsp::w(rb, "FIXME device name")))) }))) }, - //(":view/options", view_options), -}); -fn wrap_dialog (dialog: impl Content) -> impl Content { - Fixed::xy(70, 23, Tui::fg_bg(Rgb(255,255,255), Rgb(16,16,16), Bsp::b( - Repeat(" "), Outer(true, Style::default().fg(Tui::g(96))).enclose(dialog)))) -} -impl ScenesView for App { - fn h_scenes (&self) -> u16 { - (self.height() as u16).saturating_sub(20) - } - fn w_side (&self) -> u16 { - 20 - } - fn w_mid (&self) -> u16 { - (self.width() as u16).saturating_sub(self.w_side()) - } -} - - //Bsp::s("", - //Map::south(1, - //move||app.config.binds.layers.iter() - //.filter_map(|a|(a.0)(app).then_some(a.1)) - //.flat_map(|a|a) - //.filter_map(|x|if let Value::Exp(_, iter)=x.value{ Some(iter) } else { None }) - //.skip(offset) - //.take(20), - //|mut b,i|Fixed::x(60, Align::w(Bsp::e("(", Bsp::e( - //b.next().map(|t|Fixed::x(16, Align::w(Tui::fg(Rgb(64,224,0), format!("{}", t.value))))), - //Bsp::e(" ", Align::w(format!("{}", b.0.0.trim()))))))))))), - - //Dialog::Browser(BrowserTarget::Load, browser) => { - //"bobcat".boxed() - ////Bsp::s( - ////Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( - ////Tui::bold(true, " Load project: "), - ////Shrink::x(3, Fixed::y(1, RepeatH("๐Ÿญป"))))))), - ////Outer(true, Style::default().fg(Tui::g(96))) - ////.enclose(Fill::xy(browser))) - //}, - //Dialog::Browser(BrowserTarget::Export, browser) => { - //"bobcat".boxed() - ////Bsp::s( - ////Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( - ////Tui::bold(true, " Export: "), - ////Shrink::x(3, Fixed::y(1, RepeatH("๐Ÿญป"))))))), - ////Outer(true, Style::default().fg(Tui::g(96))) - ////.enclose(Fill::xy(browser))) - //}, - //Dialog::Browser(BrowserTarget::Import, browser) => { - //"bobcat".boxed() - ////Bsp::s( - ////Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( - ////Tui::bold(true, " Import: "), - ////Shrink::x(3, Fixed::y(1, RepeatH("๐Ÿญป"))))))), - ////Outer(true, Style::default().fg(Tui::g(96))) - ////.enclose(Fill::xy(browser))) - //}, -// - //pub fn view_meters_input (&self) -> impl Content + use<'_> { - //self.project.sampler().map(|s| - //s.view_meters_input()) - //} - //pub fn view_meters_output (&self) -> impl Content + use<'_> { - //self.project.sampler().map(|s| - //s.view_meters_output()) - //} - //pub fn view_history (&self) -> impl Content { - //Fixed::y(1, Fill::x(Align::w(FieldH(self.color, - //format!("History ({})", self.history.len()), - //self.history.last().map(|last|Fill::x(Align::w(format!("{:?}", last.0)))))))) - //} - //pub fn view_status_h2 (&self) -> impl Content { - //self.update_clock(); - //let theme = self.color; - //let clock = self.clock(); - //let playing = clock.is_rolling(); - //let cache = clock.view_cache.clone(); - ////let selection = self.selection().describe(self.tracks(), self.scenes()); - //let hist_len = self.history.len(); - //let hist_last = self.history.last(); - //Fixed::y(2, Stack::east(move|add: &mut dyn FnMut(&dyn Render)|{ - //add(&Fixed::x(5, Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, - //Either::new(false, // TODO - //Thunk::new(move||Fixed::x(9, Either::new(playing, - //Tui::fg(Rgb(0, 255, 0), " PLAYING "), - //Tui::fg(Rgb(255, 128, 0), " STOPPED "))) - //), - //Thunk::new(move||Fixed::x(5, Either::new(playing, - //Tui::fg(Rgb(0, 255, 0), Bsp::s(" ๐Ÿญ๐Ÿญ‘๐Ÿฌฝ ", " ๐Ÿญž๐Ÿญœ๐Ÿญ˜ ",)), - //Tui::fg(Rgb(255, 128, 0), Bsp::s(" โ–—โ–„โ–– ", " โ–โ–€โ–˜ ",)))) - //) - //) - //))); - //add(&" "); - //{ - //let cache = cache.read().unwrap(); - //add(&Fixed::x(15, Align::w(Bsp::s( - //FieldH(theme, "Beat", cache.beat.view.clone()), - //FieldH(theme, "Time", cache.time.view.clone()), - //)))); - //add(&Fixed::x(13, Align::w(Bsp::s( - //Fill::x(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), - //Fill::x(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), - //)))); - //add(&Fixed::x(12, Align::w(Bsp::s( - //Fill::x(Align::w(FieldH(theme, "Buf", cache.buf.view.clone()))), - //Fill::x(Align::w(FieldH(theme, "Lat", cache.lat.view.clone()))), - //)))); - ////add(&Bsp::s( - //////Fill::x(Align::w(FieldH(theme, "Selected", Align::w(selection)))), - ////Fill::x(Align::w(FieldH(theme, format!("History ({})", hist_len), - ////hist_last.map(|last|Fill::x(Align::w(format!("{:?}", last.0))))))), - ////"" - ////)); - //////if let Some(last) = self.history.last() { - //////add(&FieldV(theme, format!("History ({})", self.history.len()), - //////Fill::x(Align::w(format!("{:?}", last.0))))); - //////} - //} - //})) - //} - //pub fn view_status_v (&self) -> impl Content + use<'_> { - //self.update_clock(); - //let cache = self.project.clock.view_cache.read().unwrap(); - //let theme = self.color; - //let playing = self.clock().is_rolling(); - //Tui::bg(theme.darker.rgb, Fixed::xy(20, 5, Outer(true, Style::default().fg(Tui::g(96))).enclose( - //col!( - //Fill::x(Align::w(Bsp::e( - //Align::w(Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, - //Either::new(false, // TODO - //Thunk::new(move||Fixed::x(9, Either::new(playing, - //Tui::fg(Rgb(0, 255, 0), " PLAYING "), - //Tui::fg(Rgb(255, 128, 0), " STOPPED "))) - //), - //Thunk::new(move||Fixed::x(5, Either::new(playing, - //Tui::fg(Rgb(0, 255, 0), Bsp::s(" ๐Ÿญ๐Ÿญ‘๐Ÿฌฝ ", " ๐Ÿญž๐Ÿญœ๐Ÿญ˜ ",)), - //Tui::fg(Rgb(255, 128, 0), Bsp::s(" โ–—โ–„โ–– ", " โ–โ–€โ–˜ ",)))) - //) - //) - //)), - //Bsp::s( - //FieldH(theme, "Beat", cache.beat.view.clone()), - //FieldH(theme, "Time", cache.time.view.clone()), - //), - //))), - //Fill::x(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), - //Fill::x(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), - //Fill::x(Align::w(FieldH(theme, "Buf", Bsp::e(cache.buf.view.clone(), Bsp::e(" = ", cache.lat.view.clone()))))), - //)))) - //} - //pub fn view_status (&self) -> impl Content + use<'_> { - //self.update_clock(); - //let cache = self.project.clock.view_cache.read().unwrap(); - //view_status(Some(self.project.selection.describe(self.tracks(), self.scenes())), - //cache.sr.view.clone(), cache.buf.view.clone(), cache.lat.view.clone()) - //} - //pub fn view_transport (&self) -> impl Content + use<'_> { - //self.update_clock(); - //let cache = self.project.clock.view_cache.read().unwrap(); - //view_transport(self.project.clock.is_rolling(), - //cache.bpm.view.clone(), cache.beat.view.clone(), cache.time.view.clone()) - //} - //pub fn view_editor (&self) -> impl Content + use<'_> { - //let bg = self.editor() - //.and_then(|editor|editor.clip().clone()) - //.map(|clip|clip.read().unwrap().color.darker) - //.unwrap_or(self.color.darker); - //Fill::xy(Tui::bg(bg.rgb, self.editor())) - //} - //pub fn view_editor_status (&self) -> impl Content + use<'_> { - //self.editor().map(|e|Fixed::x(20, Outer(true, Style::default().fg(Tui::g(96))).enclose( - //Fill::y(Align::n(Bsp::s(e.clip_status(), e.edit_status())))))) - //} - //pub fn view_midi_ins_status (&self) -> impl Content + use<'_> { - //self.project.view_midi_ins_status(self.color) - //} - //pub fn view_midi_outs_status (&self) -> impl Content + use<'_> { - //self.project.view_midi_outs_status(self.color) - //} - //pub fn view_audio_ins_status (&self) -> impl Content + use<'_> { - //self.project.view_audio_ins_status(self.color) - //} - //pub fn view_audio_outs_status (&self) -> impl Content + use<'_> { - //self.project.view_audio_outs_status(self.color) - //} - //pub fn view_scenes (&self) -> impl Content + use<'_> { - //Bsp::e( - //Fixed::x(20, Align::nw(self.project.view_scenes_names())), - //self.project.view_scenes_clips(), - //) - //} - //pub fn view_scenes_names (&self) -> impl Content + use<'_> { - //self.project.view_scenes_names() - //} - //pub fn view_scenes_clips (&self) -> impl Content + use<'_> { - //self.project.view_scenes_clips() - //} - //pub fn view_tracks_inputs <'a> (&'a self) -> impl Content + use<'a> { - //Fixed::y(1 + self.project.midi_ins.len() as u16, - //self.project.view_inputs(self.color)) - //} - //pub fn view_tracks_outputs <'a> (&'a self) -> impl Content + use<'a> { - //self.project.view_outputs(self.color) - //} - //pub fn view_tracks_devices <'a> (&'a self) -> impl Content + use<'a> { - //Fixed::y(4, self.project.view_track_devices(self.color)) - //} - //pub fn view_tracks_names <'a> (&'a self) -> impl Content + use<'a> { - //Fixed::y(2, self.project.view_track_names(self.color)) - //} - //pub fn view_pool (&self) -> impl Content + use<'_> { - //Fixed::x(20, Bsp::s( - //Fill::x(Align::w(FieldH(self.color, "Clip pool:", ""))), - //Fill::y(Align::n(Tui::bg(Rgb(0, 0, 0), Outer(true, Style::default().fg(Tui::g(96))) - //.enclose(PoolView(&self.pool))))))) - //} - //pub fn view_samples_keys (&self) -> impl Content + use<'_> { - //self.project.sampler().map(|s|s.view_list(true, self.editor().unwrap())) - //} - //pub fn view_samples_grid (&self) -> impl Content + use<'_> { - //self.project.sampler().map(|s|s.view_grid()) - //} - //pub fn view_sample_viewer (&self) -> impl Content + use<'_> { - //self.project.sampler().map(|s|s.view_sample(self.editor().unwrap().get_note_pos())) - //} - //pub fn view_sample_info (&self) -> impl Content + use<'_> { - //self.project.sampler().map(|s|s.view_sample_info(self.editor().unwrap().get_note_pos())) - //} - //pub fn view_sample_status (&self) -> impl Content + use<'_> { - //self.project.sampler().map(|s|Outer(true, Style::default().fg(Tui::g(96))).enclose( - //Fill::y(Align::n(s.view_sample_status(self.editor().unwrap().get_note_pos()))))) - //} - ////let options = ||["Projects", "Settings", "Help", "Quit"].iter(); - ////let option = |a,i|Tui::fg(Rgb(255,255,255), format!("{}", a)); - ////Bsp::s(Tui::bold(true, "tek!"), Bsp::s("", Map::south(1, options, option))) diff --git a/crates/cli/tek.rs b/crates/cli/tek.rs index 1b8638ca..8ffcfcb1 100644 --- a/crates/cli/tek.rs +++ b/crates/cli/tek.rs @@ -75,11 +75,15 @@ impl Cli { }; let clock = Clock::new(&jack, self.bpm)?; let mut app = App { - jack: jack.clone(), config, + jack: jack.clone(), color: ItemTheme::random(), dialog: Dialog::Menu(0), - mode: Mode { view: vec![":view/menu".into()], ..Default::default() }, + mode: Mode { + view: vec![":view/menu".into()], + keys: vec![":keys/axis/y".into()], + ..Default::default() + }, project: Arrangement { name: Default::default(), color: ItemTheme::random(), diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index da077e32..4bbec40d 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -93,7 +93,7 @@ pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, AtomicBool, Orderi pub(crate) use std::fmt::Debug; pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem}; -pub(crate) use ::tengri::{from, Usually, Perhaps, Has, has, maybe_has, tui::*, dsl::*}; +pub(crate) use ::tengri::{from, Usually, tui::*, dsl::*}; pub use ::atomic_float; pub(crate) use atomic_float::*; diff --git a/deps/tengri b/deps/tengri index ab1afa21..a1190a24 160000 --- a/deps/tengri +++ b/deps/tengri @@ -1 +1 @@ -Subproject commit ab1afa219f520138ff1a089de4223e52298b1d0e +Subproject commit a1190a24a1bdea774cedf86fff7b7e407c0a4cc7 diff --git a/tek.edn b/tek.edn index 5cdd6c8d..42897107 100644 --- a/tek.edn +++ b/tek.edn @@ -1,17 +1,19 @@ -(keys :help (@f1 dialog :help)) +(keys :help (@f1 dialog :help)) (keys :back (@escape back)) -(keys :axis/x (@left x/prev) (@right x/next)) +(keys :confirm (@enter confirm)) +(keys :page (@pgup page/up) (@pgdn page/down)) +(keys :axis/x (@left x/prev) (@right x/next)) (keys :axis/x2 (@shift/left x2/prev) (@shift/right x2/next)) -(keys :axis/y (@up y/prev) (@down y/next)) -(keys :axis/y2 (@shift/up y2/prev) (@shift/down y2/next)) -(keys :axis/z (@comma z/prev) (@period z/next)) -(keys :axis/z2 (@lt z2/prev) (@gt z2/next)) +(keys :axis/y (@up y/prev) (@down y/next)) +(keys :axis/y2 (@shift/up y2/prev) (@shift/down y2/next)) +(keys :axis/z (@minus z/prev) (@equal z/next)) +(keys :axis/z2 (@underscore z2/prev) (@plus z2/next)) +(keys :axis/i (@comma i/prev) (@period z/next)) +(keys :axis/i2 (@lt i2/prev) (@gt z2/next)) (keys :axis/w (@openbracket w/prev) (@closebracket w/next)) -(keys :axis/w2 (@openbrace w2/prev) (@closebrace w2/next)) -(keys :axis/page (@pgup page/up) (@pgdn page/down)) -(keys :confirm (@enter confirm)) -(keys :delete (@delete delete)) -(keys :backspace (@backspace backspace)) +(keys :axis/w2 (@openbrace w2/prev) (@closebrace w2/next)) +(keys :delete (@delete delete)) +(keys :backspace (@backspace delete/back)) (keys :input (see :delete :axis/x :backspace) (:char input)) (keys :length (see :confirm :axis/y :axis/x)) (keys :list (see :confirm :axis/y)) @@ -24,115 +26,121 @@ (keys :saveload (@f6 dialog :save) (@f9 dialog :load)) (keys :global (see :history :saveload) (@f8 dialog :options) (@f10 dialog :quit)) -(mode :transport (name Transport) (info A JACK transport controller.) - (keys :clock :global) :view/transport) +(mode :transport + (name Transport) + (info A JACK transport controller.) + (keys :clock :global) + :transport) + +(mode :arranger + (name Arranger) + (info A grid of launchable clips arranged by track and scene.) + (when :mode/editor (keys :editor)) + (when :mode/dialog (keys :dialog)) + (when :mode/message (keys :message)) + (when :mode/add-device (keys :add-device)) + (when :mode/browse (keys :browse)) + (when :mode/rename (keys :pool/rename)) + (when :mode/length (keys :pool/length)) + (when :mode/clip (keys :clip)) + (when :mode/track (keys :track)) + (when :mode/scene (keys :scene)) + (when :mode/mix (keys :mix)) + (keys :clock :arranger :global) + (bsp/w :meters/output (bsp/e :meters/input (stack/n + (fixed/y 2 :status/h2) :tracks/inputs (stack/s + :tracks/devices :tracks/outputs :tracks/names + (fill/xy (either :mode/editor (bsp/e :scenes/names :editor) :scenes))))))) -(mode :arranger (name Arranger) (info A grid of launchable clips arranged by track and scene.) - (when :mode/editor (keys :editor)) - (when :mode/dialog (keys :dialog)) - (when :mode/message (keys :message)) - (when :mode/add-device (keys :add-device)) - (when :mode/browse (keys :browse)) - (when :mode/rename (keys :pool/rename)) - (when :mode/length (keys :pool/length)) - (when :mode/clip (keys :clip)) - (when :mode/track (keys :track)) - (when :mode/scene (keys :scene)) - (when :mode/mix (keys :mix)) - (keys :clock :arranger :global) - (bsp/w :view/meters/output - (bsp/e :view/meters/input - (stack/n - (fixed/y 2 :view/status/h2) - :view/tracks/inputs - (stack/s - :view/tracks/devices - :view/tracks/outputs - :view/tracks/names - (fill/xy (either :mode/editor - (bsp/e :view/scenes/names :view/editor) - :view/scenes))))))) (keys :arranger (see :color :launch :scenes :tracks) - (@tab project/edit) (@enter project/edit) - (@shift/I project/input/add) (@shift/O project/output/add) - (@shift/S project/scene/add) (@shift/T project/track/add) - (@shift/D dialog/show :dialog/device)) + (@tab project/edit) (@enter project/edit) + (@shift/I project/input/add) (@shift/O project/output/add) + (@shift/S project/scene/add) (@shift/T project/track/add) + (@shift/D dialog/show :dialog/device)) + (keys :tracks - (@t select :select/track) (@left select :select/track/prev) (@right select :select/track/next)) + (@t select :select/track) + (@left select :select/track/prev) + (@right select :select/track/next)) + (keys :track (see :color :launch :axis/z :axis/z2 :delete) - (@r toggle :rec) (@m toggle :mon) (@p toggle :play) (@P toggle :solo)) + (@r toggle :rec) + (@m toggle :mon) + (@p toggle :play) + (@P toggle :solo)) + (keys :scenes - (@s select :select/scene) (@up select :select/scene/prev) (@down select :select/scene/next)) -(keys :scene - (see :color :launch :axis/z :axis/z2 :delete)) + (@s select :select/scene) + (@up select :select/scene/prev) + (@down select :select/scene/next)) + +(keys :scene (see :color :launch :axis/z :axis/z2 :delete)) + (keys :clip (see :color :launch :axis/z :axis/z2 :delete) - (@l toggle :loop)) + (@l toggle :loop)) -(mode :groovebox (name Groovebox) (info A sequencer with built-in sampler.) - (when :mode/browse (keys :browse)) - (when :mode/rename (keys :pool-rename)) - (when :mode/length (keys :pool-length)) - (keys :clock :editor :sampler :global) - (bsp/w :view/meters/output - (bsp/e :view/meters/input - (bsp/w - (fill/y (align/n (stack/s :view/midi-ins/status - :view/midi-outs/status - :view/audio-ins/status - :view/audio-outs/status - :view/pool))) - (bsp/n (fixed/y :h-sample-detail (bsp/e (fill/y (fixed/x 20 (align/nw :view/sample-status))) - :view/sample-viewer)) - (bsp/e (fill/y (align/n (bsp/s :view/status/v :view/editor-status))) - (bsp/e :view/samples/keys - :view/editor))))))) +(mode :groovebox + (name Groovebox) + (info A sequencer with built-in sampler.) + (when :mode/browse (keys :browse)) + (when :mode/rename (keys :pool-rename)) + (when :mode/length (keys :pool-length)) + (keys :clock :editor :sampler :global) + (bsp/w :meters/output (bsp/e :meters/input (bsp/w + (fill/y (align/n + (stack/s :midi-ins/status :midi-outs/status :audio-ins/status :audio-outs/status :pool))) + (bsp/n + (fixed/y :h-sample-detail (bsp/e + (fill/y (fixed/x 20 (align/nw :sample-status))) + :sample-viewer)) + (bsp/e (fill/y (align/n (bsp/s :status/v :editor-status))) + (bsp/e :samples/keys :editor))))))) -(mode :sampler (name Sampler) (info A sampling soundboard.) - (keys :sampler :global) - (bsp/s (fixed/y 1 :view/transport) - (bsp/n (fixed/y 1 :view/status) - (fill/xy :view/samples/grid)))) -(keys :sampler (see :sampler/directions :sampler/record :sampler/play)) -(keys :sampler/play (@p sampler/play/sample :sample/selected) (@P sampler/stop/sample :sample/selected)) -(keys :sampler/record (@r sampler/record/toggle :sample/selected) (@shift/R sampler/record/back)) -(keys :sampler/import-export (@shift/f6 dialog :dialog/export/sample) (@shift/f9 dialog :dialog/import/sample)) -(keys :sampler/directions (@up sampler/select :sample/above) (@down sampler/select :sample/below) (@left sampler/select :sample/to/left) (@right sampler/select :sample/to/right)) +(mode :sampler + (name Sampler) + (info A sampling soundboard.) + (keys :sampler :global) + (bsp/s (fixed/y 1 :transport) (bsp/n (fixed/y 1 :status) (fill/xy :samples/grid)))) -(mode :sequencer (name Sequencer) (info A MIDI sequencer.) - (when :mode/browse (keys :browse)) - (when :mode/rename (keys :pool-rename)) - (when :mode/length (keys :pool-length)) - (keys :editor :clock :global) - (bsp/s (fixed/y 1 :view/transport) - (bsp/n (fixed/y 1 :view/status) - (fill/xy (bsp/a (fill/xy (align/e :view/pool)) - :view/editor))))) -(keys :sequencer (see :color :launch) (@shift/I input/add) (@shift/O output/add)) +(keys :sampler + (see :sampler/directions :sampler/record :sampler/play)) +(keys :sampler/play + (@p sampler/play/sample :sample/selected) + (@P sampler/stop/sample :sample/selected)) +(keys :sampler/record + (@r sampler/record/toggle :sample/selected) + (@shift/R sampler/record/back)) +(keys :sampler/import-export + (@shift/f6 dialog :dialog/export/sample) + (@shift/f9 dialog :dialog/import/sample)) +(keys :sampler/directions + (@up sampler/select :sample/above) + (@down sampler/select :sample/below) + (@left sampler/select :sample/to/left) + (@right sampler/select :sample/to/right)) + +(mode :sequencer + (name Sequencer) + (info A MIDI sequencer.) + (when :mode/browse (keys :browse)) + (when :mode/rename (keys :pool/rename)) + (when :mode/length (keys :pool/length)) + (keys :editor :clock :global) + (bsp/s (fixed/y 1 :transport) + (bsp/n (fixed/y 1 :status) + (fill/xy (bsp/a (fill/xy (align/e :pool)) + :editor))))) + +(keys :sequencer (see :color :launch) + (@shift/I input/add) + (@shift/O output/add)) (keys :pool (see :axis-y :axis-w :axis/z2 :color :delete) - (@n rename/begin) (@t length/begin) (@m import/begin) (@x export/begin) - (@shift/A clip/add :after :new/clip) - (@shift/D clip/add :after :cloned/clip)) + (@n rename/begin) (@t length/begin) (@m import/begin) (@x export/begin) + (@shift/A clip/add :after :new/clip) + (@shift/D clip/add :after :cloned/clip)) (keys :editor (see :editor/view :editor/note)) -(keys :editor/view - (@left editor/time/set :time/pos/prev) - (@shift/left editor/time/set :time/pos/prev/fine) - (@right editor/time/set :time/pos/next) - (@shift/right editor/time/set :time/pos/next/fine) - (@equal editor/zoom/set :time/zoom/prev) - (@minus editor/zoom/set :time/zoom/next) - (@plus editor/zoom/set :time/zoom/next/fine) - (@underscore editor/zoom/set :time/zoom/prev/fine) - (@z editor/lock/set)) -(keys :editor/note - (@comma editor/length/set :note/len/prev) - (@period editor/length/set :note/len/next) - (@lt editor/length/set :note/len/prev) - (@gt editor/length/set :note/len/next) - (@up editor/pitch/set :note/pos/next) - (@down editor/pitch/set :note/pos/prev) - (@pgup editor/pitch/set :note/pos/next/octave) - (@pgdn editor/pitch/set :note/pos/prev/octave) - (@a editor/append :true) - (@enter editor/append :false) - (@del editor/delete/note) - (@shift/del editor/delete/note)) +(keys :editor/view (see :axis/x :axis/x2 :axis/z :axis/z2) + (@z toggle :lock)) +(keys :editor/note (see :axis/i :axis/i2 :axis/y :axis/page) + (@a editor/append :true) (@enter editor/append :false) + (@del editor/delete/note) (@shift/del editor/delete/note))