From 922c2e1f2f3779984251cdbd67e56528110182fe Mon Sep 17 00:00:00 2001 From: i do not exist Date: Thu, 17 Sep 2026 09:49:38 +0300 Subject: [PATCH] big flat --- Cargo.lock | 4 + Cargo.toml | 2 +- src/app.rs | 599 ++++++++++++++++ src/bind.rs | 92 +++ src/binds.rs | 38 + src/{device => }/browse.rs | 0 src/cli.rs | 35 +- src/{device/arrange => }/clip.rs | 4 +- src/{device => }/clock.rs | 7 - src/{device/clock => }/clock_view.rs | 0 src/config.rs | 672 ++---------------- src/device.rs | 212 ++++++ src/{device => }/dialog.rs | 193 +++-- src/{device => }/editor.rs | 5 - src/field.rs | 81 +++ src/{device/plugin.rs => lv2.rs} | 44 +- src/lv2_gui.rs | 26 + src/{device/clock => }/memo.rs | 0 src/menu_item.rs | 33 + src/{device => }/meter.rs | 34 + src/{device => }/mix.rs | 0 src/mode.rs | 72 ++ src/mode_stack.rs | 46 ++ src/modes.rs | 45 ++ src/{device/clock => }/moment.rs | 0 src/{device/editor => }/octave.rs | 0 src/{device/editor => }/piano.rs | 0 src/plugin.rs | 1 + src/{device/editor => }/point.rs | 0 src/{device => }/pool.rs | 0 src/{device/arrange => }/port.rs | 4 +- src/{device/arrange.rs => project.rs} | 106 ++- src/{device/editor => }/range.rs | 0 src/{device => }/sampler.rs | 12 + src/{device/arrange => }/scene.rs | 10 +- src/{device/arrange => }/select.rs | 4 +- src/{device => }/sequence.rs | 0 src/tek.edn | 8 +- src/tek.rs | 970 +++----------------------- src/{device/clock => }/ticker.rs | 0 src/{device/clock => }/timebase.rs | 0 src/{device/arrange => }/track.rs | 35 +- src/views.rs | 304 ++++++++ src/vst2.rs | 15 + tengri | 2 +- 45 files changed, 1950 insertions(+), 1765 deletions(-) create mode 100644 src/app.rs create mode 100644 src/bind.rs create mode 100644 src/binds.rs rename src/{device => }/browse.rs (100%) rename src/{device/arrange => }/clip.rs (98%) rename src/{device => }/clock.rs (98%) rename src/{device/clock => }/clock_view.rs (100%) create mode 100644 src/device.rs rename src/{device => }/dialog.rs (50%) rename src/{device => }/editor.rs (99%) create mode 100644 src/field.rs rename src/{device/plugin.rs => lv2.rs} (82%) create mode 100644 src/lv2_gui.rs rename src/{device/clock => }/memo.rs (100%) create mode 100644 src/menu_item.rs rename src/{device => }/meter.rs (67%) rename src/{device => }/mix.rs (100%) create mode 100644 src/mode.rs create mode 100644 src/mode_stack.rs create mode 100644 src/modes.rs rename src/{device/clock => }/moment.rs (100%) rename src/{device/editor => }/octave.rs (100%) rename src/{device/editor => }/piano.rs (100%) create mode 100644 src/plugin.rs rename src/{device/editor => }/point.rs (100%) rename src/{device => }/pool.rs (100%) rename src/{device/arrange => }/port.rs (95%) rename src/{device/arrange.rs => project.rs} (61%) rename src/{device/editor => }/range.rs (100%) rename src/{device => }/sampler.rs (98%) rename src/{device/arrange => }/scene.rs (97%) rename src/{device/arrange => }/select.rs (97%) rename src/{device => }/sequence.rs (100%) rename src/{device/clock => }/ticker.rs (100%) rename src/{device/clock => }/timebase.rs (100%) rename src/{device/arrange => }/track.rs (95%) create mode 100644 src/views.rs create mode 100644 src/vst2.rs diff --git a/Cargo.lock b/Cargo.lock index d4e3b5ca..387d9832 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3893,9 +3893,13 @@ dependencies = [ "midly", "palette", "parking_lot 0.12.5", + "profiling", "quanta", "rand 0.8.7", "ratatui", + "tracing", + "tracing-flame", + "tracing-subscriber", "unicode-width 0.2.0", ] diff --git a/Cargo.toml b/Cargo.toml index 0411d666..e09c7e05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,7 +54,7 @@ proptest-derive = { version = "^0.5.1" } default = ["cli", "arranger", "sampler", "prof"] prof = [] -#prof2 = ["prof", "tengri/prof"] +prof2 = ["prof", "tengri/prof"] hotpath = ["hotpath/hotpath"] hotpath-cpu = ["hotpath/hotpath-cpu"] hotpath-alloc = ["hotpath/hotpath-alloc"] diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 00000000..7a354268 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,599 @@ +use crate::*; + +/// Total application state. +/// +/// ``` +/// use tek::{HasTracks, HasScenes, TracksView, ScenesView}; +/// let mut app = tek::App::default(); +/// let _ = app.scenes_add_one(None, None).unwrap(); +/// let _ = app.update_clock(); +/// app.project.editor = Some(Default::default()); +/// //let _: Vec<_> = app.project.inputs_with_sizes().collect(); +/// //let _: Vec<_> = app.project.outputs_with_sizes().collect(); +/// let _: Vec<_> = app.project.tracks_with_sizes().collect(); +/// //let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect(); +/// //let _: Vec<_> = app.scenes_with_colors(true, 10).collect(); +/// //let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect(); +/// let _ = app.project.w(); +/// //let _ = app.project.w_sidebar(); +/// //let _ = app.project.w_tracks_area(); +/// let _ = app.project.h(); +/// //let _ = app.project.h_tracks_area(); +/// //let _ = app.project.h_inputs(); +/// //let _ = app.project.h_outputs(); +/// let _ = app.project.h_scenes(); +/// ``` +#[derive(Default, Debug)] +#[namespace(MidiClip)] +#[namespace(ItemColor)] +#[namespace(ItemTheme)] +#[namespace(isize)] +#[namespace(u8)] +#[namespace(bool get_bool)] +#[namespace(u16 get_u16)] +#[namespace(usize get_usize)] +#[namespace(Arc<[Connect]> get_arc_array_connect)] +#[namespace(Arc get_arc_str)] +#[namespace(Color get_color)] +#[namespace(ControlAxis get_axis)] +#[namespace(Dialog get_dialog)] +#[namespace(Option> get_opt_arc_array_connect)] +#[namespace(Option> get_opt_arc_str)] +#[namespace(Option get_opt_itemtheme)] +#[namespace(Option>>>> get_opt_vec_opt_arc_rwlock_midiclip)] +#[namespace(Option get_opt_bool)] +#[namespace(Option get_opt_u16)] +#[namespace(Option get_opt_usize)] +#[namespace(PathBuf get_path_buf)] +#[namespace(Vec> get_vec_arc_str)] +pub struct App { + /// Exit flag + pub exit: Exit, + /// Base color. + pub color: ItemTheme, + /// Must not be dropped for the duration of the process + pub jack: Jack<'static>, + /// Display size + pub size: Sizer, + /// Performance counter + pub perf: PerfModel, + /// Available view modes and input bindings + pub config: Arc, + /// Currently selected mode + pub modes: 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: Project, + /// Error, if any + pub error: Arc>>>, + + #[cfg(feature = "prof2")] + /// Tracing guard + pub guard: Option>>, +} + +impl App { + /// Create a new application instance from a backend, project, config, and mode + /// + /// ``` + /// let mut proj = tek::Project::default(); + /// proj.jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack"); + /// let mut conf = tek::config_load(tek::Config::default(), "(mode hello)").unwrap(); + /// let tek = tek::App::new(None, proj, conf.into(), "hello"); + /// ``` + pub fn new ( + exit: Option, + project: Project, + config: Arc, + mode: impl AsRef + ) -> Self { + App { + exit: exit.unwrap_or_default(), + jack: project.jack.clone(), + color: ItemTheme::random(), + dialog: Dialog::welcome(), + modes: Arc::new(RwLock::new(vec![mode.as_ref().into()])), + config, + project, + ..Default::default() + } + } +} + +fn get_arc_str <'a> (_: &App, src: impl Language) -> Perhaps> { + Ok(src.src()?.map(|x|x.into())) +} + +fn get_u16 <'a> (state: &App, src: impl Language) -> Perhaps { + Ok(Some(match src.word()? { + Some(":w/sidebar") => state.project.w_sidebar(state.editor().is_some()), + Some(":h/sample-detail") => 6.max(state.size.h() as u16 * 3 / 9), + _ => return try_to_u16(src) + })) +} + +fn get_usize <'a> (state: &App, src: impl Language) -> Perhaps { + Ok(Some(match src.word()? { + Some(":scene-count") => state.scenes().len(), + Some(":track-count") => state.tracks().len(), + Some(":device-kind") => state.dialog.device_kind().unwrap_or(0), + Some(":device-kind/next") => state.dialog.device_kind_next().unwrap_or(0), + Some(":device-kind/prev") => state.dialog.device_kind_prev().unwrap_or(0), + _ => return try_to_usize(src) + })) +} + +fn get_bool <'a> (state: &App, src: impl Language) -> Perhaps { + src.word()?.map(|word|Ok(match word { + "Y" => true, + "N" => false, + ":mode/editor" => state.project.editor.is_some(), + ":focused/dialog" => !matches!(state.dialog, Dialog::None), + ":focused/message" => matches!(state.dialog, Dialog::Message(..)), + ":focused/add_device" => matches!(state.dialog, Dialog::Device(..)), + ":focused/browser" => state.dialog.browser().is_some(), + ":focused/pool/import" => matches!(state.pool.mode, Some(PoolMode::Import(..))), + ":focused/pool/export" => matches!(state.pool.mode, Some(PoolMode::Export(..))), + ":focused/pool/rename" => matches!(state.pool.mode, Some(PoolMode::Rename(..))), + ":focused/pool/length" => matches!(state.pool.mode, Some(PoolMode::Length(..))), + ":focused/clip" => !state.editor_focused() && matches!(state.selection(), Selection::TrackClip{..}), + ":focused/track" => !state.editor_focused() && matches!(state.selection(), Selection::Track(..)), + ":focused/scene" => !state.editor_focused() && matches!(state.selection(), Selection::Scene(..)), + ":focused/mix" => !state.editor_focused() && matches!(state.selection(), Selection::Mix), + _ => return Err(format!("not bool: {word}").into()) + })).transpose() +} + +#[allow(unused)] +fn get_selection <'a> (state: &App, src: impl Language) -> Perhaps { + src.word()?.map(|word|Ok(match word { + ":select/scene" => state.selection().select_scene(state.tracks().len()), + ":select/scene/next" => state.selection().select_scene_next(state.scenes().len()), + ":select/scene/prev" => state.selection().select_scene_prev(), + ":select/track" => state.selection().select_track(state.tracks().len()), + ":select/track/next" => state.selection().select_track_next(state.tracks().len()), + ":select/track/prev" => state.selection().select_track_prev(), + _ => return Err(format!("not selection: {word}").into()) + })).transpose() +} + +fn get_color <'a> (_: &App, src: impl Language) -> Perhaps { + if let Some(expr) = src.expr()? { + match (expr.head()?, expr.tail()?) { + (Some("g"), Some(tail)) => { + 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(tail.head().map_err(Into::into))? + .ok_or(LanguageError::domain("not red"))?; + let g = try_to_u8(tail.tail().head().map_err(Into::into))? + .ok_or(LanguageError::domain("not green"))?; + 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))) + }, + (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()) + } +} + +#[allow(unused)] +fn get_opt_u7 <'a> (state: &App, src: impl Language) -> Perhaps> { + src.word()?.map(|word|Ok(match word { + ":editor/pitch" => Some(( + state.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_usize <'a> (state: &App, src: impl Language) -> Perhaps> { + src.word()?.map(|word|Ok(match word { + ":selected/scene" => state.selection().scene(), + ":selected/track" => state.selection().track(), + _ => return Err(format!("unknown opt: {word}").into()) + })).transpose() +} + +#[allow(unused)] +fn get_clip <'a> (state: &App, src: impl Language) -> Perhaps>>> { + src.word()?.map(|word|Ok(match word { + ":selected/clip" if let Selection::TrackClip { track, scene } = state.selection() => + state.scenes()[*scene].clips[*track].clone(), + _ => return Err(format!("not a clip: {word}").into()) + })).transpose() +} + +fn get_axis <'a> (_: &App, src: impl Language) -> Perhaps { + Ok(src.word()?.map(|word|Ok(match word { + ":x" => ControlAxis::X, + ":y" => ControlAxis::Y, + _ => return Err(format!("unknown axis {word}")) + })).transpose()?) +} + +fn get_opt_u16 <'a> (_: &App, _src: impl Language) -> Perhaps> { + Ok(None) +} + +fn get_opt_itemtheme <'a> (_: &App, _: impl Language) -> Perhaps> { + todo!() +} + +fn get_opt_vec_opt_arc_rwlock_midiclip <'a> (_: &App, _: impl Language) -> Perhaps>>>>> { + todo!() +} + +fn get_opt_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps>> { + todo!() +} + +fn get_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps> { + todo!() +} + +fn get_opt_arc_str <'a> (_: &App, _: impl Language) -> Perhaps>> { + todo!() +} + +fn get_opt_bool <'a> (_: &App, _: impl Language) -> Perhaps> { + todo!() +} + +fn get_path_buf <'a> (_: &App, _: impl Language) -> Perhaps { + todo!() +} + +fn get_vec_arc_str <'a> (_: &App, _: impl Language) -> Perhaps>> { + todo!() +} + +impl_has!(Clock: |self: App|self.project.clock); +impl_has!(Dialog: |self: App|self.dialog); +impl_has!(Jack<'static>: |self: App|self.jack); +impl_has!(Pool: |self: App|self.pool); +impl_has!(Selection: |self: App|self.project.selection); +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); + +primitive!(u8: try_to_u8); +primitive!(u16: try_to_u16); +primitive!(usize: try_to_usize); +primitive!(isize: try_to_isize); + +tui_keys!(self: App, input { + #[cfg(feature = "prof2")] profiling::scope!("App::tui_keys!"); + Self::input_command(self, input) +}); + +impl App { + + fn input_command (state: &mut Self, input: &TuiEvent) -> Usually<()> { + if let Some(bindings) = Self::input_command_find(state, input) { + return Self::input_command_exec(state, bindings) + } + Ok(()) + } + + fn input_command_find (state: &mut Self, input: &TuiEvent) -> Option>]>> { + for name in state.modes.read().iter().rev() { + let mode = state.config.get_mode(name).as_ref().map(Arc::clone); + if let Some(mode) = mode { + let binds = state.config.binds.clone(); + for id in mode.keys.iter() { + if let Some(event_map) = binds.try_read().unwrap().get(id.as_ref()) + && let Some(bindings) = event_map.query(input) { + return Some(bindings.into()) + } + } + } + } + None + } + + fn input_command_exec (state: &mut Self, bindings: Arc<[Binding>]>) -> Usually<()> { + for binding in bindings.iter() { + for command in binding.commands.iter() { + if Self::command::(state, command)? { + return Ok(()) + } + } + } + Ok(()) + } + + fn command < + T: Dispatch + Clone + Into + > (state: &mut Self, src: &str) -> Usually where + Self: Namespace, + { + if let Some(command) = Namespace::::namespace(state, &src)? { + return Self::command_dispatch(state, command.into()) + } + Ok(false) + } + + fn command_dispatch (state: &mut Self, command: AppCommand) -> Usually where + Self: Namespace, + { + match command.clone().dispatch(state) { + Err(err) => { + state.history.push((command.into(), None)); + return Err(err) + }, + Ok(undo) => { + state.history.push((command.into(), undo.map(Into::into))); + return Ok(true) + } + } + } +} + +#[tek_proc::commands(AppCommand)] +impl App { + + /// Exit the application. + #[command(Exit = "exit")] + pub fn command_exit (&mut self) -> Perhaps { + self.exit.exit(); + Ok(None) + } + + /// Cancel current selection. + #[command(Cancel = "cancel")] + pub fn command_cancel (&mut self) -> Perhaps { + todo!("cancel") + } + + /// Confirm current selection. + #[command(Confirm = "confirm")] + pub fn command_confirm (&mut self) -> Perhaps { + Ok(match self.dialog.clone() { + Dialog::Menu(index, items) => { + for command in items[index].1.iter() { + App::command_dispatch(self, command.clone())?; + } + None + } + _ => todo!(), + }) + } + + /// Swap currently active modal dialog. + /// + /// ``` + /// let _ = tek::App::default().set_dialog(tek::Dialog::welcome()); + /// ``` + #[command(Dialog = "dialog")] + pub fn command_dialog (&mut self, dialog: Dialog) -> Perhaps { + let mut dialog = dialog.clone(); + std::mem::swap(&mut self.dialog, &mut dialog); + Ok(Some(AppCommand::Dialog { dialog })) + } + + #[command(Mode = "mode")] + pub fn command_mode (&mut self, command: ModeCommand) -> Perhaps { + Ok(command.dispatch(self)?.map(Into::into)) + } + + #[command(Project = "session")] + pub fn command_session (&mut self, command: ProjectCommand) -> Perhaps { + Ok(command.dispatch(self)?.map(Into::into)) + } + + #[command(Axis = "axis")] + pub fn command_axis (&mut self, command: AxisCommand) -> Perhaps { + Ok(command.dispatch(self)?.map(Into::into)) + } + + #[command(Browse = "browse")] + pub fn command_browse (&mut self, command: BrowseCommand) -> Perhaps { + Ok(command.dispatch(self)?.map(Into::into)) + } + + #[command(MidiClip = "clip")] + pub fn command_midi_clip (&mut self, command: MidiClipCommand) -> Perhaps { + Ok(command.dispatch(self)?.map(Into::into)) + } + + #[command(Pool = "pool")] + pub fn command_pool (&mut self, command: PoolCommand) -> Perhaps { + Ok(command.dispatch(self)?.map(Into::into)) + } + + #[command(Scene = "scene")] + pub fn command_scene (&mut self, command: SceneCommand) -> Perhaps { + Ok(command.dispatch(self)?.map(Into::into)) + } + + #[command(Scenes = "scenes")] + pub fn command_scenes (&mut self, command: ScenesCommand) -> Perhaps { + Ok(command.dispatch(self)?.map(Into::into)) + } + + #[command(Track = "track")] + pub fn command_track (&mut self, command: TrackCommand) -> Perhaps { + Ok(command.dispatch(self)?.map(Into::into)) + } + + #[command(Tracks = "tracks")] + pub fn command_tracks (&mut self, command: TracksCommand) -> Perhaps { + Ok(command.dispatch(self)?.map(Into::into)) + } + +} + +/// A control axis. +/// +/// ``` +/// let axis = tek::ControlAxis::X; +/// ``` +#[derive(Debug, Copy, Clone)] pub enum ControlAxis { + X, Y, Z, I +} + +#[tek_proc::commands(AxisCommand)] +impl App { + /// Increment a given data axis. + #[command(Inc = "inc")] + pub fn inc (&mut self, axis: ControlAxis) -> Perhaps { + Ok(match (&self.dialog, axis) { + (Dialog::None, _) => todo!(), + (Dialog::Menu(_, _), ControlAxis::Y) => { + AppCommand::Dialog { dialog: self.dialog.menu_next() }.dispatch(self)?; + None + }, + _ => todo!() + }) + } + /// Decrement a given data axis. + #[command(Dec = "dec")] + pub fn dec (&mut self, axis: ControlAxis) -> Perhaps { + Ok(match (&self.dialog, axis) { + (Dialog::None, _) => None, + (Dialog::Menu(_, _), ControlAxis::Y) => { + AppCommand::Dialog { dialog: self.dialog.menu_prev() }.dispatch(self)?; + None + }, + _ => todo!() + }) + } +} + +impl_from!(AppCommand: |x: AxisCommand| AppCommand::Axis { command: x }); +impl_from!(AppCommand: |x: BrowseCommand| AppCommand::Browse { command: x }); +impl_from!(AppCommand: |x: MidiClipCommand| AppCommand::MidiClip { command: x }); +impl_from!(AppCommand: |x: ModeCommand| AppCommand::Mode { command: x }); +impl_from!(AppCommand: |x: PoolCommand| AppCommand::Pool { command: x }); +impl_from!(AppCommand: |x: ProjectCommand| AppCommand::Project { command: x }); +impl_from!(AppCommand: |x: SceneCommand| AppCommand::Scene { command: x }); +impl_from!(AppCommand: |x: ScenesCommand| AppCommand::Scenes { command: x }); +impl_from!(AppCommand: |x: TrackCommand| AppCommand::Track { command: x }); +impl_from!(AppCommand: |x: TracksCommand| AppCommand::Tracks { command: x }); + +/// The [Draw] implementation for [App] handles the loaded view, +/// which is defined in terms of [dizzle] DSL. +/// +/// If there is an error, the error is displayed. FIXME: overlay it +/// Then, every top-level form of the DSL description is rendered. +impl Draw for App { + fn draw (&self, to: &mut Tui) -> Drawn { + #[cfg(feature = "prof2")] profiling::scope!("App::draw"); + //self.perf.cycle(&mut |_|{ + self.draw_error(to)?; + self.draw_modes(to)?; + self.draw_debug(to)?; + #[cfg(feature = "prof2")] profiling::finish_frame!(); + Ok(Some(to.area().into())) + //}) + } +} + +impl App { + fn draw_error (&self, to: &mut Tui) -> Usually<()> { + if let Some(e) = self.error.try_read().unwrap().as_ref() { + e.as_ref().align_c().draw(to)?; + } + Ok(()) + } + + fn draw_modes (&self, to: &mut Tui) -> Usually<()> { + for name in self.modes.read().iter() { + let mut error = false; + if let Some(mode) = self.config.get_mode(name).as_ref().map(Arc::clone) { + for (index, view) in mode.view.iter().enumerate() { + match (view.render)(self, to) { + Ok(None) => {}, + Ok(Some(XYWH(.., w, h))) => { + self.size.0.store(w as usize, Relaxed); + self.size.1.store(h as usize, Relaxed); + }, + Err(e) => { + let message = format!( + "Mode: {:?}\n\nLayer: #{index}\n\nError: {e}\n\nSource:\n{}", + &mode.name, + &view.source + ); + *self.error.try_write().unwrap() = Some(message.into()); + error = true; + break; + } + } + } + if error { + break + } else { + *self.error.try_write().unwrap() = None; + } + } + } + Ok(()) + } + + #[allow(unused)] fn draw_debug (&self, to: &mut Tui) -> Drawn { + east( + format!("{}x{} ", + self.size.0.load(Relaxed), + self.size.1.load(Relaxed)), + format!("{}/{} {} ", + self.perf.used.load(Relaxed), + self.perf.window.load(Relaxed), + self.perf.clock.raw() / 10000000), + ).align_se().draw(to) + } +} + +impl_has!(Sizer: |self: App|self.size); + +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 view_templates <'a> (state: &'a App) -> impl Draw + use<'a> { + let height = (state.config.modes.len() * 2) as u16; + draw(move |to: &mut Tui|{ + let mut index = 0; + state.config.modes.for_each(&mut |id: &str, profile: &Mode| { + 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) +} + +pub fn draw_status_bar (to: &mut Tui, state: &App) -> Drawn { + bar(Split::South, 1u16, + dim(true, "Mode"), + bold(true, state.modes.read().get(0)) + ).push_x(1).draw(to) +} diff --git a/src/bind.rs b/src/bind.rs new file mode 100644 index 00000000..a1b75a5e --- /dev/null +++ b/src/bind.rs @@ -0,0 +1,92 @@ +use crate::*; + +/// An map of input events (e.g. [TuiEvent]) to [Binding]s. +/// +/// ``` +/// use ::{std::sync::Arc, tek::{Bind, tengri::TuiEvent}}; +/// let bind = Bind::>::load(&stringify!( +/// (@x (nop)) +/// (@y (nop) (nop)))).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>>); + +/// 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, "*") }); + +//take!(DeviceCommand|state: Project, iter|state.selected_device().as_ref() + //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); diff --git a/src/binds.rs b/src/binds.rs new file mode 100644 index 00000000..5df8ac0b --- /dev/null +++ b/src/binds.rs @@ -0,0 +1,38 @@ +use crate::*; + +/// Collection of input bindings. +pub type Binds = Arc, Bind>>>>; + +pub fn binds_add <'a> (binds: &Binds, expr: impl Language) -> UsuallyRef<'a, ()> { + //println!("\n\rbinds_add: {expr:?}"); + let name = expr.head()?.ok_or("bind: missing name")?; + let body = expr.tail()?.unwrap_or(""); + binds.try_write().unwrap().insert(name.into(), { + let mut map = Bind::new(); + body.each((), |_, item: &str|if matches!(item.expr().head(), Ok(Some("see"))) { + // TODO + Ok(()) + } else if let Ok(Some(_word)) = item.expr().head().word() { + let expr = item.expr()?; + let head = expr.head()?; + if let Some(event) = TuiKey::from_dsl(&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!("binds_add: invalid key: {:?}", item.expr()?.head()?).into()) + } + } else { + return Err(format!("binds_add: unexpected: {item:?}").into()) + })?; + map + }); + Ok(()) +} diff --git a/src/device/browse.rs b/src/browse.rs similarity index 100% rename from src/device/browse.rs rename to src/browse.rs diff --git a/src/cli.rs b/src/cli.rs index a8d760b6..6f70fb47 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -10,32 +10,6 @@ pub fn show_version () { println!("versions aint real man"); } -#[cfg(not(feature = "cli"))] -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(None, 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()), - ), config, mode)) - })?) -} - pub fn run_with_config (config: Arc) -> Usually<()> { Cli::parse().run(Some(config)) } @@ -99,7 +73,7 @@ pub enum Action { } impl Action { - fn run (&self, config: Arc, trace: bool) -> Usually<()> { + fn run (&self, config: Arc, _trace: bool) -> Usually<()> { use Action::*; match self { Version => show_version(), @@ -109,6 +83,7 @@ impl Action { New(sesh) => Exit::run(|exit|Tui::run_main( exit.clone(), { + #[allow(unused_mut)] let mut app = App::new(Some(exit), sesh.init()?, config, ":menu"); #[cfg(feature = "prof2")] { if trace { @@ -165,7 +140,7 @@ pub struct ProjectInit { #[arg(short='s', long)] scenes: Option, } impl ProjectInit { - pub fn init (&self) -> Usually { + pub fn init (&self) -> Usually { let Self { name, bpm, tracks, scenes, sync_lead: _, sync_follow: _, @@ -175,7 +150,7 @@ impl ProjectInit { } = self; let name = name.as_ref().map_or("tek", |x|x.as_str()); let jack = Jack::new(&name)?; - let mut proj = Arrangement::new( + let mut proj = Project::new( &jack, name.into(), Clock::new(&jack, *bpm)?, @@ -207,7 +182,7 @@ impl ProjectInit { } } -pub fn print_status (project: &Arrangement) { +pub fn print_status (project: &Project) { println!("Name: {:?}", &project.name); println!("JACK: {:?}", &project.jack); println!("Buffer: {:?}", &project.clock.chunk); diff --git a/src/device/arrange/clip.rs b/src/clip.rs similarity index 98% rename from src/device/arrange/clip.rs rename to src/clip.rs index 33b88c67..7c9394ec 100644 --- a/src/device/arrange/clip.rs +++ b/src/clip.rs @@ -1,6 +1,6 @@ use crate::*; -impl Arrangement { +impl Project { /// Toggle looping for the active clip pub fn toggle_loop (&mut self) { if let Some(clip) = self.selected_clip() { @@ -39,7 +39,7 @@ impl HasClipsSize for App { fn clips_size (&self) -> &Sizer { &self.project.size_inner } } -impl HasClipsSize for Arrangement { +impl HasClipsSize for Project { fn clips_size (&self) -> &Sizer { &self.size_inner } } diff --git a/src/device/clock.rs b/src/clock.rs similarity index 98% rename from src/device/clock.rs rename to src/clock.rs index 009fb8b2..f43fc344 100644 --- a/src/device/clock.rs +++ b/src/clock.rs @@ -1,11 +1,5 @@ use crate::*; -mod memo; pub use self::memo::*; -mod moment; pub use self::moment::*; -mod ticker; pub use self::ticker::*; -mod timebase; pub use self::timebase::*; -mod clock_view; pub use self::clock_view::*; - impl App { /// Update memoized render of clock values. /// ``` @@ -508,4 +502,3 @@ pub fn button_play_pause <'a> (playing: bool, compact: bool) -> impl Draw { ) ) } - diff --git a/src/device/clock/clock_view.rs b/src/clock_view.rs similarity index 100% rename from src/device/clock/clock_view.rs rename to src/clock_view.rs diff --git a/src/config.rs b/src/config.rs index 9918d660..e3ee9014 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,184 +2,12 @@ use crate::*; use std::path::PathBuf; use notify_debouncer_full::notify::{RecursiveMode, PollWatcher, Config as NotifyConfig, Watcher}; -impl AsRef for Config { - fn as_ref (&self) -> &Config { - self - } -} - -/// Write initial contents of configuration. -pub fn config_init > (config: C) -> Usually { - if config.as_ref().find_file().is_none() { - std::fs::write(config.as_ref().place_file()?, Config::DEFAULTS)?; - } - Ok(if let Some(path) = config.as_ref().find_file() { - config_load(config, std::fs::read_to_string(&path)?)? - } else { - return Err(format!("config_init: not found").into()) - }) -} - -pub fn config_load , L: Language> (config: C, src: L) -> Usually { - config.as_ref().clear(); - config.as_ref().stamp.store(quanta::Clock::new().raw(), Relaxed); - src.each(config, |c, s|config_load_item(c, s.trim())) -} - -pub fn config_load_item , L: Language> (config: C, src: L) -> Usually { - if let Some(expr) = src.expr()? { - config_load_kind(config, expr) - } else { - Err(format!("Config::add_one: tried to add empty item").into()) - } -} - -pub fn config_load_kind , L: Language> (config: C, src: L) -> Usually { - //if let Some(tail) = src.tail()? { - match src.head()? { - Some("mode") => modes_add(&config.as_ref().modes, src.tail()), - Some("keys") => binds_add(&config.as_ref().binds, src.tail()), - Some("view") => views_add(&config.as_ref().views, src.tail()), - _ => return Err(format!("Config::load: expected view/keys/mode expr, got: {src:?}").into()) - }?; - //} - Ok(config) -} - -/// Watch a config's file for changes. -pub fn config_watch ( - config: Arc, poll: Option -) -> Usually<()> { - let poll = poll.unwrap_or(Duration::from_millis(250)); - let opts = NotifyConfig::default().with_poll_interval(poll); - let mut watcher = ::notify_debouncer_full::notify::poll::PollWatcher::new({ - let config = config.clone(); - move|result|{ - match result { - Ok(_events) => if let Err(e) = config_init(config.as_ref()) { - *config.as_ref().error.try_write().unwrap() = Some(format!("{e:?}").into()); - panic!("{e:?}"); - } else { - //println!("config updated"); - }, - Err(errors) => { - panic!("{errors:?}"); - } - } - } - }, opts)?; - if let Some(path) = config.as_ref().get_file() { - //println!("watching: {path:?}"); - watcher.watch(&path, RecursiveMode::NonRecursive)?; - *config.as_ref().watch.try_write().unwrap() = Some(watcher); - Ok(()) - } else { - Err(format!("no config path").into()) - } -} - -/// Register a mode. -pub fn modes_add <'a> (modes: &Modes, expr: impl Language) -> UsuallyRef<'a, ()> { - let name = expr.head()?.ok_or("mode: missing name")?; - let body = expr.tail()?.ok_or("mode: missing body")?; - let mode = Mode::default(); - let mode = body.each(mode, |c, s|mode_add(c, s))?; - modes.0.try_write().unwrap().insert(name.into(), Arc::new(mode)); - Ok(()) -} - -impl AsMut for Mode { - fn as_mut (&mut self) -> &mut Self { - self - } -} - -/// Add a definition to the mode. -/// -/// Supported definitions: -/// -/// - (name ...) -> name -/// - (info ...) -> description -/// - (keys ...) -> key bindings -/// - (mode ...) -> submode -/// - ... -> view -/// -/// ``` -/// let mut mode: tek::Mode = tek::mode_add(tek::Mode::default(), "(name hello)").unwrap(); -/// ``` -pub fn mode_add > (mut mode: T, 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 { - "mode" => { - let name = tail.head()?.ok_or("submode: missing name")?; - let body = tail.tail()?.ok_or("submode: missing body")?; - let submode = Mode::default(); - let submode = body.each(submode, |c,s|mode_add(c,s))?; - let modes = mode.as_mut().modes.clone(); - modes.0.try_write().unwrap().insert(name.into(), Arc::new(submode)); - mode - }, - "keys" => { - tail.each(mode, |mut mode: T, expr: &str|{ - mode.as_mut().keys.push(expr.trim().into()); - Ok(mode) - })? - }, - "name" => { mode.as_mut().name.push(tail.into()); mode }, - "info" => { mode.as_mut().info.push(tail.into()); mode }, - "view" => { mode.as_mut().view.push(View::new(tail)?.into()); mode }, - _ => { mode.as_mut().view.push(View::new(tail)?.into()); mode }, - } - } else if let Ok(Some(word)) = dsl.word() { - mode.as_mut().view.push(View::new(word)?.into()); - mode - } else { - return Err(format!("Mode::add: unexpected: {dsl:?}").into()); - }) -} - -pub fn binds_add <'a> (binds: &Binds, expr: impl Language) -> UsuallyRef<'a, ()> { - //println!("\n\rbinds_add: {expr:?}"); - let name = expr.head()?.ok_or("bind: missing name")?; - let body = expr.tail()?.unwrap_or(""); - binds.try_write().unwrap().insert(name.into(), { - let mut map = Bind::new(); - body.each((), |_, item: &str|if matches!(item.expr().head(), Ok(Some("see"))) { - // TODO - Ok(()) - } else if let Ok(Some(_word)) = item.expr().head().word() { - let expr = item.expr()?; - let head = expr.head()?; - if let Some(event) = TuiKey::from_dsl(&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!("binds_add: invalid key: {:?}", item.expr()?.head()?).into()) - } - } else { - return Err(format!("binds_add: unexpected: {item:?}").into()) - })?; - map - }); - Ok(()) -} - /// Configuration: mode, view, and bind definitions. /// /// ``` -/// use ::{std::sync::Arc, tek::{Config, modes_add, Mode}}; +/// use ::{std::sync::Arc, tek::{Config, Modes::add, Mode}}; /// let config: Config = Default::default(); -/// modes_add(&config.modes, stringify!( +/// Modes::add(&config.modes, stringify!( /// (mode :menu (name Menu) /// (info Mode selector.) (keys :axis/y :confirm) /// (view (bg (g 0) (bsp/s :ports/out @@ -206,339 +34,84 @@ pub struct Config { pub watch: RwLock> } -/// 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 AsRef for Config { + fn as_ref (&self) -> &Config { + self + } } -/// Collection of custom view definitions. -pub type Views = Arc, Arc>>>>; - -/// Custom view definition is a boxed closure emitting a [Draw]able from state `S`. -pub struct View { - pub source: Arc, - pub render: ArcDrawn + Send + Sync>> -} - -impl_debug!( View |self, w| { write!(w, "View({})", self.source) }); - -impl_display!( View |self, w| { write!(w, "View({})", self.source) }); - -impl View { - - pub fn new (source: impl AsRef) -> Usually { - Ok(Self { - source: source.as_ref().into(), - render: Self::compile(source)? +impl Config { + /// Write initial contents of configuration. + pub fn init > (config: C) -> Usually { + if config.as_ref().find_file().is_none() { + std::fs::write(config.as_ref().place_file()?, Config::DEFAULTS)?; + } + Ok(if let Some(path) = config.as_ref().find_file() { + Config::load(config, std::fs::read_to_string(&path)?)? + } else { + return Err(format!("config_init: not found").into()) }) } - fn boxed Drawn + Send + Sync + 'static> (f: F) - -> BoxDrawn + Send + Sync + 'static> - { - Box::new(f) + pub fn load , L: Language> (config: C, src: L) -> Usually { + config.as_ref().clear(); + config.as_ref().stamp.store(quanta::Clock::new().raw(), Relaxed); + src.each(config, |c, s|Config::load_item(c, s.trim())) } - fn compile (source: impl AsRef) -> - UsuallyDrawn + Send + Sync>>> - { - let source = source.as_ref(); - let layer = if let Ok(Some(expr)) = source.expr() { - Self::compile_expr(expr.into())? - } else if let Ok(Some(word)) = source.word() { - Self::compile_word(word.into())? + pub fn load_item , L: Language> (config: C, src: L) -> Usually { + if let Some(expr) = src.expr()? { + Config::load_kind(config, expr) } else { - return Err(format!("not word/expr:\n{source:?}").into()) - }; - Ok(Arc::new(Box::new(move|state, screen|layer(state, screen)))) + Err(format!("Config::add_one: tried to add empty item").into()) + } } - fn compile_expr (expr: Arc) -> - UsuallyDrawn + Send + Sync>>> - { + pub fn load_kind , L: Language> (config: C, src: L) -> Usually { + //if let Some(tail) = src.tail()? { + match src.head()? { + Some("mode") => Modes::add(&config.as_ref().modes, src.tail()), + Some("keys") => binds_add(&config.as_ref().binds, src.tail()), + Some("view") => views_add(&config.as_ref().views, src.tail()), + _ => return Err(format!("Config::load: expected view/keys/mode expr, got: {src:?}").into()) + }?; + //} + Ok(config) + } - macro_rules! arg { - ($src:expr, $head:expr, $index:expr, $name:expr) => { - $src.nth($index)? - .ok_or_else(||Box::::from(format!( - "{}: no arg #{} ({}) in {}", $head, $index, $name, $src - )))? - }; - } - - Ok(Arc::new(if let Some(head) = expr.head()? && let Some(ns) = head.split('/').next() { - - match ns { - - "when" => { - let cond = Arc::from(arg!(expr, head, 1, "condition")); - let cond = move|state: &App|state.namespace(&cond)?.ok_or_else(||Box::::from("when: no condition value")); - let thunk = Self::compile(arg!(expr, head, 2, "content"))?; - Self::boxed(move|state, screen|{ - when( - cond(state)?, - draw(|screen|thunk(state, screen)) - ).draw(screen) - }) - }, - - "either" => { - let cond = Arc::from(arg!(expr, head, 1, "condition")); - let cond = move|state: &App|state.namespace(&cond)?.ok_or_else(||Box::::from("either: no condition value")); - let a = Self::compile(arg!(expr, head, 2, "content A"))?; - let b = Self::compile(arg!(expr, head, 3, "content B"))?; - Self::boxed(move|state, screen|{ - either( - cond(state)?, - draw(|screen|a(state, screen)), - draw(|screen|b(state, screen)), - ).draw(screen) - }) - }, - - "bsp" | "split" | "stack" => { - let split = head.split('/').skip(1).next(); - let split = match split { - Some("n") => Split::North, - Some("s") => Split::South, - Some("e") => Split::East, - Some("w") => Split::West, - Some("a") => Split::Above, - Some("b") => Split::Below, - _ => return Err(format!("invalid split: {split:?}").into()) - }; - let a = Self::compile(arg!(expr, head, 1, "content A"))?; - let b = Self::compile(arg!(expr, head, 2, "content B"))?; - Self::boxed(move|state, screen|split.stack( - draw(|screen|a(state, screen)), - draw(|screen|b(state, screen)), - ).draw(screen)) - }, - - "bar" => { - let split = head.split('/').skip(1).next(); - let split = match split { - Some("n") => Split::North, - Some("s") => Split::South, - Some("e") => Split::East, - Some("w") => Split::West, - Some("a") => Split::Above, - Some("b") => Split::Below, - _ => return Err(format!("invalid split: {split:?}").into()) - }; - let size = Arc::from(arg!(expr, head, 1, "size")); - let size = move|state: &App|state.namespace(&size)?.ok_or_else(||Box::::from("bar: no size")); - let a = Self::compile(arg!(expr, head, 2, "content A"))?; - let b = Self::compile(arg!(expr, head, 3, "content B"))?; - Self::boxed(move|state, screen|split.bar( - size(state)?, - draw(|screen|a(state, screen)), - draw(|screen|b(state, screen)), - ).draw(screen)) - }, - - "align" => { - let azimuth = head.split('/').skip(1).next(); - let azimuth = match azimuth { - Some("n") => Azimuth::N, - Some("s") => Azimuth::S, - Some("e") => Azimuth::E, - Some("w") => Azimuth::W, - Some("ne") => Azimuth::NE, - Some("se") => Azimuth::SE, - Some("nw") => Azimuth::NW, - Some("sw") => Azimuth::SW, - Some("c") => Azimuth::C, - Some("x") => Azimuth::X, - Some("y") => Azimuth::Y, - _ => return Err(format!("invalid azimuth: {azimuth:?}").into()) - }; - let thunk = Self::compile(arg!(expr, head, 1, "content"))?; - Self::boxed(move|state, screen|{ - Align( - Some(azimuth), - draw(|screen|thunk(state, screen)) - ).draw(screen) - }) - }, - - "full" | "fill" => { - let thunk = Self::compile(arg!(expr, head, 1, "content"))?; - match head.split('/').skip(1).next() { - Some("w") | Some("x") => Self::boxed(move|state, screen|{ - Full::W(draw(|screen|thunk(state, screen))).draw(screen) - }), - Some("h") | Some("y") => Self::boxed(move|state, screen|{ - Full::H(draw(|screen|thunk(state, screen))).draw(screen) - }), - Some("wh") | Some("xy") | None => Self::boxed(move|state, screen|{ - Full::WH(draw(|screen|thunk(state, screen))).draw(screen) - }), - _ => unreachable!() + /// Watch a config's file for changes. + pub fn watch ( + config: Arc, poll: Option + ) -> Usually<()> { + let poll = poll.unwrap_or(Duration::from_millis(250)); + let opts = NotifyConfig::default().with_poll_interval(poll); + let mut watcher = ::notify_debouncer_full::notify::poll::PollWatcher::new({ + let config = config.clone(); + move|result|{ + match result { + Ok(_events) => if let Err(e) = Config::init(config.as_ref()) { + *config.as_ref().error.try_write().unwrap() = Some(format!("{e:?}").into()); + panic!("{e:?}"); + } else { + //println!("config updated"); + }, + Err(errors) => { + panic!("{errors:?}"); } - }, - - "exact" | "min" | "max" | "push" | "pull" | "pad" => { - match head.split('/').skip(1).next() { - Some("w") | Some("x") => { - let value = Arc::from(arg!(expr, head, 1, "value")); - let value = move|state: &App|state.namespace(&value); - let thunk = Self::compile(arg!(expr, head, 2, "content"))?; - match ns { - "exact" => Self::boxed(move|state, screen|Exact::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), - "push" => Self::boxed(move|state, screen|Push::X(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), - "pull" => Self::boxed(move|state, screen|Pull::X(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), - "pad" => Self::boxed(move|state, screen|Pad::X(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), - "min" => Self::boxed(move|state, screen|Min::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), - "max" => Self::boxed(move|state, screen|Max::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), - _ => unreachable!() - } - }, - Some("h") | Some("y") => { - let value = Arc::from(arg!(expr, head, 1, "value")); - let value = move|state: &App|state.namespace(&value); - let thunk = Self::compile(arg!(expr, head, 2, "content"))?; - match ns { - "exact" => Self::boxed(move|state, screen|Exact::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), - "push" => Self::boxed(move|state, screen|Push::Y(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), - "pull" => Self::boxed(move|state, screen|Pull::Y(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), - "pad" => Self::boxed(move|state, screen|Pad::Y(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), - "min" => Self::boxed(move|state, screen|Min::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), - "max" => Self::boxed(move|state, screen|Max::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), - _ => unreachable!() - } - }, - Some("wh") | Some("xy") | None => { - let value1 = Arc::from(arg!(expr, head, 1, "value")); - let value1 = move|state: &App|state.namespace(&value1); - let value2 = Arc::from(arg!(expr, head, 2, "value")); - let value2 = move|state: &App|state.namespace(&value2); - let thunk = Self::compile(arg!(expr, head, 3, "content"))?; - match ns { - "exact" => Self::boxed(move|state, screen|Exact::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)), - "push" => Self::boxed(move|state, screen|Push::XY(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)), - "pull" => Self::boxed(move|state, screen|Pull::XY(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)), - "pad" => Self::boxed(move|state, screen|Pad::XY(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)), - "min" => Self::boxed(move|state, screen|Min::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)), - "max" => Self::boxed(move|state, screen|Max::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)), - _ => unreachable!() - } - }, - _ => unreachable!() - } - }, - - "fg" | "bg" => { - let color = Arc::from(arg!(expr, head, 1, "color")); - let color = move|state: &App|state.namespace(&color); - let thunk = Self::compile(arg!(expr, head, 2, "content"))?; - match ns { - "fg" => Self::boxed(move|state, screen|fg( - color(state)?.unwrap_or_default(), draw(|screen|thunk(state, screen)) - ).draw(screen)), - "bg" => Self::boxed(move|state, screen|bg( - color(state)?.unwrap_or_default(), draw(|screen|thunk(state, screen)) - ).draw(screen)), - _ => unreachable!() - } - }, - - "text" => { - let text: Arc = Arc::from(expr.tail()?.unwrap_or_default()); - Self::boxed(move|_state, screen|{ - text.draw(screen) - }) - }, - - _ => return Err(format!("compile_expr: unexpected: {expr:?}").into()) - + } } - + }, opts)?; + if let Some(path) = config.as_ref().get_file() { + //println!("watching: {path:?}"); + watcher.watch(&path, RecursiveMode::NonRecursive)?; + *config.as_ref().watch.try_write().unwrap() = Some(watcher); + Ok(()) } else { - return Err(format!("compile_expr: invalid expression: {expr:?}").into()) - })) - } - - fn compile_word (word: Arc) - -> UsuallyDrawn + Send + Sync>>> - { - macro_rules! draw { - (|$state:ident|$body:expr)=>{Self::boxed(move|$state, to|$body.draw(to))} + Err(format!("no config path").into()) } - - Ok(Arc::new(match word.split("/").next() { - //Some(":logo") => view_logo().draw(to), - Some(":meters") => match word.split("/").skip(1).next() { - Some("input") => Self::boxed(move|_, to|bg(Rgb(30, 30, 30), "Input Meters".align_s().full_h()).draw(to)), - Some("output") => Self::boxed(move|_, to|bg(Rgb(30, 30, 30), "Output Meters".align_s().full_h()).draw(to)), - _ => panic!() - }, - Some(":tracks") => match word.split("/").skip(1).next() { - None => Self::boxed(move|_, to|"TODO tracks".draw(to)), - Some("names") => draw!(|app|app.project.view_track_names(app.color.clone())), - Some("inputs") => draw!(|app|app.project.view_track_inputs(app.color.clone())), - Some("devices") => draw!(|app|app.project.view_track_devices(app.color.clone())), - Some("outputs") => draw!(|app|app.project.view_track_outputs(app.color.clone(), 0)), - _ => panic!() - }, - Some(":scenes") => match word.split("/").skip(1).next() { - Some("clips") => draw!(|app|app.view_scenes_clips()), - Some("names") => draw!(|app|app.view_scenes_names()), - _ => panic!() - }, - Some(":templates") => draw!(|app|view_templates(app)), - Some(":browse/title") => draw!(|app|view_browse_title(app)), - Some(":device") => draw!(|app|view_device(app)), - Some(":dialog") => Self::boxed(move|state, to|draw_dialog(to, word.split("/").skip(1), state)), - Some(":sessions") => Self::boxed(move|_, to|view_sessions().draw(to)), - Some(":status") => Self::boxed(move|state, to|draw_status_bar(to, state)), - Some(":editor") => Self::boxed(move|_, to|"TODO Editor".draw(to)), - Some(":transport") => Self::boxed(move|_, to|view_transport(true, "", "", "").draw(to)), - Some(":debug") => Self::boxed(move|_, to|format!("[{:?}]", to.area()).exact_h(1).draw(to)), - Some(_) => Self::boxed(move|state, to|if let Some(view) = state.config.get_view(word.as_ref()) { - (view.render)(state, to) - } else { - fg(Color::Rgb(128, 32, 32), format!("undefined: {word:?}")).draw(to) - }), - _ => unreachable!() - })) } } -pub fn draw_status_bar (to: &mut Tui, state: &App) -> Drawn { - bar(Split::South, 1u16, dim(true, "Mode"), bold(true, &state.mode)).push_x(1).draw(to) -} - -/// Load custom view definition. -pub fn views_add <'a> (views: &Views, expr: impl Language) -> UsuallyRef<'a, ()> { - views.try_write().unwrap().insert( - expr.head()?.ok_or("view: missing name")?.into(), - View::new(expr.tail()?.ok_or("view: missing body")?)?.into() - ); - Ok(()) -} - impl Config { /// Default configuration directory. @@ -567,14 +140,14 @@ impl Config { /// Create, initialize, and watch a new configuration. pub fn watched (callback: impl FnOnce(Arc)->T) -> Usually { let config = Self::init_new(None)?; - config_watch(config.clone(), None)?; + Config::watch(config.clone(), None)?; let result = callback(config); Ok(result) } /// Create and initialize a new configuration. pub fn init_new (_dirs: Option) -> Usually> { - config_init(Arc::new(Self::new(None))) + Config::init(Arc::new(Self::new(None))) } /// Find the config file. @@ -614,120 +187,3 @@ impl Config { } } - -impl Modes { - /// Get a mode by name. - pub fn get (&self, name: impl AsRef) -> Option> { - self.0.try_read().unwrap().get(name.as_ref()).cloned() - } - /// Run something for each mode. - pub fn for_each (&self, mut ator: impl FnMut(&str, &Mode)->T) { - for (k, v) in self.0.try_read().unwrap().iter() { - let _ = ator(k.as_ref(), v.as_ref()); - } - } - /// Count modes. - pub fn len (&self) -> usize { - self.0.try_read().unwrap().len() - } -} - -pub use self::bind::*; -mod bind { - use crate::*; - - /// Collection of input bindings. - pub type Binds = Arc, Bind>>>>; - - /// An map of input events (e.g. [TuiEvent]) to [Binding]s. - /// - /// ``` - /// use ::{std::sync::Arc, tek::{Bind, tengri::TuiEvent}}; - /// let bind = Bind::>::load(&stringify!( - /// (@x (nop)) - /// (@y (nop) (nop)))).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> { - - } - - /// 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, "*") }); -} diff --git a/src/device.rs b/src/device.rs new file mode 100644 index 00000000..8dd6bae4 --- /dev/null +++ b/src/device.rs @@ -0,0 +1,212 @@ +use crate::*; + +impl App { + /// 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!(), + } + } +} + +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.tracks_jack_process(client, scope); + state.perf.update_from_jack_scope(t0, scope); + result +} + +fn tek_jack_event (state: &mut App, event: JackEvent) { + use JackEvent::*; + match event { + SampleRate(sr) => { state.clock().timebase.sr.set(sr as f64); }, + PortRegistration(_id, true) => { + //let port = self.jack().port_by_id(id); + //println!("\rport add: {id} {port:?}"); + //println!("\rport add: {id}"); + }, + PortRegistration(_id, false) => { + /*println!("\rport del: {id}")*/ + }, + PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ }, + PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ }, + ClientRegistration(_id, true) => {}, + ClientRegistration(_id, false) => {}, + ThreadInit => {}, + XRun => {}, + GraphReorder => {}, + _ => { panic!("{event:?}"); } + } +} + +#[tek_proc::command(Device)] +#[tek_proc::keyword(Device)] +#[derive(Debug)] +pub enum DeviceCommand {} + +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 fn view_device <'a> (state: &'a App) -> impl Draw { + let selected = state.dialog.device_kind().unwrap(); + south( + bold(true, "Add device"), + iter_south(move||device_kinds().iter().enumerate().map(move|(i, _label)|{ + 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/device/dialog.rs b/src/dialog.rs similarity index 50% rename from src/device/dialog.rs rename to src/dialog.rs index e4dca836..f19e5639 100644 --- a/src/device/dialog.rs +++ b/src/dialog.rs @@ -1,4 +1,51 @@ -use crate::{*, device::*}; +use crate::*; + +pub fn get_dialog <'a> (_: &App, src: impl Language) -> Perhaps { + src.word()?.map(|word|Ok(match word { + ":dialog/none" => Dialog::None, + ":dialog/options" => Dialog::Options, + ":dialog/device" => Dialog::Device(0), + ":dialog/device/prev" => Dialog::Device(0), + ":dialog/device/next" => Dialog::Device(0), + ":dialog/help" => Dialog::Help(0), + ":dialog/save" => Dialog::Browse( + BrowseTarget::SaveProject, + Browse::new(None).unwrap().into()), + ":dialog/load" => Dialog::Browse( + BrowseTarget::LoadProject, + Browse::new(None).unwrap().into()), + ":dialog/import/clip" => Dialog::Browse( + BrowseTarget::ImportClip(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/export/clip" => Dialog::Browse( + BrowseTarget::ExportClip(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/import/sample" => Dialog::Browse( + BrowseTarget::ImportSample(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/export/sample" => Dialog::Browse( + BrowseTarget::ExportSample(Default::default()), + Browse::new(None).unwrap().into()), + _ => return Err( + format!("not a dialog: {}", src.src()?.unwrap_or("")).into() + ) + })).transpose() +} + +/// Various possible dialog modes. +/// +/// ``` +/// let dialog: tek::Dialog = Default::default(); +/// ``` +#[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog { + #[default] None, + Help(usize), + Menu(usize, Arc<[MenuItem]>), + Device(usize), + Message(Arc), + Browse(BrowseTarget, Arc), + Options, +} pub fn draw_dialog <'a, I: Debug + Iterator> (to: &mut Tui, mut frags: I, state: &App) -> Drawn @@ -6,7 +53,7 @@ pub fn draw_dialog <'a, I: Debug + Iterator> (to: &mut Tui, mut match frags.next() { Some("menu") => if let Dialog::Menu(selected, items) = &state.dialog { - Some(iter_south(move||items.0.iter().enumerate().map(move|(index, MenuItem(item, _))|{ + Some(iter_south(move||items.iter().enumerate().map(move|(index, MenuItem(item, _))|{ 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).exact_h(2).align_x() @@ -19,112 +66,49 @@ pub fn draw_dialog <'a, I: Debug + Iterator> (to: &mut Tui, mut } } -impl App { - - pub fn get_dialog <'a> (&self, src: impl Language) -> Perhaps { - src.word()?.map(|word|Ok(match word { - ":dialog/none" => Dialog::None, - ":dialog/options" => Dialog::Options, - ":dialog/device" => Dialog::Device(0), - ":dialog/device/prev" => Dialog::Device(0), - ":dialog/device/next" => Dialog::Device(0), - ":dialog/help" => Dialog::Help(0), - ":dialog/save" => Dialog::Browse( - BrowseTarget::SaveProject, - Browse::new(None).unwrap().into()), - ":dialog/load" => Dialog::Browse( - BrowseTarget::LoadProject, - Browse::new(None).unwrap().into()), - ":dialog/import/clip" => Dialog::Browse( - BrowseTarget::ImportClip(Default::default()), - Browse::new(None).unwrap().into()), - ":dialog/export/clip" => Dialog::Browse( - BrowseTarget::ExportClip(Default::default()), - Browse::new(None).unwrap().into()), - ":dialog/import/sample" => Dialog::Browse( - BrowseTarget::ImportSample(Default::default()), - Browse::new(None).unwrap().into()), - ":dialog/export/sample" => Dialog::Browse( - BrowseTarget::ExportSample(Default::default()), - Browse::new(None).unwrap().into()), - _ => return Err( - format!("not a dialog: {}", src.src()?.unwrap_or("")).into() - ) - })).transpose() - } - -} - -/// Various possible dialog modes. -/// -/// ``` -/// let dialog: tek::Dialog = Default::default(); -/// ``` -#[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog { - #[default] None, - Help(usize), - Menu(usize, MenuItems), - Device(usize), - Message(Arc), - Browse(BrowseTarget, Arc), - Options, -} - -/// List of menu items. -/// -/// ``` -/// let items: tek::MenuItems = Default::default(); -/// ``` -#[derive(Debug, Clone, Default, PartialEq)] pub struct MenuItems( - pub Arc<[MenuItem]> -); - -/// An item of a menu. -/// -/// ``` -/// let item: tek::MenuItem = Default::default(); -/// ``` -#[derive(Clone)] pub struct MenuItem( - /// Label - pub Arc, - /// Callback - pub ArcUsually<()> + Send + Sync>> -); - -impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) }); - -impl_default!(MenuItem: Self("".into(), Arc::new(Box::new(|_|Ok(()))))); - -impl PartialEq for MenuItem { fn eq (&self, other: &Self) -> bool { self.0 == other.0 } } - -impl AsRef> for MenuItems { fn as_ref (&self) -> &Arc<[MenuItem]> { &self.0 } } - impl Dialog { + /// ``` /// let _ = tek::Dialog::welcome(); /// ``` pub fn welcome () -> Self { - Self::Menu(1, MenuItems([ + Self::Menu(1, Arc::new([ - MenuItem(" Resume session ".into(), Arc::new(Box::new(|_app|{ - Ok(()) - }))), + MenuItem::new(" Resume session ", [ + AppCommand::Project { + command: ProjectCommand::LoadLast + } + ]), - MenuItem(" Create session ".into(), Arc::new(Box::new(|app|Ok({ - app.dialog = Dialog::None; - app.mode = Some(":arranger".into()); - })))), + MenuItem::new(" Create session ", [ + AppCommand::Dialog { + dialog: Dialog::None + }, + AppCommand::Mode { + command: ModeCommand::Reset { + modes: vec![":arranger".into()] + } + }, + ]), - MenuItem(" Load session ".into(), Arc::new(Box::new(|_|{ - Ok(()) - }))), + MenuItem::new(" Load session ", [ + AppCommand::Dialog { + dialog: Dialog::Browse( + BrowseTarget::LoadProject, + Browse::new(None).unwrap().into() + ) + //dialog: Dialog::Browse { + //title: "Load session".into(), + //filter: "\\.tek$", + //} + } + ]), - MenuItem(" Exit ".into(), Arc::new(Box::new(|app|Ok({ - app.exit.exit(); - println!("Exited cleanly.") - })))), + MenuItem::new(" Exit ", [ + AppCommand::Exit + ]), - ].into())) + ])) } /// FIXME: generalize @@ -134,26 +118,29 @@ impl Dialog { pub fn menu_selected (&self) -> Option { if let Self::Menu(selected, _) = self { Some(*selected) } else { None } } + /// FIXME: generalize /// ``` /// let _ = tek::Dialog::welcome().menu_next(); /// ``` pub fn menu_next (&self) -> Self { match self { - Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.0.len()), items.clone()), + Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.len()), items.clone()), _ => Self::None } } + /// FIXME: generalize /// ``` /// let _ = tek::Dialog::welcome().menu_prev(); /// ``` pub fn menu_prev (&self) -> Self { match self { - Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.0.len()), items.clone()), + Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.len()), items.clone()), _ => Self::None } } + /// FIXME: generalize /// ``` /// let _ = tek::Dialog::welcome().device_kind(); @@ -161,6 +148,7 @@ impl Dialog { pub fn device_kind (&self) -> Option { if let Self::Device(index) = self { Some(*index) } else { None } } + /// FIXME: generalize /// ``` /// let _ = tek::Dialog::welcome().device_kind_next(); @@ -168,6 +156,7 @@ impl Dialog { pub fn device_kind_next (&self) -> Option { self.device_kind().map(|index|(index + 1) % device_kinds().len()) } + /// FIXME: generalize /// ``` /// let _ = tek::Dialog::welcome().device_kind_prev(); @@ -175,12 +164,16 @@ impl Dialog { pub fn device_kind_prev (&self) -> Option { self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1))) } + /// FIXME: implement pub fn message (&self) -> Option<&str> { todo!() } + /// FIXME: implement pub fn browser (&self) -> Option<&Arc> { todo!() } + /// FIXME: implement pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() } + } /// Increment a wrapping counter. diff --git a/src/device/editor.rs b/src/editor.rs similarity index 99% rename from src/device/editor.rs rename to src/editor.rs index edc897fb..dc784d43 100644 --- a/src/device/editor.rs +++ b/src/editor.rs @@ -432,8 +432,3 @@ impl Draw for MidiEditor { self.size.of(&self.mode).draw(to) } } - -mod octave; pub use self::octave::*; -mod piano; pub use self::piano::*; -mod point; pub use self::point::*; -mod range; pub use self::range::*; diff --git a/src/field.rs b/src/field.rs new file mode 100644 index 00000000..ca808cb3 --- /dev/null +++ b/src/field.rs @@ -0,0 +1,81 @@ +use crate::*; + +pub fn field_h <'a, T: Screen> ( + _theme: ItemTheme, _head: impl Draw, _body: impl Draw +) -> impl Draw { +} + +pub fn field_v <'a, T: Screen> ( + _theme: ItemTheme, _head: impl Draw, _body: impl Draw +) -> impl Draw { +} + +pub fn view_sessions <'a> () -> impl Draw { + let h = 6; + let w = Some(30); + let f = Rgb(224, 192, 128); + draw(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) +} + +/// ``` +/// 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 <'a> (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::button_2("", "", true); +/// let _ = tek::button_2("", "", false); +/// ``` +pub fn button_2 <'a> (key: impl Draw, label: impl Draw, hide: bool) + -> impl Draw +{ + let c1 = tui_orange(); + let c2 = tui_g(0); + let c3 = tui_g(96); + let c4 = tui_g(255); + bold(true, fg_bg(c1, c2, + bar(Split::East, 1u16, fg(c2, "▐"), + bar(Split::East, 1u16, key, + bar(Split::East, 1u16, fg(c3, "▐"), + when(!hide, fg_bg(c4, c3, label))))))) +} + +/// ``` +/// let _ = tek::button_3("", "", "", true); +/// let _ = tek::button_3("", "", "", false); +/// ``` +pub fn button_3 <'a> ( + key: impl Draw, + label: impl Draw, + value: impl Draw, + editing: bool, +) -> impl Draw { + let c1 = tui_orange(); + let c2 = tui_g(0); + let c3 = tui_g(96); + let c4 = tui_g(255); + let c5 = tui_g(128); + bold(true, bar(Split::East, 3u16, + fg_bg(c1, c2, + bar(Split::East, 1u16, fg(c2, "▐"), + bar(Split::East, 1u16, key, fg(if editing { c5 } else { c3 }, "▐")))), + east( + when(!editing, east(fg_bg(c4, c3, label), fg_bg(c5, c3, "▐"),)), + east(fg_bg(tui_g(224), c5, value), fg_bg(c5, Reset, "▌"), )))) +} diff --git a/src/device/plugin.rs b/src/lv2.rs similarity index 82% rename from src/device/plugin.rs rename to src/lv2.rs index a3bafa53..aafdc18b 100644 --- a/src/device/plugin.rs +++ b/src/lv2.rs @@ -1,15 +1,7 @@ use crate::*; -#[cfg(feature = "lv2_gui")] use ::winit::{ - application::ApplicationHandler, - event::WindowEvent, - event_loop::{ActiveEventLoop, ControlFlow, EventLoop}, - window::{Window, WindowId}, - platform::x11::EventLoopBuilderExtX11 -}; - /// A LV2 plugin. -#[derive(Debug)] #[cfg(feature = "lv2")] pub struct Lv2 { +#[derive(Debug)] pub struct Lv2 { /// JACK client handle (needs to not be dropped for standalone mode to work). pub jack: Jack<'static>, pub name: Arc, @@ -30,11 +22,6 @@ use crate::*; pub lv2_ui_thread: Option>, } -/// A LV2 plugin's X11 UI. -#[cfg(feature = "lv2_gui")] pub struct LV2PluginUI { - pub window: Option -} - impl_audio!(Lv2: lv2_jack_process); impl Lv2 { @@ -183,32 +170,3 @@ fn draw_header (state: &Lv2, to: &mut Tui, x: u16, y: u16, w: u16) { } //Ok(Rect { x, y, width: w, height: 1 }) } - -#[cfg(feature = "vst2")] impl ::vst::host::Host for Plugin {} - - -#[cfg(feature = "vst2")] fn set_vst_plugin ( - host: &Arc>>, _path: &str -) -> Usually { - let mut loader = ::vst::host::PluginLoader::load( - &std::path::Path::new("/nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lxvst/helm.so"), - host.clone() - )?; - Ok(PluginKind::VST2 { - instance: loader.instance()? - }) -} - -#[cfg(feature = "lv2_gui")] -pub fn run_lv2_ui (mut ui: LV2PluginUI) -> Usually> { - Ok(spawn(move||{ - let event_loop = EventLoop::builder().with_x11().with_any_thread(true).build().unwrap(); - event_loop.set_control_flow(ControlFlow::Wait); - event_loop.run_app(&mut ui).unwrap() - })) -} - -#[cfg(feature = "lv2_gui")] -fn lv2_ui_instantiate (kind: &str) { - //let host = Suil -} diff --git a/src/lv2_gui.rs b/src/lv2_gui.rs new file mode 100644 index 00000000..0c70afbf --- /dev/null +++ b/src/lv2_gui.rs @@ -0,0 +1,26 @@ +use crate::*; + +use ::winit::{ + application::ApplicationHandler, + event::WindowEvent, + event_loop::{ActiveEventLoop, ControlFlow, EventLoop}, + window::{Window, WindowId}, + platform::x11::EventLoopBuilderExtX11 +}; + +/// A LV2 plugin's X11 UI. +pub struct LV2PluginUI { + pub window: Option +} + +pub fn run_lv2_ui (mut ui: LV2PluginUI) -> Usually> { + Ok(spawn(move||{ + let event_loop = EventLoop::builder().with_x11().with_any_thread(true).build().unwrap(); + event_loop.set_control_flow(ControlFlow::Wait); + event_loop.run_app(&mut ui).unwrap() + })) +} + +fn lv2_ui_instantiate (kind: &str) { + //let host = Suil +} diff --git a/src/device/clock/memo.rs b/src/memo.rs similarity index 100% rename from src/device/clock/memo.rs rename to src/memo.rs diff --git a/src/menu_item.rs b/src/menu_item.rs new file mode 100644 index 00000000..73537a8d --- /dev/null +++ b/src/menu_item.rs @@ -0,0 +1,33 @@ +use crate::*; + +/// An item of a menu. +/// +/// ``` +/// let item: tek::MenuItem = Default::default(); +/// ``` +#[derive(Clone)] pub struct MenuItem( + pub Arc, + pub Arc<[AppCommand]>, +); + +impl MenuItem { + pub fn new (name: impl AsRef, actions: [AppCommand; N]) -> Self { + Self(name.as_ref().into(), Arc::new(actions)) + } +} + +impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) }); + +impl_default!(MenuItem: Self::new("", [])); + +impl PartialEq for MenuItem { + fn eq (&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +//impl AsRef> for MenuItems { + //fn as_ref (&self) -> &Arc<[MenuItem]> { + //&self.0 + //} +//} diff --git a/src/device/meter.rs b/src/meter.rs similarity index 67% rename from src/device/meter.rs rename to src/meter.rs index 52c17949..dd3f3331 100644 --- a/src/device/meter.rs +++ b/src/meter.rs @@ -60,6 +60,40 @@ pub fn to_rms (samples: &[f32]) -> f32 { (sum / samples.len() as f32).sqrt() } +/// ``` +/// 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 +{ + 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) +} + #[cfg(test)] mod test_view_meter { use super::*; use proptest::prelude::*; diff --git a/src/device/mix.rs b/src/mix.rs similarity index 100% rename from src/device/mix.rs rename to src/mix.rs diff --git a/src/mode.rs b/src/mode.rs new file mode 100644 index 00000000..4402a462 --- /dev/null +++ b/src/mode.rs @@ -0,0 +1,72 @@ +use crate::*; + +/// 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 = tek::Mode::add(tek::Mode::default(), "(name hello)").unwrap(); + /// ``` + pub fn add > (mut mode: T, 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 { + "mode" => { + let name = tail.head()?.ok_or("submode: missing name")?; + let body = tail.tail()?.ok_or("submode: missing body")?; + let submode = Mode::default(); + let submode = body.each(submode, |c,s|Mode::add(c,s))?; + let modes = mode.as_mut().modes.clone(); + modes.0.try_write().unwrap().insert(name.into(), Arc::new(submode)); + mode + }, + "keys" => { + tail.each(mode, |mut mode: T, expr: &str|{ + mode.as_mut().keys.push(expr.trim().into()); + Ok(mode) + })? + }, + "name" => { mode.as_mut().name.push(tail.into()); mode }, + "info" => { mode.as_mut().info.push(tail.into()); mode }, + "view" => { mode.as_mut().view.push(View::new(tail)?.into()); mode }, + _ => { mode.as_mut().view.push(View::new(tail)?.into()); mode }, + } + } else if let Ok(Some(word)) = dsl.word() { + mode.as_mut().view.push(View::new(word)?.into()); + mode + } else { + return Err(format!("Mode::add: unexpected: {dsl:?}").into()); + }) + } + +} + +impl AsMut for Mode { + fn as_mut (&mut self) -> &mut Self { + self + } +} diff --git a/src/mode_stack.rs b/src/mode_stack.rs new file mode 100644 index 00000000..3d381a3d --- /dev/null +++ b/src/mode_stack.rs @@ -0,0 +1,46 @@ +use crate::*; + +pub trait HasModeStack { + fn modes (&self) -> RwLockReadGuard>>; + fn modes_mut (&mut self) -> RwLockWriteGuard>>; +} + +impl HasModeStack for App { + fn modes (&self) -> RwLockReadGuard>> { + self.modes.try_read().unwrap() + } + fn modes_mut (&mut self) -> RwLockWriteGuard>> { + self.modes.try_write().unwrap() + } +} + +impl ModeController for T where T: HasModeStack + + Namespace>> + + Namespace> + + Namespace +{} + +#[tek_proc::commands(ModeCommand)] +pub trait ModeController: HasModeStack + + Namespace>> + + Namespace> + + Namespace +{ + #[command(Reset = "reset")] + fn mode_reset (&mut self, modes: Vec>) -> Perhaps { + *self.modes_mut() = modes.into(); + Ok(None) + } + + #[command(Push = "push")] + fn mode_push (&mut self, mode: Arc) -> Perhaps { + self.modes_mut().push(mode); + Ok(Some(ModeCommand::Pop)) + } + + #[command(Pop = "pop")] + fn mode_pop (&mut self) -> Perhaps { + let mode = self.modes_mut().pop(); + Ok(mode.map(|mode|ModeCommand::Push { mode })) + } +} diff --git a/src/modes.rs b/src/modes.rs new file mode 100644 index 00000000..7a718106 --- /dev/null +++ b/src/modes.rs @@ -0,0 +1,45 @@ +use crate::*; + +/// 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( + pub Arc, Arc>>> +); + +impl Modes { + + /// Register a mode. + pub fn add <'a> (modes: &Modes, expr: impl Language) -> UsuallyRef<'a, ()> { + let name = expr.head()?.ok_or("mode: missing name")?; + let body = expr.tail()?.ok_or("mode: missing body")?; + let mode = Mode::default(); + let mode = body.each(mode, |c, s|Mode::add(c, s))?; + modes.0.try_write().unwrap().insert(name.into(), Arc::new(mode)); + Ok(()) + } + + /// Get a mode by name. + pub fn get (&self, name: impl AsRef) -> Option> { + self.0.try_read().unwrap().get(name.as_ref()).cloned() + } + + /// Run something for each mode. + pub fn for_each (&self, mut ator: impl FnMut(&str, &Mode)->T) { + for (k, v) in self.0.try_read().unwrap().iter() { + let _ = ator(k.as_ref(), v.as_ref()); + } + } + + /// Count modes. + pub fn len (&self) -> usize { + self.0.try_read().unwrap().len() + } + +} diff --git a/src/device/clock/moment.rs b/src/moment.rs similarity index 100% rename from src/device/clock/moment.rs rename to src/moment.rs diff --git a/src/device/editor/octave.rs b/src/octave.rs similarity index 100% rename from src/device/editor/octave.rs rename to src/octave.rs diff --git a/src/device/editor/piano.rs b/src/piano.rs similarity index 100% rename from src/device/editor/piano.rs rename to src/piano.rs diff --git a/src/plugin.rs b/src/plugin.rs new file mode 100644 index 00000000..c7b7e813 --- /dev/null +++ b/src/plugin.rs @@ -0,0 +1 @@ +use crate::*; diff --git a/src/device/editor/point.rs b/src/point.rs similarity index 100% rename from src/device/editor/point.rs rename to src/point.rs diff --git a/src/device/pool.rs b/src/pool.rs similarity index 100% rename from src/device/pool.rs rename to src/pool.rs diff --git a/src/device/arrange/port.rs b/src/port.rs similarity index 95% rename from src/device/arrange/port.rs rename to src/port.rs index ffe9398a..54cc6a7a 100644 --- a/src/device/arrange/port.rs +++ b/src/port.rs @@ -1,7 +1,7 @@ use crate::*; def_sizes_iter!(PortsSizes => Arc, [Connect]); -impl_has!(Vec: |self: Arrangement| self.midi_ins); -impl_has!(Vec: |self: Arrangement| self.midi_outs); +impl_has!(Vec: |self: Project| self.midi_ins); +impl_has!(Vec: |self: Project| self.midi_outs); impl_has!(Vec: |self: App|self.project.midi_ins); impl_has!(Vec: |self: App|self.project.midi_outs); diff --git a/src/device/arrange.rs b/src/project.rs similarity index 61% rename from src/device/arrange.rs rename to src/project.rs index 724673fa..35afbbe0 100644 --- a/src/device/arrange.rs +++ b/src/project.rs @@ -1,12 +1,45 @@ use crate::*; +impl AsRef> for App { + fn as_ref (&self) -> &Option { + todo!() + } +} + +impl AsMut> for App { + fn as_mut (&mut self) -> &mut Option { + todo!() + } +} + +pub trait HasProject: AsRef> + AsMut> {} + +impl> + AsMut>> HasProject for T {} + +impl ProjectController for T {} + +#[tek_proc::commands(ProjectCommand)] +pub trait ProjectController: HasProject { + + #[command(LoadLast = "load-last")] + fn load_last (&mut self) -> Perhaps { + todo!() + } + + #[command(Load = "load")] + fn load (&mut self) -> Perhaps { + todo!() + } + +} + /// Arranger. /// /// ``` -/// let arranger = tek::Arrangement::default(); +/// let arranger = tek::Project::default(); /// ``` #[derive(Default, Debug)] -pub struct Arrangement { +pub struct Project { /// JACK client handle. pub jack: Jack<'static>, /// Project name. @@ -46,7 +79,7 @@ pub struct Arrangement { #[cfg(feature = "scene")] pub scene_scroll: usize, } -impl Arrangement { +impl Project { /// Create a new arrangement. pub fn new ( @@ -104,43 +137,15 @@ impl Arrangement { } } -/// Get the first sampler of the active track -#[cfg(feature = "sampler")] -impl Arrangement { - pub fn sampler (&self) -> Option<&Sampler> { - self.selected_track()?.sampler(0) - } +impl HasJack<'static> for Project { fn jack (&self) -> &Jack<'static> { &self.jack } } +impl_has!(Jack<'static>: |self: Project| self.jack); +impl_has!(Sizer: |self: Project| self.size); +impl_has!(Clock: |self: Project| self.clock); +impl_has!(Selection: |self: Project| self.selection); +impl_as_ref_opt!(MidiEditor: |self: Project| self.editor.as_ref()); +impl_as_mut_opt!(MidiEditor: |self: Project| self.editor.as_mut()); - /// Get the first sampler of the active track - #[cfg(feature = "sampler")] - pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { - self.selected_track_mut()?.sampler_mut(0) - } -} - -impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &self.jack } } -impl_has!(Jack<'static>: |self: Arrangement| self.jack); -impl_has!(Sizer: |self: Arrangement| self.size); -impl_has!(Clock: |self: Arrangement| self.clock); -impl_has!(Selection: |self: Arrangement| self.selection); -impl_as_ref_opt!(MidiEditor: |self: Arrangement| self.editor.as_ref()); -impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut()); - -/// 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 {} - } -} - -mod clip; pub use self::clip::*; -mod scene; pub use self::scene::*; -mod select; pub use self::select::*; -mod port; pub use self::port::*; -mod track; pub use self::track::*; - -pub(self) fn swap_value ( +pub fn swap_value ( target: &mut T, value: &T, returned: impl Fn(T)->U ) -> Perhaps { if *target == *value { @@ -152,7 +157,7 @@ pub(self) fn swap_value ( } } -pub(self) fn toggle_bool ( +pub fn toggle_bool ( target: &mut bool, value: &Option, returned: impl Fn(Option)->U ) -> Perhaps { let mut value = value.unwrap_or(!*target); @@ -163,24 +168,3 @@ pub(self) fn toggle_bool ( Ok(Some(returned(Some(value)))) } } - - //pub fn per_track <'a, T: Draw<'_, Tui> + 'a, U: TracksSizes<'a>> ( - //tracks: impl Fn() -> U + Send + Sync + 'a, - //callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a - //) -> impl Draw<'_, Tui> + 'a { - //per_track_top(tracks, move|index, track|callback(index, track).full_h().align_y()) - //} - - //pub fn per_track_top <'a, T: Draw<'_, Tui> + 'a, U: TracksSizes<'a>> ( - //tracks: impl Fn() -> U + Send + Sync + 'a, - //callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a - //) -> impl Draw<'_, Tui> + 'a { - //bg(Reset, iter_east(||tracks() - //.map(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()) - //} diff --git a/src/device/editor/range.rs b/src/range.rs similarity index 100% rename from src/device/editor/range.rs rename to src/range.rs diff --git a/src/device/sampler.rs b/src/sampler.rs similarity index 98% rename from src/device/sampler.rs rename to src/sampler.rs index 4f702894..55d35da7 100644 --- a/src/device/sampler.rs +++ b/src/sampler.rs @@ -8,6 +8,18 @@ pub(crate) use symphonia::{ }, }; +/// Get the first sampler of the active track +impl Project { + pub fn sampler (&self) -> Option<&Sampler> { + self.selected_track()?.sampler(0) + } + + /// Get the first sampler of the active track + pub fn sampler_mut (&mut self) -> Option<&mut Sampler> { + self.selected_track_mut()?.sampler_mut(0) + } +} + pub trait HasSampler: AsRef + AsMut { fn sampler (&self) -> &Sampler { self.as_ref() diff --git a/src/device/arrange/scene.rs b/src/scene.rs similarity index 97% rename from src/device/arrange/scene.rs rename to src/scene.rs index 1dc9f76f..f0798ca0 100644 --- a/src/device/arrange/scene.rs +++ b/src/scene.rs @@ -57,8 +57,8 @@ impl Scene { impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt()); impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt()); -impl_as_ref_opt!(Scene: |self: Arrangement| self.selected_scene()); -impl_as_mut_opt!(Scene: |self: Arrangement| self.selected_scene_mut()); +impl_as_ref_opt!(Scene: |self: Project| self.selected_scene()); +impl_as_mut_opt!(Scene: |self: Project| self.selected_scene_mut()); impl + AsMutOpt + Send + Sync> HasScene for T {} @@ -123,7 +123,7 @@ pub trait SceneController: HasScene pub type SceneWith<'a, T> = (usize, &'a Scene, usize, usize, T); def_sizes_iter!(ScenesSizes => Scene); -impl_has!(Vec: |self: Arrangement| self.scenes); +impl_has!(Vec: |self: Project| self.scenes); impl_as_ref!(Vec: |self: App| self.project.as_ref()); impl_as_mut!(Vec: |self: App| self.project.as_mut()); @@ -271,7 +271,7 @@ impl ScenesView for App { } } -impl ScenesView for Arrangement { +impl ScenesView for Project { fn h_scenes (&self) -> u16 { (self.size.h() as u16).saturating_sub(20) } @@ -311,7 +311,7 @@ pub trait HasSceneScroll: HasScenes { fn scene_scroll (&self) -> usize; } -impl HasSceneScroll for Arrangement { +impl HasSceneScroll for Project { fn scene_scroll (&self) -> usize { self.scene_scroll } diff --git a/src/device/arrange/select.rs b/src/select.rs similarity index 97% rename from src/device/arrange/select.rs rename to src/select.rs index b3d23a5f..e061294b 100644 --- a/src/device/arrange/select.rs +++ b/src/select.rs @@ -1,7 +1,7 @@ use crate::*; -impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track()); -impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut()); +impl_as_ref_opt!(Track: |self: Project| self.selected_track()); +impl_as_mut_opt!(Track: |self: Project| self.selected_track_mut()); impl_as_ref_opt!(Track: |self: App| self.project.as_ref_opt()); impl_as_mut_opt!(Track: |self: App| self.project.as_mut_opt()); impl +AsMut> HasSelection for T {} diff --git a/src/device/sequence.rs b/src/sequence.rs similarity index 100% rename from src/device/sequence.rs rename to src/sequence.rs diff --git a/src/tek.edn b/src/tek.edn index 61775aeb..78f5a22c 100644 --- a/src/tek.edn +++ b/src/tek.edn @@ -20,13 +20,13 @@ (bar/s 2 (align/x (bg (g 12) :logo)) :dialog/menu))))) -(keys :back (@escape (back))) -(keys :confirm (@enter (dialog confirm))) +(keys :back (@escape cancel)) +(keys :confirm (@enter confirm)) (keys :axis/y (@up (axis (dec :y))) (@down (axis (inc :y)))) (mode :arranger (name Arranger) (info Launch grid.) - (keys :clock :color :launch :scenes :tracks :global) + (keys :clock :color :launch :scenes :tracks :global :history :saveload) (mode :editor (keys :editor)) (mode :dialog (keys :dialog)) (mode :message (keys :message)) @@ -46,7 +46,7 @@ (bar/n 2 :tracks/inputs (bar/e 16 :scenes/names :scenes/clips)))))))) -(keys :global (see :history :saveload) +(keys :global (@f1 (dialog :help)) (@f8 (dialog :options)) (@f10 (dialog :quit))) diff --git a/src/tek.rs b/src/tek.rs index 6c099a47..f062dd15 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -1,13 +1,66 @@ #![allow(clippy::unit_arg)] -//#![feature(impl_trait_in_assoc_type)] // Used by EntriesIterator; TODO remove -mod deps; pub use self::deps::*; + +/// 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 {} + } +} + +mod app; pub use self::app::*; +mod bind; pub use self::bind::*; +mod binds; pub use self::binds::*; +mod browse; pub use self::browse::*; +mod cli; pub use self::cli::*; +mod clip; pub use self::clip::*; +mod clock; pub use self::clock::*; +mod clock_view; pub use self::clock_view::*; +mod config; pub use self::config::*; +mod deps; pub use self::deps::*; +mod device; pub use self::device::*; +mod dialog; pub use self::dialog::*; +mod editor; pub use self::editor::*; +mod field; pub use self::field::*; +mod memo; pub use self::memo::*; +mod menu_item; pub use self::menu_item::*; +mod meter; pub use self::meter::*; +mod mix; pub use self::mix::*; +mod mode; pub use self::mode::*; +mod mode_stack; pub use self::mode_stack::*; +mod modes; pub use self::modes::*; +mod moment; pub use self::moment::*; +mod octave; pub use self::octave::*; +mod piano; pub use self::piano::*; +mod point; pub use self::point::*; +mod pool; pub use self::pool::*; +mod port; pub use self::port::*; +mod project; pub use self::project::*; +mod range; pub use self::range::*; +mod sampler; pub use self::sampler::*; +mod scene; pub use self::scene::*; +mod select; pub use self::select::*; +mod sequence; pub use self::sequence::*; +mod ticker; pub use self::ticker::*; +mod timebase; pub use self::timebase::*; +mod track; pub use self::track::*; +mod views; pub use self::views::*; + +#[cfg(feature = "lv2")] mod lv2; +#[cfg(feature = "lv2")] pub use self::lv2::*; + +#[cfg(feature = "lv2_gui")] mod lv2_gui; +#[cfg(feature = "lv2_gui")] pub use self::lv2_gui::*; + +#[cfg(feature = "vst2")] mod vst2; +#[cfg(feature = "vst2")] pub use self::vst2::*; /// Command-line entrypoint. #[allow(unused)] #[hotpath::main] fn main () -> Usually<()> { tengri::Tui::setup_panic(); - #[cfg(feature = "cli")] let outcome = Config::watched(run_with_config); + #[cfg(feature = "cli")] let outcome = Config::watched(run_with_config); #[cfg(not(feature = "cli"))] let outcome = Config::watched(run_new_plain); if let Err(e) = outcome { println!("{e:#?}"); @@ -16,891 +69,28 @@ fn main () -> Usually<()> { Ok(()) } -pub use self::cli::*; -mod cli; - -pub use self::config::*; -mod config; - -pub use self::app::*; -mod app { - use crate::*; - 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!(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_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.scenes_add_one(None, None).unwrap(); - /// let _ = app.update_clock(); - /// app.project.editor = Some(Default::default()); - /// //let _: Vec<_> = app.project.inputs_with_sizes().collect(); - /// //let _: Vec<_> = app.project.outputs_with_sizes().collect(); - /// let _: Vec<_> = app.project.tracks_with_sizes().collect(); - /// //let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect(); - /// //let _: Vec<_> = app.scenes_with_colors(true, 10).collect(); - /// //let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect(); - /// let _ = app.project.w(); - /// //let _ = app.project.w_sidebar(); - /// //let _ = app.project.w_tracks_area(); - /// let _ = app.project.h(); - /// //let _ = app.project.h_tracks_area(); - /// //let _ = app.project.h_inputs(); - /// //let _ = app.project.h_outputs(); - /// let _ = app.project.h_scenes(); - /// ``` - #[derive(Default, Debug)] - #[namespace(MidiClip)] - #[namespace(ItemColor)] - #[namespace(ItemTheme)] - #[namespace(isize)] - #[namespace(u8)] - #[namespace(Arc<[Connect]> get_arc_array_connect)] - #[namespace(Arc get_arc_str)] - #[namespace(Color get_color)] - #[namespace(ControlAxis get_axis)] - #[namespace(Option> get_opt_arc_array_connect)] - #[namespace(Option> get_opt_arc_str)] - #[namespace(Option get_opt_itemtheme)] - #[namespace(Option>>>> get_opt_vec_opt_arc_rwlock_midiclip)] - #[namespace(Option get_opt_u16)] - #[namespace(Option get_opt_usize)] - #[namespace(Option get_opt_bool)] - #[namespace(bool get_bool)] - #[namespace(u16 get_u16)] - #[namespace(PathBuf get_path_buf)] - #[namespace(usize get_usize)] - #[namespace(Dialog App::get_dialog)] - pub struct App { - /// Exit flag - pub exit: Exit, - /// Base color. - pub color: ItemTheme, - /// Must not be dropped for the duration of the process - pub jack: Jack<'static>, - /// Display size - pub size: Sizer, - /// Performance counter - pub perf: PerfModel, - /// Available view modes and input bindings - pub config: Arc, - /// 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>>>, - - #[cfg(feature = "prof2")] - /// Tracing guard - pub guard: Option>>, - } - - 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_load(tek::Config::default(), "(mode hello)").unwrap(); - /// let tek = tek::App::new(None, proj, conf.into(), "hello"); - /// ``` - pub fn new ( - exit: Option, - project: Arrangement, - config: Arc, - mode: impl AsRef - ) -> Self { - App { - exit: exit.unwrap_or_default(), - jack: project.jack.clone(), - color: ItemTheme::random(), - dialog: Dialog::welcome(), - mode: Some(mode.as_ref().into()), - config, - project, - ..Default::default() - } - } - } - - fn get_arc_str <'a> (_: &App, src: impl Language) -> Perhaps> { - Ok(src.src()?.map(|x|x.into())) - } - fn get_u16 <'a> (state: &App, src: impl Language) -> Perhaps { - Ok(Some(match src.word()? { - Some(":w/sidebar") => state.project.w_sidebar(state.editor().is_some()), - Some(":h/sample-detail") => 6.max(state.size.h() as u16 * 3 / 9), - _ => return try_to_u16(src) - })) - } - fn get_usize <'a> (state: &App, src: impl Language) -> Perhaps { - Ok(Some(match src.word()? { - Some(":scene-count") => state.scenes().len(), - Some(":track-count") => state.tracks().len(), - Some(":device-kind") => state.dialog.device_kind().unwrap_or(0), - Some(":device-kind/next") => state.dialog.device_kind_next().unwrap_or(0), - Some(":device-kind/prev") => state.dialog.device_kind_prev().unwrap_or(0), - _ => return try_to_usize(src) - })) - } - fn get_bool <'a> (state: &App, src: impl Language) -> Perhaps { - src.word()?.map(|word|Ok(match word { - "Y" => true, - "N" => false, - ":mode/editor" => state.project.editor.is_some(), - ":focused/dialog" => !matches!(state.dialog, Dialog::None), - ":focused/message" => matches!(state.dialog, Dialog::Message(..)), - ":focused/add_device" => matches!(state.dialog, Dialog::Device(..)), - ":focused/browser" => state.dialog.browser().is_some(), - ":focused/pool/import" => matches!(state.pool.mode, Some(PoolMode::Import(..))), - ":focused/pool/export" => matches!(state.pool.mode, Some(PoolMode::Export(..))), - ":focused/pool/rename" => matches!(state.pool.mode, Some(PoolMode::Rename(..))), - ":focused/pool/length" => matches!(state.pool.mode, Some(PoolMode::Length(..))), - ":focused/clip" => !state.editor_focused() && matches!(state.selection(), Selection::TrackClip{..}), - ":focused/track" => !state.editor_focused() && matches!(state.selection(), Selection::Track(..)), - ":focused/scene" => !state.editor_focused() && matches!(state.selection(), Selection::Scene(..)), - ":focused/mix" => !state.editor_focused() && matches!(state.selection(), Selection::Mix), - _ => return Err(format!("not bool: {word}").into()) - })).transpose() - } - #[allow(unused)] - fn get_selection <'a> (state: &App, src: impl Language) -> Perhaps { - src.word()?.map(|word|Ok(match word { - ":select/scene" => state.selection().select_scene(state.tracks().len()), - ":select/scene/next" => state.selection().select_scene_next(state.scenes().len()), - ":select/scene/prev" => state.selection().select_scene_prev(), - ":select/track" => state.selection().select_track(state.tracks().len()), - ":select/track/next" => state.selection().select_track_next(state.tracks().len()), - ":select/track/prev" => state.selection().select_track_prev(), - _ => return Err(format!("not selection: {word}").into()) - })).transpose() - } - fn get_color <'a> (_: &App, src: impl Language) -> Perhaps { - if let Some(expr) = src.expr()? { - match (expr.head()?, expr.tail()?) { - (Some("g"), Some(tail)) => { - 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(tail.head().map_err(Into::into))? - .ok_or(LanguageError::domain("not red"))?; - let g = try_to_u8(tail.tail().head().map_err(Into::into))? - .ok_or(LanguageError::domain("not green"))?; - 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))) - }, - (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()) - } - } - #[allow(unused)] - fn get_opt_u7 <'a> (state: &App, src: impl Language) -> Perhaps> { - src.word()?.map(|word|Ok(match word { - ":editor/pitch" => Some(( - state.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_usize <'a> (state: &App, src: impl Language) -> Perhaps> { - src.word()?.map(|word|Ok(match word { - ":selected/scene" => state.selection().scene(), - ":selected/track" => state.selection().track(), - _ => return Err(format!("unknown opt: {word}").into()) - })).transpose() - } - #[allow(unused)] - fn get_clip <'a> (state: &App, src: impl Language) -> Perhaps>>> { - src.word()?.map(|word|Ok(match word { - ":selected/clip" if let Selection::TrackClip { track, scene } = state.selection() => - state.scenes()[*scene].clips[*track].clone(), - _ => return Err(format!("not a clip: {word}").into()) - })).transpose() - } - fn get_axis <'a> (_: &App, src: impl Language) -> Perhaps { - Ok(src.word()?.map(|word|Ok(match word { - ":x" => ControlAxis::X, - ":y" => ControlAxis::Y, - _ => return Err(format!("unknown axis {word}")) - })).transpose()?) - } - fn get_opt_u16 <'a> (_: &App, _src: impl Language) -> Perhaps> { - Ok(None) - } - fn get_opt_itemtheme <'a> (_: &App, _: impl Language) -> Perhaps> { - todo!() - } - fn get_opt_vec_opt_arc_rwlock_midiclip <'a> (_: &App, _: impl Language) -> Perhaps>>>>> { - todo!() - } - fn get_opt_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps>> { - todo!() - } - fn get_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps> { - todo!() - } - fn get_opt_arc_str <'a> (_: &App, _: impl Language) -> Perhaps>> { - todo!() - } - fn get_opt_bool <'a> (_: &App, _: impl Language) -> Perhaps> { - todo!() - } - fn get_path_buf <'a> (_: &App, _: impl Language) -> Perhaps { - todo!() - } -} - -pub use self::bind::*; -mod bind { - use crate::*; - - tui_keys!(self: App, input { - #[cfg(feature = "prof2")] profiling::scope!("App::tui_keys!"); - let name = self.mode.as_ref(); - let mode = name.and_then(|m|self.config.get_mode(m)).as_ref().map(Arc::clone); - if let Some(mode) = mode { - let binds = self.config.binds.clone(); - for id in mode.keys.iter() { - if let Some(event_map) = binds.try_read().unwrap().get(id.as_ref()) - && let Some(bindings) = event_map.query(input) { - for binding in bindings { - for command in binding.commands.iter() { - if Self::command::(self, command)? { - break - } - } - } - } - } - } - Ok(()) - }); - - impl App { - fn command < - T: Dispatch + Clone + Into - > (state: &mut Self, src: &str) -> Usually where - Self: Namespace, - { - if let Some(command) = Namespace::::namespace(state, &src)? { - match command.clone().dispatch(state) { - Err(err) => { - state.history.push((command.into(), None)); - return Err(err) - }, - Ok(undo) => { - state.history.push((command.into(), undo.map(Into::into))); - return Ok(true) - } - }; - } - Ok(false) - } - } - - impl_from!(AppCommand: |x: AxisCommand| AppCommand::Axis { command: x }); - impl_from!(AppCommand: |x: BrowseCommand| AppCommand::Browse { command: x }); - impl_from!(AppCommand: |x: DialogCommand| AppCommand::Dialog { command: x }); - impl_from!(AppCommand: |x: MidiClipCommand| AppCommand::MidiClip { command: x }); - impl_from!(AppCommand: |x: PoolCommand| AppCommand::Pool { command: x }); - impl_from!(AppCommand: |x: SceneCommand| AppCommand::Scene { command: x }); - impl_from!(AppCommand: |x: ScenesCommand| AppCommand::Scenes { command: x }); - impl_from!(AppCommand: |x: TrackCommand| AppCommand::Track { command: x }); - impl_from!(AppCommand: |x: TracksCommand| AppCommand::Tracks { command: x }); - #[tek_proc::commands(AppCommand)] - impl App { - #[command(Axis = "axis")] - pub fn command_axis (&mut self, command: AxisCommand) -> Perhaps { - Ok(command.dispatch(self)?.map(Into::into)) - } - #[command(Browse = "browse")] - pub fn command_browse (&mut self, command: BrowseCommand) -> Perhaps { - Ok(command.dispatch(self)?.map(Into::into)) - } - #[command(Dialog = "dialog")] - pub fn command_dialog (&mut self, command: DialogCommand) -> Perhaps { - Ok(command.dispatch(self)?.map(Into::into)) - } - #[command(MidiClip = "clip")] - pub fn command_midi_clip (&mut self, command: MidiClipCommand) -> Perhaps { - Ok(command.dispatch(self)?.map(Into::into)) - } - #[command(Pool = "pool")] - pub fn command_pool (&mut self, command: PoolCommand) -> Perhaps { - Ok(command.dispatch(self)?.map(Into::into)) - } - #[command(Scene = "scene")] - pub fn command_scene (&mut self, command: SceneCommand) -> Perhaps { - Ok(command.dispatch(self)?.map(Into::into)) - } - #[command(Scenes = "scenes")] - pub fn command_scenes (&mut self, command: ScenesCommand) -> Perhaps { - Ok(command.dispatch(self)?.map(Into::into)) - } - #[command(Track = "track")] - pub fn command_track (&mut self, command: TrackCommand) -> Perhaps { - Ok(command.dispatch(self)?.map(Into::into)) - } - #[command(Tracks = "tracks")] - pub fn command_tracks (&mut self, command: TracksCommand) -> Perhaps { - Ok(command.dispatch(self)?.map(Into::into)) - } - } - - #[tek_proc::commands(DialogCommand)] - impl App { - /// Cancel current dialog - #[command(Cancel = "cancel")] - pub fn cancel (&mut self) -> Perhaps { - todo!() - } - /// Confirm current dialog selection. - #[command(Confirm = "confirm")] - 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!(), - }) - } - /// Swap currently active modal dialog. - /// - /// ``` - /// let _ = tek::App::default().set_dialog(tek::Dialog::welcome()); - /// ``` - #[command(SetDialog = "seg")] - pub fn set_dialog (&mut self, dialog: Dialog) -> Perhaps { - let mut dialog = dialog.clone(); - std::mem::swap(&mut self.dialog, &mut dialog); - Ok(Some(DialogCommand::SetDialog { dialog })) - } - } - - #[tek_proc::commands(AxisCommand)] - impl App { - /// Increment a given data axis. - #[command(Inc = "inc")] - pub fn inc (&mut self, axis: ControlAxis) -> Perhaps { - Ok(match (&self.dialog, axis) { - (Dialog::None, _) => todo!(), - (Dialog::Menu(_, _), ControlAxis::Y) => { - DialogCommand::SetDialog { dialog: self.dialog.menu_next() }.dispatch(self)?; - None - }, - _ => todo!() - }) - } - /// Decrement a given data axis. - #[command(Dec = "dec")] - pub fn dec (&mut self, axis: ControlAxis) -> Perhaps { - Ok(match (&self.dialog, axis) { - (Dialog::None, _) => None, - (Dialog::Menu(_, _), ControlAxis::Y) => { - DialogCommand::SetDialog { dialog: self.dialog.menu_prev() }.dispatch(self)?; - None - }, - _ => todo!() - }) - } - } - - /// 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())); -} - -pub use self::device::*; -mod device { - use crate::*; - - impl App { - /// 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!(), - } - } - } - - 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.tracks_jack_process(client, scope); - state.perf.update_from_jack_scope(t0, scope); - result - } - - fn tek_jack_event (state: &mut App, event: JackEvent) { - use JackEvent::*; - match event { - SampleRate(sr) => { state.clock().timebase.sr.set(sr as f64); }, - PortRegistration(_id, true) => { - //let port = self.jack().port_by_id(id); - //println!("\rport add: {id} {port:?}"); - //println!("\rport add: {id}"); - }, - PortRegistration(_id, false) => { - /*println!("\rport del: {id}")*/ - }, - PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ }, - PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ }, - ClientRegistration(_id, true) => {}, - ClientRegistration(_id, false) => {}, - ThreadInit => {}, - XRun => {}, - GraphReorder => {}, - _ => { panic!("{event:?}"); } - } - } - - #[tek_proc::command(Device)] - #[tek_proc::keyword(Device)] - #[derive(Debug)] - pub enum DeviceCommand {} - - 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 fn view_device <'a> (state: &'a App) -> impl Draw { - let selected = state.dialog.device_kind().unwrap(); - south( - bold(true, "Add device"), - iter_south(move||device_kinds().iter().enumerate().map(move|(i, _label)|{ - 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() - }))) - } - - pub mod arrange; pub use self::arrange::*; - pub mod browse; pub use self::browse::*; - pub mod pool; pub use self::pool::*; - pub mod clock; pub use self::clock::*; - pub mod dialog; pub use self::dialog::*; - pub mod editor; pub use self::editor::*; - 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::*; -} - -pub use self::draw::*; -mod draw { - use crate::*; - - /// The [Draw] implementation for [App] handles the loaded view, - /// which is defined in terms of [dizzle] DSL. - /// - /// If there is an error, the error is displayed. FIXME: overlay it - /// Then, every top-level form of the DSL description is rendered. - impl Draw for App { - fn draw (&self, to: &mut Tui) -> Drawn { - #[cfg(feature = "prof2")] profiling::scope!("App::draw"); - //self.perf.cycle(&mut |_|{ - self.draw_error(to)?; - self.draw_modes(to)?; - self.draw_debug(to)?; - #[cfg(feature = "prof2")] profiling::finish_frame!(); - Ok(Some(to.area().into())) - //}) - } - } - - impl App { - fn draw_error (&self, to: &mut Tui) -> Usually<()> { - if let Some(e) = self.error.try_read().unwrap().as_ref() { - e.as_ref().align_c().draw(to)?; - } - Ok(()) - } - - fn draw_modes (&self, to: &mut Tui) -> Usually<()> { - if let Some(mode) = self.mode.as_ref().and_then(|m|self.config.get_mode(m)) { - let mut error = false; - for (index, view) in mode.view.iter().enumerate() { - match (view.render)(self, to) { - Ok(None) => {}, - Ok(Some(XYWH(.., w, h))) => { - self.size.0.store(w as usize, Relaxed); - self.size.1.store(h as usize, Relaxed); - }, - Err(e) => { - let message = format!( - "Mode: {:?}\n\nLayer: #{index}\n\nError: {e}\n\nSource:\n{}", - &mode.name, - &view.source - ); - *self.error.try_write().unwrap() = Some(message.into()); - error = true; - break; - } - } - } - if !error { - *self.error.try_write().unwrap() = None; - } - } - Ok(()) - } - - #[allow(unused)] fn draw_debug (&self, to: &mut Tui) -> Drawn { - east( - format!("{}x{} ", - self.size.0.load(Relaxed), - self.size.1.load(Relaxed)), - format!("{}/{} {} ", - self.perf.used.load(Relaxed), - self.perf.window.load(Relaxed), - self.perf.clock.raw() / 10000000), - ).align_se().draw(to) - } - } - - impl_has!(Sizer: |self: App|self.size); - - 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 view_templates <'a> (state: &'a App) -> impl Draw + use<'a> { - let height = (state.config.modes.len() * 2) as u16; - draw(move |to: &mut Tui|{ - let mut index = 0; - state.config.modes.for_each(&mut |id: &str, profile: &Mode| { - 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) - } - - pub fn field_h <'a, T: Screen> ( - _theme: ItemTheme, _head: impl Draw, _body: impl Draw - ) -> impl Draw { - } - - pub fn field_v <'a, T: Screen> ( - _theme: ItemTheme, _head: impl Draw, _body: impl Draw - ) -> impl Draw { - } - - pub fn view_sessions <'a> () -> impl Draw { - let h = 6; - let w = Some(30); - let f = Rgb(224, 192, 128); - draw(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) - } - - /// ``` - /// 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 <'a> (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 - { - 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_track_header <'a> (theme: ItemTheme, content: impl Draw) - -> impl Draw - { - bg(theme.darker.term, content.align_e().full_w()).exact_w(12) - } - - /// ``` - /// let _ = tek::button_2("", "", true); - /// let _ = tek::button_2("", "", false); - /// ``` - pub fn button_2 <'a> (key: impl Draw, label: impl Draw, hide: bool) - -> impl Draw - { - let c1 = tui_orange(); - let c2 = tui_g(0); - let c3 = tui_g(96); - let c4 = tui_g(255); - bold(true, fg_bg(c1, c2, - bar(Split::East, 1u16, fg(c2, "▐"), - bar(Split::East, 1u16, key, - bar(Split::East, 1u16, fg(c3, "▐"), - when(!hide, fg_bg(c4, c3, label))))))) - } - - /// ``` - /// let _ = tek::button_3("", "", "", true); - /// let _ = tek::button_3("", "", "", false); - /// ``` - pub fn button_3 <'a> ( - key: impl Draw, - label: impl Draw, - value: impl Draw, - editing: bool, - ) -> impl Draw { - let c1 = tui_orange(); - let c2 = tui_g(0); - let c3 = tui_g(96); - let c4 = tui_g(255); - let c5 = tui_g(128); - bold(true, bar(Split::East, 3u16, - fg_bg(c1, c2, - bar(Split::East, 1u16, fg(c2, "▐"), - bar(Split::East, 1u16, key, fg(if editing { c5 } else { c3 }, "▐")))), - east( - when(!editing, east(fg_bg(c4, c3, label), fg_bg(c5, c3, "▐"),)), - east(fg_bg(tui_g(224), c5, value), fg_bg(c5, Reset, "▌"), )))) - } - +#[cfg(not(feature = "cli"))] +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(None, Project::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()), + ), config, mode)) + })?) } diff --git a/src/device/clock/ticker.rs b/src/ticker.rs similarity index 100% rename from src/device/clock/ticker.rs rename to src/ticker.rs diff --git a/src/device/clock/timebase.rs b/src/timebase.rs similarity index 100% rename from src/device/clock/timebase.rs rename to src/timebase.rs diff --git a/src/device/arrange/track.rs b/src/track.rs similarity index 95% rename from src/device/arrange/track.rs rename to src/track.rs index dc0ab38d..4b6c524b 100644 --- a/src/device/arrange/track.rs +++ b/src/track.rs @@ -1,4 +1,4 @@ -use crate::{*, arrange::*}; +use crate::*; /// A track consists of a sequencer and zero or more devices chained after it. /// @@ -155,7 +155,7 @@ pub(crate) fn track_width (_index: usize, track: &Track) -> u16 { } def_sizes_iter!(TracksSizes => Track); -impl_has!(Vec: |self: Arrangement| self.tracks); +impl_has!(Vec: |self: Project| self.tracks); impl_as_ref!(Vec: |self: App| self.project.as_ref()); impl_as_mut!(Vec: |self: App| self.project.as_mut()); @@ -626,7 +626,7 @@ pub trait HasTrackScroll { fn tracks_last_mut (&mut self) -> &mut usize; } -impl HasTrackScroll for &mut Arrangement { +impl HasTrackScroll for &mut Project { fn track_scroll (&self) -> usize { self.track_scroll } @@ -638,7 +638,7 @@ impl HasTrackScroll for &mut Arrangement { } } -impl HasTrackScroll for Arrangement { +impl HasTrackScroll for Project { fn track_scroll (&self) -> usize { self.track_scroll } @@ -680,3 +680,30 @@ fn view_track_row_section <'a> ( ) ) } + +pub fn view_track_header <'a> (theme: ItemTheme, content: impl Draw) + -> impl Draw +{ + bg(theme.darker.term, content.align_e().full_w()).exact_w(12) +} + + //pub fn per_track <'a, T: Draw<'_, Tui> + 'a, U: TracksSizes<'a>> ( + //tracks: impl Fn() -> U + Send + Sync + 'a, + //callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a + //) -> impl Draw<'_, Tui> + 'a { + //per_track_top(tracks, move|index, track|callback(index, track).full_h().align_y()) + //} + + //pub fn per_track_top <'a, T: Draw<'_, Tui> + 'a, U: TracksSizes<'a>> ( + //tracks: impl Fn() -> U + Send + Sync + 'a, + //callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a + //) -> impl Draw<'_, Tui> + 'a { + //bg(Reset, iter_east(||tracks() + //.map(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()) + //} diff --git a/src/views.rs b/src/views.rs new file mode 100644 index 00000000..8a64f218 --- /dev/null +++ b/src/views.rs @@ -0,0 +1,304 @@ +use crate::*; + +/// Collection of custom view definitions. +pub type Views = Arc, Arc>>>>; + +/// Custom view definition is a boxed closure emitting a [Draw]able from state `S`. +pub struct View { + pub source: Arc, + pub render: ArcDrawn + Send + Sync>> +} + +impl_debug!( View |self, w| { write!(w, "View({})", self.source) }); + +impl_display!( View |self, w| { write!(w, "View({})", self.source) }); + +impl View { + + pub fn new (source: impl AsRef) -> Usually { + Ok(Self { + source: source.as_ref().into(), + render: Self::compile(source)? + }) + } + + fn boxed Drawn + Send + Sync + 'static> (f: F) + -> BoxDrawn + Send + Sync + 'static> + { + Box::new(f) + } + + fn compile (source: impl AsRef) -> + UsuallyDrawn + Send + Sync>>> + { + let source = source.as_ref(); + let layer = if let Ok(Some(expr)) = source.expr() { + Self::compile_expr(expr.into())? + } else if let Ok(Some(word)) = source.word() { + Self::compile_word(word.into())? + } else { + return Err(format!("not word/expr:\n{source:?}").into()) + }; + Ok(Arc::new(Box::new(move|state, screen|layer(state, screen)))) + } + + fn compile_expr (expr: Arc) -> + UsuallyDrawn + Send + Sync>>> + { + + macro_rules! arg { + ($src:expr, $head:expr, $index:expr, $name:expr) => { + $src.nth($index)? + .ok_or_else(||Box::::from(format!( + "{}: no arg #{} ({}) in {}", $head, $index, $name, $src + )))? + }; + } + + Ok(Arc::new(if let Some(head) = expr.head()? && let Some(ns) = head.split('/').next() { + + match ns { + + "when" => { + let cond = Arc::from(arg!(expr, head, 1, "condition")); + let cond = move|state: &App|state.namespace(&cond)?.ok_or_else(||Box::::from("when: no condition value")); + let thunk = Self::compile(arg!(expr, head, 2, "content"))?; + Self::boxed(move|state, screen|{ + when( + cond(state)?, + draw(|screen|thunk(state, screen)) + ).draw(screen) + }) + }, + + "either" => { + let cond = Arc::from(arg!(expr, head, 1, "condition")); + let cond = move|state: &App|state.namespace(&cond)?.ok_or_else(||Box::::from("either: no condition value")); + let a = Self::compile(arg!(expr, head, 2, "content A"))?; + let b = Self::compile(arg!(expr, head, 3, "content B"))?; + Self::boxed(move|state, screen|{ + either( + cond(state)?, + draw(|screen|a(state, screen)), + draw(|screen|b(state, screen)), + ).draw(screen) + }) + }, + + "bsp" | "split" | "stack" => { + let split = head.split('/').skip(1).next(); + let split = match split { + Some("n") => Split::North, + Some("s") => Split::South, + Some("e") => Split::East, + Some("w") => Split::West, + Some("a") => Split::Above, + Some("b") => Split::Below, + _ => return Err(format!("invalid split: {split:?}").into()) + }; + let a = Self::compile(arg!(expr, head, 1, "content A"))?; + let b = Self::compile(arg!(expr, head, 2, "content B"))?; + Self::boxed(move|state, screen|split.stack( + draw(|screen|a(state, screen)), + draw(|screen|b(state, screen)), + ).draw(screen)) + }, + + "bar" => { + let split = head.split('/').skip(1).next(); + let split = match split { + Some("n") => Split::North, + Some("s") => Split::South, + Some("e") => Split::East, + Some("w") => Split::West, + Some("a") => Split::Above, + Some("b") => Split::Below, + _ => return Err(format!("invalid split: {split:?}").into()) + }; + let size = Arc::from(arg!(expr, head, 1, "size")); + let size = move|state: &App|state.namespace(&size)?.ok_or_else(||Box::::from("bar: no size")); + let a = Self::compile(arg!(expr, head, 2, "content A"))?; + let b = Self::compile(arg!(expr, head, 3, "content B"))?; + Self::boxed(move|state, screen|split.bar( + size(state)?, + draw(|screen|a(state, screen)), + draw(|screen|b(state, screen)), + ).draw(screen)) + }, + + "align" => { + let azimuth = head.split('/').skip(1).next(); + let azimuth = match azimuth { + Some("n") => Azimuth::N, + Some("s") => Azimuth::S, + Some("e") => Azimuth::E, + Some("w") => Azimuth::W, + Some("ne") => Azimuth::NE, + Some("se") => Azimuth::SE, + Some("nw") => Azimuth::NW, + Some("sw") => Azimuth::SW, + Some("c") => Azimuth::C, + Some("x") => Azimuth::X, + Some("y") => Azimuth::Y, + _ => return Err(format!("invalid azimuth: {azimuth:?}").into()) + }; + let thunk = Self::compile(arg!(expr, head, 1, "content"))?; + Self::boxed(move|state, screen|{ + Align( + Some(azimuth), + draw(|screen|thunk(state, screen)) + ).draw(screen) + }) + }, + + "full" | "fill" => { + let thunk = Self::compile(arg!(expr, head, 1, "content"))?; + match head.split('/').skip(1).next() { + Some("w") | Some("x") => Self::boxed(move|state, screen|{ + Full::W(draw(|screen|thunk(state, screen))).draw(screen) + }), + Some("h") | Some("y") => Self::boxed(move|state, screen|{ + Full::H(draw(|screen|thunk(state, screen))).draw(screen) + }), + Some("wh") | Some("xy") | None => Self::boxed(move|state, screen|{ + Full::WH(draw(|screen|thunk(state, screen))).draw(screen) + }), + _ => unreachable!() + } + }, + + "exact" | "min" | "max" | "push" | "pull" | "pad" => { + match head.split('/').skip(1).next() { + Some("w") | Some("x") => { + let value = Arc::from(arg!(expr, head, 1, "value")); + let value = move|state: &App|state.namespace(&value); + let thunk = Self::compile(arg!(expr, head, 2, "content"))?; + match ns { + "exact" => Self::boxed(move|state, screen|Exact::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), + "push" => Self::boxed(move|state, screen|Push::X(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), + "pull" => Self::boxed(move|state, screen|Pull::X(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), + "pad" => Self::boxed(move|state, screen|Pad::X(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), + "min" => Self::boxed(move|state, screen|Min::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), + "max" => Self::boxed(move|state, screen|Max::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), + _ => unreachable!() + } + }, + Some("h") | Some("y") => { + let value = Arc::from(arg!(expr, head, 1, "value")); + let value = move|state: &App|state.namespace(&value); + let thunk = Self::compile(arg!(expr, head, 2, "content"))?; + match ns { + "exact" => Self::boxed(move|state, screen|Exact::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), + "push" => Self::boxed(move|state, screen|Push::Y(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), + "pull" => Self::boxed(move|state, screen|Pull::Y(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), + "pad" => Self::boxed(move|state, screen|Pad::Y(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), + "min" => Self::boxed(move|state, screen|Min::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), + "max" => Self::boxed(move|state, screen|Max::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), + _ => unreachable!() + } + }, + Some("wh") | Some("xy") | None => { + let value1 = Arc::from(arg!(expr, head, 1, "value")); + let value1 = move|state: &App|state.namespace(&value1); + let value2 = Arc::from(arg!(expr, head, 2, "value")); + let value2 = move|state: &App|state.namespace(&value2); + let thunk = Self::compile(arg!(expr, head, 3, "content"))?; + match ns { + "exact" => Self::boxed(move|state, screen|Exact::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)), + "push" => Self::boxed(move|state, screen|Push::XY(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)), + "pull" => Self::boxed(move|state, screen|Pull::XY(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)), + "pad" => Self::boxed(move|state, screen|Pad::XY(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)), + "min" => Self::boxed(move|state, screen|Min::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)), + "max" => Self::boxed(move|state, screen|Max::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)), + _ => unreachable!() + } + }, + _ => unreachable!() + } + }, + + "fg" | "bg" => { + let color = Arc::from(arg!(expr, head, 1, "color")); + let color = move|state: &App|state.namespace(&color); + let thunk = Self::compile(arg!(expr, head, 2, "content"))?; + match ns { + "fg" => Self::boxed(move|state, screen|fg( + color(state)?.unwrap_or_default(), draw(|screen|thunk(state, screen)) + ).draw(screen)), + "bg" => Self::boxed(move|state, screen|bg( + color(state)?.unwrap_or_default(), draw(|screen|thunk(state, screen)) + ).draw(screen)), + _ => unreachable!() + } + }, + + "text" => { + let text: Arc = Arc::from(expr.tail()?.unwrap_or_default()); + Self::boxed(move|_state, screen|{ + text.draw(screen) + }) + }, + + _ => return Err(format!("compile_expr: unexpected: {expr:?}").into()) + + } + + } else { + return Err(format!("compile_expr: invalid expression: {expr:?}").into()) + })) + } + + fn compile_word (word: Arc) + -> UsuallyDrawn + Send + Sync>>> + { + macro_rules! draw { + (|$state:ident|$body:expr)=>{Self::boxed(move|$state, to|$body.draw(to))} + } + + Ok(Arc::new(match word.split("/").next() { + //Some(":logo") => view_logo().draw(to), + Some(":meters") => match word.split("/").skip(1).next() { + Some("input") => Self::boxed(move|_, to|bg(Rgb(30, 30, 30), "Input Meters".align_s().full_h()).draw(to)), + Some("output") => Self::boxed(move|_, to|bg(Rgb(30, 30, 30), "Output Meters".align_s().full_h()).draw(to)), + _ => panic!() + }, + Some(":tracks") => match word.split("/").skip(1).next() { + None => Self::boxed(move|_, to|"TODO tracks".draw(to)), + Some("names") => draw!(|app|app.project.view_track_names(app.color.clone())), + Some("inputs") => draw!(|app|app.project.view_track_inputs(app.color.clone())), + Some("devices") => draw!(|app|app.project.view_track_devices(app.color.clone())), + Some("outputs") => draw!(|app|app.project.view_track_outputs(app.color.clone(), 0)), + _ => panic!() + }, + Some(":scenes") => match word.split("/").skip(1).next() { + Some("clips") => draw!(|app|app.view_scenes_clips()), + Some("names") => draw!(|app|app.view_scenes_names()), + _ => panic!() + }, + Some(":templates") => draw!(|app|view_templates(app)), + Some(":browse/title") => draw!(|app|view_browse_title(app)), + Some(":device") => draw!(|app|view_device(app)), + Some(":dialog") => Self::boxed(move|state, to|draw_dialog(to, word.split("/").skip(1), state)), + Some(":sessions") => Self::boxed(move|_, to|view_sessions().draw(to)), + Some(":status") => Self::boxed(move|state, to|draw_status_bar(to, state)), + Some(":editor") => Self::boxed(move|_, to|"TODO Editor".draw(to)), + Some(":transport") => Self::boxed(move|_, to|view_transport(true, "", "", "").draw(to)), + Some(":debug") => Self::boxed(move|_, to|format!("[{:?}]", to.area()).exact_h(1).draw(to)), + Some(_) => Self::boxed(move|state, to|if let Some(view) = state.config.get_view(word.as_ref()) { + (view.render)(state, to) + } else { + fg(Color::Rgb(128, 32, 32), format!("undefined: {word:?}")).draw(to) + }), + _ => unreachable!() + })) + } +} + +/// Load custom view definition. +pub fn views_add <'a> (views: &Views, expr: impl Language) -> UsuallyRef<'a, ()> { + views.try_write().unwrap().insert( + expr.head()?.ok_or("view: missing name")?.into(), + View::new(expr.tail()?.ok_or("view: missing body")?)?.into() + ); + Ok(()) +} diff --git a/src/vst2.rs b/src/vst2.rs new file mode 100644 index 00000000..44643faa --- /dev/null +++ b/src/vst2.rs @@ -0,0 +1,15 @@ +use crate::*; + +impl ::vst::host::Host for Plugin {} + +fn set_vst_plugin ( + host: &Arc>>, _path: &str +) -> Usually { + let mut loader = ::vst::host::PluginLoader::load( + &std::path::Path::new("/nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lxvst/helm.so"), + host.clone() + )?; + Ok(PluginKind::VST2 { + instance: loader.instance()? + }) +} diff --git a/tengri b/tengri index 5272b8ec..87d1c0a8 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit 5272b8ec55e48d135c88cbc840eab530afa1ae73 +Subproject commit 87d1c0a8111fddb1f2acf64e7ad72c353c363540