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 e0ed14ce..14b1da71 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,11 +25,7 @@ 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" } backtrace = { version = "0.3.72" } diff --git a/Justfile b/Justfile index e9bbdcb7..199d5799 100644 --- a/Justfile +++ b/Justfile @@ -18,6 +18,8 @@ default: just -l bacon: bacon -s +check: + cargo check run: {{debug}} run-init: 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 1b6c200a..00000000 --- a/crates/app/tek.rs +++ /dev/null @@ -1,172 +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!(), - } - } - pub fn update_clock (&self) { - ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) - } -} - -//#[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 ff1f6307..00000000 --- a/crates/app/tek_data.rs +++ /dev/null @@ -1,141 +0,0 @@ -use crate::*; - -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 - }); -}); 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/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 c9bb65b5..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/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 baa582b9..18b68039 160000 --- a/deps/tengri +++ b/deps/tengri @@ -1 +1 @@ -Subproject commit baa582b9ddc1dd9c078b223fae7e3c10cf348166 +Subproject commit 18b6803912105cc6a23217641a68967695a2166b diff --git a/tek/Cargo.toml b/tek/Cargo.toml new file mode 100644 index 00000000..3e005db3 --- /dev/null +++ b/tek/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "tek" +edition = { workspace = true } +version = { workspace = true } + +[lib] +path = "tek.rs" + +[[bin]] +name = "tek" +path = "tek_cli.rs" + +[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 100% rename from crates/device/src/arranger.rs rename to tek/device/arranger.rs 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 100% rename from crates/device/src/arranger/arranger_tracks.rs rename to tek/device/arranger/arranger_tracks.rs diff --git a/crates/device/src/arranger/arranger_view.rs b/tek/device/arranger/arranger_view.rs similarity index 100% rename from crates/device/src/arranger/arranger_view.rs rename to tek/device/arranger/arranger_view.rs 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 100% rename from crates/device/src/clock/clock_view.rs rename to tek/device/clock/clock_view.rs 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 100% rename from crates/device/src/editor/editor_view.rs rename to tek/device/editor/editor_view.rs diff --git a/crates/device/src/editor/editor_view_h.rs b/tek/device/editor/editor_view_h.rs similarity index 100% rename from crates/device/src/editor/editor_view_h.rs rename to tek/device/editor/editor_view_h.rs diff --git a/crates/device/src/editor/editor_view_v.rs b/tek/device/editor/editor_view_v.rs similarity index 100% rename from crates/device/src/editor/editor_view_v.rs rename to tek/device/editor/editor_view_v.rs diff --git a/crates/device/src/lib.rs b/tek/device/lib.rs similarity index 100% rename from crates/device/src/lib.rs rename to tek/device/lib.rs 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 100% rename from crates/device/src/lv2/lv2_tui.rs rename to tek/device/lv2/lv2_tui.rs diff --git a/crates/device/src/meter.rs b/tek/device/meter.rs similarity index 100% rename from crates/device/src/meter.rs rename to tek/device/meter.rs 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 100% rename from crates/device/src/sampler/sampler_browse.rs rename to tek/device/sampler/sampler_browse.rs 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 100% rename from crates/device/src/sampler/sampler_view.rs rename to tek/device/sampler/sampler_view.rs 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 100% rename from crates/device/src/sequencer/seq_launch.rs rename to tek/device/sequencer/seq_launch.rs 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 100% rename from tek.edn rename to tek/tek.edn diff --git a/crates/app/tek_view.rs b/tek/tek.rs similarity index 62% rename from crates/app/tek_view.rs rename to tek/tek.rs index 9804ba0e..8fb554c2 100644 --- a/crates/app/tek_view.rs +++ b/tek/tek.rs @@ -1,4 +1,264 @@ -use crate::*; +#![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 } @@ -274,6 +534,98 @@ impl App { } +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("ยท")) //} @@ -488,3 +840,38 @@ impl App { ////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 100% rename from crates/app/tek_bind.rs rename to tek/tek_bind.rs 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