From b4424fcb6912fecc19752706a93b0abfc69c7e0b Mon Sep 17 00:00:00 2001 From: i do not exist Date: Tue, 28 Jul 2026 04:15:27 +0300 Subject: [PATCH 1/2] multiline view error --- src/app.rs | 398 ----------- src/app/bind.rs | 16 +- src/app/config.rs | 49 +- src/app/config/modes.rs | 27 - src/app/config/views.rs | 10 - src/app/draw.rs | 179 ++--- src/app/{config/mode.rs => modes.rs} | 28 +- src/deps.rs | 32 - src/device.rs | 144 ---- src/device/arrange.rs | 4 +- src/device/dialog.rs | 8 +- src/device/sequence.rs | 2 +- src/tek.edn | 65 +- src/tek.rs | 964 +++++++++++++++++++++------ tengri | 2 +- 15 files changed, 979 insertions(+), 949 deletions(-) delete mode 100644 src/app.rs delete mode 100644 src/app/config/modes.rs delete mode 100644 src/app/config/views.rs rename src/app/{config/mode.rs => modes.rs} (77%) delete mode 100644 src/deps.rs delete mode 100644 src/device.rs diff --git a/src/app.rs b/src/app.rs deleted file mode 100644 index a583e394..00000000 --- a/src/app.rs +++ /dev/null @@ -1,398 +0,0 @@ -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 size; pub use self::size::*; -primitive!(u8: try_to_u8); -primitive!(u16: try_to_u16); -primitive!(usize: try_to_usize); -primitive!(isize: try_to_isize); -impl_has!(Clock: |self: App|self.project.clock); -impl_has!(Vec: |self: App|self.project.midi_ins); -impl_has!(Vec: |self: App|self.project.midi_outs); -impl_has!(Dialog: |self: App|self.dialog); -impl_has!(Jack<'static>: |self: App|self.jack); -impl_has!(Pool: |self: App|self.pool); -impl_has!(Selection: |self: App|self.project.selection); -impl_as_ref!(Vec: |self: App|self.project.as_ref()); -impl_as_mut!(Vec: |self: App|self.project.as_mut()); -impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt()); -impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt()); -impl_has_clips!( |self: App|self.pool.clips); -/// Total application state. -/// -/// ``` -/// use tek::{HasTracks, HasScenes, TracksView, ScenesView}; -/// let mut app = tek::App::default(); -/// let _ = app.scene_add(None, None).unwrap(); -/// let _ = app.update_clock(); -/// app.project.editor = Some(Default::default()); -/// //let _: Vec<_> = app.project.inputs_with_sizes().collect(); -/// //let _: Vec<_> = app.project.outputs_with_sizes().collect(); -/// let _: Vec<_> = app.project.tracks_with_sizes().collect(); -/// //let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect(); -/// //let _: Vec<_> = app.scenes_with_colors(true, 10).collect(); -/// //let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect(); -/// let _ = app.project.w(); -/// //let _ = app.project.w_sidebar(); -/// //let _ = app.project.w_tracks_area(); -/// let _ = app.project.h(); -/// //let _ = app.project.h_tracks_area(); -/// //let _ = app.project.h_inputs(); -/// //let _ = app.project.h_outputs(); -/// let _ = app.project.h_scenes(); -/// ``` -#[derive(Default, Debug)] -#[namespace(u8)] -#[namespace(isize)] -#[namespace(ItemTheme)] -#[namespace(Arc App::get_arc_str)] -#[namespace(u16 App::get_u16)] -#[namespace(usize App::get_usize)] -#[namespace(bool App::get_bool)] -#[namespace(Selection App::get_selection)] -#[namespace(Color App::get_color)] -#[namespace(Option App::get_opt_u7)] -#[namespace(Option App::get_opt_u16)] -#[namespace(Option App::get_opt_usize)] -#[namespace(Option>> App::get_clip)] -pub struct App { - /// Base color. - pub color: ItemTheme, - /// Must not be dropped for the duration of the process - pub jack: Jack<'static>, - /// Display size - pub size: Sizer, - /// Performance counter - pub perf: PerfModel, - /// Available view modes and input bindings - pub config: Config, - /// Currently selected mode - pub mode: Arc>>, - /// Undo history - pub history: Vec<(AppCommand, Option)>, - /// Dialog overlay - pub dialog: Dialog, - /// Contains all recently created clips. - pub pool: Pool, - /// Contains the currently edited musical arrangement - pub project: Arrangement, - /// Error, if any - pub error: Arc>>> -} -impl App { - /// Create a new application instance from a backend, project, config, and mode - /// - /// ``` - /// let jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack"); - /// let proj = tek::Arrangement::default(); - /// let mut conf = tek::Config::default(); - /// conf.add("(mode hello)"); - /// let tek = tek::App::new(&jack, proj, conf, "hello"); - /// ``` - pub fn new ( - jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef - ) -> Self { - let mode: &str = mode.as_ref(); - App { - color: ItemTheme::random(), - dialog: Dialog::welcome(), - jack: jack.clone(), - mode: config.get_mode(mode).expect(&format!("failed to find mode '{mode}'")), - config, - project, - ..Default::default() - } - } - /// Update memoized render of clock values. - /// ``` - /// tek::App::default().update_clock(); - /// ``` - pub fn update_clock (&self) { - ClockView::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) - } - - /// Set modal dialog. - /// - /// ``` - /// let previous: tek::Dialog = tek::App::default().set_dialog(tek::Dialog::welcome()); - /// ``` - pub fn set_dialog (&mut self, mut dialog: Dialog) -> Dialog { - std::mem::swap(&mut self.dialog, &mut dialog); - dialog - } - - /// FIXME: generalize. Set picked device in device pick dialog. - /// - /// ``` - /// tek::App::default().device_pick(0); - /// ``` - pub fn device_pick (&mut self, index: usize) { - self.dialog = Dialog::Device(index); - } - - /// FIXME: generalize. Add device to current track. - pub fn add_device (&mut self, index: usize) -> Usually<()> { - match index { - 0 => { - let name = self.jack.with_client(|c|c.name().to_string()); - let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name(); - let track = self.track().expect("no active track"); - let port = format!("{}/Sampler", &track.name); - let connect = Connect::exact(format!("{name}:{midi}")); - let sampler = if let Ok(sampler) = Sampler::new( - &self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]] - ) { - self.dialog = Dialog::None; - Device::Sampler(sampler) - } else { - self.dialog = Dialog::Message("Failed to add device.".into()); - return Err("failed to add device".into()) - }; - let track = self.track_mut().expect("no active track"); - track.devices.push(sampler); - Ok(()) - }, - 1 => { - todo!(); - //Ok(()) - }, - _ => unreachable!(), - } - } - - /// Return reference to content browser if open. - /// - /// ``` - /// assert_eq!(tek::App::default().browser(), None); - /// ``` - pub fn browser (&self) -> Option<&Browse> { - if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } - } - - /// Is a MIDI editor currently focused? - /// - /// ``` - /// tek::App::default().editor_focused(); - /// ``` - pub fn editor_focused (&self) -> bool { - false - } - - /// Toggle MIDI editor. - /// - /// ``` - /// tek::App::default().toggle_editor(None); - /// ``` - pub fn toggle_editor (&mut self, value: Option) { - //FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed); - let value = value.unwrap_or_else(||!self.editor().is_some()); - if value { - // Create new clip in pool when entering empty cell - if let Selection::TrackClip { track, scene } = *self.selection() - && let Some(scene) = self.project.scenes.get_mut(scene) - && let Some(slot) = scene.clips.get_mut(track) - && slot.is_none() - && let Some(track) = self.project.tracks.get_mut(track) - { - let (_index, clip) = self.pool.add_new_clip(); - // autocolor: new clip colors from scene and track color - let color = track.color.base.mix(scene.color.base, 0.5); - clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); - if let Some(editor) = &mut self.project.editor { - editor.set_clip(Some(&clip)); - } - *slot = Some(clip.clone()); - //Some(clip) - } else { - //None - } - } else if let Selection::TrackClip { track, scene } = *self.selection() - && let Some(scene) = self.project.scenes.get_mut(scene) - && let Some(slot) = scene.clips.get_mut(track) - && let Some(clip) = slot.as_mut() - { - // Remove clip from arrangement when exiting empty clip editor - let mut swapped = None; - if clip.read().unwrap().count_midi_messages() == 0 { - std::mem::swap(&mut swapped, slot); - } - if let Some(clip) = swapped { - self.pool.delete_clip(&clip.read().unwrap()); - } - } - } - fn get_arc_str (&self, src: impl Language) -> Perhaps> { - Ok(src.src()?.map(|x|x.into())) - } - fn get_u16 (&self, src: impl Language) -> Perhaps { - Ok(Some(match src.word()? { - Some(":w/sidebar") => self.project.w_sidebar(self.editor().is_some()), - Some(":h/sample-detail") => 6.max(self.size.h() as u16 * 3 / 9), - _ => return try_to_u16(src) - })) - } - fn get_usize (&self, src: impl Language) -> Perhaps { - Ok(Some(match src.word()? { - Some(":scene-count") => self.scenes().len(), - Some(":track-count") => self.tracks().len(), - Some(":device-kind") => self.dialog.device_kind().unwrap_or(0), - Some(":device-kind/next") => self.dialog.device_kind_next().unwrap_or(0), - Some(":device-kind/prev") => self.dialog.device_kind_prev().unwrap_or(0), - _ => return try_to_usize(src) - })) - } - fn get_bool (&self, src: impl Language) -> Perhaps { - src.word()?.map(|word|Ok(match word { - "Y" => true, - "N" => false, - ":mode/editor" => self.project.editor.is_some(), - ":focused/dialog" => !matches!(self.dialog, Dialog::None), - ":focused/message" => matches!(self.dialog, Dialog::Message(..)), - ":focused/add_device" => matches!(self.dialog, Dialog::Device(..)), - ":focused/browser" => self.dialog.browser().is_some(), - ":focused/pool/import" => matches!(self.pool.mode, Some(PoolMode::Import(..))), - ":focused/pool/export" => matches!(self.pool.mode, Some(PoolMode::Export(..))), - ":focused/pool/rename" => matches!(self.pool.mode, Some(PoolMode::Rename(..))), - ":focused/pool/length" => matches!(self.pool.mode, Some(PoolMode::Length(..))), - ":focused/clip" => !self.editor_focused() && matches!(self.selection(), Selection::TrackClip{..}), - ":focused/track" => !self.editor_focused() && matches!(self.selection(), Selection::Track(..)), - ":focused/scene" => !self.editor_focused() && matches!(self.selection(), Selection::Scene(..)), - ":focused/mix" => !self.editor_focused() && matches!(self.selection(), Selection::Mix), - _ => return Err(format!("not bool: {word}").into()) - })).transpose() - } - fn get_selection (&self, src: impl Language) -> Perhaps { - src.word()?.map(|word|Ok(match word { - ":select/scene" => self.selection().select_scene(self.tracks().len()), - ":select/scene/next" => self.selection().select_scene_next(self.scenes().len()), - ":select/scene/prev" => self.selection().select_scene_prev(), - ":select/track" => self.selection().select_track(self.tracks().len()), - ":select/track/next" => self.selection().select_track_next(self.tracks().len()), - ":select/track/prev" => self.selection().select_track_prev(), - _ => return Err(format!("not selection: {word}").into()) - })).transpose() - } - fn get_color (&self, src: impl Language) -> Perhaps { - 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"))?; - Ok(Some(Color::Rgb(n, n, n))) - }, - (Some("rgb"), Some(tail)) => { - let r = try_to_u8(expr.tail().map_err(Into::into))? - .ok_or(LanguageError::Domain("not red"))?; - let g = try_to_u8(expr.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))? - .ok_or(LanguageError::Domain("not blue"))?; - Ok(Some(Color::Rgb(r, g, b))) - }, - (Some(_), _) => return Err(format!("not a color expression: {expr}").into()), - (None, _) => return Err(format!("not a color expression: {expr}").into()), - } - } else if let Ok(Some(sym)) = src.word() { - Ok(match sym { - ":color/bg" => Some(Color::Rgb(28, 32, 36)), - ":color/fg" => Some(Color::Rgb(98, 92, 96)), - _ => return Err(format!("not a color: {sym}").into()) - }) - } else { - return Err(format!("not a color: {:?}", src.src()?).into()) - } - } - fn get_opt_u7 (&self, src: impl Language) -> Perhaps> { - src.word()?.map(|word|Ok(match word { - ":editor/pitch" => Some(( - self.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8 - ).into()), - _ => return Err(format!("unknown midi note: {word}").into()) - })).transpose() - } - fn get_opt_u16 (&self, _src: impl Language) -> Perhaps> { - Ok(None) - } - fn get_opt_usize (&self, src: impl Language) -> Perhaps> { - src.word()?.map(|word|Ok(match word { - ":selected/scene" => self.selection().scene(), - ":selected/track" => self.selection().track(), - _ => return Err(format!("unknown opt: {word}").into()) - })).transpose() - } - fn get_clip (&self, src: impl Language) -> Perhaps>>> { - src.word()?.map(|word|Ok(match word { - ":selected/clip" if let Selection::TrackClip { track, scene } = self.selection() => - self.scenes()[*scene].clips[*track].clone(), - _ => return Err(format!("not a clip: {word}").into()) - })).transpose() - } - pub fn inc (&mut self, axis: &ControlAxis) -> Perhaps { - Ok(match (&self.dialog, axis) { - (Dialog::None, _) => todo!(), - (Dialog::Menu(_, _), ControlAxis::Y) => - AppCommand::SetDialog { dialog: self.dialog.menu_next() }.act(self)?, - _ => todo!() - }) - } - pub fn dec (&mut self, axis: &ControlAxis) -> Perhaps { - Ok(match (&self.dialog, axis) { - (Dialog::None, _) => None, - (Dialog::Menu(_, _), ControlAxis::Y) => - AppCommand::SetDialog { dialog: self.dialog.menu_prev() }.act(self)?, - _ => todo!() - }) - } - pub fn confirm (&mut self) -> Perhaps { - Ok(match &self.dialog { - Dialog::Menu(index, items) => { - let callback = items.0[*index].1.clone(); - callback(self)?; - None - }, - _ => todo!(), - }) - } -} - -pub fn swap_value ( - target: &mut T, value: &T, returned: impl Fn(T)->U -) -> Perhaps { - if *target == *value { - Ok(None) - } else { - let mut value = value.clone(); - std::mem::swap(target, &mut value); - Ok(Some(returned(value))) - } -} - -pub fn toggle_bool ( - target: &mut bool, value: &Option, returned: impl Fn(Option)->U -) -> Perhaps { - let mut value = value.unwrap_or(!*target); - if value == *target { - Ok(None) - } else { - std::mem::swap(target, &mut value); - Ok(Some(returned(Some(value)))) - } -} - -pub fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { - let (mut subdirs, mut files) = std::fs::read_dir(dir)? - .fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{ - let entry = entry.expect("failed to read drectory entry"); - let meta = entry.metadata().expect("failed to read entry metadata"); - if meta.is_file() { - files.push(entry.file_name()); - } else if meta.is_dir() { - subdirs.push(entry.file_name()); - } - (subdirs, files) - }); - subdirs.sort(); - files.sort(); - Ok((subdirs, files)) -} diff --git a/src/app/bind.rs b/src/app/bind.rs index c1a514f8..2fb24e8a 100644 --- a/src/app/bind.rs +++ b/src/app/bind.rs @@ -11,13 +11,15 @@ fn tek_commands_collect (app: &App, input: &TuiEvent) -> Usually> { let mut commands = vec![]; - for id in app.mode.keys.iter() { - if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref()) - && let Some(bindings) = event_map.query(input) { - for binding in bindings { - for command in binding.commands.iter() { - if let Some(command) = app.namespace(command)? as Option { - commands.push(command) + 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) + } } } } diff --git a/src/app/config.rs b/src/app/config.rs index ff935638..b0c6cf04 100644 --- a/src/app/config.rs +++ b/src/app/config.rs @@ -115,6 +115,49 @@ impl Config { } } -mod views; pub use self::views::*; -mod modes; pub use self::modes::*; -mod mode; pub use self::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/config/modes.rs b/src/app/config/modes.rs deleted file mode 100644 index 9ff83991..00000000 --- a/src/app/config/modes.rs +++ /dev/null @@ -1,27 +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() - } -} diff --git a/src/app/config/views.rs b/src/app/config/views.rs deleted file mode 100644 index 79560813..00000000 --- a/src/app/config/views.rs +++ /dev/null @@ -1,10 +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(()) -} diff --git a/src/app/draw.rs b/src/app/draw.rs index 3f439d12..118123bf 100644 --- a/src/app/draw.rs +++ b/src/app/draw.rs @@ -1,5 +1,21 @@ 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. /// @@ -9,17 +25,24 @@ 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, e.as_ref()))?; + //to.show(area(xywh, format!("KYPbanica {xywh:?}").align_c()))?; + //to.show(ShowSize.align_se())?; + to.show(e.as_ref().align_c())?; } - for (index, dsl) in self.mode.view.iter().enumerate() { - if let Err(e) = self.interpret(to, dsl) { - *self.error.write().unwrap() = Some(format!( - "mode {:?} view #{index}: {e}", &self.mode.name, - ).into()); - break; + + 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)) }) } @@ -144,6 +167,77 @@ pub fn draw_templates (to: &mut Tui, frags: std::str::Split<&str>, state: &App) }).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()); @@ -677,74 +771,3 @@ pub fn view_track_devices ( Some(h + 1), ))) } - -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() - })) -} diff --git a/src/app/config/mode.rs b/src/app/modes.rs similarity index 77% rename from src/app/config/mode.rs rename to src/app/modes.rs index 83d25ba9..9cd59827 100644 --- a/src/app/config/mode.rs +++ b/src/app/modes.rs @@ -1,5 +1,31 @@ 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. /// /// ``` @@ -41,7 +67,7 @@ impl Mode> { _ => self.add_view(tail)?, }; } else if let Ok(Some(word)) = dsl.word() { - self.add_view(word); + self.add_view(word)?; } else { return Err(format!("Mode::add: unexpected: {dsl:?}").into()); }) diff --git a/src/deps.rs b/src/deps.rs deleted file mode 100644 index 2f9683a8..00000000 --- a/src/deps.rs +++ /dev/null @@ -1,32 +0,0 @@ -#[allow(unused)] -pub(crate) use ::{ - std::{ - cmp::Ord, - collections::BTreeMap, - error::Error, - ffi::OsString, - fmt::{Write, Debug, Formatter}, - fs::File, - ops::{Add, Sub, Mul, Div, Rem}, - path::{Path, PathBuf}, - sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}}, - time::Duration, - thread::{spawn, JoinHandle}, - }, - xdg::{ - BaseDirectories, - }, - tengri::{ - *, - lang::*, - midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*}, - crossterm::event::{Event, KeyEvent}, - ratatui::{ - self, - prelude::{Rect, Style, Stylize, Buffer, Color::{self, *}}, - widgets::{Widget, canvas::{Canvas, Line}}, - }, - }, -}; -#[cfg(feature = "cli")] -pub(crate) use ::clap::{self, Parser, Subcommand}; diff --git a/src/device.rs b/src/device.rs deleted file mode 100644 index 8e842c52..00000000 --- a/src/device.rs +++ /dev/null @@ -1,144 +0,0 @@ -use crate::*; - -def_command!(DeviceCommand: |device: Device| {}); - -impl Device { - pub fn name (&self) -> &str { - match self { - Self::Sampler(sampler) => sampler.name.as_ref(), - _ => todo!(), - } - } - pub fn midi_ins (&self) -> &[MidiInput] { - match self { - //Self::Sampler(Sampler { midi_in, .. }) => &[midi_in], - _ => todo!() - } - } - pub fn midi_outs (&self) -> &[MidiOutput] { - match self { - Self::Sampler(_) => &[], - _ => todo!() - } - } - pub fn audio_ins (&self) -> &[AudioInput] { - match self { - Self::Sampler(Sampler { audio_ins, .. }) => audio_ins.as_slice(), - _ => todo!() - } - } - pub fn audio_outs (&self) -> &[AudioOutput] { - match self { - Self::Sampler(Sampler { audio_outs, .. }) => audio_outs.as_slice(), - _ => todo!() - } - } -} - -/// A device that can be plugged into the chain. -/// -/// ``` -/// let device = tek::Device::default(); -/// ``` -#[derive(Debug, Default)] pub enum Device { - #[default] - Bypass, - Mute, - #[cfg(feature = "sampler")] - Sampler(Sampler), - #[cfg(feature = "lv2")] // TODO - Lv2(Lv2), - #[cfg(feature = "vst2")] // TODO - Vst2, - #[cfg(feature = "vst3")] // TODO - Vst3, - #[cfg(feature = "clap")] // TODO - Clap, - #[cfg(feature = "sf2")] // TODO - Sf2, -} - -/// Some sort of wrapper? -pub struct DeviceAudio<'a>(pub &'a mut Device); - -impl_audio!(|self: DeviceAudio<'a>, client, scope|{ - use Device::*; - match self.0 { - Mute => { Control::Continue }, - Bypass => { /*TODO*/ Control::Continue }, - #[cfg(feature = "sampler")] Sampler(sampler) => sampler.process(client, scope), - #[cfg(feature = "lv2")] Lv2(lv2) => lv2.process(client, scope), - #[cfg(feature = "vst2")] Vst2 => { todo!() }, // TODO - #[cfg(feature = "vst3")] Vst3 => { todo!() }, // TODO - #[cfg(feature = "clap")] Clap => { todo!() }, // TODO - #[cfg(feature = "sf2")] Sf2 => { todo!() }, // TODO - } -}); - -pub fn device_kinds () -> &'static [&'static str] { - &[ - #[cfg(feature = "sampler")] "Sampler", - #[cfg(feature = "lv2")] "Plugin (LV2)", - ] -} - -impl> + AsMut>> HasDevices for T { - fn devices (&self) -> &Vec { - self.as_ref() - } - fn devices_mut (&mut self) -> &mut Vec { - self.as_mut() - } -} - -pub trait HasDevices: AsRef> + AsMut> { - fn devices (&self) -> &Vec { - self.as_ref() - } - fn devices_mut (&mut self) -> &mut Vec { - self.as_mut() - } -} - -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::*; -pub mod dialog; pub use self::dialog::*; -pub mod editor; pub use self::editor::*; -pub mod menu; pub use self::menu::*; -pub mod meter; pub use self::meter::*; -pub mod mix; pub use self::mix::*; -pub mod sampler; pub use self::sampler::*; -pub mod sequence; pub use self::sequence::*; - -#[cfg(feature = "plugin")] pub mod plugin; -#[cfg(feature = "plugin")] pub use self::plugin::*; - -def_command!(AudioInputCommand: |port: AudioInput| { - Close => todo!(), - Connect { audio_out: Arc } => todo!(), -}); - -def_command!(AudioOutputCommand: |port: AudioOutput| { - Close => todo!(), - Connect { audio_in: Arc } => todo!(), -}); - -def_command!(MidiInputCommand: |port: MidiInput| { - Close => todo!(), - Connect { midi_out: Arc } => todo!(), -}); - -def_command!(MidiOutputCommand: |port: MidiOutput| { - Close => todo!(), - Connect { midi_in: Arc } => todo!(), -}); - -pub struct Junction(T); - -impl View for Junction { - fn view (&self) -> impl Draw { - T::KIND - } -} diff --git a/src/device/arrange.rs b/src/device/arrange.rs index d54a8b2e..92cdbc23 100644 --- a/src/device/arrange.rs +++ b/src/device/arrange.rs @@ -7,12 +7,12 @@ use crate::*; /// ``` #[derive(Default, Debug)] pub struct Arrangement { + /// JACK client handle. + pub jack: Jack<'static>, /// Project name. pub name: Arc, /// Base color. pub color: ItemTheme, - /// JACK client handle. - pub jack: Jack<'static>, /// FIXME a render of the project arrangement, redrawn on update. /// TODO rename to "render_cache" or smth pub arranger: Arc>, diff --git a/src/device/dialog.rs b/src/device/dialog.rs index b007ffa2..3d8424ed 100644 --- a/src/device/dialog.rs +++ b/src/device/dialog.rs @@ -55,17 +55,23 @@ impl Dialog { /// ``` pub fn welcome () -> Self { Self::Menu(1, MenuItems([ + MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))), + MenuItem("New session".into(), Arc::new(Box::new(|app|Ok({ app.dialog = Dialog::None; - app.mode = app.config.modes.get(":arranger").unwrap(); + app.mode = app.config.modes.get(":arranger"); })))), + MenuItem("Load session".into(), Arc::new(Box::new(|_|Ok(())))), + MenuItem("Exit".into(), Arc::new(Box::new(|_|Ok({ () })))), + ].into())) } + /// FIXME: generalize /// ``` /// let _ = tek::Dialog::welcome().menu_selected(); diff --git a/src/device/sequence.rs b/src/device/sequence.rs index 8088a404..368bebb5 100644 --- a/src/device/sequence.rs +++ b/src/device/sequence.rs @@ -42,7 +42,7 @@ impl +AsMut> HasSequencer for T {} /// let mut clip = tek::MidiClip::new("clip", true, 1, None, None); /// clip.set_length(96); /// clip.toggle_loop(); -/// clip.record_event(12, midly::MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }); +/// clip.record_event(12, ::tengri::midly::MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }); /// assert!(clip.contains_note_on(36.into(), 6, 18)); /// assert_eq!(&clip.notes, &clip.duplicate().notes); /// diff --git a/src/tek.edn b/src/tek.edn index d14123c0..ab8d2229 100644 --- a/src/tek.edn +++ b/src/tek.edn @@ -2,12 +2,14 @@ (bsp/s (exact/y 1 (text ~~~~ β•‘ ~ β•Ÿβ”€β•Œ ~β•Ÿβ”€< ~~ v0.3.0 ~~)) (bsp/s (exact/y 1 (text ~~~~ ╨ ~ β•™β”€β”€β•œ ╨ β•œ ~~~~~~~~~~~~)) (text dig?))))) -(view :browse (bsp/s (padding 3 1 :browse-title) (enclose (fg (g 96)) browser))) +(view :browse (bsp/s + (padding 3 1 :browse-title) + (enclose (fg (g 96)) browser))) (mode :transport (name Transport) (info JACK transport controller.) (keys :clock :global) :transport) -(mode :menu (name Menu) (info Mode selector.) (keys :axis/y :confirm) +(mode :menu (name Menu) (info Mode selector.) (keys :axis/y :confirm) (view (bg (g 0) (bsp/s (max/y 2 :transport (bsp/s (max/y 3 (bg (g 80) :ports/out)) @@ -29,13 +31,13 @@ (bsp/n (fixed/y 1 :status) (fill :samples/grid)))) -(view :ports/out (fill/x (bsp/s (fill/x (align/w (text L-AUDIO-OUT))) - (bsp/e (text MIDI-OUT) - (fill/x (align/e (text AUDIO-OUT-R))))))) +(view :ports/out + (fill/x (bsp/s (fill/x (align/w (text L-AUDIO-OUT))) + (bsp/e (text MIDI-OUT) (fill/x (align/e (text AUDIO-OUT-R))))))) -(view :ports/in (fill/x (bsp/s (fill/x (align/w (text L-AUDIO-IN))) - (bsp/e (text MIDI-IN) - (fill/x (align/e (text AUDIO-IN-R))))))) +(view :ports/in + (fill/x (bsp/s (fill/x (align/w (text L-AUDIO-IN))) + (bsp/e (text MIDI-IN) (fill/x (align/e (text AUDIO-IN-R))))))) (mode :groovebox (name Groovebox) (info Sequencer with sampler.) (keys :clock :editor :sampler :global) @@ -84,7 +86,8 @@ (keys :axis/w (@openbracket w/dec) (@closebracket w/inc)) (keys :axis/w2 (@openbrace w2/dec) (@closebrace w2/inc)) (keys :focus) -(keys :editor (see :axis/i :axis/i2 :axis/y :page :editor/view :editor/add :editor/del)) +(keys :editor (see :axis/i :axis/i2 :axis/y + :page :editor/view :editor/add :editor/del)) (keys :editor/view (see :axis/x :axis/x2 :axis/z :axis/z2) (@z toggle :lock)) (keys :editor/add (@a editor/append :true) @@ -115,35 +118,35 @@ (@up select :select/scene/dec) (@down select :select/scene/inc)) (keys :scene (see :color :launch :axis/z :axis/z2 :delete)) -(keys :help (@f1 dialog :help)) -(keys :page (@pgup page/up) - (@pgdn page/down)) -(keys :delete (@delete delete) +(keys :help (@f1 dialog :help)) +(keys :page (@pgup page/up) + (@pgdn page/down)) +(keys :delete (@delete delete) (@backspace delete/back)) (keys :input (see :axis/x :delete) (:char input)) (keys :list (see :axis/y :confirm)) (keys :length (see :axis/x :axis/y :confirm)) (keys :browse (see :list :input :focus)) -(keys :history (@u undo 1) - (@r redo 1)) -(keys :saveload (@f6 dialog :save) - (@f9 dialog :load)) -(keys :color (@c color)) -(keys :launch (@q launch)) -(keys :clock (@space clock/toggle 0) +(keys :history (@u undo 1) + (@r redo 1)) +(keys :saveload (@f6 dialog :save) + (@f9 dialog :load)) +(keys :color (@c color)) +(keys :launch (@q launch)) +(keys :clock (@space clock/toggle 0) (@shift/space clock/toggle 0)) (keys :global (see :history :saveload) - (@f8 dialog :options) - (@f10 dialog :quit)) + (@f8 dialog :options) + (@f10 dialog :quit)) (keys :clip (see :color :launch :axis/z :axis/z2 :delete) - (@l toggle :loop)) + (@l toggle :loop)) (keys :sequencer (see :color :launch) - (@shift/I input/add) - (@shift/O output/add)) + (@shift/I input/add) + (@shift/O output/add)) (keys :pool (see :axis-y :axis-w :axis/z2 :color :delete) - (@n rename/begin) - (@t length/begin) - (@m import/begin) - (@x export/begin) - (@shift/A clip/add :after :new/clip) - (@shift/D clip/add :after :cloned/clip)) + (@n rename/begin) + (@t length/begin) + (@m import/begin) + (@x export/begin) + (@shift/A clip/add :after :new/clip) + (@shift/D clip/add :after :cloned/clip)) diff --git a/src/tek.rs b/src/tek.rs index 1f291ac9..7e13aa6a 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -9,72 +9,140 @@ pub extern crate atomic_float; pub extern crate xdg; pub extern crate tengri; +#[cfg(feature = "cli")] +pub(crate) use ::clap::{self, Parser, Subcommand}; +#[allow(unused)] +pub(crate) use ::{ + std::{ + cmp::Ord, + collections::BTreeMap, + error::Error, + ffi::OsString, + fmt::{Write, Debug, Formatter}, + fs::File, + ops::{Add, Sub, Mul, Div, Rem}, + path::{Path, PathBuf}, + sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}}, + time::Duration, + thread::{spawn, JoinHandle}, + }, + xdg::{ + BaseDirectories, + }, + tengri::{ + *, + lang::*, + midly::{ + Smf, TrackEventKind, MidiMessage, Error as MidiError, + num::*, + live::*, + }, + crossterm::event::{Event, KeyEvent}, + ratatui::{ + self, + prelude::{Rect, Style, Stylize, Buffer, Color::{self, *}}, + widgets::{Widget, canvas::{Canvas, Line}}, + }, + }, +}; + /// Command-line entrypoint. -#[cfg(feature = "cli")] pub fn main () -> Usually<()> { +#[allow(unused)] fn main () -> Usually<()> { tengri::Tui::setup_panic(); - let name = "tek"; - let mode = ":menu"; - let title = "untitled!"; - let bpm = 74.; - Config::watch(|config|tengri::Tui::run_main(Jack::new_run(name, move|jack|{ - let clock = Clock::new(&jack, Some(bpm))?; - let tracks = []; - let scenes = []; - let project = Arrangement::new( - &jack, title.into(), clock, + #[cfg(feature = "cli")] { + Config::watch(crate::cli::run_with_config).map(|_|()) + } + #[cfg(not(feature = "cli"))] { + Config::watch(run_new_plain).map(|_|()) + } +} + +fn run_new_plain (config: Config) -> Usually<()> { + let name = "tek"; + tengri::Tui::run_main(Jack::new_run(name, move|jack|{ + let mode = ":menu"; + let title = "untitled!"; + let bpm = 74.; + let clock = Clock::new(&jack, Some(bpm))?; + let tracks = []; + let scenes = []; + Ok(App::new(Arrangement::new( + &jack, + title.into(), + clock, tracks.into_iter(), scenes.into_iter(), - Connect::midi_ins(&jack, &"M", &[], None)?.into_iter(), - Connect::midi_outs(&jack, &"M", &[], None)?.into_iter(), - [].into_iter() - .chain(Connect::audio_ins(&jack, &"L", &[], None)?.into_iter()) - .chain(Connect::audio_ins(&jack, &"R", &[], None)?.into_iter()), - [].into_iter() - .chain(Connect::audio_outs(&jack, &"L", &[], None)?.into_iter()) - .chain(Connect::audio_outs(&jack, &"R", &[], None)?.into_iter()), - ); - Ok(App::new(&jack, project, config, mode)) - })?)).map(|_|()) + connect_midi_ins(&jack, &"M", &[], None)?.into_iter(), + connect_midi_outs(&jack, &"M", &[], None)?.into_iter(), + [].into_iter().chain(connect_audio_ins(&jack, &"L", &[], None)?.into_iter()) + .chain(connect_audio_ins(&jack, &"R", &[], None)?.into_iter()), + [].into_iter().chain(connect_audio_outs(&jack, &"L", &[], None)?.into_iter()) + .chain(connect_audio_outs(&jack, &"R", &[], None)?.into_iter()), + ), config, mode)) + })?) } -pub mod deps; -pub(crate) use self::deps::*; -pub mod device; -pub use self::device::*; -pub mod app; -pub use self::app::*; + /// CLI banner. pub(crate) const HEADER: &'static str = r#" ~ β–ˆβ–€β–ˆβ–€β–ˆ β–ˆβ–€β–€β–ˆ β–ˆ β–ˆ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ - β–ˆ β–ˆβ–€ β–ˆβ–€β–€β–„ ~ v0.4.0, 2026 heatwave edition ~ + β–ˆ β–ˆβ–€ β–ˆβ–€β–€β–„ ~ heatwave is the new darkwave ~ ~ β–€ β–ˆβ–€β–€β–ˆ β–€ β–€ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#; -#[cfg(feature = "cli")] -mod cli { +#[cfg(feature = "cli")] pub mod cli { use crate::*; + pub fn run_with_config (config: Config) -> Usually<()> { + Cli::parse().run(Some(config)) + } + + /// Command-line configuration. + impl Cli { + pub fn run (&self, mut config: Option) -> Usually<()> { + if config.is_none() { + config = Some(Config::init_new(None)?); + } + self.action.run(config.unwrap()) + } + } /// The command-line interface descriptor. /// /// ``` - /// let cli: tek::Cli = Default::default(); + /// let cli: tek::cli::Cli = Default::default(); /// /// use clap::CommandFactory; - /// tek::Cli::command().debug_assert(); + /// tek::cli::Cli::command().debug_assert(); /// ``` - #[derive(Parser)] + #[derive(Parser, Debug, Default)] #[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))] - #[derive(Debug, Default)] pub struct Cli { + pub struct Cli { /// Pre-defined configuration modes. /// /// TODO: Replace these with scripted configurations. #[command(subcommand)] pub action: Action, } - + impl Action { + fn run (&self, config: Config) -> Usually<()> { + use Action::*; + match self { + Version => show_version(), + Config => print_config(&config), + Resume => todo!("resume session"), + List => todo!("list sessions"), + New(sesh) => Tui::run_main( + Arc::new(RwLock::new(App::new(sesh.init()?, config, ":menu"))) + ).map(|_|())?, + _ => todo!() + } + Ok(()) + } + } /// Application modes that can be passed to the mommand line interface. /// /// ``` - /// let action: tek::Action = Default::default(); + /// let action: tek::cli::Action = Default::default(); /// ``` - #[derive(Debug, Clone, Subcommand, Default)] pub enum Action { + #[derive(Debug, Clone, Subcommand, Default)] + pub enum Action { /// Continue where you left off #[default] Resume, /// Run headlessly in current session. @@ -86,38 +154,7 @@ mod cli { /// Continue work in a copy of the current session. Fork, /// Create a new empty session. - New { - /// Name of JACK client - #[arg(short='n', long)] name: Option, - /// Whether to attempt to become transport master - #[arg(short='Y', long, default_value_t = false)] sync_lead: bool, - /// Whether to sync to external transport master - #[arg(short='y', long, default_value_t = true)] sync_follow: bool, - /// Initial tempo in beats per minute - #[arg(short='b', long, default_value = None)] bpm: Option, - /// Whether to include a transport toolbar (default: true) - #[arg(short='c', long, default_value_t = true)] show_clock: bool, - /// MIDI outs to connect to (multiple instances accepted) - #[arg(short='I', long)] midi_from: Vec, - /// MIDI outs to connect to (multiple instances accepted) - #[arg(short='i', long)] midi_from_re: Vec, - /// MIDI ins to connect to (multiple instances accepted) - #[arg(short='O', long)] midi_to: Vec, - /// MIDI ins to connect to (multiple instances accepted) - #[arg(short='o', long)] midi_to_re: Vec, - /// Audio outs to connect to left input - #[arg(short='l', long)] left_from: Vec, - /// Audio outs to connect to right input - #[arg(short='r', long)] right_from: Vec, - /// Audio ins to connect from left output - #[arg(short='L', long)] left_to: Vec, - /// Audio ins to connect from right output - #[arg(short='R', long)] right_to: Vec, - /// Tracks to creat - #[arg(short='t', long)] tracks: Option, - /// Scenes to create - #[arg(short='s', long)] scenes: Option, - }, + New(ProjectInit), /// Import media as new session. Import, /// Show configuration. @@ -125,75 +162,659 @@ mod cli { /// Show version. Version, } + #[derive(Debug, Clone, Parser, Default)] + pub struct ProjectInit { + /// Name of JACK client + #[arg(short='n', long)] name: Option, + /// Whether to attempt to become transport master + #[arg(short='Y', long, default_value_t = false)] sync_lead: bool, + /// Whether to sync to external transport master + #[arg(short='y', long, default_value_t = true)] sync_follow: bool, + /// Initial tempo in beats per minute + #[arg(short='b', long, default_value = None)] bpm: Option, + /// Whether to include a transport toolbar (default: true) + #[arg(short='c', long, default_value_t = true)] show_clock: bool, + /// MIDI outs to connect to (multiple instances accepted) + #[arg(short='I', long)] midi_from: Vec, + /// MIDI outs to connect to (multiple instances accepted) + #[arg(short='i', long)] midi_from_re: Vec, + /// MIDI ins to connect to (multiple instances accepted) + #[arg(short='O', long)] midi_to: Vec, + /// MIDI ins to connect to (multiple instances accepted) + #[arg(short='o', long)] midi_to_re: Vec, + /// Audio outs to connect to left input + #[arg(short='l', long)] left_from: Vec, + /// Audio outs to connect to right input + #[arg(short='r', long)] right_from: Vec, + /// Audio ins to connect from left output + #[arg(short='L', long)] left_to: Vec, + /// Audio ins to connect from right output + #[arg(short='R', long)] right_to: Vec, + /// Tracks to creat + #[arg(short='t', long)] tracks: Option, + /// Scenes to create + #[arg(short='s', long)] scenes: Option, + } + impl ProjectInit { + pub fn init (&self) -> Usually { + let Self { + name, bpm, tracks, scenes, + sync_lead: _, sync_follow: _, + left_from, right_from, midi_from, midi_from_re, + left_to, right_to, midi_to, midi_to_re, + .. + } = self; + let name = name.as_ref().map_or("tek", |x|x.as_str()); + let jack = Jack::new(&name)?; + let mut proj = Arrangement::new( + &jack, + name.into(), + Clock::new(&jack, *bpm)?, + [].into_iter(), + [].into_iter(), + connect_midi_ins(&jack, &"M".to_string(), midi_from.as_ref(), Some(midi_from_re))?.into_iter(), + connect_midi_outs(&jack, &"M".to_string(), midi_to.as_ref(), Some(midi_to_re))?.into_iter(), + [].into_iter() + .chain(connect_audio_ins(&jack, &"L".to_string(), &left_from, None)?.into_iter()) + .chain(connect_audio_ins(&jack, &"R".to_string(), &right_from, None)?.into_iter()), + [].into_iter() + .chain(connect_audio_outs(&jack, &"L".to_string(), &left_to, None)?.into_iter()) + .chain(connect_audio_outs(&jack, &"R".to_string(), &right_to, None)?.into_iter())); + //&jack, Clock::new(&jack, *bpm)?, &lf, <, &rf, &rt, &mf, &mt, &mfr, &mtr)?; + proj.tracks_add(tracks.unwrap_or(0), None, &[], &[])?; + proj.scenes_add(scenes.unwrap_or(0))?; + //if matches!(self, Action::Status) { + //// Show status and exit + //tek_print_status(&proj); + //return Ok(()) + //} + Ok(proj) + } + } +} - /// Command-line configuration. - #[cfg(feature = "cli")] - impl Cli { - pub fn run (&self) -> Usually<()> { - self.action.run(Config::init_new(None)?) +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); + primitive!(isize: try_to_isize); + impl_has!(Clock: |self: App|self.project.clock); + impl_has!(Vec: |self: App|self.project.midi_ins); + impl_has!(Vec: |self: App|self.project.midi_outs); + impl_has!(Dialog: |self: App|self.dialog); + impl_has!(Jack<'static>: |self: App|self.jack); + impl_has!(Pool: |self: App|self.pool); + impl_has!(Selection: |self: App|self.project.selection); + impl_as_ref!(Vec: |self: App|self.project.as_ref()); + impl_as_mut!(Vec: |self: App|self.project.as_mut()); + impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt()); + impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt()); + impl_has_clips!( |self: App|self.pool.clips); + /// Total application state. + /// + /// ``` + /// use tek::{HasTracks, HasScenes, TracksView, ScenesView}; + /// let mut app = tek::App::default(); + /// let _ = app.scene_add(None, None).unwrap(); + /// let _ = app.update_clock(); + /// app.project.editor = Some(Default::default()); + /// //let _: Vec<_> = app.project.inputs_with_sizes().collect(); + /// //let _: Vec<_> = app.project.outputs_with_sizes().collect(); + /// let _: Vec<_> = app.project.tracks_with_sizes().collect(); + /// //let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect(); + /// //let _: Vec<_> = app.scenes_with_colors(true, 10).collect(); + /// //let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect(); + /// let _ = app.project.w(); + /// //let _ = app.project.w_sidebar(); + /// //let _ = app.project.w_tracks_area(); + /// let _ = app.project.h(); + /// //let _ = app.project.h_tracks_area(); + /// //let _ = app.project.h_inputs(); + /// //let _ = app.project.h_outputs(); + /// let _ = app.project.h_scenes(); + /// ``` + #[derive(Default, Debug)] + #[namespace(u8)] + #[namespace(isize)] + #[namespace(ItemTheme)] + #[namespace(Arc App::get_arc_str)] + #[namespace(u16 App::get_u16)] + #[namespace(usize App::get_usize)] + #[namespace(bool App::get_bool)] + #[namespace(Selection App::get_selection)] + #[namespace(Color App::get_color)] + #[namespace(Option App::get_opt_u7)] + #[namespace(Option App::get_opt_u16)] + #[namespace(Option App::get_opt_usize)] + #[namespace(Option>> App::get_clip)] + pub struct App { + /// Base color. + pub color: ItemTheme, + /// Must not be dropped for the duration of the process + pub jack: Jack<'static>, + /// Display size + pub size: Sizer, + /// Performance counter + pub perf: PerfModel, + /// Available view modes and input bindings + pub config: Config, + /// Currently selected mode + pub mode: Option>>>, + /// Undo history + pub history: Vec<(AppCommand, Option)>, + /// Dialog overlay + pub dialog: Dialog, + /// Contains all recently created clips. + pub pool: Pool, + /// Contains the currently edited musical arrangement + pub project: Arrangement, + /// Error, if any + pub error: Arc>>> + } + impl App { + /// Create a new application instance from a backend, project, config, and mode + /// + /// ``` + /// let mut proj = tek::Arrangement::default(); + /// proj.jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack"); + /// let mut conf = tek::Config::default(); + /// conf.add("(mode hello)"); + /// let tek = tek::App::new(proj, conf, "hello"); + /// ``` + pub fn new (project: Arrangement, config: Config, mode: impl AsRef) -> Self { + let mode: &str = mode.as_ref(); + App { + jack: project.jack.clone(), + color: ItemTheme::random(), + dialog: Dialog::welcome(), + mode: config.get_mode(mode), + config, + project, + ..Default::default() + } + } + + /// Update memoized render of clock values. + /// ``` + /// tek::App::default().update_clock(); + /// ``` + pub fn update_clock (&self) { + ClockView::update_clock( + &self.project.clock.view_cache, self.clock(), self.size.w() > 80 + ) + } + + /// Set modal dialog. + /// + /// ``` + /// let previous: tek::Dialog = tek::App::default().set_dialog(tek::Dialog::welcome()); + /// ``` + pub fn set_dialog (&mut self, mut dialog: Dialog) -> Dialog { + std::mem::swap(&mut self.dialog, &mut dialog); + dialog + } + + /// FIXME: generalize. Set picked device in device pick dialog. + /// + /// ``` + /// tek::App::default().device_pick(0); + /// ``` + pub fn device_pick (&mut self, index: usize) { + self.dialog = Dialog::Device(index); + } + + /// FIXME: generalize. Add device to current track. + pub fn add_device (&mut self, index: usize) -> Usually<()> { + match index { + 0 => { + let name = self.jack.with_client(|c|c.name().to_string()); + let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name(); + let track = self.track().expect("no active track"); + let port = format!("{}/Sampler", &track.name); + let connect = Connect::exact(format!("{name}:{midi}")); + let sampler = if let Ok(sampler) = Sampler::new( + &self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]] + ) { + self.dialog = Dialog::None; + Device::Sampler(sampler) + } else { + self.dialog = Dialog::Message("Failed to add device.".into()); + return Err("failed to add device".into()) + }; + let track = self.track_mut().expect("no active track"); + track.devices.push(sampler); + Ok(()) + }, + 1 => { + todo!(); + //Ok(()) + }, + _ => unreachable!(), + } + } + + /// Return reference to content browser if open. + /// + /// ``` + /// assert_eq!(tek::App::default().browser(), None); + /// ``` + pub fn browser (&self) -> Option<&Browse> { + if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } + } + + /// Is a MIDI editor currently focused? + /// + /// ``` + /// tek::App::default().editor_focused(); + /// ``` + pub fn editor_focused (&self) -> bool { + false + } + + /// Toggle MIDI editor. + /// + /// ``` + /// tek::App::default().toggle_editor(None); + /// ``` + pub fn toggle_editor (&mut self, value: Option) { + //FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed); + let value = value.unwrap_or_else(||!self.editor().is_some()); + if value { + // Create new clip in pool when entering empty cell + if let Selection::TrackClip { track, scene } = *self.selection() + && let Some(scene) = self.project.scenes.get_mut(scene) + && let Some(slot) = scene.clips.get_mut(track) + && slot.is_none() + && let Some(track) = self.project.tracks.get_mut(track) + { + let (_index, clip) = self.pool.add_new_clip(); + // autocolor: new clip colors from scene and track color + let color = track.color.base.mix(scene.color.base, 0.5); + clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); + if let Some(editor) = &mut self.project.editor { + editor.set_clip(Some(&clip)); + } + *slot = Some(clip.clone()); + //Some(clip) + } else { + //None + } + } else if let Selection::TrackClip { track, scene } = *self.selection() + && let Some(scene) = self.project.scenes.get_mut(scene) + && let Some(slot) = scene.clips.get_mut(track) + && let Some(clip) = slot.as_mut() + { + // Remove clip from arrangement when exiting empty clip editor + let mut swapped = None; + if clip.read().unwrap().count_midi_messages() == 0 { + std::mem::swap(&mut swapped, slot); + } + if let Some(clip) = swapped { + self.pool.delete_clip(&clip.read().unwrap()); + } + } + } + fn get_arc_str (&self, src: impl Language) -> Perhaps> { + Ok(src.src()?.map(|x|x.into())) + } + fn get_u16 (&self, src: impl Language) -> Perhaps { + Ok(Some(match src.word()? { + Some(":w/sidebar") => self.project.w_sidebar(self.editor().is_some()), + Some(":h/sample-detail") => 6.max(self.size.h() as u16 * 3 / 9), + _ => return try_to_u16(src) + })) + } + fn get_usize (&self, src: impl Language) -> Perhaps { + Ok(Some(match src.word()? { + Some(":scene-count") => self.scenes().len(), + Some(":track-count") => self.tracks().len(), + Some(":device-kind") => self.dialog.device_kind().unwrap_or(0), + Some(":device-kind/next") => self.dialog.device_kind_next().unwrap_or(0), + Some(":device-kind/prev") => self.dialog.device_kind_prev().unwrap_or(0), + _ => return try_to_usize(src) + })) + } + fn get_bool (&self, src: impl Language) -> Perhaps { + src.word()?.map(|word|Ok(match word { + "Y" => true, + "N" => false, + ":mode/editor" => self.project.editor.is_some(), + ":focused/dialog" => !matches!(self.dialog, Dialog::None), + ":focused/message" => matches!(self.dialog, Dialog::Message(..)), + ":focused/add_device" => matches!(self.dialog, Dialog::Device(..)), + ":focused/browser" => self.dialog.browser().is_some(), + ":focused/pool/import" => matches!(self.pool.mode, Some(PoolMode::Import(..))), + ":focused/pool/export" => matches!(self.pool.mode, Some(PoolMode::Export(..))), + ":focused/pool/rename" => matches!(self.pool.mode, Some(PoolMode::Rename(..))), + ":focused/pool/length" => matches!(self.pool.mode, Some(PoolMode::Length(..))), + ":focused/clip" => !self.editor_focused() && matches!(self.selection(), Selection::TrackClip{..}), + ":focused/track" => !self.editor_focused() && matches!(self.selection(), Selection::Track(..)), + ":focused/scene" => !self.editor_focused() && matches!(self.selection(), Selection::Scene(..)), + ":focused/mix" => !self.editor_focused() && matches!(self.selection(), Selection::Mix), + _ => return Err(format!("not bool: {word}").into()) + })).transpose() + } + fn get_selection (&self, src: impl Language) -> Perhaps { + src.word()?.map(|word|Ok(match word { + ":select/scene" => self.selection().select_scene(self.tracks().len()), + ":select/scene/next" => self.selection().select_scene_next(self.scenes().len()), + ":select/scene/prev" => self.selection().select_scene_prev(), + ":select/track" => self.selection().select_track(self.tracks().len()), + ":select/track/next" => self.selection().select_track_next(self.tracks().len()), + ":select/track/prev" => self.selection().select_track_prev(), + _ => return Err(format!("not selection: {word}").into()) + })).transpose() + } + fn get_color (&self, src: impl Language) -> Perhaps { + 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"))?; + Ok(Some(Color::Rgb(n, n, n))) + }, + (Some("rgb"), Some(tail)) => { + let r = try_to_u8(expr.tail().map_err(Into::into))? + .ok_or(LanguageError::Domain("not red"))?; + let g = try_to_u8(expr.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))? + .ok_or(LanguageError::Domain("not blue"))?; + Ok(Some(Color::Rgb(r, g, b))) + }, + (Some(_), _) => return Err(format!("not a color expression: {expr}").into()), + (None, _) => return Err(format!("not a color expression: {expr}").into()), + } + } else if let Ok(Some(sym)) = src.word() { + Ok(match sym { + ":color/bg" => Some(Color::Rgb(28, 32, 36)), + ":color/fg" => Some(Color::Rgb(98, 92, 96)), + _ => return Err(format!("not a color: {sym}").into()) + }) + } else { + return Err(format!("not a color: {:?}", src.src()?).into()) + } + } + fn get_opt_u7 (&self, src: impl Language) -> Perhaps> { + src.word()?.map(|word|Ok(match word { + ":editor/pitch" => Some(( + self.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8 + ).into()), + _ => return Err(format!("unknown midi note: {word}").into()) + })).transpose() + } + fn get_opt_u16 (&self, _src: impl Language) -> Perhaps> { + Ok(None) + } + fn get_opt_usize (&self, src: impl Language) -> Perhaps> { + src.word()?.map(|word|Ok(match word { + ":selected/scene" => self.selection().scene(), + ":selected/track" => self.selection().track(), + _ => return Err(format!("unknown opt: {word}").into()) + })).transpose() + } + fn get_clip (&self, src: impl Language) -> Perhaps>>> { + src.word()?.map(|word|Ok(match word { + ":selected/clip" if let Selection::TrackClip { track, scene } = self.selection() => + self.scenes()[*scene].clips[*track].clone(), + _ => return Err(format!("not a clip: {word}").into()) + })).transpose() + } + pub fn inc (&mut self, axis: &ControlAxis) -> Perhaps { + Ok(match (&self.dialog, axis) { + (Dialog::None, _) => todo!(), + (Dialog::Menu(_, _), ControlAxis::Y) => + AppCommand::SetDialog { dialog: self.dialog.menu_next() }.act(self)?, + _ => todo!() + }) + } + pub fn dec (&mut self, axis: &ControlAxis) -> Perhaps { + Ok(match (&self.dialog, axis) { + (Dialog::None, _) => None, + (Dialog::Menu(_, _), ControlAxis::Y) => + AppCommand::SetDialog { dialog: self.dialog.menu_prev() }.act(self)?, + _ => todo!() + }) + } + pub fn confirm (&mut self) -> Perhaps { + Ok(match &self.dialog { + Dialog::Menu(index, items) => { + let callback = items.0[*index].1.clone(); + callback(self)?; + None + }, + _ => todo!(), + }) } } - #[cfg(feature = "cli")] - impl Action { - fn run (&self, config: Config) -> Usually<()> { - use Action::*; + pub fn swap_value ( + target: &mut T, value: &T, returned: impl Fn(T)->U + ) -> Perhaps { + if *target == *value { + Ok(None) + } else { + let mut value = value.clone(); + std::mem::swap(target, &mut value); + Ok(Some(returned(value))) + } + } + + pub fn toggle_bool ( + target: &mut bool, value: &Option, returned: impl Fn(Option)->U + ) -> Perhaps { + let mut value = value.unwrap_or(!*target); + if value == *target { + Ok(None) + } else { + std::mem::swap(target, &mut value); + Ok(Some(returned(Some(value)))) + } + } + + pub fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { + let (mut subdirs, mut files) = std::fs::read_dir(dir)? + .fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{ + let entry = entry.expect("failed to read drectory entry"); + let meta = entry.metadata().expect("failed to read entry metadata"); + if meta.is_file() { + files.push(entry.file_name()); + } else if meta.is_dir() { + subdirs.push(entry.file_name()); + } + (subdirs, files) + }); + subdirs.sort(); + files.sort(); + Ok((subdirs, files)) + } +} + +pub use self::device::*; +mod device { + use crate::*; + def_command!(DeviceCommand: |device: Device| {}); + impl Device { + pub fn name (&self) -> &str { match self { - Version => show_version(), - Config => print_config(&config), - List => todo!("list sessions"), - Resume => todo!("resume session"), - New { - name, bpm, tracks, scenes, - sync_lead, sync_follow, - midi_from: mf, midi_from_re: mfr, midi_to: mt, midi_to_re: mtr, - left_from: lf, right_from: rf, left_to: lt, right_to: rt, .. - } => { - let name = name.as_ref().map_or("tek", |x|x.as_str()); - let jack = Jack::new(&name)?; - let mut proj = Arrangement::new( - &jack, - name.into(), - Clock::new(&jack, None)?, - [].into_iter(), - [].into_iter(), - Connect::midi_ins(&jack, &"M", &[], None)?.into_iter(), - Connect::midi_outs(&jack, &"M", &[], None)?.into_iter(), - [].into_iter() - .chain(Connect::audio_ins(&jack, &"L", &[], None)?.into_iter()) - .chain(Connect::audio_ins(&jack, &"R", &[], None)?.into_iter()), - [].into_iter() - .chain(Connect::audio_outs(&jack, &"L", &[], None)?.into_iter()) - .chain(Connect::audio_outs(&jack, &"R", &[], None)?.into_iter()), - ); - //&jack, Clock::new(&jack, *bpm)?, &lf, <, &rf, &rt, &mf, &mt, &mfr, &mtr)?; - proj.tracks_add(tracks.unwrap_or(0), None, &[], &[])?; - proj.scenes_add(scenes.unwrap_or(0))?; - //if matches!(self, Action::Status) { - //// Show status and exit - //tek_print_status(&proj); - //return Ok(()) - //} - // Initialize the app state - let app = Arc::new(RwLock::new(App::new(&jack, proj, config, ":menu"))); - //if matches!(self, Action::Headless) { - //// TODO: Headless mode (daemon + client over IPC, then over network...) - //println!("todo headless"); - //return Ok(()) - //} - let (_keyboard, _terminal) = Exit::run(|exited|Tui::io( - exited.as_ref(), - &app, - Duration::from_millis(100), - Duration::from_millis(10), - std::io::stdout() - ))?; - }, + Self::Sampler(sampler) => sampler.name.as_ref(), + _ => todo!(), + } + } + pub fn midi_ins (&self) -> &[MidiInput] { + match self { + //Self::Sampler(Sampler { midi_in, .. }) => &[midi_in], + _ => todo!() + } + } + pub fn midi_outs (&self) -> &[MidiOutput] { + match self { + Self::Sampler(_) => &[], + _ => todo!() + } + } + pub fn audio_ins (&self) -> &[AudioInput] { + match self { + Self::Sampler(Sampler { audio_ins, .. }) => audio_ins.as_slice(), + _ => todo!() + } + } + pub fn audio_outs (&self) -> &[AudioOutput] { + match self { + Self::Sampler(Sampler { audio_outs, .. }) => audio_outs.as_slice(), _ => todo!() } - Ok(()) } } + /// A device that can be plugged into the chain. + /// + /// ``` + /// let device = tek::Device::default(); + /// ``` + #[derive(Debug, Default)] pub enum Device { + #[default] + Bypass, + Mute, + #[cfg(feature = "sampler")] + Sampler(Sampler), + #[cfg(feature = "lv2")] // TODO + Lv2(Lv2), + #[cfg(feature = "vst2")] // TODO + Vst2, + #[cfg(feature = "vst3")] // TODO + Vst3, + #[cfg(feature = "clap")] // TODO + Clap, + #[cfg(feature = "sf2")] // TODO + Sf2, + } + + /// Some sort of wrapper? + pub struct DeviceAudio<'a>(pub &'a mut Device); + + impl_audio!(|self: DeviceAudio<'a>, client, scope|{ + use Device::*; + match self.0 { + Mute => { Control::Continue }, + Bypass => { /*TODO*/ Control::Continue }, + #[cfg(feature = "sampler")] Sampler(sampler) => sampler.process(client, scope), + #[cfg(feature = "lv2")] Lv2(lv2) => lv2.process(client, scope), + #[cfg(feature = "vst2")] Vst2 => { todo!() }, // TODO + #[cfg(feature = "vst3")] Vst3 => { todo!() }, // TODO + #[cfg(feature = "clap")] Clap => { todo!() }, // TODO + #[cfg(feature = "sf2")] Sf2 => { todo!() }, // TODO + } + }); + + pub fn device_kinds () -> &'static [&'static str] { + &[ + #[cfg(feature = "sampler")] "Sampler", + #[cfg(feature = "lv2")] "Plugin (LV2)", + ] + } + + impl> + AsMut>> HasDevices for T { + fn devices (&self) -> &Vec { + self.as_ref() + } + fn devices_mut (&mut self) -> &mut Vec { + self.as_mut() + } + } + + pub trait HasDevices: AsRef> + AsMut> { + fn devices (&self) -> &Vec { + self.as_ref() + } + fn devices_mut (&mut self) -> &mut Vec { + self.as_mut() + } + } + + 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::*; + pub mod dialog; pub use self::dialog::*; + pub mod editor; pub use self::editor::*; + pub mod menu; pub use self::menu::*; + pub mod meter; pub use self::meter::*; + pub mod mix; pub use self::mix::*; + pub mod sampler; pub use self::sampler::*; + pub mod sequence; pub use self::sequence::*; + + #[cfg(feature = "plugin")] pub mod plugin; + #[cfg(feature = "plugin")] pub use self::plugin::*; + + def_command!(AudioInputCommand: |port: AudioInput| { + Close => todo!(), + Connect { audio_out: Arc } => todo!(), + }); + + def_command!(AudioOutputCommand: |port: AudioOutput| { + Close => todo!(), + Connect { audio_in: Arc } => todo!(), + }); + + def_command!(MidiInputCommand: |port: MidiInput| { + Close => todo!(), + Connect { midi_out: Arc } => todo!(), + }); + + def_command!(MidiOutputCommand: |port: MidiOutput| { + Close => todo!(), + Connect { midi_in: Arc } => todo!(), + }); + + pub struct Junction(T); + + impl View for Junction { + fn view (&self) -> impl Draw { + T::KIND + } + } +} + +pub fn print_status (project: &Arrangement) { + println!("Name: {:?}", &project.name); + println!("JACK: {:?}", &project.jack); + println!("Buffer: {:?}", &project.clock.chunk); + println!("Sample rate: {:?}", &project.clock.timebase.sr); + println!("MIDI PPQ: {:?}", &project.clock.timebase.ppq); + println!("Tempo: {:?}", &project.clock.timebase.bpm); + println!("Quantize: {:?}", &project.clock.quant); + println!("Launch: {:?}", &project.clock.sync); + println!("Playhead: {:?}us", &project.clock.playhead.usec); + println!("Playhead: {:?}s", &project.clock.playhead.sample); + println!("Playhead: {:?}p", &project.clock.playhead.pulse); + println!("Started: {:?}", &project.clock.started); + println!("Tracks:"); + for (i, t) in project.tracks.iter().enumerate() { + println!(" Track {i}: {} {} {:?} {:?}", t.name, t.width, + &t.sequencer.play_clip, &t.sequencer.next_clip); + } + println!("Scenes:"); + for (i, t) in project.scenes.iter().enumerate() { + println!(" Scene {i}: {} {:?}", &t.name, &t.clips); + } + println!("MIDI Ins: {:?}", &project.midi_ins); + println!("MIDI Outs: {:?}", &project.midi_outs); + println!("Audio Ins: {:?}", &project.audio_ins); + println!("Audio Outs: {:?}", &project.audio_outs); + // TODO git integration + // TODO dawvert integration +} + +pub fn show_version () { + println!("versions aint real man"); +} //pub fn tui ( //app: Arc>, @@ -215,86 +836,3 @@ mod cli { //Ok(app) //})?)? //} - - pub fn show_version () { - println!("todo version"); - } - - 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!(); - }); - } - - pub fn print_status (project: &Arrangement) { - println!("Name: {:?}", &project.name); - println!("JACK: {:?}", &project.jack); - println!("Buffer: {:?}", &project.clock.chunk); - println!("Sample rate: {:?}", &project.clock.timebase.sr); - println!("MIDI PPQ: {:?}", &project.clock.timebase.ppq); - println!("Tempo: {:?}", &project.clock.timebase.bpm); - println!("Quantize: {:?}", &project.clock.quant); - println!("Launch: {:?}", &project.clock.sync); - println!("Playhead: {:?}us", &project.clock.playhead.usec); - println!("Playhead: {:?}s", &project.clock.playhead.sample); - println!("Playhead: {:?}p", &project.clock.playhead.pulse); - println!("Started: {:?}", &project.clock.started); - println!("Tracks:"); - for (i, t) in project.tracks.iter().enumerate() { - println!(" Track {i}: {} {} {:?} {:?}", t.name, t.width, - &t.sequencer.play_clip, &t.sequencer.next_clip); - } - println!("Scenes:"); - for (i, t) in project.scenes.iter().enumerate() { - println!(" Scene {i}: {} {:?}", &t.name, &t.clips); - } - println!("MIDI Ins: {:?}", &project.midi_ins); - println!("MIDI Outs: {:?}", &project.midi_outs); - println!("Audio Ins: {:?}", &project.audio_ins); - println!("Audio Outs: {:?}", &project.audio_outs); - // TODO git integration - // TODO dawvert integration - } - -} diff --git a/tengri b/tengri index c0d6d017..25354099 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit c0d6d0174e8858108ec053f4ac486cf6229980e1 +Subproject commit 25354099fe3cde43a242d41fdb673c4fce5c943e 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 2/2] 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