tek/src/tek.rs
i do not exist 914e3e094b
Some checks are pending
/ build (push) Waiting to run
no first load of config?
2026-10-05 02:48:44 +03:00

1797 lines
62 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#![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 = Watch::run_watched(main_plain, None);
#[cfg(feature = "cli")]
let outcome = Watch::run_watched(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();
/// 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)]
#[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,
/// Available view modes and input bindings
pub watch: Arc<Watch>,
/// Currently active modes
pub stack: Arc<RwLock<Vec<Arc<str>>>>,
/// 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 tek = config_reload(App::new(None, proj, None, "Arranger"), stringify!(
/// (mode Arranger (name Arranger) (info Launch grid.)
/// (axis X Track)
/// (axis Y Scene)
/// (bind Global Clock Tracks Scenes Clip Launch Color History)
/// (view (bar/n 2 StatusBar (bar/s 8 (bar/e 16 (align/nw Transport)
/// (bar/w 20 (bar/s 2 (bar/w 4 TracksOutputsAdd TracksOutputsCount)
/// (bar/s 2 (bar/w 4 TracksDevicesAdd TracksDevicesCount)
/// (bar/s 2 (bar/w 4 TracksNamesAdd TracksNamesCount)
/// (bar/w 4 TracksInputsAdd TracksInputsCount))))
/// (align/x (bar/s 2 TracksOutputsCells
/// (bar/s 2 TracksDevicesCells
/// (bar/s 2 TracksNamesCells TracksInputsCells))))))
/// (bar/w 20 (text) (bar/e 16 ScenesNames ScenesClips))))))
/// )).unwrap();
/// ```
pub fn new (
exit: Option<Exit>,
project: Project,
watch: impl Into<Option<Arc<Watch>>>,
mode: impl AsRef<str>
) -> Self {
App {
exit: exit.unwrap_or_default(),
jack: project.jack.clone(),
color: ItemTheme::random(),
dialog: Dialog::welcome(),
stack: Arc::new(RwLock::new(vec![mode.as_ref().into()])),
watch: watch.into().unwrap_or_default(),
project,
..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,
/// Watcher
pub watch: 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 {
Self {
dirs: dirs.unwrap_or_else(||BaseDirectories::with_profile(Self::DIR, Self::SUB)),
..Default::default()
}
}
/// Create, initialize, and watch a new configuration.
pub fn run_watched <T: 'static> (
callback: impl FnOnce(Arc<Self>)->T,
dirs: impl Into<Option<BaseDirectories>>,
) -> Usually<T> {
let watch = Arc::new(Self::new(dirs.into()));
Self::watch(watch.clone(), None)?;
let result = callback(watch);
Ok(result)
}
/// Watch a config's file for changes.
pub fn watch (config: Arc<Self>, poll: Option<Duration>) -> Usually<()> {
let poll = poll.unwrap_or(Duration::from_millis(250));
let opts = NotifyConfig::default().with_poll_interval(poll);
let mut watcher = ::notify_debouncer_full::notify::poll::PollWatcher::new({
let config = config.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:?}");
}
}
}
}, opts)?;
if let Some(path) = config.get_file() {
//println!("watching: {path:?}");
watcher.watch(&path, RecursiveMode::NonRecursive)?;
*config.as_ref().watch.try_write().unwrap() = Some(watcher);
Ok(())
} else {
Err(format!("no config path").into())
}
}
/// 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)
}
}
/// 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,
}
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:?}"); }
}
}
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()
}
}
/// Width of display
pub fn w (&self) -> u16 {
self.size.w() as u16
}
/// Width allocated for sidebar.
pub fn w_sidebar (&self, is_editing: bool) -> u16 {
self.w() / if is_editing { 16 } else { 8 } as u16
}
/// Width available to display tracks.
fn w_tracks_area (&self, is_editing: bool) -> u16 {
self.w().saturating_sub(self.w_sidebar(is_editing))
}
/// Height of display
pub fn h (&self) -> u16 {
self.size.h() as u16
}
/// Height taken by visible device slots.
fn h_devices (&self) -> u16 {
2
//1 + self.devices_with_sizes().last().map(|(_, _, _, _, y)|y as u16).unwrap_or(0)
}
}
impl HasJack<'static> for App {
fn jack (&self) -> &Jack<'static> {
&self.jack
}
}
impl HasJack<'static> for Project {
fn jack (&self) -> &Jack<'static> {
&self.jack
}
}
impl ScenesView for Project {
fn h_scenes (&self) -> u16 {
(self.size.h() as u16).saturating_sub(20)
}
fn w_side (&self) -> u16 {
(self.size.w() as u16 * 2 / 10).max(20)
}
fn w_mid (&self) -> u16 {
(self.size.w() as u16).saturating_sub(2 * self.w_side()).max(40)
}
}
impl HasScenes for Project {
fn scenes_scroll (&self) -> usize {
self.scenes_scroll
}
fn scenes (&self) -> &Vec<Scene> {
&self.scenes
}
fn scenes_mut (&mut self) -> &mut Vec<Scene> {
&mut self.scenes
}
}
/// Represents the current user selection in the arranger
#[derive(PartialEq, Clone, Copy, Debug, Default)]
pub enum Selection {
#[default]
/// Nothing is selected
Nothing,
/// The whole mix is selected
Mix,
/// A MIDI input is selected.
Input(usize),
/// A MIDI output is selected.
Output(usize),
/// A clip is selected.
Clip(usize),
/// A sample is selected.
Sample(usize),
/// A scene is selected.
#[cfg(feature = "scene")] Scene(usize),
/// A track is selected.
#[cfg(feature = "track")] Track(usize),
/// A clip slot (track × scene) is selected.
#[cfg(feature = "track")] TrackClip { track: usize, scene: usize },
/// A track's MIDI input connection is selected.
#[cfg(feature = "track")] TrackInput { track: usize, port: usize },
/// A track's MIDI output connection is selected.
#[cfg(feature = "track")] TrackOutput { track: usize, port: usize },
/// A track device slot is selected.
#[cfg(feature = "track")] TrackDevice { track: usize, device: usize },
}
impl Selection {
pub fn describe (
&self,
#[cfg(feature = "track")] tracks: &[Track],
#[cfg(feature = "scene")] scenes: &[Scene],
) -> Arc<str> {
use Selection::*;
format!("{}", match self {
Mix => "Everything".to_string(),
#[cfg(feature = "scene")] Scene(s) =>
scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)).unwrap_or_else(||"S??".into()),
#[cfg(feature = "track")] Track(t) =>
tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)).unwrap_or_else(||"T??".into()),
TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) {
(Some(_), Some(s)) => match &s.clips.get(*track) {
None => format!("T{track} S{scene}: Empty"),
Some(clip) => format!("T{track} S{scene} C{}",
clip.as_ref().map(|clip|clip.read().name.clone()).unwrap_or_default()),
},
_ => format!("T{track} S{scene}: Empty"),
},
_ => todo!()
}).into()
}
}
#[cfg(feature = "scene")]
impl Selection {
pub fn scene (&self) -> Option<usize> {
use Selection::*;
match self { Scene(scene) | TrackClip { scene, .. } => Some(*scene), _ => None }
}
pub fn select_scene (&self, scene_count: usize) -> Self {
use Selection::*;
match self {
Mix | Track(_) => Scene(0),
Scene(s) => Scene((s + 1) % scene_count),
TrackClip { scene, .. } => Track(*scene),
_ => todo!(),
}
}
pub fn select_scene_next (&self, len: usize) -> Self {
use Selection::*;
match self {
Mix => Scene(0),
Track(t) => TrackClip { track: *t, scene: 0 },
Scene(s) => if s + 1 < len { Scene(s + 1) } else { Mix },
TrackClip { track, scene } => if scene + 1 < len { TrackClip { track: *track, scene: scene + 1 } } else { Track(*track) },
_ => todo!()
}
}
pub fn select_scene_prev (&self) -> Self {
use Selection::*;
match self {
Mix | Scene(0) => Mix,
Scene(s) => Scene(s - 1),
Track(t) => Track(*t),
TrackClip { track, scene: 0 } => Track(*track),
TrackClip { track, scene } => TrackClip { track: *track, scene: scene - 1 },
_ => todo!()
}
}
}
#[cfg(feature = "track")]
impl Selection {
pub fn track (&self) -> Option<usize> {
use Selection::*;
if let Track(track)|TrackClip{track,..}|TrackInput{track,..}|TrackOutput{track,..}|TrackDevice{track,..} = self {
Some(*track)
} else {
None
}
}
pub fn select_track (&self, track_count: usize) -> Self {
use Selection::*;
match self {
Mix => Track(0),
Scene(_) => Mix,
Track(t) => Track((t + 1) % track_count),
TrackClip { track, .. } => Track(*track),
_ => todo!(),
}
}
pub fn select_track_next (&self, len: usize) -> Self {
use Selection::*;
match self {
Mix => Track(0),
Scene(s) => TrackClip { track: 0, scene: *s },
Track(t) => if t + 1 < len { Track(t + 1) } else { Mix },
TrackClip {track, scene} => if track + 1 < len { TrackClip { track: track + 1, scene: *scene } } else { Scene(*scene) },
_ => todo!()
}
}
pub fn select_track_prev (&self) -> Self {
use Selection::*;
match self {
Mix => Mix,
Scene(s) => Scene(*s),
Track(0) => Mix,
Track(t) => Track(t - 1),
TrackClip { track: 0, scene } => Scene(*scene),
TrackClip { track: t, scene } => TrackClip { track: t - 1, scene: *scene },
_ => todo!()
}
}
}
impl<T: HasSelection> SelectionModel for T {}
pub trait SelectionModel: HasSelection {
/// Get the active track
#[cfg(feature = "track")] fn selected_track (&self) -> Option<&Track> where Self: HasTracks {
let index = self.selection().track()?;
self.tracks().get(index)
}
/// Get a mutable reference to the active track
#[cfg(feature = "track")] fn selected_track_mut (&mut self) -> Option<&mut Track> where Self: HasTracks {
let index = self.selection().track()?;
self.tracks_mut().get_mut(index)
}
/// Get the active scene
#[cfg(feature = "scene")] fn selected_scene (&self) -> Option<&Scene> where Self: HasScenes {
let index = self.selection().scene()?;
self.scenes().get(index)
}
/// Get a mutable reference to the active scene
#[cfg(feature = "scene")] fn selected_scene_mut (&mut self) -> Option<&mut Scene> where Self: HasScenes {
let index = self.selection().scene()?;
self.scenes_mut().get_mut(index)
}
/// Get the active clip
#[cfg(feature = "clip")] fn selected_clip (&self) -> Option<Slot<MidiClip>> where Self: HasScenes + HasTracks {
self.selected_scene()?.clips.read().get(self.selection().track()?).cloned()
}
}
/// A scene consists of a set of clips to play together.
///
/// ```
/// let scene: tek::Scene = Default::default();
/// let _ = scene.pulses();
/// let _ = scene.is_playing(&[]);
/// ```
#[derive(Debug, Default)] pub struct Scene {
/// Name of scene
pub name: Arc<str>,
/// Identifying color of scene
pub color: ItemTheme,
/// Clips in scene, one per track
pub clips: Pool<MidiClip>,
}
impl Scene {
pub const DEFAULT_HEIGHT: usize = 2;
/// Get pulse length of the longest clip in the scene
pub fn pulses (&self) -> usize {
self.clips.read().iter().fold(0, |a, p|{
a.max(p.read().as_ref().map(|q|q.read().length).unwrap_or(0))
})
}
/// True if all clips in scene are currently playing on given tracks.
pub fn is_playing (&self, tracks: &[Track]) -> bool {
// Not "playing" if there are no clips
if !self.clips.read().iter().any(|clip|clip.read().is_some()) {
return false
}
// Playing if at least 1 clip is playing
self.clips.read().iter().enumerate().all(|(track_index, clip)|match &*clip.read() {
Some(c) => tracks
.get(track_index)
.map(|track|{
if let Some((_, Some(clip))) = track.sequencer().play_clip() {
*clip.read() == *c.read()
} else {
false
}
})
.unwrap_or(false),
None => true
})
}
}
impl ScenesView for App {
fn w_mid (&self) -> u16 {
(self.size.w() as u16).saturating_sub(self.w_side())
}
fn w_side (&self) -> u16 {
20
}
fn h_scenes (&self) -> u16 {
(self.size.h() as u16).saturating_sub(20)
}
}
impl HasScenes for App {
fn scenes_scroll (&self) -> usize {
self.project.scenes_scroll()
}
fn scenes (&self) -> &Vec<Scene> {
self.project.scenes()
}
fn scenes_mut (&mut self) -> &mut Vec<Scene> {
self.project.scenes_mut()
}
}
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 }
}
}
/// Has a MIDI editor.
///
/// ```
/// use tek::{*, tengri::*};
///
/// struct Test(Option<MidiEditor>);
/// impl_as_ref!(Option<MidiEditor>: |self: Test|&self.0);
/// impl_as_mut!(Option<MidiEditor>: |self: Test|&mut self.0);
///
/// let mut host = Test(Some(MidiEditor::default()));
/// let _ = host.editor();
/// let _ = host.editor_mut();
/// let _ = host.is_editing();
/// let _ = host.editor_w();
/// let _ = host.editor_h();
/// ```
impl<T> HasEditor for T where T:
AsRef<Option<MidiEditor>> + AsMut<Option<MidiEditor>> {}
pub trait HasEditor:
AsRef<Option<MidiEditor>> + AsMut<Option<MidiEditor>>
{
fn editor (&self) -> &Option<MidiEditor> {
self.as_ref()
}
fn editor_mut (&mut self) -> &mut Option<MidiEditor> {
self.as_mut()
}
}
/// Has a pair of buffers for MIDI IO.
pub trait HasMidiBuffers {
fn note_buf_mut (&mut self) -> &mut Vec<u8>;
fn midi_buf_mut (&mut self) -> &mut Vec<Vec<Vec<u8>>>;
}
/// Can have a currently playing clip.
pub trait HasPlayClip: HasClock {
fn reset (&self) -> bool;
fn reset_mut (&mut self) -> &mut bool;
fn play_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
fn next_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
}
/// Has a MIDI sequencer.
impl <T> HasSequencer for T where T: AsRef<Sequencer> + AsMut<Sequencer> {}
pub trait HasSequencer: AsRef<Sequencer> + AsMut<Sequencer> {
fn sequencer (&self) -> &Sequencer {
self.as_ref()
}
fn sequencer_mut (&mut self) -> &mut Sequencer {
self.as_mut()
}
}
/// Has a clock.
pub trait HasClock: AsRef<Clock> + AsMut<Clock> {
fn clock (&self) -> &Clock {
self.as_ref()
}
fn clock_mut (&mut self) -> &mut Clock {
self.as_mut()
}
}
impl<T> HasClock for T where T: AsRef<Clock> + AsMut<Clock> {}
/// Has a chain of devices.
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 HasProject: AsRef<Option<Project>> + AsMut<Option<Project>> {}
/// Has a loaded project.
impl<T> HasProject for T where T: AsRef<Option<Project>> + AsMut<Option<Project>> {}
pub trait HasScene: AsRefOpt<Scene> + AsMutOpt<Scene> {
fn scene (&self) -> Option<&Scene> {
self.as_ref_opt()
}
fn scene_mut (&mut self) -> Option<&mut Scene> {
self.as_mut_opt()
}
}
impl<T> HasScene for T where T: AsRefOpt<Scene> + AsMutOpt<Scene> + Send + Sync {}
pub trait HasScenes {
fn scenes (&self) -> &Vec<Scene>;
fn scenes_mut (&mut self) -> &mut Vec<Scene>;
fn scenes_scroll (&self) -> usize {
0
}
}
impl<T> HasSelection for T where T: AsRef<Selection> + AsMut<Selection> {}
pub trait HasSelection: AsRef<Selection> + AsMut<Selection> {
fn selection (&self) -> &Selection {
self.as_ref()
}
fn selection_mut (&mut self) -> &mut Selection {
self.as_mut()
}
}
impl<T> HasTrack for T where T: AsRefOpt<Track> + AsMutOpt<Track> {}
pub trait HasTrack: AsRefOpt<Track> + AsMutOpt<Track> {
fn track (&self) -> Option<&Track> {
self.as_ref_opt()
}
fn track_mut (&mut self) -> Option<&mut Track> {
self.as_mut_opt()
}
}
impl<T> HasTracks for T where T: AsRef<Vec<Track>> + AsMut<Vec<Track>> + HasClock + HasTrackScroll {}
pub trait HasTracks: AsRef<Vec<Track>> + AsMut<Vec<Track>> + HasClock + HasTrackScroll {
/// Read-only reference to collection of [Track]s.
fn tracks (&self) -> &Vec<Track> {
self.as_ref()
}
/// Mutable reference to collection of [Track]s.
fn tracks_mut (&mut self) -> &mut Vec<Track> {
self.as_mut()
}
}
pub trait HasTrackScroll {
fn track_scroll (&self) -> usize;
fn tracks_last (&self) -> usize;
fn tracks_last_mut (&mut self) -> &mut usize;
}
impl HasTrackScroll for &mut Project {
fn track_scroll (&self) -> usize {
self.track_scroll
}
fn tracks_last (&self) -> usize {
self.track_last
}
fn tracks_last_mut (&mut self) -> &mut usize {
&mut self.track_last
}
}
impl HasTrackScroll for Project {
fn track_scroll (&self) -> usize {
self.track_scroll
}
fn tracks_last (&self) -> usize {
self.track_last
}
fn tracks_last_mut (&mut self) -> &mut usize {
&mut self.track_last
}
}
impl HasTrackScroll for App {
fn track_scroll (&self) -> usize {
self.project.track_scroll()
}
fn tracks_last (&self) -> usize {
self.project.track_last
}
fn tracks_last_mut (&mut self) -> &mut usize {
&mut self.project.track_last
}
}
pub trait HasClip {
fn clip_slot (&self) -> Option<Slot<MidiClip>>;
fn clip (&self) -> Option<ArcRwLockReadGuard<RawRwLock, Option<Arc<RwLock<MidiClip>>>>> {
self.clip_slot().map(|slot|slot.read())
}
fn clip_mut (&self) -> Option<ArcRwLockWriteGuard<RawRwLock, Option<Arc<RwLock<MidiClip>>>>> {
self.clip_slot().map(|slot|slot.write())
}
}
impl HasClip for App {
fn clip_slot (&self) -> Option<Slot<MidiClip>> {
self.project.clip_slot()
}
}
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
}
}
}
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);
}
pub trait HasClipsSize {
/// Size of clips area.
fn clips_size (&self) -> &Sizer;
/// Draw content annotated with value of [Self::clips_size].
fn with_clips_size (&self, show: bool, content: impl Draw<Tui>) -> impl Draw<Tui> {
let size = self.clips_size();
above(
when(show, fg(Green, east!(size.w() as usize, "x", size.h() as usize)).align_se()),
size.of(content.align_c().full_wh())
)
}
}
impl HasClipsSize for App {
fn clips_size (&self) -> &Sizer {
&self.project.size_inner
}
}
impl HasClipsSize for Project {
fn clips_size (&self) -> &Sizer {
&self.size_inner
}
}
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)
}
}
/// 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!() }
}
/// 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))
}
pub trait DispatchBind<C: Clone>: HasBinds + HasModeStack + 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.mode_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 name in self.mode_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.mode_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 (ctrl, _param) in mode.axis.iter() {
if let Some(axis) = axis.get(ctrl) {
if *input == axis.0 {
return Some([Binding { commands: [].into(), ..Default::default() }].into())
} else if *input == axis.1 {
return Some([Binding { commands: [].into(), ..Default::default() }].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.mode_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() {
println!("{input:?} {id:?}");
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 { commands: [tail.into()].into(), ..Default::default() }].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) },
}
}
}
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 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");
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() {
config_reload(config, std::fs::read_to_string(&path)?)?
} 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() }
}
}
}
pub use self::gear::*; mod gear {
mod device; pub use self::device::*;
mod midi; pub use self::midi::*;
mod mixer; pub use self::mixer::*;
mod pool; pub use self::pool::*;
mod sample; pub use self::sample::*;
mod track; pub use self::track::*;
#[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::*;
#[cfg(feature = "vst2")] mod vst2;
#[cfg(feature = "vst2")] pub use self::vst2::*;
}
#[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, "Menu");
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(())
}
}
/// 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>,
}
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)
}
}
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
}
}
/// Define and implement a unit of time
macro_rules! impl_time_unit {
($T:ident) => {
impl TimeUnit for $T {}
impl_op!($T, Add, add, |a, b|{a + b});
impl_op!($T, Sub, sub, |a, b|{a - b});
impl_op!($T, Mul, mul, |a, b|{a * b});
impl_op!($T, Div, div, |a, b|{a / b});
impl_op!($T, Rem, rem, |a, b|{a % b});
impl_from!($T: |value: f64| { Self(value.into()) });
impl_from!($T: |value: usize| { Self((value as f64).into()) });
impl From<$T> for f64 { fn from (value: $T) -> Self { value.get() } }
impl From<&$T> for f64 { fn from (value: &$T) -> Self { value.get() } }
impl From<$T> for usize { fn from (value: $T) -> Self { value.get() as usize } }
impl From<&$T> for usize { fn from (value: &$T) -> Self { value.get() as usize } }
impl Clone for $T { fn clone (&self) -> Self { Self(self.get().into()) } }
impl $T {
pub fn get (&self) -> f64 {
self.0.load(Relaxed)
}
pub fn set (&self, value: f64) -> f64 {
let old = self.get();
self.0.store(value, Relaxed);
old
}
}
}
}
impl_time_unit!(SampleCount);
impl_time_unit!(SampleRate);
impl_time_unit!(Microsecond);
impl_time_unit!(Quantize);
impl_time_unit!(Ppq);
impl_time_unit!(Pulse);
impl_time_unit!(Bpm);
impl_time_unit!(LaunchSync);
/// 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_has!(Clock: |self: Track|self.sequencer.clock);
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!(Clock: |self: Project| self.clock);
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!(Clock: |self: App|self.project.clock);
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_mode_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()))
})
}
}