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] 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