diff --git a/Cargo.lock b/Cargo.lock index 051935e5..b1a63742 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2381,56 +2381,27 @@ dependencies = [ [[package]] name = "tek" -version = "0.2.1" +version = "0.3.0" dependencies = [ + "atomic_float", "backtrace", "clap", + "jack", "konst", + "livi", + "midly", "palette", "proptest", "proptest-derive", "rand 0.8.5", - "tek_config", - "tek_device", - "tek_engine", + "symphonia", "tengri", "tengri_proc", "toml", - "xdg", -] - -[[package]] -name = "tek_config" -version = "0.2.1" -dependencies = [ - "tengri", - "xdg", -] - -[[package]] -name = "tek_device" -version = "0.2.1" -dependencies = [ - "livi", - "symphonia", - "tek_engine", - "tengri", - "tengri_proc", "uuid", "wavers", "winit", -] - -[[package]] -name = "tek_engine" -version = "0.2.1" -dependencies = [ - "atomic_float", - "jack", - "midly", - "tengri", - "tengri_proc", - "uuid", + "xdg", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3bb50a68..396c69f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,11 @@ -[workspace.package] -edition = "2024" -version = "0.2.1" - [workspace] resolver = "2" -members = [ - "./crates/engine", - "./crates/device", - "./crates/config", - "./crates/app", -] -exclude = [ - "./deps/tengri" -] +members = [ "./tek" ] +exclude = [ "./deps/tengri" ] + +[workspace.package] +edition = "2024" +version = "0.3.0" [profile.release] lto = true @@ -32,14 +25,11 @@ path = "./deps/tengri/proc" path = "./deps/rust-jack" [workspace.dependencies] -tek_device = { path = "./crates/device" } -tek_engine = { path = "./crates/engine" } -tek_config = { path = "./crates/config" } -tek = { path = "./crates/app" } -tek_cli = { path = "./crates/cli" } +tek = { path = "./tek" } -atomic_float = { version = "1.0.0" } +atomic_float = { version = "1.0.0" } backtrace = { version = "0.3.72" } +bumpalo = { version = "3.19.0" } clap = { version = "4.5.4", features = [ "derive" ] } gtk = { version = "0.18.1" } konst = { version = "0.3.16", features = [ "rust_1_83" ] } diff --git a/Justfile b/Justfile index 86e1bf36..b783e089 100644 --- a/Justfile +++ b/Justfile @@ -1,4 +1,4 @@ -export RUSTFLAGS := "--cfg procmacro2_semver_exempt -Zmacro-backtrace" +export RUSTFLAGS := "--cfg procmacro2_semver_exempt -Zmacro-backtrace -Clink-arg=-fuse-ld=mold" export RUST_BACKTRACE := "1" debug := "reset && cargo run --" @@ -18,6 +18,18 @@ default: just -l bacon: bacon -s +check: + reset && cargo check +build: + reset && cargo build +run: + {{debug}} +run-init: + rm -rf ~/.config/tek && {{debug}} +release: + {{release}} +build-release: + time cargo build -j4 --release tui: cargo run --example tui cloc: @@ -25,7 +37,7 @@ cloc: test: cargo test --workspace --exclude jack prof: - CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph -- arranger + CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph -- doc: cargo doc -j4 --workspace --document-private-items cov: @@ -50,15 +62,6 @@ fpush: ftpush: git push --tags -fu codeberg && git push --tags -fu origin -run: - {{debug}} -run-init: - rm -rf ~/.config/tek && {{debug}} -release: - {{release}} -build-release: - time cargo build -j4 --release - clock: {{debug}} {{name}} {{bpm}} clock clock-release: diff --git a/bacon.toml b/bacon.toml index e2eeae40..53b4f1ff 100644 --- a/bacon.toml +++ b/bacon.toml @@ -3,24 +3,30 @@ default_job = "check" env.CARGO_TERM_COLOR = "always" [keybindings] c = "job:check" +t = "job:test" +n = "job:nextest" l = "job:clippy" [jobs] [jobs.check] command = ["cargo", "check"] need_stdout = false -watch = ["crates", "deps"] +watch = ["tek", "deps"] [jobs.check-all] command = ["cargo", "check", "--all-targets"] need_stdout = false +watch = ["tek", "deps"] [jobs.clippy] command = ["cargo", "clippy"] need_stdout = false +watch = ["tek", "deps"] [jobs.clippy-all] command = ["cargo", "clippy", "--all-targets"] need_stdout = false +watch = ["tek", "deps"] [jobs.test] command = ["cargo", "test"] need_stdout = true +watch = ["tek", "deps"] [jobs.nextest] command = [ "cargo", "nextest", "run", diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml deleted file mode 100644 index 6f5a5768..00000000 --- a/crates/app/Cargo.toml +++ /dev/null @@ -1,36 +0,0 @@ -[package] -name = "tek" -edition = { workspace = true } -version = { workspace = true } - -[dependencies] -tengri = { workspace = true } -tengri_proc = { workspace = true } - -tek_config = { workspace = true } -tek_device = { workspace = true } -tek_engine = { workspace = true } - -backtrace = { workspace = true } -clap = { workspace = true, optional = true } -konst = { workspace = true } -palette = { workspace = true } -rand = { workspace = true } -toml = { workspace = true } -xdg = { workspace = true } - -[dev-dependencies] -proptest = { workspace = true } -proptest-derive = { workspace = true } - -[features] -default = ["cli"] -cli = ["clap"] -host = ["tek_device/lv2"] - -[lib] -path = "tek.rs" - -[[bin]] -name = "tek" -path = "tek_cli.rs" diff --git a/crates/app/tek.rs b/crates/app/tek.rs deleted file mode 100644 index 12f52769..00000000 --- a/crates/app/tek.rs +++ /dev/null @@ -1,180 +0,0 @@ -#![allow(unused, clippy::unit_arg)] -#![feature(adt_const_params, associated_type_defaults, if_let_guard, impl_trait_in_assoc_type, - type_alias_impl_trait, trait_alias, type_changing_struct_update, closure_lifetime_binder)] -#[cfg(test)] mod tek_test; -mod tek_bind; pub use self::tek_bind::*; -mod tek_data; pub use self::tek_data::*; -mod tek_deps; pub use self::tek_deps::*; -mod tek_jack; pub use self::tek_jack::*; -mod tek_menu; pub use self::tek_menu::*; -mod tek_view; pub use self::tek_view::*; -/// Total state -#[derive(Default, Debug)] -pub struct App { - /// Base color. - pub color: ItemTheme, - /// Must not be dropped for the duration of the process - pub jack: Jack<'static>, - /// Display size - pub size: Measure, - /// Performance counter - pub perf: PerfModel, - /// Available view modes and input bindings - pub config: Config, - /// Currently selected mode - pub mode: Arc>>, - /// Undo history - pub history: Vec<(AppCommand, Option)>, - /// Dialog overlay - pub dialog: Dialog, - /// Contains all recently created clips. - pub pool: Pool, - /// Contains the currently edited musical arrangement - pub project: Arrangement, -} -has!(Jack<'static>: |self: App|self.jack); -has!(Pool: |self: App|self.pool); -has!(Dialog: |self: App|self.dialog); -has!(Clock: |self: App|self.project.clock); -has!(Option: |self: App|self.project.editor); -has!(Selection: |self: App|self.project.selection); -has!(Vec: |self: App|self.project.midi_ins); -has!(Vec: |self: App|self.project.midi_outs); -has!(Vec: |self: App|self.project.scenes); -has!(Vec: |self: App|self.project.tracks); -has!(Measure: |self: App|self.size); -has_clips!( |self: App|self.pool.clips); -maybe_has!(Track: |self: App| { MaybeHas::::get(&self.project) }; - { MaybeHas::::get_mut(&mut self.project) }); -maybe_has!(Scene: |self: App| { MaybeHas::::get(&self.project) }; - { MaybeHas::::get_mut(&mut 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() } } -impl App { - pub fn editor_focused (&self) -> bool { - false - } - 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(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<&Browse> { - if let Dialog::Browse(_, 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!(), - } - } -} - -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 w_side (&self) -> u16 { 20 } - fn w_mid (&self) -> u16 { (self.width() as u16).saturating_sub(self.w_side()) } - fn h_scenes (&self) -> u16 { (self.height() as u16).saturating_sub(20) } -} - - -//#[dizzle::ns] -//#[dizzle::ns::include(TuiOut)] -//trait AppApi { - //#[dizzle::ns::word("sessions")] - //fn view_sessions (&self) -> Box> { - //Min::x(30, Fixed::y(6, Stack::south( - //move|add: &mut dyn FnMut(&dyn Render)|{ - //let fg = Rgb(224, 192, 128); - //for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { - //let bg = if index == 0 { Rgb(48,64,32) } else { Rgb(16, 32, 24) }; - //add(&Fixed::y(2, Fill::x(Tui::bg(bg, Align::w(Tui::fg(fg, name)))))); - //} - //} - //))).into() - //} - //#[dizzle::ns::expr("bold")] - //fn view_bold (&self, value: bool, x: Box>) -> Box> { - //Box::new(Tui::bold(value, x)) - //} -//} - -/////////////////////////////////////////////////////////////////////////////////////////////////// -//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(); -//}); diff --git a/crates/app/tek_data.rs b/crates/app/tek_data.rs deleted file mode 100644 index 2b27fbb8..00000000 --- a/crates/app/tek_data.rs +++ /dev/null @@ -1,122 +0,0 @@ -use crate::*; - -impl<'t> DslNs<'t, Arc> for App { - dsl_exprs!(|app| -> Arc {}); - dsl_words!(|app| -> Arc {}); - fn from_literal (&self, dsl: &D) -> Perhaps> { - Ok(dsl.src()?.map(|x|x.into())) - } -} - -impl<'t> DslNs<'t, bool> for App { - dsl_words!(|app| -> bool { - ":mode/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/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(..))), - ":focused/clip" => !app.editor_focused() && matches!(app.selection(), - Selection::TrackClip{..}), - ":focused/track" => !app.editor_focused() && matches!(app.selection(), - Selection::Track(..)), - ":focused/scene" => !app.editor_focused() && matches!(app.selection(), - Selection::Scene(..)), - ":focused/mix" => !app.editor_focused() && matches!(app.selection(), - Selection::Mix), - }); - //fn from_literal (&self, dsl: &D) -> Perhaps> { - //TODO - //} -} - -impl<'t> DslNs<'t, ItemTheme> for App {} - -impl<'t> DslNs<'t, Dialog> for App { - dsl_words!(|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/save" => Dialog::Browse(BrowseTarget::SaveProject, - Browse::new(None).unwrap().into()), - ":dialog/load" => Dialog::Browse(BrowseTarget::LoadProject, - Browse::new(None).unwrap().into()), - ":dialog/import/clip" => Dialog::Browse(BrowseTarget::ImportClip(Default::default()), - Browse::new(None).unwrap().into()), - ":dialog/export/clip" => Dialog::Browse(BrowseTarget::ExportClip(Default::default()), - Browse::new(None).unwrap().into()), - ":dialog/import/sample" => Dialog::Browse(BrowseTarget::ImportSample(Default::default()), - Browse::new(None).unwrap().into()), - ":dialog/export/sample" => Dialog::Browse(BrowseTarget::ExportSample(Default::default()), - Browse::new(None).unwrap().into()), - }); -} - -impl<'t> DslNs<'t, Selection> for App { - dsl_words!(|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(), - }); -} - -impl<'t> DslNs<'t, Color> for App { - dsl_words!(|app| -> Color { - ":color/bg" => Color::Rgb(28, 32, 36), - }); - dsl_exprs!(|app| -> Color { - "g" (n: u8) => Color::Rgb(n, n, n), - "rgb" (r: u8, g: u8, b: u8) => Color::Rgb(r, g, b), - }); -} - -impl<'t> DslNs<'t, Option> for App { - dsl_words!(|app| -> Option { - ":editor/pitch" => Some( - (app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into() - ) - }); -} - -impl<'t> DslNs<'t, Option> for App { - dsl_words!(|app| -> Option { - ":selected/scene" => app.selection().scene(), - ":selected/track" => app.selection().track(), - }); -} - -impl<'t> DslNs<'t, Option>>> for App { - dsl_words!(|app| -> Option>> { - ":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() { - app.scenes()[*scene].clips[*track].clone() - } else { - None - } - }); -} - -dsl_ns! { num |app: App| - u8; - u16 => { - ":w/sidebar" => app.project.w_sidebar(app.editor().is_some()), - ":h/sample-detail" => 6.max(app.height() as u16 * 3 / 9), - }; - 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), - }; - isize; -} diff --git a/crates/app/tek_jack.rs b/crates/app/tek_jack.rs deleted file mode 100644 index 57f30f5d..00000000 --- a/crates/app/tek_jack.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::*; - -impl HasJack<'static> for App { - fn jack (&self) -> &Jack<'static> { - &self.jack - } -} - -audio!( - |self: App, client, scope|{ - let t0 = self.perf.get_t0(); - self.clock().update_from_scope(scope).unwrap(); - let midi_in = self.project.midi_input_collect(scope); - if let Some(editor) = &self.editor() { - let mut pitch: Option = None; - for port in midi_in.iter() { - for event in port.iter() { - if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..})) - = event - { - pitch = Some(key.clone()); - } - } - } - if let Some(pitch) = pitch { - editor.set_note_pos(pitch.as_int() as usize); - } - } - let result = self.project.process_tracks(client, scope); - self.perf.update_from_jack_scope(t0, scope); - result - }; - |self, event|{ - use JackEvent::*; - match event { - SampleRate(sr) => { - self.clock().timebase.sr.set(sr as f64); - }, - PortRegistration(id, true) => { - //let port = self.jack().port_by_id(id); - //println!("\rport add: {id} {port:?}"); - //println!("\rport add: {id}"); - }, - PortRegistration(id, false) => { - /*println!("\rport del: {id}")*/ - }, - PortsConnected(a, b, true) => { /*println!("\rport conn: {a} {b}")*/ }, - PortsConnected(a, b, false) => { /*println!("\rport disc: {a} {b}")*/ }, - ClientRegistration(id, true) => {}, - ClientRegistration(id, false) => {}, - ThreadInit => {}, - XRun => {}, - GraphReorder => {}, - _ => { panic!("{event:?}"); } - } - } -); diff --git a/crates/app/tek_view.rs b/crates/app/tek_view.rs deleted file mode 100644 index 880bdf6f..00000000 --- a/crates/app/tek_view.rs +++ /dev/null @@ -1,414 +0,0 @@ -use crate::*; - -impl Content for App { - fn content (&self) -> impl Render + '_ { - Stack::above(move|add|for dsl in self.mode.view.iter() { - add(&Fill::xy(self.view(dsl.as_ref()))); - }) - } -} - -impl App { - fn view <'t, D: Dsl> (&'t self, index: D) -> TuiBox<'t> { - 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) - } -} - -type TuiBox<'t> = Box + 't>; - -fn render_dsl <'t> ( - state: &'t impl DslNs<'t, TuiBox<'t>>, - src: &str -) -> TuiBox<'t> { - let err: Option> = match state.from(&src) { - Ok(Some(value)) => return value, Ok(None) => None, Err(e) => Some(e), - }; - let (fg_1, bg_1) = (Color::Rgb(240, 160, 100), Color::Rgb(48, 0, 0)); - let (fg_2, bg_2) = (Color::Rgb(250, 200, 180), Color::Rgb(48, 0, 0)); - let (fg_3, bg_3) = (Color::Rgb(250, 200, 120), Color::Rgb(0, 0, 0)); - let bg = Color::Rgb(24, 0, 0); - Box::new(col! { - Tui::fg(bg, Fixed::y(1, Fill::x(RepeatH("▄")))), - Tui::bg(bg, col! { - Fill::x(Bsp::e( - Tui::bold(true, Tui::fg_bg(fg_1, bg_1, " Render error: ")), - Tui::fg_bg(fg_2, bg_2, err.map(|e|format!(" {e} "))), - )), - Fill::x(Align::x(Tui::fg_bg(fg_3, bg_3, format!(" {src} ")))), - }), - Tui::fg(bg, Fixed::y(1, Fill::x(RepeatH("▀")))), - }) -} - -impl<'t> DslNs<'t, TuiBox<'t>> for App { - dsl_exprs!(|app| -> TuiBox<'t> { - "text" (tail: Arc) => Box::new(tail), - - "fg" (color: Color, x: TuiBox<'t>) => Box::new(Tui::fg(color, x)), - "bg" (color: Color, x: TuiBox<'t>) => Box::new(Tui::bg(color, x)), - "fg/bg" (fg: Color, bg: Color, x: TuiBox<'t>) => Box::new(Tui::fg_bg(fg, bg, x)), - - "either" (cond: bool, a: TuiBox<'t>, b: TuiBox<'t>) => - Box::new(Either(cond, a, b)), - - "bsp/n" (a: TuiBox<'t>, b: TuiBox<'t>) => Box::new(Bsp::n(a, b)), - "bsp/s" (a: TuiBox<'t>, b: TuiBox<'t>) => Box::new(Bsp::s(a, b)), - "bsp/e" (a: TuiBox<'t>, b: TuiBox<'t>) => Box::new(Bsp::e(a, b)), - "bsp/w" (a: TuiBox<'t>, b: TuiBox<'t>) => Box::new(Bsp::w(a, b)), - "bsp/a" (a: TuiBox<'t>, b: TuiBox<'t>) => Box::new(Bsp::a(a, b)), - "bsp/b" (a: TuiBox<'t>, b: TuiBox<'t>) => Box::new(Bsp::b(a, b)), - - "align/nw" (x: TuiBox<'t>) => Box::new(Align::nw(x)), - "align/ne" (x: TuiBox<'t>) => Box::new(Align::ne(x)), - "align/n" (x: TuiBox<'t>) => Box::new(Align::n(x)), - "align/s" (x: TuiBox<'t>) => Box::new(Align::s(x)), - "align/e" (x: TuiBox<'t>) => Box::new(Align::e(x)), - "align/w" (x: TuiBox<'t>) => Box::new(Align::w(x)), - "align/x" (x: TuiBox<'t>) => Box::new(Align::x(x)), - "align/y" (x: TuiBox<'t>) => Box::new(Align::y(x)), - "align/c" (x: TuiBox<'t>) => Box::new(Align::c(x)), - - "fill/x" (x: TuiBox<'t>) => Box::new(Fill::x(x)), - "fill/y" (x: TuiBox<'t>) => Box::new(Fill::y(x)), - "fill/xy" (x: TuiBox<'t>) => Box::new(Fill::xy(x)), - - "fixed/x" (x: u16, c: TuiBox<'t>) => Box::new(Fixed::x(x, c)), - "fixed/y" (y: u16, c: TuiBox<'t>) => Box::new(Fixed::y(y, c)), - "fixed/xy" (x: u16, y: u16, c: TuiBox<'t>) => Box::new(Fixed::xy(x, y, c)), - - "min/x" (x: u16, c: TuiBox<'t>) => Box::new(Min::x(x, c)), - "min/y" (y: u16, c: TuiBox<'t>) => Box::new(Min::y(y, c)), - "min/xy" (x: u16, y: u16, c: TuiBox<'t>) => Box::new(Min::xy(x, y, c)), - - "max/x" (x: u16, c: TuiBox<'t>) => Box::new(Max::x(x, c)), - "max/y" (y: u16, c: TuiBox<'t>) => Box::new(Max::y(y, c)), - "max/xy" (x: u16, y: u16, c: TuiBox<'t>) => Box::new(Max::xy(x, y, c)), - }); - dsl_words!(|app| -> TuiBox<'t> { - ":logo" => Box::new(Fixed::xy(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), Stack::south(|add|{ - add(&Fixed::y(1, "")); - add(&Fixed::y(1, "")); - add(&Fixed::y(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~")); - add(&Fixed::y(1, Bsp::e("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", Bsp::e(Tui::fg(Rgb(230,100,40), "v0.3.0"), " ~~")))); - add(&Fixed::y(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~")); - }))))), - ":meters/input" => Box::new(Tui::bg(Rgb(30, 30, 30), Fill::y(Align::s("Input Meters")))), - ":meters/output" => Box::new(Tui::bg(Rgb(30, 30, 30), Fill::y(Align::s("Output Meters")))), - ":status" => Box::new("Status Bar"), - ":tracks/names" => Box::new(app.project.view_track_names(app.color.clone())),//Tui::bg(Rgb(40, 40, 40), Fill::x(Align::w("Track Names")))), - ":tracks/inputs" => Box::new(Tui::bg(Rgb(40, 40, 40), Fill::x(Align::w("Track Inputs")))), - ":tracks/devices" => Box::new(Tui::bg(Rgb(40, 40, 40), Fill::x(Align::w("Track Devices")))), - ":tracks/outputs" => Box::new(Tui::bg(Rgb(40, 40, 40), Fill::x(Align::w("Track Outputs")))), - ":scenes/names" => Box::new("Scene Names"), - ":editor" => Box::new("Editor"), - ":scenes" => Box::new("Scenes"), - ":dialog/menu" => Box::new(if let Dialog::Menu(selected, items) = &app.dialog { - let items = items.clone(); - let selected = *selected; - Some(Fill::x(Stack::south(move|add|{ - for (index, MenuItem(item, _)) in items.0.iter().enumerate() { - add(&Tui::fg_bg( - if selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, - if selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, - Fixed::y(2, Align::n(Fill::x(item))) - )); - } - }))) - } else { - None - }), - ":templates" => Box::new({ - let modes = app.config.modes.clone(); - let height = (modes.read().unwrap().len() * 2) as u16; - Fixed::y(height, Min::x(30, Stack::south(move|add|{ - for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { - let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; - 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(""); - let fg1 = Rgb(224, 192, 128); - let fg2 = Rgb(224, 128, 32); - let field_name = Fill::x(Align::w(Tui::fg(fg1, name))); - let field_id = Fill::x(Align::e(Tui::fg(fg2, id))); - let field_info = Fill::x(Align::w(info)); - add(&Fixed::y(2, Fill::x(Tui::bg(bg, Bsp::s( - Bsp::a(field_name, field_id), field_info))))); - } - }))) - }), - ":sessions" => Box::new(Fixed::y(6, Min::x(30, Stack::south(move|add|{ - let fg = Rgb(224, 192, 128); - for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { - let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; - add(&Fixed::y(2, Fill::x(Tui::bg(bg, Align::w(Tui::fg(fg, name)))))); - } - })))), - ":browse/title" => Box::new(Fill::x(Align::w(FieldV(Default::default(), - match app.dialog.browser_target().unwrap() { - BrowseTarget::SaveProject => "Save project:", - BrowseTarget::LoadProject => "Load project:", - BrowseTarget::ImportSample(_) => "Import sample:", - BrowseTarget::ExportSample(_) => "Export sample:", - BrowseTarget::ImportClip(_) => "Import clip:", - BrowseTarget::ExportClip(_) => "Export clip:", - }, Shrink::x(3, Fixed::y(1, Tui::fg(Tui::g(96), RepeatH("🭻")))))))), - ":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")))) }))) }, - }); - /// Resolve an expression if known. - fn from_expr (&self, dsl: &D) -> Perhaps> { - if let Some(head) = dsl.expr().head()? { - for (key, value) in Self::EXPRS.0.iter() { - if head == *key { - return value(self, dsl.expr().tail()?.unwrap_or("")) - } - } - } - return Ok(None) - } - /// Resolve a symbol if known. - fn from_word (&self, dsl: &D) -> Perhaps> { - if let Some(dsl) = dsl.word()? { - let views = self.config.views.read().unwrap(); - if let Some(view) = views.get(dsl) { - let view = view.clone(); - std::mem::drop(views); - return Ok(Some(render_dsl(self, view.as_ref()))) - } - for (word, get) in Self::WORDS.0 { - if dsl == *word { - return get(self) - } - } - } - return Ok(None) - } -} - -pub fn view_nil (_: &App) -> TuiBox { - Box::new(Fill::xy("·")) -} - - //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::Browse(BrowseTarget::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::Browse(BrowseTarget::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::Browse(BrowseTarget::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_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/config/Cargo.toml b/crates/config/Cargo.toml deleted file mode 100644 index b5605dcd..00000000 --- a/crates/config/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "tek_config" -edition = { workspace = true } -version = { workspace = true } - -[lib] -path = "config.rs" - -[dependencies] -xdg = { workspace = true } -tengri = { workspace = true } diff --git a/crates/config/config.rs b/crates/config/config.rs deleted file mode 100644 index 3b23f99d..00000000 --- a/crates/config/config.rs +++ /dev/null @@ -1,232 +0,0 @@ -#![feature(if_let_guard)] - -use ::{ - std::{ - sync::{Arc, RwLock}, - collections::BTreeMap, - path::PathBuf - }, - tengri::{ - Usually, impl_debug, - tui::TuiEvent, - dsl::{Dsl, DslWord, DslExpr} - }, - xdg::BaseDirectories, -}; - -/// Configuration. -/// -/// Contains mode, view, and bind definitions. -#[derive(Default, Debug)] -pub struct Config { - pub dirs: BaseDirectories, - pub modes: Modes, - pub views: Views, - pub binds: Binds, -} - -type Modes = Arc, Arc>>>>>; -type Binds = Arc, EventMap>>>>; -type Views = Arc, Arc>>>; - -/// A set of currently active view and keys definitions, -/// with optional name and description. -#[derive(Default, Debug)] -pub struct Mode { - pub path: PathBuf, - pub name: Vec, - pub info: Vec, - pub view: Vec, - pub keys: Vec, - pub modes: Modes, -} - -/// A collection of input bindings. -#[derive(Debug)] -pub struct EventMap( - /// Map of each event (e.g. key combination) to - /// all command expressions bound to it by - /// all loaded input layers. - pub BTreeMap>> -); - -/// An input binding. -#[derive(Debug, Clone)] -pub struct Binding { - pub commands: Arc<[C]>, - pub condition: Option, - pub description: Option>, - pub source: Option>, -} - -/// Input bindings are only returned if this evaluates to true -#[derive(Clone)] -pub struct Condition(Arcbool + Send + Sync>>); - -impl Config { - const CONFIG: &'static str = "tek.edn"; - const DEFAULTS: &'static str = include_str!("../../tek.edn"); - - pub fn new (dirs: Option) -> Self { - Self { - dirs: dirs.unwrap_or_else(||BaseDirectories::with_profile("tek", "v0")), - ..Default::default() - } - } - pub fn init (&mut self) -> Usually<()> { - self.init_file(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|cfgs.load(dsl))?; - Ok(()) - } - pub fn init_file ( - &mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()> - ) -> Usually<()> { - if self.dirs.find_config_file(path).is_none() { - println!("Creating {path:?}"); - std::fs::write(self.dirs.place_config_file(path)?, defaults); - } - Ok(if let Some(path) = self.dirs.find_config_file(path) { - println!("Loading {path:?}"); - let src = std::fs::read_to_string(&path)?; - src.as_str().each(move|item|each(self, item))?; - } else { - return Err(format!("{path}: not found").into()) - }) - } - pub fn load (&mut self, dsl: impl Dsl) -> Usually<()> { - dsl.each(|item|if let Some(expr) = item.expr()? { - let head = expr.head()?; - let tail = expr.tail()?; - let name = tail.head()?; - let body = tail.tail()?; - println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default()); - match head { - Some("mode") if let Some(name) = name => - Mode::>::load_into(&self.modes, &name, &body)?, - Some("keys") if let Some(name) = name => - EventMap::>::load_into(&self.binds, &name, &body)?, - Some("view") if let Some(name) = name => { - self.views.write().unwrap().insert(name.into(), body.src()?.unwrap_or_default().into()); - }, - _ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into()) - } - Ok(()) - } else { - return Err(format!("Config::load: expected expr, got: {item:?}").into()) - }) - } -} - -impl Mode> { - pub fn load_into (modes: &Modes, name: &impl AsRef, body: &impl Dsl) -> Usually<()> { - let mut mode = Self::default(); - println!("Mode::load_into: {}: {body:?}", name.as_ref()); - body.each(|item|mode.load_one(item))?; - modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode)); - Ok(()) - } - pub fn load_one (&mut self, dsl: impl Dsl) -> Usually<()> { - Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() { - println!("Mode::load_one: {head} {:?}", expr.tail()); - let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); - match head { - "name" => self.name.push(tail.into()), - "info" => self.info.push(tail.into()), - "view" => self.view.push(tail.into()), - "keys" => tail.each(|expr|{self.keys.push(expr.trim().into()); Ok(())})?, - "mode" => if let Some(id) = tail.head()? { - Self::load_into(&self.modes, &id, &tail.tail())?; - } else { - return Err(format!("Mode::load_one: self: incomplete: {expr:?}").into()); - }, - _ => { - return Err(format!("Mode::load_one: unexpected expr: {head:?} {tail:?}").into()) - }, - }; - } else if let Ok(Some(word)) = dsl.word() { - self.view.push(word.into()); - } else { - return Err(format!("Mode::load_one: unexpected: {dsl:?}").into()); - }) - } -} - -/// Default is always empty map regardless if `E` and `C` implement [Default]. -impl Default for EventMap { - fn default () -> Self { Self(Default::default()) } -} - -impl EventMap { - /// Create a new event map - pub fn new () -> Self { - Default::default() - } - /// Add a binding to an owned event map. - pub fn def (mut self, event: E, binding: Binding) -> Self { - self.add(event, binding); - self - } - /// Add a binding to an event map. - pub fn add (&mut self, event: E, binding: Binding) -> &mut Self { - if !self.0.contains_key(&event) { - self.0.insert(event.clone(), Default::default()); - } - self.0.get_mut(&event).unwrap().push(binding); - self - } - /// Return the binding(s) that correspond to an event. - pub fn query (&self, event: &E) -> Option<&[Binding]> { - self.0.get(event).map(|x|x.as_slice()) - } - /// Return the first binding that corresponds to an event, considering conditions. - pub fn dispatch (&self, event: &E) -> Option<&Binding> { - self.query(event) - .map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next()) - .flatten() - } -} - -impl EventMap> { - pub fn load_into (binds: &Binds, name: &impl AsRef, body: &impl Dsl) -> Usually<()> { - println!("EventMap::load_into: {}: {body:?}", name.as_ref()); - let mut map = Self::new(); - body.each(|item|if item.expr().head() == Ok(Some("see")) { - // TODO - Ok(()) - } else if let Ok(Some(word)) = item.expr().head().word() { - if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? { - map.add(key, Binding { - commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(), - condition: None, - description: None, - source: None - }); - Ok(()) - } else if Some(":char") == item.expr()?.head()? { - // TODO - return Ok(()) - } else { - return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into()) - } - } else { - return Err(format!("Config::load_bind: unexpected: {item:?}").into()) - })?; - binds.write().unwrap().insert(name.as_ref().into(), map); - Ok(()) - } -} - -impl Binding { - pub fn from_dsl (dsl: impl Dsl) -> Usually { - let command: Option = None; - let condition: Option = None; - let description: Option> = None; - let source: Option> = None; - if let Some(command) = command { - Ok(Self { commands: [command].into(), condition, description, source }) - } else { - Err(format!("no command in {dsl:?}").into()) - } - } -} - -impl_debug!(Condition |self, w| { write!(w, "*") }); diff --git a/crates/device/Cargo.toml b/crates/device/Cargo.toml deleted file mode 100644 index 0a3b6dd2..00000000 --- a/crates/device/Cargo.toml +++ /dev/null @@ -1,35 +0,0 @@ -[package] -name = "tek_device" -edition = { workspace = true } -version = { workspace = true } - -[dependencies] -tengri = { workspace = true } -tengri_proc = { workspace = true } - -tek_engine = { workspace = true } - -uuid = { workspace = true, optional = true } -livi = { workspace = true, optional = true } -symphonia = { workspace = true, optional = true } -wavers = { workspace = true, optional = true } -winit = { workspace = true, optional = true } - -[features] -default = [ "arranger", "sampler", "lv2" ] - -arranger = [ "port", "editor", "sequencer", "editor" ] -browse = [] -clap = [] -clock = [] -editor = [] -lv2 = [ "port", "livi", "winit" ] -meter = [] -mixer = [] -pool = [] -port = [] -sampler = [ "port", "meter", "mixer", "browse", "symphonia", "wavers" ] -sequencer = [ "port", "clock", "uuid", "pool" ] -sf2 = [] -vst2 = [] -vst3 = [] diff --git a/crates/device/src/arranger/arranger_view.rs b/crates/device/src/arranger/arranger_view.rs deleted file mode 100644 index 3daf8ed0..00000000 --- a/crates/device/src/arranger/arranger_view.rs +++ /dev/null @@ -1,386 +0,0 @@ -use crate::*; - -impl Arrangement { - pub fn view_inputs <'a> (&'a self, _theme: ItemTheme) -> impl Content + 'a { - Stack::south(move|add|{ - add(&Fixed::y(1, - Bsp::e(Fixed::x(20, Align::w(button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))), - Bsp::w(Fixed::x(4, button_2("I", "+", false)), - Stack::east(move|add|{ - for (_index, track, _x1, _x2) in self.tracks_with_sizes() { - add(&Tui::bg(track.color.dark.rgb, Align::w(Fixed::x(track.width as u16, row!( - Either(track.sequencer.monitoring, Tui::fg(Green, "mon "), "mon "), - Either(track.sequencer.recording, Tui::fg(Red, "rec "), "rec "), - Either(track.sequencer.overdub, Tui::fg(Yellow, "dub "), "dub "), - ))))) - } - }))))); - for (_index, port) in self.midi_ins().iter().enumerate() { - add(&Fixed::y(1, Bsp::e( - Fixed::x(20, Align::w(Bsp::e(" ● ", - Tui::bold(true, Tui::fg(Rgb(255,255,255), port.port_name()))))), - Bsp::w(Fixed::x(4, ()), - Stack::east(move|add|{ - for (_index, track, _x1, _x2) in self.tracks_with_sizes() { - add(&Tui::bg(track.color.darker.rgb, Align::w(Fixed::x(track.width as u16, row!( - Either(track.sequencer.monitoring, Tui::fg(Green, " ● "), " · "), - Either(track.sequencer.recording, Tui::fg(Red, " ● "), " · "), - Either(track.sequencer.overdub, Tui::fg(Yellow, " ● "), " · "), - ))))) - } - }))))); - } - }) - } - pub fn view_outputs <'a> (&'a self, theme: ItemTheme) -> impl Content + 'a { - let mut h = 1; - for output in self.midi_outs().iter() { - h += 1 + output.connections.len(); - } - let h = h as u16; - let list = Bsp::s( - Fixed::y(1, Fill::x(Align::w(button_3("o", "utput", format!("{}", self.midi_outs.len()), false)))), - Fixed::y(h - 1, Fill::xy(Align::nw(Stack::south(|add|{ - for (_index, port) in self.midi_outs().iter().enumerate() { - add(&Fixed::y(1,Fill::x(Bsp::e( - Align::w(Bsp::e(" ● ", Tui::fg(Rgb(255,255,255),Tui::bold(true, port.port_name())))), - Fill::x(Align::e(format!("{}/{} ", - port.port().get_connections().len(), - port.connections.len()))))))); - for (index, conn) in port.connections.iter().enumerate() { - add(&Fixed::y(1, Fill::x(Align::w(format!(" c{index:02}{}", conn.info()))))); - } - } - }))))); - Fixed::y(h, Self::view_track_row_section(theme, list, button_2("O", "+", false), - Tui::bg(theme.darker.rgb, Align::w(Fill::x( - Stack::east(move|add|{ - for (index, track, _x1, _x2) in self.tracks_with_sizes() { - add(&Fixed::x(self.track_width(index, track), - Stack::south(move|add|{ - add(&Fixed::y(1, Align::w(Bsp::e( - Either(true, Tui::fg(Green, "play "), "play "), - Either(false, Tui::fg(Yellow, "solo "), "solo "), - )))); - for (_index, port) in self.midi_outs().iter().enumerate() { - add(&Fixed::y(1, Align::w(Bsp::e( - Either(true, Tui::fg(Green, " ● "), " · "), - Either(false, Tui::fg(Yellow, " ● "), " · "), - )))); - for (_index, _conn) in port.connections.iter().enumerate() { - add(&Fixed::y(1, Fill::x(""))); - } - }})))}})))))) - } - pub fn view_track_devices <'a> (&'a self, theme: ItemTheme) -> impl Content + 'a { - let mut h = 2u16; - for track in self.tracks().iter() { - h = h.max(track.devices.len() as u16 * 2); - } - Self::view_track_row_section( - theme, - button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false), - button_2("D", "+", false), - Stack::east(move|add|{ - for (index, track, _x1, _x2) in self.tracks_with_sizes() { - add(&Fixed::xy(self.track_width(index, track), h + 1, - Tui::bg(track.color.dark.rgb, Align::nw(Map::south(2, move||0..h, - |_, _index|Fixed::xy(track.width as u16, 2, - Tui::fg_bg( - ItemTheme::G[32].lightest.rgb, - ItemTheme::G[32].dark.rgb, - Align::nw(format!(" · {}", "--"))))))))); - } - })) - } -} - -pub trait HasClipsSize { - fn clips_size (&self) -> &Measure; -} -impl HasClipsSize for Arrangement { - fn clips_size (&self) -> &Measure { &self.inner_size } -} - -impl TracksView for Arrangement {} - -impl ClipsView for T {} - -pub trait TracksView: - ScenesView + - HasMidiIns + - HasMidiOuts + - HasSize + - HasTrackScroll + - HasSelection + - HasEditor + - HasClipsSize -{ - fn tracks_width_available (&self) -> u16 { - (self.width() as u16).saturating_sub(40) - } - /// Iterate over tracks with their corresponding sizes. - fn tracks_with_sizes (&self) -> impl TracksSizes<'_> { - let _editor_width = self.editor().map(|e|e.width()); - let _active_track = self.selection().track(); - let mut x = 0; - self.tracks().iter().enumerate().map_while(move |(index, track)|{ - let width = track.width.max(8); - if x + width < self.clips_size().w() { - let data = (index, track, x, x + width); - x += width + Self::TRACK_SPACING; - Some(data) - } else { - None - } - }) - } - fn view_track_row_section ( - _theme: ItemTheme, - button: impl Content, - button_add: impl Content, - content: impl Content - ) -> impl Content { - Bsp::w(Fill::y(Fixed::x(4, Align::nw(button_add))), - Bsp::e(Fixed::x(20, Fill::y(Align::nw(button))), Fill::xy(Align::c(content)))) - } - fn view_track_header <'a, T: Content> ( - &'a self, theme: ItemTheme, content: T - ) -> impl Content { - Fixed::x(12, Tui::bg(theme.darker.rgb, Fill::x(Align::e(content)))) - } - fn view_track_names (&self, theme: ItemTheme) -> impl Content { - let track_count = self.tracks().len(); - let scene_count = self.scenes().len(); - let selected = self.selection(); - let button = Bsp::s( - button_3("t", "rack ", format!("{}{track_count}", selected.track() - .map(|track|format!("{track}/")).unwrap_or_default()), false), - button_3("s", "cene ", format!("{}{scene_count}", selected.scene() - .map(|scene|format!("{scene}/")).unwrap_or_default()), false)); - let button_2 = Bsp::s( - button_2("T", "+", false), - button_2("S", "+", false)); - Self::view_track_row_section(theme, button, button_2, Tui::bg(theme.darker.rgb, - Fixed::y(2, Fill::x(Stack::east(move|add|{ - for (index, track, _x1, _x2) in self.tracks_with_sizes() { - add(&Fixed::x(self.track_width(index, track), - Tui::bg(if selected.track() == Some(index) { - track.color.light.rgb - } else { - track.color.base.rgb - }, Bsp::s(Fill::x(Align::nw(Bsp::e( - format!("·t{index:02} "), - Tui::fg(Rgb(255, 255, 255), Tui::bold(true, &track.name)) - ))), ""))) ); - } - }))))) - } - fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Content { - Self::view_track_row_section(theme, - Bsp::s( - Fill::x(Align::w(button_2("o", "utput", false))), - Fill::xy(Stack::south(|add|{ - for port in self.midi_outs().iter() { - add(&Fill::x(Align::w(port.port_name()))); - } - }))), - button_2("O", "+", false), - Tui::bg(theme.darker.rgb, Align::w(Fill::x( - Stack::east(move|add|{ - for (index, track, _x1, _x2) in self.tracks_with_sizes() { - add(&Fixed::x(self.track_width(index, track), - Align::nw(Fill::y(Map::south(1, ||track.sequencer.midi_outs.iter(), - |port, index|Tui::fg(Rgb(255, 255, 255), - Fixed::y(1, Tui::bg(track.color.dark.rgb, Fill::x(Align::w( - format!("·o{index:02} {}", port.port_name()))))))))))); - } - }))))) - } - fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Content { - let mut h = 0u16; - for track in self.tracks().iter() { - h = h.max(track.sequencer.midi_ins.len() as u16); - } - Self::view_track_row_section( - theme, - button_2("i", "nput", false), - button_2("I", "+", false), - Tui::bg(theme.darker.rgb, Align::w(Fill::x( - Stack::east(move|add|{ - for (index, track, _x1, _x2) in self.tracks_with_sizes() { - add(&Fixed::xy(self.track_width(index, track), h + 1, - Align::nw(Bsp::s( - Tui::bg(track.color.base.rgb, - Fill::x(Align::w(row!( - Either(track.sequencer.monitoring, Tui::fg(Green, "●mon "), "·mon "), - Either(track.sequencer.recording, Tui::fg(Red, "●rec "), "·rec "), - Either(track.sequencer.overdub, Tui::fg(Yellow, "●dub "), "·dub "), - )))), - Map::south(1, ||track.sequencer.midi_ins.iter(), - |port, index|Tui::fg_bg(Rgb(255, 255, 255), track.color.dark.rgb, - Fill::x(Align::w(format!("·i{index:02} {}", port.port_name()))))))))); - }}))))) - } - fn track_width (&self, index: usize, track: &Track) -> u16 { - track.width as u16 - } -} - -pub trait ScenesView: - HasEditor + - HasSelection + - HasSceneScroll + - HasClipsSize + - Send + - Sync -{ - /// Default scene height. - const H_SCENE: usize = 2; - /// Default editor height. - const H_EDITOR: usize = 15; - fn h_scenes (&self) -> u16; - fn w_side (&self) -> u16; - fn w_mid (&self) -> u16; - fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> { - let mut y = 0; - self.scenes().iter().enumerate().skip(self.scene_scroll()).map_while(move|(s, scene)|{ - let height = if self.selection().scene() == Some(s) && self.editor().is_some() { - 8 - } else { - Self::H_SCENE - }; - if y + height <= self.clips_size().h() { - let data = (s, scene, y, y + height); - y += height; - Some(data) - } else { - None - } - }) - } - fn view_scenes_names (&self) -> impl Content { - Fixed::x(20, Stack::south(move|add|{ - for (index, scene, ..) in self.scenes_with_sizes() { - add(&self.view_scene_name(index, scene)); - } - })) - } - fn view_scene_name (&self, index: usize, scene: &Scene) -> impl Content { - let h = if self.selection().scene() == Some(index) && let Some(_editor) = self.editor() { - 7 - } else { - Self::H_SCENE as u16 - }; - let bg = if self.selection().scene() == Some(index) { - scene.color.light.rgb - } else { - scene.color.base.rgb - }; - Fixed::xy(20, h, Tui::bg(bg, Align::nw(Bsp::s( - Fill::x(Align::w(Bsp::e( - format!("·s{index:02} "), - Tui::fg(Rgb(255, 255, 255), Tui::bold(true, &scene.name)) - ))), - When(self.selection().scene() == Some(index) && self.is_editing(), - Fill::xy(Align::nw(Bsp::s( - self.editor().as_ref().map(|e|e.clip_status()), - self.editor().as_ref().map(|e|e.edit_status()))))))))) - } -} - -pub trait ClipsView: - TracksView + - ScenesView + - HasClipsSize + - Send + - Sync -{ - fn view_scenes_clips <'a> (&'a self) - -> impl Content + 'a - { - self.clips_size().of(Fill::xy(Bsp::a( - Fill::xy(Align::se(Tui::fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))), - Stack::::east(move|column|{ - for (track_index, track, _, _) in self.tracks_with_sizes() { - //column(&Fixed::x(5, Fill::xy(Tui::bg(Green, "kyp")))); - column(&Fixed::x( - track.width as u16, - Fill::y(self.view_track_clips(track_index, track)) - )) - } - })))) - } - - fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Content + 'a { - Stack::south(move|cell|{ - for (scene_index, scene, ..) in self.scenes_with_sizes() { - let (name, theme): (Arc, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(track_index) { - let clip = clip.read().unwrap(); - (format!(" ⏹ {}", &clip.name).into(), clip.color) - } else { - (" ⏹ -- ".into(), ItemTheme::G[32]) - }; - let fg = theme.lightest.rgb; - let mut outline = theme.base.rgb; - let bg = if self.selection().track() == Some(track_index) - && self.selection().scene() == Some(scene_index) - { - outline = theme.lighter.rgb; - theme.light.rgb - } else if self.selection().track() == Some(track_index) - || self.selection().scene() == Some(scene_index) - { - outline = theme.darkest.rgb; - theme.base.rgb - } else { - theme.dark.rgb - }; - let w = if self.selection().track() == Some(track_index) - && let Some(editor) = self.editor () - { - editor.width().max(24).max(track.width) - } else { - track.width - } as u16; - let y = if self.selection().scene() == Some(scene_index) - && let Some(editor) = self.editor () - { - editor.height().max(12) - } else { - Self::H_SCENE - } as u16; - cell(&Fixed::xy(w, y, Bsp::b( - Fill::xy(Outer(true, Style::default().fg(outline))), - Fill::xy(Bsp::b( - Bsp::b( - Tui::fg_bg(outline, bg, Fill::xy("")), - Fill::xy(Align::nw(Tui::fg_bg(fg, bg, Tui::bold(true, name)))), - ), - Fill::xy(When(self.selection().track() == Some(track_index) - && self.selection().scene() == Some(scene_index) - && self.is_editing(), self.editor()))))))); - } - }) - } -} - -pub trait HasWidth { - const MIN_WIDTH: usize; - /// Increment track width. - fn width_inc (&mut self); - /// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH]. - fn width_dec (&mut self); -} - -impl HasWidth for Track { - const MIN_WIDTH: usize = 9; - fn width_inc (&mut self) { - self.width += 1; - } - fn width_dec (&mut self) { - if self.width > Track::MIN_WIDTH { - self.width -= 1; - } - } -} diff --git a/crates/device/src/device.rs b/crates/device/src/device.rs deleted file mode 100644 index e6667d17..00000000 --- a/crates/device/src/device.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::*; - -pub fn device_kinds () -> &'static [&'static str] { - &[ - "Sampler", - "Plugin (LV2)", - ] -} - -impl> + Has> HasDevices for T { - fn devices (&self) -> &Vec { - self.get() - } - fn devices_mut (&mut self) -> &mut Vec { - self.get_mut() - } -} - -pub trait HasDevices: Has { - fn devices (&self) -> &Vec; - fn devices_mut (&mut self) -> &mut Vec; -} - -#[derive(Debug)] -pub enum Device { - #[cfg(feature = "sampler")] - Sampler(Sampler), - #[cfg(feature = "lv2")] // TODO - Lv2(Lv2), - #[cfg(feature = "vst2")] // TODO - Vst2, - #[cfg(feature = "vst3")] // TODO - Vst3, - #[cfg(feature = "clap")] // TODO - Clap, - #[cfg(feature = "sf2")] // TODO - Sf2, -} - -impl Device { - pub fn name (&self) -> &str { - match self { - Self::Sampler(sampler) => sampler.name.as_ref(), - _ => todo!(), - } - } - pub fn midi_ins (&self) -> &[MidiInput] { - match self { - //Self::Sampler(Sampler { midi_in, .. }) => &[midi_in], - _ => todo!() - } - } - pub fn midi_outs (&self) -> &[MidiOutput] { - match self { - Self::Sampler(_) => &[], - _ => todo!() - } - } - pub fn audio_ins (&self) -> &[AudioInput] { - match self { - Self::Sampler(Sampler { audio_ins, .. }) => audio_ins.as_slice(), - _ => todo!() - } - } - pub fn audio_outs (&self) -> &[AudioOutput] { - match self { - Self::Sampler(Sampler { audio_outs, .. }) => audio_outs.as_slice(), - _ => todo!() - } - } -} - -pub struct DeviceAudio<'a>(pub &'a mut Device); - -audio!(|self: DeviceAudio<'a>, client, scope|{ - use Device::*; - match self.0 { - #[cfg(feature = "sampler")] Sampler(sampler) => sampler.process(client, scope), - #[cfg(feature = "lv2")] Lv2(lv2) => lv2.process(client, scope), - #[cfg(feature = "vst2")] Vst2 => { todo!() }, // TODO - #[cfg(feature = "vst3")] Vst3 => { todo!() }, // TODO - #[cfg(feature = "clap")] Clap => { todo!() }, // TODO - #[cfg(feature = "sf2")] Sf2 => { todo!() }, // TODO - } -}); - -def_command!(DeviceCommand: |device: Device| {}); diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml deleted file mode 100644 index d63ae531..00000000 --- a/crates/engine/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "tek_engine" -edition = { workspace = true } -version = { workspace = true } - -[dependencies] -tengri = { workspace = true } -tengri_proc = { workspace = true } -jack = { workspace = true } -midly = { workspace = true } -uuid = { workspace = true } -atomic_float = { workspace = true } diff --git a/deps/tengri b/deps/tengri index b98fccd9..a4dbf882 160000 --- a/deps/tengri +++ b/deps/tengri @@ -1 +1 @@ -Subproject commit b98fccd98ba13be8eabe29d66621a15d025b2042 +Subproject commit a4dbf88220f75ccaf9d14cc2e4fb7c00479e3940 diff --git a/shell.nix b/shell.nix index 7f22e603..8782e6dd 100755 --- a/shell.nix +++ b/shell.nix @@ -1,32 +1,40 @@ #!/usr/bin/env nix-shell {pkgs?import{}}:let - stdenv = pkgs.clang19Stdenv; name = "tek"; - nativeBuildInputs = with pkgs; [ pkg-config freetype libclang ]; - buildInputs = with pkgs; let - #suil = pkgs.enableDebugging (pkgs.suil.overrideAttrs (a: b: { - #dontStrip = true; separateDebugInfo = true; - #})); - in [ jack2 lilv serd libclang /*suil*/ glib gtk3 ]; + stdenv = pkgs.clang19Stdenv; + nativeBuildInputs = [ + pkgs.pkg-config + pkgs.freetype + pkgs.libclang + pkgs.mold + ]; + buildInputs = [ + pkgs.jack2 + pkgs.lilv + pkgs.serd + pkgs.libclang + ]; VST3_SDK_DIR = "/home/user/Lab/Music/tek/vst3sdk/"; LIBCLANG_PATH = "${pkgs.libclang.lib.outPath}/lib"; - LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath (with pkgs; [ - pipewire.jack + LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ + pkgs.pipewire.jack # for ChowKick.lv2: - freetype - libgcc.lib + pkgs.freetype + pkgs.libgcc.lib # for Panagement - xorg.libX11 - xorg.libXcursor - xorg.libXi - libxkbcommon + pkgs.xorg.libX11 + pkgs.xorg.libXcursor + pkgs.xorg.libXi + pkgs.libxkbcommon + pkgs.lilv + pkgs.serd #suil # for Helm: - alsa-lib - curl - libglvnd + pkgs.alsa-lib + pkgs.curl + pkgs.libglvnd #xorg_sys_opengl - ]); + ]; in pkgs.mkShell.override { inherit stdenv; } { diff --git a/tek/Cargo.toml b/tek/Cargo.toml new file mode 100644 index 00000000..f29213a0 --- /dev/null +++ b/tek/Cargo.toml @@ -0,0 +1,58 @@ +[package] +name = "tek" +edition = { workspace = true } +version = { workspace = true } + +[lib] +path = "tek.rs" + +[[bin]] +name = "tek" +path = "tek_cli.rs" + +[target.'cfg(target_os = "linux")'] +rustflags = ["-C", "link-arg=-fuse-ld=mold"] + +[dependencies] +tengri = { workspace = true } +tengri_proc = { workspace = true } + +atomic_float = { workspace = true } +backtrace = { workspace = true } +clap = { workspace = true, optional = true } +jack = { workspace = true } +konst = { workspace = true } +livi = { workspace = true, optional = true } +midly = { workspace = true } +palette = { workspace = true } +rand = { workspace = true } +symphonia = { workspace = true, optional = true } +toml = { workspace = true } +uuid = { workspace = true, optional = true } +wavers = { workspace = true, optional = true } +winit = { workspace = true, optional = true } +xdg = { workspace = true } + +[dev-dependencies] +proptest = { workspace = true } +proptest-derive = { workspace = true } + +[features] +arranger = ["port", "editor", "sequencer", "editor"] +browse = [] +clap = [] +cli = ["dep:clap"] +clock = [] +default = ["cli", "arranger", "sampler", "lv2"] +editor = [] +host = ["lv2"] +lv2 = ["port", "livi", "winit"] +meter = [] +mixer = [] +pool = [] +port = [] +sampler = ["port", "meter", "mixer", "browse", "symphonia", "wavers"] +sequencer = ["port", "clock", "uuid", "pool"] +sf2 = [] +vst2 = [] +vst3 = [] diff --git a/tek/config.rs b/tek/config.rs new file mode 100644 index 00000000..ac8a2114 --- /dev/null +++ b/tek/config.rs @@ -0,0 +1,81 @@ +pub(self) use ::{ + std::{ + sync::{Arc, RwLock}, + collections::BTreeMap, + path::PathBuf + }, + tengri::{ + Usually, impl_debug, + tui::TuiEvent, + dsl::{Dsl, DslWord, DslExpr} + }, + xdg::BaseDirectories, +}; + +mod bind; pub use self::bind::*; +mod mode; pub use self::mode::*; +mod view; pub use self::view::*; + +/// Configuration. +/// +/// Contains mode, view, and bind definitions. +#[derive(Default, Debug)] +pub struct Config { + pub dirs: BaseDirectories, + pub modes: Modes, + pub views: Views, + pub binds: Binds, +} + +impl Config { + const CONFIG: &'static str = "tek.edn"; + const DEFAULTS: &'static str = include_str!("./tek.edn"); + + pub fn new (dirs: Option) -> Self { + Self { + dirs: dirs.unwrap_or_else(||BaseDirectories::with_profile("tek", "v0")), + ..Default::default() + } + } + pub fn init (&mut self) -> Usually<()> { + self.init_file(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|cfgs.load(&dsl))?; + Ok(()) + } + pub fn init_file ( + &mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()> + ) -> Usually<()> { + if self.dirs.find_config_file(path).is_none() { + println!("Creating {path:?}"); + std::fs::write(self.dirs.place_config_file(path)?, defaults)?; + } + Ok(if let Some(path) = self.dirs.find_config_file(path) { + println!("Loading {path:?}"); + let src = std::fs::read_to_string(&path)?; + src.as_str().each(move|item|each(self, item))?; + } else { + return Err(format!("{path}: not found").into()) + }) + } + pub fn load (&mut self, dsl: impl Dsl) -> Usually<()> { + dsl.each(|item|if let Some(expr) = item.expr()? { + let head = expr.head()?; + let tail = expr.tail()?; + let name = tail.head()?; + let body = tail.tail()?; + println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default()); + match head { + Some("mode") if let Some(name) = name => + Mode::>::load_into(&self.modes, &name, &body)?, + Some("keys") if let Some(name) = name => + EventMap::>::load_into(&self.binds, &name, &body)?, + Some("view") if let Some(name) = name => { + self.views.write().unwrap().insert(name.into(), body.src()?.unwrap_or_default().into()); + }, + _ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into()) + } + Ok(()) + } else { + return Err(format!("Config::load: expected expr, got: {item:?}").into()) + }) + } +} diff --git a/tek/config/bind.rs b/tek/config/bind.rs new file mode 100644 index 00000000..0795f799 --- /dev/null +++ b/tek/config/bind.rs @@ -0,0 +1,106 @@ +use super::*; + +pub type Binds = Arc, EventMap>>>>; + +/// A collection of input bindings. +#[derive(Debug)] +pub struct EventMap( + /// Map of each event (e.g. key combination) to + /// all command expressions bound to it by + /// all loaded input layers. + pub BTreeMap>> +); + +/// An input binding. +#[derive(Debug, Clone)] +pub struct Binding { + pub commands: Arc<[C]>, + pub condition: Option, + pub description: Option>, + pub source: Option>, +} + +/// Input bindings are only returned if this evaluates to true +#[derive(Clone)] +pub struct Condition(Arcbool + Send + Sync>>); + +/// Default is always empty map regardless if `E` and `C` implement [Default]. +impl Default for EventMap { + fn default () -> Self { Self(Default::default()) } +} + +impl EventMap { + /// Create a new event map + pub fn new () -> Self { + Default::default() + } + /// Add a binding to an owned event map. + pub fn def (mut self, event: E, binding: Binding) -> Self { + self.add(event, binding); + self + } + /// Add a binding to an event map. + pub fn add (&mut self, event: E, binding: Binding) -> &mut Self { + if !self.0.contains_key(&event) { + self.0.insert(event.clone(), Default::default()); + } + self.0.get_mut(&event).unwrap().push(binding); + self + } + /// Return the binding(s) that correspond to an event. + pub fn query (&self, event: &E) -> Option<&[Binding]> { + self.0.get(event).map(|x|x.as_slice()) + } + /// Return the first binding that corresponds to an event, considering conditions. + pub fn dispatch (&self, event: &E) -> Option<&Binding> { + self.query(event) + .map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next()) + .flatten() + } +} + +impl EventMap> { + pub fn load_into (binds: &Binds, name: &impl AsRef, body: &impl Dsl) -> Usually<()> { + println!("EventMap::load_into: {}: {body:?}", name.as_ref()); + let mut map = Self::new(); + body.each(|item|if item.expr().head() == Ok(Some("see")) { + // TODO + Ok(()) + } else if let Ok(Some(_word)) = item.expr().head().word() { + if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? { + map.add(key, Binding { + commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(), + condition: None, + description: None, + source: None + }); + Ok(()) + } else if Some(":char") == item.expr()?.head()? { + // TODO + return Ok(()) + } else { + return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into()) + } + } else { + return Err(format!("Config::load_bind: unexpected: {item:?}").into()) + })?; + binds.write().unwrap().insert(name.as_ref().into(), map); + Ok(()) + } +} + +impl Binding { + pub fn from_dsl (dsl: impl Dsl) -> Usually { + let command: Option = None; + let condition: Option = None; + let description: Option> = None; + let source: Option> = None; + if let Some(command) = command { + Ok(Self { commands: [command].into(), condition, description, source }) + } else { + Err(format!("no command in {dsl:?}").into()) + } + } +} + +impl_debug!(Condition |self, w| { write!(w, "*") }); diff --git a/tek/config/mode.rs b/tek/config/mode.rs new file mode 100644 index 00000000..71e435c2 --- /dev/null +++ b/tek/config/mode.rs @@ -0,0 +1,49 @@ +use super::*; + +pub type Modes = Arc, Arc>>>>>; + +/// A set of currently active view and keys definitions, +/// with optional name and description. +#[derive(Default, Debug)] +pub struct Mode { + pub path: PathBuf, + pub name: Vec, + pub info: Vec, + pub view: Vec, + pub keys: Vec, + pub modes: Modes, +} + +impl Mode> { + pub fn load_into (modes: &Modes, name: &impl AsRef, body: &impl Dsl) -> Usually<()> { + let mut mode = Self::default(); + println!("Mode::load_into: {}: {body:?}", name.as_ref()); + body.each(|item|mode.load_one(item))?; + modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode)); + Ok(()) + } + pub fn load_one (&mut self, dsl: impl Dsl) -> Usually<()> { + Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() { + println!("Mode::load_one: {head} {:?}", expr.tail()); + let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); + match head { + "name" => self.name.push(tail.into()), + "info" => self.info.push(tail.into()), + "view" => self.view.push(tail.into()), + "keys" => tail.each(|expr|{self.keys.push(expr.trim().into()); Ok(())})?, + "mode" => if let Some(id) = tail.head()? { + Self::load_into(&self.modes, &id, &tail.tail())?; + } else { + return Err(format!("Mode::load_one: self: incomplete: {expr:?}").into()); + }, + _ => { + return Err(format!("Mode::load_one: unexpected expr: {head:?} {tail:?}").into()) + }, + }; + } else if let Ok(Some(word)) = dsl.word() { + self.view.push(word.into()); + } else { + return Err(format!("Mode::load_one: unexpected: {dsl:?}").into()); + }) + } +} diff --git a/tek/config/view.rs b/tek/config/view.rs new file mode 100644 index 00000000..d8cb5e2f --- /dev/null +++ b/tek/config/view.rs @@ -0,0 +1,3 @@ +use super::*; + +pub type Views = Arc, Arc>>>; diff --git a/crates/app/tek_deps.rs b/tek/deps.rs similarity index 58% rename from crates/app/tek_deps.rs rename to tek/deps.rs index 8084c703..12e55caa 100644 --- a/crates/app/tek_deps.rs +++ b/tek/deps.rs @@ -1,11 +1,11 @@ pub use ::{ - tek_engine::*, - tek_config::*, - tek_device::{self, *}, tengri::{ - Usually, Perhaps, Has, MaybeHas, has, maybe_has, impl_debug, - dsl::*, input::*, output::*, tui::*, + Usually, Perhaps, Has, MaybeHas, has, maybe_has, impl_debug, from, + dsl::*, + input::*, + output::{*, Layout}, tui::{ + *, ratatui::{ self, prelude::{Style, Stylize, Buffer, Modifier, buffer::Cell, Color::{self, *}} }, @@ -25,3 +25,12 @@ pub use ::{ }, xdg::BaseDirectories, }; + +pub(crate) use atomic_float::*; +pub(crate) use ratatui::{prelude::Rect, widgets::{Widget, canvas::{Canvas, Line}}}; +pub(crate) use std::cmp::Ord; +pub(crate) use std::ffi::OsString; +pub(crate) use std::fmt::{Debug, Formatter}; +pub(crate) use std::fs::File; +pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem}; +pub(crate) use std::thread::JoinHandle; diff --git a/tek/device.rs b/tek/device.rs new file mode 100644 index 00000000..d534b0e9 --- /dev/null +++ b/tek/device.rs @@ -0,0 +1,135 @@ +use crate::*; + +/// Define a type alias for iterators of sized items (columns). +macro_rules! def_sizes_iter { + ($Type:ident => $($Item:ty),+) => { + pub trait $Type<'a> = + Iterator + Send + Sync + 'a; + } +} + +#[cfg(feature = "arranger")] mod arranger; #[cfg(feature = "arranger")] pub use self::arranger::*; +#[cfg(feature = "browse")] mod browse; #[cfg(feature = "browse")] pub use self::browse::*; +#[cfg(feature = "clap")] mod clap; #[cfg(feature = "clap")] pub use self::clap::*; +#[cfg(feature = "clock")] mod clock; #[cfg(feature = "clock")] pub use self::clock::*; +#[cfg(feature = "editor")] mod editor; #[cfg(feature = "editor")] pub use self::editor::*; +#[cfg(feature = "lv2")] mod lv2; #[cfg(feature = "lv2")] pub use self::lv2::*; +#[cfg(feature = "meter")] mod meter; #[cfg(feature = "meter")] pub use self::meter::*; +#[cfg(feature = "mixer")] mod mixer; #[cfg(feature = "mixer")] pub use self::mixer::*; +#[cfg(feature = "pool")] mod pool; #[cfg(feature = "pool")] pub use self::pool::*; +#[cfg(feature = "port")] mod port; #[cfg(feature = "port")] pub use self::port::*; +#[cfg(feature = "sampler")] mod sampler; #[cfg(feature = "sampler")] pub use self::sampler::*; +#[cfg(feature = "sequencer")] mod sequencer; #[cfg(feature = "sequencer")] pub use self::sequencer::*; +#[cfg(feature = "sf2")] mod sf2; #[cfg(feature = "sf2")] pub use self::sf2::*; +#[cfg(feature = "vst2")] mod vst2; #[cfg(feature = "vst2")] pub use self::vst2::*; +#[cfg(feature = "vst3")] mod vst3; #[cfg(feature = "vst3")] pub use self::vst3::*; + +pub fn swap_value ( + target: &mut T, value: &T, returned: impl Fn(T)->U +) -> Perhaps { + if *target == *value { + Ok(None) + } else { + let mut value = value.clone(); + std::mem::swap(target, &mut value); + Ok(Some(returned(value))) + } +} + +pub fn toggle_bool ( + target: &mut bool, value: &Option, returned: impl Fn(Option)->U +) -> Perhaps { + let mut value = value.unwrap_or(!*target); + if value == *target { + Ok(None) + } else { + std::mem::swap(target, &mut value); + Ok(Some(returned(Some(value)))) + } +} + +pub fn device_kinds () -> &'static [&'static str] { + &[ + "Sampler", + "Plugin (LV2)", + ] +} + +impl> + Has> HasDevices for T { + fn devices (&self) -> &Vec { + self.get() + } + fn devices_mut (&mut self) -> &mut Vec { + self.get_mut() + } +} + +pub trait HasDevices: Has { + fn devices (&self) -> &Vec; + fn devices_mut (&mut self) -> &mut Vec; +} + +#[derive(Debug)] +pub enum Device { + #[cfg(feature = "sampler")] + Sampler(Sampler), + #[cfg(feature = "lv2")] // TODO + Lv2(Lv2), + #[cfg(feature = "vst2")] // TODO + Vst2, + #[cfg(feature = "vst3")] // TODO + Vst3, + #[cfg(feature = "clap")] // TODO + Clap, + #[cfg(feature = "sf2")] // TODO + Sf2, +} + +impl Device { + pub fn name (&self) -> &str { + match self { + Self::Sampler(sampler) => sampler.name.as_ref(), + _ => todo!(), + } + } + pub fn midi_ins (&self) -> &[MidiInput] { + match self { + //Self::Sampler(Sampler { midi_in, .. }) => &[midi_in], + _ => todo!() + } + } + pub fn midi_outs (&self) -> &[MidiOutput] { + match self { + Self::Sampler(_) => &[], + _ => todo!() + } + } + pub fn audio_ins (&self) -> &[AudioInput] { + match self { + Self::Sampler(Sampler { audio_ins, .. }) => audio_ins.as_slice(), + _ => todo!() + } + } + pub fn audio_outs (&self) -> &[AudioOutput] { + match self { + Self::Sampler(Sampler { audio_outs, .. }) => audio_outs.as_slice(), + _ => todo!() + } + } +} + +pub struct DeviceAudio<'a>(pub &'a mut Device); + +audio!(|self: DeviceAudio<'a>, client, scope|{ + use Device::*; + match self.0 { + #[cfg(feature = "sampler")] Sampler(sampler) => sampler.process(client, scope), + #[cfg(feature = "lv2")] Lv2(lv2) => lv2.process(client, scope), + #[cfg(feature = "vst2")] Vst2 => { todo!() }, // TODO + #[cfg(feature = "vst3")] Vst3 => { todo!() }, // TODO + #[cfg(feature = "clap")] Clap => { todo!() }, // TODO + #[cfg(feature = "sf2")] Sf2 => { todo!() }, // TODO + } +}); + +def_command!(DeviceCommand: |device: Device| {}); diff --git a/crates/device/src/arranger.rs b/tek/device/arranger.rs similarity index 96% rename from crates/device/src/arranger.rs rename to tek/device/arranger.rs index 56ac336b..0963df93 100644 --- a/crates/device/src/arranger.rs +++ b/tek/device/arranger.rs @@ -13,12 +13,6 @@ def_sizes_iter!(InputsSizes => MidiInput); def_sizes_iter!(OutputsSizes => MidiOutput); def_sizes_iter!(PortsSizes => Arc, [Connect]); -pub(crate) fn wrap (bg: Color, fg: Color, content: impl Content) -> impl Content { - let left = Tui::fg_bg(bg, Reset, Fixed::x(1, RepeatV("▐"))); - let right = Tui::fg_bg(bg, Reset, Fixed::x(1, RepeatV("▌"))); - Bsp::e(left, Bsp::w(right, Tui::fg_bg(fg, bg, content))) -} - #[derive(Default, Debug)] pub struct Arrangement { /// Project name. pub name: Arc, diff --git a/crates/device/src/arranger/arranger_api.rs b/tek/device/arranger/arranger_api.rs similarity index 100% rename from crates/device/src/arranger/arranger_api.rs rename to tek/device/arranger/arranger_api.rs diff --git a/crates/device/src/arranger/arranger_clip.rs b/tek/device/arranger/arranger_clip.rs similarity index 100% rename from crates/device/src/arranger/arranger_clip.rs rename to tek/device/arranger/arranger_clip.rs diff --git a/crates/device/src/arranger/arranger_scenes.rs b/tek/device/arranger/arranger_scenes.rs similarity index 100% rename from crates/device/src/arranger/arranger_scenes.rs rename to tek/device/arranger/arranger_scenes.rs diff --git a/crates/device/src/arranger/arranger_select.rs b/tek/device/arranger/arranger_select.rs similarity index 100% rename from crates/device/src/arranger/arranger_select.rs rename to tek/device/arranger/arranger_select.rs diff --git a/crates/device/src/arranger/arranger_tracks.rs b/tek/device/arranger/arranger_tracks.rs similarity index 85% rename from crates/device/src/arranger/arranger_tracks.rs rename to tek/device/arranger/arranger_tracks.rs index 364ffab8..3f002a11 100644 --- a/crates/device/src/arranger/arranger_tracks.rs +++ b/tek/device/arranger/arranger_tracks.rs @@ -241,34 +241,38 @@ impl Track { pub trait HasTrack { fn track (&self) -> Option<&Track>; fn track_mut (&mut self) -> Option<&mut Track>; - fn view_midi_ins_status (&self, theme: ItemTheme) -> impl Content { + fn view_midi_ins_status (&self, theme: ItemTheme) -> impl Draw { self.track().map(|track|{ let ins = track.sequencer.midi_ins.len() as u16; - Fixed::xy(20, 1 + ins, Outer(true, Style::default().fg(Tui::g(96))).enclose( + let border = Outer(true, Style::default().fg(Tui::g(96))); + Fixed::xy(20, 1 + ins, border.enclose( Fixed::xy(20, 1 + ins, FieldV(theme, format!("MIDI ins: "), Map::south(1, ||track.sequencer.midi_ins.iter(), |port, index|Fill::x(Align::w(format!(" {index} {}", port.port_name()))))))))}) } - fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Content { + fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Draw { self.track().map(|track|{ let outs = track.sequencer.midi_outs.len() as u16; - Fixed::xy(20, 1 + outs, Outer(true, Style::default().fg(Tui::g(96))).enclose( + let border = Outer(true, Style::default().fg(Tui::g(96))); + Fixed::xy(20, 1 + outs, border.enclose( Fixed::xy(20, 1 + outs, FieldV(theme, format!("MIDI outs: "), Map::south(1, ||track.sequencer.midi_outs.iter(), |port, index|Fill::x(Align::w(format!(" {index} {}", port.port_name()))))))))}) } - fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Content { + fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Draw { self.track().and_then(|track|track.devices.get(0)).map(|device|{ let ins = device.audio_ins().len() as u16; - Fixed::xy(20, 1 + ins, Outer(true, Style::default().fg(Tui::g(96))).enclose( + let border = Outer(true, Style::default().fg(Tui::g(96))); + Fixed::xy(20, 1 + ins, border.enclose( Fixed::xy(20, 1 + ins, FieldV(theme, format!("Audio ins: "), Map::south(1, ||device.audio_ins().iter(), |port, index|Fill::x(Align::w(format!(" {index} {}", port.port_name()))))))))}) } - fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Content { + fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Draw { self.track().and_then(|track|track.devices.last()).map(|device|{ let outs = device.audio_outs().len() as u16; - Fixed::xy(20, 1 + outs, Outer(true, Style::default().fg(Tui::g(96))).enclose( + let border = Outer(true, Style::default().fg(Tui::g(96))); + Fixed::xy(20, 1 + outs, border.enclose( Fixed::xy(20, 1 + outs, FieldV(theme, format!("Audio outs: "), Map::south(1, ||device.audio_outs().iter(), |port, index|Fill::x(Align::w(format!(" {index} {}", port.port_name()))))))))}) @@ -276,10 +280,10 @@ pub trait HasTrack { } impl Track { - pub fn per <'a, T: Content + 'a, U: TracksSizes<'a>> ( + pub fn per <'a, T: Draw + Layout + 'a, U: TracksSizes<'a>> ( tracks: impl Fn() -> U + Send + Sync + 'a, callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a - ) -> impl Content + 'a { + ) -> impl Draw + Layout + 'a { Map::new(tracks, move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ let width = (x2 - x1) as u16; @@ -290,10 +294,10 @@ impl Track { } } -pub(crate) fn per_track_top <'a, T: Content + 'a, U: TracksSizes<'a>> ( +pub(crate) fn per_track_top <'a, T: Draw + Layout + 'a, U: TracksSizes<'a>> ( tracks: impl Fn() -> U + Send + Sync + 'a, callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a -) -> impl Content + 'a { +) -> impl Draw + Layout + 'a { Align::x(Tui::bg(Reset, Map::new(tracks, move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ let width = (x2 - x1) as u16; @@ -303,37 +307,39 @@ pub(crate) fn per_track_top <'a, T: Content + 'a, U: TracksSizes<'a>> ( callback(index, track))))}))) } -pub(crate) fn per_track <'a, T: Content + 'a, U: TracksSizes<'a>> ( +pub(crate) fn per_track <'a, T: Draw + Layout + 'a, U: TracksSizes<'a>> ( tracks: impl Fn() -> U + Send + Sync + 'a, callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a -) -> impl Content + 'a { +) -> impl Draw + 'a { per_track_top(tracks, move|index, track|Fill::y(Align::y(callback(index, track)))) } pub(crate) fn io_ports <'a, T: PortsSizes<'a>> ( fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a -) -> impl Content + 'a { +) -> impl Draw + Layout + 'a { Map::new(iter, move|( _index, name, connections, y, y2 ): (usize, &'a Arc, &'a [Connect], usize, usize), _| map_south(y as u16, (y2-y) as u16, Bsp::s( - Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(Bsp::e(" 󰣲 ", name))))), + Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(Bsp::e(&" 󰣲 ", name))))), Map::new(||connections.iter(), move|connect: &'a Connect, index|map_south(index as u16, 1, Fill::x(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg, &connect.info))))))))) } -pub(crate) fn io_conns <'a, T: PortsSizes<'a>> ( - fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a -) -> impl Content + 'a { - Map::new(iter, move|( - _index, _name, connections, y, y2 - ): (usize, &'a Arc, &'a [Connect], usize, usize), _| - map_south(y as u16, (y2-y) as u16, Bsp::s( - Fill::x(Tui::bold(true, wrap(bg, fg, Fill::x(Align::w("▞▞▞▞ ▞▞▞▞"))))), - Map::new(||connections.iter(), move|_conn, index|map_south(index as u16, 1, - Fill::x(Align::w(Tui::bold(false, wrap(bg, fg, Fill::x("")))))))))) -} +//pub(crate) fn io_conns <'a, T: PortsSizes<'a>> ( + //fg: Color, bg: Color, iter: &mut impl Iterator, &'a [Connect], usize, usize)> +//) -> impl Draw + Layout + 'a { + //Fill::xy(Thunk::new(move|to: &mut TuiOut|for (_, _, connections, y, y2) in &mut *iter { + //to.place(&map_south(y as u16, (y2-y) as u16, Bsp::s( + //Fill::x(Tui::bold(true, wrap(bg, fg, Fill::x(Align::w(&"▞▞▞▞ ▞▞▞▞"))))), + //Thunk::new(|to: &mut TuiOut|for (index, _connection) in connections.iter().enumerate() { + //to.place(&map_south(index as u16, 1, Fill::x(Align::w(Tui::bold(false, + //wrap(bg, fg, Fill::x(&""))))))) + //}) + //))) + //})) +//} //track_scroll: Fill::x(Fixed::y(1, ScrollbarH { //offset: arrangement.track_scroll, //length: h_tracks_area as usize, diff --git a/tek/device/arranger/arranger_view.rs b/tek/device/arranger/arranger_view.rs new file mode 100644 index 00000000..dcc87a92 --- /dev/null +++ b/tek/device/arranger/arranger_view.rs @@ -0,0 +1,391 @@ +use crate::*; + +pub(crate) fn wrap ( + bg: Color, fg: Color, content: impl Draw + Layout +) -> impl Draw + Layout { + let left = Tui::fg_bg(bg, Reset, Fixed::x(1, RepeatV("▐"))); + let right = Tui::fg_bg(bg, Reset, Fixed::x(1, RepeatV("▌"))); + Bsp::e(left, Bsp::w(right, Tui::fg_bg(fg, bg, content))) +} + +type Add<'a> = &'a dyn FnMut(&dyn Draw); + +impl Arrangement { + + pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw + Layout + '_ { + Bsp::s(self.view_inputs_header(), Thunk::new(|to: &mut TuiOut|{ + for port in self.midi_ins().iter() { + to.place(&self.view_inputs_row(port)) + } + })) + } + + fn view_inputs_header (&self) -> impl Draw + Layout + '_ { + Fixed::y(1, Bsp::e( + Fixed::x(20, Align::w( + button_3("i", "nput ", format!("{}", self.midi_ins.len()), false))), + Bsp::w(Fixed::x(4, button_2("I", "+", false)), + Thunk::new(|to: &mut TuiOut|for (_index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&Tui::bg(track.color.dark.rgb, Align::w(Fixed::x(track.width as u16, row!( + Either::new(track.sequencer.monitoring, Tui::fg(Green, "mon "), "mon "), + Either::new(track.sequencer.recording, Tui::fg(Red, "rec "), "rec "), + Either::new(track.sequencer.overdub, Tui::fg(Yellow, "dub "), "dub "), + ))))) + })))) + } + + fn view_inputs_row (&self, port: &MidiInput) -> impl Draw + Layout { + Fixed::y(1, Bsp::e( + Fixed::x(20, Align::w( + Bsp::e(" ● ", Tui::bold(true, Tui::fg(Rgb(255,255,255), port.port_name()))))), + Bsp::w(Fixed::x(4, ()), Thunk::new(|to: &mut TuiOut|{ + for (_index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&Tui::bg(track.color.darker.rgb, Align::w(Fixed::x(track.width as u16, row!( + Either::new(track.sequencer.monitoring, Tui::fg(Green, " ● "), " · "), + Either::new(track.sequencer.recording, Tui::fg(Red, " ● "), " · "), + Either::new(track.sequencer.overdub, Tui::fg(Yellow, " ● "), " · "), + ))))) + } + })))) + } + + pub fn view_outputs (&self, theme: ItemTheme) -> impl Draw + Layout { + let mut h = 1; + for output in self.midi_outs().iter() { + h += 1 + output.connections.len(); + } + let h = h as u16; + let list = Bsp::s( + Fixed::y(1, Fill::x(Align::w(button_3("o", "utput", format!("{}", self.midi_outs.len()), false)))), + Fixed::y(h - 1, Fill::xy(Align::nw(Thunk::new(|to: &mut TuiOut|{ + for (_index, port) in self.midi_outs().iter().enumerate() { + to.place(&Fixed::y(1,Fill::x(Bsp::e( + Align::w(Bsp::e(" ● ", Tui::fg(Rgb(255,255,255),Tui::bold(true, port.port_name())))), + Fill::x(Align::e(format!("{}/{} ", + port.port().get_connections().len(), + port.connections.len()))))))); + for (index, conn) in port.connections.iter().enumerate() { + to.place(&Fixed::y(1, Fill::x(Align::w(format!(" c{index:02}{}", conn.info()))))); + } + } + }))))); + Fixed::y(h, view_track_row_section(theme, list, button_2("O", "+", false), + Tui::bg(theme.darker.rgb, Align::w(Fill::x( + Thunk::new(|to: &mut TuiOut|{ + for (index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&Fixed::x(self.track_width(index, track), + Thunk::new(|to: &mut TuiOut|{ + to.place(&Fixed::y(1, Align::w(Bsp::e( + Either::new(true, Tui::fg(Green, "play "), "play "), + Either::new(false, Tui::fg(Yellow, "solo "), "solo "), + )))); + for (_index, port) in self.midi_outs().iter().enumerate() { + to.place(&Fixed::y(1, Align::w(Bsp::e( + Either::new(true, Tui::fg(Green, " ● "), " · "), + Either::new(false, Tui::fg(Yellow, " ● "), " · "), + )))); + for (_index, _conn) in port.connections.iter().enumerate() { + to.place(&Fixed::y(1, Fill::x(""))); + } + }})))}})))))) + } + + pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw + Layout { + let mut h = 2u16; + for track in self.tracks().iter() { + h = h.max(track.devices.len() as u16 * 2); + } + view_track_row_section(theme, + button_3("d", "evice", format!("{}", self.track().map(|t|t.devices.len()).unwrap_or(0)), false), + button_2("D", "+", false), + Thunk::new(move|to: &mut TuiOut|for (index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&Fixed::xy(self.track_width(index, track), h + 1, + Tui::bg(track.color.dark.rgb, Align::nw(Map::south(2, move||0..h, + |_, _index|Fixed::xy(track.width as u16, 2, + Tui::fg_bg( + ItemTheme::G[32].lightest.rgb, + ItemTheme::G[32].dark.rgb, + Align::nw(format!(" · {}", "--"))))))))); + })) + } + +} + +fn view_track_row_section ( + _theme: ItemTheme, + button: impl Draw + Layout, + button_add: impl Draw + Layout, + content: impl Draw + Layout, +) -> impl Draw + Layout { + Bsp::w(Fill::y(Fixed::x(4, Align::nw(button_add))), + Bsp::e(Fixed::x(20, Fill::y(Align::nw(button))), Fill::xy(Align::c(content)))) +} + +pub trait HasClipsSize { + fn clips_size (&self) -> &Measure; +} +impl HasClipsSize for Arrangement { + fn clips_size (&self) -> &Measure { &self.inner_size } +} + +impl TracksView for Arrangement {} + +impl ClipsView for T {} + +pub trait TracksView: + ScenesView + + HasMidiIns + + HasMidiOuts + + HasSize + + HasTrackScroll + + HasSelection + + HasEditor + + HasClipsSize +{ + fn tracks_width_available (&self) -> u16 { + (self.width() as u16).saturating_sub(40) + } + /// Iterate over tracks with their corresponding sizes. + fn tracks_with_sizes (&self) -> impl TracksSizes<'_> { + let _editor_width = self.editor().map(|e|e.width()); + let _active_track = self.selection().track(); + let mut x = 0; + self.tracks().iter().enumerate().map_while(move |(index, track)|{ + let width = track.width.max(8); + if x + width < self.clips_size().w() { + let data = (index, track, x, x + width); + x += width + Self::TRACK_SPACING; + Some(data) + } else { + None + } + }) + } + fn view_track_header <'a> ( + &'a self, theme: ItemTheme, content: impl Draw + Layout + ) -> impl Draw + Layout { + Fixed::x(12, Tui::bg(theme.darker.rgb, Fill::x(Align::e(content)))) + } + fn view_track_names (&self, theme: ItemTheme) -> impl Draw + Layout { + let track_count = self.tracks().len(); + let scene_count = self.scenes().len(); + let selected = self.selection(); + let button = Bsp::s( + button_3("t", "rack ", format!("{}{track_count}", selected.track() + .map(|track|format!("{track}/")).unwrap_or_default()), false), + button_3("s", "cene ", format!("{}{scene_count}", selected.scene() + .map(|scene|format!("{scene}/")).unwrap_or_default()), false)); + let button_2 = Bsp::s( + button_2("T", "+", false), + button_2("S", "+", false)); + view_track_row_section(theme, button, button_2, Tui::bg(theme.darker.rgb, + Fixed::y(2, Thunk::new(|to: &mut TuiOut|{ + for (index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&Fixed::x(self.track_width(index, track), + Tui::bg(if selected.track() == Some(index) { + track.color.light.rgb + } else { + track.color.base.rgb + }, Bsp::s(Fill::x(Align::nw(Bsp::e( + format!("·t{index:02} "), + Tui::fg(Rgb(255, 255, 255), Tui::bold(true, &track.name)) + ))), ""))) );}})))) + } + fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw + Layout { + view_track_row_section(theme, + Bsp::s(Fill::x(Align::w(button_2("o", "utput", false))), + Thunk::new(|to: &mut TuiOut|for port in self.midi_outs().iter() { + to.place(&Fill::x(Align::w(port.port_name()))); + })), + button_2("O", "+", false), + Tui::bg(theme.darker.rgb, Align::w(Thunk::new(|to: &mut TuiOut|{ + for (index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&Fixed::x(self.track_width(index, track), + Align::nw(Fill::y(Map::south(1, ||track.sequencer.midi_outs.iter(), + |port, index|Tui::fg(Rgb(255, 255, 255), + Fixed::y(1, Tui::bg(track.color.dark.rgb, Fill::x(Align::w( + format!("·o{index:02} {}", port.port_name())))))))))));}})))) + } + fn view_track_inputs <'a> (&'a self, theme: ItemTheme) -> impl Draw + Layout { + let mut h = 0u16; + for track in self.tracks().iter() { + h = h.max(track.sequencer.midi_ins.len() as u16); + } + view_track_row_section(theme, + button_2("i", "nput", false), + button_2("I", "+", false), + Tui::bg(theme.darker.rgb, Align::w(Thunk::new(move|to: &mut TuiOut|{ + for (index, track, _x1, _x2) in self.tracks_with_sizes() { + to.place(&Fixed::xy(self.track_width(index, track), h + 1, + Align::nw(Bsp::s( + Tui::bg(track.color.base.rgb, + Fill::x(Align::w(row!( + Either::new(track.sequencer.monitoring, + Tui::fg(Green, "●mon "), "·mon "), + Either::new(track.sequencer.recording, + Tui::fg(Red, "●rec "), "·rec "), + Either::new(track.sequencer.overdub, + Tui::fg(Yellow, "●dub "), "·dub "), + )))), + Map::south(1, ||track.sequencer.midi_ins.iter(), + |port, index|Tui::fg_bg(Rgb(255, 255, 255), track.color.dark.rgb, + Fill::x(Align::w(format!("·i{index:02} {}", port.port_name()))))))))); + } + })))) + } + fn track_width (&self, index: usize, track: &Track) -> u16 { + track.width as u16 + } +} + +pub trait ScenesView: + HasEditor + + HasSelection + + HasSceneScroll + + HasClipsSize + + Send + + Sync +{ + /// Default scene height. + const H_SCENE: usize = 2; + /// Default editor height. + const H_EDITOR: usize = 15; + fn h_scenes (&self) -> u16; + fn w_side (&self) -> u16; + fn w_mid (&self) -> u16; + fn scenes_with_sizes (&self) -> impl ScenesSizes<'_> { + let mut y = 0; + self.scenes().iter().enumerate().skip(self.scene_scroll()).map_while(move|(s, scene)|{ + let height = if self.selection().scene() == Some(s) && self.editor().is_some() { + 8 + } else { + Self::H_SCENE + }; + if y + height <= self.clips_size().h() { + let data = (s, scene, y, y + height); + y += height; + Some(data) + } else { + None + } + }) + } + fn view_scenes_names (&self) -> impl Draw + Layout { + Fixed::x(20, Thunk::new(|to: &mut TuiOut|for (index, scene, ..) in self.scenes_with_sizes() { + to.place(&self.view_scene_name(index, scene)); + })) + } + fn view_scene_name <'a> (&'a self, index: usize, scene: &'a Scene) -> impl Draw + Layout + 'a { + let h = if self.selection().scene() == Some(index) && let Some(_editor) = self.editor() { + 7 + } else { + Self::H_SCENE as u16 + }; + let bg = if self.selection().scene() == Some(index) { + scene.color.light.rgb + } else { + scene.color.base.rgb + }; + let a = Fill::x(Align::w(Bsp::e(format!("·s{index:02} "), + Tui::fg(Tui::g(255), Tui::bold(true, &scene.name))))); + let b = When(self.selection().scene() == Some(index) && self.is_editing(), + Fill::xy(Align::nw(Bsp::s( + self.editor().as_ref().map(|e|e.clip_status()), + self.editor().as_ref().map(|e|e.edit_status()))))); + Fixed::xy(20, h, Tui::bg(bg, Align::nw(Bsp::s(a, b)))) + } +} + +pub trait ClipsView: + TracksView + + ScenesView + + HasClipsSize + + Send + + Sync +{ + fn view_scenes_clips <'a> (&'a self) + -> impl Draw + 'a + { + self.clips_size().of(Fill::xy(Bsp::a( + Fill::xy(Align::se(Tui::fg(Green, format!("{}x{}", self.clips_size().w(), self.clips_size().h())))), + Thunk::new(|to: &mut TuiOut|for ( + track_index, track, _, _ + ) in self.tracks_with_sizes() { + to.place(&Fixed::x(track.width as u16, + Fill::y(self.view_track_clips(track_index, track)))) + })))) + } + + fn view_track_clips <'a> (&'a self, track_index: usize, track: &'a Track) -> impl Draw + 'a { + Thunk::new(move|to: &mut TuiOut|for ( + scene_index, scene, .. + ) in self.scenes_with_sizes() { + let (name, theme): (Arc, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(track_index) { + let clip = clip.read().unwrap(); + (format!(" ⏹ {}", &clip.name).into(), clip.color) + } else { + (" ⏹ -- ".into(), ItemTheme::G[32]) + }; + let fg = theme.lightest.rgb; + let mut outline = theme.base.rgb; + let bg = if self.selection().track() == Some(track_index) + && self.selection().scene() == Some(scene_index) + { + outline = theme.lighter.rgb; + theme.light.rgb + } else if self.selection().track() == Some(track_index) + || self.selection().scene() == Some(scene_index) + { + outline = theme.darkest.rgb; + theme.base.rgb + } else { + theme.dark.rgb + }; + let w = if self.selection().track() == Some(track_index) + && let Some(editor) = self.editor () + { + editor.width().max(24).max(track.width) + } else { + track.width + } as u16; + let y = if self.selection().scene() == Some(scene_index) + && let Some(editor) = self.editor () + { + editor.height().max(12) + } else { + Self::H_SCENE + } as u16; + + to.place(&Fixed::xy(w, y, Bsp::b( + Fill::xy(Outer(true, Style::default().fg(outline))), + Fill::xy(Bsp::b( + Bsp::b( + Tui::fg_bg(outline, bg, Fill::xy("")), + Fill::xy(Align::nw(Tui::fg_bg(fg, bg, Tui::bold(true, name)))), + ), + Fill::xy(When(self.selection().track() == Some(track_index) + && self.selection().scene() == Some(scene_index) + && self.is_editing(), self.editor()))))))); + }) + } +} + +pub trait HasWidth { + const MIN_WIDTH: usize; + /// Increment track width. + fn width_inc (&mut self); + /// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH]. + fn width_dec (&mut self); +} + +impl HasWidth for Track { + const MIN_WIDTH: usize = 9; + fn width_inc (&mut self) { + self.width += 1; + } + fn width_dec (&mut self) { + if self.width > Track::MIN_WIDTH { + self.width -= 1; + } + } +} diff --git a/crates/device/src/browse.rs b/tek/device/browse.rs similarity index 100% rename from crates/device/src/browse.rs rename to tek/device/browse.rs diff --git a/crates/device/src/browse/browse_api.rs b/tek/device/browse/browse_api.rs similarity index 100% rename from crates/device/src/browse/browse_api.rs rename to tek/device/browse/browse_api.rs diff --git a/crates/device/src/browse/browse_view.rs b/tek/device/browse/browse_view.rs similarity index 100% rename from crates/device/src/browse/browse_view.rs rename to tek/device/browse/browse_view.rs diff --git a/crates/device/src/clap.rs b/tek/device/clap.rs similarity index 100% rename from crates/device/src/clap.rs rename to tek/device/clap.rs diff --git a/crates/device/src/clock.rs b/tek/device/clock.rs similarity index 100% rename from crates/device/src/clock.rs rename to tek/device/clock.rs diff --git a/crates/device/src/clock/clock_api.rs b/tek/device/clock/clock_api.rs similarity index 100% rename from crates/device/src/clock/clock_api.rs rename to tek/device/clock/clock_api.rs diff --git a/crates/device/src/clock/clock_model.rs b/tek/device/clock/clock_model.rs similarity index 96% rename from crates/device/src/clock/clock_model.rs rename to tek/device/clock/clock_model.rs index 0a881025..8496f93a 100644 --- a/crates/device/src/clock/clock_model.rs +++ b/tek/device/clock/clock_model.rs @@ -3,7 +3,7 @@ use crate::*; #[derive(Clone, Default)] pub struct Clock { /// JACK transport handle. - pub transport: Arc>, + pub transport: Arc>, /// Global temporal resolution (shared by [Moment] fields) pub timebase: Arc, /// Current global sample and usec (monotonic from JACK clock) @@ -130,7 +130,7 @@ impl Clock { self.set_chunk(scope.n_frames() as usize); // Store reported global frame and usec - let tek_engine::jack::CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?; + let CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?; self.global.sample.set(current_frames as f64); self.global.usec.set(current_usecs as f64); @@ -160,12 +160,12 @@ impl Clock { Ok(()) } - pub fn bbt (&self) -> tek_engine::jack::PositionBBT { + pub fn bbt (&self) -> PositionBBT { let pulse = self.playhead.pulse.get() as i32; let ppq = self.timebase.ppq.get() as i32; let bpm = self.timebase.bpm.get(); let bar = (pulse / ppq) / 4; - tek_engine::jack::PositionBBT { + PositionBBT { bar: 1 + bar, beat: 1 + (pulse / ppq) % 4, tick: (pulse % ppq), diff --git a/crates/device/src/clock/clock_view.rs b/tek/device/clock/clock_view.rs similarity index 92% rename from crates/device/src/clock/clock_view.rs rename to tek/device/clock/clock_view.rs index b839a3ca..287eec9f 100644 --- a/crates/device/src/clock/clock_view.rs +++ b/tek/device/clock/clock_view.rs @@ -6,7 +6,7 @@ pub fn view_transport ( bpm: Arc>, beat: Arc>, time: Arc>, -) -> impl Content { +) -> impl Draw + Layout { let theme = ItemTheme::G[96]; Tui::bg(Black, row!(Bsp::a( Fill::xy(Align::w(button_play_pause(play))), @@ -23,7 +23,7 @@ pub fn view_status ( sr: Arc>, buf: Arc>, lat: Arc>, -) -> impl Content { +) -> impl Draw + Layout { let theme = ItemTheme::G[96]; Tui::bg(Black, row!(Bsp::a( Fill::xy(Align::w(sel.map(|sel|FieldH(theme, "Selected", sel)))), @@ -35,18 +35,18 @@ pub fn view_status ( ))) } -pub(crate) fn button_play_pause (playing: bool) -> impl Content { +pub(crate) fn button_play_pause (playing: bool) -> impl Draw + Layout { let compact = true;//self.is_editing(); 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, + Thunk::new(move|to: &mut TuiOut|to.place(&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, + )), + Thunk::new(move|to: &mut TuiOut|to.place(&Fixed::x(5, Either::new(playing, Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",)))) - ) + )) ) ) } @@ -93,7 +93,7 @@ impl ViewCache { //} //pub fn scene_add (cache: &Arc>, scene: usize, scenes: usize, is_editing: bool) - //-> impl Content + //-> impl Draw //{ //let data = (scene, scenes); //cache.write().unwrap().scns.update(Some(data), rewrite!(buf, "({}/{})", data.0, data.1)); @@ -133,7 +133,7 @@ impl ViewCache { } } - //pub fn view_h2 (&self) -> impl Content { + //pub fn view_h2 (&self) -> impl Draw { //let cache = self.project.clock.view_cache.clone(); //let cache = cache.read().unwrap(); //add(&Fixed::x(15, Align::w(Bsp::s( diff --git a/crates/device/src/editor.rs b/tek/device/editor.rs similarity index 100% rename from crates/device/src/editor.rs rename to tek/device/editor.rs diff --git a/crates/device/src/editor/editor_api.rs b/tek/device/editor/editor_api.rs similarity index 100% rename from crates/device/src/editor/editor_api.rs rename to tek/device/editor/editor_api.rs diff --git a/crates/device/src/editor/editor_model.rs b/tek/device/editor/editor_model.rs similarity index 100% rename from crates/device/src/editor/editor_model.rs rename to tek/device/editor/editor_model.rs diff --git a/crates/device/src/editor/editor_view.rs b/tek/device/editor/editor_view.rs similarity index 89% rename from crates/device/src/editor/editor_view.rs rename to tek/device/editor/editor_view.rs index 0a11c72c..afcf904e 100644 --- a/crates/device/src/editor/editor_view.rs +++ b/tek/device/editor/editor_view.rs @@ -1,5 +1,7 @@ use crate::*; +tui_layout!(|self: MidiEditor, to|self.content().layout(to)); +tui_draw!(|self: MidiEditor, to|self.content().draw(to)); content!(TuiOut: |self: MidiEditor| { self.autoscroll(); //self.autozoom(); @@ -8,7 +10,7 @@ content!(TuiOut: |self: MidiEditor| { impl MidiEditor { - pub fn clip_status (&self) -> impl Content + '_ { + pub fn clip_status (&self) -> impl Draw + Layout + '_ { let (_color, name, length, looped) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { (clip.color, clip.name.clone(), clip.length, clip.looped) } else { (ItemTheme::G[64], String::new().into(), 0, false) }; @@ -25,7 +27,7 @@ impl MidiEditor { )) } - pub fn edit_status (&self) -> impl Content + '_ { + pub fn edit_status (&self) -> impl Draw + Layout + '_ { let (_color, length) = if let Some(clip) = self.clip().as_ref().map(|p|p.read().unwrap()) { (clip.color, clip.length) } else { (ItemTheme::G[64], 0) }; diff --git a/crates/device/src/editor/editor_view_h.rs b/tek/device/editor/editor_view_h.rs similarity index 95% rename from crates/device/src/editor/editor_view_h.rs rename to tek/device/editor/editor_view_h.rs index bf04f16f..e461e738 100644 --- a/crates/device/src/editor/editor_view_h.rs +++ b/tek/device/editor/editor_view_h.rs @@ -23,7 +23,7 @@ has!(Measure:|self:PianoHorizontal|self.size); impl PianoHorizontal { pub fn new (clip: Option<&Arc>>) -> Self { let size = Measure::new(); - let mut range = MidiRangeModel::from((12, true)); + let mut range = MidiRangeModel::from((12, true)); range.time_axis = size.x.clone(); range.note_axis = size.y.clone(); let piano = Self { @@ -46,6 +46,8 @@ pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) (note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n)) } +tui_layout!(|self: PianoHorizontal, to|self.content().layout(to)); +tui_draw!(|self: PianoHorizontal, to|self.content().draw(to)); content!(TuiOut:|self: PianoHorizontal| Bsp::s( Bsp::e( Fixed::x(5, format!("{}x{}", self.size.w(), self.size.h())), @@ -139,12 +141,12 @@ impl PianoHorizontal { } } - fn notes (&self) -> impl Content { + fn notes (&self) -> impl Draw { let time_start = self.get_time_start(); let note_lo = self.get_note_lo(); let note_hi = self.get_note_hi(); let buffer = self.buffer.clone(); - ThunkRender::new(move|to: &mut TuiOut|{ + Thunk::new(move|to: &mut TuiOut|{ let source = buffer.read().unwrap(); let [x0, y0, w, _h] = to.area().xywh(); //if h as usize != note_axis { @@ -170,7 +172,7 @@ impl PianoHorizontal { } }) } - fn cursor (&self) -> impl Content { + fn cursor (&self) -> impl Draw + Layout { let note_hi = self.get_note_hi(); let note_lo = self.get_note_lo(); let note_pos = self.get_note_pos(); @@ -179,7 +181,7 @@ impl PianoHorizontal { let time_start = self.get_time_start(); let time_zoom = self.get_time_zoom(); let style = Some(Style::default().fg(self.color.lightest.rgb)); - ThunkRender::new(move|to: &mut TuiOut|{ + Thunk::new(move|to: &mut TuiOut|{ let [x0, y0, w, _] = to.area().xywh(); for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { if note == note_pos { @@ -201,7 +203,7 @@ impl PianoHorizontal { } }) } - fn keys (&self) -> impl Content { + fn keys (&self) -> impl Draw + Layout { let state = self; let color = state.color; let note_lo = state.get_note_lo(); @@ -210,7 +212,7 @@ impl PianoHorizontal { let key_style = Some(Style::default().fg(Rgb(192, 192, 192)).bg(Rgb(0, 0, 0))); let off_style = Some(Style::default().fg(Tui::g(255))); let on_style = Some(Style::default().fg(Rgb(255,0,0)).bg(color.base.rgb).bold()); - Fill::y(Fixed::x(self.keys_width, ThunkRender::new(move|to: &mut TuiOut|{ + Fill::y(Fixed::x(self.keys_width, Thunk::new(move|to: &mut TuiOut|{ let [x, y0, _w, _h] = to.area().xywh(); for (_area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { to.blit(&to_key(note), x, screen_y, key_style); @@ -225,8 +227,8 @@ impl PianoHorizontal { } }))) } - fn timeline (&self) -> impl Content + '_ { - Fill::x(Fixed::y(1, ThunkRender::new(move|to: &mut TuiOut|{ + fn timeline (&self) -> impl Draw + Layout + '_ { + Fill::x(Fixed::y(1, Thunk::new(move|to: &mut TuiOut|{ let [x, y, w, _h] = to.area(); let style = Some(Style::default().dim()); let length = self.clip.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); diff --git a/crates/device/src/editor/editor_view_v.rs b/tek/device/editor/editor_view_v.rs similarity index 92% rename from crates/device/src/editor/editor_view_v.rs rename to tek/device/editor/editor_view_v.rs index 493c6de9..2913f04a 100644 --- a/crates/device/src/editor/editor_view_v.rs +++ b/tek/device/editor/editor_view_v.rs @@ -1,3 +1,5 @@ +//! TODO + use crate::*; pub struct OctaveVertical { @@ -24,7 +26,7 @@ impl OctaveVertical { } impl Content for OctaveVertical { - fn content (&self) -> impl Render { + fn content (&self) -> impl Draw + Layout + '_ { row!( Tui::fg_bg(self.color(0), self.color(1), "▙"), Tui::fg_bg(self.color(2), self.color(3), "▙"), diff --git a/crates/device/src/lib.rs b/tek/device/lib.rs similarity index 71% rename from crates/device/src/lib.rs rename to tek/device/lib.rs index 22edce2e..56df9abf 100644 --- a/crates/device/src/lib.rs +++ b/tek/device/lib.rs @@ -14,7 +14,7 @@ pub(crate) use std::error::Error; pub(crate) use std::ffi::OsString; pub(crate) use ::tengri::{from, has, maybe_has, Usually, Perhaps, Has, MaybeHas}; -pub(crate) use ::tengri::{dsl::*, input::*, output::*, tui::{*, ratatui::prelude::*}}; +pub(crate) use ::tengri::{dsl::*, input::*, output::{*, Layout}, tui::{*, ratatui::prelude::*}}; pub(crate) use ::tek_engine::*; pub(crate) use ::tek_engine::midi::{u7, LiveEvent, MidiMessage}; pub(crate) use ::tek_engine::jack::{Control, ProcessScope, RawMidi}; @@ -47,45 +47,6 @@ macro_rules! def_sizes_iter { #[cfg(feature = "vst2")] mod vst2; #[cfg(feature = "vst2")] pub use self::vst2::*; #[cfg(feature = "vst3")] mod vst3; #[cfg(feature = "vst3")] pub use self::vst3::*; -pub fn button_2 <'a> ( - key: impl Content + 'a, label: impl Content + 'a, editing: bool, -) -> impl Content + 'a { - let key = Tui::fg_bg(Tui::orange(), Tui::g(0), Bsp::e( - Tui::fg(Tui::g(0), "▐"), - Bsp::e(key, Tui::fg(Tui::g(96), "▐")) - )); - let label = When::new(!editing, Tui::fg_bg(Tui::g(255), Tui::g(96), label)); - Tui::bold(true, Bsp::e(key, label)) -} - -pub fn button_3 <'a, K, L, V> ( - key: K, - label: L, - value: V, - editing: bool, -) -> impl Content + 'a where - K: Content + 'a, - L: Content + 'a, - V: Content + 'a, -{ - let key = Tui::fg_bg(Tui::orange(), Tui::g(0), - Bsp::e(Tui::fg(Tui::g(0), "▐"), Bsp::e(key, Tui::fg(if editing { - Tui::g(128) - } else { - Tui::g(96) - }, "▐")))); - let label = Bsp::e( - When::new(!editing, Bsp::e( - Tui::fg_bg(Tui::g(255), Tui::g(96), label), - Tui::fg_bg(Tui::g(128), Tui::g(96), "▐"), - )), - Bsp::e( - Tui::fg_bg(Tui::g(224), Tui::g(128), value), - Tui::fg_bg(Tui::g(128), Reset, "▌"), - )); - Tui::bold(true, Bsp::e(key, label)) -} - pub fn swap_value ( target: &mut T, value: &T, returned: impl Fn(T)->U ) -> Perhaps { diff --git a/crates/device/src/lv2.rs b/tek/device/lv2.rs similarity index 100% rename from crates/device/src/lv2.rs rename to tek/device/lv2.rs diff --git a/crates/device/src/lv2/lv2_audio.rs b/tek/device/lv2/lv2_audio.rs similarity index 100% rename from crates/device/src/lv2/lv2_audio.rs rename to tek/device/lv2/lv2_audio.rs diff --git a/crates/device/src/lv2/lv2_gui.rs b/tek/device/lv2/lv2_gui.rs similarity index 100% rename from crates/device/src/lv2/lv2_gui.rs rename to tek/device/lv2/lv2_gui.rs diff --git a/crates/device/src/lv2/lv2_model.rs b/tek/device/lv2/lv2_model.rs similarity index 100% rename from crates/device/src/lv2/lv2_model.rs rename to tek/device/lv2/lv2_model.rs diff --git a/crates/device/src/lv2/lv2_tui.rs b/tek/device/lv2/lv2_tui.rs similarity index 98% rename from crates/device/src/lv2/lv2_tui.rs rename to tek/device/lv2/lv2_tui.rs index 0c76a1c9..8bfad8a2 100644 --- a/crates/device/src/lv2/lv2_tui.rs +++ b/tek/device/lv2/lv2_tui.rs @@ -1,8 +1,8 @@ use crate::*; use super::*; -impl Content for Lv2 { - fn render (&self, to: &mut TuiOut) { +impl Draw for Lv2 { + fn draw (&self, to: &mut TuiOut) { let area = to.area(); let [x, y, _, height] = area; let mut width = 20u16; diff --git a/crates/device/src/meter.rs b/tek/device/meter.rs similarity index 89% rename from crates/device/src/meter.rs rename to tek/device/meter.rs index 106975c7..48f2c774 100644 --- a/crates/device/src/meter.rs +++ b/tek/device/meter.rs @@ -9,8 +9,8 @@ pub enum MeteringMode { #[derive(Debug, Default, Clone)] pub struct Log10Meter(pub f32); - -render!(TuiOut: |self: Log10Meter, to| { +tui_layout!(|self: Log10Meter, to|to); +tui_draw!(|self: Log10Meter, to| { let [x, y, w, h] = to.area(); let signal = 100.0 - f32::max(0.0, f32::min(100.0, self.0.abs())); let v = (signal * h as f32 / 100.0).ceil() as u16; @@ -32,7 +32,7 @@ pub fn to_log10 (samples: &[f32]) -> f32 { #[derive(Debug, Default, Clone)] pub struct RmsMeter(pub f32); -render!(TuiOut: |self: RmsMeter, to| { +draw!(TuiOut: |self: RmsMeter, to| { let [x, y, w, h] = to.area(); let signal = f32::max(0.0, f32::min(100.0, self.0.abs())); let v = (signal * h as f32).ceil() as u16; @@ -53,7 +53,7 @@ pub fn to_rms (samples: &[f32]) -> f32 { (sum / samples.len() as f32).sqrt() } -pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Content + 'a { +pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Draw + 'a { col!( FieldH(ItemTheme::G[128], label, format!("{:>+9.3}", value)), Fixed::xy(if value >= 0.0 { 13 } @@ -74,7 +74,7 @@ pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Content + 'a else { Green }, ()))) } -pub fn view_meters (values: &[f32;2]) -> impl Content + use<'_> { +pub fn view_meters (values: &[f32;2]) -> impl Draw + use<'_> { let left = format!("L/{:>+9.3}", values[0]); let right = format!("R/{:>+9.3}", values[1]); Bsp::s(left, right) diff --git a/crates/device/src/mixer.rs b/tek/device/mixer.rs similarity index 100% rename from crates/device/src/mixer.rs rename to tek/device/mixer.rs diff --git a/crates/device/src/pool.rs b/tek/device/pool.rs similarity index 100% rename from crates/device/src/pool.rs rename to tek/device/pool.rs diff --git a/crates/device/src/pool/pool_api.rs b/tek/device/pool/pool_api.rs similarity index 100% rename from crates/device/src/pool/pool_api.rs rename to tek/device/pool/pool_api.rs diff --git a/crates/device/src/pool/pool_view.rs b/tek/device/pool/pool_view.rs similarity index 100% rename from crates/device/src/pool/pool_view.rs rename to tek/device/pool/pool_view.rs diff --git a/crates/device/src/port.rs b/tek/device/port.rs similarity index 96% rename from crates/device/src/port.rs rename to tek/device/port.rs index edfae329..d25ca0c0 100644 --- a/crates/device/src/port.rs +++ b/tek/device/port.rs @@ -2,8 +2,8 @@ use crate::*; mod port_api; pub use self::port_api::*; mod port_connect; pub use self::port_connect::*; -mod port_audio_out; pub use self::port_audio_out::*; -mod port_audio_in; pub use self::port_audio_in::*; +mod port_audio_out; //pub use self::port_audio_out::*; +mod port_audio_in; //pub use self::port_audio_in::*; mod port_midi_out; pub use self::port_midi_out::*; mod port_midi_in; pub use self::port_midi_in::*; pub(crate) use ConnectName::*; diff --git a/crates/device/src/port/port_api.rs b/tek/device/port/port_api.rs similarity index 100% rename from crates/device/src/port/port_api.rs rename to tek/device/port/port_api.rs diff --git a/crates/device/src/port/port_audio_in.rs b/tek/device/port/port_audio_in.rs similarity index 100% rename from crates/device/src/port/port_audio_in.rs rename to tek/device/port/port_audio_in.rs diff --git a/crates/device/src/port/port_audio_out.rs b/tek/device/port/port_audio_out.rs similarity index 100% rename from crates/device/src/port/port_audio_out.rs rename to tek/device/port/port_audio_out.rs diff --git a/crates/device/src/port/port_connect.rs b/tek/device/port/port_connect.rs similarity index 100% rename from crates/device/src/port/port_connect.rs rename to tek/device/port/port_connect.rs diff --git a/crates/device/src/port/port_midi_in.rs b/tek/device/port/port_midi_in.rs similarity index 100% rename from crates/device/src/port/port_midi_in.rs rename to tek/device/port/port_midi_in.rs diff --git a/crates/device/src/port/port_midi_out.rs b/tek/device/port/port_midi_out.rs similarity index 100% rename from crates/device/src/port/port_midi_out.rs rename to tek/device/port/port_midi_out.rs diff --git a/crates/device/src/sampler.rs b/tek/device/sampler.rs similarity index 100% rename from crates/device/src/sampler.rs rename to tek/device/sampler.rs diff --git a/crates/device/src/sampler/sampler_api.rs b/tek/device/sampler/sampler_api.rs similarity index 100% rename from crates/device/src/sampler/sampler_api.rs rename to tek/device/sampler/sampler_api.rs diff --git a/crates/device/src/sampler/sampler_audio.rs b/tek/device/sampler/sampler_audio.rs similarity index 100% rename from crates/device/src/sampler/sampler_audio.rs rename to tek/device/sampler/sampler_audio.rs diff --git a/crates/device/src/sampler/sampler_browse.rs b/tek/device/sampler/sampler_browse.rs similarity index 98% rename from crates/device/src/sampler/sampler_browse.rs rename to tek/device/sampler/sampler_browse.rs index f239767c..9d9bf491 100644 --- a/crates/device/src/sampler/sampler_browse.rs +++ b/tek/device/sampler/sampler_browse.rs @@ -143,8 +143,8 @@ fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { Ok((subdirs, files)) } -impl Content for AddSampleModal { - fn render (&self, _to: &mut TuiOut) { +impl Draw for AddSampleModal { + fn draw (&self, _to: &mut TuiOut) { todo!() //let area = to.area(); //to.make_dim(); diff --git a/crates/device/src/sampler/sampler_data.rs b/tek/device/sampler/sampler_data.rs similarity index 97% rename from crates/device/src/sampler/sampler_data.rs rename to tek/device/sampler/sampler_data.rs index b4834174..2bca4558 100644 --- a/crates/device/src/sampler/sampler_data.rs +++ b/tek/device/sampler/sampler_data.rs @@ -58,7 +58,7 @@ impl Sample { // Decode a packet let decoded = decoder .decode(&packet) - .map_err(|e|Box::::from(e))?; + .map_err(|e|Box::::from(e))?; // Determine sample rate let spec = *decoded.spec(); if let Some(rate) = self.rate { diff --git a/crates/device/src/sampler/sampler_midi.rs b/tek/device/sampler/sampler_midi.rs similarity index 100% rename from crates/device/src/sampler/sampler_midi.rs rename to tek/device/sampler/sampler_midi.rs diff --git a/crates/device/src/sampler/sampler_view.rs b/tek/device/sampler/sampler_view.rs similarity index 83% rename from crates/device/src/sampler/sampler_view.rs rename to tek/device/sampler/sampler_view.rs index 90999e2c..20c4ab13 100644 --- a/crates/device/src/sampler/sampler_view.rs +++ b/tek/device/sampler/sampler_view.rs @@ -2,28 +2,31 @@ use crate::*; impl Sampler { - pub fn view_grid (&self) -> impl Content + use<'_> { + pub fn view_grid (&self) -> impl Draw + Layout + use<'_> { let cells_x = 8u16; let cells_y = 8u16; let cell_width = 10u16; let cell_height = 2u16; //let width = cells_x * cell_width; //let height = cells_y * cell_height; - let cols = Map::east( - cell_width, - move||0..cells_x, - move|x, _|Map::south( - cell_height, - move||0..cells_y, - move|y, _|self.view_grid_cell("........", x, y, cell_width, cell_height) - ) - ); - cols + //let cols = Map::east( + //cell_width, + //move||0..cells_x, + //move|x, _|Map::south( + //cell_height, + //move||0..cells_y, + //move|y, _|self.view_grid_cell("........", x, y, cell_width, cell_height) + //) + //); + //cols + //Thunk::new(|to: &mut TuiOut|{ + //}) + "TODO" } pub fn view_grid_cell <'a> ( &'a self, name: &'a str, x: u16, y: u16, w: u16, h: u16 - ) -> impl Content + use<'a> { + ) -> impl Draw + use<'a> { let cursor = self.cursor(); let hi_fg = Color::Rgb(64, 64, 64); let hi_bg = if y == 0 { Color::Reset } else { Color::Rgb(64, 64, 64) /*prev*/ }; @@ -55,7 +58,7 @@ impl Sampler { pub fn view_list <'a, T: NotePoint + NoteRange> ( &'a self, compact: bool, editor: &T - ) -> impl Content + 'a { + ) -> impl Draw + 'a { let note_lo = editor.get_note_lo(); let note_pt = editor.get_note_pos(); let note_hi = editor.get_note_hi(); @@ -97,7 +100,7 @@ impl Sampler { } } - pub fn view_sample (&self, note_pt: usize) -> impl Content + use<'_> { + pub fn view_sample (&self, note_pt: usize) -> impl Draw + use<'_> { Outer(true, Style::default().fg(Tui::g(96))) .enclose(Fill::xy(draw_viewer(if let Some((_, Some(sample))) = &self.recording { Some(sample) @@ -108,7 +111,7 @@ impl Sampler { }))) } - pub fn view_sample_info (&self, note_pt: usize) -> impl Content + use<'_> { + pub fn view_sample_info (&self, note_pt: usize) -> impl Draw + use<'_> { Fill::x(Fixed::y(1, draw_info(if let Some((_, Some(sample))) = &self.recording { Some(sample) } else if let Some(sample) = &self.mapped[note_pt] { @@ -118,7 +121,7 @@ impl Sampler { }))) } - pub fn view_sample_status (&self, note_pt: usize) -> impl Content + use<'_> { + pub fn view_sample_status (&self, note_pt: usize) -> impl Draw + use<'_> { Fixed::x(20, draw_info_v(if let Some((_, Some(sample))) = &self.recording { Some(sample) } else if let Some(sample) = &self.mapped[note_pt] { @@ -128,20 +131,20 @@ impl Sampler { })) } - pub fn view_status (&self, index: usize) -> impl Content { + pub fn view_status (&self, index: usize) -> impl Draw { draw_status(self.mapped[index].as_ref()) } - pub fn view_meters_input (&self) -> impl Content + use<'_> { + pub fn view_meters_input (&self) -> impl Draw + use<'_> { draw_meters(&self.input_meters) } - pub fn view_meters_output (&self) -> impl Content + use<'_> { + pub fn view_meters_output (&self) -> impl Draw + use<'_> { draw_meters(&self.output_meters) } } -fn draw_meters (meters: &[f32]) -> impl Content + use<'_> { +fn draw_meters (meters: &[f32]) -> impl Draw + use<'_> { Tui::bg(Black, Fixed::x(2, Map::east(1, ||meters.iter(), |value, _index|{ Fill::y(RmsMeter(*value)) }))) @@ -163,9 +166,9 @@ fn draw_list_item (sample: &Option>>) -> String { } } -fn draw_viewer (sample: Option<&Arc>>) -> impl Content + use<'_> { +fn draw_viewer (sample: Option<&Arc>>) -> impl Draw + Layout + use<'_> { let min_db = -64.0; - ThunkRender::new(move|to: &mut TuiOut|{ + Thunk::new(move|to: &mut TuiOut|{ let [x, y, width, height] = to.area(); let area = Rect { x, y, width, height }; if let Some(sample) = &sample { @@ -218,41 +221,41 @@ fn draw_viewer (sample: Option<&Arc>>) -> impl Content + }) } -fn draw_info (sample: Option<&Arc>>) -> impl Content + use<'_> { - When(sample.is_some(), Thunk::new(move||{ +fn draw_info (sample: Option<&Arc>>) -> impl Draw + Layout + use<'_> { + When(sample.is_some(), Thunk::new(move|to: &mut TuiOut|{ let sample = sample.unwrap().read().unwrap(); let theme = sample.color; - row!( + to.place(&row!( FieldH(theme, "Name", format!("{:<10}", sample.name.clone())), FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())), FieldH(theme, "Start", format!("{:<8}", sample.start)), FieldH(theme, "End", format!("{:<8}", sample.end)), FieldH(theme, "Trans", "0"), FieldH(theme, "Gain", format!("{}", sample.gain)), - ) + )) })) } -fn draw_info_v (sample: Option<&Arc>>) -> impl Content + use<'_> { - Either(sample.is_some(), Thunk::new(move||{ +fn draw_info_v (sample: Option<&Arc>>) -> impl Draw + Layout + use<'_> { + Either::new(sample.is_some(), Thunk::new(move|to: &mut TuiOut|{ let sample = sample.unwrap().read().unwrap(); let theme = sample.color; - Fixed::x(20, col!( + to.place(&Fixed::x(20, col!( Fill::x(Align::w(FieldH(theme, "Name ", format!("{:<10}", sample.name.clone())))), Fill::x(Align::w(FieldH(theme, "Length", format!("{:<8}", sample.channels[0].len())))), Fill::x(Align::w(FieldH(theme, "Start ", format!("{:<8}", sample.start)))), Fill::x(Align::w(FieldH(theme, "End ", format!("{:<8}", sample.end)))), Fill::x(Align::w(FieldH(theme, "Trans ", "0"))), Fill::x(Align::w(FieldH(theme, "Gain ", format!("{}", sample.gain)))), - )) - }), Thunk::new(move||Tui::fg(Red, col!( + ))) + }), Thunk::new(|to: &mut TuiOut|to.place(&Tui::fg(Red, col!( Tui::bold(true, "× No sample."), "[r] record", "[Shift-F9] import", - )))) + ))))) } -fn draw_status (sample: Option<&Arc>>) -> impl Content { +fn draw_status (sample: Option<&Arc>>) -> impl Draw + Layout { Tui::bold(true, Tui::fg(Tui::g(224), sample .map(|sample|{ let sample = sample.read().unwrap(); diff --git a/crates/device/src/sequencer.rs b/tek/device/sequencer.rs similarity index 100% rename from crates/device/src/sequencer.rs rename to tek/device/sequencer.rs diff --git a/crates/device/src/sequencer/seq_audio.rs b/tek/device/sequencer/seq_audio.rs similarity index 100% rename from crates/device/src/sequencer/seq_audio.rs rename to tek/device/sequencer/seq_audio.rs diff --git a/crates/device/src/sequencer/seq_clip.rs b/tek/device/sequencer/seq_clip.rs similarity index 100% rename from crates/device/src/sequencer/seq_clip.rs rename to tek/device/sequencer/seq_clip.rs diff --git a/crates/device/src/sequencer/seq_launch.rs b/tek/device/sequencer/seq_launch.rs similarity index 97% rename from crates/device/src/sequencer/seq_launch.rs rename to tek/device/sequencer/seq_launch.rs index 48d30f1c..fb8a0524 100644 --- a/crates/device/src/sequencer/seq_launch.rs +++ b/tek/device/sequencer/seq_launch.rs @@ -38,7 +38,7 @@ pub trait HasPlayClip: HasClock { *self.reset_mut() = true; } - fn play_status (&self) -> impl Content { + fn play_status (&self) -> impl Draw { let (name, color): (Arc, ItemTheme) = if let Some((_, Some(clip))) = self.play_clip() { let MidiClip { ref name, color, .. } = *clip.read().unwrap(); (name.clone(), color) @@ -51,7 +51,7 @@ pub trait HasPlayClip: HasClock { FieldV(color, "Now:", format!("{} {}", time, name)) } - fn next_status (&self) -> impl Content { + fn next_status (&self) -> impl Draw { let mut time: Arc = String::from("--.-.--").into(); let mut name: Arc = String::from("").into(); let mut color = ItemTheme::G[64]; diff --git a/crates/device/src/sequencer/seq_model.rs b/tek/device/sequencer/seq_model.rs similarity index 100% rename from crates/device/src/sequencer/seq_model.rs rename to tek/device/sequencer/seq_model.rs diff --git a/crates/device/src/sequencer/seq_view.rs b/tek/device/sequencer/seq_view.rs similarity index 100% rename from crates/device/src/sequencer/seq_view.rs rename to tek/device/sequencer/seq_view.rs diff --git a/crates/device/src/sf2.rs b/tek/device/sf2.rs similarity index 100% rename from crates/device/src/sf2.rs rename to tek/device/sf2.rs diff --git a/crates/device/src/vst2.rs b/tek/device/vst2.rs similarity index 100% rename from crates/device/src/vst2.rs rename to tek/device/vst2.rs diff --git a/crates/device/src/vst3.rs b/tek/device/vst3.rs similarity index 100% rename from crates/device/src/vst3.rs rename to tek/device/vst3.rs diff --git a/crates/engine/src/lib.rs b/tek/engine.rs similarity index 89% rename from crates/engine/src/lib.rs rename to tek/engine.rs index e3ff52aa..460b33b5 100644 --- a/crates/engine/src/lib.rs +++ b/tek/engine.rs @@ -1,4 +1,75 @@ -#![feature(type_alias_impl_trait)] +use crate::*; + +mod time; pub use self::time::*; +mod note; pub use self::note::*; +mod jack; pub use self::jack::*; +mod midi; pub use self::midi::*; + +//pub trait MaybeHas: Send + Sync { + //fn get (&self) -> Option<&T>; +//} + +//impl>> MaybeHas for U { + //fn get (&self) -> Option<&T> { + //Has::>::get(self).as_ref() + //} +//} + +pub trait HasN: Send + Sync { + fn get_nth (&self, key: usize) -> &T; + fn get_nth_mut (&mut self, key: usize) -> &mut T; +} + +pub trait Gettable { + /// Returns current value + fn get (&self) -> T; +} + +pub trait Mutable: Gettable { + /// Sets new value, returns old + fn set (&mut self, value: T) -> T; +} + +pub trait InteriorMutable: Gettable { + /// Sets new value, returns old + fn set (&self, value: T) -> T; +} + +impl Gettable for AtomicBool { + fn get (&self) -> bool { self.load(Relaxed) } +} + +impl InteriorMutable for AtomicBool { + fn set (&self, value: bool) -> bool { self.swap(value, Relaxed) } +} + +impl Gettable for AtomicUsize { + fn get (&self) -> usize { self.load(Relaxed) } +} + +impl InteriorMutable for AtomicUsize { + fn set (&self, value: usize) -> usize { self.swap(value, Relaxed) } +} + +#[cfg(test)] #[test] fn test_time () -> Usually<()> { + // TODO! + Ok(()) +} + +#[cfg(test)] #[test] fn test_midi_range () { + let model = MidiRangeModel::from((1, false)); + + let _ = model.get_time_len(); + let _ = model.get_time_zoom(); + let _ = model.get_time_lock(); + let _ = model.get_time_start(); + let _ = model.get_time_axis(); + let _ = model.get_time_end(); + + let _ = model.get_note_lo(); + let _ = model.get_note_axis(); + let _ = model.get_note_hi(); +} //macro_rules! impl_port { //($Name:ident : $Spec:ident -> $Pair:ident |$jack:ident, $name:ident|$port:expr) => { @@ -83,88 +154,3 @@ //} //}; //} - -mod time; pub use self::time::*; -mod note; pub use self::note::*; -pub mod jack; pub use self::jack::*; -pub mod midi; pub use self::midi::*; -pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, AtomicBool, Ordering::Relaxed}}; -pub(crate) use std::fmt::Debug; -pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem}; -pub(crate) use ::tengri::{from, Usually}; - -pub use ::atomic_float; pub(crate) use atomic_float::*; - -//pub trait MaybeHas: Send + Sync { - //fn get (&self) -> Option<&T>; -//} - -//impl>> MaybeHas for U { - //fn get (&self) -> Option<&T> { - //Has::>::get(self).as_ref() - //} -//} - -#[macro_export] macro_rules! as_ref { - ($T:ty: |$self:ident : $S:ty| $x:expr) => { - impl AsRef<$T> for $S { - fn as_ref (&$self) -> &$T { &$x } - } - }; -} - -pub trait HasN: Send + Sync { - fn get_nth (&self, key: usize) -> &T; - fn get_nth_mut (&mut self, key: usize) -> &mut T; -} - -pub trait Gettable { - /// Returns current value - fn get (&self) -> T; -} - -pub trait Mutable: Gettable { - /// Sets new value, returns old - fn set (&mut self, value: T) -> T; -} - -pub trait InteriorMutable: Gettable { - /// Sets new value, returns old - fn set (&self, value: T) -> T; -} - -impl Gettable for AtomicBool { - fn get (&self) -> bool { self.load(Relaxed) } -} - -impl InteriorMutable for AtomicBool { - fn set (&self, value: bool) -> bool { self.swap(value, Relaxed) } -} - -impl Gettable for AtomicUsize { - fn get (&self) -> usize { self.load(Relaxed) } -} - -impl InteriorMutable for AtomicUsize { - fn set (&self, value: usize) -> usize { self.swap(value, Relaxed) } -} - -#[cfg(test)] #[test] fn test_time () -> Usually<()> { - // TODO! - Ok(()) -} - -#[cfg(test)] #[test] fn test_midi_range () { - let model = MidiRangeModel::from((1, false)); - - let _ = model.get_time_len(); - let _ = model.get_time_zoom(); - let _ = model.get_time_lock(); - let _ = model.get_time_start(); - let _ = model.get_time_axis(); - let _ = model.get_time_end(); - - let _ = model.get_note_lo(); - let _ = model.get_note_axis(); - let _ = model.get_note_hi(); -} diff --git a/crates/engine/src/jack.rs b/tek/engine/jack.rs similarity index 100% rename from crates/engine/src/jack.rs rename to tek/engine/jack.rs diff --git a/crates/engine/src/midi.rs b/tek/engine/midi.rs similarity index 100% rename from crates/engine/src/midi.rs rename to tek/engine/midi.rs diff --git a/crates/engine/src/note.rs b/tek/engine/note.rs similarity index 100% rename from crates/engine/src/note.rs rename to tek/engine/note.rs diff --git a/crates/engine/src/note/note_pitch.rs b/tek/engine/note/note_pitch.rs similarity index 100% rename from crates/engine/src/note/note_pitch.rs rename to tek/engine/note/note_pitch.rs diff --git a/crates/engine/src/note/note_point.rs b/tek/engine/note/note_point.rs similarity index 100% rename from crates/engine/src/note/note_point.rs rename to tek/engine/note/note_point.rs diff --git a/crates/engine/src/note/note_range.rs b/tek/engine/note/note_range.rs similarity index 100% rename from crates/engine/src/note/note_range.rs rename to tek/engine/note/note_range.rs diff --git a/crates/engine/src/time.rs b/tek/engine/time.rs similarity index 100% rename from crates/engine/src/time.rs rename to tek/engine/time.rs diff --git a/crates/engine/src/time/time_moment.rs b/tek/engine/time/time_moment.rs similarity index 100% rename from crates/engine/src/time/time_moment.rs rename to tek/engine/time/time_moment.rs diff --git a/crates/engine/src/time/time_note.rs b/tek/engine/time/time_note.rs similarity index 100% rename from crates/engine/src/time/time_note.rs rename to tek/engine/time/time_note.rs diff --git a/crates/engine/src/time/time_perf.rs b/tek/engine/time/time_perf.rs similarity index 100% rename from crates/engine/src/time/time_perf.rs rename to tek/engine/time/time_perf.rs diff --git a/crates/engine/src/time/time_pulse.rs b/tek/engine/time/time_pulse.rs similarity index 100% rename from crates/engine/src/time/time_pulse.rs rename to tek/engine/time/time_pulse.rs diff --git a/crates/engine/src/time/time_sample_count.rs b/tek/engine/time/time_sample_count.rs similarity index 100% rename from crates/engine/src/time/time_sample_count.rs rename to tek/engine/time/time_sample_count.rs diff --git a/crates/engine/src/time/time_sample_rate.rs b/tek/engine/time/time_sample_rate.rs similarity index 100% rename from crates/engine/src/time/time_sample_rate.rs rename to tek/engine/time/time_sample_rate.rs diff --git a/crates/engine/src/time/time_timebase.rs b/tek/engine/time/time_timebase.rs similarity index 100% rename from crates/engine/src/time/time_timebase.rs rename to tek/engine/time/time_timebase.rs diff --git a/crates/engine/src/time/time_unit.rs b/tek/engine/time/time_unit.rs similarity index 100% rename from crates/engine/src/time/time_unit.rs rename to tek/engine/time/time_unit.rs diff --git a/crates/engine/src/time/time_usec.rs b/tek/engine/time/time_usec.rs similarity index 100% rename from crates/engine/src/time/time_usec.rs rename to tek/engine/time/time_usec.rs diff --git a/tek.edn b/tek/tek.edn similarity index 99% rename from tek.edn rename to tek/tek.edn index 8f4c82d0..30849ccb 100644 --- a/tek.edn +++ b/tek/tek.edn @@ -32,7 +32,7 @@ (mode :menu (keys :axis/y :confirm) :menu) (keys :confirm (@enter confirm)) -(view :menu (bg (g 40) (bsp/s +(view :menu (bg (g 0) (bsp/s :ports/out (bsp/n :ports/in (bg (g 30) (bsp/s (fixed/y 7 :logo) (fill/xy :dialog/menu))))))) (view :ports/out (fill/x (fixed/y 3 (bsp/a diff --git a/tek/tek.rs b/tek/tek.rs new file mode 100644 index 00000000..7ff10600 --- /dev/null +++ b/tek/tek.rs @@ -0,0 +1,878 @@ +#![feature( + adt_const_params, + associated_type_defaults, + closure_lifetime_binder, + if_let_guard, + impl_trait_in_assoc_type, + trait_alias, + type_alias_impl_trait, + type_changing_struct_update, +)] + +#![allow( + clippy::unit_arg +)] + +mod deps; pub use self::deps::*; + +mod config; pub use self::config::*; +mod device; pub use self::device::*; +mod engine; pub use self::engine::*; + +mod tek_bind; pub use self::tek_bind::*; +mod tek_menu; pub use self::tek_menu::*; + +#[cfg(test)] mod test; + +/// Total state +#[derive(Default, Debug)] +pub struct App { + /// Base color. + pub color: ItemTheme, + /// Must not be dropped for the duration of the process + pub jack: Jack<'static>, + /// Display size + pub size: Measure, + /// Performance counter + pub perf: PerfModel, + /// Available view modes and input bindings + pub config: Config, + /// Currently selected mode + pub mode: Arc>>, + /// Undo history + pub history: Vec<(AppCommand, Option)>, + /// Dialog overlay + pub dialog: Dialog, + /// Contains all recently created clips. + pub pool: Pool, + /// Contains the currently edited musical arrangement + pub project: Arrangement, +} + +audio!( + |self: App, client, scope|{ + let t0 = self.perf.get_t0(); + self.clock().update_from_scope(scope).unwrap(); + let midi_in = self.project.midi_input_collect(scope); + if let Some(editor) = &self.editor() { + let mut pitch: Option = None; + for port in midi_in.iter() { + for event in port.iter() { + if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..})) + = event + { + pitch = Some(key.clone()); + } + } + } + if let Some(pitch) = pitch { + editor.set_note_pos(pitch.as_int() as usize); + } + } + let result = self.project.process_tracks(client, scope); + self.perf.update_from_jack_scope(t0, scope); + result + }; + |self, event|{ + use JackEvent::*; + match event { + SampleRate(sr) => { + self.clock().timebase.sr.set(sr as f64); + }, + PortRegistration(id, true) => { + //let port = self.jack().port_by_id(id); + //println!("\rport add: {id} {port:?}"); + //println!("\rport add: {id}"); + }, + PortRegistration(id, false) => { + /*println!("\rport del: {id}")*/ + }, + PortsConnected(a, b, true) => { /*println!("\rport conn: {a} {b}")*/ }, + PortsConnected(a, b, false) => { /*println!("\rport disc: {a} {b}")*/ }, + ClientRegistration(id, true) => {}, + ClientRegistration(id, false) => {}, + ThreadInit => {}, + XRun => {}, + GraphReorder => {}, + _ => { panic!("{event:?}"); } + } + } +); + +dsl_ns!(App: Arc { + literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); +}); + +dsl_ns!(App: ItemTheme {}); + +dsl_ns!(App: bool { + word = |app| { + ":mode/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/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(..))), + ":focused/clip" => !app.editor_focused() && matches!(app.selection(), + Selection::TrackClip{..}), + ":focused/track" => !app.editor_focused() && matches!(app.selection(), + Selection::Track(..)), + ":focused/scene" => !app.editor_focused() && matches!(app.selection(), + Selection::Scene(..)), + ":focused/mix" => !app.editor_focused() && matches!(app.selection(), + Selection::Mix), + }; +}); + +dsl_ns!(App: Dialog { + word = |app| { + ":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/save" => Dialog::Browse(BrowseTarget::SaveProject, + Browse::new(None).unwrap().into()), + ":dialog/load" => Dialog::Browse(BrowseTarget::LoadProject, + Browse::new(None).unwrap().into()), + ":dialog/import/clip" => Dialog::Browse(BrowseTarget::ImportClip(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/export/clip" => Dialog::Browse(BrowseTarget::ExportClip(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/import/sample" => Dialog::Browse(BrowseTarget::ImportSample(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/export/sample" => Dialog::Browse(BrowseTarget::ExportSample(Default::default()), + Browse::new(None).unwrap().into()), + }; +}); + +dsl_ns!(App: Selection { + word = |app| { + ":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_ns!(App: Color { + word = |app| { + ":color/bg" => Color::Rgb(28, 32, 36), + }; + expr = |app| { + "g" (n: u8) => Color::Rgb(n, n, n), + "rgb" (r: u8, g: u8, b: u8) => Color::Rgb(r, g, b), + }; +}); + +dsl_ns!(App: Option { + word = |app| { + ":editor/pitch" => Some( + (app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into() + ) + }; +}); + +dsl_ns!(App: Option { + word = |app| { + ":selected/scene" => app.selection().scene(), + ":selected/track" => app.selection().track(), + }; +}); + +dsl_ns!(App: Option>> { + word = |app| { + ":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() { + app.scenes()[*scene].clips[*track].clone() + } else { + None + } + }; +}); + +dsl_ns!(App: u8 { + literal = |dsl|Ok(if let Some(src) = dsl.src()? { + Some(to_number(src)? as u8) + } else { + None + }); +}); + +dsl_ns!(App: u16 { + literal = |dsl|Ok(if let Some(src) = dsl.src()? { + Some(to_number(src)? as u16) + } else { + None + }); + word = |app| { + ":w/sidebar" => app.project.w_sidebar(app.editor().is_some()), + ":h/sample-detail" => 6.max(app.height() as u16 * 3 / 9), + }; +}); + +dsl_ns!(App: usize { + literal = |dsl|Ok(if let Some(src) = dsl.src()? { + Some(to_number(src)? as usize) + } else { + None + }); + word = |app| { + ":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_ns!(App: isize { + literal = |dsl|Ok(if let Some(src) = dsl.src()? { + Some(to_number(src)? as isize) + } else { + None + }); +}); + +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() } } +impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } } +has!(Jack<'static>: |self: App|self.jack); +has!(Pool: |self: App|self.pool); +has!(Dialog: |self: App|self.dialog); +has!(Clock: |self: App|self.project.clock); +has!(Option: |self: App|self.project.editor); +has!(Selection: |self: App|self.project.selection); +has!(Vec: |self: App|self.project.midi_ins); +has!(Vec: |self: App|self.project.midi_outs); +has!(Vec: |self: App|self.project.scenes); +has!(Vec: |self: App|self.project.tracks); +has!(Measure: |self: App|self.size); +has_clips!( |self: App|self.pool.clips); +maybe_has!(Track: |self: App| { MaybeHas::::get(&self.project) }; + { MaybeHas::::get_mut(&mut self.project) }); +maybe_has!(Scene: |self: App| { MaybeHas::::get(&self.project) }; + { MaybeHas::::get_mut(&mut self.project) }); + +impl ScenesView for App { + fn w_side (&self) -> u16 { 20 } + fn w_mid (&self) -> u16 { (self.width() as u16).saturating_sub(self.w_side()) } + fn h_scenes (&self) -> u16 { (self.height() as u16).saturating_sub(20) } +} + +impl Draw for App { + fn draw (&self, to: &mut TuiOut) { + for (index, dsl) in self.mode.view.iter().enumerate() { + to.place(&Align::nw(Push::y(1 + index as u16 * 2, dsl.src().unwrap()))); + //let _ = self.view(to, dsl).expect("render failed"); + } + } +} + +impl App { + + fn view <'a> (&'a self, to: &mut TuiOut, dsl: impl Dsl + 'a) -> Usually<()> { + if let Ok(Some(expr)) = dsl.expr() { + self.view_expr(to, expr) + } else if let Ok(Some(word)) = dsl.word() { + self.view_word(to, word) + } else { + panic!("{dsl:?}: invalid") + } + } + + fn view_expr <'a> (&'a self, to: &mut TuiOut, expr: impl DslExpr + 'a) -> Usually<()> { + let head = expr.head()?; let args = expr.tail(); + let mut frags = head.src()?.unwrap_or_default().split("/"); + let arg0 = args.head(); let tail0 = args.tail(); + let arg1 = tail0.head(); let tail1 = tail0.tail(); + let arg2 = tail1.head(); let tail2 = tail1.tail(); + let arg3 = tail2.head(); let tail3 = tail2.tail(); + match frags.next() { + + Some("text") => to.place(&frags.next()), + + Some("when") => to.place(&When::new( + self.from(arg0?)?.unwrap(), + Thunk::new(move|to: &mut TuiOut|self.view(to, arg1).unwrap()) + )), + + Some("either") => to.place(&Either::new( + self.from(arg0?)?.unwrap(), + Thunk::new(move|to: &mut TuiOut|self.view(to, arg1).unwrap()), + Thunk::new(move|to: &mut TuiOut|self.view(to, arg2).unwrap()) + )), + + Some("fg") => { + let arg0 = arg0?.expect("fg: expected arg 0 (color)"); + to.place(&Tui::fg( + DslNs::::from(self, arg0)?.unwrap_or_else(||panic!("fg: {arg0:?}: not a color")), + Thunk::new(move|to: &mut TuiOut|self.view(to, arg1).unwrap()), + )) + }, + + Some("bg") => { + let arg0 = arg0?.expect("bg: expected arg 0 (color)"); + to.place(&Tui::bg( + DslNs::::from(self, arg0)?.unwrap_or_else(||panic!("bg: {arg0:?}: not a color")), + Thunk::new(move|to: &mut TuiOut|self.view(to, arg1).unwrap()), + )) + }, + + Some("bsp") => to.place(&{ + let a = Thunk::new(move|to: &mut TuiOut|self.view(to, arg0).unwrap()); + let b = Thunk::new(move|to: &mut TuiOut|self.view(to, arg1).unwrap()); + match frags.next() { + Some("n") => Bsp::n(a, b), + Some("s") => Bsp::s(a, b), + Some("e") => Bsp::e(a, b), + Some("w") => Bsp::w(a, b), + Some("a") => Bsp::a(a, b), + Some("b") => Bsp::b(a, b), + frag => unimplemented!("bsp/{frag:?}") + } + }), + + Some("align") => to.place(&{ + let a = Thunk::new(move|to: &mut TuiOut|self.view(to, arg0).unwrap()); + match frags.next() { + Some("n") => Align::n(a), + Some("s") => Align::s(a), + Some("e") => Align::e(a), + Some("w") => Align::w(a), + Some("x") => Align::x(a), + Some("y") => Align::y(a), + Some("c") => Align::c(a), + frag => unimplemented!("align/{frag:?}") + } + }), + + Some("fill") => to.place(&{ + let a = Thunk::new(move|to: &mut TuiOut|self.view(to, arg0).unwrap()); + match frags.next() { + Some("x") => Fill::X(a), + Some("y") => Fill::Y(a), + Some("xy") => Fill::XY(a), + frag => unimplemented!("fill/{frag:?}") + } + }), + + Some("fixed") => to.place(&{ + let axis = frags.next(); + let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") => arg2, _ => panic!() }; + let cb = Thunk::new(move|to: &mut TuiOut|self.view(to, arg).unwrap()); + match axis { + Some("x") => Fixed::X(self.from(arg0?)?.unwrap(), cb), + Some("y") => Fixed::Y(self.from(arg0?)?.unwrap(), cb), + Some("xy") => Fixed::XY(self.from(arg0?)?.unwrap(), self.from(arg1?)?.unwrap(), cb), + frag => unimplemented!("fixed/{frag:?} ({expr:?}) ({head:?}) ({:?})", + head.src()?.unwrap_or_default().split("/").next()) + } + }), + + Some("min") => to.place(&{ + let c = match frags.next() { + Some("x") | Some("y") => arg1, Some("xy") => arg2, _ => panic!() + }; + let cb = Thunk::new(move|to: &mut TuiOut|self.view(to, c).unwrap()); + match frags.next() { + Some("x") => Min::X(self.from(arg0?)?.unwrap(), cb), + Some("y") => Min::Y(self.from(arg0?)?.unwrap(), cb), + Some("xy") => Min::XY(self.from(arg0?)?.unwrap(), self.from(arg1?)?.unwrap(), cb), + frag => unimplemented!("min/{frag:?}") + } + }), + + Some("max") => to.place(&{ + let c = match frags.next() { + Some("x") | Some("y") => arg1, Some("xy") => arg2, _ => panic!() + }; + let cb = Thunk::new(move|to: &mut TuiOut|self.view(to, c).unwrap()); + match frags.next() { + Some("x") => Max::X(self.from(arg0?)?.unwrap(), cb), + Some("y") => Max::Y(self.from(arg0?)?.unwrap(), cb), + Some("xy") => Max::XY(self.from(arg0?)?.unwrap(), self.from(arg1?)?.unwrap(), cb), + frag => unimplemented!("max/{frag:?}") + } + }), + + _ => panic!("unexpected: {expr:?}") + + }; + Ok(()) + } + + fn view_word <'a> (&'a self, to: &mut TuiOut, dsl: impl DslExpr + 'a) -> Usually<()> { + let mut frags = dsl.src()?.unwrap().split("/"); + match frags.next() { + + Some(":logo") => to.place(&Fixed::xy(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), col!{ + Fixed::y(1, ""), + Fixed::y(1, ""), + Fixed::y(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"), + Fixed::y(1, Bsp::e("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", Bsp::e(Tui::fg(Rgb(230,100,40), "v0.3.0"), " ~~"))), + Fixed::y(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"), + })))), + + Some(":status") => to.place(&"TODO: Status Bar"), + + Some(":meters") => match frags.next() { + Some("input") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::y(Align::s("Input Meters")))), + Some("output") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::y(Align::s("Output Meters")))), + _ => panic!() + }, + + Some(":tracks") => match frags.next() { + None => to.place(&"TODO tracks"), + Some("names") => to.place(&self.project.view_track_names(self.color.clone())),//Tui::bg(Rgb(40, 40, 40), Fill::x(Align::w("Track Names")))), + Some("names") => to.place(&self.project.view_track_names(self.color.clone())),//Tui::bg(Rgb(40, 40, 40), Fill::x(Align::w("Track Names")))), + Some("inputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::x(Align::w("Track Inputs")))), + Some("devices") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::x(Align::w("Track Devices")))), + Some("outputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::x(Align::w("Track Outputs")))), + _ => panic!() + }, + + Some(":scenes") => match frags.next() { + None => to.place(&"TODO scenes"), + Some(":scenes/names") => to.place(&"TODO Scene Names"), + _ => panic!() + }, + + Some(":editor") => to.place(&"TODO Editor"), + + Some(":dialog") => match frags.next() { + Some("menu") => to.place(&if let Dialog::Menu(selected, items) = &self.dialog { + let items = items.clone(); + let selected = selected; + Some(Fill::xy(Thunk::new(move|to: &mut TuiOut|{ + for (index, MenuItem(item, _)) in items.0.iter().enumerate() { + to.place(&Push::y((2 * index) as u16, + Tui::fg_bg( + if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, + if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, + Fixed::y(2, Align::n(Fill::x(item))) + ))); + } + }))) + } else { + None + }), + _ => unimplemented!("App::view_word: {dsl:?} ({frags:?})"), + }, + + Some(":templates") => to.place(&{ + let modes = self.config.modes.clone(); + let height = (modes.read().unwrap().len() * 2) as u16; + Fixed::y(height, Min::x(30, Thunk::new(move |to: &mut TuiOut|{ + for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { + let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; + 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(""); + let fg1 = Rgb(224, 192, 128); + let fg2 = Rgb(224, 128, 32); + let field_name = Fill::x(Align::w(Tui::fg(fg1, name))); + let field_id = Fill::x(Align::e(Tui::fg(fg2, id))); + let field_info = Fill::x(Align::w(info)); + to.place(&Push::y((2 * index) as u16, + Fixed::y(2, Fill::x(Tui::bg(bg, Bsp::s( + Bsp::a(field_name, field_id), field_info)))))); + } + }))) + }), + + Some(":sessions") => to.place(&Fixed::y(6, Min::x(30, Thunk::new(|to: &mut TuiOut|{ + let fg = Rgb(224, 192, 128); + for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { + let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; + to.place(&Push::y((2 * index) as u16, + &Fixed::y(2, Fill::x(Tui::bg(bg, Align::w(Tui::fg(fg, name))))))); + } + })))), + + Some(":browse/title") => to.place(&Fill::x(Align::w(FieldV(Default::default(), + match self.dialog.browser_target().unwrap() { + BrowseTarget::SaveProject => "Save project:", + BrowseTarget::LoadProject => "Load project:", + BrowseTarget::ImportSample(_) => "Import sample:", + BrowseTarget::ExportSample(_) => "Export sample:", + BrowseTarget::ImportClip(_) => "Import clip:", + BrowseTarget::ExportClip(_) => "Export clip:", + }, Shrink::x(3, Fixed::y(1, Tui::fg(Tui::g(96), RepeatH("🭻")))))))), + + Some(":device") => { + let selected = self.dialog.device_kind().unwrap(); + to.place(&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")))) }))) + }, + + Some(_) => { + let views = self.config.views.read().unwrap(); + if let Some(dsl) = views.get(dsl.src()?.unwrap()) { + let dsl = dsl.clone(); + std::mem::drop(views); + self.view(to, dsl)? + } else { + unimplemented!("{dsl:?}"); + } + }, + + _ => unreachable!() + + } + Ok(()) + } + +} + +impl App { + + pub fn editor_focused (&self) -> bool { + false + } + + 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(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<&Browse> { + if let Dialog::Browse(_, 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!(), + } + } + + pub fn update_clock (&self) { + ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) + } +} + +//pub fn view_nil (_: &App) -> TuiCb { + //|to|to.place(&Fill::xy("·")) +//} + + //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::Browse(BrowseTarget::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::Browse(BrowseTarget::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::Browse(BrowseTarget::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_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 Draw)|{ + //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))) + +//#[dizzle::ns] +//#[dizzle::ns::include(TuiOut)] +//trait AppApi { + //#[dizzle::ns::word("sessions")] + //fn view_sessions (&self) -> Box> { + //Min::x(30, Fixed::y(6, Stack::south( + //move|add: &mut dyn FnMut(&dyn Render)|{ + //let fg = Rgb(224, 192, 128); + //for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { + //let bg = if index == 0 { Rgb(48,64,32) } else { Rgb(16, 32, 24) }; + //add(&Fixed::y(2, Fill::x(Tui::bg(bg, Align::w(Tui::fg(fg, name)))))); + //} + //} + //))).into() + //} + //#[dizzle::ns::expr("bold")] + //fn view_bold (&self, value: bool, x: Box>) -> Box> { + //Box::new(Tui::bold(value, x)) + //} +//} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +//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(); +//}); diff --git a/crates/app/tek_bind.rs b/tek/tek_bind.rs similarity index 97% rename from crates/app/tek_bind.rs rename to tek/tek_bind.rs index c0c4a7a7..b6b02d01 100644 --- a/crates/app/tek_bind.rs +++ b/tek/tek_bind.rs @@ -33,9 +33,10 @@ handle!(TuiIn:|self: App, input|{ #[derive(Debug, Copy, Clone)] pub enum Axis { X, Y, Z, I } -impl<'t> DslNs<'t, AppCommand> for App { - dsl_exprs!(|app| -> AppCommand { /* TODO */ }); - dsl_words!(|app| -> AppCommand { +impl<'a> DslNs<'a, AppCommand> for App {} +impl<'a> DslNsExprs<'a, AppCommand> for App {} +impl<'a> DslNsWords<'a, AppCommand> for App { + dsl_words!('a |app| -> AppCommand { "x/inc" => AppCommand::Inc { axis: Axis::X }, "x/dec" => AppCommand::Dec { axis: Axis::X }, "y/inc" => AppCommand::Inc { axis: Axis::Y }, diff --git a/crates/app/tek_cli.rs b/tek/tek_cli.rs similarity index 100% rename from crates/app/tek_cli.rs rename to tek/tek_cli.rs diff --git a/crates/app/tek_menu.rs b/tek/tek_menu.rs similarity index 100% rename from crates/app/tek_menu.rs rename to tek/tek_menu.rs diff --git a/crates/app/tek_test.rs b/tek/test.rs similarity index 100% rename from crates/app/tek_test.rs rename to tek/test.rs