tek/crates/app/app.rs
2025-08-10 03:37:04 +03:00

278 lines
11 KiB
Rust

// ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
//██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_dsl; pub use self::app_dsl::*;
macro_rules!dsl_sym(
(|$state:ident:$State:ty| -> $type:ty {$($lit:literal => $exp:expr),* $(,)?})=>{
impl<'t> DslSymNs<'t, $type> for $State {
const NS: DslNs<'t, fn (&'t $State)->$type> =
DslNs(&[$(($lit, |$state: &$State|$exp)),*]); } });
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<TuiOut>,
/// 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<AppCommand>)>,
/// Dialog overlay
pub dialog: Dialog,
/// Base color.
pub color: ItemTheme,
}
type ModuleMap<T> = Arc<RwLock<BTreeMap<Arc<str>, T>>>;
/// Configuration
#[derive(Default, Debug)]
pub struct Config {
/// XDG basedirs
pub dirs: BaseDirectories,
/// Available view profiles
pub views: ModuleMap<Profile>,
/// Available input bindings
pub binds: ModuleMap<EventMap<Option<TuiEvent>, Arc<str>>>,
}
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<Self> {
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 <D: Dsl> (&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 <D: Dsl> (&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 <D: Dsl, U> (dsl: D, cb: impl Fn(&str, &str)->Usually<U>) -> 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<Arc<str>>,
/// Description of configuration
pub info: Option<Arc<str>>,
/// View definition
pub view: Arc<str>,
// Input keymap
pub keys: EventMap<TuiEvent, AppCommand>,
}
impl Profile {
fn from_dsl (dsl: impl Dsl) -> Usually<Self> {
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.
impl App {
pub fn update_clock (&self) {
ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80)
}
pub fn focused_editor (&self) -> bool {
false
}
}
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<MidiEditor>: |self: App|self.project.editor);
has!(Selection: |self: App|self.project.selection);
has!(Vec<MidiInput>: |self: App|self.project.midi_ins);
has!(Vec<MidiOutput>: |self: App|self.project.midi_outs);
has!(Vec<Scene>: |self: App|self.project.scenes);
has!(Vec<Track>: |self: App|self.project.tracks);
has!(Measure<TuiOut>: |self: App|self.size);
has_clips!( |self: App|self.pool.clips);
maybe_has!(Track: |self: App| { MaybeHas::<Track>::get(&self.project) };
{ MaybeHas::<Track>::get_mut(&mut self.project) });
maybe_has!(Scene: |self: App| { MaybeHas::<Scene>::get(&self.project) };
{ MaybeHas::<Scene>::get_mut(&mut self.project) });
impl HasClipsSize for App { fn clips_size (&self) -> &Measure<TuiOut> { &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()
}
#[derive(Debug, Clone, Default)]
pub enum Dialog {
#[default] None,
Help(usize),
Menu(usize),
Device(usize),
Message(Arc<str>),
Browser(BrowserTarget, Arc<Browser>),
Options,
}
impl Dialog {
fn menu_selected (&self) -> Option<usize> {
if let Self::Menu(selected) = self { Some(*selected) } else { None }
}
fn device_kind (&self) -> Option<usize> {
if let Self::Device(index) = self { Some(*index) } else { None }
}
fn device_kind_next (&self) -> Option<usize> {
self.device_kind().map(|index|(index + 1) % device_kinds().len())
}
fn device_kind_prev (&self) -> Option<usize> {
self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1)))
}
fn message (&self) -> Option<&str> {
todo!()
}
fn browser (&self) -> Option<&Arc<Browser>> {
todo!()
}
fn browser_target (&self) -> Option<&BrowserTarget> {
todo!()
}
}