mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 03:36:41 +01:00
268 lines
12 KiB
Rust
268 lines
12 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::*;
|
|
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 modes and input bindings
|
|
pub config: Config,
|
|
/// Currently selected mode
|
|
pub mode: Mode<Arc<str>>,
|
|
/// 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,
|
|
}
|
|
/// Configuration
|
|
#[derive(Default, Debug)]
|
|
pub struct Config {
|
|
pub dirs: BaseDirectories,
|
|
pub modes: Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode<Arc<str>>>>>>,
|
|
pub binds: Arc<RwLock<BTreeMap<Arc<str>, EventMap<Option<TuiEvent>, Arc<str>>>>>,
|
|
}
|
|
#[derive(Default, Debug)]
|
|
pub struct Mode<D: Dsl + Ord> {
|
|
pub path: PathBuf,
|
|
pub name: Vec<D>,
|
|
pub info: Vec<D>,
|
|
pub view: Vec<D>,
|
|
pub keys: Vec<D>,
|
|
pub modes: BTreeMap<D, Mode<D>>,
|
|
}
|
|
impl Config {
|
|
const CONFIG: &'static str = "tek.edn";
|
|
const DEFAULTS: &'static str = include_str!("../../tek.edn");
|
|
pub fn init () -> Usually<Self> {
|
|
let mut cfgs: Self = Default::default();
|
|
cfgs.dirs = BaseDirectories::with_profile("tek", "v0");
|
|
cfgs.init_file(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|cfgs.load_defs(dsl))?;
|
|
Ok(cfgs)
|
|
}
|
|
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_defs <D: Dsl> (&mut self, dsl: D) -> Usually<()> {
|
|
dsl.each(|item|{
|
|
println!("{item:?}");
|
|
Ok(match item.exp().head() {
|
|
Ok(Some("keys")) if let Some(id) = item.exp().tail().head()? => {
|
|
self.binds.write().unwrap().insert(id.into(), {
|
|
let mut map = EventMap::new();
|
|
item.exp().tail().tail()?.each(|item|Ok({
|
|
if let Ok(Some(sym)) = item.exp().head().sym() {
|
|
map.add(TuiEvent::from_dsl(item.exp()?.head()?)?, Binding {
|
|
command: item.exp()?.tail()?.unwrap_or_default().into(),
|
|
condition: None,
|
|
description: None,
|
|
source: None
|
|
});
|
|
} else if item.exp().head() == Ok(Some("see")) {
|
|
// TODO
|
|
} else {
|
|
return Err(format!("load_defs: unexpected: {item:?}").into())
|
|
}
|
|
}))?;
|
|
map
|
|
});
|
|
},
|
|
Ok(Some("mode")) if let Some(id) = item.exp().tail().head()? => {
|
|
self.modes.write().unwrap().insert(id.into(), {
|
|
let mut mode = Mode::default();
|
|
item.exp().tail().tail()?.each(|item|Ok(if let Ok(Some(exp)) = item.exp() {
|
|
match exp.head()? {
|
|
Some("name") => mode.name.push(
|
|
exp.tail()?.map(|x|x.trim()).unwrap_or("").into()
|
|
),
|
|
Some("info") => mode.info.push(
|
|
exp.tail()?.map(|x|x.trim()).unwrap_or("").into()
|
|
),
|
|
Some("keys") => if let Some(tail) = exp.tail()? {
|
|
tail.each(|keys|Ok(mode.keys.push(keys.trim().into())))?;
|
|
} else {
|
|
return Err(format!("load_view: empty keys: {exp}").into())
|
|
},
|
|
Some("mode") => if let (Some(name), Some(tail)) = (
|
|
exp.tail()?.head()?, exp.tail()?.tail()?,
|
|
) {
|
|
let mut submode: Mode<Arc<str>> = Default::default();
|
|
tail.each(|item|Ok(if let Ok(Some(exp)) = item.exp() {
|
|
match exp.head()? {
|
|
Some("keys") => if let Some(tail) = exp.tail()? {
|
|
tail.each(|keys|Ok(mode.keys.push(keys.trim().into())))?;
|
|
} else {
|
|
return Err(format!("load_view: empty keys: {exp}").into())
|
|
},
|
|
_ => {
|
|
return Err(format!("load_view: unexpected in mode {name}: {item:?}").into())
|
|
}
|
|
}
|
|
} else if let Ok(Some(sym)) = item.sym() {
|
|
// TODO
|
|
} else {
|
|
return Err(format!("load_view: unexpected in mode {name}: {item:?}").into())
|
|
}))?;
|
|
mode.modes.insert(name.trim().into(), submode);
|
|
} else {
|
|
return Err(format!("load_view: empty mode: {exp}").into())
|
|
},
|
|
Some(_) => mode.view.push(exp.into()),
|
|
None => return Err(format!("load_view: empty: {exp}").into())
|
|
}
|
|
} else if let Ok(Some(sym)) = item.sym() {
|
|
mode.view.push(sym.into());
|
|
} else {
|
|
return Err(format!("load_view: unexpected: {dsl:?}").into())
|
|
}))?;
|
|
mode.into()
|
|
});
|
|
},
|
|
_ => return Err(format!("load_defs: unexpected: {item:?}").into())
|
|
})
|
|
})
|
|
}
|
|
}
|
|
fn load_modules <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());
|
|
}
|
|
}
|
|
/// 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() } }
|
|
|
|
#[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!()
|
|
}
|
|
}
|