From 044f60ebcb80850d2d4e087d3c49f7628d32a610 Mon Sep 17 00:00:00 2001 From: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Date: Sat, 17 Jan 2026 03:43:48 +0200 Subject: [PATCH] refactor: extract dizzle --- .gitmodules | 3 + Cargo.lock | 37 +++--- Cargo.toml | 2 +- app/tek.edn | 10 +- app/tek.rs | 294 +++++++++++++++++++++++++----------------------- app/tek_test.rs | 8 +- deps/dizzle | 1 + deps/tengri | 2 +- 8 files changed, 176 insertions(+), 181 deletions(-) create mode 160000 deps/dizzle diff --git a/.gitmodules b/.gitmodules index 15f065ba..04c3bcce 100644 --- a/.gitmodules +++ b/.gitmodules @@ -8,3 +8,6 @@ [submodule "deps/rust-jack"] path = deps/rust-jack url = https://codeberg.org/unspeaker/rust-jack +[submodule "deps/dizzle"] + path = deps/dizzle + url = ssh://git@codeberg.org/unspeaker/dizzle.git diff --git a/Cargo.lock b/Cargo.lock index 9be82217..5350e2ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -655,6 +655,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "dizzle" +version = "0.1.0" +dependencies = [ + "const_panic", + "itertools 0.14.0", + "konst", + "thiserror 2.0.16", +] + [[package]] name = "dlib" version = "0.5.2" @@ -2468,41 +2478,24 @@ dependencies = [ name = "tengri" version = "0.14.0" dependencies = [ - "tengri_core", - "tengri_dsl", + "dizzle", "tengri_input", "tengri_output", "tengri_tui", ] -[[package]] -name = "tengri_core" -version = "0.14.0" - -[[package]] -name = "tengri_dsl" -version = "0.14.0" -dependencies = [ - "const_panic", - "itertools 0.14.0", - "konst", - "tengri_core", - "thiserror 2.0.16", -] - [[package]] name = "tengri_input" version = "0.14.0" dependencies = [ - "tengri_core", + "dizzle", ] [[package]] name = "tengri_output" version = "0.14.0" dependencies = [ - "tengri_core", - "tengri_dsl", + "dizzle", ] [[package]] @@ -2512,13 +2505,11 @@ dependencies = [ "atomic_float", "better-panic", "crossterm 0.29.0", - "konst", + "dizzle", "palette", "quanta", "rand 0.8.5", "ratatui", - "tengri_core", - "tengri_dsl", "tengri_input", "tengri_output", "unicode-width 0.2.0", diff --git a/Cargo.toml b/Cargo.toml index 9f379dc6..84c082bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] resolver = "2" members = [ "./app", "./engine", "./device" ] -exclude = [ "./deps/tengri" ] +exclude = [ "./deps/tengri", "./deps/dizzle" ] [workspace.package] edition = "2024" diff --git a/app/tek.edn b/app/tek.edn index 17b02a6c..08376129 100644 --- a/app/tek.edn +++ b/app/tek.edn @@ -38,12 +38,6 @@ :ports/out (bsp/n :ports/in (bg (g 30) (bsp/s (fixed/y 7 :logo) (fill :dialog/menu))))))) -(view :menu (bsp/s - (push/y 4 (fixed/xy 20 2 (bg (g 0) :debug))) - (fixed 20 2 (bg (g 20) (push/x 2 :debug))))) - -(view :menu (bsp/s (fixed/y 4 :debug) :debug)) - (view :ports/out (fill/x (fixed/y 3 (bsp/a (fill/x (align/w (text L-AUDIO-OUT))) (bsp/a (text MIDI-OUT) (fill/x (align/e (text AUDIO-OUT-R)))))))) @@ -111,9 +105,7 @@ (mode :add-device (keys :add-device)) (mode :browse (keys :browse)) (mode :rename (keys :input)) (mode :length (keys :rename)) (mode :clip (keys :clip)) (mode :track (keys :track)) (mode :scene (keys :scene)) (mode :mix (keys :mix)) - (keys :clock :arranger :global) :arranger) - -(view :arranger (bsp/n + (keys :clock :arranger :global) (bsp/n :status (bsp/w :meters/output (bsp/e :meters/input :arrangement)))) diff --git a/app/tek.rs b/app/tek.rs index 70d0dcd3..cbe9f14e 100644 --- a/app/tek.rs +++ b/app/tek.rs @@ -27,8 +27,7 @@ xdg::BaseDirectories, atomic_float::*, tek_device::{*, tek_engine::*}, - tengri::{*, dsl::*, input::*, output::*}, - tengri::tui::*, + tengri::{*, input::*, output::*, tui::*}, tengri::tui::ratatui::{ self, prelude::{Rect, Style, Stylize, Buffer, Modifier, buffer::Cell, Color::{self, *}}, @@ -161,91 +160,6 @@ pub mod core { ..Default::default() } } - pub fn update_clock (&self) { - ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) - } - /// Set modal dialog. - pub fn set_dialog (&mut self, mut dialog: Dialog) -> Dialog { - std::mem::swap(&mut self.dialog, &mut dialog); - dialog - } - /// Set picked device in device pick dialog. - 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!(), - } - } - /// Return reference to content browser if open. - pub fn browser (&self) -> Option<&Browse> { - if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } - } - /// Is a MIDI editor currently focused? - pub fn editor_focused (&self) -> bool { false } - /// Toggle MIDI editor. - 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()); - } - } - } } impl Config { const CONFIG: &'static str = "tek.edn"; @@ -259,11 +173,11 @@ pub mod core { } /// Write initial contents of configuration. pub fn init (&mut self) -> Usually<()> { - self.init_file(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|cfgs.load(&dsl))?; + self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|cfgs.add(&dsl))?; Ok(()) } /// Write initial contents of a configuration file. - pub fn init_file ( + pub fn init_one ( &mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()> ) -> Usually<()> { if self.dirs.find_config_file(path).is_none() { @@ -278,41 +192,69 @@ pub mod core { return Err(format!("{path}: not found").into()) }) } - /// Load a configuration from [Dsl] source. - pub fn load (&mut self, dsl: impl Dsl) -> Usually<()> { - dsl.each(|item|if let Some(expr) = item.expr()? { + /// Add statements to configuration from [Dsl] source. + pub fn add (&mut self, dsl: impl Dsl) -> Usually<()> { + dsl.each(|item|self.add_one(item)) + } + fn add_one (&self, item: impl Dsl) -> Usually<()> { + 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 => - Bind::>::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()); - }, + Some("mode") if let Some(name) = name => load_mode(&self.modes, &name, &body)?, + Some("keys") if let Some(name) = name => load_bind(&self.binds, &name, &body)?, + Some("view") if let Some(name) = name => load_view(&self.views, &name, &body)?, _ => 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)); + pub fn load_view (views: &Views, name: &impl AsRef, body: &impl Dsl) -> Usually<()> { + views.write().unwrap().insert(name.as_ref().into(), body.src()?.unwrap_or_default().into()); + Ok(()) + } + pub fn load_mode (modes: &Modes, name: &impl AsRef, body: &impl Dsl) -> Usually<()> { + let mut mode = Mode::default(); + body.each(|item|mode.add(item))?; + modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode)); + Ok(()) + } + pub fn load_bind (binds: &Binds, name: &impl AsRef, body: &impl Dsl) -> Usually<()> { + let mut map = Bind::new(); + body.each(|item|if item.expr().head() == Ok(Some("see")) { + // TODO Ok(()) - } - fn load_one (&mut self, dsl: impl Dsl) -> Usually<()> { + } 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 Mode> { + fn add (&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()); + //println!("Mode::add: {head} {:?}", expr.tail()); let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); match head { "name" => self.name.push(tail.into()), @@ -323,18 +265,18 @@ pub mod core { Ok(()) })?, "mode" => if let Some(id) = tail.head()? { - Self::load_into(&self.modes, &id, &tail.tail())?; + load_mode(&self.modes, &id, &tail.tail())?; } else { - return Err(format!("Mode::load_one: self: incomplete: {expr:?}").into()); + return Err(format!("Mode::add: self: incomplete: {expr:?}").into()); }, _ => { - return Err(format!("Mode::load_one: unexpected expr: {head:?} {tail:?}").into()) + return Err(format!("Mode::add: 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()); + return Err(format!("Mode::add: unexpected: {dsl:?}").into()); }) } } @@ -367,35 +309,6 @@ pub mod core { .flatten() } } - impl Bind> { - pub fn load_into (binds: &Binds, name: &impl AsRef, body: &impl Dsl) -> Usually<()> { - //println!("Bind::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; @@ -411,6 +324,15 @@ pub mod core { } } pub mod ns { + // TODO make these enumerable: + // + // impl Ns> for App { + // const NS: To> = To::new(|_, _|Default::default()) + // .key(":foo", |_|"bar".into()) + // .key(":bar", |_|"baz".into()); + // } + // + // use super::{*, model::*, gui::*}; // Allow source to be read as Literal string dsl_ns!(App: Arc { literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); }); @@ -537,7 +459,6 @@ pub mod ns { None }); }); - impl<'a> DslNs<'a, AppCommand> for App {} impl<'a> DslNsExprs<'a, AppCommand> for App {} impl<'a> DslNsWords<'a, AppCommand> for App { @@ -550,6 +471,93 @@ pub mod ns { "cancel" => AppCommand::Cancel, }); } + impl App { + pub fn update_clock (&self) { + ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) + } + /// Set modal dialog. + pub fn set_dialog (&mut self, mut dialog: Dialog) -> Dialog { + std::mem::swap(&mut self.dialog, &mut dialog); + dialog + } + /// Set picked device in device pick dialog. + 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!(), + } + } + /// Return reference to content browser if open. + pub fn browser (&self) -> Option<&Browse> { + if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } + } + /// Is a MIDI editor currently focused? + pub fn editor_focused (&self) -> bool { false } + /// Toggle MIDI editor. + 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 mod tui { use super::{*, model::*, gui::*}; diff --git a/app/tek_test.rs b/app/tek_test.rs index d7bd3ca8..c5d05cb7 100644 --- a/app/tek_test.rs +++ b/app/tek_test.rs @@ -2,12 +2,12 @@ use crate::*; #[cfg(test)] #[test] fn test_cli () { use clap::CommandFactory; - Cli::command().debug_assert(); + cli::Cli::command().debug_assert(); //let jack = Jack::default(); } #[cfg(test)] #[test] fn test_app () -> Usually<()> { - let mut app = App::default(); + let mut app = model::App::default(); let _ = app.scene_add(None, None)?; let _ = app.update_clock(); Ok(()) @@ -59,7 +59,7 @@ use crate::*; } #[cfg(test)] #[test] fn test_view_iter () { - let mut app = App::default(); + let mut app = model::App::default(); app.project.editor = Some(Default::default()); //let _: Vec<_> = app.project.inputs_with_sizes().collect(); //let _: Vec<_> = app.project.outputs_with_sizes().collect(); @@ -70,7 +70,7 @@ use crate::*; } #[cfg(test)] #[test] fn test_view_sizes () { - let app = App::default(); + let app = model::App::default(); let _ = app.project.w(); //let _ = app.project.w_sidebar(); //let _ = app.project.w_tracks_area(); diff --git a/deps/dizzle b/deps/dizzle new file mode 160000 index 00000000..1ce18223 --- /dev/null +++ b/deps/dizzle @@ -0,0 +1 @@ +Subproject commit 1ce18223c60eac427da617d948ad18808d2f5b38 diff --git a/deps/tengri b/deps/tengri index b0d2fad1..1344967f 160000 --- a/deps/tengri +++ b/deps/tengri @@ -1 +1 @@ -Subproject commit b0d2fad17beef84be8f1fdad116643563379a2c1 +Subproject commit 1344967f33ac8c8e87ff8585fe8b463a15f088f7