unify default configs
Some checks failed
/ build (push) Has been cancelled

the definitions are unified alright. it's just not supported yet :D

the idea being that tek offers to write out the default configs to
~/.config/tek-v0 where the user can customize them.
This commit is contained in:
🪞👃🪞 2025-07-30 19:12:45 +03:00
parent 9e147cda69
commit 3c8616deba
43 changed files with 441 additions and 465 deletions

View file

@ -15,6 +15,7 @@ clap = { workspace = true, optional = true }
palette = { workspace = true }
rand = { workspace = true }
toml = { workspace = true }
xdg = { workspace = true }
[dev-dependencies]
proptest = { workspace = true }

View file

@ -1,20 +0,0 @@
use tek::*;
use tengri::input::*;
use std::sync::*;
struct ExampleClips(Arc<RwLock<Vec<Arc<RwLock<MidiClip>>>>>);
impl HasClips for ExampleClips {
fn clips (&self) -> RwLockReadGuard<'_, Vec<Arc<RwLock<MidiClip>>>> {
self.0.read().unwrap()
}
fn clips_mut (&self) -> RwLockWriteGuard<'_, Vec<Arc<RwLock<MidiClip>>>> {
self.0.write().unwrap()
}
}
fn main () -> Result<(), Box<dyn std::error::Error>> {
let mut clips = Pool::default();//ExampleClips(Arc::new(vec![].into()));
PoolClipCommand::Import {
index: 0,
path: std::path::PathBuf::from("./example.mid")
}.execute(&mut clips)?;
Ok(())
}

View file

@ -7,7 +7,9 @@ macro_rules! cmd_todo { ($msg:literal) => {{ println!($msg); None }}; }
handle!(TuiIn: |self: App, input|self.handle_tui_key_with_history(input));
impl App {
fn handle_tui_key_with_history (&mut self, input: &TuiIn) -> Perhaps<bool> {
Ok(if let Some(binding) = self.config.keys.dispatch(input.event()) {
Ok(if let Some(binding) = self.configs.current.as_ref()
.map(|c|c.keys.dispatch(input.event())).flatten()
{
let binding = binding.clone();
let undo = binding.command.clone().execute(self)?;
// FIXME failed commands are not persisted in undo history

111
crates/app/src/config.rs Normal file
View file

@ -0,0 +1,111 @@
use crate::*;
use xdg::BaseDirectories;
/// Configurations
#[derive(Default, Debug)]
pub struct Configurations {
pub dirs: BaseDirectories,
pub modules: Arc<RwLock<std::collections::BTreeMap<Arc<str>, Arc<str>>>>,
pub current: Option<Configuration>
}
/// Configuration
#[derive(Default, Debug)]
pub struct Configuration {
/// Path of configuration entrypoint
pub path: std::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>,
}
macro_rules! dsl_for_each (($dsl:expr => |$head:ident|$body:expr)=>{
let mut dsl: Arc<str> = $dsl.src().into();
let mut $head: Option<Arc<str>> = dsl.head()?.map(Into::into);
let mut tail: Option<Arc<str>> = dsl.tail()?.map(Into::into);
loop {
if let Some($head) = $head {
$body;
} else {
break
}
if let Some(next) = tail {
$head = next.head()?.map(Into::into);
tail = next.tail()?.map(Into::into);
} else {
break
}
}
});
impl Configurations {
const DEFAULT_TEMPLATES: &'static str = include_str!("../../../config/templates.edn");
const DEFAULT_BINDINGS: &'static str = include_str!("../../../config/bindings.edn");
pub fn init () -> Usually<Self> {
let mut dirs = BaseDirectories::with_profile("tek", "v0");
let mut cfgs = Self { dirs, ..Default::default() };
cfgs.init_file("templates.edn", Self::DEFAULT_TEMPLATES)?;
cfgs.load_file("templates.edn", |head|{ Ok(()) })?;
cfgs.init_file("bindings.edn", Self::DEFAULT_BINDINGS)?;
cfgs.load_file("bindings.end", |head|{ Ok(()) })?;
Ok(cfgs)
}
fn init_file (&mut self, path: &str, val: &str) -> Usually<()> {
if self.dirs.find_config_file("templates.edn").is_none() {
std::fs::write(self.dirs.place_config_file("templates.edn")?, Self::DEFAULT_TEMPLATES);
}
Ok(())
}
fn load_file (&mut self, path: &str, mut each: impl FnMut(&Arc<str>)->Usually<()> ) -> Usually<()> {
Ok(if let Some(path) = self.dirs.find_config_file("templates.edn") {
dsl_for_each!(std::fs::read_to_string(path)?.as_str() => |dsl|each(&dsl));
} else {
return Err(format!("{path}: not found").into())
})
}
}
impl Configuration {
fn load_template (&mut self, dsl: impl Dsl) -> Usually<&mut Self> {
dsl_for_each!(dsl => |dsl|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().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)
}
}
fn unquote (x: &str) -> &str {
let mut chars = x.chars();
chars.next();
//chars.next_back();
chars.as_str()
}

View file

@ -36,6 +36,7 @@ pub(crate) use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed};
mod api; pub use self::api::*;
mod audio; pub use self::audio::*;
mod config; pub use self::config::*;
mod model; pub use self::model::*;
mod view; pub use self::view::*;

View file

@ -1,66 +1,28 @@
use crate::*;
use std::path::PathBuf;
use std::error::Error;
#[derive(Default, Debug)]
pub struct App {
/// Must not be dropped for the duration of the process
pub jack: Jack<'static>,
pub jack: Jack<'static>,
/// Display size
pub size: Measure<TuiOut>,
pub size: Measure<TuiOut>,
/// Performance counter
pub perf: PerfModel,
// View and input definition
pub config: Configuration,
pub perf: PerfModel,
/// Available view definitions and input bindings
pub configs: Configurations,
/// Contains all recently created clips.
pub pool: Pool,
pub pool: Pool,
/// Contains the currently edited musical arrangement
pub project: Arrangement,
/// Undo history
pub history: Vec<(AppCommand, Option<AppCommand>)>,
// Dialog overlay
pub dialog: Option<Dialog>,
pub dialog: Option<Dialog>,
/// Base color.
pub color: ItemTheme,
pub color: ItemTheme,
}
has!(Jack<'static>: |self: App|self.jack);
has!(Pool: |self: App|self.pool);
has!(Option<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);
maybe_has!(Track: |self: App|
{ MaybeHas::<Track>::get(&self.project) };
{ MaybeHas::<Track>::get_mut(&mut self.project) });
impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() } }
maybe_has!(Scene: |self: App|
{ MaybeHas::<Scene>::get(&self.project) };
{ MaybeHas::<Scene>::get_mut(&mut self.project) });
impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } }
has_clips!(|self: App|self.pool.clips);
impl HasClipsSize for App { fn clips_size (&self) -> &Measure<TuiOut> { &self.project.inner_size } }
//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));
//take!(DialogCommand |state: App, iter|Take::take(&state.dialog, iter));
//has_editor!(|self: App|{
//editor = self.editor;
//editor_w = {
//let size = self.size.w();
//let editor = self.editor.as_ref().expect("missing editor");
//let time_len = editor.time_len().get();
//let time_zoom = editor.time_zoom().get().max(1);
//(5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16)
//};
//editor_h = 15;
//is_editing = self.editor.is_some();
//});
impl App {
pub fn update_clock (&self) {
@ -190,20 +152,16 @@ impl App {
self.browser().is_some()
}
fn focus_clip (&self) -> bool {
!self.focus_editor() && matches!(self.selection(),
Selection::TrackClip{..})
!self.focus_editor() && matches!(self.selection(), Selection::TrackClip{..})
}
fn focus_track (&self) -> bool {
!self.focus_editor() && matches!(self.selection(),
Selection::Track(..))
!self.focus_editor() && matches!(self.selection(), Selection::Track(..))
}
fn focus_scene (&self) -> bool {
!self.focus_editor() && matches!(self.selection(),
Selection::Scene(..))
!self.focus_editor() && matches!(self.selection(), Selection::Scene(..))
}
fn focus_mix (&self) -> bool {
!self.focus_editor() && matches!(self.selection(),
Selection::Mix)
!self.focus_editor() && matches!(self.selection(), Selection::Mix)
}
fn focus_pool_import (&self) -> bool {
matches!(self.pool.mode, Some(PoolMode::Import(..)))
@ -318,99 +276,43 @@ impl App {
}
}
/// Configuration
#[derive(Default, Debug)]
pub struct Configuration {
/// 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>,
}
has!(Jack<'static>: |self: App|self.jack);
has!(Pool: |self: App|self.pool);
has!(Option<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);
maybe_has!(Track: |self: App|
{ MaybeHas::<Track>::get(&self.project) };
{ MaybeHas::<Track>::get_mut(&mut self.project) });
impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() } }
maybe_has!(Scene: |self: App|
{ MaybeHas::<Scene>::get(&self.project) };
{ MaybeHas::<Scene>::get_mut(&mut self.project) });
impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } }
has_clips!(|self: App|self.pool.clips);
impl HasClipsSize for App { fn clips_size (&self) -> &Measure<TuiOut> { &self.project.inner_size } }
impl Configuration {
pub fn from_path (path: &impl AsRef<Path>, _watch: bool) -> Usually<Self> {
let mut config = Self { path: path.as_ref().into(), ..Default::default() };
let mut dsl = read_and_leak(path.as_ref())?;
let mut head: Option<Arc<str>> = dsl.head()?.map(Into::into);
let mut tail: Option<Arc<str>> = dsl.tail()?.map(Into::into);
loop {
if let Some(exp) = head.exp()? {
match exp.head()?.key()? {
Some("name") => match exp.tail()?.text()? {
Some(name) => config.name = Some(name.into()),
_ => return Err(format!("missing name definition").into())
},
Some("info") => match exp.tail()?.text()? {
Some(info) => config.info = Some(info.into()),
_ => return Err(format!("missing info definition").into())
},
Some("keys") => match exp.tail()? {
Some(keys) => config.keys = EventMap::from_dsl(&mut &keys)?,
_ => return Err(format!("missing keys definition").into())
},
Some("view") => match exp.tail()? {
Some(tail) => config.view = tail.src().into(),
_ => return Err(format!("missing view definition").into())
},
Some(k) => return Err(format!("(e3) unexpected key {k:?} in {exp:?}").into()),
None => return Err(format!("(e2) unexpected exp {exp:?}").into()),
}
} else {
break
}
if let Some(next) = tail {
head = next.head()?.map(Into::into);
tail = next.tail()?.map(Into::into);
} else {
break
}
}
Ok(config)
}
}
fn read_and_leak (path: impl AsRef<Path>) -> Usually<&'static str> {
Ok(leak(String::from_utf8(std::fs::read(path.as_ref())?)?))
}
fn leak (x: impl AsRef<str>) -> &'static str {
Box::leak(x.as_ref().into())
}
fn unquote (x: &str) -> &str {
let mut chars = x.chars();
chars.next();
//chars.next_back();
chars.as_str()
}
macro_rules! default_config { ($path:literal) => { ($path, include_str!($path)) }; }
pub const DEFAULT_CONFIGS: &'static [(&'static str, &'static str)] = &[
default_config!("../../../config/config_arranger.edn"),
default_config!("../../../config/config_groovebox.edn"),
default_config!("../../../config/config_sampler.edn"),
default_config!("../../../config/config_sequencer.edn"),
default_config!("../../../config/config_transport.edn"),
default_config!("../../../config/keys_arranger.edn"),
default_config!("../../../config/keys_clip.edn"),
default_config!("../../../config/keys_clock.edn"),
default_config!("../../../config/keys_editor.edn"),
default_config!("../../../config/keys_global.edn"),
default_config!("../../../config/keys_groovebox.edn"),
default_config!("../../../config/keys_length.edn"),
default_config!("../../../config/keys_mix.edn"),
default_config!("../../../config/keys_pool.edn"),
default_config!("../../../config/keys_pool_file.edn"),
default_config!("../../../config/keys_rename.edn"),
default_config!("../../../config/keys_sampler.edn"),
default_config!("../../../config/keys_scene.edn"),
default_config!("../../../config/keys_sequencer.edn"),
default_config!("../../../config/keys_track.edn"),
];
//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));
//take!(DialogCommand |state: App, iter|Take::take(&state.dialog, iter));
//has_editor!(|self: App|{
//editor = self.editor;
//editor_w = {
//let size = self.size.w();
//let editor = self.editor.as_ref().expect("missing editor");
//let time_len = editor.time_len().get();
//let time_zoom = editor.time_zoom().get().max(1);
//(5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16)
//};
//editor_h = 15;
//is_editing = self.editor.is_some();
//});

View file

@ -23,14 +23,14 @@ impl<T: Content<TuiOut>> Content<TuiOut> for ErrorBoundary<TuiOut, T> {
}
impl App {
pub fn view (model: &Self) -> impl Content<TuiOut> + '_ {
ErrorBoundary::new(Ok(Some(Tui::bg(Black, model.view_menu()))))
pub fn view (&self) -> impl Content<TuiOut> + '_ {
ErrorBoundary::new(Ok(Some(Tui::bg(Black, self.view_menu()))))
//ErrorBoundary::new(Take::take(model, &mut model.config.view.clone()))
//ErrorBoundary::new(Give::give(model, &mut model.config.view.clone()))
}
}
content!(TuiOut: |self: App| ErrorBoundary::new(Ok(Some(Tui::bg(Black, self.view_nil())))));
content!(TuiOut: |self: App| ErrorBoundary::new(Ok(Some(Tui::bg(Black, self.view())))));
#[tengri_proc::view(TuiOut)]
impl App {
@ -39,7 +39,7 @@ impl App {
}
pub fn view_menu (&self) -> impl Content<TuiOut> + use<'_> {
Stack::south(|add: &mut dyn FnMut(&dyn Render<TuiOut>)|{
add(&Tui::bold(true, "tek"));
add(&Tui::bold(true, "tek 0.3.0-rc0"));
add(&"");
add(&"+ new session");
})

View file

@ -13,7 +13,7 @@ pub struct Cli {
/// Pre-defined configuration modes.
///
/// TODO: Replace these with scripted configurations.
#[command(subcommand)] mode: LaunchMode,
#[command(subcommand)] mode: Option<LaunchMode>,
/// Name of JACK client
#[arg(short='n', long)] name: Option<String>,
/// Whether to attempt to become transport master
@ -45,29 +45,8 @@ pub struct Cli {
/// Application modes
#[derive(Debug, Clone, Subcommand)]
pub enum LaunchMode {
/// ⏯️ A standalone transport clock.
Clock,
/// 🎼 A MIDI sequencer.
Sequencer,
/// 🎺 A MIDI-controlled audio sampler.
Sampler,
/// 📻 Sequencer and sampler together.
Groovebox,
/// 🎧 Multi-track MIDI sequencer.
Arranger {
/// Number of scenes
#[arg(short = 'y', long, default_value_t = 16)] scenes: usize,
/// Number of tracks
#[arg(short = 'x', long, default_value_t = 12)] tracks: usize,
/// Width of tracks
#[arg(short = 'w', long, default_value_t = 15)] track_width: usize,
},
/// TODO: A MIDI-controlled audio mixer
Mixer,
/// TODO: A customizable channel strip
Track,
/// TODO: An audio plugin host
Plugin,
/// Create a new session instead of loading the previous one.
New,
}
impl Cli {
@ -86,9 +65,6 @@ impl Cli {
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 clip = Arc::new(RwLock::new(MidiClip::new(
"Clip", true, 384usize, None, Some(ItemColor::random().into())),
));
Tui::new()?.run(&Jack::new_run(&name, move|jack|{
for (index, connect) in midi_froms.iter().enumerate() {
midi_ins.push(jack.midi_in(&format!("M/{index}"), &[connect.clone()])?);
@ -96,34 +72,12 @@ impl Cli {
for (index, connect) in midi_tos.iter().enumerate() {
midi_outs.push(jack.midi_out(&format!("{index}/M"), &[connect.clone()])?);
};
let config = Configuration::from_path(&match self.mode {
LaunchMode::Clock => "config/config_transport.edn",
LaunchMode::Sequencer => "config/config_sequencer.edn",
LaunchMode::Groovebox => "config/config_groovebox.edn",
LaunchMode::Arranger { .. } => "config/config_arranger.edn",
LaunchMode::Sampler => "config/config_sampler.edn",
_ => todo!("{:?}", self.mode),
}, false)?;
let configs = Configurations::init();
let clock = Clock::new(&jack, self.bpm)?;
match self.mode {
LaunchMode::Sequencer => tracks.push(Track::new(
&name, None, &jack, Some(&clock), Some(&clip),
midi_froms.as_slice(), midi_tos.as_slice()
)?),
LaunchMode::Groovebox | LaunchMode::Sampler => tracks.push(Track::new_with_sampler(
&name, None, &jack, Some(&clock), Some(&clip),
midi_froms.as_slice(), midi_tos.as_slice(), audio_froms, audio_tos,
)?),
_ => {}
}
let mut app = App {
jack: jack.clone(),
config,
color: ItemTheme::random(),
pool: match self.mode {
LaunchMode::Sequencer | LaunchMode::Groovebox => (&clip).into(),
_ => Default::default()
},
jack: jack.clone(),
configs: Configurations::init()?,
color: ItemTheme::random(),
project: Arrangement {
name: Default::default(),
color: ItemTheme::random(),
@ -134,20 +88,16 @@ impl Cli {
selection: Selection::TrackClip { track: 0, scene: 0 },
midi_ins,
midi_outs,
editor: match self.mode {
LaunchMode::Sequencer | LaunchMode::Groovebox => Some((&clip).into()),
_ => None
},
..Default::default()
},
..Default::default()
};
if let LaunchMode::Arranger { scenes, tracks, track_width, .. } = self.mode {
app.project.arranger = Default::default();
app.project.selection = Selection::TrackClip { track: 1, scene: 1 };
app.project.scenes_add(scenes)?;
app.project.tracks_add(tracks, Some(track_width), &[], &[])?;
}
//if let LaunchMode::Arranger { scenes, tracks, track_width, .. } = self.mode {
//app.project.arranger = Default::default();
//app.project.selection = Selection::TrackClip { track: 1, scene: 1 };
//app.project.scenes_add(scenes)?;
//app.project.tracks_add(tracks, Some(track_width), &[], &[])?;
//}
jack.sync_lead(self.sync_lead, |mut state|{
let clock = app.clock();
clock.playhead.update_from_sample(state.position.frame() as f64);
@ -162,10 +112,12 @@ impl Cli {
/// CLI header
const HEADER: &'static str = r#"
"#;
~ ~~~~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~~~~~~
~ ~ ~ ~< ~ v0.3.0-rc.0 "no, i insist that i am not a dj ~
~ ~ ~ ~ 2025, summer, the nose of the cat. J ~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
On first run, Tek will create configuration and state dirs. ~
On subsequent runs, Tek should resume from where you left off. ~"#;
#[cfg(test)] #[test] fn test_cli () {
use clap::CommandFactory;