mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-04-03 21:00:44 +02:00
1808 lines
65 KiB
Rust
1808 lines
65 KiB
Rust
#![allow(clippy::unit_arg)]
|
||
#![feature(
|
||
adt_const_params, associated_type_defaults, closure_lifetime_binder,
|
||
impl_trait_in_assoc_type, trait_alias, type_alias_impl_trait, type_changing_struct_update
|
||
)]
|
||
|
||
pub mod arrange;
|
||
pub mod browse;
|
||
pub mod connect;
|
||
pub mod device;
|
||
pub mod mix;
|
||
pub mod plugin;
|
||
pub mod sample;
|
||
pub mod sequence;
|
||
pub mod tick;
|
||
|
||
use clap::{self, Parser, Subcommand};
|
||
use builder_pattern::Builder;
|
||
|
||
extern crate xdg;
|
||
pub(crate) use ::xdg::BaseDirectories;
|
||
pub extern crate atomic_float;
|
||
pub(crate) use atomic_float::AtomicF64;
|
||
//pub extern crate jack;
|
||
//pub(crate) use ::jack::{*, contrib::{*, ClosureProcessHandler}};
|
||
pub extern crate midly;
|
||
pub(crate) use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*};
|
||
pub extern crate tengri;
|
||
pub(crate) use tengri::{
|
||
*,
|
||
lang::*,
|
||
play::*,
|
||
keys::*,
|
||
sing::*,
|
||
time::*,
|
||
draw::*,
|
||
term::*,
|
||
color::*,
|
||
space::*,
|
||
crossterm::event::{Event, KeyEvent},
|
||
ratatui::{
|
||
self,
|
||
prelude::{Rect, Style, Stylize, Buffer, Color::{self, *}},
|
||
widgets::{Widget, canvas::{Canvas, Line}},
|
||
},
|
||
};
|
||
#[cfg(feature = "sampler")] pub(crate) use symphonia::{
|
||
default::get_codecs,
|
||
core::{//errors::Error as SymphoniaError,
|
||
audio::SampleBuffer, formats::Packet, io::MediaSourceStream, probe::Hint,
|
||
codecs::{Decoder, CODEC_TYPE_NULL},
|
||
},
|
||
};
|
||
#[cfg(feature = "lv2_gui")] use ::winit::{
|
||
application::ApplicationHandler,
|
||
event::WindowEvent,
|
||
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
|
||
window::{Window, WindowId},
|
||
platform::x11::EventLoopBuilderExtX11
|
||
};
|
||
#[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}},
|
||
time::Duration,
|
||
thread::{spawn, JoinHandle},
|
||
},
|
||
};
|
||
|
||
pub(crate) use ConnectName::*;
|
||
pub(crate) use ConnectScope::*;
|
||
pub(crate) use ConnectStatus::*;
|
||
|
||
/// Command-line entrypoint.
|
||
#[cfg(feature = "cli")] pub fn main () -> Usually<()> {
|
||
Config::watch(|config|{
|
||
Exit::enter(|exit|{
|
||
Jack::connect("tek", |jack|{
|
||
let state = Arc::new(RwLock::new(App {
|
||
color: ItemTheme::random(),
|
||
config: Config::init(),
|
||
dialog: Dialog::welcome(),
|
||
jack: jack.clone(),
|
||
mode: ":menu",
|
||
project: Arrangement::new(&jack, &Clock::new(&jack, 51)),
|
||
..Default::default()
|
||
}));
|
||
// TODO: Sync these timings with main clock, so that things
|
||
// "accidentally" fall on the beat in overload conditions.
|
||
let keyboard = run_tui_in(&exit, &state, Duration::from_millis(100))?;
|
||
let terminal = run_tui_out(&exit, &state, Duration::from_millis(10))?;
|
||
(keyboard, terminal)
|
||
})
|
||
})
|
||
})
|
||
}
|
||
|
||
/// Create a new application from a backend, project, config, and mode
|
||
///
|
||
/// ```
|
||
/// let jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack");
|
||
/// let proj = tek::Arrangement::default();
|
||
/// let mut conf = tek::Config::default();
|
||
/// conf.add("(mode hello)");
|
||
/// let tek = tek::tek(&jack, proj, conf, "hello");
|
||
/// ```
|
||
pub fn tek (
|
||
jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef<str>
|
||
) -> App {
|
||
let mode: &str = mode.as_ref();
|
||
App {
|
||
color: ItemTheme::random(),
|
||
dialog: Dialog::welcome(),
|
||
jack: jack.clone(),
|
||
mode: config.get_mode(mode).expect(&format!("failed to find mode '{mode}'")),
|
||
config,
|
||
project,
|
||
..Default::default()
|
||
}
|
||
}
|
||
|
||
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(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(())
|
||
}
|
||
|
||
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(())
|
||
}
|
||
|
||
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(())
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
/// Return boxed iterator of MIDI events
|
||
pub fn parse_midi_input <'a> (input: ::tengri::jack::MidiIter<'a>)
|
||
-> Box<dyn Iterator<Item=(usize, LiveEvent<'a>, &'a [u8])> + 'a>
|
||
{
|
||
Box::new(input.map(|::tengri::jack::RawMidi { time, bytes }|(
|
||
time as usize,
|
||
LiveEvent::parse(bytes).unwrap(),
|
||
bytes
|
||
)))
|
||
}
|
||
|
||
/// Add "all notes off" to the start of a buffer.
|
||
pub fn all_notes_off (output: &mut [Vec<Vec<u8>>]) {
|
||
let mut buf = vec![];
|
||
let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() };
|
||
let evt = LiveEvent::Midi { channel: 0.into(), message: msg };
|
||
evt.write(&mut buf).unwrap();
|
||
output[0].push(buf);
|
||
}
|
||
|
||
/// Update notes_in array
|
||
pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) {
|
||
match message {
|
||
MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; }
|
||
MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; },
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
/// Returns the next shorter length
|
||
pub fn note_duration_prev (pulses: usize) -> usize {
|
||
for (length, _) in NOTE_DURATIONS.iter().rev() { if *length < pulses { return *length } }
|
||
pulses
|
||
}
|
||
|
||
/// Returns the next longer length
|
||
pub fn note_duration_next (pulses: usize) -> usize {
|
||
for (length, _) in NOTE_DURATIONS.iter() { if *length > pulses { return *length } }
|
||
pulses
|
||
}
|
||
|
||
pub fn note_duration_to_name (pulses: usize) -> &'static str {
|
||
for (length, name) in NOTE_DURATIONS.iter() { if *length == pulses { return name } }
|
||
""
|
||
}
|
||
|
||
pub fn note_pitch_to_name (n: usize) -> &'static str {
|
||
if n > 127 {
|
||
panic!("to_note_name({n}): must be 0-127");
|
||
}
|
||
NOTE_NAMES[n]
|
||
}
|
||
|
||
pub fn swap_value <T: Clone + PartialEq, U> (
|
||
target: &mut T, value: &T, returned: impl Fn(T)->U
|
||
) -> Perhaps<U> {
|
||
if *target == *value {
|
||
Ok(None)
|
||
} else {
|
||
let mut value = value.clone();
|
||
std::mem::swap(target, &mut value);
|
||
Ok(Some(returned(value)))
|
||
}
|
||
}
|
||
|
||
pub fn toggle_bool <U> (
|
||
target: &mut bool, value: &Option<bool>, returned: impl Fn(Option<bool>)->U
|
||
) -> Perhaps<U> {
|
||
let mut value = value.unwrap_or(!*target);
|
||
if value == *target {
|
||
Ok(None)
|
||
} else {
|
||
std::mem::swap(target, &mut value);
|
||
Ok(Some(returned(Some(value))))
|
||
}
|
||
}
|
||
|
||
pub fn device_kinds () -> &'static [&'static str] {
|
||
&[
|
||
#[cfg(feature = "sampler")] "Sampler",
|
||
#[cfg(feature = "lv2")] "Plugin (LV2)",
|
||
]
|
||
}
|
||
|
||
impl<T: AsRef<Vec<Device>> + AsMut<Vec<Device>>> HasDevices for T {
|
||
fn devices (&self) -> &Vec<Device> {
|
||
self.as_ref()
|
||
}
|
||
fn devices_mut (&mut self) -> &mut Vec<Device> {
|
||
self.as_mut()
|
||
}
|
||
}
|
||
|
||
impl Device {
|
||
pub fn name (&self) -> &str {
|
||
match self {
|
||
Self::Sampler(sampler) => sampler.name.as_ref(),
|
||
_ => todo!(),
|
||
}
|
||
}
|
||
pub fn midi_ins (&self) -> &[MidiInput] {
|
||
match self {
|
||
//Self::Sampler(Sampler { midi_in, .. }) => &[midi_in],
|
||
_ => todo!()
|
||
}
|
||
}
|
||
pub fn midi_outs (&self) -> &[MidiOutput] {
|
||
match self {
|
||
Self::Sampler(_) => &[],
|
||
_ => todo!()
|
||
}
|
||
}
|
||
pub fn audio_ins (&self) -> &[AudioInput] {
|
||
match self {
|
||
Self::Sampler(Sampler { audio_ins, .. }) => audio_ins.as_slice(),
|
||
_ => todo!()
|
||
}
|
||
}
|
||
pub fn audio_outs (&self) -> &[AudioOutput] {
|
||
match self {
|
||
Self::Sampler(Sampler { audio_outs, .. }) => audio_outs.as_slice(),
|
||
_ => todo!()
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
//take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref()
|
||
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));
|
||
|
||
#[macro_export] macro_rules! has_clip {
|
||
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
|
||
impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? {
|
||
fn clip (&$self) -> Option<Arc<RwLock<MidiClip>>> { $cb }
|
||
}
|
||
}
|
||
}
|
||
|
||
fn to_key (note: usize) -> &'static str {
|
||
match note % 12 {
|
||
11 | 9 | 7 | 5 | 4 | 2 | 0 => "████▌",
|
||
10 | 8 | 6 | 3 | 1 => " ",
|
||
_ => unreachable!(),
|
||
}
|
||
}
|
||
|
||
pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16)
|
||
-> impl Iterator<Item=(usize, u16, usize)>
|
||
{
|
||
(note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n))
|
||
}
|
||
|
||
|
||
#[cfg(feature = "lv2_gui")]
|
||
pub fn run_lv2_ui (mut ui: LV2PluginUI) -> Usually<JoinHandle<()>> {
|
||
Ok(spawn(move||{
|
||
let event_loop = EventLoop::builder().with_x11().with_any_thread(true).build().unwrap();
|
||
event_loop.set_control_flow(ControlFlow::Wait);
|
||
event_loop.run_app(&mut ui).unwrap()
|
||
}))
|
||
}
|
||
|
||
#[cfg(feature = "lv2_gui")]
|
||
fn lv2_ui_instantiate (kind: &str) {
|
||
//let host = Suil
|
||
}
|
||
|
||
pub fn mix_summing <const N: usize> (
|
||
buffer: &mut [Vec<f32>], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>,
|
||
) -> bool {
|
||
let channels = buffer.len();
|
||
for index in 0..frames {
|
||
if let Some(frame) = next() {
|
||
for (channel, sample) in frame.iter().enumerate() {
|
||
let channel = channel % channels;
|
||
buffer[channel][index] += sample * gain;
|
||
}
|
||
} else {
|
||
return false
|
||
}
|
||
}
|
||
true
|
||
}
|
||
|
||
pub fn mix_average <const N: usize> (
|
||
buffer: &mut [Vec<f32>], gain: f32, frames: usize, mut next: impl FnMut()->Option<[f32;N]>,
|
||
) -> bool {
|
||
let channels = buffer.len();
|
||
for index in 0..frames {
|
||
if let Some(frame) = next() {
|
||
for (channel, sample) in frame.iter().enumerate() {
|
||
let channel = channel % channels;
|
||
let value = buffer[channel][index];
|
||
buffer[channel][index] = (value + sample * gain) / 2.0;
|
||
}
|
||
} else {
|
||
return false
|
||
}
|
||
}
|
||
true
|
||
}
|
||
|
||
pub fn to_log10 (samples: &[f32]) -> f32 {
|
||
let total: f32 = samples.iter().map(|x|x.abs()).sum();
|
||
let count = samples.len() as f32;
|
||
10. * (total / count).log10()
|
||
}
|
||
|
||
|
||
pub fn to_rms (samples: &[f32]) -> f32 {
|
||
let sum = samples.iter()
|
||
.map(|s|*s)
|
||
.reduce(|sum, sample|sum + sample.abs())
|
||
.unwrap_or(0.0);
|
||
(sum / samples.len() as f32).sqrt()
|
||
}
|
||
|
||
|
||
fn read_sample_data (_: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
|
||
todo!();
|
||
}
|
||
|
||
fn scan (dir: &PathBuf) -> Usually<(Vec<OsString>, Vec<OsString>)> {
|
||
let (mut subdirs, mut files) = std::fs::read_dir(dir)?
|
||
.fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{
|
||
let entry = entry.expect("failed to read drectory entry");
|
||
let meta = entry.metadata().expect("failed to read entry metadata");
|
||
if meta.is_file() {
|
||
files.push(entry.file_name());
|
||
} else if meta.is_dir() {
|
||
subdirs.push(entry.file_name());
|
||
}
|
||
(subdirs, files)
|
||
});
|
||
subdirs.sort();
|
||
files.sort();
|
||
Ok((subdirs, files))
|
||
}
|
||
|
||
|
||
|
||
pub(crate) fn track_width (_index: usize, track: &Track) -> u16 {
|
||
track.width as u16
|
||
}
|
||
|
||
|
||
#[cfg(feature = "vst2")] fn set_vst_plugin <E: Engine> (
|
||
host: &Arc<Mutex<Plugin<E>>>, _path: &str
|
||
) -> Usually<PluginKind> {
|
||
let mut loader = ::vst::host::PluginLoader::load(
|
||
&std::path::Path::new("/nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lxvst/helm.so"),
|
||
host.clone()
|
||
)?;
|
||
Ok(PluginKind::VST2 {
|
||
instance: loader.instance()?
|
||
})
|
||
}
|
||
|
||
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 })
|
||
},
|
||
});
|
||
|
||
def_command!(ClockCommand: |clock: Clock| {
|
||
SeekUsec { usec: f64 } => {
|
||
clock.playhead.update_from_usec(*usec); Ok(None) },
|
||
SeekSample { sample: f64 } => {
|
||
clock.playhead.update_from_sample(*sample); Ok(None) },
|
||
SeekPulse { pulse: f64 } => {
|
||
clock.playhead.update_from_pulse(*pulse); Ok(None) },
|
||
SetBpm { bpm: f64 } => Ok(Some(
|
||
Self::SetBpm { bpm: clock.timebase().bpm.set(*bpm) })),
|
||
SetQuant { quant: f64 } => Ok(Some(
|
||
Self::SetQuant { quant: clock.quant.set(*quant) })),
|
||
SetSync { sync: f64 } => Ok(Some(
|
||
Self::SetSync { sync: clock.sync.set(*sync) })),
|
||
|
||
Play { position: Option<u32> } => {
|
||
clock.play_from(*position)?; Ok(None) /* TODO Some(Pause(previousPosition)) */ },
|
||
Pause { position: Option<u32> } => {
|
||
clock.pause_at(*position)?; Ok(None) },
|
||
|
||
TogglePlayback { position: u32 } => Ok(if clock.is_rolling() {
|
||
clock.pause_at(Some(*position))?; None
|
||
} else {
|
||
clock.play_from(Some(*position))?; None
|
||
}),
|
||
});
|
||
|
||
def_command!(DeviceCommand: |device: Device| {});
|
||
|
||
def_command!(ClipCommand: |clip: MidiClip| {
|
||
SetColor { color: Option<ItemTheme> } => {
|
||
//(SetColor [t: usize, s: usize, c: ItemTheme]
|
||
//clip.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o)))));
|
||
//("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random())))
|
||
todo!()
|
||
},
|
||
SetLoop { looping: Option<bool> } => {
|
||
//(SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}"))
|
||
//("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap())))
|
||
todo!()
|
||
}
|
||
});
|
||
|
||
def_command!(BrowseCommand: |browse: Browse| {
|
||
SetVisible => Ok(None),
|
||
SetPath { address: PathBuf } => Ok(None),
|
||
SetSearch { filter: Arc<str> } => Ok(None),
|
||
SetCursor { cursor: usize } => Ok(None),
|
||
});
|
||
|
||
def_command!(MidiEditCommand: |editor: MidiEditor| {
|
||
Show { clip: Option<Arc<RwLock<MidiClip>>> } => {
|
||
editor.set_clip(clip.as_ref()); editor.redraw(); Ok(None) },
|
||
DeleteNote => {
|
||
editor.redraw(); todo!() },
|
||
AppendNote { advance: bool } => {
|
||
editor.put_note(*advance); editor.redraw(); Ok(None) },
|
||
SetNotePos { pos: usize } => {
|
||
editor.set_note_pos((*pos).min(127)); editor.redraw(); Ok(None) },
|
||
SetNoteLen { len: usize } => {
|
||
editor.set_note_len(*len); editor.redraw(); Ok(None) },
|
||
SetNoteScroll { scroll: usize } => {
|
||
editor.set_note_lo((*scroll).min(127)); editor.redraw(); Ok(None) },
|
||
SetTimePos { pos: usize } => {
|
||
editor.set_time_pos(*pos); editor.redraw(); Ok(None) },
|
||
SetTimeScroll { scroll: usize } => {
|
||
editor.set_time_start(*scroll); editor.redraw(); Ok(None) },
|
||
SetTimeZoom { zoom: usize } => {
|
||
editor.set_time_zoom(*zoom); editor.redraw(); Ok(None) },
|
||
SetTimeLock { lock: bool } => {
|
||
editor.set_time_lock(*lock); editor.redraw(); Ok(None) },
|
||
// TODO: 1-9 seek markers that by default start every 8th of the clip
|
||
});
|
||
|
||
def_command!(PoolCommand: |pool: Pool| {
|
||
// Toggle visibility of pool
|
||
Show { visible: bool } => { pool.visible = *visible; Ok(Some(Self::Show { visible: !visible })) },
|
||
// Select a clip from the clip pool
|
||
Select { index: usize } => { pool.set_clip_index(*index); Ok(None) },
|
||
// Update the contents of the clip pool
|
||
Clip { command: PoolClipCommand } => Ok(command.execute(pool)?.map(|command|Self::Clip{command})),
|
||
// Rename a clip
|
||
Rename { command: RenameCommand } => Ok(command.delegate(pool, |command|Self::Rename{command})?),
|
||
// Change the length of a clip
|
||
Length { command: CropCommand } => Ok(command.delegate(pool, |command|Self::Length{command})?),
|
||
// Import from file
|
||
Import { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() {
|
||
command.delegate(browse, |command|Self::Import{command})?
|
||
} else {
|
||
None
|
||
}),
|
||
// Export to file
|
||
Export { command: BrowseCommand } => Ok(if let Some(browse) = pool.browse.as_mut() {
|
||
command.delegate(browse, |command|Self::Export{command})?
|
||
} else {
|
||
None
|
||
}),
|
||
});
|
||
|
||
def_command!(PoolClipCommand: |pool: Pool| {
|
||
Delete { index: usize } => {
|
||
let index = *index;
|
||
let clip = pool.clips_mut().remove(index).read().unwrap().clone();
|
||
Ok(Some(Self::Add { index, clip }))
|
||
},
|
||
Swap { index: usize, other: usize } => {
|
||
let index = *index;
|
||
let other = *other;
|
||
pool.clips_mut().swap(index, other);
|
||
Ok(Some(Self::Swap { index, other }))
|
||
},
|
||
Export { index: usize, path: PathBuf } => {
|
||
todo!("export clip to midi file");
|
||
},
|
||
Add { index: usize, clip: MidiClip } => {
|
||
let index = *index;
|
||
let mut index = index;
|
||
let clip = Arc::new(RwLock::new(clip.clone()));
|
||
let mut clips = pool.clips_mut();
|
||
if index >= clips.len() {
|
||
index = clips.len();
|
||
clips.push(clip)
|
||
} else {
|
||
clips.insert(index, clip);
|
||
}
|
||
Ok(Some(Self::Delete { index }))
|
||
},
|
||
Import { index: usize, path: PathBuf } => {
|
||
let index = *index;
|
||
let bytes = std::fs::read(&path)?;
|
||
let smf = Smf::parse(bytes.as_slice())?;
|
||
let mut t = 0u32;
|
||
let mut events = vec![];
|
||
for track in smf.tracks.iter() {
|
||
for event in track.iter() {
|
||
t += event.delta.as_int();
|
||
if let TrackEventKind::Midi { channel, message } = event.kind {
|
||
events.push((t, channel.as_int(), message));
|
||
}
|
||
}
|
||
}
|
||
let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None);
|
||
for event in events.iter() {
|
||
clip.notes[event.0 as usize].push(event.2);
|
||
}
|
||
Ok(Self::Add { index, clip }.execute(pool)?)
|
||
},
|
||
SetName { index: usize, name: Arc<str> } => {
|
||
let index = *index;
|
||
let clip = &mut pool.clips_mut()[index];
|
||
let old_name = clip.read().unwrap().name.clone();
|
||
clip.write().unwrap().name = name.clone();
|
||
Ok(Some(Self::SetName { index, name: old_name }))
|
||
},
|
||
SetLength { index: usize, length: usize } => {
|
||
let index = *index;
|
||
let clip = &mut pool.clips_mut()[index];
|
||
let old_len = clip.read().unwrap().length;
|
||
clip.write().unwrap().length = *length;
|
||
Ok(Some(Self::SetLength { index, length: old_len }))
|
||
},
|
||
SetColor { index: usize, color: ItemColor } => {
|
||
let index = *index;
|
||
let mut color = ItemTheme::from(*color);
|
||
std::mem::swap(&mut color, &mut pool.clips()[index].write().unwrap().color);
|
||
Ok(Some(Self::SetColor { index, color: color.base }))
|
||
},
|
||
});
|
||
|
||
def_command!(RenameCommand: |pool: Pool| {
|
||
Begin => unreachable!(),
|
||
Cancel => {
|
||
if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() {
|
||
pool.clips()[clip].write().unwrap().name = old_name.clone().into();
|
||
}
|
||
Ok(None)
|
||
},
|
||
Confirm => {
|
||
if let Some(PoolMode::Rename(_clip, ref mut old_name)) = pool.mode_mut().clone() {
|
||
let old_name = old_name.clone(); *pool.mode_mut() = None; return Ok(Some(Self::Set { value: old_name }))
|
||
}
|
||
Ok(None)
|
||
},
|
||
Set { value: Arc<str> } => {
|
||
if let Some(PoolMode::Rename(clip, ref mut _old_name)) = pool.mode_mut().clone() {
|
||
pool.clips()[clip].write().unwrap().name = value.clone();
|
||
}
|
||
Ok(None)
|
||
},
|
||
});
|
||
|
||
def_command!(CropCommand: |pool: Pool| {
|
||
Begin => unreachable!(),
|
||
Cancel => { if let Some(PoolMode::Length(..)) = pool.mode_mut().clone() { *pool.mode_mut() = None; } Ok(None) },
|
||
Set { length: usize } => {
|
||
if let Some(PoolMode::Length(clip, ref mut length, ref mut _focus))
|
||
= pool.mode_mut().clone()
|
||
{
|
||
let old_length;
|
||
{
|
||
let clip = pool.clips()[clip].clone();//.write().unwrap();
|
||
old_length = Some(clip.read().unwrap().length);
|
||
clip.write().unwrap().length = *length;
|
||
}
|
||
*pool.mode_mut() = None;
|
||
return Ok(old_length.map(|length|Self::Set { length }))
|
||
}
|
||
Ok(None)
|
||
},
|
||
Next => {
|
||
if let Some(PoolMode::Length(_clip, ref mut _length, ref mut focus)) = pool.mode_mut().clone() { focus.next() }; Ok(None)
|
||
},
|
||
Prev => {
|
||
if let Some(PoolMode::Length(_clip, ref mut _length, ref mut focus)) = pool.mode_mut().clone() { focus.prev() }; Ok(None)
|
||
},
|
||
Inc => {
|
||
if let Some(PoolMode::Length(_clip, ref mut length, ref mut focus)) = pool.mode_mut().clone() {
|
||
match focus {
|
||
ClipLengthFocus::Bar => { *length += 4 * PPQ },
|
||
ClipLengthFocus::Beat => { *length += PPQ },
|
||
ClipLengthFocus::Tick => { *length += 1 },
|
||
}
|
||
}
|
||
Ok(None)
|
||
},
|
||
Dec => {
|
||
if let Some(PoolMode::Length(_clip, ref mut length, ref mut focus)) = pool.mode_mut().clone() {
|
||
match focus {
|
||
ClipLengthFocus::Bar => { *length = length.saturating_sub(4 * PPQ) },
|
||
ClipLengthFocus::Beat => { *length = length.saturating_sub(PPQ) },
|
||
ClipLengthFocus::Tick => { *length = length.saturating_sub(1) },
|
||
}
|
||
}
|
||
Ok(None)
|
||
}
|
||
});
|
||
|
||
def_command!(MidiInputCommand: |port: MidiInput| {
|
||
Close => todo!(),
|
||
Connect { midi_out: Arc<str> } => todo!(),
|
||
});
|
||
|
||
def_command!(MidiOutputCommand: |port: MidiOutput| {
|
||
Close => todo!(),
|
||
Connect { midi_in: Arc<str> } => todo!(),
|
||
});
|
||
|
||
def_command!(AudioInputCommand: |port: AudioInput| {
|
||
Close => todo!(),
|
||
Connect { audio_out: Arc<str> } => todo!(),
|
||
});
|
||
|
||
def_command!(AudioOutputCommand: |port: AudioOutput| {
|
||
Close => todo!(),
|
||
Connect { audio_in: Arc<str> } => todo!(),
|
||
});
|
||
|
||
def_command!(SamplerCommand: |sampler: Sampler| {
|
||
RecordToggle { slot: usize } => {
|
||
let slot = *slot;
|
||
let recording = sampler.recording.as_ref().map(|x|x.0);
|
||
let _ = Self::RecordFinish.execute(sampler)?;
|
||
// autoslice: continue recording at next slot
|
||
if recording != Some(slot) {
|
||
Self::RecordBegin { slot }.execute(sampler)
|
||
} else {
|
||
Ok(None)
|
||
}
|
||
},
|
||
RecordBegin { slot: usize } => {
|
||
let slot = *slot;
|
||
sampler.recording = Some((
|
||
slot,
|
||
Some(Arc::new(RwLock::new(Sample::new(
|
||
"Sample", 0, 0, vec![vec![];sampler.audio_ins.len()]
|
||
))))
|
||
));
|
||
Ok(None)
|
||
},
|
||
RecordFinish => {
|
||
let _prev_sample = sampler.recording.as_mut().map(|(index, sample)|{
|
||
std::mem::swap(sample, &mut sampler.samples.0[*index]);
|
||
sample
|
||
}); // TODO: undo
|
||
Ok(None)
|
||
},
|
||
RecordCancel => {
|
||
sampler.recording = None;
|
||
Ok(None)
|
||
},
|
||
PlaySample { slot: usize } => {
|
||
let slot = *slot;
|
||
if let Some(ref sample) = sampler.samples.0[slot] {
|
||
sampler.voices.write().unwrap().push(Sample::play(sample, 0, &u7::from(128)));
|
||
}
|
||
Ok(None)
|
||
},
|
||
StopSample { slot: usize } => {
|
||
let _slot = *slot;
|
||
todo!();
|
||
//Ok(None)
|
||
},
|
||
});
|
||
|
||
def_command!(FileBrowserCommand: |sampler: Sampler|{
|
||
//("begin" [] Some(Self::Begin))
|
||
//("cancel" [] Some(Self::Cancel))
|
||
//("confirm" [] Some(Self::Confirm))
|
||
//("select" [i: usize] Some(Self::Select(i.expect("no index"))))
|
||
//("chdir" [p: PathBuf] Some(Self::Chdir(p.expect("no path"))))
|
||
//("filter" [f: Arc<str>] Some(Self::Filter(f.expect("no filter")))))
|
||
});
|
||
|
||
def_command!(SceneCommand: |scene: Scene| {
|
||
SetSize { size: usize } => { todo!() },
|
||
SetZoom { size: usize } => { todo!() },
|
||
SetName { name: Arc<str> } =>
|
||
swap_value(&mut scene.name, name, |name|Self::SetName{name}),
|
||
SetColor { color: ItemTheme } =>
|
||
swap_value(&mut scene.color, color, |color|Self::SetColor{color}),
|
||
});
|
||
|
||
def_command!(TrackCommand: |track: Track| {
|
||
Stop => { track.sequencer.enqueue_next(None); Ok(None) },
|
||
SetMute { mute: Option<bool> } => todo!(),
|
||
SetSolo { solo: Option<bool> } => todo!(),
|
||
SetSize { size: usize } => todo!(),
|
||
SetZoom { zoom: usize } => todo!(),
|
||
SetName { name: Arc<str> } =>
|
||
swap_value(&mut track.name, name, |name|Self::SetName { name }),
|
||
SetColor { color: ItemTheme } =>
|
||
swap_value(&mut track.color, color, |color|Self::SetColor { color }),
|
||
SetRec { rec: Option<bool> } =>
|
||
toggle_bool(&mut track.sequencer.recording, rec, |rec|Self::SetRec { rec }),
|
||
SetMon { mon: Option<bool> } =>
|
||
toggle_bool(&mut track.sequencer.monitoring, mon, |mon|Self::SetMon { mon }),
|
||
});
|
||
|
||
pub(crate) use self::size::*;
|
||
mod size {
|
||
use crate::*;
|
||
/// Define a type alias for iterators of sized items (columns).
|
||
macro_rules! def_sizes_iter {
|
||
($Type:ident => $($Item:ty),+) => {
|
||
pub trait $Type<'a> =
|
||
Iterator<Item=(usize, $(&'a $Item,)+ usize, usize)> + Send + Sync + 'a;
|
||
}
|
||
}
|
||
def_sizes_iter!(InputsSizes => MidiInput);
|
||
def_sizes_iter!(OutputsSizes => MidiOutput);
|
||
def_sizes_iter!(PortsSizes => Arc<str>, [Connect]);
|
||
def_sizes_iter!(ScenesSizes => Scene);
|
||
def_sizes_iter!(TracksSizes => Track);
|
||
}
|
||
|
||
pub use self::view::*;
|
||
mod view {
|
||
use crate::*;
|
||
|
||
/// ```
|
||
/// let _ = tek::view_logo();
|
||
/// ```
|
||
pub fn view_logo () -> impl Draw<Tui> {
|
||
wh_exact(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), south!{
|
||
h_exact(1, ""),
|
||
h_exact(1, ""),
|
||
h_exact(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"),
|
||
h_exact(1, east("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", east(Tui::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: Arc<RwLock<String>>,
|
||
beat: Arc<RwLock<String>>,
|
||
time: Arc<RwLock<String>>,
|
||
) -> impl Draw<Tui> {
|
||
let theme = ItemTheme::G[96];
|
||
Tui::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<Arc<str>>,
|
||
sr: Arc<RwLock<String>>,
|
||
buf: Arc<RwLock<String>>,
|
||
lat: Arc<RwLock<String>>,
|
||
) -> impl Draw<Tui> {
|
||
let theme = ItemTheme::G[96];
|
||
Tui::bg(Black, east!(above(
|
||
wh_full(origin_w(sel.map(|sel|field_h(theme, "Selected", sel)))),
|
||
wh_full(origin_e(east!(
|
||
field_h(theme, "SR", sr),
|
||
field_h(theme, "Buf", buf),
|
||
field_h(theme, "Lat", lat),
|
||
)))
|
||
)))
|
||
}
|
||
|
||
/// ```
|
||
/// let _ = tek::button_play_pause(true);
|
||
/// ```
|
||
pub fn button_play_pause (playing: bool) -> impl Draw<Tui> {
|
||
let compact = true;//self.is_editing();
|
||
Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) },
|
||
either(compact,
|
||
Thunk::new(move|to: &mut Tui|to.place(&w_exact(9, either(playing,
|
||
Tui::fg(Rgb(0, 255, 0), " PLAYING "),
|
||
Tui::fg(Rgb(255, 128, 0), " STOPPED ")))
|
||
)),
|
||
Thunk::new(move|to: &mut Tui|to.place(&w_exact(5, either(playing,
|
||
Tui::fg(Rgb(0, 255, 0), south(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)),
|
||
Tui::fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",))))
|
||
))
|
||
)
|
||
)
|
||
}
|
||
|
||
#[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 = Tui::fg_bg(bg, Reset, w_exact(1, y_repeat("▐")));
|
||
let right = Tui::fg_bg(bg, Reset, w_exact(1, y_repeat("▌")));
|
||
east(left, west(right, Tui::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 {
|
||
south!(
|
||
field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value)),
|
||
wh_exact(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 }, 1, Tui::bg(if value >= 0.0 { Red }
|
||
else if value >= -3.0 { Yellow }
|
||
else { Green }, ())))
|
||
}
|
||
|
||
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 draw_info (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
|
||
when(sample.is_some(), Thunk::new(move|to: &mut Tui|{
|
||
let sample = sample.unwrap().read().unwrap();
|
||
let theme = sample.color;
|
||
to.place(&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)),
|
||
))
|
||
}))
|
||
}
|
||
|
||
pub fn draw_info_v (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
|
||
either(sample.is_some(), Thunk::new(move|to: &mut Tui|{
|
||
let sample = sample.unwrap().read().unwrap();
|
||
let theme = sample.color;
|
||
to.place(&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)))),
|
||
)))
|
||
}), Thunk::new(|to: &mut Tui|to.place(&Tui::fg(Red, south!(
|
||
Tui::bold(true, "× No sample."),
|
||
"[r] record",
|
||
"[Shift-F9] import",
|
||
)))))
|
||
}
|
||
|
||
pub fn draw_status (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> {
|
||
Tui::bold(true, Tui::fg(Tui::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, Tui::bg(theme.darker.rgb, 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(Tui::g(96)));
|
||
let iter = move||ports.iter();
|
||
let names = iter_south(1, iter, move|port, index|h_full(origin_w(format!(" {index} {}", port.port_name()))));
|
||
let field = field_v(theme, title, names);
|
||
wh_exact(20, 1 + ins, frame.enclose(wh_exact(20, 1 + ins, field)))
|
||
}
|
||
|
||
pub fn io_ports <'a, T: PortsSizes<'a>> (
|
||
fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a
|
||
) -> impl Draw<Tui> + 'a {
|
||
iter(items, move|(
|
||
_index, name, connections, y, y2
|
||
): (usize, &'a Arc<str>, &'a [Connect], usize, usize), _|
|
||
iter_south(y as u16, (y2-y) as u16, south(
|
||
h_full(Tui::bold(true, Tui::fg_bg(fg, bg, origin_w(east(&" ", name))))),
|
||
iter(||connections.iter(), move|connect: &'a Connect, index|iter_south(index as u16, 1,
|
||
h_full(origin_w(Tui::bold(false, Tui::fg_bg(fg, bg,
|
||
&connect.info)))))))))
|
||
}
|
||
}
|
||
|
||
#[cfg(test)] mod test_view_meter {
|
||
use super::*;
|
||
use proptest::prelude::*;
|
||
proptest! {
|
||
|
||
#[test] fn proptest_view_meter (
|
||
label in "\\PC*", value in f32::MIN..f32::MAX
|
||
) {
|
||
let _ = view_meter(&label, value);
|
||
}
|
||
|
||
#[test] fn proptest_view_meters (
|
||
value1 in f32::MIN..f32::MAX,
|
||
value2 in f32::MIN..f32::MAX
|
||
) {
|
||
let _ = view_meters(&[value1, value2]);
|
||
}
|
||
}
|
||
}
|
||
|
||
pub const DEFAULT_PPQ: f64 = 96.0;
|
||
|
||
/// FIXME: remove this and use PPQ from timebase everywhere:
|
||
pub const PPQ: usize = 96;
|
||
|
||
/// (pulses, name), assuming 96 PPQ
|
||
pub const NOTE_DURATIONS: [(usize, &str);26] = [
|
||
(1, "1/384"), (2, "1/192"),
|
||
(3, "1/128"), (4, "1/96"),
|
||
(6, "1/64"), (8, "1/48"),
|
||
(12, "1/32"), (16, "1/24"),
|
||
(24, "1/16"), (32, "1/12"),
|
||
(48, "1/8"), (64, "1/6"),
|
||
(96, "1/4"), (128, "1/3"),
|
||
(192, "1/2"), (256, "2/3"),
|
||
(384, "1/1"), (512, "4/3"),
|
||
(576, "3/2"), (768, "2/1"),
|
||
(1152, "3/1"), (1536, "4/1"),
|
||
(2304, "6/1"), (3072, "8/1"),
|
||
(3456, "9/1"), (6144, "16/1"),
|
||
];
|
||
|
||
pub const NOTE_NAMES: [&str; 128] = [
|
||
"C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0",
|
||
"C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1",
|
||
"C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2",
|
||
"C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3",
|
||
"C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4",
|
||
"C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5",
|
||
"C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6",
|
||
"C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7",
|
||
"C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8",
|
||
"C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9",
|
||
"C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10",
|
||
];
|
||
|
||
/// CLI banner.
|
||
pub(crate) const HEADER: &'static str = r#"
|
||
|
||
~ █▀█▀█ █▀▀█ █ █ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~
|
||
█ term█▀ █▀▀▄ ~ v0.4.0, 2026 winter (or is it) ~
|
||
~ ▀ █▀▀█ ▀ ▀ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#;
|
||
|
||
/// Total state
|
||
///
|
||
/// ```
|
||
/// use tek::{HasTracks, HasScenes, TracksView, ScenesView};
|
||
/// let mut app = tek::App::default();
|
||
/// let _ = app.scene_add(None, None).unwrap();
|
||
/// let _ = app.update_clock();
|
||
/// app.project.editor = Some(Default::default());
|
||
/// //let _: Vec<_> = app.project.inputs_with_sizes().collect();
|
||
/// //let _: Vec<_> = app.project.outputs_with_sizes().collect();
|
||
/// let _: Vec<_> = app.project.tracks_with_sizes().collect();
|
||
/// //let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect();
|
||
/// //let _: Vec<_> = app.scenes_with_colors(true, 10).collect();
|
||
/// //let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect();
|
||
/// let _ = app.project.w();
|
||
/// //let _ = app.project.w_sidebar();
|
||
/// //let _ = app.project.w_tracks_area();
|
||
/// let _ = app.project.h();
|
||
/// //let _ = app.project.h_tracks_area();
|
||
/// //let _ = app.project.h_inputs();
|
||
/// //let _ = app.project.h_outputs();
|
||
/// let _ = app.project.h_scenes();
|
||
/// ```
|
||
#[derive(Default, Debug)] pub struct App {
|
||
/// Base color.
|
||
pub color: ItemTheme,
|
||
/// Must not be dropped for the duration of the process
|
||
pub jack: Jack<'static>,
|
||
/// Display size
|
||
pub size: Measure<Tui>,
|
||
/// Performance counter
|
||
pub perf: PerfModel,
|
||
/// Available view modes and input bindings
|
||
pub config: Config,
|
||
/// Currently selected mode
|
||
pub mode: Arc<Mode<Arc<str>>>,
|
||
/// Undo history
|
||
pub history: Vec<(AppCommand, Option<AppCommand>)>,
|
||
/// Dialog overlay
|
||
pub dialog: Dialog,
|
||
/// Contains all recently created clips.
|
||
pub pool: Pool,
|
||
/// Contains the currently edited musical arrangement
|
||
pub project: Arrangement,
|
||
/// Error, if any
|
||
pub error: Arc<RwLock<Option<Arc<str>>>>
|
||
}
|
||
|
||
/// 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,
|
||
}
|
||
|
||
/// 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,
|
||
}
|
||
|
||
/// 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>>
|
||
);
|
||
|
||
/// List of menu items.
|
||
///
|
||
/// ```
|
||
/// let items: tek::MenuItems = Default::default();
|
||
/// ```
|
||
#[derive(Debug, Clone, Default, PartialEq)] pub struct MenuItems(
|
||
pub Arc<[MenuItem]>
|
||
);
|
||
|
||
/// An item of a menu.
|
||
///
|
||
/// ```
|
||
/// let item: tek::MenuItem = Default::default();
|
||
/// ```
|
||
#[derive(Clone)] pub struct MenuItem(
|
||
/// Label
|
||
pub Arc<str>,
|
||
/// Callback
|
||
pub Arc<Box<dyn Fn(&mut App)->Usually<()> + Send + Sync>>
|
||
);
|
||
|
||
/// 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 create
|
||
#[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,
|
||
}
|
||
|
||
pub type SceneWith<'a, T> =
|
||
(usize, &'a Scene, usize, usize, T);
|
||
|
||
/// 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>>>>;
|
||
|
||
pub trait HasClipsSize { fn clips_size (&self) -> &Measure<Tui>; }
|
||
|
||
pub trait HasDevices: AsRef<Vec<Device>> + AsMut<Vec<Device>> {
|
||
fn devices (&self) -> &Vec<Device> { self.as_ref() }
|
||
fn devices_mut (&mut self) -> &mut Vec<Device> { self.as_mut() }
|
||
}
|
||
pub trait HasWidth {
|
||
const MIN_WIDTH: usize;
|
||
/// Increment track width.
|
||
fn width_inc (&mut self);
|
||
/// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH].
|
||
fn width_dec (&mut self);
|
||
}
|
||
|
||
/// A control axis.
|
||
///
|
||
/// ```
|
||
/// let axis = tek::ControlAxis::X;
|
||
/// ```
|
||
#[derive(Debug, Copy, Clone)] pub enum ControlAxis {
|
||
X, Y, Z, I
|
||
}
|
||
|
||
/// Various possible dialog modes.
|
||
///
|
||
/// ```
|
||
/// let dialog: tek::Dialog = Default::default();
|
||
/// ```
|
||
#[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog {
|
||
#[default] None,
|
||
Help(usize),
|
||
Menu(usize, MenuItems),
|
||
Device(usize),
|
||
Message(Arc<str>),
|
||
Browse(BrowseTarget, Arc<Browse>),
|
||
Options,
|
||
}
|
||
|
||
/// A device that can be plugged into the chain.
|
||
///
|
||
/// ```
|
||
/// let device = tek::Device::default();
|
||
/// ```
|
||
#[derive(Debug, Default)] pub enum Device {
|
||
#[default]
|
||
Bypass,
|
||
Mute,
|
||
#[cfg(feature = "sampler")]
|
||
Sampler(Sampler),
|
||
#[cfg(feature = "lv2")] // TODO
|
||
Lv2(Lv2),
|
||
#[cfg(feature = "vst2")] // TODO
|
||
Vst2,
|
||
#[cfg(feature = "vst3")] // TODO
|
||
Vst3,
|
||
#[cfg(feature = "clap")] // TODO
|
||
Clap,
|
||
#[cfg(feature = "sf2")] // TODO
|
||
Sf2,
|
||
}
|
||
|
||
/// Some sort of wrapper?
|
||
pub struct DeviceAudio<'a>(pub &'a mut Device);
|
||
|
||
/// Command-line configuration.
|
||
#[cfg(feature = "cli")] impl Cli {
|
||
pub fn run (&self) -> Usually<()> {
|
||
if let Action::Version = self.action {
|
||
return Ok(tek_show_version())
|
||
}
|
||
|
||
let mut config = Config::new(None);
|
||
config.init()?;
|
||
|
||
if let Action::Config = self.action {
|
||
tek_print_config(&config);
|
||
} else if let Action::List = self.action {
|
||
todo!("list sessions")
|
||
} else if let Action::Resume = self.action {
|
||
todo!("resume session")
|
||
} else if let Action::New {
|
||
name, bpm, tracks, scenes, sync_lead, sync_follow,
|
||
midi_from, midi_from_re, midi_to, midi_to_re,
|
||
left_from, right_from, left_to, right_to, ..
|
||
} = &self.action {
|
||
|
||
// Connect to JACK
|
||
let name = name.as_ref().map_or("tek", |x|x.as_str());
|
||
let jack = Jack::new(&name)?;
|
||
|
||
// 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 clock = Clock::new(&jack, *bpm)?;
|
||
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<_>, _>>()?
|
||
);
|
||
project.tracks_add(tracks.unwrap_or(0), None, &[], &[])?;
|
||
project.scenes_add(scenes.unwrap_or(0))?;
|
||
|
||
if matches!(self.action, Action::Status) {
|
||
// Show status and exit
|
||
tek_print_status(&project);
|
||
return Ok(())
|
||
}
|
||
|
||
// Initialize the app state
|
||
let app = tek(&jack, project, config, ":menu");
|
||
if matches!(self.action, 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(())
|
||
}
|
||
}
|
||
|
||
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");
|
||
/// 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()
|
||
}
|
||
}
|
||
|
||
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());
|
||
}))
|
||
}
|
||
}
|
||
|
||
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 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::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())
|
||
})?;
|
||
Ok(map)
|
||
}
|
||
}
|