tek/src/tek.rs
2026-10-05 16:47:07 +03:00

1247 lines
45 KiB
Rust

#![allow(clippy::unit_arg)]
pub use self::deps::*; mod deps;
/// Command-line entrypoint.
#[allow(unused)]
#[hotpath::main]
fn main () -> Usually<()> {
tengri::Tui::setup_panic();
#[cfg(not(feature = "cli"))]
let outcome = config_watch_run(main_plain, None);
#[cfg(feature = "cli")]
let outcome = config_watch_run(main_cli, None);
if let Err(e) = outcome {
println!("{e:#?}");
std::process::exit(1);
}
Ok(())
}
/// Start with empty project.
#[cfg(not(feature = "cli"))]
fn main_plain (config: Arc<Watch>) -> Usually<()> {
let name = "tek";
tengri::Tui::run_main(Jack::new_run(name, move|jack|{
let mode = "Menu";
let title = "untitled!";
let bpm = 74.;
let clock = Clock::new(&jack, Some(bpm))?;
let tracks = [];
let scenes = [];
let project = Project::create(
&jack,
title.into(),
clock,
tracks.into_iter(),
scenes.into_iter(),
connect_midi_ins(&jack, &"M", &[], None)?.into_iter(),
connect_midi_outs(&jack, &"M", &[], None)?.into_iter(),
[].into_iter().chain(connect_audio_ins(&jack, &"L", &[], None)?.into_iter())
.chain(connect_audio_ins(&jack, &"R", &[], None)?.into_iter()),
[].into_iter().chain(connect_audio_outs(&jack, &"L", &[], None)?.into_iter())
.chain(connect_audio_outs(&jack, &"R", &[], None)?.into_iter()),
);
let mut app = App::new(None, project, config, mode);
app.dialog = Dialog::welcome();
Ok(app)
})?)
}
/// Total application state.
///
/// ```
/// # use tek::*;
/// let mut app = App::default();
/// let _ = app.scenes_add_one(None, None).unwrap();
/// 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();
/// ```
#[derive(Default, Debug)]
#[namespace(Arc<[Connect]> tek_arc_array_connect)]
#[namespace(Arc<str> tek_arc_str)]
#[namespace(Color tek_color)]
#[namespace(Option<Arc<[Connect]>> tek_opt_arc_array_connect)]
#[namespace(Option<Arc<str>> tek_opt_arc_str)]
#[namespace(Option<ItemTheme> tek_opt_itemtheme)]
#[namespace(Option<bool> tek_opt_bool)]
#[namespace(Option<u16> tek_opt_u16)]
#[namespace(Option<u32> tek_opt_u32)]
#[namespace(Option<usize> tek_opt_usize)]
#[namespace(PathBuf tek_path_buf)]
#[namespace(Vec<Arc<str>> tek_vec_arc_str)]
#[namespace(bool tek_bool)]
#[namespace(f64 tek_f64)]
#[namespace(u16 tek_u16)]
#[namespace(usize tek_usize)]
#[namespace(ControlAxis ControlAxis::eval)]
#[namespace(Dialog Dialog::eval)]
#[namespace(ClipSlot ClipSlot::eval)]
#[namespace(ClipPool ClipPool::eval)]
#[namespace(SampleSlot SampleSlot::eval)]
#[namespace(SamplePool SamplePool::eval)]
#[namespace(ItemColor)]
#[namespace(ItemTheme)]
#[namespace(MidiClip)]
#[namespace(isize)]
#[namespace(u32)]
#[namespace(u8)]
pub struct App {
/// Exit flag
pub exit: Exit,
/// Base color.
pub color: ItemTheme,
/// Must not be dropped for the duration of the process
pub jack: Jack<'static>,
/// Display size
pub size: Sizer,
/// Performance counter
pub perf: PerfModel,
/// Configuration watcher
pub watch: Arc<Watch>,
/// Currently active modes
pub stack: Arc<RwLock<Vec<Frame>>>,
/// Undo history
pub history: Vec<(AppCmd, Option<AppCmd>)>,
/// Dialog overlay
pub dialog: Dialog,
/// Contains the currently edited musical arrangement
pub project: Project,
/// Error, if any
pub error: Arc<RwLock<Option<Arc<str>>>>,
/// Tracing guard
#[cfg(feature = "prof2")] pub guard: Option<tracing_flame::FlushGuard<std::io::BufWriter<std::fs::File>>>,
}
impl App {
/// Create a new application instance from a backend, project, config, and mode
///
/// ```
/// # use ::tek::{*, tengri::Jack};
/// let proj = Project::new_jack(&"test_tek").expect("failed to connect to jack");
/// let conf = include_str!("tek.edn");
/// let mode = vec!["Arranger".into(), "Menu".into()];
/// let tek = config_reload(App::new(None, proj, None, mode), conf).unwrap();
/// ```
pub fn new (
exit: Option<Exit>,
project: Project,
watch: impl Into<Option<Arc<Watch>>>,
modes: impl Into<Option<Vec<Frame>>>,
) -> Self {
App {
exit: exit.unwrap_or_default(),
jack: project.jack.clone(),
color: ItemTheme::random(),
dialog: Dialog::welcome(),
stack: Arc::new(RwLock::new(modes.into().unwrap_or_default())),
watch: watch.into().unwrap_or_default(),
project,
..Default::default()
}
}
}
pub use self::conf::*; mod conf {
use crate::*;
mod axis; pub use self::axis::*;
mod bind; pub use self::bind::*;
mod lang; pub use self::lang::*;
mod mode; pub use self::mode::*;
mod stack; pub use self::stack::*;
mod view; pub use self::view::*;
pub trait HasError<T> {
fn error (&self) -> &RwLock<Option<T>>;
fn error_set (&self, e: Option<T>) {
*self.error().write() = e;
}
}
impl<T: AsRef<Config>> HasError<Arc<str>> for T {
fn error (&self) -> &RwLock<Option<Arc<str>>> {
&self.as_ref().error
}
}
impl HasError<Arc<str>> for Arc<Watch> {
fn error (&self) -> &RwLock<Option<Arc<str>>> {
&self.as_ref().config.error
}
}
pub trait HasConfig {
fn config (&self) -> &Config;
/// Print the configuration.
fn config_print (&self) {
let config = self.config();
print_config_views(config);
print_config_binds(config);
print_config_modes(config);
}
/// Make this configuration empty.
fn config_clear (&self) {
let config = self.config();
config.modes_clear();
config.views_clear();
config.binds_clear();
config.stamp.store(quanta::Clock::new().raw(), Relaxed);
}
}
impl HasConfig for App {
fn config (&self) -> &Config {
&self.watch.config
}
}
impl HasConfig for Watch {
fn config (&self) -> &Config {
&self.config
}
}
impl HasConfig for Config {
fn config (&self) -> &Config {
self
}
}
/// Default configuration contents.
const CONFIG_DEFAULT: &'static str = include_str!("tek.edn");
/// Create, initialize, and watch a new configuration.
pub fn config_watch_run <T: 'static> (
call: impl FnOnce(Arc<Watch>)->T,
dirs: impl Into<Option<BaseDirectories>>,
) -> Usually<T> {
let config = Arc::new(Watch::new(dirs.into()));
config_init(config.as_ref())?;
let watcher = config.clone().watch_config(None)?;
*config.watcher.write() = Some(watcher);
let result = call(config);
Ok(result)
}
pub fn config_init <C: AsRef<Watch> + AsRef<Config>> (config: C) -> Usually<C> {
// Write initial contents of configuration:
let watcher: &Watch = config.as_ref();
if watcher.find_file().is_none() {
std::fs::write(watcher.place_file()?, CONFIG_DEFAULT)?;
}
// Load config from file:
Ok(if let Some(path) = watcher.find_file() {
let source = std::fs::read_to_string(&path)?;
config_reload(config, source)?
} else {
return Err(format!("config_init: not found").into())
})
}
/// Reload configuration from source.
pub fn config_reload <C: AsRef<Config>, L: Language> (state: C, src: L) -> Usually<C> {
state.as_ref().config_clear();
src.each(state, |c, s|config_add(c, s.trim()))
}
/// Load configuration item.
fn config_add <C: AsRef<Config>, L: Language> (state: C, src: L) -> Usually<C> {
if let Some(expr) = src.expr()? {
match expr.head()? {
Some("mode") => state.as_ref().modes_add(expr.tail()),
Some("view") => state.as_ref().views_add(expr.tail()),
Some("bind") => state.as_ref().binds_def_keys(expr.tail()),
Some("axis") => state.as_ref().binds_def_axis(expr.tail()),
_ => return Err(format!("config_add: unexpected: {src:?}").into())
}?;
Ok(state)
} else {
Err(format!("Config::add_one: tried to add empty item").into())
}
}
/// Contains timestamped mode, view, and bind definitions.
///
/// ```
/// # use ::{std::sync::Arc, tek::*};
///
/// // Default empty config:
/// let config: Config = Default::default();
///
/// // Define a binding:
/// config.binds_def_keys(stringify!(Confirm (@enter confirm))).unwrap();
///
/// // Define an axis:
/// config.binds_def_axis(stringify!(Y @up @down)).unwrap();
/// ```
#[derive(Default, Debug)]
pub struct Config {
/// Error caught during (re)loading
pub error: RwLock<Option<Arc<str>>>,
/// Timestamp
pub stamp: AtomicU64,
/// Active collection of interaction modes.
pub modes: Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode>>>>,
/// Active collection of view definitions.
pub views: Arc<RwLock<BTreeMap<Arc<str>, Arc<View<Tui, App>>>>>,
/// Active collection of key bindings.
pub keys: Arc<RwLock<BTreeMap<Arc<str>, Arc<Bind<TuiEvent, Arc<str>>>>>>,
/// Active collection of axis bindings.
pub axis: Arc<RwLock<BTreeMap<Arc<str>, Arc<Axis<TuiEvent>>>>>,
}
impl Config {
/// Create a new timestamped config state.
pub fn new () -> Self {
Self { stamp: quanta::Clock::new().raw().into(), ..Default::default() }
}
}
}
/// Configuration watcher and manager.
///
/// ```
/// let config = tek::Watch::new(None);
/// ```
#[derive(Default, Debug)]
pub struct Watch {
/// XDG base directories of running user.
pub dirs: BaseDirectories,
/// File system notifier
pub watcher: RwLock<Option<PollWatcher>>,
/// App configuration
pub config: Config,
}
impl Watch {
/// Default configuration directory.
const DIR: &'static str = "tek";
/// Default configuration subdirectory.
const SUB: &'static str = "v0";
/// Default configuration file name.
const EDN: &'static str = "tek.edn";
/// Create a new watched app configuration
/// from a set of XDG base directories,
pub fn new (dirs: Option<BaseDirectories>) -> Self {
let dirs = dirs.unwrap_or_else(||BaseDirectories::with_profile(Self::DIR, Self::SUB));
Self { dirs, ..Default::default() }
}
/// Find the config file.
fn find_file (&self) -> Option<PathBuf> {
self.dirs.find_config_file(Self::EDN)
}
/// Place config file in default location.
fn place_file (&self) -> Result<PathBuf, std::io::Error> {
self.dirs.place_config_file(Self::EDN)
}
/// Get path to config file.
fn get_file (&self) -> Option<PathBuf> {
self.dirs.get_config_file(Self::EDN)
}
/// Watch a config's file for changes.
pub fn watch_config (self: Arc<Self>, poll: Option<Duration>) -> Usually<PollWatcher> {
let poll = poll.unwrap_or(Duration::from_millis(250));
let opts = NotifyConfig::default().with_poll_interval(poll);
let call = self.clone().get_watch_config_callback();
let mut watcher = PollWatcher::new(call, opts)?;
if let Some(path) = self.get_file() {
watcher.watch(&path, RecursiveMode::NonRecursive)?;
Ok(watcher)
} else {
Err(format!("no config path").into())
}
}
fn get_watch_config_callback (self: Arc<Self>) -> impl Fn(Result<
notify_debouncer_full::notify::Event,
notify_debouncer_full::notify::Error,
>) {
let config = self.clone();
move|result|{
match result {
Ok(_events) => if let Err(e) = config_init(config.as_ref()) {
config.error_set(Some(format!("{e:?}").into()));
panic!("{e:?}");
} else {
//println!("config updated");
},
Err(errors) => {
panic!("{errors:?}");
}
}
}
}
}
fn tek_jack_process (state: &mut App, client: &Client, scope: &ProcessScope) -> Control {
let t0 = state.perf.get_t0();
state.clock().update_from_scope(scope).unwrap();
let midi_in = state.project.midi_input_collect(scope);
if let Some(editor) = &state.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 = state.project.tracks_jack_process(client, scope);
state.perf.update_from_jack_scope(t0, scope);
result
}
fn tek_jack_event (state: &mut App, event: JackEvent) {
use JackEvent::*;
match event {
SampleRate(sr) => { state.clock().timebase.sr.set(sr as f64); },
PortRegistration(_id, true) => {
//let port = self.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 use self::gear::*; mod gear {
use crate::*;
mod device; pub use self::device::*;
mod midi; pub use self::midi::*;
mod mixer; pub use self::mixer::*;
mod sample; pub use self::sample::*;
#[cfg(feature = "vst2")] mod vst2;
#[cfg(feature = "vst2")] pub use self::vst2::*;
#[cfg(feature = "lv2")] mod lv2;
#[cfg(feature = "lv2")] pub use self::lv2::*;
#[cfg(feature = "lv2_gui")] mod lv2_gui;
#[cfg(feature = "lv2_gui")] pub use self::lv2_gui::*;
pub struct Junction<T: JackPort>(T);
impl<T: JackPort> Draw<Tui> for Junction<T> {
fn draw (&self, to: &mut Tui) -> Drawn<u16> {
T::KIND.draw(to)
}
}
}
pub use self::proj::*; mod proj {
use crate::*;
mod pool; pub use self::pool::*;
mod scene; pub use self::scene::*;
mod select; pub use self::select::*;
mod track; pub use self::track::*;
mod time; pub use self::time::*;
pub trait HasProject: AsRef<Option<Project>> + AsMut<Option<Project>> {}
/// Has a loaded project.
impl<T> HasProject for T where T: AsRef<Option<Project>> + AsMut<Option<Project>> {}
/// Arranger.
///
/// ```
/// let arranger = tek::Project::default();
/// ```
#[namespace(ClipPool ClipPool::eval)]
#[derive(Default, Debug)]
pub struct Project {
/// JACK client handle.
pub jack: Jack<'static>,
/// Project name.
pub name: Arc<str>,
/// Base color.
pub color: ItemTheme,
/// FIXME a render of the project arrangement, redrawn on update.
/// TODO rename to "render_cache" or smth
pub arranger: Arc<RwLock<Buffer>>,
/// Display size
pub size: Sizer,
/// Display size of clips area
pub size_inner: Sizer,
/// Selected UI element
pub selection: Selection,
/// Contains all recently created clips.
#[cfg(feature = "sequencer")] pub clips: Pool<MidiClip>,
/// Contains all recently created clips.
#[cfg(feature = "sampler")] pub samples: Pool<Sample>,
/// Source of time
#[cfg(feature = "clock")] pub clock: Clock,
/// Allows one MIDI clip to be edited
#[cfg(feature = "editor")] pub editor: Option<MidiEditor>,
/// List of global midi inputs
#[cfg(feature = "port")] pub midi_ins: Vec<MidiInput>,
/// List of global midi outputs
#[cfg(feature = "port")] pub midi_outs: Vec<MidiOutput>,
/// List of global audio inputs
#[cfg(feature = "port")] pub audio_ins: Vec<AudioInput>,
/// List of global audio outputs
#[cfg(feature = "port")] pub audio_outs: Vec<AudioOutput>,
/// Last track number (to avoid duplicate port names)
#[cfg(feature = "track")] pub track_last: usize,
/// List of tracks
#[cfg(feature = "track")] pub tracks: Vec<Track>,
/// Scroll offset of tracks
#[cfg(feature = "track")] pub track_scroll: usize,
/// List of scenes
#[cfg(feature = "scene")] pub scenes: Vec<Scene>,
/// Scroll offset of scenes
#[cfg(feature = "scene")] pub scenes_scroll: usize,
}
impl Project {
pub fn new (jack: &Jack<'static>) -> Self {
Self { jack: jack.clone(), ..Default::default() }
}
pub fn new_jack (name: impl AsRef<str>) -> Usually<Self> {
let name = name.as_ref();
Ok(Self { name: name.into(), jack: Jack::new(name)?, ..Default::default() })
}
/// Create a new arrangement.
pub fn create (
jack: &Jack<'static>,
name: Arc<str>,
clock: Clock,
tracks: impl Iterator<Item = Track>,
scenes: impl Iterator<Item = Scene>,
midi_ins: impl Iterator<Item = MidiInput>,
midi_outs: impl Iterator<Item = MidiOutput>,
audio_ins: impl Iterator<Item = AudioInput>,
audio_outs: impl Iterator<Item = AudioOutput>,
) -> Self {
Self {
jack: jack.clone(),
color: ItemTheme::random(),
selection: Selection::TrackClip { track: 0, scene: 0 },
tracks: tracks.collect(),
scenes: scenes.collect(),
midi_ins: midi_ins.collect(),
midi_outs: midi_outs.collect(),
audio_ins: audio_ins.collect(),
audio_outs: audio_outs.collect(),
size_inner: Sizer(Arc::new(40.into()), Arc::new(25.into())),
clock,
name,
..Default::default()
}
}
}
impl HasJack<'static> for Project {
fn jack (&self) -> &Jack<'static> {
&self.jack
}
}
impl HasClip for Project {
fn clip_slot (&self) -> Option<Slot<MidiClip>> {
match self.selection {
Selection::Clip(index) =>
Some(self.clips.slot(index)),
Selection::TrackClip { track, scene } if let Some(scene) = self.scenes.get(scene) =>
Some(scene.clips.slot(track)),
_ =>
None
}
}
}
impl HasClipsSize for Project {
fn clips_size (&self) -> &Sizer {
&self.size_inner
}
}
#[cfg(feature = "cli")]
/// Command to launch into new project from CLI.
#[derive(Debug, Clone, Parser, Default)]
pub struct ProjectInit {
/// 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>,
}
#[cfg(feature = "cli")]
impl ProjectInit {
pub fn init (&self) -> Usually<Project> {
let name = self.name.as_ref().map_or("tek", |x|x.as_str());
let jack = Jack::new(&name)?;
let midi_ins = connect_midi_ins(&jack, &"M".to_string(), &self.midi_from, Some(&self.midi_from_re))?;
let midi_out = connect_midi_outs(&jack, &"M".to_string(), &self.midi_to, Some(&self.midi_to_re))?;
let inputs = [].into_iter()
.chain(connect_audio_ins(&jack, &"L".to_string(), &self.left_from, None)?.into_iter())
.chain(connect_audio_ins(&jack, &"R".to_string(), &self.right_from, None)?.into_iter());
let outputs = [].into_iter()
.chain(connect_audio_outs(&jack, &"L".to_string(), &self.left_to, None)?.into_iter())
.chain(connect_audio_outs(&jack, &"R".to_string(), &self.right_to, None)?.into_iter());
let mut proj = Project::create(&jack, name.into(), Clock::new(&jack, self.bpm)?,
[].into_iter(), [].into_iter(), midi_ins.into_iter(), midi_out.into_iter(),
inputs, outputs);
proj.tracks_add_many(self.tracks.unwrap_or(0), None, [].into(), [].into())?;
proj.scenes_add_many(self.scenes.unwrap_or(0))?;
Ok(proj)
}
}
}
#[cfg(feature = "cli")] pub use self::cli::*;
#[cfg(feature = "cli")] mod cli {
use super::*;
/// Banner.
pub(crate) const HEADER: &'static str = r#"
~ █▀█▀█ █▀▀█ █ █ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~
█ █▀ █▀▀▄ ~ heatwave is the new darkwave ~
~ ▀ █▀▀█ ▀ ▀ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#;
pub fn show_version () {
println!("versions aint real man");
}
pub fn main_cli (config: Arc<Watch>) -> Usually<()> {
Cli::parse().run(Some(config.into()))
}
/// The command-line interface descriptor.
///
/// ```
/// let cli: tek::Cli = Default::default();
///
/// use clap::CommandFactory;
/// tek::Cli::command().debug_assert();
/// ```
#[derive(Parser, Debug, Default)]
#[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))]
pub struct Cli {
/// Pre-defined configuration modes.
///
/// TODO: Replace these with scripted configurations.
#[command(subcommand)]
pub action: Action,
/// Record data for performance flamegraph.
#[arg(long)]
trace: bool,
}
/// Command-line configuration.
impl Cli {
pub fn run (&self, mut config: Option<Arc<Watch>>) -> Usually<()> {
self.action.run(
config.unwrap_or_else(||Arc::new(Watch::new(None))),
self.trace
)
}
}
/// 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(ProjectInit),
/// Import media as new session.
Import,
/// Show configuration.
Config,
/// Show version.
Version,
}
impl Action {
fn run (&self, watch: Arc<Watch>, _trace: bool) -> Usually<()> {
use Action::*;
match self {
Version => show_version(),
Config => watch.config_print(),
Resume => todo!("resume session"),
List => todo!("list sessions"),
New(sesh) => Exit::run(|exit|Tui::run_main(
exit.clone(),
{
#[allow(unused_mut)]
let mut app = App::new(Some(exit), sesh.init()?, watch, Some(vec![
"Menu".into()
]));
app.dialog = Dialog::welcome();
#[cfg(feature = "prof2")] {
if trace {
app.guard = Some({
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
//let tracy = tracing_tracy::TracyLayer::default();
let (flame, _guard) = tracing_flame::FlameLayer::with_file("./tracing.folded").unwrap();
//let registry = tracing_subscriber::registry().with(flame).init();
//tracing::subscriber::set_global_default(registry)?;
tracing::subscriber::set_global_default(tracing_subscriber::registry().with(flame))?;
_guard
})
}
}
Arc::new(RwLock::new(app))
})).map(|_|())?,
_ => todo!()
}
Ok(())
}
}
pub fn print_status (project: &Project) {
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
}
}
impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } }
/// 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 {}
impl<'a, T: Iterator<Item=(usize, $(&'a $Item,)+ usize, usize)> + Send + Sync + 'a> $Type<'a> for T {}
}
}
def_sizes_iter!(ScenesSizes => Scene);
def_sizes_iter!(TracksSizes => Track);
def_sizes_iter!(PortsSizes => Arc<str>, [Connect]);
impl_default!(Timebase: Self::new(48000f64, 150f64, DEFAULT_PPQ));
primitive!(isize: try_to_isize);
primitive!(u16: try_to_u16);
primitive!(u8: try_to_u8);
primitive!(usize: try_to_usize);
impl_as_ref!(Config: |self: Config| self);
impl_as_ref!(Config: |self: Watch| &self.config);
impl_as_mut!(Config: |self: Watch| &mut self.config);
impl_as_ref!(Watch: |self: Watch| &self);
impl_debug!(MenuItem: |self, w| { write!(w, "{}", &self.0) });
impl_default!(MenuItem: Self::new("", []));
impl_as_mut!(Option<MidiEditor>: |self: Project| &mut self.editor);
impl_as_mut_opt!(Scene: |self: Project| self.selected_scene_mut());
impl_as_mut_opt!(Track: |self: Project| self.selected_track_mut());
impl_as_ref!(Option<MidiEditor>: |self: Project| &self.editor);
impl_as_ref_opt!(Scene: |self: Project| self.selected_scene());
impl_as_ref_opt!(Track: |self: Project| self.selected_track());
impl_has!(Jack<'static>: |self: Project| self.jack);
impl_has!(Selection: |self: Project| self.selection);
impl_has!(Sizer: |self: Project| self.size);
impl_has!(Vec<MidiInput>: |self: Project| self.midi_ins);
impl_has!(Vec<MidiOutput>: |self: Project| self.midi_outs);
impl_as_ref!(Config: |self: App|self.watch.config.as_ref());
impl_as_mut!(Option<MidiEditor>: |self: App|self.project.as_mut());
impl_as_mut!(Option<Project>: |self: App|todo!());
impl_as_ref!(Option<MidiEditor>: |self: App|self.project.as_ref());
impl_as_ref!(Option<Project>: |self: App|todo!());
impl_as_mut_opt!(Scene: |self: App|self.project.as_mut_opt());
impl_as_mut_opt!(Track: |self: App|self.project.as_mut_opt());
impl_as_ref_opt!(Scene: |self: App|self.project.as_ref_opt());
impl_as_ref_opt!(Track: |self: App|self.project.as_ref_opt());
impl_audio!(App: tek_jack_process, tek_jack_event);
impl_has!(Dialog: |self: App|self.dialog);
impl_has!(Jack<'static>: |self: App|self.jack);
impl_has!(Pool<MidiClip>: |self: App|self.project.clips);
impl_has!(Pool<Sample>: |self: App|self.project.samples);
impl_has!(Selection: |self: App|self.project.selection);
impl_has!(Sizer: |self: App|self.size);
impl_has!(Vec<MidiInput>: |self: App|self.project.midi_ins);
impl_has!(Vec<MidiOutput>: |self: App|self.project.midi_outs);
impl_from!(AppCmd: |x: BrowseCmd| AppCmd::Browse { command: x });
impl_from!(AppCmd: |x: MidiClipCmd| AppCmd::MidiClip { command: x });
impl_from!(AppCmd: |x: MidiEditCmd| AppCmd::MidiEdit { command: x });
impl_from!(AppCmd: |x: ProjectCmd| AppCmd::Project { command: x });
impl_from!(AppCmd: |x: SceneCmd| AppCmd::Scene { command: x });
impl_from!(AppCmd: |x: ScenesCmd| AppCmd::Scenes { command: x });
impl_from!(AppCmd: |x: TrackCmd| AppCmd::Track { command: x });
impl_from!(AppCmd: |x: TracksCmd| AppCmd::Tracks { command: x });
tui_keys!(self: App, input {
#[cfg(feature = "prof2")] profiling::scope!("App::tui_keys!");
self.dispatch_bind(input)
});
/// The [Draw] implementation for [App] handles the loaded view,
/// which is defined in terms of [dizzle] DSL.
///
/// If there is an error, the error is displayed. FIXME: overlay it
/// Then, every top-level form of the DSL description is rendered.
impl Draw<Tui> for App {
fn draw (&self, to: &mut Tui) -> Drawn<u16> {
#[cfg(feature = "prof2")] profiling::scope!("App::draw");
self.perf.cycle(&mut |_|{
draw_error(to, self.error.as_ref())?;
draw_stack(to, &self)?;
view_debug(&*self.stack.read(), &self.perf, &self.size).draw(to)?;
#[cfg(feature = "prof2")] profiling::finish_frame!();
Ok(Some(to.area().into()))
})
}
}
pub trait DispatchBind<C: Clone>: HasBinds + HasStack + HasModes + Namespace<C> {
fn dispatch_bind (&mut self, input: &TuiEvent) -> Usually<()> {
if let Some(bindings) = self.dispatch_bind_match(input) {
return self.dispatch_bind_exec(bindings)
}
Ok(())
}
/// Find command bindings that match the ,nput.
/// ```
/// # use ::{std::sync::Arc, tek::*};
/// // Configure modes:
/// let app = config_reload(App::default(), stringify!(
/// (axis Y @up @down)
/// (bind Confirm (@enter confirm))
/// (mode Test (axis Y Index) (bind (@escape Cancel) Confirm))
/// )).unwrap();
/// println!("Modes: {:#?}", app.watch.config.modes);
///
/// // No mode is active:
/// assert!(app.dispatch_bind_match(&"@left".into()).is_none());
/// assert!(app.dispatch_bind_match(&"@enter".into()).is_none());
/// assert!(app.dispatch_bind_match(&"@escape".into()).is_none());
/// assert!(app.dispatch_bind_match(&"@down".into()).is_none());
/// assert!(app.dispatch_bind_match(&"@up".into()).is_none());
///
/// // Push mode onto stack:
/// app.stack_push("Test");
/// assert!(app.dispatch_bind_match(&"@left".into()).is_none());
/// assert!(app.dispatch_bind_match(&"@enter".into()).is_some());
/// assert!(app.dispatch_bind_match(&"@escape".into()).is_some());
/// assert!(app.dispatch_bind_match(&"@down".into()).is_some());
/// assert!(app.dispatch_bind_match(&"@up".into()).is_some());
/// ```
fn dispatch_bind_match (&self, input: &TuiEvent) -> Option<Arc<[Binding<Arc<str>>]>> {
for Frame { name, data } in self.stack().read().iter().rev() {
if let Some(mode) = self.modes_get(name).as_ref().map(Arc::clone) {
if let Some(binds) = self.dispatch_bind_match_axis(mode.as_ref(), input) {
return Some(binds)
}
if let Some(binds) = self.dispatch_bind_match_keys(mode.as_ref(), input) {
return Some(binds)
}
}
}
None
}
/// ```
/// # use ::{std::sync::Arc, tek::*};
/// let app = config_reload(App::default(), stringify!(
/// (axis Y @up @down) (mode Test (axis Y Index))
/// )).unwrap();
/// assert!(app.dispatch_bind_match(&"@down".into()).is_none());
/// assert!(app.dispatch_bind_match(&"@up".into()).is_none());
/// app.stack_push("Test");
/// assert!(app.dispatch_bind_match(&"@down".into()).is_some());
/// assert!(app.dispatch_bind_match(&"@up".into()).is_some());
/// ```
fn dispatch_bind_match_axis (&self, mode: &Mode, input: &TuiEvent)
-> Option<Arc<[Binding<Arc<str>>]>>
{
let axis = self.binds_axis().read();
for (name, _param) in mode.axis.iter() {
if let Some(axis) = axis.get(name) {
if *input == axis.0 {
return Some([Binding::new([format!("(dec {name})").into()].into())].into())
} else if *input == axis.1 {
return Some([Binding::new([format!("(inc {name})").into()].into())].into())
}
}
}
None
}
/// ```
/// # use ::{std::sync::Arc, tek::*};
/// let app = config_reload(App::default(), stringify!(
/// (bind Confirm (@enter confirm)) (mode Test (bind Confirm (@escape Cancel)))
/// )).unwrap();
/// assert!(app.dispatch_bind_match(&"@enter".into()).is_none());
/// assert!(app.dispatch_bind_match(&"@escape".into()).is_none());
/// app.stack_push("Test");
/// assert!(app.dispatch_bind_match(&"@enter".into()).is_some());
/// assert!(app.dispatch_bind_match(&"@escape".into()).is_some());
/// ```
fn dispatch_bind_match_keys (&self, mode: &Mode, input: &TuiEvent)
-> Option<Arc<[Binding<Arc<str>>]>>
{
for id in mode.keys.iter() {
if let Some(event_map) = self.binds_find(id)
&& let Some(bindings) = event_map.query(input) {
return Some(bindings.into())
}
if let Ok(Some(expr)) = id.expr()
&& let Ok(Some(head)) = expr.head()
&& *input == head.into()
&& let Ok(Some(tail)) = expr.tail() {
return Some([Binding::new([tail.into()].into())].into())
}
}
None
}
fn dispatch_bind_exec (&mut self, bindings: Arc<[Binding<Arc<str>>]>) -> Usually<()> {
for binding in bindings.iter() {
for src in binding.commands.iter() {
if let Some(command) = Namespace::<C>::namespace(self, &src)? {
self.cmd_dispatch(command)?;
}
}
}
Ok(())
}
fn cmd_dispatch (&mut self, command: C) -> Usually<()>;
}
impl DispatchBind<AppCmd> for App {
fn cmd_dispatch (&mut self, command: AppCmd) -> Usually<()> {
match command.clone().dispatch(self) {
Ok(undo) => { self.history.push((command, undo)); Ok(()) },
Err(err) => { self.history.push((command, None)); Err(err) },
}
}
}
/// An item of a menu.
///
/// ```
/// let item: tek::MenuItem = Default::default();
/// ```
#[derive(Clone)] pub struct MenuItem(pub Arc<str>, pub Arc<[AppCmd]>);
impl PartialEq for MenuItem {
fn eq (&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl MenuItem {
pub fn new <const N: usize> (name: impl AsRef<str>, actions: [AppCmd; N]) -> Self {
Self(name.as_ref().into(), Arc::new(actions))
}
}
/// Various possible dialog modes.
///
/// ```
/// let dialog: tek::Dialog = Default::default();
/// ```
#[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog {
#[default] None,
Help(usize),
Menu(usize, Arc<[MenuItem]>),
Device(usize),
Message(Arc<str>),
Browse(BrowseTarget, Arc<Browse>),
Options,
}
impl Dialog {
/// ```
/// let _ = tek::Dialog::welcome();
/// ```
pub fn welcome () -> Self {
Self::Menu(1, Arc::new([
MenuItem::new(" Resume session ", [
AppCmd::Project { command: ProjectCmd::LoadLast }
]),
MenuItem::new(" Create session ", [
AppCmd::Dialog { dialog: Dialog::None },
AppCmd::Mode { modes: vec!["Arranger".into()] },
AppCmd::Tracks { command: TracksCmd::Add },
AppCmd::Scenes { command: ScenesCmd::Add },
AppCmd::MidiClip { command: MidiClipCmd::Upsert },
AppCmd::MidiEdit { command: MidiEditCmd::Toggle },
]),
MenuItem::new(" Load session ", [
AppCmd::Dialog { dialog: Dialog::Browse(
BrowseTarget::LoadProject,
Browse::new(None).unwrap().into()
) }
]),
MenuItem::new(" Exit ", [
AppCmd::Exit
]),
]))
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().menu_selected();
/// ```
pub fn menu_selected (&self) -> Option<usize> {
if let Self::Menu(selected, _) = self { Some(*selected) } else { None }
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().menu_next();
/// ```
pub fn menu_next (&self) -> Self {
match self {
Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.len()), items.clone()),
_ => Self::None
}
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().menu_prev();
/// ```
pub fn menu_prev (&self) -> Self {
match self {
Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.len()), items.clone()),
_ => Self::None
}
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().device_kind();
/// ```
pub fn device_kind (&self) -> Option<usize> {
if let Self::Device(index) = self { Some(*index) } else { None }
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().device_kind_next();
/// ```
pub fn device_kind_next (&self) -> Option<usize> {
self.device_kind().map(|index|(index + 1) % device_kinds().len())
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().device_kind_prev();
/// ```
pub fn device_kind_prev (&self) -> Option<usize> {
self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1)))
}
/// FIXME: implement
pub fn message (&self) -> Option<&str> { todo!() }
/// FIXME: implement
pub fn browser (&self) -> Option<&Arc<Browse>> { todo!() }
/// FIXME: implement
pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() }
}
impl App {
/// Return reference to content browser if open.
///
/// ```
/// assert_eq!(tek::App::default().browser(), None);
/// ```
pub fn browser (&self) -> Option<&Browse> {
if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None }
}
}
/// Browses for files to load/save.
///
/// ```
/// let browse = tek::Browse::default();
/// ```
#[derive(Debug, Clone, Default, PartialEq)] pub struct Browse {
pub cwd: PathBuf,
pub dirs: Vec<(OsString, String)>,
pub files: Vec<(OsString, String)>,
pub filter: String,
pub index: usize,
pub scroll: usize,
pub size: Sizer,
}
#[derive(Clone, Debug)] pub enum BrowseTarget {
SaveProject,
LoadProject,
ImportSample(Arc<RwLock<Option<Sample>>>),
ExportSample(Arc<RwLock<Option<Sample>>>),
ImportClip(Arc<RwLock<Option<MidiClip>>>),
ExportClip(Arc<RwLock<Option<MidiClip>>>),
}
impl Browse {
pub fn new (cwd: Option<PathBuf>) -> Usually<Self> {
let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? };
let mut dirs = vec![];
let mut files = vec![];
for entry in std::fs::read_dir(&cwd)? {
let entry = entry?;
let name = entry.file_name();
let decoded = name.clone().into_string().unwrap_or_else(|_|"<unreadable>".to_string());
let meta = entry.metadata()?;
if meta.is_dir() {
dirs.push((name, format!("📁 {decoded}")));
} else if meta.is_file() {
files.push((name, format!("📄 {decoded}")));
}
}
Ok(Self { cwd, dirs, files, ..Default::default() })
}
pub fn to_chdir (&self) -> Usually<Self> {
Self::new(Some(self.path()))
}
pub fn len (&self) -> usize {
self.dirs.len() + self.files.len()
}
pub fn is_dir (&self) -> bool {
self.index < self.dirs.len()
}
pub fn is_file (&self) -> bool {
self.index >= self.dirs.len()
}
pub fn path (&self) -> PathBuf {
self.cwd.join(if self.is_dir() {
&self.dirs[self.index].0
} else if self.is_file() {
&self.files[self.index - self.dirs.len()].0
} else {
unreachable!()
})
}
//fn tui_entries (&self) -> EntriesIterator<'_, Tui> {
//EntriesIterator {
//offset: 0,
//index: 0,
//length: self.dirs.len() + self.files.len(),
//browser: self,
//_screen: Default::default(),
//}
//}
}
impl PartialEq for BrowseTarget {
fn eq (&self, other: &Self) -> bool {
match self {
Self::ImportSample(_) => false,
Self::ExportSample(_) => false,
Self::ImportClip(_) => false,
Self::ExportClip(_) => false,
#[allow(unused)] t => matches!(other, t)
}
}
}
pub 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))
}