tek/app/tek.rs
2026-02-20 23:03:02 +02:00

332 lines
12 KiB
Rust

#![allow(clippy::unit_arg)]
#![feature(adt_const_params,
associated_type_defaults,
closure_lifetime_binder,
if_let_guard,
impl_trait_in_assoc_type,
trait_alias,
type_alias_impl_trait,
type_changing_struct_update)]
#[cfg(test)] mod tek_test;
#[allow(unused)] pub(crate) use ::std::{
cmp::Ord,
collections::BTreeMap,
error::Error,
ffi::OsString,
fmt::{Write, Debug, Formatter},
fs::File,
ops::{Add, Sub, Mul, Div, Rem},
path::{Path, PathBuf},
sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}},
thread::JoinHandle,
};
#[allow(unused)] pub(crate) use ::{
xdg::BaseDirectories,
atomic_float::*,
tengri::tui::ratatui::{
self,
prelude::{Rect, Style, Stylize, Buffer, Modifier, buffer::Cell, Color::{self, *}},
widgets::{Widget, canvas::{Canvas, Line}},
},
tengri::tui::crossterm::{
self,
event::{Event, KeyEvent, KeyCode::{self, *}},
},
};
pub extern crate tengri; pub(crate) use tengri::{*, input::*, output::*, tui::*};
pub extern crate tek_device; pub(crate) use tek_device::{*, tek_engine::*};
mod tek_struct; pub use self::tek_struct::*;
mod tek_impls;
/// Command-line entrypoint.
#[cfg(feature = "cli")] pub fn main () -> Usually<()> {
use clap::Parser;
Cli::parse().run()
}
/// Create a new application from a backend, project, config, and mode
///
/// ```
/// let jack = tek_engine::Jack::new(&"test_tek").expect("failed to connect to jack");
/// let proj = Default::default();
/// let conf = Default::default();
/// let tek = tek::tek(&jack, proj, conf, "");
/// ```
pub fn tek (
jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef<str>
) -> App {
App {
color: ItemTheme::random(),
dialog: Dialog::welcome(),
jack: jack.clone(),
mode: config.get_mode(mode).expect("failed to find mode"),
config,
project,
..Default::default()
}
}
/// 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>>>>;
def_command!(AppCommand: |app: App| {
Nop => Ok(None),
Confirm => tek_confirm(app),
Cancel => todo!(), // TODO delegate:
Inc { axis: ControlAxis } => tek_inc(app, axis),
Dec { axis: ControlAxis } => tek_dec(app, axis),
SetDialog { dialog: Dialog } => {
swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog })
},
});
fn tek_confirm (state: &mut App) -> Perhaps<AppCommand> {
Ok(match &state.dialog {
Dialog::Menu(index, items) => {
let callback = items.0[*index].1.clone();
callback(state)?;
None
},
_ => todo!(),
})
}
fn tek_inc (state: &mut App, axis: &ControlAxis) -> Perhaps<AppCommand> {
Ok(match (&state.dialog, axis) {
(Dialog::None, _) => todo!(),
(Dialog::Menu(_, _), ControlAxis::Y) => AppCommand::SetDialog { dialog: state.dialog.menu_next() }
.execute(state)?,
_ => todo!()
})
}
fn tek_dec (state: &mut App, axis: &ControlAxis) -> Perhaps<AppCommand> {
Ok(match (&state.dialog, axis) {
(Dialog::None, _) => None,
(Dialog::Menu(_, _), ControlAxis::Y) => AppCommand::SetDialog { dialog: state.dialog.menu_prev() }
.execute(state)?,
_ => todo!()
})
}
pub 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(())
}
pub 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(())
}
pub fn load_bind (binds: &Binds, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
let mut map = Bind::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(())
}
/// CLI banner.
pub(crate) const HEADER: &'static str = r#"
~ █▀█▀█ █▀▀█ █ █ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~
█ █▀ █▀▀▄ ~ v0.4.0, 2026 winter (or is it) ~
~ ▀ █▀▀█ ▀ ▀ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#;
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, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"),
})))
}
fn collect_commands (
app: &App, input: &TuiIn
) -> Usually<Vec<AppCommand>> {
let mut commands = vec![];
for id in app.mode.keys.iter() {
if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref())
&& let Some(bindings) = event_map.query(input.event()) {
for binding in bindings {
for command in binding.commands.iter() {
if let Some(command) = app.namespace(command)? as Option<AppCommand> {
commands.push(command)
}
}
}
}
}
Ok(commands)
}
fn execute_commands (
app: &mut App, commands: Vec<AppCommand>
) -> Usually<Vec<(AppCommand, Option<AppCommand>)>> {
let mut history = vec![];
for command in commands.into_iter() {
let result = command.execute(app);
match result { Err(err) => { history.push((command, None)); return Err(err) }
Ok(undo) => { history.push((command, undo)); } };
}
Ok(history)
}
pub fn tek_jack_process (app: &mut App, client: &Client, scope: &ProcessScope) -> Control {
let t0 = app.perf.get_t0();
app.clock().update_from_scope(scope).unwrap();
let midi_in = app.project.midi_input_collect(scope);
if let Some(editor) = &app.editor() {
let mut pitch: Option<u7> = None;
for port in midi_in.iter() {
for event in port.iter() {
if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..}))
= event
{
pitch = Some(key.clone());
}
}
}
if let Some(pitch) = pitch {
editor.set_note_pos(pitch.as_int() as usize);
}
}
let result = app.project.process_tracks(client, scope);
app.perf.update_from_jack_scope(t0, scope);
result
}
pub fn tek_jack_event (app: &mut App, event: JackEvent) {
use JackEvent::*;
match event {
SampleRate(sr) => { app.clock().timebase.sr.set(sr as f64); },
PortRegistration(_id, true) => {
//let port = app.jack().port_by_id(id);
//println!("\rport add: {id} {port:?}");
//println!("\rport add: {id}");
},
PortRegistration(_id, false) => {
/*println!("\rport del: {id}")*/
},
PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ },
PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ },
ClientRegistration(_id, true) => {},
ClientRegistration(_id, false) => {},
ThreadInit => {},
XRun => {},
GraphReorder => {},
_ => { panic!("{event:?}"); }
}
}
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
}