// ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ //██Let me play the world's tiniest piano for you. ██ //█▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀█ //█▙▙█▙▙▙█▙▙█▙▙▙█▙▙█▙▙▙█▙▙█▙▙▙█▙▙█▙▙▙█▙▙█▙▙▙█▙▙█▙▙▙██ //█▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄█ //███████████████████████████████████████████████████ //█ ▀ ▀ ▀ █ #![allow(unused)] #![allow(clippy::unit_arg)] #![feature(adt_const_params)] #![feature(associated_type_defaults)] #![feature(if_let_guard)] #![feature(impl_trait_in_assoc_type)] #![feature(type_alias_impl_trait)] #![feature(trait_alias)] #![feature(type_changing_struct_update)] #![feature(let_chains)] #![feature(closure_lifetime_binder)] #![feature(generic_const_exprs)] #![feature(generic_arg_infer)] pub use ::tek_engine:: *; pub use ::tek_device::{self, *}; pub use ::tengri::{Usually, Perhaps, Has, MaybeHas}; pub use ::tengri::{has, maybe_has}; pub use ::tengri::dsl::*; pub use ::tengri::input::*; pub use ::tengri::output::*; pub use ::tengri::tui::*; pub use ::tengri::tui::ratatui; pub use ::tengri::tui::ratatui::prelude::buffer::Cell; pub use ::tengri::tui::ratatui::prelude::Color::{self, *}; pub use ::tengri::tui::ratatui::prelude::{Style, Stylize, Buffer, Modifier}; pub use ::tengri::tui::crossterm; pub use ::tengri::tui::crossterm::event::{Event, KeyCode::{self, *}}; use std::path::{Path, PathBuf}; use std::sync::{Arc, RwLock}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}; use std::error::Error; use std::collections::BTreeMap; use std::fmt::Write; use ::tengri::tui::ratatui::prelude::Position; use xdg::BaseDirectories; mod app_view; pub use self::app_view::*; mod app_ctrl; pub use self::app_ctrl::*; mod app_jack; pub use self::app_jack::*; #[cfg(test)] mod app_test; /// Total state #[derive(Default, Debug)] pub struct App { /// 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 profiles and input bindings pub config: Config, /// Currently selected profile pub profile: Profile, /// Contains the currently edited musical arrangement pub project: Arrangement, /// Contains all recently created clips. pub pool: Pool, /// Undo history pub history: Vec<(AppCommand, Option)>, /// Dialog overlay pub dialog: Dialog, /// Base color. pub color: ItemTheme, } type ModuleMap = Arc, T>>>; /// Configuration #[derive(Default, Debug)] pub struct Config { /// XDG basedirs pub dirs: BaseDirectories, /// Available view profiles pub views: ModuleMap, /// Available input bindings pub binds: ModuleMap, Arc>>, } impl Config { const VIEWS: &'static str = "views.edn"; const BINDS: &'static str = "binds.edn"; const DEFAULT_VIEWS: &'static str = include_str!("../../config/views.edn"); const DEFAULT_BINDS: &'static str = include_str!("../../config/binds.edn"); pub fn init () -> Usually { let mut cfgs: Self = Default::default(); cfgs.dirs = BaseDirectories::with_profile("tek", "v0"); cfgs.load(Self::VIEWS, Self::DEFAULT_VIEWS, |cfgs, dsl|cfgs.load_view(dsl))?; cfgs.load(Self::BINDS, Self::DEFAULT_BINDS, |cfgs, dsl|cfgs.load_bind(dsl))?; println!("{cfgs:#?}"); Ok(cfgs) } pub fn load ( &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!("init: {path}"); std::fs::write(self.dirs.place_config_file(path)?, defaults); } Ok(if let Some(path) = self.dirs.find_config_file(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_view (&mut self, dsl: D) -> Usually<()> { load_mod(dsl, |id, tail|Ok(self.views.write().unwrap().insert(id.into(), Profile::from_dsl(tail)?))) } pub fn load_bind (&mut self, dsl: D) -> Usually<()> { load_mod(dsl, |id, tail|Ok(self.binds.write().unwrap().insert(id.into(), { let mut map = EventMap::new(); tail.each(|item|{ println!("{item:?}"); map.add(TuiEvent::from_dsl(item.exp()?.head()?)?, Binding { command: item.exp()?.tail()?.unwrap_or_default().into(), condition: None, description: None, source: None }); Ok(()) })?; map }))) } } fn load_mod (dsl: D, cb: impl Fn(&str, &str)->Usually) -> Usually<()> { //dsl!(dsl|module :id ..body|dsl!(body|@bind #info? ..commands|) if let Some(exp) = dsl.exp()? && Some("module") == exp.head().key()? && let Some(tail) = exp.tail()? && let Some(id) = tail.head().sym()? && let Some(body) = tail.tail()? { let _ = cb(id, body)?; Ok(()) } else { return Err("unexpected: {exp:?}".into()); } } /// Profile #[derive(Default, Debug)] pub struct Profile { /// Path of configuration entrypoint pub path: PathBuf, /// Name of configuration pub name: Option>, /// Description of configuration pub info: Option>, /// View definition pub view: Arc, // Input keymap pub keys: EventMap, } impl Profile { fn from_dsl (dsl: impl Dsl) -> Usually { let mut profile = Self { ..Default::default() }; dsl.each(|dsl|{ let head = dsl.head(); let exp = dsl.exp(); Ok(if exp.head().key() == Ok(Some("name")) { profile.name = Some(exp.tail()?.unwrap_or_default().into()); } else if exp.head().key() == Ok(Some("info")) { profile.info = Some(exp.tail()?.unwrap_or_default().into()); }) })?; Ok(profile) } } impl Profile { fn load_template (&mut self, dsl: impl Dsl) -> Usually<&mut Self> { //dsl.src()?.unwrap_or_default().each(|item|Ok(match () { //_ if let Some(exp) = dsl.exp()? => match exp.head()?.key()? { //Some("name") => match exp.tail()?.text()? { //Some(name) => self.name = Some(name.into()), //_ => return Err(format!("missing name definition").into()) //}, //Some("info") => match exp.tail()?.text()? { //Some(info) => self.info = Some(info.into()), //_ => return Err(format!("missing info definition").into()) //}, //Some("bind") => match exp.tail()? { //Some(keys) => self.keys = EventMap::from_dsl(&mut &keys)?, //_ => return Err(format!("missing keys definition").into()) //}, //Some("view") => match exp.tail()? { //Some(tail) => self.view = tail.src()?.unwrap_or_default().into(), //_ => return Err(format!("missing view definition").into()) //}, //dsl => return Err(format!("unexpected: {dsl:?}").into()) //}, //_ => return Err(format!("unexpected: {dsl:?}").into()) //})); Ok(self) } fn load_binding (&mut self, dsl: impl Dsl) -> Usually<&mut Self> { todo!(); Ok(self) } } /// Various possible dialog modes. #[derive(Debug, Clone, Default)] pub enum Dialog { #[default] None, Help(usize), Menu(usize), Device(usize), Message(Arc), Browser(BrowserTarget, Arc), Options, } impl App { pub fn update_clock (&self) { ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) } } has!(Jack<'static>: |self: App|self.jack); has!(Pool: |self: App|self.pool); has!(Dialog: |self: App|self.dialog); has!(Clock: |self: App|self.project.clock); has!(Option: |self: App|self.project.editor); has!(Selection: |self: App|self.project.selection); has!(Vec: |self: App|self.project.midi_ins); has!(Vec: |self: App|self.project.midi_outs); has!(Vec: |self: App|self.project.scenes); has!(Vec: |self: App|self.project.tracks); has!(Measure: |self: App|self.size); has_clips!( |self: App|self.pool.clips); maybe_has!(Track: |self: App| { MaybeHas::::get(&self.project) }; { MaybeHas::::get_mut(&mut self.project) }); maybe_has!(Scene: |self: App| { MaybeHas::::get(&self.project) }; { MaybeHas::::get_mut(&mut self.project) }); impl HasClipsSize for App { fn clips_size (&self) -> &Measure { &self.project.inner_size } } impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() } } impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } } fn unquote (x: &str) -> &str { let mut chars = x.chars(); chars.next(); //chars.next_back(); chars.as_str() } impl Dialog { fn menu_selected (&self) -> Option { if let Self::Menu(selected) = self { Some(*selected) } else { None } } fn device_selected (&self) -> Option { if let Self::Device(selected) = self { Some(*selected) } else { None } } fn message (&self) -> Option<&str> { todo!() } fn browser (&self) -> Option<&Arc> { todo!() } fn browser_target (&self) -> Option<&BrowserTarget> { todo!() } }