wip: nromalize

This commit is contained in:
okay stopped screaming 2026-03-21 22:54:29 +02:00
parent 7ff1d989a9
commit 513b8354a3
14 changed files with 2324 additions and 2337 deletions

View file

@ -4,10 +4,18 @@
impl_trait_in_assoc_type, trait_alias, type_alias_impl_trait, type_changing_struct_update
)]
mod tek_struct; pub use self::tek_struct::*;
mod tek_trait; pub use self::tek_trait::*;
mod tek_type; pub use self::tek_type::*;
mod tek_impls;
pub mod arrange;
pub mod browse;
pub mod connect;
pub mod device;
pub mod mix;
pub mod plugin;
pub mod sample;
pub mod sequence;
pub mod tick;
use clap::{self, Parser, Subcommand};
use builder_pattern::Builder;
extern crate xdg;
pub(crate) use ::xdg::BaseDirectories;
@ -61,6 +69,7 @@ pub(crate) use tengri::{
ops::{Add, Sub, Mul, Div, Rem},
path::{Path, PathBuf},
sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}},
time::Duration,
thread::{spawn, JoinHandle},
},
};
@ -1215,3 +1224,585 @@ pub(crate) const HEADER: &'static str = r#"
~ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~
term ~ v0.4.0, 2026 winter (or is it) ~
~ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#;
/// Total state
///
/// ```
/// use tek::{HasTracks, HasScenes, TracksView, ScenesView};
/// let mut app = tek::App::default();
/// let _ = app.scene_add(None, None).unwrap();
/// let _ = app.update_clock();
/// app.project.editor = Some(Default::default());
/// //let _: Vec<_> = app.project.inputs_with_sizes().collect();
/// //let _: Vec<_> = app.project.outputs_with_sizes().collect();
/// let _: Vec<_> = app.project.tracks_with_sizes().collect();
/// //let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect();
/// //let _: Vec<_> = app.scenes_with_colors(true, 10).collect();
/// //let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect();
/// let _ = app.project.w();
/// //let _ = app.project.w_sidebar();
/// //let _ = app.project.w_tracks_area();
/// let _ = app.project.h();
/// //let _ = app.project.h_tracks_area();
/// //let _ = app.project.h_inputs();
/// //let _ = app.project.h_outputs();
/// let _ = app.project.h_scenes();
/// ```
#[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<Tui>,
/// Performance counter
pub perf: PerfModel,
/// Available view modes and input bindings
pub config: Config,
/// Currently selected mode
pub mode: Arc<Mode<Arc<str>>>,
/// Undo history
pub history: Vec<(AppCommand, Option<AppCommand>)>,
/// Dialog overlay
pub dialog: Dialog,
/// Contains all recently created clips.
pub pool: Pool,
/// Contains the currently edited musical arrangement
pub project: Arrangement,
/// Error, if any
pub error: Arc<RwLock<Option<Arc<str>>>>
}
/// Configuration: mode, view, and bind definitions.
///
/// ```
/// let config = tek::Config::default();
/// ```
///
/// ```
/// // Some dizzle.
/// // What indentation to use here lol?
/// let source = stringify!((mode :menu (name Menu)
/// (info Mode selector.) (keys :axis/y :confirm)
/// (view (bg (g 0) (bsp/s :ports/out
/// (bsp/n :ports/in
/// (bg (g 30) (bsp/s (fixed/y 7 :logo)
/// (fill :dialog/menu)))))))));
/// // Add this definition to the config and try to load it.
/// // A "mode" is basically a state machine
/// // with associated input and output definitions.
/// tek::Config::default().add(&source).unwrap().get_mode(":menu").unwrap();
/// ```
#[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,
}
/// Group of view and keys definitions.
///
/// ```
/// let mode = tek::Mode::<std::sync::Arc<str>>::default();
/// ```
#[derive(Default, Debug)] pub struct Mode<D: Language + Ord> {
pub path: PathBuf,
pub name: Vec<D>,
pub info: Vec<D>,
pub view: Vec<D>,
pub keys: Vec<D>,
pub modes: Modes,
}
/// An map of input events (e.g. [TuiEvent]) to [Binding]s.
///
/// ```
/// let lang = "(@x (nop)) (@y (nop) (nop))";
/// let bind = tek::Bind::<tek::tengri::TuiEvent, std::sync::Arc<str>>::load(&lang).unwrap();
/// assert_eq!(bind.query(&'x'.into()).map(|x|x.len()), Some(1));
/// //assert_eq!(bind.query(&'y'.into()).map(|x|x.len()), Some(2));
/// ```
#[derive(Debug)] pub struct Bind<E, C>(
/// Map of each event (e.g. key combination) to
/// all command expressions bound to it by
/// all loaded input layers.
pub BTreeMap<E, Vec<Binding<C>>>
);
/// A sequence of zero or more commands (e.g. [AppCommand]),
/// optionally filtered by [Condition] to form layers.
///
/// ```
/// //FIXME: Why does it overflow?
/// //let binding: Binding<()> = tek::Binding { ..Default::default() };
/// ```
#[derive(Debug, Clone)] pub struct Binding<C> {
pub commands: Arc<[C]>,
pub condition: Option<Condition>,
pub description: Option<Arc<str>>,
pub source: Option<Arc<PathBuf>>,
}
/// Condition that must evaluate to true in order to enable an input layer.
///
/// ```
/// let condition = tek::Condition(std::sync::Arc::new(Box::new(||{true})));
/// ```
#[derive(Clone)] pub struct Condition(
pub Arc<Box<dyn Fn()->bool + Send + Sync>>
);
/// List of menu items.
///
/// ```
/// let items: tek::MenuItems = Default::default();
/// ```
#[derive(Debug, Clone, Default, PartialEq)] pub struct MenuItems(
pub Arc<[MenuItem]>
);
/// An item of a menu.
///
/// ```
/// let item: tek::MenuItem = Default::default();
/// ```
#[derive(Clone)] pub struct MenuItem(
/// Label
pub Arc<str>,
/// Callback
pub Arc<Box<dyn Fn(&mut App)->Usually<()> + Send + Sync>>
);
/// The command-line interface descriptor.
///
/// ```
/// let cli: tek::Cli = Default::default();
///
/// use clap::CommandFactory;
/// tek::Cli::command().debug_assert();
/// ```
#[derive(Parser)]
#[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))]
#[derive(Debug, Default)] pub struct Cli {
/// Pre-defined configuration modes.
///
/// TODO: Replace these with scripted configurations.
#[command(subcommand)] pub action: Action,
}
/// Application modes that can be passed to the mommand line interface.
///
/// ```
/// let action: tek::Action = Default::default();
/// ```
#[derive(Debug, Clone, Subcommand, Default)] pub enum Action {
/// Continue where you left off
#[default] Resume,
/// Run headlessly in current session.
Headless,
/// 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<String>,
/// 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<f64>,
/// 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<String>,
/// MIDI outs to connect to (multiple instances accepted)
#[arg(short='i', long)] midi_from_re: Vec<String>,
/// MIDI ins to connect to (multiple instances accepted)
#[arg(short='O', long)] midi_to: Vec<String>,
/// MIDI ins to connect to (multiple instances accepted)
#[arg(short='o', long)] midi_to_re: Vec<String>,
/// Audio outs to connect to left input
#[arg(short='l', long)] left_from: Vec<String>,
/// Audio outs to connect to right input
#[arg(short='r', long)] right_from: Vec<String>,
/// Audio ins to connect from left output
#[arg(short='L', long)] left_to: Vec<String>,
/// Audio ins to connect from right output
#[arg(short='R', long)] right_to: Vec<String>,
/// Tracks to create
#[arg(short='t', long)] tracks: Option<usize>,
/// Scenes to create
#[arg(short='s', long)] scenes: Option<usize>,
},
/// Import media as new session.
Import,
/// Show configuration.
Config,
/// Show version.
Version,
}
pub type SceneWith<'a, T> =
(usize, &'a Scene, usize, usize, T);
/// Collection of interaction modes.
pub type Modes = Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode<Arc<str>>>>>>;
/// Collection of input bindings.
pub type Binds = Arc<RwLock<BTreeMap<Arc<str>, Bind<TuiEvent, Arc<str>>>>>;
/// Collection of view definitions.
pub type Views = Arc<RwLock<BTreeMap<Arc<str>, Arc<str>>>>;
pub trait HasClipsSize { fn clips_size (&self) -> &Measure<Tui>; }
pub trait HasDevices: AsRef<Vec<Device>> + AsMut<Vec<Device>> {
fn devices (&self) -> &Vec<Device> { self.as_ref() }
fn devices_mut (&mut self) -> &mut Vec<Device> { self.as_mut() }
}
pub trait HasWidth {
const MIN_WIDTH: usize;
/// Increment track width.
fn width_inc (&mut self);
/// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH].
fn width_dec (&mut self);
}
/// A control axis.
///
/// ```
/// let axis = tek::ControlAxis::X;
/// ```
#[derive(Debug, Copy, Clone)] pub enum ControlAxis {
X, Y, Z, I
}
/// Various possible dialog modes.
///
/// ```
/// let dialog: tek::Dialog = Default::default();
/// ```
#[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog {
#[default] None,
Help(usize),
Menu(usize, MenuItems),
Device(usize),
Message(Arc<str>),
Browse(BrowseTarget, Arc<Browse>),
Options,
}
/// A device that can be plugged into the chain.
///
/// ```
/// let device = tek::Device::default();
/// ```
#[derive(Debug, Default)] pub enum Device {
#[default]
Bypass,
Mute,
#[cfg(feature = "sampler")]
Sampler(Sampler),
#[cfg(feature = "lv2")] // TODO
Lv2(Lv2),
#[cfg(feature = "vst2")] // TODO
Vst2,
#[cfg(feature = "vst3")] // TODO
Vst3,
#[cfg(feature = "clap")] // TODO
Clap,
#[cfg(feature = "sf2")] // TODO
Sf2,
}
/// Some sort of wrapper?
pub struct DeviceAudio<'a>(pub &'a mut Device);
/// Command-line configuration.
#[cfg(feature = "cli")] impl Cli {
pub fn run (&self) -> Usually<()> {
if let Action::Version = self.action {
return Ok(tek_show_version())
}
let mut config = Config::new(None);
config.init()?;
if let Action::Config = self.action {
tek_print_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 {
// Connect to JACK
let name = name.as_ref().map_or("tek", |x|x.as_str());
let jack = Jack::new(&name)?;
// TODO: Collect audio IO:
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()];
// Create initial project:
let clock = Clock::new(&jack, *bpm)?;
let mut project = Arrangement::new(
&jack,
None,
clock,
vec![],
vec![],
Connect::collect(&midi_from, &[] as &[&str], &midi_from_re).iter().enumerate()
.map(|(index, connect)|jack.midi_in(&format!("M/{index}"), &[connect.clone()]))
.collect::<Result<Vec<_>, _>>()?,
Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate()
.map(|(index, connect)|jack.midi_out(&format!("{index}/M"), &[connect.clone()]))
.collect::<Result<Vec<_>, _>>()?
);
project.tracks_add(tracks.unwrap_or(0), None, &[], &[])?;
project.scenes_add(scenes.unwrap_or(0))?;
if matches!(self.action, Action::Status) {
// Show status and exit
tek_print_status(&project);
return Ok(())
}
// Initialize the app state
let app = tek(&jack, project, config, ":menu");
if matches!(self.action, Action::Headless) {
// TODO: Headless mode (daemon + client over IPC, then over network...)
println!("todo headless");
return Ok(())
}
// Run the [Tui] and [Jack] threads with the [App] state.
Tui::new(Box::new(std::io::stdout()))?.run(true, &jack.run(move|jack|{
// Between jack init and app's first cycle:
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
})?;
jack.sync_follow(*sync_follow)?;
// FIXME: They don't work properly.
Ok(app)
})?)?;
}
Ok(())
}
}
impl Config {
const CONFIG_DIR: &'static str = "tek";
const CONFIG_SUB: &'static str = "v0";
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<BaseDirectories>) -> Self {
let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB);
let dirs = dirs.unwrap_or_else(default);
Self { dirs, ..Default::default() }
}
/// Write initial contents of configuration.
pub fn init (&mut self) -> Usually<()> {
self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|{
cfgs.add(&dsl)?;
Ok(())
})?;
Ok(())
}
/// Write initial contents of a configuration 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() {
//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())
})
}
/// Add statements to configuration from [Dsl] source.
pub fn add (&mut self, dsl: impl Language) -> Usually<&mut Self> {
dsl.each(|item|self.add_one(item))?;
Ok(self)
}
fn add_one (&self, item: impl Language) -> 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 => 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())
}
}
pub fn get_mode (&self, mode: impl AsRef<str>) -> Option<Arc<Mode<Arc<str>>>> {
self.modes.clone().read().unwrap().get(mode.as_ref()).cloned()
}
}
impl Mode<Arc<str>> {
/// Add a definition to the mode.
///
/// Supported definitions:
///
/// - (name ...) -> name
/// - (info ...) -> description
/// - (keys ...) -> key bindings
/// - (mode ...) -> submode
/// - ... -> view
///
/// ```
/// let mut mode: tek::Mode<std::sync::Arc<str>> = Default::default();
/// mode.add("(name hello)").unwrap();
/// ```
pub fn add (&mut self, dsl: impl Language) -> Usually<()> {
Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() {
//println!("Mode::add: {head} {:?}", expr.tail());
let tail = expr.tail()?.map(|x|x.trim()).unwrap_or("");
match head {
"name" => self.add_name(tail)?,
"info" => self.add_info(tail)?,
"keys" => self.add_keys(tail)?,
"mode" => self.add_mode(tail)?,
_ => self.add_view(tail)?,
};
} else if let Ok(Some(word)) = dsl.word() {
self.add_view(word);
} else {
return Err(format!("Mode::add: unexpected: {dsl:?}").into());
})
//DslParse(dsl, ||Err(format!("Mode::add: unexpected: {dsl:?}").into()))
//.word(|word|self.add_view(word))
//.expr(|expr|expr.head(|head|{
////println!("Mode::add: {head} {:?}", expr.tail());
//let tail = expr.tail()?.map(|x|x.trim()).unwrap_or("");
//match head {
//"name" => self.add_name(tail),
//"info" => self.add_info(tail),
//"keys" => self.add_keys(tail)?,
//"mode" => self.add_mode(tail)?,
//_ => self.add_view(tail),
//};
//}))
}
fn add_name (&mut self, dsl: impl Language) -> Perhaps<()> {
Ok(dsl.src()?.map(|src|self.name.push(src.into())))
}
fn add_info (&mut self, dsl: impl Language) -> Perhaps<()> {
Ok(dsl.src()?.map(|src|self.info.push(src.into())))
}
fn add_view (&mut self, dsl: impl Language) -> Perhaps<()> {
Ok(dsl.src()?.map(|src|self.view.push(src.into())))
}
fn add_keys (&mut self, dsl: impl Language) -> Perhaps<()> {
Ok(Some(dsl.each(|expr|{ self.keys.push(expr.trim().into()); Ok(()) })?))
}
fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> {
Ok(Some(if let Some(id) = dsl.head()? {
load_mode(&self.modes, &id, &dsl.tail())?;
} else {
return Err(format!("Mode::add: self: incomplete: {dsl:?}").into());
}))
}
}
impl<E: Clone + Ord, C> Bind<E, C> {
/// 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<C>) -> Self {
self.add(event, binding);
self
}
/// Add a binding to an event map.
pub fn add (&mut self, event: E, binding: Binding<C>) -> &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<C>]> {
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<C>> {
self.query(event)
.map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next())
.flatten()
}
}
impl Bind<TuiEvent, Arc<str>> {
pub fn load (lang: &impl Language) -> Usually<Self> {
let mut map = Bind::new();
lang.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())
})?;
Ok(map)
}
}