From ba8ff1ae69a0110fc4a457d58c9a504b816d8194 Mon Sep 17 00:00:00 2001 From: i do not exist Date: Thu, 30 Jul 2026 16:38:45 +0300 Subject: [PATCH] fix warns, simplify, begin watcher --- src/app/audio.rs | 52 -- src/app/bind.rs | 199 ----- src/app/config.rs | 163 ---- src/app/draw.rs | 773 ----------------- src/app/modes.rs | 109 --- src/app/size.rs | 25 - src/device/browse.rs | 6 +- src/device/clock/memo.rs | 1 + src/device/clock/moment.rs | 2 +- src/device/clock/ticker.rs | 2 - src/device/clock/timebase.rs | 2 +- src/device/dialog.rs | 18 +- src/device/editor.rs | 1 + src/device/meter.rs | 1 + src/device/sampler.rs | 334 ++++++- src/device/sampler/sample.rs | 144 --- src/device/sampler/sample_add.rs | 122 --- src/device/sampler/sample_kit.rs | 26 - src/device/sampler/voice.rs | 29 - src/device/sequence.rs | 3 +- src/tek.edn | 11 +- src/tek.rs | 1401 +++++++++++++++++++++++++++++- tengri | 2 +- 23 files changed, 1737 insertions(+), 1689 deletions(-) delete mode 100644 src/app/audio.rs delete mode 100644 src/app/bind.rs delete mode 100644 src/app/config.rs delete mode 100644 src/app/draw.rs delete mode 100644 src/app/modes.rs delete mode 100644 src/app/size.rs delete mode 100644 src/device/sampler/sample.rs delete mode 100644 src/device/sampler/sample_add.rs delete mode 100644 src/device/sampler/sample_kit.rs delete mode 100644 src/device/sampler/voice.rs diff --git a/src/app/audio.rs b/src/app/audio.rs deleted file mode 100644 index e224068e..00000000 --- a/src/app/audio.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::*; - -impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } } - -impl_audio!(App: tek_jack_process, tek_jack_event); - -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 = 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.process_tracks(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:?}"); } - } -} diff --git a/src/app/bind.rs b/src/app/bind.rs deleted file mode 100644 index 2fb24e8a..00000000 --- a/src/app/bind.rs +++ /dev/null @@ -1,199 +0,0 @@ -use crate::*; - -tui_keys!(self: App, input { - let commands = tek_commands_collect(self, input)?; - let results = tek_commands_execute(self, commands)?; - self.history.extend(results.into_iter()); - Ok(()) -}); - -fn tek_commands_collect (app: &App, input: &TuiEvent) - -> Usually> -{ - let mut commands = vec![]; - if let Some(ref mode) = app.mode { - for id in 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) { - 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_commands_execute (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) -} - -/// Collection of input bindings. -pub type Binds = Arc, Bind>>>>; - -pub(crate) fn load_bind (binds: &Binds, name: &impl AsRef, body: &impl Language) -> Usually<()> { - binds.write().unwrap().insert(name.as_ref().into(), Bind::load(body)?); - Ok(()) -} - -/// An map of input events (e.g. [TuiEvent]) to [Binding]s. -/// -/// ``` -/// let lang = "(@x (nop)) (@y (nop) (nop))"; -/// let bind = tek::Bind::>::load(&lang).unwrap(); -/// assert_eq!(bind.query(&'x'.into()).map(|x|x.len()), Some(1)); -/// //assert_eq!(bind.query(&'y'.into()).map(|x|x.len()), Some(2)); -/// ``` -#[derive(Debug)] pub struct Bind( - /// Map of each event (e.g. key combination) to - /// all command expressions bound to it by - /// all loaded input layers. - pub BTreeMap>> -); - -/// A sequence of zero or more commands (e.g. [AppCommand]), -/// optionally filtered by [Condition] to form layers. -/// -/// ``` -/// //FIXME: Why does it overflow? -/// //let binding: Binding<()> = tek::Binding { ..Default::default() }; -/// ``` -#[derive(Debug, Clone)] pub struct Binding { - pub commands: Arc<[C]>, - pub condition: Option, - pub description: Option>, - pub source: Option>, -} - -/// Condition that must evaluate to true in order to enable an input layer. -/// -/// ``` -/// let condition = tek::Condition(std::sync::Arc::new(Box::new(||{true}))); -/// ``` -#[derive(Clone)] pub struct Condition( - pub Arcbool + Send + Sync>> -); - -impl Bind> { - pub fn load (lang: &impl Language) -> Usually { - let mut map = Self::new(); - lang.each(|item|if item.expr().head() == Ok(Some("see")) { - // TODO - Ok(()) - } else if let Ok(Some(_word)) = item.expr().head().word() { - if let Some(event) = TuiKey::from_dsl(item.expr()?.head()?)?.to_crossterm() { - map.add(TuiEvent(event), Binding { - commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(), - condition: None, - description: None, - source: None - }); - Ok(()) - } else if Some(":char") == item.expr()?.head()? { - // TODO - return Ok(()) - } else { - return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into()) - } - } else { - return Err(format!("Config::load_bind: unexpected: {item:?}").into()) - })?; - Ok(map) - } -} - -/// Default is always empty map regardless if `E` and `C` implement [Default]. -impl Default for Bind { - fn default () -> Self { Self(Default::default()) } -} - -impl Default for Binding { - fn default () -> Self { - Self { - commands: Default::default(), - condition: Default::default(), - description: Default::default(), - source: Default::default(), - } - } -} - -impl Bind { - /// Create a new event map - pub fn new () -> Self { - Default::default() - } - /// Add a binding to an owned event map. - pub fn def (mut self, event: E, binding: Binding) -> Self { - self.add(event, binding); - self - } - /// Add a binding to an event map. - pub fn add (&mut self, event: E, binding: Binding) -> &mut Self { - if !self.0.contains_key(&event) { - self.0.insert(event.clone(), Default::default()); - } - self.0.get_mut(&event).unwrap().push(binding); - self - } - /// Return the binding(s) that correspond to an event. - pub fn query (&self, event: &E) -> Option<&[Binding]> { - self.0.get(event).map(|x|x.as_slice()) - } - /// Return the first binding that corresponds to an event, considering conditions. - pub fn dispatch (&self, event: &E) -> Option<&Binding> { - self.query(event) - .map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next()) - .flatten() - } -} - -impl_debug!(Condition |self, w| { write!(w, "*") }); - -impl_default!(AppCommand: Self::Nop); - -def_command!(AppCommand: |app: App| { - Nop => Ok(None), - Cancel => todo!(), // TODO delegate: - Confirm => app.confirm(), - Inc { axis: ControlAxis } => app.inc(axis), - Dec { axis: ControlAxis } => app.dec(axis), - SetDialog { dialog: Dialog } => { - swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }) - }, -}); - -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, - }); -} - -/// A control axis. -/// -/// ``` -/// let axis = tek::ControlAxis::X; -/// ``` -#[derive(Debug, Copy, Clone)] pub enum ControlAxis { - X, Y, Z, I -} - -//take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref() - //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); diff --git a/src/app/config.rs b/src/app/config.rs deleted file mode 100644 index b0c6cf04..00000000 --- a/src/app/config.rs +++ /dev/null @@ -1,163 +0,0 @@ -use crate::*; - -/// Configuration: mode, view, and bind definitions. -/// -/// ``` -/// let config = tek::Config::default(); -/// ``` -/// -/// ``` -/// // Some dizzle. -/// // What indentation to use here lol? -/// let source = stringify!((mode :menu (name Menu) -/// (info Mode selector.) (keys :axis/y :confirm) -/// (view (bg (g 0) (bsp/s :ports/out -/// (bsp/n :ports/in -/// (bg (g 30) (bsp/s (fixed/y 7 :logo) -/// (fill :dialog/menu))))))))); -/// // Add this definition to the config and try to load it. -/// // A "mode" is basically a state machine -/// // with associated input and output definitions. -/// tek::Config::default().add(&source).unwrap().get_mode(":menu").unwrap(); -/// ``` -#[derive(Default, Debug)] pub struct Config { - /// XDG base directories of running user. - pub dirs: BaseDirectories, - /// Active collection of interaction modes. - pub modes: Modes, - /// Active collection of event bindings. - pub binds: Binds, - /// Active collection of view definitions. - pub views: Views, -} - -impl Config { - const CONFIG_DIR: &'static str = "tek"; - const CONFIG_SUB: &'static str = "v0"; - const CONFIG: &'static str = "tek.edn"; - const DEFAULTS: &'static str = include_str!("../tek.edn"); - - pub fn watch (callback: impl FnOnce(Self)->T) -> Usually { - let config = Self::init_new(None)?; - let watcher = notify_debouncer_mini::new_debouncer(Duration::from_millis(500), |res| { - println!("{res:?}"); - })?; - let result = callback(config); - Ok(result) - } - - pub fn init_new (dirs: Option) -> Usually { - let mut config = Self::new(None); - config.init()?; - Ok(config) - } - - /// Create a new app configuration from a set of XDG base directories, - pub fn new (dirs: Option) -> Self { - let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB); - let dirs = dirs.unwrap_or_else(default); - Self { dirs, ..Default::default() } - } - - /// Write initial contents of configuration. - pub fn init (&mut self) -> Usually<()> { - self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|{ - cfgs.add(&dsl)?; - Ok(()) - })?; - Ok(()) - } - - /// Write initial contents of a configuration file. - pub fn init_one ( - &mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()> - ) -> Usually<()> { - if self.dirs.find_config_file(path).is_none() { - //println!("Creating {path:?}"); - std::fs::write(self.dirs.place_config_file(path)?, defaults)?; - } - Ok(if let Some(path) = self.dirs.find_config_file(path) { - //println!("Loading {path:?}"); - let src = std::fs::read_to_string(&path)?; - src.as_str().each(move|item|each(self, item))?; - } else { - return Err(format!("{path}: not found").into()) - }) - } - - /// Add statements to configuration from [Dsl] source. - pub fn add (&mut self, dsl: impl Language) -> Usually<&mut Self> { - dsl.each(|item|self.add_one(item))?; - Ok(self) - } - - fn add_one (&self, item: impl Language) -> Usually<()> { - if let Some(expr) = item.expr()? { - let head = expr.head()?; - let tail = expr.tail()?; - let name = tail.head()?; - let body = tail.tail()?; - //println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default()); - match head { - Some("mode") if let Some(name) = name => self.modes.add(&name, &body)?, - Some("keys") if let Some(name) = name => load_bind(&self.binds, &name, &body)?, - Some("view") if let Some(name) = name => load_view(&self.views, &name, &body)?, - _ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into()) - } - Ok(()) - } else { - return Err(format!("Config::load: expected expr, got: {item:?}").into()) - } - } - - pub fn get_mode (&self, mode: impl AsRef) -> Option>>> { - self.modes.get(mode) - } -} - -pub fn print_config (config: &Config) { - use ::ansi_term::Color::*; - println!("{:?}", config.dirs); - for (k, v) in config.views.read().unwrap().iter() { - println!("{} {} {v}", Green.paint("VIEW"), Green.bold().paint(format!("{k:<16}"))); - } - for (k, v) in config.binds.read().unwrap().iter() { - println!("{} {}", Green.paint("BIND"), Green.bold().paint(format!("{k:<16}"))); - for (k, v) in v.0.iter() { - print!("{} ", &Yellow.paint(match &k.0 { - Event::Key(KeyEvent { modifiers, .. }) => - format!("{:>16}", format!("{modifiers}")), - _ => unimplemented!() - })); - print!("{}", &Yellow.bold().paint(match &k.0 { - Event::Key(KeyEvent { code, .. }) => - format!("{:<10}", format!("{code}")), - _ => unimplemented!() - })); - for v in v.iter() { - print!(" => {:?}", v.commands); - print!(" {}", v.condition.as_ref().map(|x|format!("{x:?}")).unwrap_or_default()); - println!(" {}", v.description.as_ref().map(|x|x.as_ref()).unwrap_or_default()); - //println!(" {:?}", v.source); - } - } - } - config.modes.for_each(|k, v|{ - println!(); - for v in v.name.iter() { print!("{}", Green.bold().paint(format!("{v} "))); } - for v in v.info.iter() { print!("\n{}", Green.paint(format!("{v}"))); } - print!("\n{} {}", Blue.paint("TOOL"), Green.bold().paint(format!("{k:<16}"))); - print!("\n{}", Blue.paint("KEYS")); - for v in v.keys.iter() { print!("{}", Green.paint(format!(" {v}"))); } - println!(); - v.modes.for_each(|k, v|{ - print!("{} {} {:?}", Blue.paint("MODE"), Green.bold().paint(format!("{k:<16}")), v.name); - print!( " INFO={:?}", v.info); - print!( " VIEW={:?}", v.view); - println!(" KEYS={:?}", v.keys); - }); - print!("{}", Blue.paint("VIEW")); - for v in v.view.iter() { print!("{}", Green.paint(format!(" {v}"))); } - println!(); - }); -} diff --git a/src/app/draw.rs b/src/app/draw.rs deleted file mode 100644 index 118123bf..00000000 --- a/src/app/draw.rs +++ /dev/null @@ -1,773 +0,0 @@ -use crate::*; - -/// Collection of custom view definitions. -pub type Views = Arc, Arc>>>; - -/// Load custom view definition. -pub(crate) fn load_view ( - views: &Views, - name: &impl AsRef, - body: &impl Language, -) -> Usually<()> { - views.write().unwrap().insert( - name.as_ref().into(), - body.src()?.unwrap_or_default().into() - ); - Ok(()) -} - -/// 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 View for App { - fn view (&self) -> impl Draw { - thunk(|to: &mut Tui|{ - let xywh = to.area().into(); - - if let Some(e) = self.error.read().unwrap().as_ref() { - //to.show(area(xywh, format!("KYPbanica {xywh:?}").align_c()))?; - //to.show(ShowSize.align_se())?; - to.show(e.as_ref().align_c())?; - } - - if let Some(ref mode) = self.mode { - for (index, dsl) in mode.view.iter().enumerate() { - if let Err(e) = self.interpret(to, dsl) { - let src = &dsl.src().unwrap_or(Some("")).unwrap_or(""); - let message = format!("mode {:?} view #{index}:\n{e}\n{}", &mode.name, &src); - *self.error.write().unwrap() = Some(message.into()); - break; - } - } - } - - Ok(Some(xywh)) - }) - } -} - -impl Interpret>> for App { - fn interpret_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Drawn { - tek_draw_expr(self, to, lang) - } - fn interpret_word <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Drawn { - tek_draw_word(self, to, lang) - } -} - -fn tek_draw_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Drawn { - Ok(Some(if let Some(area) = eval_view(state, to, lang)? { - area - } else if let Some(area) = eval_view_tui(state, to, lang)? { - area - } else { - return Err(format!("App::interpret_expr: unexpected: {lang:?}").into()) - })) -} - -fn tek_draw_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Drawn { - let mut frags = dsl.src()?.unwrap().split("/"); - match frags.next() { - //Some(":logo") => view_logo().draw(to), - Some(":meters") => draw_meter_section(to, frags), - Some(":tracks") => draw_tracks(to, frags, state), - Some(":scenes") => draw_scenes(to, frags), - Some(":dialog") => draw_dialog(to, frags, state, dsl), - Some(":templates") => draw_templates(to, frags, state), - Some(":sessions") => view_sessions().draw(to), - Some(":browse/title") => view_browse_title(state).draw(to), - Some(":device") => view_device(state).draw(to), - Some(":status") => "TODO: Status Bar".exact_h(1).draw(to), - Some(":editor") => "TODO Editor".draw(to), - Some(":transport") => view_transport(true, "", "", "").draw(to), - Some(":debug") => format!("[{:?}]", to.area()).exact_h(1).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!() - } -} - -pub fn draw_meter_section (to: &mut Tui, mut frags: std::str::Split<&str>) -> Drawn { - match frags.next() { - Some("input") => bg(Rgb(30, 30, 30), "Input Meters".align_s().full_h()).draw(to), - Some("output") => bg(Rgb(30, 30, 30), "Output Meters".align_s().full_h()).draw(to), - _ => panic!() - } -} - -pub fn draw_tracks (to: &mut Tui, mut frags: std::str::Split<&str>, state: &App) -> Drawn { - 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), full_w(align_w("Track Names")))), - Some("inputs") => bg(Rgb(40, 40, 40), "Track Inputs".align_w().full_w()).draw(to), - Some("devices") => bg(Rgb(40, 40, 40), "Track Devices".align_w().full_w()).draw(to), - Some("outputs") => bg(Rgb(40, 40, 40), "Track Outputs".align_w().full_w()).draw(to), - _ => panic!() - } -} - -pub fn draw_scenes (to: &mut Tui, mut frags: std::str::Split<&str>) -> Drawn { - match frags.next() { - None => "TODO Scenes".draw(to), - Some(":scenes/names") => "TODO Scene Names".draw(to), - _ => panic!() - } -} - -pub fn draw_dialog ( - to: &mut Tui, mut frags: std::str::Split<&str>, state: &App, dsl: &impl Expression) -> Drawn { - match frags.next() { - Some("menu") => if let Dialog::Menu(selected, items) = &state.dialog { - let items = items.clone(); - let selected = selected; - Some(thunk(move|to: &mut Tui|{ - for (index, MenuItem(item, _)) in items.0.iter().enumerate() { - let f = if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }; - let b = if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }; - fg_bg(f, b, item.full_w().align_w().exact_h(2)) - .push_y((4 * index) as u16).draw(to)?; - } - Ok(Some(to.area().into())) - }).full_wh()) - } else { - None - }.draw(to), - _ => unimplemented!("App::interpret_word: {dsl:?} ({frags:?})"), - } -} - -pub fn draw_templates (to: &mut Tui, frags: std::str::Split<&str>, state: &App) -> Drawn { - let height = (state.config.modes.len() * 2) as u16; - thunk(move |to: &mut Tui|{ - let mut index = 0; - state.config.modes.for_each(|id, profile| { - let b = 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 = fg(fg1, name).align_w().full_w(); - let field_id = fg(fg2, id).align_e().full_w(); - let field_info = info.align_w().full_w(); - let _ = bg(b, south(above(field_name, field_id), field_info)) - .full_w().exact_h(2).push_y((2 * index) as u16).draw(to); - index += 1; - }); - Ok(Some(to.area().into())) - }).min_w(30).exact_h(height).draw(to) -} - -pub fn per_track <'a, T: Draw + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a -) -> impl Draw + 'a { - per_track_top(tracks, move|index, track|callback(index, track).full_h().align_y()) -} - -pub fn per_track_top <'a, T: Draw + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a -) -> impl Draw + 'a { - bg(Reset, iter_east(tracks, - move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ - fg_bg( - track.color.lightest.term, - track.color.base.term, - callback(index, track) - ).exact_w((x2 - x1) as u16) - }).align_x()) -} - -pub fn field_h ( - _theme: ItemTheme, _head: impl Draw, _body: impl Draw -) -> impl Draw { -} - -pub fn field_v ( - _theme: ItemTheme, _head: impl Draw, _body: impl Draw -) -> impl Draw { -} - -pub fn view_sessions () -> impl Draw { - let h = 6; - let w = Some(30); - let f = Rgb(224, 192, 128); - thunk(move |to: &mut Tui|{ - for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { - let b = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; - let y = (2 * index) as u16; - let h = 2; - bg(b, fg(f, *name).align_w()).full_w().exact_h(h).push_y(y).draw(to)?; - } - Ok(Some(to.area().into())) - }).min_w(w).exact_h(h) -} - -pub fn view_browse_title (state: &App) -> impl Draw { - field_v(ItemTheme::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:", - }, fg(g(96), x_repeat("🭻")).exact_h(1) - ).align_w().full_w() -} - -pub fn view_device (state: &App) -> impl Draw { - let selected = state.dialog.device_kind().unwrap(); - south(bold(true, "Add device"), iter_south( - move||device_kinds().iter(), - move|_label: &&'static str, i|{ - let b = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; - let l = if i == selected { "[ " } else { " " }; - let r = if i == selected { " ]" } else { " " }; - bg(b, east(l, west(r, "FIXME device name"))).full_w() - })) -} - -/// ``` -/// let x = ""; -/// let _ = tek::view_transport(true, x.as_ref(), x.as_ref(), x.as_ref()); -/// let _ = tek::view_transport(false, x.as_ref(), x.as_ref(), x.as_ref()); -/// ``` -pub fn view_transport (play: bool, bpm: &str, beat: &str, time: &str) -> impl Draw { - let theme = ItemTheme::G[96]; - bg(Black, east!(above( - button_play_pause(play, false).align_w(), - east!( - field_h(theme, "BPM", bpm), - field_h(theme, "Beat", beat), - field_h(theme, "Time", time), - ).align_e().full_wh() - ))) -} - -/// ``` -/// let x = ""; -/// let _ = tek::view_status(None, x.as_ref(), x.as_ref(), x.as_ref()); -/// let _ = tek::view_status(Some("".into()), x.as_ref(), x.as_ref(), x.as_ref()); -/// ``` -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( - sel.map(|sel|field_h(theme, "Selected", sel)).align_w().full_wh(), - east!(sr, buf, lat).align_e().full_wh(), - ))) -} - -/// ``` -/// let _ = tek::button_play_pause(true, true); -/// let _ = tek::button_play_pause(true, false); -/// let _ = tek::button_play_pause(false, true); -/// let _ = tek::button_play_pause(false, false); -/// ``` -pub fn button_play_pause (playing: bool, compact: bool) -> impl Draw { - bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, - either(compact, - thunk(move|to: &mut Tui|either(playing, - fg(Rgb(0, 255, 0), " PLAYING "), - fg(Rgb(255, 128, 0), " STOPPED "), - ).exact_w(9).draw(to)), - thunk(move|to: &mut Tui|either(playing, - fg(Rgb(0, 255, 0), south(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), - fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",)), - ).exact_w(5).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( - button_add.align_nw().exact_w(4).full_h(), - east( - button.align_nw().full_h().exact_w(20), - content.align_c().full_wh() - ) - ) -} - -/// ``` -/// 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, y_repeat("▐").exact_w(1)); - let right = fg_bg(bg, Reset, y_repeat("▌").exact_w(1)); - 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, bg(c, ()).exact_wh(w, 1)) -} - -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 view_sample_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 view_sample_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; - south!( - field_h(theme, "Name ", format!("{:<10}", sample.name.clone())) .align_w().full_w(), - field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())).align_w().full_w(), - field_h(theme, "Start ", format!("{:<8}", sample.start)) .align_w().full_w(), - field_h(theme, "End ", format!("{:<8}", sample.end)) .align_w().full_w(), - field_h(theme, "Trans ", "0") .align_w().full_w(), - field_h(theme, "Gain ", format!("{}", sample.gain)) .align_w().full_w(), - ).exact_w(20).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 view_sample_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 { - bg(theme.darker.term, content.align_e().full_w()).exact_w(12) -} - -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|format!(" {index} {}", port.port_name()).align_w().full_h()); - let field = field_v(theme, title, names); - border(true, frame, field.exact_wh(20, 1 + ins)).exact_wh(20, 1 + ins) -} - -pub fn view_io_ports <'a, T: PortsSizes<'a>> ( - fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a -) -> impl Draw + 'a { - type Item<'a> = (usize, &'a Arc, &'a [Connect], usize, usize); - iter(items, - move|(_index, name, connections, y, y2): Item<'a>, _| south( - bold(true, fg_bg(fg, bg, east(" 󰣲 ", name).align_w())).full_h(), - iter(||connections.iter(), move|connect: &'a Connect, index|{ - bold(false, fg_bg(fg, bg, &connect.info)).exact_h(1).align_w().push_y(index as u16) - }) - ).exact_h((y2 - y) as u16).push_y(y as u16)) -} - -pub fn view_scenes_clips <'a, S: ScenesSizes<'a>> ( - scenes: impl Fn()->S, - tracks: impl TracksSizes<'a>, - select: &Selection, - editor: Option<&MidiEditor>, - size: &Sizer, - editing: bool, -) -> impl Draw { - let status = fg(Green, format!("{}x{}", size.w(), size.h())).align_se().full_wh(); - let tracks = iter_once(tracks, move|(track_index, track, _, _), _| { - let scenes = iter_once(scenes(), move|(scene_index, scene, _, _), _| { - let (name, theme): (Arc, ItemTheme) = scene_name_theme(scene, track_index); - let f = theme.lightest.term; - let (b, o) = scene_bg(theme, select, track_index, scene_index); - let w = scene_w(track, select, track_index, editor); - let y = scene_y(select, scene_index, editor); - let is_selected = scene_sel(select, track_index, scene_index, editing); - below( - Outer(true, Style::default().fg(o)).full_wh(), - below( - below( - fg_bg(o, b, "".full_wh()), - fg_bg(f, b, bold(true, name)).align_nw().full_wh(), - ), - when(is_selected, editor.map(|e|e.view())).full_wh() - ).full_wh() - ).exact_wh(w, y) - }); - scenes.full_h().exact_w(track.width as u16) - }); - - return size.of(above(status, tracks).full_wh()); - - fn scene_name_theme (scene: &Scene, track_index: usize) -> (Arc, ItemTheme) { - if let Some(Some(clip)) = &scene.clips.get(track_index) { - let clip = clip.read().unwrap(); - (format!(" ⏹ {}", &clip.name).into(), clip.color) - } else { - (" ⏹ -- ".into(), ItemTheme::G[32]) - } - } - - fn scene_bg ( - theme: ItemTheme, select: &Selection, track_index: usize, scene_index: usize - ) -> (Color, Color) { - let mut outline = theme.base.term; - (if select.track() == Some(track_index) && select.scene() == Some(scene_index) { - outline = theme.lighter.term; - theme.light.term - } else if select.track() == Some(track_index) || select.scene() == Some(scene_index) { - outline = theme.darkest.term; - theme.base.term - } else { - theme.dark.term - }, outline) - } - - fn scene_w ( - track: &Track, select: &Selection, track_index: usize, editor: Option<&MidiEditor> - ) -> u16 { - if select.track() == Some(track_index) && let Some(editor) = editor { - (editor.size.w() as usize).max(24).max(track.width) as u16 - } else { - track.width as u16 - } - } - - fn scene_y ( - select: &Selection, scene_index: usize, editor: Option<&MidiEditor> - ) -> u16 { - if select.scene() == Some(scene_index) && let Some(editor) = editor { - editor.size.h().max(12) - } else { - H_SCENE as u16 - } - } - - fn scene_sel (select: &Selection, track_index: usize, scene_index: usize, editing: bool) -> bool { - editing && select.track() == Some(track_index) && select.scene() == Some(scene_index) - } -} - -pub fn view_track_names ( - theme: ItemTheme, - tracks: impl TracksSizes<'_>, - track_count: usize, - scene_count: usize, - selected: &Selection, -) -> impl Draw { - let button = south( - button_3("t", "rack ", format!("{}{track_count}", selected.track() - .map(|track|format!("{track}/")).unwrap_or_default()), false), - button_3("s", "cene ", format!("{}{scene_count}", selected.scene() - .map(|scene|format!("{scene}/")).unwrap_or_default()), false)); - let button_2 = south( - button_2("T", "+", false), - button_2("S", "+", false)); - view_track_row_section(theme, button, button_2, bg(theme.darker.term, - thunk(|to: &mut Tui|{ - for (index, track, x1, _x2) in tracks { - let b = if selected.track() == Some(index) { - track.color.light.term - } else { - track.color.base.term - }; - bg(b, south(east( - format!("·t{index:02} "), - fg(Rgb(255, 255, 255), bold(true, &track.name)) - ).align_nw().full_w(), "")) - .exact_w(track_width(index, track)) - .push_x(x1 as u16) - .draw(to)?; - } - Ok(Some(XYWH(0, 0, 0, 0))) - }).exact_h(2))) -} - -pub fn view_track_outputs ( - theme: ItemTheme, tracks: impl TracksSizes<'_>, midi_outs: impl Iterator, -) -> impl Draw { - view_track_row_section(theme, - south(button_2("o", "utput", false).align_w().full_w(), - thunk(|to: &mut Tui|{ - for port in midi_outs { - let _ = port.port_name().align_w().full_w().draw(to)?; - } - Ok(Some(XYWH(0, 0, 0, 0))) - })), - button_2("O", "+", false), - bg(theme.darker.term, thunk(|to: &mut Tui|{ - for (index, track, _x1, _x2) in tracks { - let f = Rgb(255, 255, 255); - let b = track.color.dark.term; - let iter = ||track.sequencer.midi_outs.iter(); - let draw = |port: &MidiOutput, _|fg(f, bg(b, - format!("·o{index:02} {}", port.port_name()).full_w().align_w()).exact_h(1)); - iter_south(iter, draw).full_h().align_nw() - .exact_w(track_width(index, track)) - .draw(to)?; - } - Ok(Some(XYWH(0, 0, 0, 0))) - }).align_w())) -} - -pub fn view_track_inputs ( - theme: ItemTheme, tracks: impl TracksSizes<'_>, height: u16, -) -> impl Draw { - view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false), - bg(theme.darker.term, thunk(move|to: &mut Tui|{ - for (index, track, _x1, _x2) in tracks { - south( - bg(track.color.base.term, - east!( - either(track.sequencer.monitoring, fg(Green, "●mon "), "·mon "), - either(track.sequencer.recording, fg(Red, "●rec "), "·rec "), - either(track.sequencer.overdub, fg(Yellow, "●dub "), "·dub "), - ).align_w().full_w()), - iter_south(||track.sequencer.midi_ins.iter(), - |port, _|fg_bg(Rgb(255, 255, 255), track.color.dark.term, - format!("·i{index:02} {}", port.port_name()).align_w().full_w())) - ).align_nw().exact_wh(track_width(index, track), height + 1).draw(to)?; - } - Ok(Some(XYWH(0, 0, 0, 0))) - }).align_w())) -} - -pub fn view_scenes_names ( - scenes: impl ScenesSizes<'_>, - select: &Selection, - editor: Option<&MidiEditor>, - editing: bool, -) -> impl Draw { - thunk(move |to: &mut Tui|{ - for (index, scene, ..) in scenes { - view_scene_name(select, editor, index, scene, editing).draw(to)?; - } - Ok(Some(XYWH(1, 1, 1, 1))) - }).exact_w(20) -} - -pub fn view_scene_name ( - select: &Selection, - editor: Option<&MidiEditor>, - index: usize, - scene: &Scene, - editing: bool -) -> impl Draw { - let h = if select.scene() == Some(index) && let Some(_editor) = editor { - 7 - } else { - H_SCENE as u16 - }; - let a = east(format!("·s{index:02} "), - fg(g(255), bold(true, &scene.name))).align_w().full_w(); - let b = when(select.scene() == Some(index) && editing, south( - editor.as_ref().map(|e|e.clip_status()), - editor.as_ref().map(|e|e.edit_status())).align_nw().full_wh()); - let c = if select.scene() == Some(index) { - scene.color.light.term - } else { - scene.color.base.term - }; - bg(c, south(a, b).align_nw()).exact_wh(20, h) -} - -pub fn view_midi_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { - track.map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins)) -} - -pub fn view_midi_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { - track.map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs)) -} - -pub fn view_audio_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { - track.map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins())) -} - -pub fn view_audio_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { - track.map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs())) -} - -pub fn view_track_per <'a, T: Draw + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a -) -> impl Draw { - iter_east(tracks, move|(index, track, x1, x2): (usize, &Track, usize, usize), _|{ - fg_bg( - track.color.lightest.term, - track.color.base.term, - callback(index, track) - ).exact_w((x2 - x1) as u16) - }) -} - -pub fn view_per_track () -> impl Draw {} - -pub fn view_per_track_top () -> impl Draw {} - -pub fn view_inputs (tracks: impl TracksSizes<'_>, midi_ins: &[MidiInput]) -> impl Draw { - let title_1 = button_3("i", "nput ", format!("{}", midi_ins.len()), false).align_w().exact_wh(20, 1); - let title_2 = button_2("I", "+", false).exact_wh(4, 1); - east(title_1, west(title_2, thunk(move|to: &mut Tui|{ - for (_index, track, x1, _x2) in tracks { - let _ = south( - bg(track.color.dark.term, east!( - either(track.sequencer.monitoring, fg(Green, "mon "), "mon "), - either(track.sequencer.recording, fg(Red, "rec "), "rec "), - either(track.sequencer.overdub, fg(Yellow, "dub "), "dub "), - ).exact_w(track.width as u16)).align_w().push_x(x1 as u16), - thunk(move |to: &mut Tui|{ - for (index, port) in midi_ins.iter().enumerate() { - let _ = east( - east( - " ● ", - bold(true, fg(Rgb(255,255,255), port.port_name())) - ).align_w().exact_w(20), - west( - ().exact_w(4), - bg(track.color.darker.term, east!( - either(track.sequencer.monitoring, fg(Green, " ● "), " · "), - either(track.sequencer.recording, fg(Red, " ● "), " · "), - either(track.sequencer.overdub, fg(Yellow, " ● "), " · "), - ).exact_w(track.width as u16).align_w()) - ) - ).push_x(index as u16 * 10).exact_h(1).draw(to)?; - } - todo!() - }) - ).draw(to)?; - } - todo!() - }))) -} - -pub fn view_outputs ( - theme: ItemTheme, - tracks: impl TracksSizes<'_>, - midi_outs: &[MidiOutput], - height: u16, -) -> impl Draw { - - let list = south( - button_3( - "o", "utput", format!("{}", midi_outs.len()), false - ).align_w().full_w().exact_h(1), - thunk(|to: &mut Tui|{ - for (_index, port) in midi_outs.iter().enumerate() { - east( - east(" ● ", fg(Rgb(255,255,255), bold(true, port.port_name()))).align_w(), - format!("{}/{} ", - port.port().get_connections().len(), - port.connections.len()).align_e().full_w().exact_h(1)).full_w().draw(to)?; - for (index, conn) in port.connections.iter().enumerate() { - format!(" c{index:02}{}", conn.info()).align_w().full_w().exact_h(1).draw(to)?; - } - } - todo!(); - }).align_nw().full_wh().exact_h(height - 1) - ); - - view_track_row_section(theme, list, button_2("O", "+", false), - bg(theme.darker.term, thunk(|to: &mut Tui|{ - for (index, track, _x1, _x2) in tracks { - let _ = thunk(|to: &mut Tui|{ - east( - either(true, fg(Green, "play "), "play "), - either(false, fg(Yellow, "solo "), "solo "), - ).align_w().exact_h(1).draw(to)?; - for (_index, port) in midi_outs.iter().enumerate() { - east( - either(true, fg(Green, " ● "), " · "), - either(false, fg(Yellow, " ● "), " · "), - ).align_w().exact_h(1).draw(to)?; - for (_index, _conn) in port.connections.iter().enumerate() { - "".full_w().exact_h(1).draw(to)?; - } - } - todo!() - }).exact_w(track_width(index, track)).draw(to)?; - } - todo!() - }).align_w().full_w())).exact_h(height) -} - -pub fn view_track_devices ( - theme: ItemTheme, - tracks: impl TracksSizes<'_>, - track: Option<&Track>, - h: u16, -) -> impl Draw { - view_track_row_section(theme, - button_3("d", "evice", format!("{}", track.map(|t|t.devices.len()).unwrap_or(0)), false), - button_2("D", "+", false), - iter_once(tracks, move|(_, track, _x1, _x2), index|bg( - track.color.dark.term, - iter_south(move||0..h, - |_, _index|fg_bg( - ItemTheme::G[32].lightest.term, - ItemTheme::G[32].dark.term, - format!(" · {}", "--").align_nw() - ).exact_wh(track.width as u16, 2) - ).align_nw()).exact_wh( - Some(track_width(index, track)), - Some(h + 1), - ))) -} diff --git a/src/app/modes.rs b/src/app/modes.rs deleted file mode 100644 index 9cd59827..00000000 --- a/src/app/modes.rs +++ /dev/null @@ -1,109 +0,0 @@ -use crate::*; - -/// Collection of UI modes. -#[derive(Default, Debug, Clone)] -pub struct Modes( - Arc, Arc>>>>> -); - -impl Modes { - pub fn add (&self, name: &impl AsRef, body: &impl Language) -> Usually<()> { - let mut mode = Mode::default(); - body.each(|item|mode.add(item))?; - self.0.write().unwrap().insert(name.as_ref().into(), Arc::new(mode)); - Ok(()) - } - pub fn get (&self, name: impl AsRef) -> Option>>> { - self.0.read().unwrap().get(name.as_ref()).cloned() - } - pub fn for_each (&self, mut ator: impl FnMut(&str, &Mode>)->T) { - for (k, v) in self.0.read().unwrap().iter() { - let _ = ator(k.as_ref(), v.as_ref()); - } - } - pub fn len (&self) -> usize { - self.0.read().unwrap().len() - } -} - -/// Group of view and keys definitions. -/// -/// ``` -/// let mode = tek::Mode::>::default(); -/// ``` -#[derive(Default, Debug)] pub struct Mode { - pub path: PathBuf, - pub name: Vec, - pub info: Vec, - pub view: Vec, - pub keys: Vec, - pub modes: Modes, -} - -impl Mode> { - /// Add a definition to the mode. - /// - /// Supported definitions: - /// - /// - (name ...) -> name - /// - (info ...) -> description - /// - (keys ...) -> key bindings - /// - (mode ...) -> submode - /// - ... -> view - /// - /// ``` - /// let mut mode: tek::Mode> = Default::default(); - /// mode.add("(name hello)").unwrap(); - /// ``` - pub fn add (&mut self, dsl: impl Language) -> Usually<()> { - Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() { - //println!("Mode::add: {head} {:?}", expr.tail()); - let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); - match head { - "name" => self.add_name(tail)?, - "info" => self.add_info(tail)?, - "keys" => self.add_keys(tail)?, - "mode" => self.add_mode(tail)?, - _ => self.add_view(tail)?, - }; - } else if let Ok(Some(word)) = dsl.word() { - self.add_view(word)?; - } else { - return Err(format!("Mode::add: unexpected: {dsl:?}").into()); - }) - - //DslParse(dsl, ||Err(format!("Mode::add: unexpected: {dsl:?}").into())) - //.word(|word|self.add_view(word)) - //.expr(|expr|expr.head(|head|{ - ////println!("Mode::add: {head} {:?}", expr.tail()); - //let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); - //match head { - //"name" => self.add_name(tail), - //"info" => self.add_info(tail), - //"keys" => self.add_keys(tail)?, - //"mode" => self.add_mode(tail)?, - //_ => self.add_view(tail), - //}; - //})) - } - - fn add_name (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(dsl.src()?.map(|src|self.name.push(src.into()))) - } - fn add_info (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(dsl.src()?.map(|src|self.info.push(src.into()))) - } - fn add_view (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(dsl.src()?.map(|src|self.view.push(src.into()))) - } - fn add_keys (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(Some(dsl.each(|expr|{ self.keys.push(expr.trim().into()); Ok(()) })?)) - } - fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(Some(if let Some(id) = dsl.head()? { - self.modes.add(&id, &dsl.tail())?; - } else { - return Err(format!("Mode::add: self: incomplete: {dsl:?}").into()); - })) - } -} diff --git a/src/app/size.rs b/src/app/size.rs deleted file mode 100644 index 8c80c077..00000000 --- a/src/app/size.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::*; - -impl_has!(Sizer: |self: App|self.size); - -/// 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); - -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); -} diff --git a/src/device/browse.rs b/src/device/browse.rs index 7eccc2dd..a5f61c7a 100644 --- a/src/device/browse.rs +++ b/src/device/browse.rs @@ -1,4 +1,4 @@ -use crate::{*, clock::*, sequence::*, sampler::*}; +use crate::*; def_command!(FileBrowserCommand: |sampler: Sampler|{ //("begin" [] Some(Self::Begin)) @@ -246,7 +246,7 @@ impl<'a> PoolView<'a> { move|clip: Arc>, i: usize|{ let MidiClip { ref name, color, length, .. } = *clip.read().unwrap(); let item_height = 1; - let item_offset = i as u16 * item_height; + let _item_offset = i as u16 * item_height; let selected = i == pool.clip_index(); let b = if selected { color.light.term } else { color.base.term }; let f = color.lightest.term; @@ -317,7 +317,7 @@ impl Browse { fn tui (&self) -> impl Draw { iter_south_fixed(1, ||self.tui_entries(), |entry, _index|entry.origin_w().full_w()) } - fn tui_entries (&self) -> EntriesIterator { + fn tui_entries (&self) -> EntriesIterator<'_, Tui> { EntriesIterator { offset: 0, index: 0, diff --git a/src/device/clock/memo.rs b/src/device/clock/memo.rs index 91a3290c..33691d80 100644 --- a/src/device/clock/memo.rs +++ b/src/device/clock/memo.rs @@ -1,3 +1,4 @@ +#![allow(unused)] use crate::*; #[macro_export] macro_rules! rewrite { diff --git a/src/device/clock/moment.rs b/src/device/clock/moment.rs index ed93e2e2..d0e9eac2 100644 --- a/src/device/clock/moment.rs +++ b/src/device/clock/moment.rs @@ -1,5 +1,5 @@ use crate::*; -use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}}; +use ::std::sync::Arc; use ::atomic_float::AtomicF64; /// A point in time in all time scales (microsecond, sample, MIDI pulse) diff --git a/src/device/clock/ticker.rs b/src/device/clock/ticker.rs index 1cb163b0..29f49a27 100644 --- a/src/device/clock/ticker.rs +++ b/src/device/clock/ticker.rs @@ -1,6 +1,4 @@ use crate::*; -use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}}; -use ::atomic_float::AtomicF64; /// Iterator that emits subsequent ticks within a range. /// diff --git a/src/device/clock/timebase.rs b/src/device/clock/timebase.rs index fef79202..15e3cb87 100644 --- a/src/device/clock/timebase.rs +++ b/src/device/clock/timebase.rs @@ -1,5 +1,5 @@ use crate::*; -use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}}; +use ::std::sync::Arc; use ::atomic_float::AtomicF64; /// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat) diff --git a/src/device/dialog.rs b/src/device/dialog.rs index 3d8424ed..169acb48 100644 --- a/src/device/dialog.rs +++ b/src/device/dialog.rs @@ -1,4 +1,4 @@ -use crate::{*, browse::*, device::*, menu::*}; +use crate::{*, device::*}; /// Various possible dialog modes. /// @@ -127,3 +127,19 @@ impl Dialog { /// FIXME: implement pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() } } + +/// Increment a wrapping counter. +pub const fn wrap_inc (index: usize, count: usize) -> usize { + if count > 0 { (index + 1) % count } else { 0 } +} + +/// Decrement a wrapping counter. +pub const fn wrap_dec (index: usize, count: usize) -> usize { + if count > 0 { + let a = index.overflowing_sub(1).0; + let b = count.saturating_sub(1); + if a < b { a } else { b } + } else { + 0 + } +} diff --git a/src/device/editor.rs b/src/device/editor.rs index 0abc07b5..bd02db77 100644 --- a/src/device/editor.rs +++ b/src/device/editor.rs @@ -1,3 +1,4 @@ +#![allow(unused)] use crate::*; /// Contains state for viewing and editing a clip. diff --git a/src/device/meter.rs b/src/device/meter.rs index fc28c19a..52c17949 100644 --- a/src/device/meter.rs +++ b/src/device/meter.rs @@ -1,3 +1,4 @@ +#![allow(unused)] use crate::*; #[derive(Debug, Default)] pub enum MeteringMode { diff --git a/src/device/sampler.rs b/src/device/sampler.rs index f82c0a68..baeae448 100644 --- a/src/device/sampler.rs +++ b/src/device/sampler.rs @@ -1,5 +1,5 @@ -use crate::{*, device::*, browse::*, mix::*}; - +#![allow(unused)] +use crate::*; pub(crate) use symphonia::{ default::get_codecs, core::{//errors::Error as SymphoniaError, @@ -8,11 +8,6 @@ pub(crate) use symphonia::{ }, }; -mod voice; pub use self::voice::*; -mod sample; pub use self::sample::*; -mod sample_add; pub use self::sample_add::*; -mod sample_kit; pub use self::sample_kit::*; - /// Plays [Voice]s from [Sample]s. /// /// ``` @@ -355,10 +350,6 @@ fn draw_sample ( Ok(label1.len() + label2.len() + 4) } -fn read_sample_data (_: &str) -> Usually<(usize, Vec>)> { - todo!(); -} - def_command!(SamplerCommand: |sampler: Sampler| { RecordToggle { slot: usize } => { let slot = *slot; @@ -405,3 +396,324 @@ def_command!(SamplerCommand: |sampler: Sampler| { //Ok(None) }, }); + +/// A currently playing instance of a sample. +#[derive(Default, Debug, Clone)] pub struct Voice { + pub sample: Arc>, + pub after: usize, + pub position: usize, + pub velocity: f32, +} + +impl Iterator for Voice { + type Item = [f32;2]; + fn next (&mut self) -> Option { + if self.after > 0 { + self.after -= 1; + return Some([0.0, 0.0]) + } + let sample = self.sample.read().unwrap(); + if self.position < sample.end { + let position = self.position; + self.position += 1; + return sample.channels[0].get(position).map(|_amplitude|[ + sample.channels[0][position] * self.velocity * sample.gain, + sample.channels[0][position] * self.velocity * sample.gain, + ]) + } + None + } +} + +/// Collection of samples, one per slot, fixed number of slots. +/// +/// History: Separated to cleanly implement [Default]. +/// +/// ``` +/// let samples = tek::SampleKit([None, None, None, None]); +/// ``` +#[derive(Debug)] pub struct SampleKit ( + pub [Option>>;N] +); + +impl Default for SampleKit { + fn default () -> Self { Self([const { None }; N]) } +} + +impl SampleKit { + pub fn get (&self, index: usize) -> &Option>> { + if index < self.0.len() { + &self.0[index] + } else { + &None + } + } +} + +/// A sound cut. +/// +/// ``` +/// let sample = tek::Sample::default(); +/// let sample = tek::Sample::new("test", 0, 0, vec![]); +/// ``` +#[derive(Default, Debug)] pub struct Sample { + pub name: Arc, + pub start: usize, + pub end: usize, + pub channels: Vec>, + pub rate: Option, + pub gain: f32, + pub color: ItemTheme, +} + +impl Sample { + pub fn new (name: impl AsRef, start: usize, end: usize, channels: Vec>) -> Self { + Self { + name: name.as_ref().into(), + start, + end, + channels, + rate: None, + gain: 1.0, + color: ItemTheme::random(), + } + } + pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { + Voice { + sample: sample.clone(), + after, + position: sample.read().unwrap().start, + velocity: velocity.as_int() as f32 / 127.0, + } + } + pub fn handle_cc (&mut self, controller: u7, value: u7) { + let percentage = value.as_int() as f64 / 127.; + match controller.as_int() { + 20 => { + self.start = (percentage * self.end as f64) as usize; + }, + 21 => { + let length = self.channels[0].len(); + self.end = length.min( + self.start + (percentage * (length as f64 - self.start as f64)) as usize + ); + }, + 22 => { /*attack*/ }, + 23 => { /*decay*/ }, + 24 => { + self.gain = percentage as f32 * 2.0; + }, + 26 => { /* pan */ } + 25 => { /* pitch */ } + _ => {} + } + } + /// Read WAV from file + pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { + let mut channels: Vec> = vec![]; + for channel in wavers::Wav::from_path(src)?.channels() { + channels.push(channel); + } + let mut end = 0; + let mut data: Vec> = vec![]; + for samples in channels.iter() { + let channel = Vec::from(samples.as_ref()); + end = end.max(channel.len()); + data.push(channel); + } + Ok((end, data)) + } + pub fn from_file (path: &PathBuf) -> Usually { + let name = path.file_name().unwrap().to_string_lossy().into(); + let mut sample = Self { name, ..Default::default() }; + // Use file extension if present + let mut hint = Hint::new(); + if let Some(ext) = path.extension() { + hint.with_extension(&ext.to_string_lossy()); + } + let probed = symphonia::default::get_probe().format( + &hint, + MediaSourceStream::new( + Box::new(File::open(path)?), + Default::default(), + ), + &Default::default(), + &Default::default() + )?; + let mut format = probed.format; + let params = &format.tracks().iter() + .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) + .expect("no tracks found") + .codec_params; + let mut decoder = get_codecs().make(params, &Default::default())?; + loop { + match format.next_packet() { + Ok(packet) => sample.decode_packet(&mut decoder, packet)?, + Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(), + Err(err) => return Err(err.into()), + }; + }; + sample.end = sample.channels.iter().fold(0, |l, c|l + c.len()); + Ok(sample) + } + fn decode_packet ( + &mut self, decoder: &mut Box, packet: Packet + ) -> Usually<()> { + // Decode a packet + let decoded = decoder + .decode(&packet) + .map_err(|e|Box::::from(e))?; + // Determine sample rate + let spec = *decoded.spec(); + if let Some(rate) = self.rate { + if rate != spec.rate as usize { + panic!("sample rate changed"); + } + } else { + self.rate = Some(spec.rate as usize); + } + // Determine channel count + while self.channels.len() < spec.channels.count() { + self.channels.push(vec![]); + } + // Load sample + let mut samples = SampleBuffer::new( + decoded.frames() as u64, + spec + ); + if samples.capacity() > 0 { + samples.copy_interleaved_ref(decoded); + for frame in samples.samples().chunks(spec.channels.count()) { + for (chan, frame) in frame.iter().enumerate() { + self.channels[chan].push(*frame) + } + } + } + Ok(()) + } +} + +#[derive(Default, Debug)] pub struct SampleAdd { + pub exited: bool, + pub dir: PathBuf, + pub subdirs: Vec, + pub files: Vec, + pub cursor: usize, + pub offset: usize, + pub sample: Arc>, + pub voices: Arc>>, + pub _search: Option, +} + +impl_draw!(|self: SampleAdd, to: Tui|{ todo!() }); + +impl SampleAdd { + fn exited (&self) -> bool { + self.exited + } + fn exit (&mut self) { + self.exited = true + } + pub fn new ( + sample: &Arc>, + voices: &Arc>> + ) -> Usually { + let dir = std::env::current_dir()?; + let (subdirs, files) = scan(&dir)?; + Ok(Self { + exited: false, + dir, + subdirs, + files, + cursor: 0, + offset: 0, + sample: sample.clone(), + voices: voices.clone(), + _search: None + }) + } + fn rescan (&mut self) -> Usually<()> { + scan(&self.dir).map(|(subdirs, files)|{ + self.subdirs = subdirs; + self.files = files; + }) + } + fn prev (&mut self) { + self.cursor = self.cursor.saturating_sub(1); + } + fn next (&mut self) { + self.cursor = self.cursor + 1; + } + fn try_preview (&mut self) -> Usually<()> { + if let Some(path) = self.cursor_file() { + if let Ok(sample) = Sample::from_file(&path) { + *self.sample.write().unwrap() = sample; + self.voices.write().unwrap().push( + Sample::play(&self.sample, 0, &u7::from(100u8)) + ); + } + //load_sample(&path)?; + //let src = std::fs::File::open(&path)?; + //let mss = MediaSourceStream::new(Box::new(src), Default::default()); + //let mut hint = Hint::new(); + //if let Some(ext) = path.extension() { + //hint.with_extension(&ext.to_string_lossy()); + //} + //let meta_opts: MetadataOptions = Default::default(); + //let fmt_opts: FormatOptions = Default::default(); + //if let Ok(mut probed) = symphonia::default::get_probe() + //.format(&hint, mss, &fmt_opts, &meta_opts) + //{ + //panic!("{:?}", probed.format.metadata()); + //}; + } + Ok(()) + } + fn cursor_dir (&self) -> Option { + if self.cursor < self.subdirs.len() { + Some(self.dir.join(&self.subdirs[self.cursor])) + } else { + None + } + } + fn cursor_file (&self) -> Option { + if self.cursor < self.subdirs.len() { + return None + } + let index = self.cursor.saturating_sub(self.subdirs.len()); + if index < self.files.len() { + Some(self.dir.join(&self.files[index])) + } else { + None + } + } + fn pick (&mut self) -> Usually { + if self.cursor == 0 { + if let Some(parent) = self.dir.parent() { + self.dir = parent.into(); + self.rescan()?; + self.cursor = 0; + return Ok(false) + } + } + if let Some(dir) = self.cursor_dir() { + self.dir = dir; + self.rescan()?; + self.cursor = 0; + return Ok(false) + } + if let Some(path) = self.cursor_file() { + let (end, channels) = read_sample_data(&path.to_string_lossy())?; + let mut sample = self.sample.write().unwrap(); + sample.name = path.file_name().unwrap().to_string_lossy().into(); + sample.end = end; + sample.channels = channels; + return Ok(true) + } + return Ok(false) + } +} + +fn read_sample_data (_: &str) -> Usually<(usize, Vec>)> { + todo!(); +} diff --git a/src/device/sampler/sample.rs b/src/device/sampler/sample.rs deleted file mode 100644 index 06caf01f..00000000 --- a/src/device/sampler/sample.rs +++ /dev/null @@ -1,144 +0,0 @@ -use crate::*; - -/// A sound cut. -/// -/// ``` -/// let sample = tek::Sample::default(); -/// let sample = tek::Sample::new("test", 0, 0, vec![]); -/// ``` -#[derive(Default, Debug)] pub struct Sample { - pub name: Arc, - pub start: usize, - pub end: usize, - pub channels: Vec>, - pub rate: Option, - pub gain: f32, - pub color: ItemTheme, -} - -impl Sample { - pub fn new (name: impl AsRef, start: usize, end: usize, channels: Vec>) -> Self { - Self { - name: name.as_ref().into(), - start, - end, - channels, - rate: None, - gain: 1.0, - color: ItemTheme::random(), - } - } - pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { - Voice { - sample: sample.clone(), - after, - position: sample.read().unwrap().start, - velocity: velocity.as_int() as f32 / 127.0, - } - } - pub fn handle_cc (&mut self, controller: u7, value: u7) { - let percentage = value.as_int() as f64 / 127.; - match controller.as_int() { - 20 => { - self.start = (percentage * self.end as f64) as usize; - }, - 21 => { - let length = self.channels[0].len(); - self.end = length.min( - self.start + (percentage * (length as f64 - self.start as f64)) as usize - ); - }, - 22 => { /*attack*/ }, - 23 => { /*decay*/ }, - 24 => { - self.gain = percentage as f32 * 2.0; - }, - 26 => { /* pan */ } - 25 => { /* pitch */ } - _ => {} - } - } - /// Read WAV from file - pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { - let mut channels: Vec> = vec![]; - for channel in wavers::Wav::from_path(src)?.channels() { - channels.push(channel); - } - let mut end = 0; - let mut data: Vec> = vec![]; - for samples in channels.iter() { - let channel = Vec::from(samples.as_ref()); - end = end.max(channel.len()); - data.push(channel); - } - Ok((end, data)) - } - pub fn from_file (path: &PathBuf) -> Usually { - let name = path.file_name().unwrap().to_string_lossy().into(); - let mut sample = Self { name, ..Default::default() }; - // Use file extension if present - let mut hint = Hint::new(); - if let Some(ext) = path.extension() { - hint.with_extension(&ext.to_string_lossy()); - } - let probed = symphonia::default::get_probe().format( - &hint, - MediaSourceStream::new( - Box::new(File::open(path)?), - Default::default(), - ), - &Default::default(), - &Default::default() - )?; - let mut format = probed.format; - let params = &format.tracks().iter() - .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) - .expect("no tracks found") - .codec_params; - let mut decoder = get_codecs().make(params, &Default::default())?; - loop { - match format.next_packet() { - Ok(packet) => sample.decode_packet(&mut decoder, packet)?, - Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(), - Err(err) => return Err(err.into()), - }; - }; - sample.end = sample.channels.iter().fold(0, |l, c|l + c.len()); - Ok(sample) - } - fn decode_packet ( - &mut self, decoder: &mut Box, packet: Packet - ) -> Usually<()> { - // Decode a packet - let decoded = decoder - .decode(&packet) - .map_err(|e|Box::::from(e))?; - // Determine sample rate - let spec = *decoded.spec(); - if let Some(rate) = self.rate { - if rate != spec.rate as usize { - panic!("sample rate changed"); - } - } else { - self.rate = Some(spec.rate as usize); - } - // Determine channel count - while self.channels.len() < spec.channels.count() { - self.channels.push(vec![]); - } - // Load sample - let mut samples = SampleBuffer::new( - decoded.frames() as u64, - spec - ); - if samples.capacity() > 0 { - samples.copy_interleaved_ref(decoded); - for frame in samples.samples().chunks(spec.channels.count()) { - for (chan, frame) in frame.iter().enumerate() { - self.channels[chan].push(*frame) - } - } - } - Ok(()) - } -} diff --git a/src/device/sampler/sample_add.rs b/src/device/sampler/sample_add.rs deleted file mode 100644 index 4fa88b67..00000000 --- a/src/device/sampler/sample_add.rs +++ /dev/null @@ -1,122 +0,0 @@ -use crate::{*, device::sampler::*}; - -#[derive(Default, Debug)] pub struct SampleAdd { - pub exited: bool, - pub dir: PathBuf, - pub subdirs: Vec, - pub files: Vec, - pub cursor: usize, - pub offset: usize, - pub sample: Arc>, - pub voices: Arc>>, - pub _search: Option, -} - -impl_draw!(|self: SampleAdd, to: Tui|{ todo!() }); - -impl SampleAdd { - fn exited (&self) -> bool { - self.exited - } - fn exit (&mut self) { - self.exited = true - } - pub fn new ( - sample: &Arc>, - voices: &Arc>> - ) -> Usually { - let dir = std::env::current_dir()?; - let (subdirs, files) = scan(&dir)?; - Ok(Self { - exited: false, - dir, - subdirs, - files, - cursor: 0, - offset: 0, - sample: sample.clone(), - voices: voices.clone(), - _search: None - }) - } - fn rescan (&mut self) -> Usually<()> { - scan(&self.dir).map(|(subdirs, files)|{ - self.subdirs = subdirs; - self.files = files; - }) - } - fn prev (&mut self) { - self.cursor = self.cursor.saturating_sub(1); - } - fn next (&mut self) { - self.cursor = self.cursor + 1; - } - fn try_preview (&mut self) -> Usually<()> { - if let Some(path) = self.cursor_file() { - if let Ok(sample) = Sample::from_file(&path) { - *self.sample.write().unwrap() = sample; - self.voices.write().unwrap().push( - Sample::play(&self.sample, 0, &u7::from(100u8)) - ); - } - //load_sample(&path)?; - //let src = std::fs::File::open(&path)?; - //let mss = MediaSourceStream::new(Box::new(src), Default::default()); - //let mut hint = Hint::new(); - //if let Some(ext) = path.extension() { - //hint.with_extension(&ext.to_string_lossy()); - //} - //let meta_opts: MetadataOptions = Default::default(); - //let fmt_opts: FormatOptions = Default::default(); - //if let Ok(mut probed) = symphonia::default::get_probe() - //.format(&hint, mss, &fmt_opts, &meta_opts) - //{ - //panic!("{:?}", probed.format.metadata()); - //}; - } - Ok(()) - } - fn cursor_dir (&self) -> Option { - if self.cursor < self.subdirs.len() { - Some(self.dir.join(&self.subdirs[self.cursor])) - } else { - None - } - } - fn cursor_file (&self) -> Option { - if self.cursor < self.subdirs.len() { - return None - } - let index = self.cursor.saturating_sub(self.subdirs.len()); - if index < self.files.len() { - Some(self.dir.join(&self.files[index])) - } else { - None - } - } - fn pick (&mut self) -> Usually { - if self.cursor == 0 { - if let Some(parent) = self.dir.parent() { - self.dir = parent.into(); - self.rescan()?; - self.cursor = 0; - return Ok(false) - } - } - if let Some(dir) = self.cursor_dir() { - self.dir = dir; - self.rescan()?; - self.cursor = 0; - return Ok(false) - } - if let Some(path) = self.cursor_file() { - let (end, channels) = read_sample_data(&path.to_string_lossy())?; - let mut sample = self.sample.write().unwrap(); - sample.name = path.file_name().unwrap().to_string_lossy().into(); - sample.end = end; - sample.channels = channels; - return Ok(true) - } - return Ok(false) - } -} diff --git a/src/device/sampler/sample_kit.rs b/src/device/sampler/sample_kit.rs deleted file mode 100644 index 382ce678..00000000 --- a/src/device/sampler/sample_kit.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::*; - -/// Collection of samples, one per slot, fixed number of slots. -/// -/// History: Separated to cleanly implement [Default]. -/// -/// ``` -/// let samples = tek::SampleKit([None, None, None, None]); -/// ``` -#[derive(Debug)] pub struct SampleKit ( - pub [Option>>;N] -); - -impl Default for SampleKit { - fn default () -> Self { Self([const { None }; N]) } -} - -impl SampleKit { - pub fn get (&self, index: usize) -> &Option>> { - if index < self.0.len() { - &self.0[index] - } else { - &None - } - } -} diff --git a/src/device/sampler/voice.rs b/src/device/sampler/voice.rs deleted file mode 100644 index 632e5202..00000000 --- a/src/device/sampler/voice.rs +++ /dev/null @@ -1,29 +0,0 @@ -use crate::*; - -/// A currently playing instance of a sample. -#[derive(Default, Debug, Clone)] pub struct Voice { - pub sample: Arc>, - pub after: usize, - pub position: usize, - pub velocity: f32, -} - -impl Iterator for Voice { - type Item = [f32;2]; - fn next (&mut self) -> Option { - if self.after > 0 { - self.after -= 1; - return Some([0.0, 0.0]) - } - let sample = self.sample.read().unwrap(); - if self.position < sample.end { - let position = self.position; - self.position += 1; - return sample.channels[0].get(position).map(|_amplitude|[ - sample.channels[0][position] * self.velocity * sample.gain, - sample.channels[0][position] * self.velocity * sample.gain, - ]) - } - None - } -} diff --git a/src/device/sequence.rs b/src/device/sequence.rs index 368bebb5..573ea4c3 100644 --- a/src/device/sequence.rs +++ b/src/device/sequence.rs @@ -1,5 +1,4 @@ -use crate::{*, clock::*, device::*}; - +use crate::*; impl +AsMut> HasSequencer for T {} diff --git a/src/tek.edn b/src/tek.edn index ab8d2229..4dda6306 100644 --- a/src/tek.edn +++ b/src/tek.edn @@ -6,15 +6,18 @@ (padding 3 1 :browse-title) (enclose (fg (g 96)) browser))) -(mode :transport (name Transport) (info JACK transport controller.) (keys :clock :global) +(mode :transport + (name Transport) + (info JACK transport controller.) + (keys :clock :global) :transport) (mode :menu (name Menu) (info Mode selector.) (keys :axis/y :confirm) - (view (bg (g 0) - (bsp/s (max/y 2 :transport + (view (bg (g 64) + (bsp/s (max/xy 80 2 :transport) (bsp/s (max/y 3 (bg (g 80) :ports/out)) (bsp/n (max/y 3 (bg (g 80) :ports/in)) - (bg (g 30) (bsp/s (max/h 6 (bg (g 70) :logo) :dialog/menu)))))))))) + (bg (g 30) (bsp/s (max/y 6 :logo) :dialog/menu)))))))) (mode :sequencer (name Sequencer) (info MIDI sequencer.) (keys :editor :clock :global) diff --git a/src/tek.rs b/src/tek.rs index 7e13aa6a..4d918519 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -1,11 +1,5 @@ #![allow(clippy::unit_arg)] -#![feature( - adt_const_params, - anonymous_lifetime_in_impl_trait, - impl_trait_in_assoc_type, - trait_alias, - type_changing_struct_update -)] +#![feature(impl_trait_in_assoc_type)] // Used by EntriesIterator; TODO remove pub extern crate atomic_float; pub extern crate xdg; pub extern crate tengri; @@ -50,13 +44,14 @@ pub(crate) use ::{ #[allow(unused)] fn main () -> Usually<()> { tengri::Tui::setup_panic(); #[cfg(feature = "cli")] { - Config::watch(crate::cli::run_with_config).map(|_|()) + Config::watched(crate::cli::run_with_config).map(|_|()) } #[cfg(not(feature = "cli"))] { - Config::watch(run_new_plain).map(|_|()) + Config::watched(run_new_plain).map(|_|()) } } +#[cfg(not(feature = "cli"))] fn run_new_plain (config: Config) -> Usually<()> { let name = "tek"; tengri::Tui::run_main(Jack::new_run(name, move|jack|{ @@ -125,7 +120,7 @@ pub(crate) const HEADER: &'static str = r#" use Action::*; match self { Version => show_version(), - Config => print_config(&config), + Config => config.print(), Resume => todo!("resume session"), List => todo!("list sessions"), New(sesh) => Tui::run_main( @@ -236,12 +231,6 @@ pub(crate) const HEADER: &'static str = r#" pub use self::app::*; mod app { use crate::*; - pub mod audio; #[allow(unused)] pub use self::audio::*; - pub mod bind; pub use self::bind::*; - pub mod config; pub use self::config::*; - pub mod draw; pub use self::draw::*; - pub mod modes; pub use self::modes::*; - pub mod size; pub use self::size::*; primitive!(u8: try_to_u8); primitive!(u16: try_to_u16); primitive!(usize: try_to_usize); @@ -517,15 +506,15 @@ mod app { if let Some(expr) = src.expr()? { match (expr.head()?, expr.tail()?) { (Some("g"), Some(tail)) => { - let n = try_to_u8(expr.tail().map_err(Into::into))?.ok_or(LanguageError::Domain("not gray"))?; + let n = try_to_u8(tail.head().map_err(Into::into))?.ok_or(LanguageError::Domain("not gray"))?; Ok(Some(Color::Rgb(n, n, n))) }, (Some("rgb"), Some(tail)) => { - let r = try_to_u8(expr.tail().map_err(Into::into))? + let r = try_to_u8(tail.head().map_err(Into::into))? .ok_or(LanguageError::Domain("not red"))?; - let g = try_to_u8(expr.tail().tail().head().map_err(Into::into))? + let g = try_to_u8(tail.tail().head().map_err(Into::into))? .ok_or(LanguageError::Domain("not green"))?; - let b = try_to_u8(expr.tail().tail().tail().head().map_err(Into::into))? + let b = try_to_u8(tail.tail().tail().head().map_err(Into::into))? .ok_or(LanguageError::Domain("not blue"))?; Ok(Some(Color::Rgb(r, g, b))) }, @@ -635,6 +624,214 @@ mod app { files.sort(); Ok((subdirs, files)) } + + tui_keys!(self: App, input { + let commands = tek_commands_collect(self, input)?; + let results = tek_commands_execute(self, commands)?; + self.history.extend(results.into_iter()); + Ok(()) + }); + + fn tek_commands_collect (app: &App, input: &TuiEvent) + -> Usually> + { + let mut commands = vec![]; + if let Some(ref mode) = app.mode { + for id in 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) { + 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_commands_execute (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(crate) fn load_bind (binds: &Binds, name: &impl AsRef, body: &impl Language) -> Usually<()> { + binds.write().unwrap().insert(name.as_ref().into(), Bind::load(body)?); + Ok(()) + } + + impl Bind> { + pub fn load (lang: &impl Language) -> Usually { + let mut map = Self::new(); + lang.each(|item|if item.expr().head() == Ok(Some("see")) { + // TODO + Ok(()) + } else if let Ok(Some(_word)) = item.expr().head().word() { + if let Some(event) = TuiKey::from_dsl(item.expr()?.head()?)?.to_crossterm() { + map.add(TuiEvent(event), Binding { + commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(), + condition: None, + description: None, + source: None + }); + Ok(()) + } else if Some(":char") == item.expr()?.head()? { + // TODO + return Ok(()) + } else { + return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into()) + } + } else { + return Err(format!("Config::load_bind: unexpected: {item:?}").into()) + })?; + Ok(map) + } + } + + /// Default is always empty map regardless if `E` and `C` implement [Default]. + impl Default for Bind { + fn default () -> Self { Self(Default::default()) } + } + + impl Default for Binding { + fn default () -> Self { + Self { + commands: Default::default(), + condition: Default::default(), + description: Default::default(), + source: Default::default(), + } + } + } + + impl Bind { + /// Create a new event map + pub fn new () -> Self { + Default::default() + } + /// Add a binding to an owned event map. + pub fn def (mut self, event: E, binding: Binding) -> Self { + self.add(event, binding); + self + } + /// Add a binding to an event map. + pub fn add (&mut self, event: E, binding: Binding) -> &mut Self { + if !self.0.contains_key(&event) { + self.0.insert(event.clone(), Default::default()); + } + self.0.get_mut(&event).unwrap().push(binding); + self + } + /// Return the binding(s) that correspond to an event. + pub fn query (&self, event: &E) -> Option<&[Binding]> { + self.0.get(event).map(|x|x.as_slice()) + } + /// Return the first binding that corresponds to an event, considering conditions. + pub fn dispatch (&self, event: &E) -> Option<&Binding> { + self.query(event) + .map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next()) + .flatten() + } + } + + impl_debug!(Condition |self, w| { write!(w, "*") }); + + impl_default!(AppCommand: Self::Nop); + + def_command!(AppCommand: |app: App| { + Nop => Ok(None), + Cancel => todo!(), // TODO delegate: + Confirm => app.confirm(), + Inc { axis: ControlAxis } => app.inc(axis), + Dec { axis: ControlAxis } => app.dec(axis), + SetDialog { dialog: Dialog } => { + swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }) + }, + }); + + 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, + }); + } + + /// A control axis. + /// + /// ``` + /// let axis = tek::ControlAxis::X; + /// ``` + #[derive(Debug, Copy, Clone)] pub enum ControlAxis { + X, Y, Z, I + } + + //take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref() + //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); + + impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } } + + impl_audio!(App: tek_jack_process, tek_jack_event); + + 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 = 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.process_tracks(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::device::*; @@ -738,7 +935,6 @@ mod device { } } - pub use ::tengri::sing::*; pub mod arrange; pub use self::arrange::*; pub mod browse; pub use self::browse::*; pub mod clock; pub use self::clock::*; @@ -836,3 +1032,1166 @@ pub fn show_version () { //Ok(app) //})?)? //} + +pub use self::config::*; +mod config { + use crate::*; + use std::path::PathBuf; + use notify_debouncer_mini::{ + new_debouncer, Debouncer, DebounceEventResult, + notify::{RecursiveMode, RecommendedWatcher} + }; + + /// Configuration: mode, view, and bind definitions. + /// + /// ``` + /// let config = tek::Config::default(); + /// ``` + /// + /// ``` + /// // Some dizzle. + /// // What indentation to use here lol? + /// let source = stringify!((mode :menu (name Menu) + /// (info Mode selector.) (keys :axis/y :confirm) + /// (view (bg (g 0) (bsp/s :ports/out + /// (bsp/n :ports/in + /// (bg (g 30) (bsp/s (fixed/y 7 :logo) + /// (fill :dialog/menu))))))))); + /// // Add this definition to the config and try to load it. + /// // A "mode" is basically a state machine + /// // with associated input and output definitions. + /// tek::Config::default().add(&source).unwrap().get_mode(":menu").unwrap(); + /// ``` + #[derive(Default, Debug)] + pub struct Config { + /// XDG base directories of running user. + pub dirs: BaseDirectories, + /// Active collection of interaction modes. + pub modes: Modes, + /// Active collection of event bindings. + pub binds: Binds, + /// Active collection of view definitions. + pub views: Views, + } + + /// Collection of custom view definitions. + pub type Views = Arc, Arc>>>; + + /// Collection of input bindings. + pub type Binds = Arc, Bind>>>>; + + /// An map of input events (e.g. [TuiEvent]) to [Binding]s. + /// + /// ``` + /// let lang = "(@x (nop)) (@y (nop) (nop))"; + /// let bind = tek::Bind::>::load(&lang).unwrap(); + /// assert_eq!(bind.query(&'x'.into()).map(|x|x.len()), Some(1)); + /// //assert_eq!(bind.query(&'y'.into()).map(|x|x.len()), Some(2)); + /// ``` + #[derive(Debug)] + pub struct Bind( + /// Map of each event (e.g. key combination) to + /// all command expressions bound to it by + /// all loaded input layers. + pub BTreeMap>> + ); + + /// A sequence of zero or more commands (e.g. [AppCommand]), + /// optionally filtered by [Condition] to form layers. + /// + /// ``` + /// //FIXME: Why does it overflow? + /// //let binding: Binding<()> = tek::Binding { ..Default::default() }; + /// ``` + #[derive(Debug, Clone)] pub struct Binding { + pub commands: Arc<[C]>, + pub condition: Option, + pub description: Option>, + pub source: Option>, + } + + /// Condition that must evaluate to true in order to enable an input layer. + /// + /// ``` + /// let condition = tek::Condition(std::sync::Arc::new(Box::new(||{true}))); + /// ``` + #[derive(Clone)] + pub struct Condition( + pub Arcbool + Send + Sync>> + ); + + /// Collection of UI modes. + /// + /// ``` + /// let mut modes = tek::Modes::default(); + /// assert_eq!(modes.len(), 0); + /// let _ = modes.add(&":foo", &"(mode (name Foo) (info Bar))"); + /// assert_eq!(modes.len(), 1); + /// ``` + #[derive(Default, Debug, Clone)] + pub struct Modes(Arc, Arc>>>>>); + + /// Group of view and keys definitions. + /// + /// ``` + /// let mode = tek::Mode::>::default(); + /// ``` + #[derive(Default, Debug)] + pub struct Mode { + pub path: PathBuf, + pub name: Vec, + pub info: Vec, + pub view: Vec, + pub keys: Vec, + pub modes: Modes, + } + + impl Config { + const CONFIG_DIR: &'static str = "tek"; + const CONFIG_SUB: &'static str = "v0"; + const CONFIG: &'static str = "tek.edn"; + const DEFAULTS: &'static str = include_str!("tek.edn"); + + pub fn watched (callback: impl FnOnce(Self)->T) -> Usually { + let config = Self::init_new(None)?; + let _watch = config.watch(None)?; + let result = callback(config); + Ok(result) + } + + pub fn init_new (_dirs: Option) -> Usually { + let mut config = Self::new(None); + config.init()?; + Ok(config) + } + + /// Create a new app configuration from a set of XDG base directories, + pub fn new (dirs: Option) -> Self { + let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB); + let dirs = dirs.unwrap_or_else(default); + Self { dirs, ..Default::default() } + } + + pub fn watch (&self, bounce: Option) -> Usually> { + let mut debouncer = new_debouncer( + bounce.unwrap_or(Duration::from_millis(500)), + |events: DebounceEventResult| { + panic!("updated: {events:?}"); + } + )?; + if let Some(path) = self.get_file() { + debouncer.watcher().watch(&path, RecursiveMode::NonRecursive)?; + Ok(debouncer) + } else { + Err(format!("no config path").into()) + } + } + + fn find_file (&self) -> Option { + self.dirs.find_config_file(Self::CONFIG) + } + + fn place_file (&self) -> Result { + self.dirs.place_config_file(Self::CONFIG) + } + + fn get_file (&self) -> Option { + self.dirs.get_config_file(Self::CONFIG) + } + + /// Write initial contents of configuration. + pub fn init (&mut self) -> Usually<()> { + self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|{ + cfgs.add(&dsl)?; + Ok(()) + })?; + Ok(()) + } + + /// Write initial contents of a configuration file. + pub fn init_one ( + &mut self, + path: &str, + defaults: &str, + mut each: impl FnMut(&mut Self, &str)->Usually<()> + ) -> Usually<()> { + if self.find_file().is_none() { + //println!("Creating {path:?}"); + std::fs::write(self.place_file()?, defaults)?; + } + Ok(if let Some(path) = self.find_file() { + //println!("Loading {path:?}"); + let src = std::fs::read_to_string(&path)?; + src.as_str().each(move|item|each(self, item))?; + } else { + return Err(format!("{path}: not found").into()) + }) + } + + /// Add statements to configuration from [Dsl] source. + pub fn add (&mut self, dsl: impl Language) -> Usually<&mut Self> { + dsl.each(|item|self.add_one(item))?; + Ok(self) + } + + fn add_one (&self, item: impl Language) -> Usually<()> { + if let Some(expr) = item.expr()? { + let head = expr.head()?; + let tail = expr.tail()?; + let name = tail.head()?; + let body = tail.tail()?; + //println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default()); + match head { + Some("mode") if let Some(name) = name => self.modes.add(&name, &body)?, + Some("keys") if let Some(name) = name => load_bind(&self.binds, &name, &body)?, + Some("view") if let Some(name) = name => load_view(&self.views, &name, &body)?, + _ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into()) + } + Ok(()) + } else { + return Err(format!("Config::load: expected expr, got: {item:?}").into()) + } + } + + pub fn get_mode (&self, mode: impl AsRef) -> Option>>> { + self.modes.get(mode) + } + + pub fn print (&self) { + use ::ansi_term::Color::*; + println!("{:?}", self.dirs); + for (k, v) in self.views.read().unwrap().iter() { + println!("{} {} {v}", Green.paint("VIEW"), Green.bold().paint(format!("{k:<16}"))); + } + for (k, v) in self.binds.read().unwrap().iter() { + println!("{} {}", Green.paint("BIND"), Green.bold().paint(format!("{k:<16}"))); + for (k, v) in v.0.iter() { + print!("{} ", &Yellow.paint(match &k.0 { + Event::Key(KeyEvent { modifiers, .. }) => + format!("{:>16}", format!("{modifiers}")), + _ => unimplemented!() + })); + print!("{}", &Yellow.bold().paint(match &k.0 { + Event::Key(KeyEvent { code, .. }) => + format!("{:<10}", format!("{code}")), + _ => unimplemented!() + })); + for v in v.iter() { + print!(" => {:?}", v.commands); + print!(" {}", v.condition.as_ref().map(|x|format!("{x:?}")).unwrap_or_default()); + println!(" {}", v.description.as_ref().map(|x|x.as_ref()).unwrap_or_default()); + //println!(" {:?}", v.source); + } + } + } + self.modes.for_each(|k, v|{ + println!(); + for v in v.name.iter() { print!("{}", Green.bold().paint(format!("{v} "))); } + for v in v.info.iter() { print!("\n{}", Green.paint(format!("{v}"))); } + print!("\n{} {}", Blue.paint("TOOL"), Green.bold().paint(format!("{k:<16}"))); + print!("\n{}", Blue.paint("KEYS")); + for v in v.keys.iter() { print!("{}", Green.paint(format!(" {v}"))); } + println!(); + v.modes.for_each(|k, v|{ + print!("{} {} {:?}", Blue.paint("MODE"), Green.bold().paint(format!("{k:<16}")), v.name); + print!( " INFO={:?}", v.info); + print!( " VIEW={:?}", v.view); + println!(" KEYS={:?}", v.keys); + }); + print!("{}", Blue.paint("VIEW")); + for v in v.view.iter() { print!("{}", Green.paint(format!(" {v}"))); } + println!(); + }); + } + } + + impl Modes { + pub fn add (&self, name: &impl AsRef, body: &impl Language) -> Usually<()> { + let mut mode = Mode::default(); + body.each(|item|mode.add(item))?; + self.0.write().unwrap().insert(name.as_ref().into(), Arc::new(mode)); + Ok(()) + } + pub fn get (&self, name: impl AsRef) -> Option>>> { + self.0.read().unwrap().get(name.as_ref()).cloned() + } + pub fn for_each (&self, mut ator: impl FnMut(&str, &Mode>)->T) { + for (k, v) in self.0.read().unwrap().iter() { + let _ = ator(k.as_ref(), v.as_ref()); + } + } + pub fn len (&self) -> usize { + self.0.read().unwrap().len() + } + } + + impl Mode> { + /// Add a definition to the mode. + /// + /// Supported definitions: + /// + /// - (name ...) -> name + /// - (info ...) -> description + /// - (keys ...) -> key bindings + /// - (mode ...) -> submode + /// - ... -> view + /// + /// ``` + /// let mut mode: tek::Mode> = Default::default(); + /// mode.add("(name hello)").unwrap(); + /// ``` + pub fn add (&mut self, dsl: impl Language) -> Usually<()> { + Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() { + //println!("Mode::add: {head} {:?}", expr.tail()); + let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); + match head { + "name" => self.add_name(tail)?, + "info" => self.add_info(tail)?, + "keys" => self.add_keys(tail)?, + "mode" => self.add_mode(tail)?, + _ => self.add_view(tail)?, + }; + } else if let Ok(Some(word)) = dsl.word() { + self.add_view(word)?; + } else { + return Err(format!("Mode::add: unexpected: {dsl:?}").into()); + }) + + //DslParse(dsl, ||Err(format!("Mode::add: unexpected: {dsl:?}").into())) + //.word(|word|self.add_view(word)) + //.expr(|expr|expr.head(|head|{ + ////println!("Mode::add: {head} {:?}", expr.tail()); + //let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); + //match head { + //"name" => self.add_name(tail), + //"info" => self.add_info(tail), + //"keys" => self.add_keys(tail)?, + //"mode" => self.add_mode(tail)?, + //_ => self.add_view(tail), + //}; + //})) + } + + fn add_name (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.name.push(src.into()))) + } + fn add_info (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.info.push(src.into()))) + } + fn add_view (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.view.push(src.into()))) + } + fn add_keys (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(Some(dsl.each(|expr|{ self.keys.push(expr.trim().into()); Ok(()) })?)) + } + fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(Some(if let Some(id) = dsl.head()? { + self.modes.add(&id, &dsl.tail())?; + } else { + return Err(format!("Mode::add: self: incomplete: {dsl:?}").into()); + })) + } + } +} + +pub use self::draw::*; +mod draw { + use crate::*; + + /// Load custom view definition. + pub(crate) fn load_view ( + views: &Views, + name: &impl AsRef, + body: &impl Language, + ) -> Usually<()> { + views.write().unwrap().insert( + name.as_ref().into(), + body.src()?.unwrap_or_default().into() + ); + Ok(()) + } + + /// 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 View for App { + fn view (&self) -> impl Draw { + thunk(|to: &mut Tui|{ + let xywh = to.area().into(); + + if let Some(e) = self.error.read().unwrap().as_ref() { + //to.show(area(xywh, format!("KYPbanica {xywh:?}").align_c()))?; + to.show(e.as_ref().align_c())?; + } + + if let Some(ref mode) = self.mode { + for (index, dsl) in mode.view.iter().enumerate() { + if let Err(e) = self.interpret(to, dsl) { + let src = &dsl.src().unwrap_or(Some("")).unwrap_or(""); + let message = format!("Mode: {:?}\nLayer: #{index}\nError: {e}\nSource:\n{}", &mode.name, &src); + *self.error.write().unwrap() = Some(message.into()); + break; + } + } + } + + to.show(ShowSize.align_se())?; + + Ok(Some(xywh)) + }) + } + } + + impl Interpret>> for App { + fn interpret_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Drawn { + tek_draw_expr(self, to, lang) + } + fn interpret_word <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Drawn { + tek_draw_word(self, to, lang) + } + } + + fn tek_draw_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Drawn { + Ok(Some(if let Some(area) = eval_view(state, to, lang)? { + area + } else if let Some(area) = eval_view_tui(state, to, lang)? { + area + } else { + return Err(format!("App::interpret_expr: unexpected: {lang:?}").into()) + })) + } + + fn tek_draw_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Drawn { + let mut frags = dsl.src()?.unwrap().split("/"); + match frags.next() { + //Some(":logo") => view_logo().draw(to), + Some(":meters") => draw_meter_section(to, frags), + Some(":tracks") => draw_tracks(to, frags, state), + Some(":scenes") => draw_scenes(to, frags), + Some(":dialog") => draw_dialog(to, frags, state, dsl), + Some(":templates") => draw_templates(to, frags, state), + Some(":sessions") => view_sessions().draw(to), + Some(":browse/title") => view_browse_title(state).draw(to), + Some(":device") => view_device(state).draw(to), + Some(":status") => "TODO: Status Bar".exact_h(1).draw(to), + Some(":editor") => "TODO Editor".draw(to), + Some(":transport") => view_transport(true, "", "", "").draw(to), + Some(":debug") => format!("[{:?}]", to.area()).exact_h(1).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!() + } + } + + impl_has!(Sizer: |self: App|self.size); + + /// 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 {} + impl<'a, T: Iterator + Send + Sync + 'a> $Type<'a> for T {} + } + } + + def_sizes_iter!(PortsSizes => Arc, [Connect]); + def_sizes_iter!(ScenesSizes => Scene); + def_sizes_iter!(TracksSizes => Track); + + 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 fn draw_meter_section (to: &mut Tui, mut frags: std::str::Split<&str>) -> Drawn { + match frags.next() { + Some("input") => bg(Rgb(30, 30, 30), "Input Meters".align_s().full_h()).draw(to), + Some("output") => bg(Rgb(30, 30, 30), "Output Meters".align_s().full_h()).draw(to), + _ => panic!() + } + } + + pub fn draw_tracks (to: &mut Tui, mut frags: std::str::Split<&str>, state: &App) -> Drawn { + 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), full_w(align_w("Track Names")))), + Some("inputs") => bg(Rgb(40, 40, 40), "Track Inputs".align_w().full_w()).draw(to), + Some("devices") => bg(Rgb(40, 40, 40), "Track Devices".align_w().full_w()).draw(to), + Some("outputs") => bg(Rgb(40, 40, 40), "Track Outputs".align_w().full_w()).draw(to), + _ => panic!() + } + } + + pub fn draw_scenes (to: &mut Tui, mut frags: std::str::Split<&str>) -> Drawn { + match frags.next() { + None => "TODO Scenes".draw(to), + Some(":scenes/names") => "TODO Scene Names".draw(to), + _ => panic!() + } + } + + pub fn draw_dialog ( + to: &mut Tui, mut frags: std::str::Split<&str>, state: &App, dsl: &impl Expression) -> Drawn { + match frags.next() { + Some("menu") => if let Dialog::Menu(selected, items) = &state.dialog { + let items = items.clone(); + let selected = selected; + Some(thunk(move|to: &mut Tui|{ + for (index, MenuItem(item, _)) in items.0.iter().enumerate() { + let f = if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }; + let b = if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }; + fg_bg(f, b, item.full_w().align_w().exact_h(2)) + .push_y((4 * index) as u16).draw(to)?; + } + Ok(Some(to.area().into())) + }).full_wh()) + } else { + None + }.draw(to), + _ => unimplemented!("App::interpret_word: {dsl:?} ({frags:?})"), + } + } + + pub fn draw_templates (to: &mut Tui, _frags: std::str::Split<&str>, state: &App) -> Drawn { + let height = (state.config.modes.len() * 2) as u16; + thunk(move |to: &mut Tui|{ + let mut index = 0; + state.config.modes.for_each(|id, profile| { + let b = 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 = fg(fg1, name).align_w().full_w(); + let field_id = fg(fg2, id).align_e().full_w(); + let field_info = info.align_w().full_w(); + let _ = bg(b, south(above(field_name, field_id), field_info)) + .full_w().exact_h(2).push_y((2 * index) as u16).draw(to); + index += 1; + }); + Ok(Some(to.area().into())) + }).min_w(30).exact_h(height).draw(to) + } + + pub fn per_track <'a, T: Draw + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a + ) -> impl Draw + 'a { + per_track_top(tracks, move|index, track|callback(index, track).full_h().align_y()) + } + + pub fn per_track_top <'a, T: Draw + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a + ) -> impl Draw + 'a { + bg(Reset, iter_east(tracks, + move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{ + fg_bg( + track.color.lightest.term, + track.color.base.term, + callback(index, track) + ).exact_w((x2 - x1) as u16) + }).align_x()) + } + + pub fn field_h ( + _theme: ItemTheme, _head: impl Draw, _body: impl Draw + ) -> impl Draw { + } + + pub fn field_v ( + _theme: ItemTheme, _head: impl Draw, _body: impl Draw + ) -> impl Draw { + } + + pub fn view_sessions () -> impl Draw { + let h = 6; + let w = Some(30); + let f = Rgb(224, 192, 128); + thunk(move |to: &mut Tui|{ + for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { + let b = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; + let y = (2 * index) as u16; + let h = 2; + bg(b, fg(f, *name).align_w()).full_w().exact_h(h).push_y(y).draw(to)?; + } + Ok(Some(to.area().into())) + }).min_w(w).exact_h(h) + } + + pub fn view_browse_title (state: &App) -> impl Draw { + field_v(ItemTheme::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:", + }, fg(g(96), x_repeat("🭻")).exact_h(1) + ).align_w().full_w() + } + + pub fn view_device (state: &App) -> impl Draw { + let selected = state.dialog.device_kind().unwrap(); + south(bold(true, "Add device"), iter_south( + move||device_kinds().iter(), + move|_label: &&'static str, i|{ + let b = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; + let l = if i == selected { "[ " } else { " " }; + let r = if i == selected { " ]" } else { " " }; + bg(b, east(l, west(r, "FIXME device name"))).full_w() + })) + } + + /// ``` + /// let x = ""; + /// let _ = tek::view_transport(true, x.as_ref(), x.as_ref(), x.as_ref()); + /// let _ = tek::view_transport(false, x.as_ref(), x.as_ref(), x.as_ref()); + /// ``` + pub fn view_transport (play: bool, bpm: &str, beat: &str, time: &str) -> impl Draw { + let theme = ItemTheme::G[96]; + bg(Black, east!(above( + button_play_pause(play, false).align_w(), + east!( + field_h(theme, "BPM", bpm), + field_h(theme, "Beat", beat), + field_h(theme, "Time", time), + ).align_e().full_wh() + ))) + } + + /// ``` + /// let x = ""; + /// let _ = tek::view_status(None, x.as_ref(), x.as_ref(), x.as_ref()); + /// let _ = tek::view_status(Some("".into()), x.as_ref(), x.as_ref(), x.as_ref()); + /// ``` + 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( + sel.map(|sel|field_h(theme, "Selected", sel)).align_w().full_wh(), + east!(sr, buf, lat).align_e().full_wh(), + ))) + } + + /// ``` + /// let _ = tek::button_play_pause(true, true); + /// let _ = tek::button_play_pause(true, false); + /// let _ = tek::button_play_pause(false, true); + /// let _ = tek::button_play_pause(false, false); + /// ``` + pub fn button_play_pause (playing: bool, compact: bool) -> impl Draw { + bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, + either(compact, + thunk(move|to: &mut Tui|either(playing, + fg(Rgb(0, 255, 0), " PLAYING "), + fg(Rgb(255, 128, 0), " STOPPED "), + ).exact_w(9).draw(to)), + thunk(move|to: &mut Tui|either(playing, + fg(Rgb(0, 255, 0), south(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), + fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",)), + ).exact_w(5).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( + button_add.align_nw().exact_w(4).full_h(), + east( + button.align_nw().full_h().exact_w(20), + content.align_c().full_wh() + ) + ) + } + + /// ``` + /// 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, y_repeat("▐").exact_w(1)); + let right = fg_bg(bg, Reset, y_repeat("▌").exact_w(1)); + 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, bg(c, ()).exact_wh(w, 1)) + } + + 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 view_sample_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 view_sample_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; + south!( + field_h(theme, "Name ", format!("{:<10}", sample.name.clone())) .align_w().full_w(), + field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())).align_w().full_w(), + field_h(theme, "Start ", format!("{:<8}", sample.start)) .align_w().full_w(), + field_h(theme, "End ", format!("{:<8}", sample.end)) .align_w().full_w(), + field_h(theme, "Trans ", "0") .align_w().full_w(), + field_h(theme, "Gain ", format!("{}", sample.gain)) .align_w().full_w(), + ).exact_w(20).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 view_sample_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 { + bg(theme.darker.term, content.align_e().full_w()).exact_w(12) + } + + 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|format!(" {index} {}", port.port_name()).align_w().full_h()); + let field = field_v(theme, title, names); + border(true, frame, field.exact_wh(20, 1 + ins)).exact_wh(20, 1 + ins) + } + + pub fn view_io_ports <'a, T: PortsSizes<'a>> ( + fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a + ) -> impl Draw + 'a { + type Item<'a> = (usize, &'a Arc, &'a [Connect], usize, usize); + iter(items, + move|(_index, name, connections, y, y2): Item<'a>, _| south( + bold(true, fg_bg(fg, bg, east(" 󰣲 ", name).align_w())).full_h(), + iter(||connections.iter(), move|connect: &'a Connect, index|{ + bold(false, fg_bg(fg, bg, &connect.info)).exact_h(1).align_w().push_y(index as u16) + }) + ).exact_h((y2 - y) as u16).push_y(y as u16)) + } + + pub fn view_scenes_clips <'a, S: ScenesSizes<'a>> ( + scenes: impl Fn()->S, + tracks: impl TracksSizes<'a>, + select: &Selection, + editor: Option<&MidiEditor>, + size: &Sizer, + editing: bool, + ) -> impl Draw { + let status = fg(Green, format!("{}x{}", size.w(), size.h())).align_se().full_wh(); + let tracks = iter_once(tracks, move|(track_index, track, _, _), _| { + let scenes = iter_once(scenes(), move|(scene_index, scene, _, _), _| { + let (name, theme): (Arc, ItemTheme) = scene_name_theme(scene, track_index); + let f = theme.lightest.term; + let (b, o) = scene_bg(theme, select, track_index, scene_index); + let w = scene_w(track, select, track_index, editor); + let y = scene_y(select, scene_index, editor); + let is_selected = scene_sel(select, track_index, scene_index, editing); + below( + Outer(true, Style::default().fg(o)).full_wh(), + below( + below( + fg_bg(o, b, "".full_wh()), + fg_bg(f, b, bold(true, name)).align_nw().full_wh(), + ), + when(is_selected, editor.map(|e|e.view())).full_wh() + ).full_wh() + ).exact_wh(w, y) + }); + scenes.full_h().exact_w(track.width as u16) + }); + + return size.of(above(status, tracks).full_wh()); + + fn scene_name_theme (scene: &Scene, track_index: usize) -> (Arc, ItemTheme) { + if let Some(Some(clip)) = &scene.clips.get(track_index) { + let clip = clip.read().unwrap(); + (format!(" ⏹ {}", &clip.name).into(), clip.color) + } else { + (" ⏹ -- ".into(), ItemTheme::G[32]) + } + } + + fn scene_bg ( + theme: ItemTheme, select: &Selection, track_index: usize, scene_index: usize + ) -> (Color, Color) { + let mut outline = theme.base.term; + (if select.track() == Some(track_index) && select.scene() == Some(scene_index) { + outline = theme.lighter.term; + theme.light.term + } else if select.track() == Some(track_index) || select.scene() == Some(scene_index) { + outline = theme.darkest.term; + theme.base.term + } else { + theme.dark.term + }, outline) + } + + fn scene_w ( + track: &Track, select: &Selection, track_index: usize, editor: Option<&MidiEditor> + ) -> u16 { + if select.track() == Some(track_index) && let Some(editor) = editor { + (editor.size.w() as usize).max(24).max(track.width) as u16 + } else { + track.width as u16 + } + } + + fn scene_y ( + select: &Selection, scene_index: usize, editor: Option<&MidiEditor> + ) -> u16 { + if select.scene() == Some(scene_index) && let Some(editor) = editor { + editor.size.h().max(12) + } else { + H_SCENE as u16 + } + } + + fn scene_sel (select: &Selection, track_index: usize, scene_index: usize, editing: bool) -> bool { + editing && select.track() == Some(track_index) && select.scene() == Some(scene_index) + } + } + + pub fn view_track_names <'a> ( + theme: ItemTheme, + tracks: impl TracksSizes<'a>, + track_count: usize, + scene_count: usize, + selected: &Selection, + ) -> impl Draw { + let button = south( + button_3("t", "rack ", format!("{}{track_count}", selected.track() + .map(|track|format!("{track}/")).unwrap_or_default()), false), + button_3("s", "cene ", format!("{}{scene_count}", selected.scene() + .map(|scene|format!("{scene}/")).unwrap_or_default()), false)); + let button_2 = south( + button_2("T", "+", false), + button_2("S", "+", false)); + view_track_row_section(theme, button, button_2, bg(theme.darker.term, + thunk(|to: &mut Tui|{ + for (index, track, x1, _x2) in tracks { + let b = if selected.track() == Some(index) { + track.color.light.term + } else { + track.color.base.term + }; + bg(b, south(east( + format!("·t{index:02} "), + fg(Rgb(255, 255, 255), bold(true, &track.name)) + ).align_nw().full_w(), "")) + .exact_w(track_width(index, track)) + .push_x(x1 as u16) + .draw(to)?; + } + Ok(Some(XYWH(0, 0, 0, 0))) + }).exact_h(2))) + } + + pub fn view_track_outputs <'a> ( + theme: ItemTheme, + tracks: impl TracksSizes<'a>, + midi_outs: impl Iterator, + ) -> impl Draw { + view_track_row_section(theme, + south(button_2("o", "utput", false).align_w().full_w(), + thunk(|to: &mut Tui|{ + for port in midi_outs { + let _ = port.port_name().align_w().full_w().draw(to)?; + } + Ok(Some(XYWH(0, 0, 0, 0))) + })), + button_2("O", "+", false), + bg(theme.darker.term, thunk(|to: &mut Tui|{ + for (index, track, _x1, _x2) in tracks { + let f = Rgb(255, 255, 255); + let b = track.color.dark.term; + let iter = ||track.sequencer.midi_outs.iter(); + let draw = |port: &MidiOutput, _|fg(f, bg(b, + format!("·o{index:02} {}", port.port_name()).full_w().align_w()).exact_h(1)); + iter_south(iter, draw).full_h().align_nw() + .exact_w(track_width(index, track)) + .draw(to)?; + } + Ok(Some(XYWH(0, 0, 0, 0))) + }).align_w())) + } + + pub fn view_track_inputs <'a> ( + theme: ItemTheme, tracks: impl TracksSizes<'a>, height: u16, + ) -> impl Draw { + view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false), + bg(theme.darker.term, thunk(move|to: &mut Tui|{ + for (index, track, _x1, _x2) in tracks { + south( + bg(track.color.base.term, + east!( + either(track.sequencer.monitoring, fg(Green, "●mon "), "·mon "), + either(track.sequencer.recording, fg(Red, "●rec "), "·rec "), + either(track.sequencer.overdub, fg(Yellow, "●dub "), "·dub "), + ).align_w().full_w()), + iter_south(||track.sequencer.midi_ins.iter(), + |port, _|fg_bg(Rgb(255, 255, 255), track.color.dark.term, + format!("·i{index:02} {}", port.port_name()).align_w().full_w())) + ).align_nw().exact_wh(track_width(index, track), height + 1).draw(to)?; + } + Ok(Some(XYWH(0, 0, 0, 0))) + }).align_w())) + } + + pub fn view_scenes_names <'a> ( + scenes: impl ScenesSizes<'a>, + select: &Selection, + editor: Option<&MidiEditor>, + editing: bool, + ) -> impl Draw { + thunk(move |to: &mut Tui|{ + for (index, scene, ..) in scenes { + view_scene_name(select, editor, index, scene, editing).draw(to)?; + } + Ok(Some(XYWH(1, 1, 1, 1))) + }).exact_w(20) + } + + pub fn view_scene_name ( + select: &Selection, + editor: Option<&MidiEditor>, + index: usize, + scene: &Scene, + editing: bool + ) -> impl Draw { + let h = if select.scene() == Some(index) && let Some(_editor) = editor { + 7 + } else { + H_SCENE as u16 + }; + let a = east(format!("·s{index:02} "), + fg(g(255), bold(true, &scene.name))).align_w().full_w(); + let b = when(select.scene() == Some(index) && editing, south( + editor.as_ref().map(|e|e.clip_status()), + editor.as_ref().map(|e|e.edit_status())).align_nw().full_wh()); + let c = if select.scene() == Some(index) { + scene.color.light.term + } else { + scene.color.base.term + }; + bg(c, south(a, b).align_nw()).exact_wh(20, h) + } + + pub fn view_midi_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { + track.map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins)) + } + + pub fn view_midi_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { + track.map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs)) + } + + pub fn view_audio_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { + track.map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins())) + } + + pub fn view_audio_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { + track.map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs())) + } + + pub fn view_track_per <'a, T: Draw + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a + ) -> impl Draw { + iter_east(tracks, move|(index, track, x1, x2): (usize, &Track, usize, usize), _|{ + fg_bg( + track.color.lightest.term, + track.color.base.term, + callback(index, track) + ).exact_w((x2 - x1) as u16) + }) + } + + pub fn view_per_track () -> impl Draw {} + + pub fn view_per_track_top () -> impl Draw {} + + pub fn view_inputs <'a> ( + tracks: impl TracksSizes<'a>, midi_ins: &[MidiInput] + ) -> impl Draw { + let title_1 = button_3("i", "nput ", format!("{}", midi_ins.len()), false).align_w().exact_wh(20, 1); + let title_2 = button_2("I", "+", false).exact_wh(4, 1); + east(title_1, west(title_2, thunk(move|to: &mut Tui|{ + for (_index, track, x1, _x2) in tracks { + let _ = south( + bg(track.color.dark.term, east!( + either(track.sequencer.monitoring, fg(Green, "mon "), "mon "), + either(track.sequencer.recording, fg(Red, "rec "), "rec "), + either(track.sequencer.overdub, fg(Yellow, "dub "), "dub "), + ).exact_w(track.width as u16)).align_w().push_x(x1 as u16), + thunk(move |to: &mut Tui|{ + for (index, port) in midi_ins.iter().enumerate() { + let _ = east( + east( + " ● ", + bold(true, fg(Rgb(255,255,255), port.port_name())) + ).align_w().exact_w(20), + west( + ().exact_w(4), + bg(track.color.darker.term, east!( + either(track.sequencer.monitoring, fg(Green, " ● "), " · "), + either(track.sequencer.recording, fg(Red, " ● "), " · "), + either(track.sequencer.overdub, fg(Yellow, " ● "), " · "), + ).exact_w(track.width as u16).align_w()) + ) + ).push_x(index as u16 * 10).exact_h(1).draw(to)?; + } + todo!() + }) + ).draw(to)?; + } + todo!() + }))) + } + + pub fn view_outputs <'a> ( + theme: ItemTheme, + tracks: impl TracksSizes<'a>, + midi_outs: &[MidiOutput], + height: u16, + ) -> impl Draw { + + let list = south( + button_3( + "o", "utput", format!("{}", midi_outs.len()), false + ).align_w().full_w().exact_h(1), + thunk(|to: &mut Tui|{ + for (_index, port) in midi_outs.iter().enumerate() { + east( + east(" ● ", fg(Rgb(255,255,255), bold(true, port.port_name()))).align_w(), + format!("{}/{} ", + port.port().get_connections().len(), + port.connections.len()).align_e().full_w().exact_h(1)).full_w().draw(to)?; + for (index, conn) in port.connections.iter().enumerate() { + format!(" c{index:02}{}", conn.info()).align_w().full_w().exact_h(1).draw(to)?; + } + } + todo!(); + }).align_nw().full_wh().exact_h(height - 1) + ); + + view_track_row_section(theme, list, button_2("O", "+", false), + bg(theme.darker.term, thunk(|to: &mut Tui|{ + for (index, track, _x1, _x2) in tracks { + let _ = thunk(|to: &mut Tui|{ + east( + either(true, fg(Green, "play "), "play "), + either(false, fg(Yellow, "solo "), "solo "), + ).align_w().exact_h(1).draw(to)?; + for (_index, port) in midi_outs.iter().enumerate() { + east( + either(true, fg(Green, " ● "), " · "), + either(false, fg(Yellow, " ● "), " · "), + ).align_w().exact_h(1).draw(to)?; + for (_index, _conn) in port.connections.iter().enumerate() { + "".full_w().exact_h(1).draw(to)?; + } + } + todo!() + }).exact_w(track_width(index, track)).draw(to)?; + } + todo!() + }).align_w().full_w())).exact_h(height) + } + + pub fn view_track_devices <'a> ( + theme: ItemTheme, + tracks: impl TracksSizes<'a>, + track: Option<&Track>, + h: u16, + ) -> impl Draw { + view_track_row_section(theme, + button_3("d", "evice", format!("{}", track.map(|t|t.devices.len()).unwrap_or(0)), false), + button_2("D", "+", false), + iter_once(tracks, move|(_, track, _x1, _x2), index|bg( + track.color.dark.term, + iter_south(move||0..h, + |_, _index|fg_bg( + ItemTheme::G[32].lightest.term, + ItemTheme::G[32].dark.term, + format!(" · {}", "--").align_nw() + ).exact_wh(track.width as u16, 2) + ).align_nw()).exact_wh( + Some(track_width(index, track)), + Some(h + 1), + ))) + } + +} diff --git a/tengri b/tengri index 25354099..5ca32929 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit 25354099fe3cde43a242d41fdb673c4fce5c943e +Subproject commit 5ca329292f808c137b6e2b7e80892e2a9e855696