restruct: 92e

This commit is contained in:
i do not exist 2026-06-23 21:39:12 +03:00
parent 4ec2165e3d
commit ae347eeef7
31 changed files with 2924 additions and 2663 deletions

130
src/app/bind.rs Normal file
View file

@ -0,0 +1,130 @@
use crate::*;
/// A control axis.
///
/// ```
/// let axis = tek::ControlAxis::X;
/// ```
#[derive(Debug, Copy, Clone)] pub enum ControlAxis {
X, Y, Z, I
}
/// Collection of input bindings.
pub type Binds = Arc<RwLock<BTreeMap<Arc<str>, Bind<TuiEvent, Arc<str>>>>>;
pub(crate) fn load_bind (binds: &Binds, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
binds.write().unwrap().insert(name.as_ref().into(), Bind::load(body)?);
Ok(())
}
/// 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>>
);
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::named(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)
}
}
/// Default is always empty map regardless if `E` and `C` implement [Default].
impl<E, C> Default for Bind<E, C> {
fn default () -> Self { Self(Default::default()) }
}
impl<C: Default> Default for Binding<C> {
fn default () -> Self {
Self {
commands: Default::default(),
condition: Default::default(),
description: Default::default(),
source: Default::default(),
}
}
}
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_debug!(Condition |self, w| { write!(w, "*") });

260
src/app/cli.rs Normal file
View file

@ -0,0 +1,260 @@
use crate::*;
/// 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 creat
#[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,
}
/// Command-line configuration.
#[cfg(feature = "cli")]
impl Cli {
pub fn run (&self) -> Usually<()> {
self.action.run(&Config::init_new(None)?)
}
}
#[cfg(feature = "cli")]
impl Action {
fn run (&self, config: &Config) -> Usually<()> {
use Action::*;
match self {
Version => tek_show_version(),
Config => tek_print_config(&config),
List => todo!("list sessions"),
Resume => todo!("resume session"),
New {
name, bpm, tracks, scenes, sync_lead, sync_follow,
midi_from: mf, midi_from_re: mfr, midi_to: mt, midi_to_re: mtr,
left_from: lf, right_from: rf, left_to: lt, right_to: rt, ..
} => {
let name = name.as_ref().map_or("tek", |x|x.as_str());
let jack = Jack::new(&name)?;
let proj = tek_project_new(
&jack, Clock::new(&jack, *bpm)?, &lf, &lt, &rf, &rt, &mf, &mt, &mfr, &mtr
)?;
proj.tracks_add(tracks.unwrap_or(0), None, &[], &[])?;
proj.scenes_add(scenes.unwrap_or(0))?;
//if matches!(self, Action::Status) {
//// Show status and exit
//tek_print_status(&proj);
//return Ok(())
//}
// Initialize the app state
let app = tek(&jack, proj, config, ":menu");
//if matches!(self, 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(())
}
}
pub fn tek_project_new (
jack: &Jack,
clock: Clock,
left_from: &[impl AsRef<str>],
left_to: &[impl AsRef<str>],
right_from: &[impl AsRef<str>],
right_to: &[impl AsRef<str>],
midi_from: &[impl AsRef<str>],
midi_to: &[impl AsRef<str>],
midi_from_re: &[impl AsRef<str>],
midi_to_re: &[impl AsRef<str>],
) -> Usually<Arrangement> {
// 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 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<_>, _>>()?
);
Ok(project)
}
pub fn tek_show_version () {
println!("todo version");
}
pub fn tek_print_config (config: &Config) {
use ::ansi_term::Color::*;
println!("{:?}", config.dirs);
for (k, v) in config.views.read().unwrap().iter() {
println!("{} {} {v}", Green.paint("VIEW"), Green.bold().paint(format!("{k:<16}")));
}
for (k, v) in config.binds.read().unwrap().iter() {
println!("{} {}", Green.paint("BIND"), Green.bold().paint(format!("{k:<16}")));
for (k, v) in v.0.iter() {
print!("{} ", &Yellow.paint(match &k.0 {
Event::Key(KeyEvent { modifiers, .. }) =>
format!("{:>16}", format!("{modifiers}")),
_ => unimplemented!()
}));
print!("{}", &Yellow.bold().paint(match &k.0 {
Event::Key(KeyEvent { code, .. }) =>
format!("{:<10}", format!("{code}")),
_ => unimplemented!()
}));
for v in v.iter() {
print!(" => {:?}", v.commands);
print!(" {}", v.condition.as_ref().map(|x|format!("{x:?}")).unwrap_or_default());
println!(" {}", v.description.as_ref().map(|x|x.as_ref()).unwrap_or_default());
//println!(" {:?}", v.source);
}
}
}
for (k, v) in config.modes.read().unwrap().iter() {
println!();
for v in v.name.iter() { print!("{}", Green.bold().paint(format!("{v} "))); }
for v in v.info.iter() { print!("\n{}", Green.paint(format!("{v}"))); }
print!("\n{} {}", Blue.paint("TOOL"), Green.bold().paint(format!("{k:<16}")));
print!("\n{}", Blue.paint("KEYS"));
for v in v.keys.iter() { print!("{}", Green.paint(format!(" {v}"))); }
println!();
for (k, v) in v.modes.read().unwrap().iter() {
print!("{} {} {:?}",
Blue.paint("MODE"),
Green.bold().paint(format!("{k:<16}")),
v.name);
print!(" INFO={:?}",
v.info);
print!(" VIEW={:?}",
v.view);
println!(" KEYS={:?}",
v.keys);
}
print!("{}", Blue.paint("VIEW"));
for v in v.view.iter() { print!("{}", Green.paint(format!(" {v}"))); }
println!();
}
}
pub fn tek_print_status (project: &Arrangement) {
println!("Name: {:?}", &project.name);
println!("JACK: {:?}", &project.jack);
println!("Buffer: {:?}", &project.clock.chunk);
println!("Sample rate: {:?}", &project.clock.timebase.sr);
println!("MIDI PPQ: {:?}", &project.clock.timebase.ppq);
println!("Tempo: {:?}", &project.clock.timebase.bpm);
println!("Quantize: {:?}", &project.clock.quant);
println!("Launch: {:?}", &project.clock.sync);
println!("Playhead: {:?}us", &project.clock.playhead.usec);
println!("Playhead: {:?}s", &project.clock.playhead.sample);
println!("Playhead: {:?}p", &project.clock.playhead.pulse);
println!("Started: {:?}", &project.clock.started);
println!("Tracks:");
for (i, t) in project.tracks.iter().enumerate() {
println!(" Track {i}: {} {} {:?} {:?}", t.name, t.width,
&t.sequencer.play_clip, &t.sequencer.next_clip);
}
println!("Scenes:");
for (i, t) in project.scenes.iter().enumerate() {
println!(" Scene {i}: {} {:?}", &t.name, &t.clips);
}
println!("MIDI Ins: {:?}", &project.midi_ins);
println!("MIDI Outs: {:?}", &project.midi_outs);
println!("Audio Ins: {:?}", &project.audio_ins);
println!("Audio Outs: {:?}", &project.audio_outs);
// TODO git integration
// TODO dawvert integration
}

192
src/app/config.rs Normal file
View file

@ -0,0 +1,192 @@
use crate::{*, bind::*, view::*};
/// 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,
}
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");
pub fn init_new (dirs: Option<BaseDirectories>) -> Usually<Self> {
let mut config = Self::new(None);
config.init()?;
Ok(config)
}
/// 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()
}
}
pub(crate) fn load_mode (modes: &Modes, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
let mut mode = Mode::default();
body.each(|item|mode.add(item))?;
modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode));
Ok(())
}
/// Collection of interaction modes.
pub type Modes = Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode<Arc<str>>>>>>;
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());
}))
}
}
/// 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,
}

590
src/app/view.rs Normal file
View file

@ -0,0 +1,590 @@
use crate::*;
/// Collection of custom view definitions.
pub type Views = Arc<RwLock<BTreeMap<Arc<str>, Arc<str>>>>;
/// Load custom view definition.
pub(crate) fn load_view (views: &Views, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
views.write().unwrap().insert(name.as_ref().into(), body.src()?.unwrap_or_default().into());
Ok(())
}
/// ```
/// let _ = tek::view_logo();
/// ```
pub fn view_logo () -> impl Draw<Tui> {
wh_exact(Some(32), Some(7), bold(true, fg(Rgb(240, 200, 180), south!{
h_exact(1, ""),
h_exact(1, ""),
h_exact(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"),
h_exact(1, east("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", east(fg(Rgb(230,100,40), "v0.3.0"), " ~~"))),
h_exact(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"),
})))
}
/// ```
/// let x = std::sync::Arc::<std::sync::RwLock<String>>::default();
/// let _ = tek::view_transport(true, x.clone(), x.clone(), x.clone());
/// let _ = tek::view_transport(false, x.clone(), x.clone(), x.clone());
/// ```
pub fn view_transport (play: bool, bpm: &str, beat: &str, time: &str) -> impl Draw<Tui> {
let theme = ItemTheme::G[96];
bg(Black, east!(above(
wh_full(origin_w(button_play_pause(play))),
wh_full(origin_e(east!(
field_h(theme, "BPM", bpm),
field_h(theme, "Beat", beat),
field_h(theme, "Time", time),
)))
)))
}
/// ```
/// let x = std::sync::Arc::<std::sync::RwLock<String>>::default();
/// let _ = tek::view_status(None, x.clone(), x.clone(), x.clone());
/// let _ = tek::view_status(Some("".into()), x.clone(), x.clone(), x.clone());
/// ```
pub fn view_status (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl Draw<Tui> {
let theme = ItemTheme::G[96];
let sr = field_h(theme, "SR", sr);
let buf = field_h(theme, "Buf", buf);
let lat = field_h(theme, "Lat", lat);
bg(Black, east!(above(
wh_full(origin_w(sel.map(|sel|field_h(theme, "Selected", sel)))),
wh_full(origin_e(east!(sr, buf, lat))),
)))
}
/// ```
/// let _ = tek::button_play_pause(true);
/// ```
pub fn button_play_pause (playing: bool) -> impl Draw<Tui> {
let compact = true;//self.is_editing();
bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) },
either(compact,
thunk(move|to: &mut Tui|w_exact(9, either(playing,
fg(Rgb(0, 255, 0), " PLAYING "),
fg(Rgb(255, 128, 0), " STOPPED "))
).draw(to)),
thunk(move|to: &mut Tui|w_exact(5, either(playing,
fg(Rgb(0, 255, 0), south(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)),
fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",)))
).draw(to)),
)
)
}
#[cfg(feature = "track")] pub fn view_track_row_section (
_theme: ItemTheme,
button: impl Draw<Tui>,
button_add: impl Draw<Tui>,
content: impl Draw<Tui>,
) -> impl Draw<Tui> {
west(h_full(w_exact(4, origin_nw(button_add))),
east(w_exact(20, h_full(origin_nw(button))), wh_full(origin_c(content))))
}
/// ```
/// let bg = tengri::ratatui::style::Color::Red;
/// let fg = tengri::ratatui::style::Color::Green;
/// let _ = tek::view_wrap(bg, fg, "and then blue, too!");
/// ```
pub fn view_wrap (bg: Color, fg: Color, content: impl Draw<Tui>) -> impl Draw<Tui> {
let left = fg_bg(bg, Reset, w_exact(1, y_repeat("")));
let right = fg_bg(bg, Reset, w_exact(1, y_repeat("")));
east(left, west(right, fg_bg(fg, bg, content)))
}
/// ```
/// let _ = tek::view_meter("", 0.0);
/// let _ = tek::view_meters(&[0.0, 0.0]);
/// ```
pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Draw<Tui> + 'a {
let f = field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value));
let w = if value >= 0.0 { 13 }
else if value >= -1.0 { 12 }
else if value >= -2.0 { 11 }
else if value >= -3.0 { 10 }
else if value >= -4.0 { 9 }
else if value >= -6.0 { 8 }
else if value >= -9.0 { 7 }
else if value >= -12.0 { 6 }
else if value >= -15.0 { 5 }
else if value >= -20.0 { 4 }
else if value >= -25.0 { 3 }
else if value >= -30.0 { 2 }
else if value >= -40.0 { 1 }
else { 0 };
let c = if value >= 0.0 { Red }
else if value >= -3.0 { Yellow }
else { Green };
south!(f, wh_exact(Some(w), Some(1), bg(c, ())))
}
pub fn view_meters (values: &[f32;2]) -> impl Draw<Tui> + use<'_> {
let left = format!("L/{:>+9.3}", values[0]);
let right = format!("R/{:>+9.3}", values[1]);
south(left, right)
}
pub fn view_sample_info (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
when(sample.is_some(), thunk(move|to: &mut Tui|{
let sample = sample.unwrap().read().unwrap();
let theme = sample.color;
east!(
field_h(theme, "Name", format!("{:<10}", sample.name.clone())),
field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())),
field_h(theme, "Start", format!("{:<8}", sample.start)),
field_h(theme, "End", format!("{:<8}", sample.end)),
field_h(theme, "Trans", "0"),
field_h(theme, "Gain", format!("{}", sample.gain)),
).draw(to)
}))
}
pub fn view_sample_info_v (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
let a = thunk(move|to: &mut Tui|{
let sample = sample.unwrap().read().unwrap();
let theme = sample.color;
w_exact(20, south!(
w_full(origin_w(field_h(theme, "Name ", format!("{:<10}", sample.name.clone())))),
w_full(origin_w(field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())))),
w_full(origin_w(field_h(theme, "Start ", format!("{:<8}", sample.start)))),
w_full(origin_w(field_h(theme, "End ", format!("{:<8}", sample.end)))),
w_full(origin_w(field_h(theme, "Trans ", "0"))),
w_full(origin_w(field_h(theme, "Gain ", format!("{}", sample.gain)))),
)).draw(to)
});
let b = thunk(|to: &mut Tui|fg(Red, south!(
bold(true, "× No sample."),
"[r] record",
"[Shift-F9] import",
)).draw(to));
either(sample.is_some(), a, b)
}
pub fn view_sample_status (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> {
bold(true, fg(g(224), sample
.map(|sample|{
let sample = sample.read().unwrap();
format!("Sample {}-{}", sample.start, sample.end)
})
.unwrap_or_else(||"No sample".to_string())))
}
pub fn view_track_header (theme: ItemTheme, content: impl Draw<Tui>) -> impl Draw<Tui> {
w_exact(12, bg(theme.darker.term, w_full(origin_e(content))))
}
pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T])
-> impl Draw<Tui> + use<'a, T>
{
let ins = ports.len() as u16;
let frame = Outer(true, Style::default().fg(g(96)));
let iter = move||ports.iter();
let names = iter_south(iter, move|port, index|h_full(origin_w(format!(" {index} {}", port.port_name()))));
let field = field_v(theme, title, names);
wh_exact(Some(20), Some(1 + ins), border(true, frame, wh_exact(Some(20), Some(1 + ins), field)))
}
pub fn view_io_ports <'a, T: PortsSizes<'a>> (
fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a
) -> impl Draw<Tui> + 'a {
type Item<'a> = (usize, &'a Arc<str>, &'a [Connect], usize, usize);
iter(items, move|(_index, name, connections, y, y2): Item<'a>, _| {
y_push(y as u16, h_exact((y2-y) as u16,
south(h_full(bold(true, fg_bg(fg, bg, origin_w(east(" 󰣲 ", name))))),
iter(||connections.iter(), move|connect: &'a Connect, index|y_push(
index as u16, h_exact(1, origin_w(bold(false, fg_bg(fg, bg, &connect.info))))
)))))
})
}
pub fn view_scenes_clips (
scenes: impl ScenesSizes<'_>,
tracks: impl TracksSizes<'_>,
select: &Selection,
editor: Option<&MidiEditor>,
size: &Size,
is_editing: bool,
) -> impl Draw<Tui> {
size.of(wh_full(above(wh_full(origin_se(fg(Green, format!("{}x{}", size.w(), size.h())))),
thunk(|to: &mut Tui|{
for (index, track, _, _) in tracks {
let clips = view_track_clips(scenes, select, editor, index, track, is_editing);
w_exact(track.width as u16, h_full(clips)).draw(to);
}
Ok(XYWH(0, 0, 0, 0))
}))))
}
pub fn view_track_clips (
scenes: impl ScenesSizes<'_>,
select: &Selection,
editor: &Option<MidiEditor>,
index: usize,
track: &Track,
is_editing: bool,
) -> impl Draw<Tui> {
thunk(move|to: &mut Tui|{
for (scene_index, scene, ..) in scenes {
let (
name, theme
): (Arc<str>, ItemTheme) = if let Some(Some(clip)) = &scene.clips.get(index) {
let clip = clip.read().unwrap();
(format!("{}", &clip.name).into(), clip.color)
} else {
(" ⏹ -- ".into(), ItemTheme::G[32])
};
let fg = theme.lightest.term;
let mut outline = theme.base.term;
let bg = if select.track() == Some(index) && select.scene() == Some(scene_index) {
outline = theme.lighter.term;
theme.light.term
} else if select.track() == Some(index) || select.scene() == Some(scene_index) {
outline = theme.darkest.term;
theme.base.term
} else {
theme.dark.term
};
let w = if select.track() == Some(index) && let Some(editor) = editor {
(editor.size.w() as usize).max(24).max(track.width)
} else {
track.width
} as u16;
let y = if select.scene() == Some(scene_index) && let Some(editor) = editor {
(editor.size.h() as usize).max(12)
} else {
Self::H_SCENE as usize
} as u16;
let is_selected = is_editing && select.track() == Some(index) && select.scene() == Some(scene_index);
wh_exact(Some(w), Some(y), below(
wh_full(Outer(true, Style::default().fg(outline))),
wh_full(below(
below(
fg_bg(outline, bg, wh_full("")),
wh_full(origin_nw(fg_bg(fg, bg, bold(true, &name)))),
),
wh_full(when(is_selected, editor.map(|e|e.view()))))))
).draw(to);
}
Ok(XYWH(0, 0, 0, 0))
})
}
pub fn view_track_names (
theme: ItemTheme,
tracks: impl TracksSizes<'_>,
track_count: usize,
scene_count: usize,
selected: &Selection,
) -> impl Draw<Tui> {
let button = south(
button_3("t", "rack ", format!("{}{track_count}", selected.track()
.map(|track|format!("{track}/")).unwrap_or_default()), false),
button_3("s", "cene ", format!("{}{scene_count}", selected.scene()
.map(|scene|format!("{scene}/")).unwrap_or_default()), false));
let button_2 = south(
button_2("T", "+", false),
button_2("S", "+", false));
view_track_row_section(theme, button, button_2, bg(theme.darker.term,
h_exact(2, thunk(|to: &mut Tui|{
for (index, track, x1, _x2) in tracks {
x_push(x1 as u16, w_exact(track_width(index, track),
bg(if selected.track() == Some(index) {
track.color.light.term
} else {
track.color.base.term
}, south(w_full(origin_nw(east(
format!("·t{index:02} "),
fg(Rgb(255, 255, 255), bold(true, &track.name))
))), ""))) ).draw(to);
}
Ok(XYWH(0, 0, 0, 0))
}))))
}
pub fn view_track_outputs (
theme: ItemTheme,
tracks: impl TracksSizes<'_>,
midi_outs: impl Iterator<Item = ()>,
) -> impl Draw<Tui> {
view_track_row_section(theme,
south(w_full(origin_w(button_2("o", "utput", false))),
thunk(|to: &mut Tui|{
for port in midi_outs {
w_full(origin_w(port.port_name())).draw(to);
}
Ok(XYWH(0, 0, 0, 0))
})),
button_2("O", "+", false),
bg(theme.darker.term, origin_w(thunk(|to: &mut Tui|{
for (index, track, _x1, _x2) in tracks {
let iter = ||track.sequencer.midi_outs.iter();
let draw = |port: &MidiOutput, _|fg(Rgb(255, 255, 255),
h_exact(1, bg(track.color.dark.term, w_full(origin_w(
format!("·o{index:02} {}", port.port_name()))))));
w_exact(track_width(index, track),
origin_nw(h_full(iter_south(iter, draw)))).draw(to);
}
Ok(XYWH(0, 0, 0, 0))
}))))
}
pub fn view_track_inputs (
theme: ItemTheme,
tracks: impl TracksSizes<'_>,
h: u16,
) -> impl Draw<Tui> {
view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false),
bg(theme.darker.term, origin_w(thunk(move|to: &mut Tui|{
for (index, track, _x1, _x2) in tracks {
wh_exact(Some(track_width(index, track)), Some(h + 1),
origin_nw(south(
bg(track.color.base.term,
w_full(origin_w(east!(
either(track.sequencer.monitoring, fg(Green, "●mon "), "·mon "),
either(track.sequencer.recording, fg(Red, "●rec "), "·rec "),
either(track.sequencer.overdub, fg(Yellow, "●dub "), "·dub "),
)))),
iter_south(||track.sequencer.midi_ins.iter(),
|port, _|fg_bg(Rgb(255, 255, 255), track.color.dark.term,
w_full(origin_w(format!("·i{index:02} {}", port.port_name()))))))))
.draw(to);
}
Ok(XYWH(0, 0, 0, 0))
}))))
}
pub fn view_scenes_names (
scenes: &impl ScenesSizes<'_>
) -> impl Draw<Tui> {
w_exact(20, thunk(|to: &mut Tui|{
for (index, scene, ..) in scenes {
view_scene_name(index, scene).draw(to);
}
Ok(XYWH(1, 1, 1, 1))
}))
}
pub fn view_scene_name (
select: Option<&Selection>,
editor: Option<&MidiEditor>,
index: usize,
scene: &Scene,
editing: bool
) -> impl Draw<Tui> {
let h = if select.scene() == Some(index) && let Some(_editor) = editor {
7
} else {
Self::H_SCENE as u16
};
let a = w_full(origin_w(east(format!("·s{index:02} "),
fg(g(255), bold(true, &scene.name)))));
let b = when(select.scene() == Some(index) && editing,
wh_full(origin_nw(south(
editor.as_ref().map(|e|e.clip_status()),
editor.as_ref().map(|e|e.edit_status())))));
let c = if select.scene() == Some(index) {
scene.color.light.term
} else {
scene.color.base.term
};
wh_exact(Some(20), Some(h), bg(c, origin_nw(south(a, b))))
}
pub fn view_midi_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
track.map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins))
}
pub fn view_midi_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
track.map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs))
}
pub fn view_audio_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
track.map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins()))
}
pub fn view_audio_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
track.map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs()))
}
pub fn view_track_per (
tracks: impl TracksSizes<'_>
) -> impl Draw<Tui> {
iter_east(tracks, move|(index, track, x1, x2): (usize, &Track, usize, usize), _|{
w_exact((x2 - x1) as u16, fg_bg(
track.color.lightest.term,
track.color.base.term,
callback(index, track)))
})
}
pub fn view_per_track () -> impl Draw<Tui> {}
pub fn view_per_track_top () -> impl Draw<Tui> {}
pub fn view_inputs (
tracks: impl TracksSizes<'_>,
midi_ins: &[MidiIn],
) -> impl Draw<Tui> {
let header = h_exact(1, view_inputs_header(tracks));
south(header, thunk(|to: &mut Tui|{
for (index, port) in midi_ins.iter().enumerate() {
x_push(index as u16 * 10, h_exact(1, view_inputs_row(tracks, port))).draw(to);
}
todo!()
}))
}
pub fn view_inputs_header (
tracks: impl TracksSizes<'_>,
midi_ins: &[MidiIn],
) -> impl Draw<Tui> {
east(w_exact(20, origin_w(button_3("i", "nput ", format!("{}", midi_ins.len()), false))),
west(w_exact(4, button_2("I", "+", false)), thunk(move|to: &mut Tui|{
for (_index, track, x1, _x2) in tracks {
#[cfg(feature = "track")]
x_push(x1 as u16, bg(track.color.dark.term, origin_w(w_exact(track.width as u16, east!(
either(track.sequencer.monitoring, fg(Green, "mon "), "mon "),
either(track.sequencer.recording, fg(Red, "rec "), "rec "),
either(track.sequencer.overdub, fg(Yellow, "dub "), "dub "),
))))).draw(to);
}
todo!()
})))
}
pub fn view_inputs_row (
tracks: impl TracksSizes<'_>,
port: ()
) -> impl Draw<Tui> {
east(w_exact(20, origin_w(east("", bold(true, fg(Rgb(255,255,255), port.port_name()))))),
west(w_exact(4, ()), thunk(move|to: &mut Tui|{
for (_index, track, _x1, _x2) in tracks {
#[cfg(feature = "track")]
bg(track.color.darker.term, origin_w(w_exact(track.width as u16, east!(
either(track.sequencer.monitoring, fg(Green, ""), " · "),
either(track.sequencer.recording, fg(Red, ""), " · "),
either(track.sequencer.overdub, fg(Yellow, ""), " · "),
)))).draw(to);
}
todo!()
})))
}
pub fn view_outputs (
theme: ItemTheme,
tracks: impl TracksSizes<'_>,
midi_outs: &[MidiOutput],
height: u16,
) -> impl Draw<Tui> {
let list = south(
h_exact(1, w_full(origin_w(button_3(
"o", "utput", format!("{}", midi_outs.len()), false
)))),
h_exact(height - 1, wh_full(origin_nw(thunk(|to: &mut Tui|{
for (_index, port) in midi_outs.iter().enumerate() {
h_exact(1,w_full(east(
origin_w(east("", fg(Rgb(255,255,255), bold(true, port.port_name())))),
w_full(origin_e(format!("{}/{} ",
port.port().get_connections().len(),
port.connections.len())))))).draw(to);
for (index, conn) in port.connections.iter().enumerate() {
h_exact(1, w_full(origin_w(format!(" c{index:02}{}", conn.info()))))
.draw(to);
}
}
todo!();
}))))
);
h_exact(height, view_track_row_section(theme, list, button_2("O", "+", false),
bg(theme.darker.term, origin_w(w_full(
thunk(|to: &mut Tui|{
for (index, track, _x1, _x2) in tracks {
w_exact(track_width(index, track),
thunk(|to: &mut Tui|{
h_exact(1, origin_w(east(
either(true, fg(Green, "play "), "play "),
either(false, fg(Yellow, "solo "), "solo "),
))).draw(to);
for (_index, port) in midi_outs.iter().enumerate() {
h_exact(1, origin_w(east(
either(true, fg(Green, ""), " · "),
either(false, fg(Yellow, ""), " · "),
))).draw(to);
for (_index, _conn) in port.connections.iter().enumerate() {
h_exact(1, w_full("")).draw(to);
}
}
todo!()
})
).draw(to);
}
todo!()
})
)))
))
}
pub fn view_track_devices (
theme: ItemTheme,
tracks: &impl TracksSizes<'_>,
track: Option<&Track>,
h: u16,
) -> impl Draw<Tui> {
view_track_row_section(theme,
button_3("d", "evice", format!("{}", track.map(|t|t.devices.len()).unwrap_or(0)), false),
button_2("D", "+", false),
iter(tracks, move|(_, track, _x1, _x2), index| wh_exact(
Some(track_width(index, track)),
Some(h + 1),
bg(track.color.dark.term, origin_nw(iter_south(move||0..h,
|_, _index|wh_exact(Some(track.width as u16), Some(2),
fg_bg(
ItemTheme::G[32].lightest.term,
ItemTheme::G[32].dark.term,
origin_nw(format!(" · {}", "--"))
)
)))))))
}
pub fn per_track <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
tracks: impl Fn() -> U + Send + Sync + 'a,
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
) -> impl Draw<Tui> + 'a {
per_track_top(tracks, move|index, track|h_full(origin_y(callback(index, track))))
}
pub fn per_track_top <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
tracks: impl Fn() -> U + Send + Sync + 'a,
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
) -> impl Draw<Tui> + 'a {
origin_x(bg(Reset, iter_east(tracks,
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{
w_exact((x2 - x1) as u16, fg_bg(
track.color.lightest.term,
track.color.base.term,
callback(index, track))) })))
}
pub fn field_h <T: Screen> (theme: ItemTheme, head: impl Draw<T>, body: impl Draw<T>) -> impl Draw<T> {
}
pub fn field_v <T: Screen> (theme: ItemTheme, head: impl Draw<T>, body: impl Draw<T>) -> impl Draw<T> {
}