mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-02-21 08:19:03 +01:00
332 lines
12 KiB
Rust
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
|
|
}
|