From 2fe2cc47db1180776ec4d44f77195648578615d0 Mon Sep 17 00:00:00 2001 From: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Date: Sat, 17 Jan 2026 00:24:52 +0200 Subject: [PATCH 1/8] review main module --- app/Cargo.toml | 2 +- app/tek.rs | 1735 +++++++++++++++++++++++++++++++++++------------ app/tek_bind.rs | 325 --------- app/tek_cfg.rs | 65 -- app/tek_cli.rs | 138 ---- app/tek_deps.rs | 39 -- app/tek_mode.rs | 61 -- app/tek_test.rs | 6 + app/tek_view.rs | 353 ---------- 9 files changed, 1318 insertions(+), 1406 deletions(-) delete mode 100644 app/tek_bind.rs delete mode 100644 app/tek_cfg.rs delete mode 100644 app/tek_cli.rs delete mode 100644 app/tek_deps.rs delete mode 100644 app/tek_mode.rs delete mode 100644 app/tek_view.rs diff --git a/app/Cargo.toml b/app/Cargo.toml index d6c3afa9..c315f1dc 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -8,7 +8,7 @@ path = "tek.rs" [[bin]] name = "tek" -path = "tek_cli.rs" +path = "tek.rs" [target.'cfg(target_os = "linux")'] rustflags = ["-C", "link-arg=-fuse-ld=mold"] diff --git a/app/tek.rs b/app/tek.rs index 1ce0c252..190cf83a 100644 --- a/app/tek.rs +++ b/app/tek.rs @@ -1,457 +1,1342 @@ -#![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)] -#![allow( - clippy::unit_arg -)] +#![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)] #[cfg(test)] mod tek_test; -mod tek_bind; pub use self::tek_bind::*; -mod tek_cfg; pub use self::tek_cfg::*; -mod tek_deps; pub use self::tek_deps::*; -mod tek_mode; pub use self::tek_mode::*; -mod tek_view; pub use self::tek_view::*; +#[allow(unused)] pub(crate) use ::{ + std::path::{Path, PathBuf}, + std::sync::{Arc, RwLock}, + std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}, + std::error::Error, + std::collections::BTreeMap, + std::fmt::Write, + std::cmp::Ord, + std::ffi::OsString, + std::fmt::{Debug, Formatter}, + std::fs::File, + std::ops::{Add, Sub, Mul, Div, Rem}, + std::thread::JoinHandle, + xdg::BaseDirectories, + atomic_float::*, + tek_device::{*, tek_engine::*}, + tengri::{*, dsl::*, input::*, output::*}, + tengri::tui::*, + tengri::tui::ratatui::{ + self, + prelude::{Rect, Style, Stylize, Buffer, Modifier, buffer::Cell, Color::{self, *}}, + widgets::{Widget, canvas::{Canvas, Line}}, + }, + tengri::tui::crossterm::{ + self, + event::{Event, KeyCode::{self, *}}, + }, +}; -/// 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, +pub mod model { + /// 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, + } + /// Configuration: mode, view, and bind definitions. + #[derive(Default, Debug)] pub struct Config { + /// XDG base directories of running user. + pub dirs: BaseDirectories, + /// Active collection of interaction modes. + pub modes: Modes, + /// Active collection of event bindings. + pub binds: Binds, + /// Active collection of view definitions. + pub views: Views, + } + /// Collection of interaction modes. + pub type Modes = Arc, Arc>>>>>; + /// Collection of input bindings. + pub type Binds = Arc, Bind>>>>; + /// Collection of view definitions. + pub type Views = Arc, Arc>>>; + /// Group of view and keys definitions. + #[derive(Default, Debug)] + pub struct Mode { + pub path: PathBuf, + pub name: Vec, + pub info: Vec, + pub view: Vec, + pub keys: Vec, + pub modes: Modes, + } + /// An nput binding. + #[derive(Debug)] + pub struct Bind( + /// 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>> + ); + + #[derive(Debug, Copy, Clone)] + pub enum Axis { X, Y, Z, I } + /// Various possible dialog modes. + #[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog { + #[default] None, + Help(usize), + Menu(usize, MenuItems), + Device(usize), + Message(Arc), + Browse(BrowseTarget, Arc), + Options, + } + #[derive(Debug, Clone, Default, PartialEq)] pub struct MenuItems( + pub Arc<[MenuItem]> + ); + #[derive(Clone)] pub struct MenuItem( + /// Label + pub Arc, + /// Callback + pub ArcUsually<()> + Send + Sync>> + ); } -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()); +pub mod core { + use super::{*, model::*}; + 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()); + } + } + } + } + impl Config { + const CONFIG: &'static str = "tek.edn"; + const DEFAULTS: &'static str = include_str!("./tek.edn"); + /// Create a new app configuration from a set of XDG base directories, + pub fn new (dirs: Option) -> Self { + Self { + dirs: dirs.unwrap_or_else(||BaseDirectories::with_profile("tek", "v0")), + ..Default::default() + } + } + /// Write initial contents of configuration. + pub fn init (&mut self) -> Usually<()> { + self.init_file(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|cfgs.load(&dsl))?; + Ok(()) + } + /// Write initial contents of a configuration file. + 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()) + }) + } + /// Load a configuration from [Dsl] source. + 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 => + 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()); + }, + _ => 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(()) + } + 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()); + }) + } + } + impl Bind { + /// 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 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; + 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()) + } + } + } +} +pub mod ns { + // Allow source to be read as Literal string + dsl_ns!(App: Arc { + literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); + }); + // Provide boolean values. + dsl_ns!(App: bool { + // TODO literal = ... + 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), + }; + }); + // TODO: provide colors here + dsl_ns!(App: ItemTheme {}); + 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<'a> DslNs<'a, AppCommand> for App {} + impl<'a> DslNsExprs<'a, AppCommand> for App {} + impl<'a> DslNsWords<'a, AppCommand> for App { + dsl_words!('a |app| -> AppCommand { + "x/inc" => AppCommand::Inc { axis: Axis::X }, + "x/dec" => AppCommand::Dec { axis: Axis::X }, + "y/inc" => AppCommand::Inc { axis: Axis::Y }, + "y/dec" => AppCommand::Dec { axis: Axis::Y }, + "confirm" => AppCommand::Confirm, + "cancel" => AppCommand::Cancel, + }); + } + def_command!(AppCommand: |app: App| { + Nop => Ok(None), + Confirm => Ok(match &app.dialog { + Dialog::Menu(index, items) => { + let callback = items.0[*index].1.clone(); + callback(app)?; + None + }, + _ => todo!(), + }), + Cancel => todo!(), // TODO delegate: + Inc { axis: Axis } => Ok(match (&app.dialog, axis) { + (Dialog::None, _) => todo!(), + (Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_next() } + .execute(app)?, + _ => todo!() + }), + Dec { axis: Axis } => Ok(match (&app.dialog, axis) { + (Dialog::None, _) => None, + (Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_prev() } + .execute(app)?, + _ => todo!() + }), + SetDialog { dialog: Dialog } => { + swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }) + }, + }); +} +pub mod tui { + use super::{*, model::*}; + handle!(TuiIn: |self: App, input|{ + let mut commands = vec![]; + for id in self.mode.keys.iter() { + if let Some(event_map) = self.config.binds.clone().read().unwrap().get(id.as_ref()) { + if let Some(bindings) = event_map.query(input.event()) { + for binding in bindings { + for command in binding.commands.iter() { + if let Some(command) = self.from(command)? as Option { + commands.push(command) + } + } } } } - 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:?}"); } - } - } -); - -// Allow source to be read as Literal string -dsl_ns!(App: Arc { - literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); -}); - -// Provide boolean values. -dsl_ns!(App: bool { - // TODO literal = ... - 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), - }; -}); - -// TODO: provide colors here -dsl_ns!(App: ItemTheme {}); - -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 - }); -}); - -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.size_inner } -} -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 } -} -impl ScenesView for App { - fn w_side (&self) -> u16 { 20 } - fn w_mid (&self) -> u16 { (self.width() as u16).saturating_sub(self.w_side()) } - fn h_scenes (&self) -> u16 { (self.height() as u16).saturating_sub(20) } -} - - -impl 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)); + for command in commands.into_iter() { + let result = command.execute(self); + match result { + Ok(undo) => { + self.history.push((command, undo)); + }, + Err(e) => { + self.history.push((command, None)); + return Err(e) } - *slot = Some(clip.clone()); - //Some(clip) + } + } + Ok(None) + }); + impl Draw for App { + fn draw (&self, to: &mut TuiOut) { + for (index, dsl) in self.mode.view.iter().enumerate() { + if let Err(e) = self.view(to, dsl) { + panic!("render #{index} failed ({e}): {dsl}"); + } + } + } + } + impl Draw for Mode { + fn draw (&self, _to: &mut TuiOut) { + //self.content().draw(to) + } + } + impl View for App { + fn view_expr <'a> (&'a self, to: &mut TuiOut, expr: &'a impl DslExpr) -> Usually<()> { + if evaluate_output_expression(self, to, expr)? + || evaluate_output_expression_tui(self, to, expr)? { + Ok(()) } 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()); + Err(format!("App::view_expr: unexpected: {expr:?}").into()) } } + fn view_word <'a> (&'a self, to: &mut TuiOut, dsl: &'a impl DslExpr) -> Usually<()> { + let mut frags = dsl.src()?.unwrap().split("/"); + match frags.next() { + Some(":logo") => to.place(&view_logo()), + Some(":status") => to.place(&Fixed::Y(1, "TODO: Status Bar")), + Some(":meters") => match frags.next() { + Some("input") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::Y(Align::s("Input Meters")))), + Some("output") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::Y(Align::s("Output Meters")))), + _ => panic!() + }, + Some(":tracks") => match frags.next() { + None => to.place(&"TODO tracks"), + Some("names") => to.place(&self.project.view_track_names(self.color.clone())),//Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Names")))), + Some("inputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Inputs")))), + Some("devices") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Devices")))), + Some("outputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Outputs")))), + _ => panic!() + }, + Some(":scenes") => match frags.next() { + None => to.place(&"TODO scenes"), + Some(":scenes/names") => to.place(&"TODO Scene Names"), + _ => panic!() + }, + Some(":editor") => to.place(&"TODO Editor"), + Some(":dialog") => match frags.next() { + Some("menu") => to.place(&if let Dialog::Menu(selected, items) = &self.dialog { + let items = items.clone(); + let selected = selected; + Some(Fill::XY(Thunk::new(move|to: &mut TuiOut|{ + for (index, MenuItem(item, _)) in items.0.iter().enumerate() { + to.place(&Push::Y((2 * index) as u16, + Tui::fg_bg( + if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, + if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, + Fixed::Y(2, Align::n(Fill::X(item))) + ))); + } + }))) + } else { + None + }), + _ => unimplemented!("App::view_word: {dsl:?} ({frags:?})"), + }, + Some(":templates") => to.place(&{ + let modes = self.config.modes.clone(); + let height = (modes.read().unwrap().len() * 2) as u16; + Fixed::Y(height, Min::X(30, Thunk::new(move |to: &mut TuiOut|{ + for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { + let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; + let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); + let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); + let fg1 = Rgb(224, 192, 128); + let fg2 = Rgb(224, 128, 32); + let field_name = Fill::X(Align::w(Tui::fg(fg1, name))); + let field_id = Fill::X(Align::e(Tui::fg(fg2, id))); + let field_info = Fill::X(Align::w(info)); + to.place(&Push::Y((2 * index) as u16, + Fixed::Y(2, Fill::X(Tui::bg(bg, Bsp::s( + Bsp::a(field_name, field_id), field_info)))))); + } + }))) + }), + Some(":sessions") => to.place(&Fixed::Y(6, Min::X(30, Thunk::new(|to: &mut TuiOut|{ + let fg = Rgb(224, 192, 128); + for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { + let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; + to.place(&Push::Y((2 * index) as u16, + &Fixed::Y(2, Fill::X(Tui::bg(bg, Align::w(Tui::fg(fg, name))))))); + } + })))), + Some(":browse/title") => to.place(&Fill::X(Align::w(FieldV(ItemColor::default(), + match self.dialog.browser_target().unwrap() { + BrowseTarget::SaveProject => "Save project:", + BrowseTarget::LoadProject => "Load project:", + BrowseTarget::ImportSample(_) => "Import sample:", + BrowseTarget::ExportSample(_) => "Export sample:", + BrowseTarget::ImportClip(_) => "Import clip:", + BrowseTarget::ExportClip(_) => "Export clip:", + }, Shrink::X(3, Fixed::Y(1, Tui::fg(Tui::g(96), RepeatH("๐Ÿญป")))))))), + Some(":device") => { + let selected = self.dialog.device_kind().unwrap(); + to.place(&Bsp::s(Tui::bold(true, "Add device"), Map::south(1, + move||device_kinds().iter(), + move|_label: &&'static str, i|{ + let bg = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; + let lb = if i == selected { "[ " } else { " " }; + let rb = if i == selected { " ]" } else { " " }; + Fill::X(Tui::bg(bg, Bsp::e(lb, Bsp::w(rb, "FIXME device name")))) }))) + }, + Some(":debug") => to.place(&Fixed::Y(1, format!("[{:?}]", to.area()))), + Some(_) => { + let views = self.config.views.read().unwrap(); + if let Some(dsl) = views.get(dsl.src()?.unwrap()) { + let dsl = dsl.clone(); + std::mem::drop(views); + self.view(to, &dsl)? + } else { + unimplemented!("{dsl:?}"); + } + }, + _ => unreachable!() + } + Ok(()) + } } - - pub fn browser (&self) -> Option<&Browse> { - if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } + fn view_logo () -> impl Content { + Fixed::XY(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), col!{ + Fixed::Y(1, ""), + Fixed::Y(1, ""), + Fixed::Y(1, "~~ โ•“โ”€โ•ฅโ”€โ•– โ•“โ”€โ”€โ•– โ•ฅ โ•– ~~~~~~~~~~~~"), + Fixed::Y(1, Bsp::e("~~~~ โ•‘ ~ โ•Ÿโ”€โ•Œ ~โ•Ÿโ”€< ~~ ", Bsp::e(Tui::fg(Rgb(230,100,40), "v0.3.0"), " ~~"))), + Fixed::Y(1, "~~~~ โ•จ ~ โ•™โ”€โ”€โ•œ โ•จ โ•œ ~~~~~~~~~~~~"), + }))) } - - pub fn device_pick (&mut self, index: usize) { - self.dialog = Dialog::Device(index); +} +pub mod gui { + impl Dialog { + pub fn welcome () -> Self { + Self::Menu(1, MenuItems([ + MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))), + MenuItem("Create new session".into(), Arc::new(Box::new(|app|Ok({ + app.dialog = Dialog::None; + app.mode = app.config.modes.clone().read().unwrap().get(":arranger").cloned().unwrap(); + })))), + MenuItem("Load old session".into(), Arc::new(Box::new(|_|Ok(())))), + ].into())) + } + pub fn menu_next (&self) -> Self { + match self { + Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.0.len()), items.clone()), + _ => Self::None + } + } + pub fn menu_prev (&self) -> Self { + match self { + Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.0.len()), items.clone()), + _ => Self::None + } + } + pub fn menu_selected (&self) -> Option { if let Self::Menu(selected, _) = self { Some(*selected) } else { None } } + pub fn device_kind (&self) -> Option { if let Self::Device(index) = self { Some(*index) } else { None } } + pub fn device_kind_next (&self) -> Option { self.device_kind().map(|index|(index + 1) % device_kinds().len()) } + pub fn device_kind_prev (&self) -> Option { self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1))) } + pub fn message (&self) -> Option<&str> { todo!() } + pub fn browser (&self) -> Option<&Arc> { todo!() } + pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() } } - - 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()) +} +pub mod audio { + 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:?}"); } + } + } + ); +} +pub mod glue { + use super::{*, model::*}; + has!(Jack<'static>: |self: App|self.jack); + has!(Pool: |self: App|self.pool); + has!(Dialog: |self: App|self.dialog); + has!(Clock: |self: App|self.project.clock); + has!(Option: |self: App|self.project.editor); + has!(Selection: |self: App|self.project.selection); + has!(Vec: |self: App|self.project.midi_ins); + has!(Vec: |self: App|self.project.midi_outs); + has!(Vec: |self: App|self.project.scenes); + has!(Vec: |self: App|self.project.tracks); + has!(Measure: |self: App|self.size); + has_clips!( |self: App|self.pool.clips); + maybe_has!(Track: |self: App| { MaybeHas::::get(&self.project) }; + { MaybeHas::::get_mut(&mut self.project) }); + maybe_has!(Scene: |self: App| { MaybeHas::::get(&self.project) }; + { MaybeHas::::get_mut(&mut self.project) }); + impl ScenesView for App { + fn w_side (&self) -> u16 { 20 } + fn w_mid (&self) -> u16 { (self.width() as u16).saturating_sub(self.w_side()) } + fn h_scenes (&self) -> u16 { (self.height() as u16).saturating_sub(20) } + } + impl Default for MenuItem { fn default () -> Self { Self("".into(), Arc::new(Box::new(|_|Ok(())))) } } + impl Default for AppCommand { fn default () -> Self { Self::Nop } } + impl PartialEq for MenuItem { fn eq (&self, other: &Self) -> bool { self.0 == other.0 } } + impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } } + impl HasClipsSize for App { fn clips_size (&self) -> &Measure { &self.project.size_inner } } + 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 AsRef> for MenuItems { fn as_ref (&self) -> &Arc<[MenuItem]> { &self.0 } } + impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) }); + impl_debug!(Condition |self, w| { write!(w, "*") }); + /// Default is always empty map regardless if `E` and `C` implement [Default]. + impl Default for Bind { fn default () -> Self { Self(Default::default()) } } +} +/// Command-line configuration. +#[cfg(feature = "cli")] pub mod cli { + use super::{*, model::*}; + use clap::{self, Parser, Subcommand}; + /// CLI banner. + const HEADER: &'static str = r#" + ~ โ•“โ”€โ•ฅโ”€โ•– โ•“โ”€โ”€โ•– โ•ฅ โ•– ~~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ ~~~~~~ ~ ~~~ + ~~ โ•‘ ~ โ•Ÿโ”€โ•Œ ~โ•Ÿโ”€< ~ v0.3.0, 2025 sum(m)er @ the nose of the cat. ~ + ~~~ โ•จ ~ โ•™โ”€โ”€โ•œ โ•จ โ•œ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ ~~ ~~ ~ ~~ + On first run, Tek will create configuration and state dirs: + * [x] ~/.config/tek - config + * [ ] ~/.local/share/tek - projects + * [ ] ~/.local/lib/tek - plugins + * [ ] ~/.cache/tek - cache + ~"#; + #[derive(Debug, Parser)] + #[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))] + pub struct Cli { + /// Pre-defined configuration modes. + /// + /// TODO: Replace these with scripted configurations. + #[command(subcommand)] mode: Option, + /// Name of JACK client + #[arg(short='n', long)] name: Option, + /// Whether to attempt to become transport master + #[arg(short='S', long, default_value_t = false)] sync_lead: bool, + /// Whether to sync to external transport master + #[arg(short='s', long, default_value_t = true)] sync_follow: bool, + /// Initial tempo in beats per minute + #[arg(short='b', long, default_value = None)] bpm: Option, + /// Whether to include a transport toolbar (default: true) + #[arg(short='t', long, default_value_t = true)] show_clock: bool, + /// MIDI outs to connect to (multiple instances accepted) + #[arg(short='I', long)] midi_from: Vec, + /// MIDI outs to connect to (multiple instances accepted) + #[arg(short='i', long)] midi_from_re: Vec, + /// MIDI ins to connect to (multiple instances accepted) + #[arg(short='O', long)] midi_to: Vec, + /// MIDI ins to connect to (multiple instances accepted) + #[arg(short='o', long)] midi_to_re: Vec, + /// Audio outs to connect to left input + #[arg(short='l', long)] left_from: Vec, + /// Audio outs to connect to right input + #[arg(short='r', long)] right_from: Vec, + /// Audio ins to connect from left output + #[arg(short='L', long)] left_to: Vec, + /// Audio ins to connect from right output + #[arg(short='R', long)] right_to: Vec, + } + /// Application modes + #[derive(Debug, Clone, Subcommand)] + enum LaunchMode { + /// Create a new session instead of loading the previous one. + New, + } + impl Cli { + fn midi_froms (&self) -> Vec { + Connect::collect(&self.midi_from, &[] as &[&str], &self.midi_from_re) + } + fn midi_tos (&self) -> Vec { + Connect::collect(&self.midi_to, &[] as &[&str], &self.midi_to_re) + } + pub fn run (&self) -> Usually<()> { + let name = self.name.as_ref().map_or("tek", |x|x.as_str()); + let tracks = vec![]; + let scenes = vec![]; + let empty = &[] as &[&str]; + let left_froms = Connect::collect(&self.left_from, empty, empty); + let left_tos = Connect::collect(&self.left_to, empty, empty); + let right_froms = Connect::collect(&self.right_from, empty, empty); + let right_tos = Connect::collect(&self.right_to, empty, empty); + let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; + let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()]; + let mut config = Config::new(None); + config.init()?; + Tui::new()?.run(&Jack::new_run(&name, move|jack|{ + let midi_ins = { + let mut midi_ins = vec![]; + for (index, connect) in self.midi_froms().iter().enumerate() { + midi_ins.push(jack.midi_in(&format!("M/{index}"), &[connect.clone()])?); + } + midi_ins }; - let track = self.track_mut().expect("no active track"); - track.devices.push(sampler); - Ok(()) - }, - 1 => { - todo!(); - Ok(()) - }, - _ => unreachable!(), + let midi_outs = { + let mut midi_outs = vec![]; + for (index, connect) in self.midi_tos().iter().enumerate() { + midi_outs.push(jack.midi_out(&format!("{index}/M"), &[connect.clone()])?); + }; + midi_outs + }; + let project = Arrangement { + name: Default::default(), + color: ItemTheme::random(), + jack: jack.clone(), + clock: Clock::new(&jack, self.bpm)?, + tracks, + scenes, + selection: Selection::TrackClip { track: 0, scene: 0 }, + midi_ins, + midi_outs, + ..Default::default() + }; + let app = App { + jack: jack.clone(), + color: ItemTheme::random(), + dialog: Dialog::welcome(), + mode: config.modes.clone().read().unwrap().get(":menu").cloned().unwrap(), + config, + project, + ..Default::default() + }; + jack.sync_lead(self.sync_lead, |mut state|{ + let clock = app.clock(); + clock.playhead.update_from_sample(state.position.frame() as f64); + state.position.bbt = Some(clock.bbt()); + state.position + })?; + jack.sync_follow(self.sync_follow)?; + Ok(app) + })?) } } - - pub fn update_clock (&self) { - ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) - } +} +/// Command-line entrypoint. +#[cfg(feature = "cli")] pub fn main () -> Usually<()> { + crate::cli::Cli::parse().run() } -/// Various possible dialog modes. -#[derive(Debug, Clone, Default, PartialEq)] -pub enum Dialog { - #[default] None, - Help(usize), - Menu(usize, MenuItems), - Device(usize), - Message(Arc), - Browse(BrowseTarget, Arc), - Options, -} +/////////////////////////////////////////////////////////////////////////////////////////////////// -#[derive(Debug, Clone, Default, PartialEq)] -pub struct MenuItems(pub Arc<[MenuItem]>); +//pub fn view_nil (_: &App) -> TuiCb { + //|to|to.place(&Fill::XY("ยท")) +//} -impl AsRef> for MenuItems { - fn as_ref (&self) -> &Arc<[MenuItem]> { - &self.0 - } -} + //Bsp::s("", + //Map::south(1, + //move||app.config.binds.layers.iter() + //.filter_map(|a|(a.0)(app).then_some(a.1)) + //.flat_map(|a|a) + //.filter_map(|x|if let Value::Exp(_, iter)=x.value{ Some(iter) } else { None }) + //.skip(offset) + //.take(20), + //|mut b,i|Fixed::X(60, Align::w(Bsp::e("(", Bsp::e( + //b.next().map(|t|Fixed::X(16, Align::w(Tui::fg(Rgb(64,224,0), format!("{}", t.value))))), + //Bsp::e(" ", Align::w(format!("{}", b.0.0.trim()))))))))))), -#[derive(Clone)] -pub struct MenuItem( - /// Label - pub Arc, - /// Callback - pub ArcUsually<()> + Send + Sync>> -); + //Dialog::Browse(BrowseTarget::Load, browser) => { + //"bobcat".boxed() + ////Bsp::s( + ////Fill::X(Align::w(Margin::XY(1, 1, Bsp::e( + ////Tui::bold(true, " Load project: "), + ////Shrink::X(3, Fixed::Y(1, RepeatH("๐Ÿญป"))))))), + ////Outer(true, Style::default().fg(Tui::g(96))) + ////.enclose(Fill::XY(browser))) + //}, + //Dialog::Browse(BrowseTarget::Export, browser) => { + //"bobcat".boxed() + ////Bsp::s( + ////Fill::X(Align::w(Margin::XY(1, 1, Bsp::e( + ////Tui::bold(true, " Export: "), + ////Shrink::X(3, Fixed::Y(1, RepeatH("๐Ÿญป"))))))), + ////Outer(true, Style::default().fg(Tui::g(96))) + ////.enclose(Fill::XY(browser))) + //}, + //Dialog::Browse(BrowseTarget::Import, browser) => { + //"bobcat".boxed() + ////Bsp::s( + ////Fill::X(Align::w(Margin::XY(1, 1, Bsp::e( + ////Tui::bold(true, " Import: "), + ////Shrink::X(3, Fixed::Y(1, RepeatH("๐Ÿญป"))))))), + ////Outer(true, Style::default().fg(Tui::g(96))) + ////.enclose(Fill::XY(browser))) + //}, +// + //pub fn view_history (&self) -> impl Content { + //Fixed::Y(1, Fill::X(Align::w(FieldH(self.color, + //format!("History ({})", self.history.len()), + //self.history.last().map(|last|Fill::X(Align::w(format!("{:?}", last.0)))))))) + //} + //pub fn view_status_h2 (&self) -> impl Content { + //self.update_clock(); + //let theme = self.color; + //let clock = self.clock(); + //let playing = clock.is_rolling(); + //let cache = clock.view_cache.clone(); + ////let selection = self.selection().describe(self.tracks(), self.scenes()); + //let hist_len = self.history.len(); + //let hist_last = self.history.last(); + //Fixed::Y(2, Stack::east(move|add: &mut dyn FnMut(&dyn Draw)|{ + //add(&Fixed::X(5, Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, + //Either::new(false, // TODO + //Thunk::new(move||Fixed::X(9, Either::new(playing, + //Tui::fg(Rgb(0, 255, 0), " PLAYING "), + //Tui::fg(Rgb(255, 128, 0), " STOPPED "))) + //), + //Thunk::new(move||Fixed::X(5, Either::new(playing, + //Tui::fg(Rgb(0, 255, 0), Bsp::s(" ๐Ÿญ๐Ÿญ‘๐Ÿฌฝ ", " ๐Ÿญž๐Ÿญœ๐Ÿญ˜ ",)), + //Tui::fg(Rgb(255, 128, 0), Bsp::s(" โ–—โ–„โ–– ", " โ–โ–€โ–˜ ",)))) + //) + //) + //))); + //add(&" "); + //{ + //let cache = cache.read().unwrap(); + //add(&Fixed::X(15, Align::w(Bsp::s( + //FieldH(theme, "Beat", cache.beat.view.clone()), + //FieldH(theme, "Time", cache.time.view.clone()), + //)))); + //add(&Fixed::X(13, Align::w(Bsp::s( + //Fill::X(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), + //Fill::X(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), + //)))); + //add(&Fixed::X(12, Align::w(Bsp::s( + //Fill::X(Align::w(FieldH(theme, "Buf", cache.buf.view.clone()))), + //Fill::X(Align::w(FieldH(theme, "Lat", cache.lat.view.clone()))), + //)))); + ////add(&Bsp::s( + //////Fill::X(Align::w(FieldH(theme, "Selected", Align::w(selection)))), + ////Fill::X(Align::w(FieldH(theme, format!("History ({})", hist_len), + ////hist_last.map(|last|Fill::X(Align::w(format!("{:?}", last.0))))))), + ////"" + ////)); + //////if let Some(last) = self.history.last() { + //////add(&FieldV(theme, format!("History ({})", self.history.len()), + //////Fill::X(Align::w(format!("{:?}", last.0))))); + //////} + //} + //})) + //} + //pub fn view_status_v (&self) -> impl Content + use<'_> { + //self.update_clock(); + //let cache = self.project.clock.view_cache.read().unwrap(); + //let theme = self.color; + //let playing = self.clock().is_rolling(); + //Tui::bg(theme.darker.rgb, Fixed::XY(20, 5, Outer(true, Style::default().fg(Tui::g(96))).enclose( + //col!( + //Fill::X(Align::w(Bsp::e( + //Align::w(Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, + //Either::new(false, // TODO + //Thunk::new(move||Fixed::X(9, Either::new(playing, + //Tui::fg(Rgb(0, 255, 0), " PLAYING "), + //Tui::fg(Rgb(255, 128, 0), " STOPPED "))) + //), + //Thunk::new(move||Fixed::X(5, Either::new(playing, + //Tui::fg(Rgb(0, 255, 0), Bsp::s(" ๐Ÿญ๐Ÿญ‘๐Ÿฌฝ ", " ๐Ÿญž๐Ÿญœ๐Ÿญ˜ ",)), + //Tui::fg(Rgb(255, 128, 0), Bsp::s(" โ–—โ–„โ–– ", " โ–โ–€โ–˜ ",)))) + //) + //) + //)), + //Bsp::s( + //FieldH(theme, "Beat", cache.beat.view.clone()), + //FieldH(theme, "Time", cache.time.view.clone()), + //), + //))), + //Fill::X(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), + //Fill::X(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), + //Fill::X(Align::w(FieldH(theme, "Buf", Bsp::e(cache.buf.view.clone(), Bsp::e(" = ", cache.lat.view.clone()))))), + //)))) + //} + //pub fn view_status (&self) -> impl Content + use<'_> { + //self.update_clock(); + //let cache = self.project.clock.view_cache.read().unwrap(); + //view_status(Some(self.project.selection.describe(self.tracks(), self.scenes())), + //cache.sr.view.clone(), cache.buf.view.clone(), cache.lat.view.clone()) + //} + //pub fn view_transport (&self) -> impl Content + use<'_> { + //self.update_clock(); + //let cache = self.project.clock.view_cache.read().unwrap(); + //view_transport(self.project.clock.is_rolling(), + //cache.bpm.view.clone(), cache.beat.view.clone(), cache.time.view.clone()) + //} + //pub fn view_editor (&self) -> impl Content + use<'_> { + //let bg = self.editor() + //.and_then(|editor|editor.clip().clone()) + //.map(|clip|clip.read().unwrap().color.darker) + //.unwrap_or(self.color.darker); + //Fill::XY(Tui::bg(bg.rgb, self.editor())) + //} + //pub fn view_editor_status (&self) -> impl Content + use<'_> { + //self.editor().map(|e|Fixed::X(20, Outer(true, Style::default().fg(Tui::g(96))).enclose( + //Fill::Y(Align::n(Bsp::s(e.clip_status(), e.edit_status())))))) + //} + //pub fn view_midi_ins_status (&self) -> impl Content + use<'_> { + //self.project.view_midi_ins_status(self.color) + //} + //pub fn view_midi_outs_status (&self) -> impl Content + use<'_> { + //self.project.view_midi_outs_status(self.color) + //} + //pub fn view_audio_ins_status (&self) -> impl Content + use<'_> { + //self.project.view_audio_ins_status(self.color) + //} + //pub fn view_audio_outs_status (&self) -> impl Content + use<'_> { + //self.project.view_audio_outs_status(self.color) + //} + //pub fn view_scenes (&self) -> impl Content + use<'_> { + //Bsp::e( + //Fixed::X(20, Align::nw(self.project.view_scenes_names())), + //self.project.view_scenes_clips(), + //) + //} + //pub fn view_scenes_names (&self) -> impl Content + use<'_> { + //self.project.view_scenes_names() + //} + //pub fn view_scenes_clips (&self) -> impl Content + use<'_> { + //self.project.view_scenes_clips() + //} + //pub fn view_tracks_inputs <'a> (&'a self) -> impl Content + use<'a> { + //Fixed::Y(1 + self.project.midi_ins.len() as u16, + //self.project.view_inputs(self.color)) + //} + //pub fn view_tracks_outputs <'a> (&'a self) -> impl Content + use<'a> { + //self.project.view_outputs(self.color) + //} + //pub fn view_tracks_devices <'a> (&'a self) -> impl Content + use<'a> { + //Fixed::Y(4, self.project.view_track_devices(self.color)) + //} + //pub fn view_tracks_names <'a> (&'a self) -> impl Content + use<'a> { + //Fixed::Y(2, self.project.view_track_names(self.color)) + //} + //pub fn view_pool (&self) -> impl Content + use<'_> { + //Fixed::X(20, Bsp::s( + //Fill::X(Align::w(FieldH(self.color, "Clip pool:", ""))), + //Fill::Y(Align::n(Tui::bg(Rgb(0, 0, 0), Outer(true, Style::default().fg(Tui::g(96))) + //.enclose(PoolView(&self.pool))))))) + //} + //pub fn view_samples_keys (&self) -> impl Content + use<'_> { + //self.project.sampler().map(|s|s.view_list(true, self.editor().unwrap())) + //} + //pub fn view_samples_grid (&self) -> impl Content + use<'_> { + //self.project.sampler().map(|s|s.view_grid()) + //} + //pub fn view_sample_viewer (&self) -> impl Content + use<'_> { + //self.project.sampler().map(|s|s.view_sample(self.editor().unwrap().get_note_pos())) + //} + //pub fn view_sample_info (&self) -> impl Content + use<'_> { + //self.project.sampler().map(|s|s.view_sample_info(self.editor().unwrap().get_note_pos())) + //} + //pub fn view_sample_status (&self) -> impl Content + use<'_> { + //self.project.sampler().map(|s|Outer(true, Style::default().fg(Tui::g(96))).enclose( + //Fill::Y(Align::n(s.view_sample_status(self.editor().unwrap().get_note_pos()))))) + //} + ////let options = ||["Projects", "Settings", "Help", "Quit"].iter(); + ////let option = |a,i|Tui::fg(Rgb(255,255,255), format!("{}", a)); + ////Bsp::s(Tui::bold(true, "tek!"), Bsp::s("", Map::south(1, options, option))) -impl Default for MenuItem { - fn default () -> Self { Self("".into(), Arc::new(Box::new(|_|Ok(())))) } -} + //AppCommand => { + //("x/inc" / + //("stop-all") => todo!(),//app.project.stop_all(), + //("enqueue", clip: Option>>) => todo!(), + //("history", delta: isize) => todo!(), + //("zoom", zoom: usize) => todo!(), + //("select", selection: Selection) => todo!(), + //("dialog" / command: DialogCommand) => todo!(), + //("project" / command: ArrangementCommand) => todo!(), + //("clock" / command: ClockCommand) => todo!(), + //("sampler" / command: SamplerCommand) => todo!(), + //("pool" / command: PoolCommand) => todo!(), + //("edit" / editor: MidiEditCommand) => todo!(), + //}; -impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) }); + //DialogCommand; + + //ArrangementCommand; + + //ClockCommand; + + //SamplerCommand; + + //PoolCommand; + + //MidiEditCommand; + + +//take!(DialogCommand |state: App, iter|Take::take(&state.dialog, iter)); +//#[derive(Clone, Debug)] +//pub enum DialogCommand { + //Open { dialog: Dialog }, + //Close +//} + +//impl Command> for DialogCommand { + //fn execute (self, state: &mut Option) -> Perhaps { + //match self { + //Self::Open { dialog } => { + //*state = Some(dialog); + //}, + //Self::Close => { + //*state = None; + //} + //}; + //Ok(None) + //} +//} + +//dsl!(DialogCommand: |self: Dialog, iter|todo!()); +//Dsl::take(&mut self.dialog, iter)); + +//#[tengri_proc::command(Option)]//Nope. +//impl DialogCommand { + //fn open (dialog: &mut Option, new: Dialog) -> Perhaps { + //*dialog = Some(new); + //Ok(None) + //} + //fn close (dialog: &mut Option) -> Perhaps { + //*dialog = None; + //Ok(None) + //} +//} +// +//dsl_bind!(AppCommand: App { + //enqueue = |app, clip: Option>>| { todo!() }; + //history = |app, delta: isize| { todo!() }; + //zoom = |app, zoom: usize| { todo!() }; + //stop_all = |app| { app.tracks_stop_all(); Ok(None) }; + ////dialog = |app, command: DialogCommand| + ////Ok(command.delegate(&mut app.dialog, |c|Self::Dialog{command: c})?); + //project = |app, command: ArrangementCommand| + //Ok(command.delegate(&mut app.project, |c|Self::Project{command: c})?); + //clock = |app, command: ClockCommand| + //Ok(command.execute(app.clock_mut())?.map(|c|Self::Clock{command: c})); + //sampler = |app, command: SamplerCommand| + //Ok(app.project.sampler_mut().map(|s|command.delegate(s, |command|Self::Sampler{command})) + //.transpose()?.flatten()); + //pool = |app, command: PoolCommand| { + //let undo = command.clone().delegate(&mut app.pool, |command|AppCommand::Pool{command})?; + //// update linked editor after pool action + //match command { + //// autoselect: automatically load selected clip in editor + //PoolCommand::Select { .. } | + //// autocolor: update color in all places simultaneously + //PoolCommand::Clip { command: PoolClipCommand::SetColor { .. } } => { + //let clip = app.pool.clip().clone(); + //app.editor_mut().map(|editor|editor.set_clip(clip.as_ref())) + //}, + //_ => None + //}; + //Ok(undo) + //}; + //select = |app, selection: Selection| { + //*app.project.selection_mut() = selection; + ////todo! + ////if let Some(ref mut editor) = app.editor_mut() { + ////editor.set_clip(match selection { + ////Selection::TrackClip { track, scene } if let Some(Some(Some(clip))) = app + ////.project + ////.scenes.get(scene) + ////.map(|s|s.clips.get(track)) + ////=> + ////Some(clip), + ////_ => + ////None + ////}); + ////} + //Ok(None) + ////("select" [t: usize, s: usize] Some(match (t.expect("no track"), s.expect("no scene")) { + ////(0, 0) => Self::Select(Selection::Mix), + ////(t, 0) => Self::Select(Selection::Track(t)), + ////(0, s) => Self::Select(Selection::Scene(s)), + ////(t, s) => Self::Select(Selection::TrackClip { track: t, scene: s }) }))) + //// autoedit: load focused clip in editor. + //}; + ////fn color (app: &mut App, theme: ItemTheme) -> Perhaps { + ////Ok(app.set_color(Some(theme)).map(|theme|Self::Color{theme})) + ////} + ////fn launch (app: &mut App) -> Perhaps { + ////app.project.launch(); + ////Ok(None) + ////} + //toggle_editor = |app, value: bool|{ app.toggle_editor(Some(value)); Ok(None) }; + //editor = |app, command: MidiEditCommand| Ok(if let Some(editor) = app.editor_mut() { + //let undo = command.clone().delegate(editor, |command|AppCommand::Editor{command})?; + //// update linked sampler after editor action + //app.project.sampler_mut().map(|sampler|match command { + //// autoselect: automatically select sample in sampler + //MidiEditCommand::SetNotePos { pos } => { sampler.set_note_pos(pos); }, + //_ => {} + //}); + //undo + //} else { + //None + //}); +//}); +//take!(ClockCommand |state: App, iter|Take::take(state.clock(), iter)); +//take!(MidiEditCommand |state: App, iter|Ok(state.editor().map(|x|Take::take(x, iter)).transpose()?.flatten())); +//take!(PoolCommand |state: App, iter|Take::take(&state.pool, iter)); +//take!(SamplerCommand |state: App, iter|Ok(state.project.sampler().map(|x|Take::take(x, iter)).transpose()?.flatten())); +//take!(ArrangementCommand |state: App, iter|Take::take(&state.project, iter)); -impl PartialEq for MenuItem { - fn eq (&self, other: &Self) -> bool { - self.0 == other.0 - } -} -impl Dialog { - pub fn welcome () -> Self { - Self::Menu(1, MenuItems([ - MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))), - MenuItem("Create new session".into(), Arc::new(Box::new(|app|Ok({ - app.dialog = Dialog::None; - app.mode = app.config.modes.clone().read().unwrap().get(":arranger").cloned().unwrap(); - })))), - MenuItem("Load old session".into(), Arc::new(Box::new(|_|Ok(())))), - ].into())) - } - pub fn menu_next (&self) -> Self { - match self { - Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.0.len()), items.clone()), - _ => Self::None - } - } - pub fn menu_prev (&self) -> Self { - match self { - Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.0.len()), items.clone()), - _ => Self::None - } - } - pub fn menu_selected (&self) -> Option { - if let Self::Menu(selected, _) = self { Some(*selected) } else { None } - } - pub fn device_kind (&self) -> Option { - if let Self::Device(index) = self { Some(*index) } else { None } - } - pub fn device_kind_next (&self) -> Option { - self.device_kind().map(|index|(index + 1) % device_kinds().len()) - } - pub fn device_kind_prev (&self) -> Option { - self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1))) - } - pub fn message (&self) -> Option<&str> { - todo!() - } - pub fn browser (&self) -> Option<&Arc> { - todo!() - } - pub fn browser_target (&self) -> Option<&BrowseTarget> { - todo!() - } -} /////////////////////////////////////////////////////////////////////////////////////////////////// //has_editor!(|self: App|{ @@ -466,3 +1351,5 @@ impl Dialog { //editor_h = 15; //is_editing = self.editor.is_some(); //}); + + diff --git a/app/tek_bind.rs b/app/tek_bind.rs deleted file mode 100644 index 84763b70..00000000 --- a/app/tek_bind.rs +++ /dev/null @@ -1,325 +0,0 @@ -use crate::*; - -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, "*") }); - -handle!(TuiIn:|self: App, input|{ - let mut commands = vec![]; - for id in self.mode.keys.iter() { - if let Some(event_map) = self.config.binds.clone().read().unwrap().get(id.as_ref()) { - if let Some(bindings) = event_map.query(input.event()) { - for binding in bindings { - for command in binding.commands.iter() { - if let Some(command) = self.from(command)? as Option { - commands.push(command) - } - } - } - } - } - } - for command in commands.into_iter() { - let result = command.execute(self); - match result { - Ok(undo) => { - self.history.push((command, undo)); - }, - Err(e) => { - self.history.push((command, None)); - return Err(e) - } - } - } - Ok(None) -}); - -#[derive(Debug, Copy, Clone)] -pub enum Axis { X, Y, Z, I } - -impl<'a> DslNs<'a, AppCommand> for App {} -impl<'a> DslNsExprs<'a, AppCommand> for App {} -impl<'a> DslNsWords<'a, AppCommand> for App { - dsl_words!('a |app| -> AppCommand { - "x/inc" => AppCommand::Inc { axis: Axis::X }, - "x/dec" => AppCommand::Dec { axis: Axis::X }, - "y/inc" => AppCommand::Inc { axis: Axis::Y }, - "y/dec" => AppCommand::Dec { axis: Axis::Y }, - "confirm" => AppCommand::Confirm, - "cancel" => AppCommand::Cancel, - }); -} - -impl Default for AppCommand { fn default () -> Self { Self::Nop } } - -def_command!(AppCommand: |app: App| { - Nop => Ok(None), - Confirm => Ok(match &app.dialog { - Dialog::Menu(index, items) => { - let callback = items.0[*index].1.clone(); - callback(app)?; - None - }, - _ => todo!(), - }), - Cancel => todo!(), // TODO delegate: - Inc { axis: Axis } => Ok(match (&app.dialog, axis) { - (Dialog::None, _) => todo!(), - (Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_next() } - .execute(app)?, - _ => todo!() - }), - Dec { axis: Axis } => Ok(match (&app.dialog, axis) { - (Dialog::None, _) => None, - (Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_prev() } - .execute(app)?, - _ => todo!() - }), - SetDialog { dialog: Dialog } => { - swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }) - }, -}); - - //AppCommand => { - //("x/inc" / - //("stop-all") => todo!(),//app.project.stop_all(), - //("enqueue", clip: Option>>) => todo!(), - //("history", delta: isize) => todo!(), - //("zoom", zoom: usize) => todo!(), - //("select", selection: Selection) => todo!(), - //("dialog" / command: DialogCommand) => todo!(), - //("project" / command: ArrangementCommand) => todo!(), - //("clock" / command: ClockCommand) => todo!(), - //("sampler" / command: SamplerCommand) => todo!(), - //("pool" / command: PoolCommand) => todo!(), - //("edit" / editor: MidiEditCommand) => todo!(), - //}; - - //DialogCommand; - - //ArrangementCommand; - - //ClockCommand; - - //SamplerCommand; - - //PoolCommand; - - //MidiEditCommand; - - -//take!(DialogCommand |state: App, iter|Take::take(&state.dialog, iter)); -//#[derive(Clone, Debug)] -//pub enum DialogCommand { - //Open { dialog: Dialog }, - //Close -//} - -//impl Command> for DialogCommand { - //fn execute (self, state: &mut Option) -> Perhaps { - //match self { - //Self::Open { dialog } => { - //*state = Some(dialog); - //}, - //Self::Close => { - //*state = None; - //} - //}; - //Ok(None) - //} -//} - -//dsl!(DialogCommand: |self: Dialog, iter|todo!()); -//Dsl::take(&mut self.dialog, iter)); - -//#[tengri_proc::command(Option)]//Nope. -//impl DialogCommand { - //fn open (dialog: &mut Option, new: Dialog) -> Perhaps { - //*dialog = Some(new); - //Ok(None) - //} - //fn close (dialog: &mut Option) -> Perhaps { - //*dialog = None; - //Ok(None) - //} -//} -// -//dsl_bind!(AppCommand: App { - //enqueue = |app, clip: Option>>| { todo!() }; - //history = |app, delta: isize| { todo!() }; - //zoom = |app, zoom: usize| { todo!() }; - //stop_all = |app| { app.tracks_stop_all(); Ok(None) }; - ////dialog = |app, command: DialogCommand| - ////Ok(command.delegate(&mut app.dialog, |c|Self::Dialog{command: c})?); - //project = |app, command: ArrangementCommand| - //Ok(command.delegate(&mut app.project, |c|Self::Project{command: c})?); - //clock = |app, command: ClockCommand| - //Ok(command.execute(app.clock_mut())?.map(|c|Self::Clock{command: c})); - //sampler = |app, command: SamplerCommand| - //Ok(app.project.sampler_mut().map(|s|command.delegate(s, |command|Self::Sampler{command})) - //.transpose()?.flatten()); - //pool = |app, command: PoolCommand| { - //let undo = command.clone().delegate(&mut app.pool, |command|AppCommand::Pool{command})?; - //// update linked editor after pool action - //match command { - //// autoselect: automatically load selected clip in editor - //PoolCommand::Select { .. } | - //// autocolor: update color in all places simultaneously - //PoolCommand::Clip { command: PoolClipCommand::SetColor { .. } } => { - //let clip = app.pool.clip().clone(); - //app.editor_mut().map(|editor|editor.set_clip(clip.as_ref())) - //}, - //_ => None - //}; - //Ok(undo) - //}; - //select = |app, selection: Selection| { - //*app.project.selection_mut() = selection; - ////todo! - ////if let Some(ref mut editor) = app.editor_mut() { - ////editor.set_clip(match selection { - ////Selection::TrackClip { track, scene } if let Some(Some(Some(clip))) = app - ////.project - ////.scenes.get(scene) - ////.map(|s|s.clips.get(track)) - ////=> - ////Some(clip), - ////_ => - ////None - ////}); - ////} - //Ok(None) - ////("select" [t: usize, s: usize] Some(match (t.expect("no track"), s.expect("no scene")) { - ////(0, 0) => Self::Select(Selection::Mix), - ////(t, 0) => Self::Select(Selection::Track(t)), - ////(0, s) => Self::Select(Selection::Scene(s)), - ////(t, s) => Self::Select(Selection::TrackClip { track: t, scene: s }) }))) - //// autoedit: load focused clip in editor. - //}; - ////fn color (app: &mut App, theme: ItemTheme) -> Perhaps { - ////Ok(app.set_color(Some(theme)).map(|theme|Self::Color{theme})) - ////} - ////fn launch (app: &mut App) -> Perhaps { - ////app.project.launch(); - ////Ok(None) - ////} - //toggle_editor = |app, value: bool|{ app.toggle_editor(Some(value)); Ok(None) }; - //editor = |app, command: MidiEditCommand| Ok(if let Some(editor) = app.editor_mut() { - //let undo = command.clone().delegate(editor, |command|AppCommand::Editor{command})?; - //// update linked sampler after editor action - //app.project.sampler_mut().map(|sampler|match command { - //// autoselect: automatically select sample in sampler - //MidiEditCommand::SetNotePos { pos } => { sampler.set_note_pos(pos); }, - //_ => {} - //}); - //undo - //} else { - //None - //}); -//}); -//take!(ClockCommand |state: App, iter|Take::take(state.clock(), iter)); -//take!(MidiEditCommand |state: App, iter|Ok(state.editor().map(|x|Take::take(x, iter)).transpose()?.flatten())); -//take!(PoolCommand |state: App, iter|Take::take(&state.pool, iter)); -//take!(SamplerCommand |state: App, iter|Ok(state.project.sampler().map(|x|Take::take(x, iter)).transpose()?.flatten())); -//take!(ArrangementCommand |state: App, iter|Take::take(&state.project, iter)); diff --git a/app/tek_cfg.rs b/app/tek_cfg.rs deleted file mode 100644 index ee323bb6..00000000 --- a/app/tek_cfg.rs +++ /dev/null @@ -1,65 +0,0 @@ -use crate::*; - -/// 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/app/tek_cli.rs b/app/tek_cli.rs deleted file mode 100644 index 56e03ff6..00000000 --- a/app/tek_cli.rs +++ /dev/null @@ -1,138 +0,0 @@ -pub(crate) use tek::*; -pub(crate) use tek_device::*; -pub(crate) use tek_engine::*; -pub(crate) use tengri::{*, tui::*}; -pub(crate) use clap::{self, Parser, Subcommand}; - -/// Application entrypoint. -pub fn main () -> Usually<()> { - Cli::parse().run() -} - -/// CLI header -const HEADER: &'static str = r#" -~ โ•“โ”€โ•ฅโ”€โ•– โ•“โ”€โ”€โ•– โ•ฅ โ•– ~~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ ~~~~~~ ~ ~~~ - ~~ โ•‘ ~ โ•Ÿโ”€โ•Œ ~โ•Ÿโ”€< ~ v0.3.0, 2025 sum(m)er @ the nose of the cat. ~ -~~~ โ•จ ~ โ•™โ”€โ”€โ•œ โ•จ โ•œ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ ~~ ~~ ~ ~~ - On first run, Tek will create configuration and state dirs: - * [x] ~/.config/tek - config - * [ ] ~/.local/share/tek - projects - * [ ] ~/.local/lib/tek - plugins - * [ ] ~/.cache/tek - cache -~"#; - -#[derive(Debug, Parser)] -#[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))] -pub struct Cli { - /// Pre-defined configuration modes. - /// - /// TODO: Replace these with scripted configurations. - #[command(subcommand)] mode: Option, - /// Name of JACK client - #[arg(short='n', long)] name: Option, - /// Whether to attempt to become transport master - #[arg(short='S', long, default_value_t = false)] sync_lead: bool, - /// Whether to sync to external transport master - #[arg(short='s', long, default_value_t = true)] sync_follow: bool, - /// Initial tempo in beats per minute - #[arg(short='b', long, default_value = None)] bpm: Option, - /// Whether to include a transport toolbar (default: true) - #[arg(short='t', long, default_value_t = true)] show_clock: bool, - /// MIDI outs to connect to (multiple instances accepted) - #[arg(short='I', long)] midi_from: Vec, - /// MIDI outs to connect to (multiple instances accepted) - #[arg(short='i', long)] midi_from_re: Vec, - /// MIDI ins to connect to (multiple instances accepted) - #[arg(short='O', long)] midi_to: Vec, - /// MIDI ins to connect to (multiple instances accepted) - #[arg(short='o', long)] midi_to_re: Vec, - /// Audio outs to connect to left input - #[arg(short='l', long)] left_from: Vec, - /// Audio outs to connect to right input - #[arg(short='r', long)] right_from: Vec, - /// Audio ins to connect from left output - #[arg(short='L', long)] left_to: Vec, - /// Audio ins to connect from right output - #[arg(short='R', long)] right_to: Vec, -} - -/// Application modes -#[derive(Debug, Clone, Subcommand)] -pub enum LaunchMode { - /// Create a new session instead of loading the previous one. - New, -} - -impl Cli { - fn midi_froms (&self) -> Vec { - Connect::collect(&self.midi_from, &[] as &[&str], &self.midi_from_re) - } - fn midi_tos (&self) -> Vec { - Connect::collect(&self.midi_to, &[] as &[&str], &self.midi_to_re) - } - pub fn run (&self) -> Usually<()> { - let name = self.name.as_ref().map_or("tek", |x|x.as_str()); - let tracks = vec![]; - let scenes = vec![]; - let empty = &[] as &[&str]; - let left_froms = Connect::collect(&self.left_from, empty, empty); - let left_tos = Connect::collect(&self.left_to, empty, empty); - let right_froms = Connect::collect(&self.right_from, empty, empty); - let right_tos = Connect::collect(&self.right_to, empty, empty); - let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; - let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()]; - let mut config = Config::new(None); - config.init()?; - Tui::new()?.run(&Jack::new_run(&name, move|jack|{ - let midi_ins = { - let mut midi_ins = vec![]; - for (index, connect) in self.midi_froms().iter().enumerate() { - midi_ins.push(jack.midi_in(&format!("M/{index}"), &[connect.clone()])?); - } - midi_ins - }; - let midi_outs = { - let mut midi_outs = vec![]; - for (index, connect) in self.midi_tos().iter().enumerate() { - midi_outs.push(jack.midi_out(&format!("{index}/M"), &[connect.clone()])?); - }; - midi_outs - }; - let project = Arrangement { - name: Default::default(), - color: ItemTheme::random(), - jack: jack.clone(), - clock: Clock::new(&jack, self.bpm)?, - tracks, - scenes, - selection: Selection::TrackClip { track: 0, scene: 0 }, - midi_ins, - midi_outs, - ..Default::default() - }; - let app = App { - jack: jack.clone(), - color: ItemTheme::random(), - dialog: Dialog::welcome(), - mode: config.modes.clone().read().unwrap().get(":menu").cloned().unwrap(), - config, - project, - ..Default::default() - }; - jack.sync_lead(self.sync_lead, |mut state|{ - let clock = app.clock(); - clock.playhead.update_from_sample(state.position.frame() as f64); - state.position.bbt = Some(clock.bbt()); - state.position - })?; - jack.sync_follow(self.sync_follow)?; - Ok(app) - })?) - } -} - -#[cfg(test)] #[test] fn test_cli () { - use clap::CommandFactory; - Cli::command().debug_assert(); - //let jack = Jack::default(); -} diff --git a/app/tek_deps.rs b/app/tek_deps.rs deleted file mode 100644 index d30fc79c..00000000 --- a/app/tek_deps.rs +++ /dev/null @@ -1,39 +0,0 @@ -#[allow(unused)] -pub(crate) use ::{ - tek_device::{*, tek_engine::*}, - tengri::{ - Usually, Perhaps, Has, MaybeHas, has, maybe_has, impl_debug, from, - wrap_inc, wrap_dec, - dsl::*, - input::*, - output::*, - tui::{ - *, - ratatui::{ - self, - prelude::{Rect, Style, Stylize, Buffer, Modifier, buffer::Cell, Color::{self, *}}, - widgets::{Widget, canvas::{Canvas, Line}}, - }, - crossterm::{ - self, - event::{Event, KeyCode::{self, *}}, - }, - } - }, - std::{ - path::{Path, PathBuf}, - sync::{Arc, RwLock}, - sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}, - error::Error, - collections::BTreeMap, - fmt::Write, - cmp::Ord, - ffi::OsString, - fmt::{Debug, Formatter}, - fs::File, - ops::{Add, Sub, Mul, Div, Rem}, - thread::JoinHandle - }, - xdg::BaseDirectories, - atomic_float::* -}; diff --git a/app/tek_mode.rs b/app/tek_mode.rs deleted file mode 100644 index 88749e99..00000000 --- a/app/tek_mode.rs +++ /dev/null @@ -1,61 +0,0 @@ -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 Draw for Mode { - fn draw (&self, _to: &mut TuiOut) { - //self.content().draw(to) - } -} - -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(()) - } - - 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/app/tek_test.rs b/app/tek_test.rs index 71f604be..d7bd3ca8 100644 --- a/app/tek_test.rs +++ b/app/tek_test.rs @@ -1,5 +1,11 @@ use crate::*; +#[cfg(test)] #[test] fn test_cli () { + use clap::CommandFactory; + Cli::command().debug_assert(); + //let jack = Jack::default(); +} + #[cfg(test)] #[test] fn test_app () -> Usually<()> { let mut app = App::default(); let _ = app.scene_add(None, None)?; diff --git a/app/tek_view.rs b/app/tek_view.rs deleted file mode 100644 index 9198ca0c..00000000 --- a/app/tek_view.rs +++ /dev/null @@ -1,353 +0,0 @@ -use crate::*; - -pub type Views = Arc, Arc>>>; - -impl Draw for App { - fn draw (&self, to: &mut TuiOut) { - for (index, dsl) in self.mode.view.iter().enumerate() { - if let Err(e) = self.view(to, dsl) { - panic!("render #{index} failed ({e}): {dsl}"); - } - } - } -} - -impl View for App { - fn view_expr <'a> (&'a self, to: &mut TuiOut, expr: &'a impl DslExpr) -> Usually<()> { - if evaluate_output_expression(self, to, expr)? - || evaluate_output_expression_tui(self, to, expr)? { - Ok(()) - } else { - Err(format!("App::view_expr: unexpected: {expr:?}").into()) - } - } - fn view_word <'a> (&'a self, to: &mut TuiOut, dsl: &'a impl DslExpr) -> Usually<()> { - let mut frags = dsl.src()?.unwrap().split("/"); - match frags.next() { - Some(":logo") => to.place(&view_logo()), - Some(":status") => to.place(&Fixed::Y(1, "TODO: Status Bar")), - Some(":meters") => match frags.next() { - Some("input") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::Y(Align::s("Input Meters")))), - Some("output") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::Y(Align::s("Output Meters")))), - _ => panic!() - }, - Some(":tracks") => match frags.next() { - None => to.place(&"TODO tracks"), - Some("names") => to.place(&self.project.view_track_names(self.color.clone())),//Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Names")))), - Some("inputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Inputs")))), - Some("devices") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Devices")))), - Some("outputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Outputs")))), - _ => panic!() - }, - Some(":scenes") => match frags.next() { - None => to.place(&"TODO scenes"), - Some(":scenes/names") => to.place(&"TODO Scene Names"), - _ => panic!() - }, - Some(":editor") => to.place(&"TODO Editor"), - Some(":dialog") => match frags.next() { - Some("menu") => to.place(&if let Dialog::Menu(selected, items) = &self.dialog { - let items = items.clone(); - let selected = selected; - Some(Fill::XY(Thunk::new(move|to: &mut TuiOut|{ - for (index, MenuItem(item, _)) in items.0.iter().enumerate() { - to.place(&Push::Y((2 * index) as u16, - Tui::fg_bg( - if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, - if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, - Fixed::Y(2, Align::n(Fill::X(item))) - ))); - } - }))) - } else { - None - }), - _ => unimplemented!("App::view_word: {dsl:?} ({frags:?})"), - }, - Some(":templates") => to.place(&{ - let modes = self.config.modes.clone(); - let height = (modes.read().unwrap().len() * 2) as u16; - Fixed::Y(height, Min::X(30, Thunk::new(move |to: &mut TuiOut|{ - for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { - let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; - let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); - let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); - let fg1 = Rgb(224, 192, 128); - let fg2 = Rgb(224, 128, 32); - let field_name = Fill::X(Align::w(Tui::fg(fg1, name))); - let field_id = Fill::X(Align::e(Tui::fg(fg2, id))); - let field_info = Fill::X(Align::w(info)); - to.place(&Push::Y((2 * index) as u16, - Fixed::Y(2, Fill::X(Tui::bg(bg, Bsp::s( - Bsp::a(field_name, field_id), field_info)))))); - } - }))) - }), - Some(":sessions") => to.place(&Fixed::Y(6, Min::X(30, Thunk::new(|to: &mut TuiOut|{ - let fg = Rgb(224, 192, 128); - for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { - let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; - to.place(&Push::Y((2 * index) as u16, - &Fixed::Y(2, Fill::X(Tui::bg(bg, Align::w(Tui::fg(fg, name))))))); - } - })))), - Some(":browse/title") => to.place(&Fill::X(Align::w(FieldV(ItemColor::default(), - match self.dialog.browser_target().unwrap() { - BrowseTarget::SaveProject => "Save project:", - BrowseTarget::LoadProject => "Load project:", - BrowseTarget::ImportSample(_) => "Import sample:", - BrowseTarget::ExportSample(_) => "Export sample:", - BrowseTarget::ImportClip(_) => "Import clip:", - BrowseTarget::ExportClip(_) => "Export clip:", - }, Shrink::X(3, Fixed::Y(1, Tui::fg(Tui::g(96), RepeatH("๐Ÿญป")))))))), - Some(":device") => { - let selected = self.dialog.device_kind().unwrap(); - to.place(&Bsp::s(Tui::bold(true, "Add device"), Map::south(1, - move||device_kinds().iter(), - move|_label: &&'static str, i|{ - let bg = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; - let lb = if i == selected { "[ " } else { " " }; - let rb = if i == selected { " ]" } else { " " }; - Fill::X(Tui::bg(bg, Bsp::e(lb, Bsp::w(rb, "FIXME device name")))) }))) - }, - Some(":debug") => to.place(&Fixed::Y(1, format!("[{:?}]", to.area()))), - Some(_) => { - let views = self.config.views.read().unwrap(); - if let Some(dsl) = views.get(dsl.src()?.unwrap()) { - let dsl = dsl.clone(); - std::mem::drop(views); - self.view(to, &dsl)? - } else { - unimplemented!("{dsl:?}"); - } - }, - _ => unreachable!() - } - Ok(()) - } -} - -fn view_logo () -> impl Content { - Fixed::XY(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), col!{ - Fixed::Y(1, ""), - Fixed::Y(1, ""), - Fixed::Y(1, "~~ โ•“โ”€โ•ฅโ”€โ•– โ•“โ”€โ”€โ•– โ•ฅ โ•– ~~~~~~~~~~~~"), - Fixed::Y(1, Bsp::e("~~~~ โ•‘ ~ โ•Ÿโ”€โ•Œ ~โ•Ÿโ”€< ~~ ", Bsp::e(Tui::fg(Rgb(230,100,40), "v0.3.0"), " ~~"))), - Fixed::Y(1, "~~~~ โ•จ ~ โ•™โ”€โ”€โ•œ โ•จ โ•œ ~~~~~~~~~~~~"), - }))) -} - -//pub fn view_nil (_: &App) -> TuiCb { - //|to|to.place(&Fill::XY("ยท")) -//} - - //Bsp::s("", - //Map::south(1, - //move||app.config.binds.layers.iter() - //.filter_map(|a|(a.0)(app).then_some(a.1)) - //.flat_map(|a|a) - //.filter_map(|x|if let Value::Exp(_, iter)=x.value{ Some(iter) } else { None }) - //.skip(offset) - //.take(20), - //|mut b,i|Fixed::X(60, Align::w(Bsp::e("(", Bsp::e( - //b.next().map(|t|Fixed::X(16, Align::w(Tui::fg(Rgb(64,224,0), format!("{}", t.value))))), - //Bsp::e(" ", Align::w(format!("{}", b.0.0.trim()))))))))))), - - //Dialog::Browse(BrowseTarget::Load, browser) => { - //"bobcat".boxed() - ////Bsp::s( - ////Fill::X(Align::w(Margin::XY(1, 1, Bsp::e( - ////Tui::bold(true, " Load project: "), - ////Shrink::X(3, Fixed::Y(1, RepeatH("๐Ÿญป"))))))), - ////Outer(true, Style::default().fg(Tui::g(96))) - ////.enclose(Fill::XY(browser))) - //}, - //Dialog::Browse(BrowseTarget::Export, browser) => { - //"bobcat".boxed() - ////Bsp::s( - ////Fill::X(Align::w(Margin::XY(1, 1, Bsp::e( - ////Tui::bold(true, " Export: "), - ////Shrink::X(3, Fixed::Y(1, RepeatH("๐Ÿญป"))))))), - ////Outer(true, Style::default().fg(Tui::g(96))) - ////.enclose(Fill::XY(browser))) - //}, - //Dialog::Browse(BrowseTarget::Import, browser) => { - //"bobcat".boxed() - ////Bsp::s( - ////Fill::X(Align::w(Margin::XY(1, 1, Bsp::e( - ////Tui::bold(true, " Import: "), - ////Shrink::X(3, Fixed::Y(1, RepeatH("๐Ÿญป"))))))), - ////Outer(true, Style::default().fg(Tui::g(96))) - ////.enclose(Fill::XY(browser))) - //}, -// - //pub fn view_history (&self) -> impl Content { - //Fixed::Y(1, Fill::X(Align::w(FieldH(self.color, - //format!("History ({})", self.history.len()), - //self.history.last().map(|last|Fill::X(Align::w(format!("{:?}", last.0)))))))) - //} - //pub fn view_status_h2 (&self) -> impl Content { - //self.update_clock(); - //let theme = self.color; - //let clock = self.clock(); - //let playing = clock.is_rolling(); - //let cache = clock.view_cache.clone(); - ////let selection = self.selection().describe(self.tracks(), self.scenes()); - //let hist_len = self.history.len(); - //let hist_last = self.history.last(); - //Fixed::Y(2, Stack::east(move|add: &mut dyn FnMut(&dyn Draw)|{ - //add(&Fixed::X(5, Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, - //Either::new(false, // TODO - //Thunk::new(move||Fixed::X(9, Either::new(playing, - //Tui::fg(Rgb(0, 255, 0), " PLAYING "), - //Tui::fg(Rgb(255, 128, 0), " STOPPED "))) - //), - //Thunk::new(move||Fixed::X(5, Either::new(playing, - //Tui::fg(Rgb(0, 255, 0), Bsp::s(" ๐Ÿญ๐Ÿญ‘๐Ÿฌฝ ", " ๐Ÿญž๐Ÿญœ๐Ÿญ˜ ",)), - //Tui::fg(Rgb(255, 128, 0), Bsp::s(" โ–—โ–„โ–– ", " โ–โ–€โ–˜ ",)))) - //) - //) - //))); - //add(&" "); - //{ - //let cache = cache.read().unwrap(); - //add(&Fixed::X(15, Align::w(Bsp::s( - //FieldH(theme, "Beat", cache.beat.view.clone()), - //FieldH(theme, "Time", cache.time.view.clone()), - //)))); - //add(&Fixed::X(13, Align::w(Bsp::s( - //Fill::X(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), - //Fill::X(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), - //)))); - //add(&Fixed::X(12, Align::w(Bsp::s( - //Fill::X(Align::w(FieldH(theme, "Buf", cache.buf.view.clone()))), - //Fill::X(Align::w(FieldH(theme, "Lat", cache.lat.view.clone()))), - //)))); - ////add(&Bsp::s( - //////Fill::X(Align::w(FieldH(theme, "Selected", Align::w(selection)))), - ////Fill::X(Align::w(FieldH(theme, format!("History ({})", hist_len), - ////hist_last.map(|last|Fill::X(Align::w(format!("{:?}", last.0))))))), - ////"" - ////)); - //////if let Some(last) = self.history.last() { - //////add(&FieldV(theme, format!("History ({})", self.history.len()), - //////Fill::X(Align::w(format!("{:?}", last.0))))); - //////} - //} - //})) - //} - //pub fn view_status_v (&self) -> impl Content + use<'_> { - //self.update_clock(); - //let cache = self.project.clock.view_cache.read().unwrap(); - //let theme = self.color; - //let playing = self.clock().is_rolling(); - //Tui::bg(theme.darker.rgb, Fixed::XY(20, 5, Outer(true, Style::default().fg(Tui::g(96))).enclose( - //col!( - //Fill::X(Align::w(Bsp::e( - //Align::w(Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, - //Either::new(false, // TODO - //Thunk::new(move||Fixed::X(9, Either::new(playing, - //Tui::fg(Rgb(0, 255, 0), " PLAYING "), - //Tui::fg(Rgb(255, 128, 0), " STOPPED "))) - //), - //Thunk::new(move||Fixed::X(5, Either::new(playing, - //Tui::fg(Rgb(0, 255, 0), Bsp::s(" ๐Ÿญ๐Ÿญ‘๐Ÿฌฝ ", " ๐Ÿญž๐Ÿญœ๐Ÿญ˜ ",)), - //Tui::fg(Rgb(255, 128, 0), Bsp::s(" โ–—โ–„โ–– ", " โ–โ–€โ–˜ ",)))) - //) - //) - //)), - //Bsp::s( - //FieldH(theme, "Beat", cache.beat.view.clone()), - //FieldH(theme, "Time", cache.time.view.clone()), - //), - //))), - //Fill::X(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), - //Fill::X(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), - //Fill::X(Align::w(FieldH(theme, "Buf", Bsp::e(cache.buf.view.clone(), Bsp::e(" = ", cache.lat.view.clone()))))), - //)))) - //} - //pub fn view_status (&self) -> impl Content + use<'_> { - //self.update_clock(); - //let cache = self.project.clock.view_cache.read().unwrap(); - //view_status(Some(self.project.selection.describe(self.tracks(), self.scenes())), - //cache.sr.view.clone(), cache.buf.view.clone(), cache.lat.view.clone()) - //} - //pub fn view_transport (&self) -> impl Content + use<'_> { - //self.update_clock(); - //let cache = self.project.clock.view_cache.read().unwrap(); - //view_transport(self.project.clock.is_rolling(), - //cache.bpm.view.clone(), cache.beat.view.clone(), cache.time.view.clone()) - //} - //pub fn view_editor (&self) -> impl Content + use<'_> { - //let bg = self.editor() - //.and_then(|editor|editor.clip().clone()) - //.map(|clip|clip.read().unwrap().color.darker) - //.unwrap_or(self.color.darker); - //Fill::XY(Tui::bg(bg.rgb, self.editor())) - //} - //pub fn view_editor_status (&self) -> impl Content + use<'_> { - //self.editor().map(|e|Fixed::X(20, Outer(true, Style::default().fg(Tui::g(96))).enclose( - //Fill::Y(Align::n(Bsp::s(e.clip_status(), e.edit_status())))))) - //} - //pub fn view_midi_ins_status (&self) -> impl Content + use<'_> { - //self.project.view_midi_ins_status(self.color) - //} - //pub fn view_midi_outs_status (&self) -> impl Content + use<'_> { - //self.project.view_midi_outs_status(self.color) - //} - //pub fn view_audio_ins_status (&self) -> impl Content + use<'_> { - //self.project.view_audio_ins_status(self.color) - //} - //pub fn view_audio_outs_status (&self) -> impl Content + use<'_> { - //self.project.view_audio_outs_status(self.color) - //} - //pub fn view_scenes (&self) -> impl Content + use<'_> { - //Bsp::e( - //Fixed::X(20, Align::nw(self.project.view_scenes_names())), - //self.project.view_scenes_clips(), - //) - //} - //pub fn view_scenes_names (&self) -> impl Content + use<'_> { - //self.project.view_scenes_names() - //} - //pub fn view_scenes_clips (&self) -> impl Content + use<'_> { - //self.project.view_scenes_clips() - //} - //pub fn view_tracks_inputs <'a> (&'a self) -> impl Content + use<'a> { - //Fixed::Y(1 + self.project.midi_ins.len() as u16, - //self.project.view_inputs(self.color)) - //} - //pub fn view_tracks_outputs <'a> (&'a self) -> impl Content + use<'a> { - //self.project.view_outputs(self.color) - //} - //pub fn view_tracks_devices <'a> (&'a self) -> impl Content + use<'a> { - //Fixed::Y(4, self.project.view_track_devices(self.color)) - //} - //pub fn view_tracks_names <'a> (&'a self) -> impl Content + use<'a> { - //Fixed::Y(2, self.project.view_track_names(self.color)) - //} - //pub fn view_pool (&self) -> impl Content + use<'_> { - //Fixed::X(20, Bsp::s( - //Fill::X(Align::w(FieldH(self.color, "Clip pool:", ""))), - //Fill::Y(Align::n(Tui::bg(Rgb(0, 0, 0), Outer(true, Style::default().fg(Tui::g(96))) - //.enclose(PoolView(&self.pool))))))) - //} - //pub fn view_samples_keys (&self) -> impl Content + use<'_> { - //self.project.sampler().map(|s|s.view_list(true, self.editor().unwrap())) - //} - //pub fn view_samples_grid (&self) -> impl Content + use<'_> { - //self.project.sampler().map(|s|s.view_grid()) - //} - //pub fn view_sample_viewer (&self) -> impl Content + use<'_> { - //self.project.sampler().map(|s|s.view_sample(self.editor().unwrap().get_note_pos())) - //} - //pub fn view_sample_info (&self) -> impl Content + use<'_> { - //self.project.sampler().map(|s|s.view_sample_info(self.editor().unwrap().get_note_pos())) - //} - //pub fn view_sample_status (&self) -> impl Content + use<'_> { - //self.project.sampler().map(|s|Outer(true, Style::default().fg(Tui::g(96))).enclose( - //Fill::Y(Align::n(s.view_sample_status(self.editor().unwrap().get_note_pos()))))) - //} - ////let options = ||["Projects", "Settings", "Help", "Quit"].iter(); - ////let option = |a,i|Tui::fg(Rgb(255,255,255), format!("{}", a)); - ////Bsp::s(Tui::bold(true, "tek!"), Bsp::s("", Map::south(1, options, option))) From 211b433b3a6f058ea134060683e92d288a6defc1 Mon Sep 17 00:00:00 2001 From: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Date: Sat, 17 Jan 2026 00:35:18 +0200 Subject: [PATCH 2/8] chore(shell): add bacon --- shell.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/shell.nix b/shell.nix index 8782e6dd..5f36b882 100755 --- a/shell.nix +++ b/shell.nix @@ -3,6 +3,7 @@ name = "tek"; stdenv = pkgs.clang19Stdenv; nativeBuildInputs = [ + pkgs.bacon pkgs.pkg-config pkgs.freetype pkgs.libclang From b1074bd8313d029cb52ff10291c70c7868e503e1 Mon Sep 17 00:00:00 2001 From: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Date: Sat, 17 Jan 2026 00:35:29 +0200 Subject: [PATCH 3/8] fix: compiles/runs (emptily) --- app/tek.rs | 128 ++++++++++++++++++++++++++--------------------------- 1 file changed, 63 insertions(+), 65 deletions(-) diff --git a/app/tek.rs b/app/tek.rs index 190cf83a..82ee7dda 100644 --- a/app/tek.rs +++ b/app/tek.rs @@ -41,6 +41,7 @@ }; pub mod model { + use super::{*, gui::*}; /// Total state #[derive(Default, Debug)] pub struct App { /// Base color. @@ -110,34 +111,39 @@ pub mod model { /// Input bindings are only returned if this evaluates to true #[derive(Clone)] pub struct Condition( - Arcbool + Send + Sync>> - ); - - #[derive(Debug, Copy, Clone)] - pub enum Axis { X, Y, Z, I } - /// Various possible dialog modes. - #[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog { - #[default] None, - Help(usize), - Menu(usize, MenuItems), - Device(usize), - Message(Arc), - Browse(BrowseTarget, Arc), - Options, - } - #[derive(Debug, Clone, Default, PartialEq)] pub struct MenuItems( - pub Arc<[MenuItem]> - ); - #[derive(Clone)] pub struct MenuItem( - /// Label - pub Arc, - /// Callback - pub ArcUsually<()> + Send + Sync>> + pub Arcbool + Send + Sync>> ); + def_command!(AppCommand: |app: App| { + Nop => Ok(None), + Confirm => Ok(match &app.dialog { + Dialog::Menu(index, items) => { + let callback = items.0[*index].1.clone(); + callback(app)?; + None + }, + _ => todo!(), + }), + Cancel => todo!(), // TODO delegate: + Inc { axis: Axis } => Ok(match (&app.dialog, axis) { + (Dialog::None, _) => todo!(), + (Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_next() } + .execute(app)?, + _ => todo!() + }), + Dec { axis: Axis } => Ok(match (&app.dialog, axis) { + (Dialog::None, _) => None, + (Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_prev() } + .execute(app)?, + _ => todo!() + }), + SetDialog { dialog: Dialog } => { + swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }) + }, + }); } pub mod core { - use super::{*, model::*}; + use super::{*, model::*, gui::*}; impl App { pub fn update_clock (&self) { ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) @@ -389,10 +395,9 @@ pub mod core { } } pub mod ns { + 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())); - }); + dsl_ns!(App: Arc { literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); }); // Provide boolean values. dsl_ns!(App: bool { // TODO literal = ... @@ -406,14 +411,10 @@ pub mod ns { ":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), + ":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), }; }); // TODO: provide colors here @@ -533,36 +534,9 @@ pub mod ns { "cancel" => AppCommand::Cancel, }); } - def_command!(AppCommand: |app: App| { - Nop => Ok(None), - Confirm => Ok(match &app.dialog { - Dialog::Menu(index, items) => { - let callback = items.0[*index].1.clone(); - callback(app)?; - None - }, - _ => todo!(), - }), - Cancel => todo!(), // TODO delegate: - Inc { axis: Axis } => Ok(match (&app.dialog, axis) { - (Dialog::None, _) => todo!(), - (Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_next() } - .execute(app)?, - _ => todo!() - }), - Dec { axis: Axis } => Ok(match (&app.dialog, axis) { - (Dialog::None, _) => None, - (Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_prev() } - .execute(app)?, - _ => todo!() - }), - SetDialog { dialog: Dialog } => { - swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }) - }, - }); } pub mod tui { - use super::{*, model::*}; + use super::{*, model::*, gui::*}; handle!(TuiIn: |self: App, input|{ let mut commands = vec![]; for id in self.mode.keys.iter() { @@ -731,6 +705,28 @@ pub mod tui { } } pub mod gui { + use super::{*, model::*}; + #[derive(Debug, Copy, Clone)] + pub enum Axis { X, Y, Z, I } + /// Various possible dialog modes. + #[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog { + #[default] None, + Help(usize), + Menu(usize, MenuItems), + Device(usize), + Message(Arc), + Browse(BrowseTarget, Arc), + Options, + } + #[derive(Debug, Clone, Default, PartialEq)] pub struct MenuItems( + pub Arc<[MenuItem]> + ); + #[derive(Clone)] pub struct MenuItem( + /// Label + pub Arc, + /// Callback + pub ArcUsually<()> + Send + Sync>> + ); impl Dialog { pub fn welcome () -> Self { Self::Menu(1, MenuItems([ @@ -764,6 +760,7 @@ pub mod gui { } } pub mod audio { + use super::{*, model::*}; audio!( |self: App, client, scope|{ let t0 = self.perf.get_t0(); @@ -813,7 +810,7 @@ pub mod audio { ); } pub mod glue { - use super::{*, model::*}; + use super::{*, model::*, gui::*}; has!(Jack<'static>: |self: App|self.jack); has!(Pool: |self: App|self.pool); has!(Dialog: |self: App|self.dialog); @@ -850,7 +847,7 @@ pub mod glue { } /// Command-line configuration. #[cfg(feature = "cli")] pub mod cli { - use super::{*, model::*}; + use super::{*, model::*, gui::*}; use clap::{self, Parser, Subcommand}; /// CLI banner. const HEADER: &'static str = r#" @@ -973,6 +970,7 @@ pub mod glue { } /// Command-line entrypoint. #[cfg(feature = "cli")] pub fn main () -> Usually<()> { + use clap::Parser; crate::cli::Cli::parse().run() } From 204e26a3242ede43f32af79d69c5b966ab9bfe19 Mon Sep 17 00:00:00 2001 From: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Date: Sat, 17 Jan 2026 01:03:24 +0200 Subject: [PATCH 4/8] feat: cli --- app/tek.rs | 134 +++++++++++++++++++++++++++------------------ device/arranger.rs | 19 +++++++ 2 files changed, 99 insertions(+), 54 deletions(-) diff --git a/app/tek.rs b/app/tek.rs index 82ee7dda..2964aba6 100644 --- a/app/tek.rs +++ b/app/tek.rs @@ -145,6 +145,22 @@ pub mod model { pub mod core { use super::{*, model::*, gui::*}; impl App { + pub fn new ( + jack: &Jack<'static>, + project: Arrangement, + config: Config, + mode: impl AsRef + ) -> Self { + Self { + color: ItemTheme::random(), + dialog: Dialog::welcome(), + jack: jack.clone(), + mode: config.modes.clone().read().unwrap().get(mode.as_ref()).cloned().unwrap(), + config, + project, + ..Default::default() + } + } pub fn update_clock (&self) { ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) } @@ -847,7 +863,7 @@ pub mod glue { } /// Command-line configuration. #[cfg(feature = "cli")] pub mod cli { - use super::{*, model::*, gui::*}; + use super::{*, model::*}; use clap::{self, Parser, Subcommand}; /// CLI banner. const HEADER: &'static str = r#" @@ -866,7 +882,7 @@ pub mod glue { /// Pre-defined configuration modes. /// /// TODO: Replace these with scripted configurations. - #[command(subcommand)] mode: Option, + #[command(subcommand)] action: Action, /// Name of JACK client #[arg(short='n', long)] name: Option, /// Whether to attempt to become transport master @@ -895,10 +911,22 @@ pub mod glue { #[arg(short='R', long)] right_to: Vec, } /// Application modes - #[derive(Debug, Clone, Subcommand)] - enum LaunchMode { + #[derive(Debug, Clone, Subcommand, Default)] + enum Action { + /// Continue where you left off + #[default] Resume, + /// Show version. + Version, + /// Show configuration. + Config, + /// Show status of current session. + Status, + /// Run headlessly in current session. + Headless, /// Create a new session instead of loading the previous one. New, + /// Create new session from importable file. + Import, } impl Cli { fn midi_froms (&self) -> Vec { @@ -908,63 +936,61 @@ pub mod glue { Connect::collect(&self.midi_to, &[] as &[&str], &self.midi_to_re) } pub fn run (&self) -> Usually<()> { - let name = self.name.as_ref().map_or("tek", |x|x.as_str()); - let tracks = vec![]; - let scenes = vec![]; - let empty = &[] as &[&str]; - let left_froms = Connect::collect(&self.left_from, empty, empty); - let left_tos = Connect::collect(&self.left_to, empty, empty); - let right_froms = Connect::collect(&self.right_from, empty, empty); - let right_tos = Connect::collect(&self.right_to, empty, empty); - let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; - let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()]; - let mut config = Config::new(None); - config.init()?; - Tui::new()?.run(&Jack::new_run(&name, move|jack|{ - let midi_ins = { + if matches!(self.action, Action::Version) { + println!("todo version"); + } else { + let mut config = Config::new(None); + config.init()?; + if matches!(self.action, Action::Config) { + println!("{config:#?}"); + } else { + let name = self.name.as_ref().map_or("tek", |x|x.as_str()); + let jack = Jack::new(&name)?; + let tracks = vec![]; + let scenes = vec![]; + let empty = &[] as &[&str]; + let left_froms = Connect::collect(&self.left_from, empty, empty); + let left_tos = Connect::collect(&self.left_to, empty, empty); + let right_froms = Connect::collect(&self.right_from, empty, empty); + let right_tos = Connect::collect(&self.right_to, empty, empty); + let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; + let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()]; let mut midi_ins = vec![]; + let mut midi_outs = vec![]; for (index, connect) in self.midi_froms().iter().enumerate() { midi_ins.push(jack.midi_in(&format!("M/{index}"), &[connect.clone()])?); } - midi_ins - }; - let midi_outs = { - let mut midi_outs = vec![]; for (index, connect) in self.midi_tos().iter().enumerate() { midi_outs.push(jack.midi_out(&format!("{index}/M"), &[connect.clone()])?); }; - midi_outs - }; - let project = Arrangement { - name: Default::default(), - color: ItemTheme::random(), - jack: jack.clone(), - clock: Clock::new(&jack, self.bpm)?, - tracks, - scenes, - selection: Selection::TrackClip { track: 0, scene: 0 }, - midi_ins, - midi_outs, - ..Default::default() - }; - let app = App { - jack: jack.clone(), - color: ItemTheme::random(), - dialog: Dialog::welcome(), - mode: config.modes.clone().read().unwrap().get(":menu").cloned().unwrap(), - config, - project, - ..Default::default() - }; - jack.sync_lead(self.sync_lead, |mut state|{ - let clock = app.clock(); - clock.playhead.update_from_sample(state.position.frame() as f64); - state.position.bbt = Some(clock.bbt()); - state.position - })?; - jack.sync_follow(self.sync_follow)?; - Ok(app) - })?) + let clock = Clock::new(&jack, self.bpm)?; + let project = Arrangement::new( + &jack, None, clock, tracks, scenes, midi_ins, midi_outs + ); + if matches!(self.action, Action::Status) { + println!("{project:?}"); + // TODO git integration + } else { + let app = App::new(&jack, project, config, ":menu"); + let client = jack.run(move|jack|{ + jack.sync_lead(self.sync_lead, |mut state|{ + let clock = app.clock(); + clock.playhead.update_from_sample(state.position.frame() as f64); + state.position.bbt = Some(clock.bbt()); + state.position + })?; + jack.sync_follow(self.sync_follow)?; + Ok(app) + })?; + if matches!(self.action, Action::Headless) { + println!("todo headless"); + } else { + return Tui::new()?.run(&client) + } + } + } + } + Ok(()) } } } diff --git a/device/arranger.rs b/device/arranger.rs index 10a624b1..80bc19cc 100644 --- a/device/arranger.rs +++ b/device/arranger.rs @@ -74,6 +74,25 @@ impl Arrangement { } impl Arrangement { + /// Create a new arrangement. + pub fn new ( + jack: &Jack<'static>, + name: Option>, + clock: Clock, + tracks: Vec, + scenes: Vec, + midi_ins: Vec, + midi_outs: Vec, + ) -> Self { + Self { + clock, tracks, scenes, midi_ins, midi_outs, + jack: jack.clone(), + name: name.unwrap_or_default(), + color: ItemTheme::random(), + selection: Selection::TrackClip { track: 0, scene: 0 }, + ..Default::default() + } + } /// Width of display pub fn w (&self) -> u16 { self.size.w() as u16 From a8f0fbb8976ab6ec741f738baba5f9fc93fc4537 Mon Sep 17 00:00:00 2001 From: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Date: Sat, 17 Jan 2026 01:37:50 +0200 Subject: [PATCH 5/8] feat: list views and binds --- Cargo.lock | 10 ++++++++ app/Cargo.toml | 1 + app/tek.rs | 64 +++++++++++++++++++++++++++++++++++++++++++------- deps/tengri | 2 +- 4 files changed, 68 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 34c84187..9be82217 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,6 +79,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "anstream" version = "0.6.20" @@ -2383,6 +2392,7 @@ dependencies = [ name = "tek" version = "0.3.0" dependencies = [ + "ansi_term", "atomic_float", "backtrace", "clap", diff --git a/app/Cargo.toml b/app/Cargo.toml index c315f1dc..5d48eabd 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -32,6 +32,7 @@ uuid = { workspace = true, optional = true } wavers = { workspace = true, optional = true } winit = { workspace = true, optional = true } xdg = { workspace = true } +ansi_term = "0.12.1" [dev-dependencies] proptest = { workspace = true } diff --git a/app/tek.rs b/app/tek.rs index 2964aba6..121360e7 100644 --- a/app/tek.rs +++ b/app/tek.rs @@ -36,7 +36,7 @@ }, tengri::tui::crossterm::{ self, - event::{Event, KeyCode::{self, *}}, + event::{Event, KeyEvent, KeyCode::{self, *}}, }, }; @@ -267,11 +267,11 @@ pub mod core { &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:?}"); + //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:?}"); + //println!("Loading {path:?}"); let src = std::fs::read_to_string(&path)?; src.as_str().each(move|item|each(self, item))?; } else { @@ -285,7 +285,7 @@ pub mod core { 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()); + //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)?, @@ -305,14 +305,14 @@ pub mod core { 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()); + //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(()) } 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()); + //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()), @@ -369,7 +369,7 @@ pub mod core { } impl Bind> { pub fn load_into (binds: &Binds, name: &impl AsRef, body: &impl Dsl) -> Usually<()> { - println!("Bind::load_into: {}: {body:?}", name.as_ref()); + //println!("Bind::load_into: {}: {body:?}", name.as_ref()); let mut map = Self::new(); body.each(|item|if item.expr().head() == Ok(Some("see")) { // TODO @@ -942,7 +942,55 @@ pub mod glue { let mut config = Config::new(None); config.init()?; if matches!(self.action, Action::Config) { - println!("{config:#?}"); + use ::ansi_term::Color::*; + println!("{:?}", config.dirs); + for (k, v) in config.views.read().unwrap().iter() { + println!("{} {} {v}", Green.paint("VIEW"), Green.bold().paint(format!("{k:<16}"))); + } + for (k, v) in config.binds.read().unwrap().iter() { + println!("{} {}", Green.paint("BIND"), Green.bold().paint(format!("{k:<16}"))); + for (k, v) in v.0.iter() { + print!("{} ", &Yellow.paint(match &k.0 { + Event::Key(KeyEvent { modifiers, .. }) => + format!("{:>16}", format!("{modifiers}")), + _ => unimplemented!() + })); + print!("{}", &Yellow.bold().paint(match &k.0 { + Event::Key(KeyEvent { code, .. }) => + format!("{:<10}", format!("{code}")), + _ => unimplemented!() + })); + for v in v.iter() { + print!(" => {:?}", v.commands); + print!(" {}", v.condition.as_ref().map(|x|format!("{x:?}")).unwrap_or_default()); + println!(" {}", v.description.as_ref().map(|x|x.as_ref()).unwrap_or_default()); + //println!(" {:?}", v.source); + } + } + } + for (k, v) in config.modes.read().unwrap().iter() { + println!("{} {} {:?} {:?}", Green.paint("\nTOOL "), + Green.bold().paint(format!("{k:<16}")), + v.name, v.info); + print!("{}", Green.paint(" VIEW")); + for v in v.view.iter() { print!(" {}", Yellow.paint(format!("{v}"))); } + println!(); + print!("{}", Green.paint(" KEYS")); + for v in v.keys.iter() { print!(" {}", Yellow.paint(format!("{v}"))); } + println!(); + for (k, v) in v.modes.read().unwrap().iter() { + print!("{} {} {:?}", + Green.paint(" MODE"), + Green.bold().paint(format!("{k:<16}")), + v.name); + print!(" INFO={:?}", + v.info); + print!(" VIEW={:?}", + v.view); + println!(" KEYS={:?}", + v.keys); + } + } } else { let name = self.name.as_ref().map_or("tek", |x|x.as_str()); let jack = Jack::new(&name)?; diff --git a/deps/tengri b/deps/tengri index 8c54510f..b0d2fad1 160000 --- a/deps/tengri +++ b/deps/tengri @@ -1 +1 @@ -Subproject commit 8c54510f630e8a81b7d7bdca0a51a69cdb9dffcc +Subproject commit b0d2fad17beef84be8f1fdad116643563379a2c1 From ac7fbdb77958e8d363bc110f0e060dddf8600238 Mon Sep 17 00:00:00 2001 From: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Date: Sat, 17 Jan 2026 02:07:35 +0200 Subject: [PATCH 6/8] feat: show project status --- app/tek.rs | 322 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 184 insertions(+), 138 deletions(-) diff --git a/app/tek.rs b/app/tek.rs index 121360e7..70d0dcd3 100644 --- a/app/tek.rs +++ b/app/tek.rs @@ -883,163 +883,209 @@ pub mod glue { /// /// TODO: Replace these with scripted configurations. #[command(subcommand)] action: Action, - /// Name of JACK client - #[arg(short='n', long)] name: Option, - /// Whether to attempt to become transport master - #[arg(short='S', long, default_value_t = false)] sync_lead: bool, - /// Whether to sync to external transport master - #[arg(short='s', long, default_value_t = true)] sync_follow: bool, - /// Initial tempo in beats per minute - #[arg(short='b', long, default_value = None)] bpm: Option, - /// Whether to include a transport toolbar (default: true) - #[arg(short='t', long, default_value_t = true)] show_clock: bool, - /// MIDI outs to connect to (multiple instances accepted) - #[arg(short='I', long)] midi_from: Vec, - /// MIDI outs to connect to (multiple instances accepted) - #[arg(short='i', long)] midi_from_re: Vec, - /// MIDI ins to connect to (multiple instances accepted) - #[arg(short='O', long)] midi_to: Vec, - /// MIDI ins to connect to (multiple instances accepted) - #[arg(short='o', long)] midi_to_re: Vec, - /// Audio outs to connect to left input - #[arg(short='l', long)] left_from: Vec, - /// Audio outs to connect to right input - #[arg(short='r', long)] right_from: Vec, - /// Audio ins to connect from left output - #[arg(short='L', long)] left_to: Vec, - /// Audio ins to connect from right output - #[arg(short='R', long)] right_to: Vec, } /// Application modes #[derive(Debug, Clone, Subcommand, Default)] enum Action { /// Continue where you left off #[default] Resume, - /// Show version. - Version, - /// Show configuration. - Config, - /// Show status of current session. - Status, /// Run headlessly in current session. Headless, - /// Create a new session instead of loading the previous one. - New, - /// Create new session from importable file. + /// Show status of current session. + Status, + /// List known sessions. + List, + /// Continue work in a copy of the current session. + Fork, + /// Create a new empty session. + New { + /// Name of JACK client + #[arg(short='n', long)] name: Option, + /// Whether to attempt to become transport master + #[arg(short='Y', long, default_value_t = false)] sync_lead: bool, + /// Whether to sync to external transport master + #[arg(short='y', long, default_value_t = true)] sync_follow: bool, + /// Initial tempo in beats per minute + #[arg(short='b', long, default_value = None)] bpm: Option, + /// Whether to include a transport toolbar (default: true) + #[arg(short='c', long, default_value_t = true)] show_clock: bool, + /// MIDI outs to connect to (multiple instances accepted) + #[arg(short='I', long)] midi_from: Vec, + /// MIDI outs to connect to (multiple instances accepted) + #[arg(short='i', long)] midi_from_re: Vec, + /// MIDI ins to connect to (multiple instances accepted) + #[arg(short='O', long)] midi_to: Vec, + /// MIDI ins to connect to (multiple instances accepted) + #[arg(short='o', long)] midi_to_re: Vec, + /// Audio outs to connect to left input + #[arg(short='l', long)] left_from: Vec, + /// Audio outs to connect to right input + #[arg(short='r', long)] right_from: Vec, + /// Audio ins to connect from left output + #[arg(short='L', long)] left_to: Vec, + /// Audio ins to connect from right output + #[arg(short='R', long)] right_to: Vec, + /// Tracks to create + #[arg(short='t', long)] tracks: Option, + /// Scenes to create + #[arg(short='s', long)] scenes: Option, + }, + /// Import media as new session. Import, + /// Show configuration. + Config, + /// Show version. + Version, } impl Cli { - fn midi_froms (&self) -> Vec { - Connect::collect(&self.midi_from, &[] as &[&str], &self.midi_from_re) - } - fn midi_tos (&self) -> Vec { - Connect::collect(&self.midi_to, &[] as &[&str], &self.midi_to_re) - } pub fn run (&self) -> Usually<()> { - if matches!(self.action, Action::Version) { - println!("todo version"); - } else { - let mut config = Config::new(None); - config.init()?; - if matches!(self.action, Action::Config) { - use ::ansi_term::Color::*; - println!("{:?}", config.dirs); - for (k, v) in config.views.read().unwrap().iter() { - println!("{} {} {v}", Green.paint("VIEW"), Green.bold().paint(format!("{k:<16}"))); - } - for (k, v) in config.binds.read().unwrap().iter() { - println!("{} {}", Green.paint("BIND"), Green.bold().paint(format!("{k:<16}"))); - for (k, v) in v.0.iter() { - print!("{} ", &Yellow.paint(match &k.0 { - Event::Key(KeyEvent { modifiers, .. }) => - format!("{:>16}", format!("{modifiers}")), - _ => unimplemented!() - })); - print!("{}", &Yellow.bold().paint(match &k.0 { - Event::Key(KeyEvent { code, .. }) => - format!("{:<10}", format!("{code}")), - _ => unimplemented!() - })); - for v in v.iter() { - print!(" => {:?}", v.commands); - print!(" {}", v.condition.as_ref().map(|x|format!("{x:?}")).unwrap_or_default()); - println!(" {}", v.description.as_ref().map(|x|x.as_ref()).unwrap_or_default()); - //println!(" {:?}", v.source); - } - } - } - for (k, v) in config.modes.read().unwrap().iter() { - println!("{} {} {:?} {:?}", Green.paint("\nTOOL "), - Green.bold().paint(format!("{k:<16}")), - v.name, v.info); - print!("{}", Green.paint(" VIEW")); - for v in v.view.iter() { print!(" {}", Yellow.paint(format!("{v}"))); } - println!(); - print!("{}", Green.paint(" KEYS")); - for v in v.keys.iter() { print!(" {}", Yellow.paint(format!("{v}"))); } - println!(); - for (k, v) in v.modes.read().unwrap().iter() { - print!("{} {} {:?}", - Green.paint(" MODE"), - Green.bold().paint(format!("{k:<16}")), - v.name); - print!(" INFO={:?}", - v.info); - print!(" VIEW={:?}", - v.view); - println!(" KEYS={:?}", - v.keys); - } - } + if let Action::Version = self.action { + return Ok(self.show_version()) + } + let mut config = Config::new(None); + config.init()?; + if let Action::Config = self.action { + self.show_config(&config); + } else if let Action::List = self.action { + todo!("list sessions") + } else if let Action::Resume = self.action { + todo!("resume session") + } else if let Action::New { + name, bpm, tracks, scenes, sync_lead, sync_follow, + midi_from, midi_from_re, midi_to, midi_to_re, + left_from, right_from, left_to, right_to, .. + } = &self.action { + let name = name.as_ref().map_or("tek", |x|x.as_str()); + let jack = Jack::new(&name)?; + let empty = &[] as &[&str]; + let left_froms = Connect::collect(&left_from, empty, empty); + let left_tos = Connect::collect(&left_to, empty, empty); + let right_froms = Connect::collect(&right_from, empty, empty); + let right_tos = Connect::collect(&right_to, empty, empty); + let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; + let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()]; + let mut midi_ins = vec![]; + let mut midi_outs = vec![]; + for (index, connect) in Connect::collect(&midi_from, &[] as &[&str], &midi_from_re).iter().enumerate() { + midi_ins.push(jack.midi_in(&format!("M/{index}"), &[connect.clone()])?); + } + for (index, connect) in Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate() { + midi_outs.push(jack.midi_out(&format!("{index}/M"), &[connect.clone()])?); + }; + let clock = Clock::new( + &jack, *bpm + )?; + let mut project = Arrangement::new( + &jack, None, clock, vec![], vec![], midi_ins, midi_outs + ); + project.tracks_add(tracks.unwrap_or(0), None, &[], &[])?; + project.scenes_add(scenes.unwrap_or(0))?; + if matches!(self.action, Action::Status) { + self.show_status(&project); } else { - let name = self.name.as_ref().map_or("tek", |x|x.as_str()); - let jack = Jack::new(&name)?; - let tracks = vec![]; - let scenes = vec![]; - let empty = &[] as &[&str]; - let left_froms = Connect::collect(&self.left_from, empty, empty); - let left_tos = Connect::collect(&self.left_to, empty, empty); - let right_froms = Connect::collect(&self.right_from, empty, empty); - let right_tos = Connect::collect(&self.right_to, empty, empty); - let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; - let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()]; - let mut midi_ins = vec![]; - let mut midi_outs = vec![]; - for (index, connect) in self.midi_froms().iter().enumerate() { - midi_ins.push(jack.midi_in(&format!("M/{index}"), &[connect.clone()])?); - } - for (index, connect) in self.midi_tos().iter().enumerate() { - midi_outs.push(jack.midi_out(&format!("{index}/M"), &[connect.clone()])?); - }; - let clock = Clock::new(&jack, self.bpm)?; - let project = Arrangement::new( - &jack, None, clock, tracks, scenes, midi_ins, midi_outs - ); - if matches!(self.action, Action::Status) { - println!("{project:?}"); - // TODO git integration - } else { - let app = App::new(&jack, project, config, ":menu"); - let client = jack.run(move|jack|{ - jack.sync_lead(self.sync_lead, |mut state|{ - let clock = app.clock(); - clock.playhead.update_from_sample(state.position.frame() as f64); - state.position.bbt = Some(clock.bbt()); - state.position - })?; - jack.sync_follow(self.sync_follow)?; - Ok(app) + let app = App::new(&jack, project, config, ":menu"); + let client = jack.run(move|jack|{ + jack.sync_lead(*sync_lead, |mut state|{ + let clock = app.clock(); + clock.playhead.update_from_sample(state.position.frame() as f64); + state.position.bbt = Some(clock.bbt()); + state.position })?; - if matches!(self.action, Action::Headless) { - println!("todo headless"); - } else { - return Tui::new()?.run(&client) - } + jack.sync_follow(*sync_follow)?; + Ok(app) + })?; + if matches!(self.action, Action::Headless) { + println!("todo headless"); + } else { + return Tui::new()?.run(&client) } } } Ok(()) } + fn show_version (&self) { + println!("todo version"); + } + fn show_config (&self, config: &Config) { + use ::ansi_term::Color::*; + println!("{:?}", config.dirs); + for (k, v) in config.views.read().unwrap().iter() { + println!("{} {} {v}", Green.paint("VIEW"), Green.bold().paint(format!("{k:<16}"))); + } + for (k, v) in config.binds.read().unwrap().iter() { + println!("{} {}", Green.paint("BIND"), Green.bold().paint(format!("{k:<16}"))); + for (k, v) in v.0.iter() { + print!("{} ", &Yellow.paint(match &k.0 { + Event::Key(KeyEvent { modifiers, .. }) => + format!("{:>16}", format!("{modifiers}")), + _ => unimplemented!() + })); + print!("{}", &Yellow.bold().paint(match &k.0 { + Event::Key(KeyEvent { code, .. }) => + format!("{:<10}", format!("{code}")), + _ => unimplemented!() + })); + for v in v.iter() { + print!(" => {:?}", v.commands); + print!(" {}", v.condition.as_ref().map(|x|format!("{x:?}")).unwrap_or_default()); + println!(" {}", v.description.as_ref().map(|x|x.as_ref()).unwrap_or_default()); + //println!(" {:?}", v.source); + } + } + } + for (k, v) in config.modes.read().unwrap().iter() { + println!("{} {} {:?} {:?}", Green.paint("\nTOOL "), + Green.bold().paint(format!("{k:<16}")), + v.name, v.info); + print!("{}", Green.paint(" VIEW")); + for v in v.view.iter() { print!(" {}", Yellow.paint(format!("{v}"))); } + println!(); + print!("{}", Green.paint(" KEYS")); + for v in v.keys.iter() { print!(" {}", Yellow.paint(format!("{v}"))); } + println!(); + for (k, v) in v.modes.read().unwrap().iter() { + print!("{} {} {:?}", + Green.paint(" MODE"), + Green.bold().paint(format!("{k:<16}")), + v.name); + print!(" INFO={:?}", + v.info); + print!(" VIEW={:?}", + v.view); + println!(" KEYS={:?}", + v.keys); + } + } + } + fn show_status (&self, project: &Arrangement) { + println!("Name: {:?}", &project.name); + println!("JACK: {:?}", &project.jack); + println!("Buffer: {:?}", &project.clock.chunk); + println!("Sample rate: {:?}", &project.clock.timebase.sr); + println!("MIDI PPQ: {:?}", &project.clock.timebase.ppq); + println!("Tempo: {:?}", &project.clock.timebase.bpm); + println!("Quantize: {:?}", &project.clock.quant); + println!("Launch: {:?}", &project.clock.sync); + println!("Playhead: {:?}us", &project.clock.playhead.usec); + println!("Playhead: {:?}s", &project.clock.playhead.sample); + println!("Playhead: {:?}p", &project.clock.playhead.pulse); + println!("Started: {:?}", &project.clock.started); + println!("Tracks:"); + for (i, t) in project.tracks.iter().enumerate() { + println!(" Track {i}: {} {} {:?} {:?}", t.name, t.width, + &t.sequencer.play_clip, &t.sequencer.next_clip); + } + println!("Scenes:"); + for (i, t) in project.scenes.iter().enumerate() { + println!(" Scene {i}: {} {:?}", &t.name, &t.clips); + } + println!("MIDI Ins: {:?}", &project.midi_ins); + println!("MIDI Outs: {:?}", &project.midi_outs); + println!("Audio Ins: {:?}", &project.audio_ins); + println!("Audio Outs: {:?}", &project.audio_outs); + // TODO git integration + // TODO dawvert integration + } } } /// Command-line entrypoint. From 044f60ebcb80850d2d4e087d3c49f7628d32a610 Mon Sep 17 00:00:00 2001 From: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Date: Sat, 17 Jan 2026 03:43:48 +0200 Subject: [PATCH 7/8] 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 From 6ec445aab7604e886ce20eb9a1e15bf824436e0d Mon Sep 17 00:00:00 2001 From: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Date: Sat, 17 Jan 2026 03:46:35 +0200 Subject: [PATCH 8/8] fix: orphaned fn --- deps/tengri | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/tengri b/deps/tengri index 1344967f..a933cbe2 160000 --- a/deps/tengri +++ b/deps/tengri @@ -1 +1 @@ -Subproject commit 1344967f33ac8c8e87ff8585fe8b463a15f088f7 +Subproject commit a933cbe2857613a439761e38a9634cf85dc17461