review main module

This commit is contained in:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 2026-01-17 00:24:52 +02:00
parent dcde588c7b
commit 2fe2cc47db
9 changed files with 1318 additions and 1406 deletions

View file

@ -8,7 +8,7 @@ path = "tek.rs"
[[bin]]
name = "tek"
path = "tek_cli.rs"
path = "tek.rs"
[target.'cfg(target_os = "linux")']
rustflags = ["-C", "link-arg=-fuse-ld=mold"]

1595
app/tek.rs

File diff suppressed because it is too large Load diff

View file

@ -1,325 +0,0 @@
use crate::*;
pub type Binds = Arc<RwLock<BTreeMap<Arc<str>, EventMap<TuiEvent, Arc<str>>>>>;
/// A collection of input bindings.
#[derive(Debug)]
pub struct EventMap<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>>>
);
/// An input binding.
#[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>>,
}
/// Input bindings are only returned if this evaluates to true
#[derive(Clone)]
pub struct Condition(Arc<Box<dyn Fn()->bool + Send + Sync>>);
/// Default is always empty map regardless if `E` and `C` implement [Default].
impl<E, C> Default for EventMap<E, C> {
fn default () -> Self { Self(Default::default()) }
}
impl<E: Clone + Ord, C> EventMap<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 EventMap<TuiEvent, Arc<str>> {
pub fn load_into (binds: &Binds, name: &impl AsRef<str>, body: &impl Dsl) -> Usually<()> {
println!("EventMap::load_into: {}: {body:?}", name.as_ref());
let mut map = Self::new();
body.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())
})?;
binds.write().unwrap().insert(name.as_ref().into(), map);
Ok(())
}
}
impl<C> Binding<C> {
pub fn from_dsl (dsl: impl Dsl) -> Usually<Self> {
let command: Option<C> = None;
let condition: Option<Condition> = None;
let description: Option<Arc<str>> = None;
let source: Option<Arc<PathBuf>> = None;
if let Some(command) = command {
Ok(Self { commands: [command].into(), condition, description, source })
} else {
Err(format!("no command in {dsl:?}").into())
}
}
}
impl_debug!(Condition |self, w| { write!(w, "*") });
handle!(TuiIn:|self: App, input|{
let mut commands = vec![];
for id in self.mode.keys.iter() {
if let Some(event_map) = self.config.binds.clone().read().unwrap().get(id.as_ref()) {
if let Some(bindings) = event_map.query(input.event()) {
for binding in bindings {
for command in binding.commands.iter() {
if let Some(command) = self.from(command)? as Option<AppCommand> {
commands.push(command)
}
}
}
}
}
}
for command in commands.into_iter() {
let result = command.execute(self);
match result {
Ok(undo) => {
self.history.push((command, undo));
},
Err(e) => {
self.history.push((command, None));
return Err(e)
}
}
}
Ok(None)
});
#[derive(Debug, Copy, Clone)]
pub enum Axis { X, Y, Z, I }
impl<'a> DslNs<'a, AppCommand> for App {}
impl<'a> DslNsExprs<'a, AppCommand> for App {}
impl<'a> DslNsWords<'a, AppCommand> for App {
dsl_words!('a |app| -> AppCommand {
"x/inc" => AppCommand::Inc { axis: Axis::X },
"x/dec" => AppCommand::Dec { axis: Axis::X },
"y/inc" => AppCommand::Inc { axis: Axis::Y },
"y/dec" => AppCommand::Dec { axis: Axis::Y },
"confirm" => AppCommand::Confirm,
"cancel" => AppCommand::Cancel,
});
}
impl Default for AppCommand { fn default () -> Self { Self::Nop } }
def_command!(AppCommand: |app: App| {
Nop => Ok(None),
Confirm => Ok(match &app.dialog {
Dialog::Menu(index, items) => {
let callback = items.0[*index].1.clone();
callback(app)?;
None
},
_ => todo!(),
}),
Cancel => todo!(), // TODO delegate:
Inc { axis: Axis } => Ok(match (&app.dialog, axis) {
(Dialog::None, _) => todo!(),
(Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_next() }
.execute(app)?,
_ => todo!()
}),
Dec { axis: Axis } => Ok(match (&app.dialog, axis) {
(Dialog::None, _) => None,
(Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_prev() }
.execute(app)?,
_ => todo!()
}),
SetDialog { dialog: Dialog } => {
swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog })
},
});
//AppCommand => {
//("x/inc" /
//("stop-all") => todo!(),//app.project.stop_all(),
//("enqueue", clip: Option<Arc<RwLock<MidiClip>>>) => todo!(),
//("history", delta: isize) => todo!(),
//("zoom", zoom: usize) => todo!(),
//("select", selection: Selection) => todo!(),
//("dialog" / command: DialogCommand) => todo!(),
//("project" / command: ArrangementCommand) => todo!(),
//("clock" / command: ClockCommand) => todo!(),
//("sampler" / command: SamplerCommand) => todo!(),
//("pool" / command: PoolCommand) => todo!(),
//("edit" / editor: MidiEditCommand) => todo!(),
//};
//DialogCommand;
//ArrangementCommand;
//ClockCommand;
//SamplerCommand;
//PoolCommand;
//MidiEditCommand;
//take!(DialogCommand |state: App, iter|Take::take(&state.dialog, iter));
//#[derive(Clone, Debug)]
//pub enum DialogCommand {
//Open { dialog: Dialog },
//Close
//}
//impl Command<Option<Dialog>> for DialogCommand {
//fn execute (self, state: &mut Option<Dialog>) -> Perhaps<Self> {
//match self {
//Self::Open { dialog } => {
//*state = Some(dialog);
//},
//Self::Close => {
//*state = None;
//}
//};
//Ok(None)
//}
//}
//dsl!(DialogCommand: |self: Dialog, iter|todo!());
//Dsl::take(&mut self.dialog, iter));
//#[tengri_proc::command(Option<Dialog>)]//Nope.
//impl DialogCommand {
//fn open (dialog: &mut Option<Dialog>, new: Dialog) -> Perhaps<Self> {
//*dialog = Some(new);
//Ok(None)
//}
//fn close (dialog: &mut Option<Dialog>) -> Perhaps<Self> {
//*dialog = None;
//Ok(None)
//}
//}
//
//dsl_bind!(AppCommand: App {
//enqueue = |app, clip: Option<Arc<RwLock<MidiClip>>>| { todo!() };
//history = |app, delta: isize| { todo!() };
//zoom = |app, zoom: usize| { todo!() };
//stop_all = |app| { app.tracks_stop_all(); Ok(None) };
////dialog = |app, command: DialogCommand|
////Ok(command.delegate(&mut app.dialog, |c|Self::Dialog{command: c})?);
//project = |app, command: ArrangementCommand|
//Ok(command.delegate(&mut app.project, |c|Self::Project{command: c})?);
//clock = |app, command: ClockCommand|
//Ok(command.execute(app.clock_mut())?.map(|c|Self::Clock{command: c}));
//sampler = |app, command: SamplerCommand|
//Ok(app.project.sampler_mut().map(|s|command.delegate(s, |command|Self::Sampler{command}))
//.transpose()?.flatten());
//pool = |app, command: PoolCommand| {
//let undo = command.clone().delegate(&mut app.pool, |command|AppCommand::Pool{command})?;
//// update linked editor after pool action
//match command {
//// autoselect: automatically load selected clip in editor
//PoolCommand::Select { .. } |
//// autocolor: update color in all places simultaneously
//PoolCommand::Clip { command: PoolClipCommand::SetColor { .. } } => {
//let clip = app.pool.clip().clone();
//app.editor_mut().map(|editor|editor.set_clip(clip.as_ref()))
//},
//_ => None
//};
//Ok(undo)
//};
//select = |app, selection: Selection| {
//*app.project.selection_mut() = selection;
////todo!
////if let Some(ref mut editor) = app.editor_mut() {
////editor.set_clip(match selection {
////Selection::TrackClip { track, scene } if let Some(Some(Some(clip))) = app
////.project
////.scenes.get(scene)
////.map(|s|s.clips.get(track))
////=>
////Some(clip),
////_ =>
////None
////});
////}
//Ok(None)
////("select" [t: usize, s: usize] Some(match (t.expect("no track"), s.expect("no scene")) {
////(0, 0) => Self::Select(Selection::Mix),
////(t, 0) => Self::Select(Selection::Track(t)),
////(0, s) => Self::Select(Selection::Scene(s)),
////(t, s) => Self::Select(Selection::TrackClip { track: t, scene: s }) })))
//// autoedit: load focused clip in editor.
//};
////fn color (app: &mut App, theme: ItemTheme) -> Perhaps<Self> {
////Ok(app.set_color(Some(theme)).map(|theme|Self::Color{theme}))
////}
////fn launch (app: &mut App) -> Perhaps<Self> {
////app.project.launch();
////Ok(None)
////}
//toggle_editor = |app, value: bool|{ app.toggle_editor(Some(value)); Ok(None) };
//editor = |app, command: MidiEditCommand| Ok(if let Some(editor) = app.editor_mut() {
//let undo = command.clone().delegate(editor, |command|AppCommand::Editor{command})?;
//// update linked sampler after editor action
//app.project.sampler_mut().map(|sampler|match command {
//// autoselect: automatically select sample in sampler
//MidiEditCommand::SetNotePos { pos } => { sampler.set_note_pos(pos); },
//_ => {}
//});
//undo
//} else {
//None
//});
//});
//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));

View file

@ -1,65 +0,0 @@
use crate::*;
/// Configuration.
///
/// Contains mode, view, and bind definitions.
#[derive(Default, Debug)]
pub struct Config {
pub dirs: BaseDirectories,
pub modes: Modes,
pub views: Views,
pub binds: Binds,
}
impl Config {
const CONFIG: &'static str = "tek.edn";
const DEFAULTS: &'static str = include_str!("./tek.edn");
pub fn new (dirs: Option<BaseDirectories>) -> Self {
Self {
dirs: dirs.unwrap_or_else(||BaseDirectories::with_profile("tek", "v0")),
..Default::default()
}
}
pub fn init (&mut self) -> Usually<()> {
self.init_file(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|cfgs.load(&dsl))?;
Ok(())
}
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 (&mut self, dsl: impl Dsl) -> Usually<()> {
dsl.each(|item|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 =>
Mode::<Arc<str>>::load_into(&self.modes, &name, &body)?,
Some("keys") if let Some(name) = name =>
EventMap::<TuiEvent, Arc<str>>::load_into(&self.binds, &name, &body)?,
Some("view") if let Some(name) = name => {
self.views.write().unwrap().insert(name.into(), body.src()?.unwrap_or_default().into());
},
_ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into())
}
Ok(())
} else {
return Err(format!("Config::load: expected expr, got: {item:?}").into())
})
}
}

View file

@ -1,138 +0,0 @@
pub(crate) use tek::*;
pub(crate) use tek_device::*;
pub(crate) use tek_engine::*;
pub(crate) use tengri::{*, tui::*};
pub(crate) use clap::{self, Parser, Subcommand};
/// Application entrypoint.
pub fn main () -> Usually<()> {
Cli::parse().run()
}
/// CLI header
const HEADER: &'static str = r#"
~ ~~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ ~~~~~~ ~ ~~~
~~ ~ ~< ~ v0.3.0, 2025 sum(m)er @ the nose of the cat. ~
~~~ ~ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ ~~ ~~ ~ ~~
On first run, Tek will create configuration and state dirs:
* [x] ~/.config/tek - config
* [ ] ~/.local/share/tek - projects
* [ ] ~/.local/lib/tek - plugins
* [ ] ~/.cache/tek - cache
~"#;
#[derive(Debug, Parser)]
#[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))]
pub struct Cli {
/// Pre-defined configuration modes.
///
/// TODO: Replace these with scripted configurations.
#[command(subcommand)] mode: Option<LaunchMode>,
/// Name of JACK client
#[arg(short='n', long)] name: Option<String>,
/// Whether to attempt to become transport master
#[arg(short='S', long, default_value_t = false)] sync_lead: bool,
/// Whether to sync to external transport master
#[arg(short='s', 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='t', 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>,
}
/// Application modes
#[derive(Debug, Clone, Subcommand)]
pub enum LaunchMode {
/// Create a new session instead of loading the previous one.
New,
}
impl Cli {
fn midi_froms (&self) -> Vec<Connect> {
Connect::collect(&self.midi_from, &[] as &[&str], &self.midi_from_re)
}
fn midi_tos (&self) -> Vec<Connect> {
Connect::collect(&self.midi_to, &[] as &[&str], &self.midi_to_re)
}
pub fn run (&self) -> Usually<()> {
let name = self.name.as_ref().map_or("tek", |x|x.as_str());
let tracks = vec![];
let scenes = vec![];
let empty = &[] as &[&str];
let left_froms = Connect::collect(&self.left_from, empty, empty);
let left_tos = Connect::collect(&self.left_to, empty, empty);
let right_froms = Connect::collect(&self.right_from, empty, empty);
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 mut config = Config::new(None);
config.init()?;
Tui::new()?.run(&Jack::new_run(&name, move|jack|{
let midi_ins = {
let mut midi_ins = vec![];
for (index, connect) in self.midi_froms().iter().enumerate() {
midi_ins.push(jack.midi_in(&format!("M/{index}"), &[connect.clone()])?);
}
midi_ins
};
let midi_outs = {
let mut midi_outs = vec![];
for (index, connect) in self.midi_tos().iter().enumerate() {
midi_outs.push(jack.midi_out(&format!("{index}/M"), &[connect.clone()])?);
};
midi_outs
};
let project = Arrangement {
name: Default::default(),
color: ItemTheme::random(),
jack: jack.clone(),
clock: Clock::new(&jack, self.bpm)?,
tracks,
scenes,
selection: Selection::TrackClip { track: 0, scene: 0 },
midi_ins,
midi_outs,
..Default::default()
};
let app = App {
jack: jack.clone(),
color: ItemTheme::random(),
dialog: Dialog::welcome(),
mode: config.modes.clone().read().unwrap().get(":menu").cloned().unwrap(),
config,
project,
..Default::default()
};
jack.sync_lead(self.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(self.sync_follow)?;
Ok(app)
})?)
}
}
#[cfg(test)] #[test] fn test_cli () {
use clap::CommandFactory;
Cli::command().debug_assert();
//let jack = Jack::default();
}

View file

@ -1,39 +0,0 @@
#[allow(unused)]
pub(crate) use ::{
tek_device::{*, tek_engine::*},
tengri::{
Usually, Perhaps, Has, MaybeHas, has, maybe_has, impl_debug, from,
wrap_inc, wrap_dec,
dsl::*,
input::*,
output::*,
tui::{
*,
ratatui::{
self,
prelude::{Rect, Style, Stylize, Buffer, Modifier, buffer::Cell, Color::{self, *}},
widgets::{Widget, canvas::{Canvas, Line}},
},
crossterm::{
self,
event::{Event, KeyCode::{self, *}},
},
}
},
std::{
path::{Path, PathBuf},
sync::{Arc, RwLock},
sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed},
error::Error,
collections::BTreeMap,
fmt::Write,
cmp::Ord,
ffi::OsString,
fmt::{Debug, Formatter},
fs::File,
ops::{Add, Sub, Mul, Div, Rem},
thread::JoinHandle
},
xdg::BaseDirectories,
atomic_float::*
};

View file

@ -1,61 +0,0 @@
use super::*;
pub type Modes = Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode<Arc<str>>>>>>;
/// A set of currently active view and keys definitions,
/// with optional name and description.
#[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: Modes,
}
impl<D: Dsl + Ord> Draw<TuiOut> for Mode<D> {
fn draw (&self, _to: &mut TuiOut) {
//self.content().draw(to)
}
}
impl Mode<Arc<str>> {
pub fn load_into (modes: &Modes, name: &impl AsRef<str>, body: &impl Dsl) -> Usually<()> {
let mut mode = Self::default();
println!("Mode::load_into: {}: {body:?}", name.as_ref());
body.each(|item|mode.load_one(item))?;
modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode));
Ok(())
}
fn load_one (&mut self, dsl: impl Dsl) -> Usually<()> {
Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() {
println!("Mode::load_one: {head} {:?}", expr.tail());
let tail = expr.tail()?.map(|x|x.trim()).unwrap_or("");
match head {
"name" => self.name.push(tail.into()),
"info" => self.info.push(tail.into()),
"view" => self.view.push(tail.into()),
"keys" => tail.each(|expr|{
self.keys.push(expr.trim().into());
Ok(())
})?,
"mode" => if let Some(id) = tail.head()? {
Self::load_into(&self.modes, &id, &tail.tail())?;
} else {
return Err(format!("Mode::load_one: self: incomplete: {expr:?}").into());
},
_ => {
return Err(format!("Mode::load_one: unexpected expr: {head:?} {tail:?}").into())
},
};
} else if let Ok(Some(word)) = dsl.word() {
self.view.push(word.into());
} else {
return Err(format!("Mode::load_one: unexpected: {dsl:?}").into());
})
}
}

View file

@ -1,5 +1,11 @@
use crate::*;
#[cfg(test)] #[test] fn test_cli () {
use clap::CommandFactory;
Cli::command().debug_assert();
//let jack = Jack::default();
}
#[cfg(test)] #[test] fn test_app () -> Usually<()> {
let mut app = App::default();
let _ = app.scene_add(None, None)?;

View file

@ -1,353 +0,0 @@
use crate::*;
pub type Views = Arc<RwLock<BTreeMap<Arc<str>, Arc<str>>>>;
impl Draw<TuiOut> for App {
fn draw (&self, to: &mut TuiOut) {
for (index, dsl) in self.mode.view.iter().enumerate() {
if let Err(e) = self.view(to, dsl) {
panic!("render #{index} failed ({e}): {dsl}");
}
}
}
}
impl View<TuiOut, ()> for App {
fn view_expr <'a> (&'a self, to: &mut TuiOut, expr: &'a impl DslExpr) -> Usually<()> {
if evaluate_output_expression(self, to, expr)?
|| evaluate_output_expression_tui(self, to, expr)? {
Ok(())
} else {
Err(format!("App::view_expr: unexpected: {expr:?}").into())
}
}
fn view_word <'a> (&'a self, to: &mut TuiOut, dsl: &'a impl DslExpr) -> Usually<()> {
let mut frags = dsl.src()?.unwrap().split("/");
match frags.next() {
Some(":logo") => to.place(&view_logo()),
Some(":status") => to.place(&Fixed::Y(1, "TODO: Status Bar")),
Some(":meters") => match frags.next() {
Some("input") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::Y(Align::s("Input Meters")))),
Some("output") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::Y(Align::s("Output Meters")))),
_ => panic!()
},
Some(":tracks") => match frags.next() {
None => to.place(&"TODO tracks"),
Some("names") => to.place(&self.project.view_track_names(self.color.clone())),//Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Names")))),
Some("inputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Inputs")))),
Some("devices") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Devices")))),
Some("outputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Outputs")))),
_ => panic!()
},
Some(":scenes") => match frags.next() {
None => to.place(&"TODO scenes"),
Some(":scenes/names") => to.place(&"TODO Scene Names"),
_ => panic!()
},
Some(":editor") => to.place(&"TODO Editor"),
Some(":dialog") => match frags.next() {
Some("menu") => to.place(&if let Dialog::Menu(selected, items) = &self.dialog {
let items = items.clone();
let selected = selected;
Some(Fill::XY(Thunk::new(move|to: &mut TuiOut|{
for (index, MenuItem(item, _)) in items.0.iter().enumerate() {
to.place(&Push::Y((2 * index) as u16,
Tui::fg_bg(
if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) },
if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) },
Fixed::Y(2, Align::n(Fill::X(item)))
)));
}
})))
} else {
None
}),
_ => unimplemented!("App::view_word: {dsl:?} ({frags:?})"),
},
Some(":templates") => to.place(&{
let modes = self.config.modes.clone();
let height = (modes.read().unwrap().len() * 2) as u16;
Fixed::Y(height, Min::X(30, Thunk::new(move |to: &mut TuiOut|{
for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() {
let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) };
let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or("<no name>");
let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or("<no info>");
let fg1 = Rgb(224, 192, 128);
let fg2 = Rgb(224, 128, 32);
let field_name = Fill::X(Align::w(Tui::fg(fg1, name)));
let field_id = Fill::X(Align::e(Tui::fg(fg2, id)));
let field_info = Fill::X(Align::w(info));
to.place(&Push::Y((2 * index) as u16,
Fixed::Y(2, Fill::X(Tui::bg(bg, Bsp::s(
Bsp::a(field_name, field_id), field_info))))));
}
})))
}),
Some(":sessions") => to.place(&Fixed::Y(6, Min::X(30, Thunk::new(|to: &mut TuiOut|{
let fg = Rgb(224, 192, 128);
for (index, name) in ["session1", "session2", "session3"].iter().enumerate() {
let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) };
to.place(&Push::Y((2 * index) as u16,
&Fixed::Y(2, Fill::X(Tui::bg(bg, Align::w(Tui::fg(fg, name)))))));
}
})))),
Some(":browse/title") => to.place(&Fill::X(Align::w(FieldV(ItemColor::default(),
match self.dialog.browser_target().unwrap() {
BrowseTarget::SaveProject => "Save project:",
BrowseTarget::LoadProject => "Load project:",
BrowseTarget::ImportSample(_) => "Import sample:",
BrowseTarget::ExportSample(_) => "Export sample:",
BrowseTarget::ImportClip(_) => "Import clip:",
BrowseTarget::ExportClip(_) => "Export clip:",
}, Shrink::X(3, Fixed::Y(1, Tui::fg(Tui::g(96), RepeatH("🭻")))))))),
Some(":device") => {
let selected = self.dialog.device_kind().unwrap();
to.place(&Bsp::s(Tui::bold(true, "Add device"), Map::south(1,
move||device_kinds().iter(),
move|_label: &&'static str, i|{
let bg = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) };
let lb = if i == selected { "[ " } else { " " };
let rb = if i == selected { " ]" } else { " " };
Fill::X(Tui::bg(bg, Bsp::e(lb, Bsp::w(rb, "FIXME device name")))) })))
},
Some(":debug") => to.place(&Fixed::Y(1, format!("[{:?}]", to.area()))),
Some(_) => {
let views = self.config.views.read().unwrap();
if let Some(dsl) = views.get(dsl.src()?.unwrap()) {
let dsl = dsl.clone();
std::mem::drop(views);
self.view(to, &dsl)?
} else {
unimplemented!("{dsl:?}");
}
},
_ => unreachable!()
}
Ok(())
}
}
fn view_logo () -> impl Content<TuiOut> {
Fixed::XY(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), col!{
Fixed::Y(1, ""),
Fixed::Y(1, ""),
Fixed::Y(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"),
Fixed::Y(1, Bsp::e("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", Bsp::e(Tui::fg(Rgb(230,100,40), "v0.3.0"), " ~~"))),
Fixed::Y(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"),
})))
}
//pub fn view_nil (_: &App) -> TuiCb {
//|to|to.place(&Fill::XY("·"))
//}
//Bsp::s("",
//Map::south(1,
//move||app.config.binds.layers.iter()
//.filter_map(|a|(a.0)(app).then_some(a.1))
//.flat_map(|a|a)
//.filter_map(|x|if let Value::Exp(_, iter)=x.value{ Some(iter) } else { None })
//.skip(offset)
//.take(20),
//|mut b,i|Fixed::X(60, Align::w(Bsp::e("(", Bsp::e(
//b.next().map(|t|Fixed::X(16, Align::w(Tui::fg(Rgb(64,224,0), format!("{}", t.value))))),
//Bsp::e(" ", Align::w(format!("{}", b.0.0.trim()))))))))))),
//Dialog::Browse(BrowseTarget::Load, browser) => {
//"bobcat".boxed()
////Bsp::s(
////Fill::X(Align::w(Margin::XY(1, 1, Bsp::e(
////Tui::bold(true, " Load project: "),
////Shrink::X(3, Fixed::Y(1, RepeatH("🭻"))))))),
////Outer(true, Style::default().fg(Tui::g(96)))
////.enclose(Fill::XY(browser)))
//},
//Dialog::Browse(BrowseTarget::Export, browser) => {
//"bobcat".boxed()
////Bsp::s(
////Fill::X(Align::w(Margin::XY(1, 1, Bsp::e(
////Tui::bold(true, " Export: "),
////Shrink::X(3, Fixed::Y(1, RepeatH("🭻"))))))),
////Outer(true, Style::default().fg(Tui::g(96)))
////.enclose(Fill::XY(browser)))
//},
//Dialog::Browse(BrowseTarget::Import, browser) => {
//"bobcat".boxed()
////Bsp::s(
////Fill::X(Align::w(Margin::XY(1, 1, Bsp::e(
////Tui::bold(true, " Import: "),
////Shrink::X(3, Fixed::Y(1, RepeatH("🭻"))))))),
////Outer(true, Style::default().fg(Tui::g(96)))
////.enclose(Fill::XY(browser)))
//},
//
//pub fn view_history (&self) -> impl Content<TuiOut> {
//Fixed::Y(1, Fill::X(Align::w(FieldH(self.color,
//format!("History ({})", self.history.len()),
//self.history.last().map(|last|Fill::X(Align::w(format!("{:?}", last.0))))))))
//}
//pub fn view_status_h2 (&self) -> impl Content<TuiOut> {
//self.update_clock();
//let theme = self.color;
//let clock = self.clock();
//let playing = clock.is_rolling();
//let cache = clock.view_cache.clone();
////let selection = self.selection().describe(self.tracks(), self.scenes());
//let hist_len = self.history.len();
//let hist_last = self.history.last();
//Fixed::Y(2, Stack::east(move|add: &mut dyn FnMut(&dyn Draw<TuiOut>)|{
//add(&Fixed::X(5, Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) },
//Either::new(false, // TODO
//Thunk::new(move||Fixed::X(9, Either::new(playing,
//Tui::fg(Rgb(0, 255, 0), " PLAYING "),
//Tui::fg(Rgb(255, 128, 0), " STOPPED ")))
//),
//Thunk::new(move||Fixed::X(5, Either::new(playing,
//Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)),
//Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",))))
//)
//)
//)));
//add(&" ");
//{
//let cache = cache.read().unwrap();
//add(&Fixed::X(15, Align::w(Bsp::s(
//FieldH(theme, "Beat", cache.beat.view.clone()),
//FieldH(theme, "Time", cache.time.view.clone()),
//))));
//add(&Fixed::X(13, Align::w(Bsp::s(
//Fill::X(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))),
//Fill::X(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))),
//))));
//add(&Fixed::X(12, Align::w(Bsp::s(
//Fill::X(Align::w(FieldH(theme, "Buf", cache.buf.view.clone()))),
//Fill::X(Align::w(FieldH(theme, "Lat", cache.lat.view.clone()))),
//))));
////add(&Bsp::s(
//////Fill::X(Align::w(FieldH(theme, "Selected", Align::w(selection)))),
////Fill::X(Align::w(FieldH(theme, format!("History ({})", hist_len),
////hist_last.map(|last|Fill::X(Align::w(format!("{:?}", last.0))))))),
////""
////));
//////if let Some(last) = self.history.last() {
//////add(&FieldV(theme, format!("History ({})", self.history.len()),
//////Fill::X(Align::w(format!("{:?}", last.0)))));
//////}
//}
//}))
//}
//pub fn view_status_v (&self) -> impl Content<TuiOut> + use<'_> {
//self.update_clock();
//let cache = self.project.clock.view_cache.read().unwrap();
//let theme = self.color;
//let playing = self.clock().is_rolling();
//Tui::bg(theme.darker.rgb, Fixed::XY(20, 5, Outer(true, Style::default().fg(Tui::g(96))).enclose(
//col!(
//Fill::X(Align::w(Bsp::e(
//Align::w(Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) },
//Either::new(false, // TODO
//Thunk::new(move||Fixed::X(9, Either::new(playing,
//Tui::fg(Rgb(0, 255, 0), " PLAYING "),
//Tui::fg(Rgb(255, 128, 0), " STOPPED ")))
//),
//Thunk::new(move||Fixed::X(5, Either::new(playing,
//Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)),
//Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",))))
//)
//)
//)),
//Bsp::s(
//FieldH(theme, "Beat", cache.beat.view.clone()),
//FieldH(theme, "Time", cache.time.view.clone()),
//),
//))),
//Fill::X(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))),
//Fill::X(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))),
//Fill::X(Align::w(FieldH(theme, "Buf", Bsp::e(cache.buf.view.clone(), Bsp::e(" = ", cache.lat.view.clone()))))),
//))))
//}
//pub fn view_status (&self) -> impl Content<TuiOut> + use<'_> {
//self.update_clock();
//let cache = self.project.clock.view_cache.read().unwrap();
//view_status(Some(self.project.selection.describe(self.tracks(), self.scenes())),
//cache.sr.view.clone(), cache.buf.view.clone(), cache.lat.view.clone())
//}
//pub fn view_transport (&self) -> impl Content<TuiOut> + use<'_> {
//self.update_clock();
//let cache = self.project.clock.view_cache.read().unwrap();
//view_transport(self.project.clock.is_rolling(),
//cache.bpm.view.clone(), cache.beat.view.clone(), cache.time.view.clone())
//}
//pub fn view_editor (&self) -> impl Content<TuiOut> + use<'_> {
//let bg = self.editor()
//.and_then(|editor|editor.clip().clone())
//.map(|clip|clip.read().unwrap().color.darker)
//.unwrap_or(self.color.darker);
//Fill::XY(Tui::bg(bg.rgb, self.editor()))
//}
//pub fn view_editor_status (&self) -> impl Content<TuiOut> + use<'_> {
//self.editor().map(|e|Fixed::X(20, Outer(true, Style::default().fg(Tui::g(96))).enclose(
//Fill::Y(Align::n(Bsp::s(e.clip_status(), e.edit_status()))))))
//}
//pub fn view_midi_ins_status (&self) -> impl Content<TuiOut> + use<'_> {
//self.project.view_midi_ins_status(self.color)
//}
//pub fn view_midi_outs_status (&self) -> impl Content<TuiOut> + use<'_> {
//self.project.view_midi_outs_status(self.color)
//}
//pub fn view_audio_ins_status (&self) -> impl Content<TuiOut> + use<'_> {
//self.project.view_audio_ins_status(self.color)
//}
//pub fn view_audio_outs_status (&self) -> impl Content<TuiOut> + use<'_> {
//self.project.view_audio_outs_status(self.color)
//}
//pub fn view_scenes (&self) -> impl Content<TuiOut> + use<'_> {
//Bsp::e(
//Fixed::X(20, Align::nw(self.project.view_scenes_names())),
//self.project.view_scenes_clips(),
//)
//}
//pub fn view_scenes_names (&self) -> impl Content<TuiOut> + use<'_> {
//self.project.view_scenes_names()
//}
//pub fn view_scenes_clips (&self) -> impl Content<TuiOut> + use<'_> {
//self.project.view_scenes_clips()
//}
//pub fn view_tracks_inputs <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
//Fixed::Y(1 + self.project.midi_ins.len() as u16,
//self.project.view_inputs(self.color))
//}
//pub fn view_tracks_outputs <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
//self.project.view_outputs(self.color)
//}
//pub fn view_tracks_devices <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
//Fixed::Y(4, self.project.view_track_devices(self.color))
//}
//pub fn view_tracks_names <'a> (&'a self) -> impl Content<TuiOut> + use<'a> {
//Fixed::Y(2, self.project.view_track_names(self.color))
//}
//pub fn view_pool (&self) -> impl Content<TuiOut> + use<'_> {
//Fixed::X(20, Bsp::s(
//Fill::X(Align::w(FieldH(self.color, "Clip pool:", ""))),
//Fill::Y(Align::n(Tui::bg(Rgb(0, 0, 0), Outer(true, Style::default().fg(Tui::g(96)))
//.enclose(PoolView(&self.pool)))))))
//}
//pub fn view_samples_keys (&self) -> impl Content<TuiOut> + use<'_> {
//self.project.sampler().map(|s|s.view_list(true, self.editor().unwrap()))
//}
//pub fn view_samples_grid (&self) -> impl Content<TuiOut> + use<'_> {
//self.project.sampler().map(|s|s.view_grid())
//}
//pub fn view_sample_viewer (&self) -> impl Content<TuiOut> + use<'_> {
//self.project.sampler().map(|s|s.view_sample(self.editor().unwrap().get_note_pos()))
//}
//pub fn view_sample_info (&self) -> impl Content<TuiOut> + use<'_> {
//self.project.sampler().map(|s|s.view_sample_info(self.editor().unwrap().get_note_pos()))
//}
//pub fn view_sample_status (&self) -> impl Content<TuiOut> + use<'_> {
//self.project.sampler().map(|s|Outer(true, Style::default().fg(Tui::g(96))).enclose(
//Fill::Y(Align::n(s.view_sample_status(self.editor().unwrap().get_note_pos())))))
//}
////let options = ||["Projects", "Settings", "Help", "Quit"].iter();
////let option = |a,i|Tui::fg(Rgb(255,255,255), format!("{}", a));
////Bsp::s(Tui::bold(true, "tek!"), Bsp::s("", Map::south(1, options, option)))