#![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 )] /// Implement an arithmetic operation for a unit of time #[macro_export] macro_rules! impl_op { ($T:ident, $Op:ident, $method:ident, |$a:ident,$b:ident|{$impl:expr}) => { impl $Op for $T { type Output = Self; #[inline] fn $method (self, other: Self) -> Self::Output { let $a = self.get(); let $b = other.get(); Self($impl.into()) } } impl $Op for $T { type Output = Self; #[inline] fn $method (self, other: usize) -> Self::Output { let $a = self.get(); let $b = other as f64; Self($impl.into()) } } impl $Op for $T { type Output = Self; #[inline] fn $method (self, other: f64) -> Self::Output { let $a = self.get(); let $b = other; Self($impl.into()) } } } } /// TODO: Preserve the generic passthru syntax; /// remove this macro (only used twice) and potentially the trait. #[macro_export] macro_rules! impl_has_clips { (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? { fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> { $cb.read().unwrap() } fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> { $cb.write().unwrap() } } } } pub mod arrange; pub mod bind; pub mod browse; pub mod cli; pub mod clock; pub mod config; pub mod device; pub mod dialog; pub mod editor; pub mod menu; pub mod mix; pub mod mode; pub mod sample; pub mod sequence; pub mod select; pub mod view; #[cfg(feature = "plugin")] pub mod plugin; use clap::{self, Parser, Subcommand}; use self::{ config::*, mode::*, view::*, bind::*, dialog::*, browse::*, menu::*, clock::*, sequence::*, editor::*, arrange::*, select::*, device::*, sample::*, }; 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::*, exit::*, eval::*, 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}, }, }; /// 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 = tui_keyboard(&exit, &state, Duration::from_millis(100))?; let terminal = tui_output(&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 ) -> 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 { 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 { Ok(match (&state.dialog, axis) { (Dialog::None, _) => todo!(), (Dialog::Menu(_, _), ControlAxis::Y) => AppCommand::SetDialog { dialog: state.dialog.menu_next() } .act(state)?, _ => todo!() }) } fn tek_dec (state: &mut App, axis: &ControlAxis) -> Perhaps { Ok(match (&state.dialog, axis) { (Dialog::None, _) => None, (Dialog::Menu(_, _), ControlAxis::Y) => AppCommand::SetDialog { dialog: state.dialog.menu_prev() } .act(state)?, _ => todo!() }) } //impl_handle!(TuiIn: |self: App, input|{ //let commands = tek_collect_commands(self, input)?; //let history = tek_execute_commands(self, commands)?; //self.history.extend(history.into_iter()); //Ok(None) //}); //fn tek_collect_commands (app: &App, input: &TuiIn) -> Usually> { //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 { //commands.push(command) //} //} //} //} //} //Ok(commands) //} //fn tek_execute_commands ( //app: &mut App, commands: Vec //) -> Usually)>> { //let mut history = vec![]; //for command in commands.into_iter() { //let result = command.act(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 = 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 swap_value ( target: &mut T, value: &T, returned: impl Fn(T)->U ) -> Perhaps { 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 ( target: &mut bool, value: &Option, returned: impl Fn(Option)->U ) -> Perhaps { let mut value = value.unwrap_or(!*target); if value == *target { Ok(None) } else { std::mem::swap(target, &mut value); Ok(Some(returned(Some(value)))) } } //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>> { $cb } } } } fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { 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 } 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 }) }, }); /// Define a type alias for iterators of sized items (columns). macro_rules! def_sizes_iter { ($Type:ident => $($Item:ty),+) => { pub trait $Type<'a> = Iterator + Send + Sync + 'a; } } def_sizes_iter!(InputsSizes => MidiInput); def_sizes_iter!(OutputsSizes => MidiOutput); def_sizes_iter!(PortsSizes => Arc, [Connect]); def_sizes_iter!(ScenesSizes => Scene); def_sizes_iter!(TracksSizes => Track); /// ``` /// let _ = tek::view_logo(); /// ``` pub fn view_logo () -> impl Draw { wh_exact(Some(32), Some(7), bold(true, fg(Rgb(240, 200, 180), south!{ h_exact(1, ""), h_exact(1, ""), h_exact(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"), h_exact(1, east("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", east(fg(Rgb(230,100,40), "v0.3.0"), " ~~"))), h_exact(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"), }))) } /// ``` /// let x = std::sync::Arc::>::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: &str, beat: &str, time: &str) -> impl Draw { let theme = ItemTheme::G[96]; 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::>::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<&str>, sr: &str, buf: &str, lat: &str) -> impl Draw { let theme = ItemTheme::G[96]; let sr = field_h(theme, "SR", sr); let buf = field_h(theme, "Buf", buf); let lat = field_h(theme, "Lat", lat); bg(Black, east!(above( wh_full(origin_w(sel.map(|sel|field_h(theme, "Selected", sel)))), wh_full(origin_e(east!(sr, buf, lat))), ))) } /// ``` /// let _ = tek::button_play_pause(true); /// ``` pub fn button_play_pause (playing: bool) -> impl Draw { let compact = true;//self.is_editing(); bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, either(compact, thunk(move|to: &mut Tui|w_exact(9, either(playing, fg(Rgb(0, 255, 0), " PLAYING "), fg(Rgb(255, 128, 0), " STOPPED ")) ).draw(to)), thunk(move|to: &mut Tui|w_exact(5, either(playing, fg(Rgb(0, 255, 0), south(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",))) ).draw(to)), ) ) } #[cfg(feature = "track")] pub fn view_track_row_section ( _theme: ItemTheme, button: impl Draw, button_add: impl Draw, content: impl Draw, ) -> impl Draw { 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) -> impl Draw { let left = fg_bg(bg, Reset, w_exact(1, y_repeat("▐"))); let right = fg_bg(bg, Reset, w_exact(1, y_repeat("▌"))); east(left, west(right, 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 + 'a { let f = field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value)); let w = 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 }; let c = if value >= 0.0 { Red } else if value >= -3.0 { Yellow } else { Green }; south!(f, wh_exact(Some(w), Some(1), bg(c, ()))) } pub fn view_meters (values: &[f32;2]) -> impl Draw + 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>>) -> impl Draw + use<'_> { when(sample.is_some(), thunk(move|to: &mut Tui|{ let sample = sample.unwrap().read().unwrap(); let theme = sample.color; 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)), ).draw(to) })) } pub fn draw_info_v (sample: Option<&Arc>>) -> impl Draw + use<'_> { let a = thunk(move|to: &mut Tui|{ let sample = sample.unwrap().read().unwrap(); let theme = sample.color; 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)))), )).draw(to) }); let b = thunk(|to: &mut Tui|fg(Red, south!( bold(true, "× No sample."), "[r] record", "[Shift-F9] import", )).draw(to)); either(sample.is_some(), a, b) } pub fn draw_status (sample: Option<&Arc>>) -> impl Draw { bold(true, fg(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) -> impl Draw { w_exact(12, bg(theme.darker.term, w_full(origin_e(content)))) } pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T]) -> impl Draw + use<'a, T> { let ins = ports.len() as u16; let frame = Outer(true, Style::default().fg(g(96))); let iter = move||ports.iter(); let names = iter_south(iter, move|port, index|h_full(origin_w(format!(" {index} {}", port.port_name())))); let field = field_v(theme, title, names); wh_exact(Some(20), Some(1 + ins), border(true, frame, wh_exact(Some(20), Some(1 + ins), field))) } pub fn io_ports <'a, T: PortsSizes<'a>> ( fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a ) -> impl Draw + 'a { iter(items, move|( _index, name, connections, y, y2 ): (usize, &'a Arc, &'a [Connect], usize, usize), _| { let conns = ||connections.iter(); let conn = move|connect: &'a Connect, index|iter_south_fixed( index as u16, 1, h_full(origin_w(bold(false, fg_bg(fg, bg, &connect.info)))) ); iter_south(y as u16, (y2-y) as u16, south(h_full(bold(true, fg_bg(fg, bg, origin_w(east(" 󰣲 ", name))))), iter(conns, conn))) }) } /// CLI banner. pub(crate) const HEADER: &'static str = r#" ~ █▀█▀█ █▀▀█ █ █ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ █ █▀ █▀▀▄ ~ 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: Size, /// Performance counter pub perf: PerfModel, /// Available view modes and input bindings pub config: Config, /// Currently selected mode pub mode: Arc>>, /// Undo history pub history: Vec<(AppCommand, Option)>, /// 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>>> } impl_has!(Clock: |self: App|self.project.clock); impl_has!(Vec: |self: App|self.project.midi_ins); impl_has!(Vec: |self: App|self.project.midi_outs); impl_has!(Dialog: |self: App|self.dialog); impl_has!(Jack<'static>: |self: App|self.jack); impl_has!(Size: |self: App|self.size); impl_has!(Pool: |self: App|self.pool); impl_has!(Selection: |self: App|self.project.selection); impl_as_ref!(Vec: |self: App|self.project.as_ref()); impl_as_mut!(Vec: |self: App|self.project.as_mut()); impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt()); impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt()); impl_has_clips!( |self: App|self.pool.clips); impl_audio!(App: tek_jack_process, tek_jack_event); namespace!(App: Arc { literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); }); namespace!(App: u8 { literal = |dsl|try_to_u8(dsl); }); namespace!(App: u16 { literal = |dsl|try_to_u16(dsl); symbol = |app| { ":w/sidebar" => app.project.w_sidebar(app.editor().is_some()), ":h/sample-detail" => 6.max(app.size.h() as u16 * 3 / 9), }; }); namespace!(App: isize { literal = |dsl|try_to_isize(dsl); }); namespace!(App: usize { literal = |dsl|try_to_usize(dsl); symbol = |app| { ":scene-count" => app.scenes().len(), ":track-count" => app.tracks().len(), ":device-kind" => app.dialog.device_kind().unwrap_or(0), ":device-kind/next" => app.dialog.device_kind_next().unwrap_or(0), ":device-kind/prev" => app.dialog.device_kind_prev().unwrap_or(0), }; }); namespace!(App: bool { symbol = |app| { // Provide boolean values. ":mode/editor" => app.project.editor.is_some(), ":focused/dialog" => !matches!(app.dialog, Dialog::None), ":focused/message" => matches!(app.dialog, Dialog::Message(..)), ":focused/add_device" => matches!(app.dialog, Dialog::Device(..)), ":focused/browser" => app.dialog.browser().is_some(), ":focused/pool/import" => matches!(app.pool.mode, Some(PoolMode::Import(..))), ":focused/pool/export" => matches!(app.pool.mode, Some(PoolMode::Export(..))), ":focused/pool/rename" => matches!(app.pool.mode, Some(PoolMode::Rename(..))), ":focused/pool/length" => matches!(app.pool.mode, Some(PoolMode::Length(..))), ":focused/clip" => !app.editor_focused() && matches!(app.selection(), Selection::TrackClip{..}), ":focused/track" => !app.editor_focused() && matches!(app.selection(), Selection::Track(..)), ":focused/scene" => !app.editor_focused() && matches!(app.selection(), Selection::Scene(..)), ":focused/mix" => !app.editor_focused() && matches!(app.selection(), Selection::Mix), }; }); namespace!(App: ItemTheme {}); // TODO: provide colors here namespace!(App: Selection { symbol = |app| { ":select/scene" => app.selection().select_scene(app.tracks().len()), ":select/scene/next" => app.selection().select_scene_next(app.scenes().len()), ":select/scene/prev" => app.selection().select_scene_prev(), ":select/track" => app.selection().select_track(app.tracks().len()), ":select/track/next" => app.selection().select_track_next(app.tracks().len()), ":select/track/prev" => app.selection().select_track_prev(), }; }); namespace!(App: Color { symbol = |app| { ":color/bg" => Color::Rgb(28, 32, 36), }; expression = |app| { "g" (n: u8) => Color::Rgb(n, n, n), "rgb" (r: u8, g: u8, b: u8) => Color::Rgb(r, g, b), }; }); namespace!(App: Option { symbol = |app| { ":editor/pitch" => Some((app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into()) }; }); namespace!(App: Option { symbol = |app| { ":selected/scene" => app.selection().scene(), ":selected/track" => app.selection().track(), }; }); namespace!(App: Option>> { symbol = |app| { ":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() { app.scenes()[*scene].clips[*track].clone() } else { None } }; }); pub trait HasClipsSize { fn clips_size (&self) -> &Size; } pub trait HasDevices: AsRef> + AsMut> { fn devices (&self) -> &Vec { self.as_ref() } fn devices_mut (&mut self) -> &mut Vec { 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); } impl<'a> Namespace<'a, AppCommand> for App { symbols!('a |app| -> AppCommand { "x/inc" => AppCommand::Inc { axis: ControlAxis::X }, "x/dec" => AppCommand::Dec { axis: ControlAxis::X }, "y/inc" => AppCommand::Inc { axis: ControlAxis::Y }, "y/dec" => AppCommand::Dec { axis: ControlAxis::Y }, "confirm" => AppCommand::Confirm, "cancel" => AppCommand::Cancel, }); } impl Interpret for App { fn interpret_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> { app_interpret_expr(self, to, lang) } fn interpret_word <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Usually<()> { app_interpret_word(self, to, lang) } } fn app_interpret_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Usually<()> { if eval_view(state, to, lang)? || eval_view_tui(state, to, lang)? { Ok(()) } else { Err(format!("App::interpret_expr: unexpected: {lang:?}").into()) } } fn app_interpret_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Usually<()> { let mut frags = dsl.src()?.unwrap().split("/"); match frags.next() { Some(":logo") => view_logo().draw(to), Some(":status") => h_exact(1, "TODO: Status Bar").draw(to), Some(":meters") => match frags.next() { Some("input") => bg(Rgb(30, 30, 30), h_full(origin_s("Input Meters"))).draw(to), Some("output") => bg(Rgb(30, 30, 30), h_full(origin_s("Output Meters"))).draw(to), _ => panic!() }, Some(":tracks") => match frags.next() { None => "TODO tracks".draw(to), Some("names") => state.project.view_track_names(state.color.clone()).draw(to),//bg(Rgb(40, 40, 40), w_full(origin_w("Track Names")))), Some("inputs") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Inputs"))).draw(to), Some("devices") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Devices"))).draw(to), Some("outputs") => bg(Rgb(40, 40, 40), w_full(origin_w("Track Outputs"))).draw(to), _ => panic!() }, Some(":scenes") => match frags.next() { None => "TODO scenes".draw(to), Some(":scenes/names") => "TODO Scene Names".draw(to), _ => panic!() }, Some(":editor") => "TODO Editor".draw(to), Some(":dialog") => match frags.next() { Some("menu") => if let Dialog::Menu(selected, items) = &state.dialog { let items = items.clone(); let selected = selected; Some(wh_full(thunk(move|to: &mut Tui|{ for (index, MenuItem(item, _)) in items.0.iter().enumerate() { to.place(&y_push((2 * index) as u16, fg_bg( if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, h_exact(2, origin_n(w_full(item))) ))); } }))) } else { None }.draw(to), _ => unimplemented!("App::interpret_word: {dsl:?} ({frags:?})"), }, Some(":templates") => { let modes = state.config.modes.clone(); let height = (modes.read().unwrap().len() * 2) as u16; h_exact(height, w_min(Some(30), thunk(move |to: &mut Tui|{ for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); let fg1 = Rgb(224, 192, 128); let fg2 = Rgb(224, 128, 32); let field_name = w_full(origin_w(fg(fg1, name))); let field_id = w_full(origin_e(fg(fg2, id))); let field_info = w_full(origin_w(info)); y_push((2 * index) as u16, h_exact(2, w_full(bg(bg, south( above(field_name, field_id), field_info))))).draw(to); } }))) }.draw(to), Some(":sessions") => h_exact(Some(6), w_min(Some(30), thunk(|to: &mut Tui|{ let fg = Rgb(224, 192, 128); for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; y_push((2 * index) as u16, &h_exact(2, w_full(bg(bg, origin_w(fg(fg, name)))))).draw(to); } }))).draw(to), Some(":browse/title") => w_full(origin_w(field_v(ItemColor::default(), match state.dialog.browser_target().unwrap() { BrowseTarget::SaveProject => "Save project:", BrowseTarget::LoadProject => "Load project:", BrowseTarget::ImportSample(_) => "Import sample:", BrowseTarget::ExportSample(_) => "Export sample:", BrowseTarget::ImportClip(_) => "Import clip:", BrowseTarget::ExportClip(_) => "Export clip:", }, w_shrink(3, h_exact(1, fg(g(96), x_repeat("🭻"))))))).draw(to), Some(":device") => { let selected = state.dialog.device_kind().unwrap(); south(bold(true, "Add device"), iter_south( move||device_kinds().iter(), move|_label: &&'static str, i|{ let bg = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; let lb = if i == selected { "[ " } else { " " }; let rb = if i == selected { " ]" } else { " " }; w_full(bg(bg, east(lb, west(rb, "FIXME device name")))) })).draw(to) }, Some(":debug") => h_exact(1, format!("[{:?}]", to.area())).draw(to), Some(_) => { let views = state.config.views.read().unwrap(); if let Some(dsl) = views.get(dsl.src()?.unwrap()) { let dsl = dsl.clone(); std::mem::drop(views); state.interpret(to, &dsl)? } else { unimplemented!("{dsl:?}"); } }, _ => unreachable!() } Ok(()) } impl App { /// Update memoized render of clock values. /// ``` /// tek::App::default().update_clock(); /// ``` pub fn update_clock (&self) { ClockView::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) } /// Set modal dialog. /// /// ``` /// let previous: tek::Dialog = tek::App::default().set_dialog(tek::Dialog::welcome()); /// ``` pub fn set_dialog (&mut self, mut dialog: Dialog) -> Dialog { std::mem::swap(&mut self.dialog, &mut dialog); dialog } /// FIXME: generalize. Set picked device in device pick dialog. /// /// ``` /// tek::App::default().device_pick(0); /// ``` pub fn device_pick (&mut self, index: usize) { self.dialog = Dialog::Device(index); } /// FIXME: generalize. Add device to current track. pub fn add_device (&mut self, index: usize) -> Usually<()> { match index { 0 => { let name = self.jack.with_client(|c|c.name().to_string()); let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name(); let track = self.track().expect("no active track"); let port = format!("{}/Sampler", &track.name); let connect = Connect::exact(format!("{name}:{midi}")); let sampler = if let Ok(sampler) = Sampler::new( &self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]] ) { self.dialog = Dialog::None; Device::Sampler(sampler) } else { self.dialog = Dialog::Message("Failed to add device.".into()); return Err("failed to add device".into()) }; let track = self.track_mut().expect("no active track"); track.devices.push(sampler); Ok(()) }, 1 => { todo!(); //Ok(()) }, _ => unreachable!(), } } /// 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 } } /// Is a MIDI editor currently focused? /// /// ``` /// tek::App::default().editor_focused(); /// ``` pub fn editor_focused (&self) -> bool { false } /// Toggle MIDI editor. /// /// ``` /// tek::App::default().toggle_editor(None); /// ``` pub fn toggle_editor (&mut self, value: Option) { //FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed); let value = value.unwrap_or_else(||!self.editor().is_some()); if value { // Create new clip in pool when entering empty cell if let Selection::TrackClip { track, scene } = *self.selection() && let Some(scene) = self.project.scenes.get_mut(scene) && let Some(slot) = scene.clips.get_mut(track) && slot.is_none() && let Some(track) = self.project.tracks.get_mut(track) { let (_index, clip) = self.pool.add_new_clip(); // autocolor: new clip colors from scene and track color let color = track.color.base.mix(scene.color.base, 0.5); clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); if let Some(editor) = &mut self.project.editor { editor.set_clip(Some(&clip)); } *slot = Some(clip.clone()); //Some(clip) } else { //None } } else if let Selection::TrackClip { track, scene } = *self.selection() && let Some(scene) = self.project.scenes.get_mut(scene) && let Some(slot) = scene.clips.get_mut(track) && let Some(clip) = slot.as_mut() { // Remove clip from arrangement when exiting empty clip editor let mut swapped = None; if clip.read().unwrap().count_midi_messages() == 0 { std::mem::swap(&mut swapped, slot); } if let Some(clip) = swapped { self.pool.delete_clip(&clip.read().unwrap()); } } } } impl Draw for App { fn draw (self, to: &mut Tui) -> Usually> { if let Some(e) = self.error.read().unwrap().as_ref() { to.show(to.area().into(), e.as_ref()); } for (index, dsl) in self.mode.view.iter().enumerate() { if let Err(e) = self.interpret(to, dsl) { *self.error.write().unwrap() = Some(format!("view #{index}: {e}").into()); break; } } } } impl HasClipsSize for App { fn clips_size (&self) -> &Size { &self.project.size_inner } } impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } } impl_default!(AppCommand: Self::Nop); primitive!(u8: try_to_u8); primitive!(u16: try_to_u16); primitive!(usize: try_to_usize); primitive!(isize: try_to_isize); fn field_h (theme: ItemTheme, head: impl Draw, body: impl Draw) -> impl Draw { } fn field_v (theme: ItemTheme, head: impl Draw, body: impl Draw) -> impl Draw { }