From 02dc7bc9f879054ae6978580f341f854361bc15d Mon Sep 17 00:00:00 2001 From: i do not exist Date: Mon, 5 Oct 2026 02:26:01 +0300 Subject: [PATCH] more shuffling; testing dispatch --- src/.scratch.rs | 14 + src/conf/axis.rs | 39 ++ src/conf/bind.rs | 237 +-------- src/conf/lang.rs | 330 +++++++++++++ src/conf/mode.rs | 131 +---- src/conf/view.rs | 395 ++------------- src/gear/device.rs | 122 +++++ src/gear/mixer.rs | 202 -------- src/gear/sample.rs | 8 + src/gear/track.rs | 81 ++++ src/tek.rs | 1137 +++++++++++++++++++++++++++----------------- tengri | 2 +- 12 files changed, 1339 insertions(+), 1359 deletions(-) create mode 100644 src/conf/axis.rs create mode 100644 src/gear/device.rs create mode 100644 src/gear/track.rs diff --git a/src/.scratch.rs b/src/.scratch.rs index 5a61ab40..30650f90 100644 --- a/src/.scratch.rs +++ b/src/.scratch.rs @@ -1600,3 +1600,17 @@ //fn _clip_index_next (&self) -> usize { 0 } //fn _color_random (&self) -> ItemColor { ItemColor::random() } //} + + //pub fn load_axis_bind (name: &str, body: impl Language) -> Usually { + //let mut bind: Bind> = Bind::new(); + //body.each(0, |index, key: &str|if index == 0 { + //bind.add_def(&format!("({key} (axis (dec {name})))"))?; + //Ok(1) + //} else if index == 1 { + //bind.add_def(&format!("({key} (axis (inc {name})))"))?; + //Ok(2) + //} else { + //err!("top-level (axis) takes 2 keysyms: dec and inc") + //})?; + //Ok(bind) + //} diff --git a/src/conf/axis.rs b/src/conf/axis.rs new file mode 100644 index 00000000..4defe137 --- /dev/null +++ b/src/conf/axis.rs @@ -0,0 +1,39 @@ +use crate::*; + +/// ``` +/// use ::{std::sync::Arc, tek::*}; +/// let axis = Axis::>::eval("@up @down").unwrap(); +/// ``` +pub struct Axis(pub T, pub T); + +impl_debug!( Axis: |self, w| { write!(w, "Axis({:?} {:?})", self.0, self.1) }); + +impl From<&'a str>, U: AsRef, V: AsRef> From<(U, V)> for Axis { + fn from ((decr, incr): (U, V)) -> Self { + Self(decr.as_ref().into(), incr.as_ref().into()) + } +} + +impl From<&'a str>> Axis { + pub fn eval (body: impl Language) -> Usually { + let mut axis: (Option, Option) = Default::default(); + body.each(0, |index, item: &str|if index == 0 { + axis.0 = Some(item.into()); + Ok(1) + } else if index == 1 { + axis.1 = Some(item.into()); + Ok(2) + } else { + err!("def (axis X dec inc)") + })?; + + if let Some(dec) = axis.0 && let Some(inc) = axis.1 { + Ok(Self(dec, inc)) + } else { + err!("def (axis X dec inc)") + } + } +} + +/// Named control axis. +pub struct ControlAxis(pub Arc); diff --git a/src/conf/bind.rs b/src/conf/bind.rs index 45c48557..30d6d2f0 100644 --- a/src/conf/bind.rs +++ b/src/conf/bind.rs @@ -1,50 +1,5 @@ use crate::*; -/// Collection of input bindings. -/// -/// ``` -/// # use tek::*; -/// let state = Binds::default(); -/// state.binds_def_keys(stringify!(Global (@f1 help) (@f2 rename))).unwrap(); -/// state.binds_def_axis(stringify!(X @left @right)).unwrap(); -/// ``` -#[derive(Default, Clone, Debug)] -pub struct Binds { - pub keys: Arc, Arc>>>>>, - pub axis: Arc, Arc>>>>>, -} - -impl Binds { - pub fn binds_clear (&self) { - *self.keys.write() = Default::default(); - *self.axis.write() = Default::default(); - } - pub fn binds_find (&self, id: impl AsRef) -> Option>>> { - self.keys.read().get(id.as_ref()).cloned() - } - pub fn binds_for_each ( - &self, mut cb: impl FnMut(&Arc, &Bind>)->T - ) { - for (k, v) in self.keys.read().iter() { - cb(k, v); - } - } - pub fn binds_def_keys (&self, expr: impl Language) -> Usually<()> { - self.keys.write().insert( - expr.head()?.ok_or("bind: missing name")?.into(), - Bind::new(expr.tail()?.unwrap_or(""))?.into() - ); - Ok(()) - } - pub fn binds_def_axis (&self, expr: impl Language) -> Usually<()> { - self.axis.write().insert( - expr.head()?.ok_or("axis: missing name")?.into(), - Axis::new(expr.tail()?.unwrap_or(""))?.into() - ); - Ok(()) - } -} - /// A map of input events (e.g. [TuiEvent]) to [Binding]s. /// /// ``` @@ -98,7 +53,7 @@ impl Bind> { fn add_def (&mut self, item: &str) -> Usually<()> { let expr = item.expr()?; let head = expr.head()?; - if let Some(event) = TuiKey::from_dsl(&head)?.to_crossterm() { + if let Some(event) = TuiKey::eval(&head)?.to_crossterm() { let mut commands: Vec> = vec![]; item.expr()?.tail()?.each((), |_, def|Ok(commands.push(def.into())))?; self.add(TuiEvent(event), Binding { commands: commands.into(), ..Default::default() }); @@ -107,55 +62,11 @@ impl Bind> { // TODO Ok(()) } else { - err!("Binds::add: invalid key: {:?}", item.expr()?.head()?) + err!("Bind::add_def: invalid key: {:?}", item.expr()?.head()?) } } } -/// ``` -/// use ::{std::sync::Arc, tek::*}; -/// let axis = Axis::>::new("@up @down").unwrap(); -/// ``` -pub struct Axis(T, T); - -impl_debug!( Axis: |self, w| { write!(w, "Axis({:?} {:?})", self.0, self.1) }); - -impl From<&'a str>, U: AsRef, V: AsRef> From<(U, V)> for Axis { - fn from ((decr, incr): (U, V)) -> Self { - Self(decr.as_ref().into(), incr.as_ref().into()) - } -} - -impl From<&'a str>> Axis { - pub fn new (body: impl Language) -> Usually { - let mut axis: (T, T) = Default::default(); - body.each(0, |index, item: &str|if index == 0 { - axis.0 = item.into(); - Ok(1) - } else if index == 1 { - axis.0 = item.into(); - Ok(2) - } else { - err!("def (axis X dec inc)") - })?; - Ok(Self(axis.0, axis.1)) - } -} - - //pub fn load_axis_bind (name: &str, body: impl Language) -> Usually { - //let mut bind: Bind> = Bind::new(); - //body.each(0, |index, key: &str|if index == 0 { - //bind.add_def(&format!("({key} (axis (dec {name})))"))?; - //Ok(1) - //} else if index == 1 { - //bind.add_def(&format!("({key} (axis (inc {name})))"))?; - //Ok(2) - //} else { - //err!("top-level (axis) takes 2 keysyms: dec and inc") - //})?; - //Ok(bind) - //} - impl Bind { /// Add a binding to an owned event map. pub fn def (mut self, event: E, binding: Binding) -> Self { @@ -215,152 +126,8 @@ impl Default for Binding { #[derive(Clone)] pub struct Condition(pub Arcbool + Send + Sync>>); -/// Named control axis. -pub struct ControlAxis(pub Arc); - impl_debug!(Condition: |self, w| { write!(w, "*") }); -pub trait HasBinds { - fn binds_keys (&self) -> &RwLock, Arc>>>>; - fn binds_axis (&self) -> &RwLock, Arc>>>>; - - fn binds_clear (&self) { - *self.binds_keys().write() = Default::default(); - *self.binds_axis().write() = Default::default(); - } - fn binds_find (&self, id: impl AsRef) -> Option>>> { - self.binds_keys().read().get(id.as_ref()).cloned() - } - fn binds_for_each ( - &self, mut cb: impl FnMut(&Arc, &Bind>)->T - ) { - for (k, v) in self.binds_keys().read().iter() { - cb(k, v); - } - } - /// Define a top-level key binding. - /// - /// ``` - /// # use ::{std::sync::Arc, tek::*}; - /// let config: Config = Default::default(); - /// config.binds_def_keys(stringify!(Confirm (@enter confirm))).unwrap(); - /// ``` - fn binds_def_keys (&self, expr: impl Language) -> Usually<()> { - self.binds_keys().write().insert( - expr.head()?.ok_or("bind: missing name")?.into(), - Bind::new(expr.tail()?.unwrap_or(""))?.into() - ); - Ok(()) - } - /// Define an top-level axis binding (keypair). - /// - /// ``` - /// # use ::{std::sync::Arc, tek::*}; - /// let config: Config = Default::default(); - /// config.binds_def_axis(stringify!(Y @up @down)).unwrap(); - /// ``` - fn binds_def_axis (&self, expr: impl Language) -> Usually<()> { - self.binds_axis().write().insert( - expr.head()?.ok_or("axis: missing name")?.into(), - Axis::new(expr.tail()?.unwrap_or(""))?.into() - ); - Ok(()) - } -} - -impl HasBinds for App { - fn binds_keys (&self) -> &RwLock, Arc>>>> { - &self.config.config.binds.keys - } - fn binds_axis (&self) -> &RwLock, Arc>>>> { - &self.config.config.binds.axis - } -} - -impl HasBinds for ConfigWatcher { - fn binds_keys (&self) -> &RwLock, Arc>>>> { - &self.config.binds.keys - } - fn binds_axis (&self) -> &RwLock, Arc>>>> { - &self.config.binds.axis - } -} - -impl HasBinds for Config { - fn binds_keys (&self) -> &RwLock, Arc>>>> { - &self.binds.keys - } - fn binds_axis (&self) -> &RwLock, Arc>>>> { - &self.binds.axis - } -} - -pub trait DispatchBind: HasBinds + HasModeStack + HasModes + Namespace { - fn input_cmds (&mut self, input: &TuiEvent) -> Usually<()> { - if let Some(bindings) = self.input_cmds_find(input) { - return self.input_cmds_exec(bindings) - } - Ok(()) - } - /// ``` - /// # use ::{std::sync::Arc, tek::*}; - /// let app = config_reload(App::default(), stringify!( - /// (axis Y @up @down) - /// (mode Menu - /// (axis Y prev next) - /// (bind (@enter confirm))) - /// )).unwrap(); - /// panic!("{:?}\n{:?}", app.config.config.binds, app.config.config.modes); - /// assert!(app.input_cmds_find(&"left".into()).is_none()); - /// assert!(app.input_cmds_find(&"enter".into()).is_some()); - /// assert!(app.input_cmds_find(&"down".into()).is_some()); - /// assert!(app.input_cmds_find(&"up".into()).is_some()); - /// ``` - fn input_cmds_find (&self, input: &TuiEvent) -> Option>]>> { - for name in self.mode_stack().read().iter().rev() { - let mode = self.modes_get(name).as_ref().map(Arc::clone); - if let Some(mode) = mode { - for (ctrl, (dec, inc)) in mode.axis.iter() { - panic!("{ctrl:?} {dec:?} {inc:?}"); - } - for id in mode.keys.iter() { - if let Some(event_map) = self.binds_find(id) - && let Some(bindings) = event_map.query(input) { - return Some(bindings.into()) - } - } - } - } - None - } - fn input_cmds_exec (&mut self, bindings: Arc<[Binding>]>) -> Usually<()> { - for binding in bindings.iter() { - for src in binding.commands.iter() { - if let Some(command) = Namespace::::namespace(self, &src)? { - self.cmd_dispatch(command)?; - } - } - } - Ok(()) - } - fn cmd_dispatch (&mut self, command: C) -> Usually<()>; -} - -impl DispatchBind for App { - fn cmd_dispatch (&mut self, command: AppCmd) -> Usually<()> { - match command.clone().dispatch(self) { - Ok(undo) => { - self.history.push((command, undo)); - Ok(()) - }, - Err(err) => { - self.history.push((command, None)); - Err(err) - } - } - } -} - pub fn print_config_binds (config: &Config) { use ::ansi_term::Color::*; config.binds_for_each(|k, v|{ diff --git a/src/conf/lang.rs b/src/conf/lang.rs index 4ac13194..b214a984 100644 --- a/src/conf/lang.rs +++ b/src/conf/lang.rs @@ -43,6 +43,301 @@ pub const fn wrap_dec (index: usize, count: usize) -> usize { } } +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!("not word/expr:\n{source:?}") + }; + 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!("invalid split: {split:?}") + }; + 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!("invalid split: {split:?}") + }; + 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!("invalid azimuth: {azimuth:?}") + }; + 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" => { + 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" | "shrink" | "expand" => { + 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 { + "shrink" => Self::boxed(move|state, screen|Shrink::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), + "expand" => Self::boxed(move|state, screen|Expand::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), + "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 { + "shrink" => Self::boxed(move|state, screen|Shrink::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), + "expand" => Self::boxed(move|state, screen|Expand::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), + "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 { + "shrink" => Self::boxed(move|state, screen|Shrink::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)), + "expand" => Self::boxed(move|state, screen|Expand::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)), + "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!("compile_expr: unexpected: {expr:?}") + + } + + } else { + return err!("compile_expr: invalid expression: {expr:?}") + })) + } + + fn compile_word (word: Arc) + -> UsuallyDrawn + Send + Sync>>> + { + macro_rules! view { + (|$state:ident|$body:expr)=>{Box::new(move|$state, to|$body.draw(to))}; + ($const:expr)=>{Box::new(move|_, to|$const.draw(to))}; + ()=>{Box::new(move|_, __|Ok(None))}; + } + + Ok(Arc::new(match word.split("/").next() { + Some("Beat") => view!(), + Some("Bpm") => view!(), + Some("BrowseTitle") => view!(|app|view_browse_title(app)), + Some("Config") => view!(|app|view_config(app)), + Some("Debug") => Box::new(move|_, to|format!("[{:?}]", to.area()).exact_h(1).draw(to)), + Some("Device") => view!(|app|view_device(app)), + Some("Menu") => view!(|app|view_menu(app)), + Some("Editor") => view!(|app|app.editor()), + Some("MetersIn") => view!(|___|bg(Rgb(30, 30, 30), "Input Meters".align_s().full_h())), + Some("MetersOut") => view!(|___|bg(Rgb(30, 30, 30), "Output Meters".align_s().full_h())), + Some("ModeList") => view!(|app|view_modes(app)), + Some("HistoryCount") => view!(|app|app.history.len()), + Some("HistoryList") => view!(|app|view_history_list(app)), + Some("ScenesClips") => view!(|app|view_scenes_clips(app)), + Some("ScenesNames") => view!(|app|app.view_scenes_names()), + Some("Sessions") => view!(|___|view_sessions()), + Some("Selected") => view!(|app|view_selection(app)), + Some("Templates") => view!(|app|view_templates(app)), + Some("Time") => view!(), + Some("Tracks") => view!("TODO tracks"), + Some("TracksDevices") => view!(|app|app.project.view_track_devices(app.color.clone())), + Some("TracksDevicesAdd") => view!(|app|app.project.view_track_device_add()), + Some("TracksDevicesCells") => view!(|app|app.project.view_track_device_cells(app.tracks_devices_height())), + Some("TracksDevicesCount") => view!(|app|app.project.view_track_device_count()), + Some("TracksInputs") => view!(|app|app.project.view_track_inputs(app.color.clone())), + Some("TracksInputsAdd") => view!(|app|app.project.view_track_input_add()), + Some("TracksInputsCells") => view!(|app|app.project.view_track_input_cells(app.tracks_inputs_height())), + Some("TracksInputsCount") => view!(|app|app.project.view_track_input_count()), + Some("TracksNames") => view!(|app|app.project.view_track_names(app.color.clone())), + Some("TracksNamesAdd") => view!(|app|app.project.view_track_names_add()), + Some("TracksNamesCells") => view!(|app|app.project.view_track_names_cells()), + Some("TracksNamesCount") => view!(|app|app.project.view_track_names_count()), + Some("TracksOutputs") => view!(|app|app.project.view_track_outputs(app.color.clone(), 0)), + Some("TracksOutputsAdd") => view!(|app|app.project.view_track_output_add()), + Some("TracksOutputsCells") => view!(|app|app.project.view_track_output_cells()), + Some("TracksOutputsCount") => view!(|app|app.project.view_track_output_count()), + Some(_) => Box::new(move|state, to|if let Some(view) = state.views_get(word.as_ref()) { + (view.render)(state, to) + } else { + fg(Color::Rgb(128, 32, 32), format!("undefined: {word:?}")).draw(to) + }), + _ => unreachable!() + })) + } +} //pub fn eval_expr (src: impl Language, eval: impl Fn(&str)->Usually) -> Perhaps { //Ok(match expr.head()? { @@ -936,3 +1231,38 @@ impl ControlAxis { eval_word(src, |word|Ok(ControlAxis(word.into()))) } } + +impl Dialog { + pub fn eval <'a> (_: &App, src: impl Language) -> Perhaps { + eval_word(&src, |word|Ok(match word { + ":none" => Dialog::None, + ":options" => Dialog::Options, + ":device" => Dialog::Device(0), + ":device/prev" => Dialog::Device(0), + ":device/next" => Dialog::Device(0), + ":help" => Dialog::Help(0), + ":save" => Dialog::Browse( + BrowseTarget::SaveProject, + Browse::new(None).unwrap().into()), + ":load" => Dialog::Browse( + BrowseTarget::LoadProject, + Browse::new(None).unwrap().into()), + ":import/clip" => Dialog::Browse( + BrowseTarget::ImportClip(Default::default()), + Browse::new(None).unwrap().into()), + ":export/clip" => Dialog::Browse( + BrowseTarget::ExportClip(Default::default()), + Browse::new(None).unwrap().into()), + ":import/sample" => Dialog::Browse( + BrowseTarget::ImportSample(Default::default()), + Browse::new(None).unwrap().into()), + ":export/sample" => Dialog::Browse( + BrowseTarget::ExportSample(Default::default()), + Browse::new(None).unwrap().into()), + _ => return Err( + format!("not a dialog: {}", src.src()?.unwrap_or("")).into() + ) + })) + + } +} diff --git a/src/conf/mode.rs b/src/conf/mode.rs index 8e1737df..a6125915 100644 --- a/src/conf/mode.rs +++ b/src/conf/mode.rs @@ -2,112 +2,6 @@ use crate::*; impl_as_mut!(Mode: |self: Mode| self); -/// Has a stack of currently active interaction modes. -pub trait HasModeStack { - /// Stack of active modes. - fn mode_stack (&self) -> &RwLock>>; -} - -impl HasModeStack for App { - fn mode_stack (&self) -> &RwLock>> { - &self.modes - } -} - -pub trait HasModes { - fn modes (&self) -> &RwLock, Arc>>; - - /// Register a mode. - /// - /// ``` - /// # use ::{std::sync::Arc, tek::*}; - /// let config: Config = Default::default(); - /// - /// config.modes_add(stringify!(Menu - /// (name Menu) - /// (info Mode selector.) - /// (axis Y Prev Next) - /// (bind Confirm) - /// (view (bg (g 30) (bar/s 7 Logo (full/xy Dialog)))) - /// )).unwrap(); - /// - /// let _: Arc = config.modes_get("Menu").unwrap(); - /// ``` - fn modes_add <'a> (&self, expr: impl Language) -> UsuallyRef<'a, ()> { - let name = expr.head()?.ok_or("mode: missing name")?; - let body = expr.tail()?.ok_or("mode: missing body")?; - let mut mode = Mode::default(); - body.each(&mut mode, |c, s|c.add(s))?; - self.modes().write().insert(name.into(), Arc::new(mode)); - Ok(()) - } - /// Get a mode by name. - fn modes_get (&self, name: impl AsRef) -> Option> { - self.modes().read().get(name.as_ref()).cloned() - } - /// Run something for each mode. - fn modes_for_each (&self, mut ator: impl FnMut(&str, &Mode)->T) { - for (k, v) in self.modes().read().iter() { - let _ = ator(k.as_ref(), v.as_ref()); - } - } - /// Count modes. - fn modes_count (&self) -> usize { - self.modes().read().len() - } - /// Clear modes. - fn modes_clear (&self) { - *self.modes().write() = Default::default(); - } -} - -impl HasModes for App { - fn modes (&self) -> &RwLock, Arc>> { - &self.config.config.modes.0 - } -} - -impl HasModes for ConfigWatcher { - fn modes (&self) -> &RwLock, Arc>> { - &self.config.modes.0 - } -} - -impl HasModes for Config { - fn modes (&self) -> &RwLock, Arc>> { - &self.modes.0 - } -} - -impl HasModes for Modes { - fn modes (&self) -> &RwLock, Arc>> { - &self.0 - } -} - -impl HasModes for Mode { - fn modes (&self) -> &RwLock, Arc>> { - &self.modes.0 - } -} - -/// Collection of UI modes. -/// -/// ``` -/// # use tek::*; -/// // Empty mode registry: -/// let mut state = Modes::default(); -/// assert_eq!(state.modes_count(), 0); -/// assert!(state.modes_get("Foo").is_none()); -/// -/// // Add a mode: -/// let _ = state.modes_add(&"Foo (name Foo) (info Bar)").unwrap(); -/// assert_eq!(state.modes_count(), 1); -/// assert!(state.modes_get("Foo").is_some()); -/// ``` -#[derive(Default, Debug, Clone)] -pub struct Modes(pub Arc, Arc>>>); - /// Group of view and keys definitions. /// /// ``` @@ -120,12 +14,11 @@ pub struct Mode { pub info: Vec>, pub view: Vec>>, pub keys: Vec>, - pub axis: BTreeMap, (Arc, Arc)>, - pub modes: Modes, + pub axis: BTreeMap, Arc>, + pub modes: Arc, Arc>>>, } impl Mode { - /// Add a definition to the mode. /// /// Supported definitions: @@ -164,8 +57,7 @@ impl Mode { let body = tail.tail()?.ok_or("submode: missing body")?; let mut submode = Mode::default(); body.each(&mut submode, |c, s|c.add(s))?; - let modes = self.modes.clone(); - modes.0.write().insert(name.into(), Arc::new(submode)); + self.modes.write().insert(name.into(), Arc::new(submode)); Ok(self) } @@ -177,27 +69,22 @@ impl Mode { } fn add_axis (&mut self, tail: &str) -> Usually<&mut Self> { - let mut axis: Option> = None; - let mut dec: Option> = None; - let mut inc: Option> = None; + let mut axis: Option> = None; + let mut param: Option> = None; tail.each(0, |index: usize, src: &str|if index == 0 { axis = Some(src.into()); Ok(1) } else if index == 1 { - dec = Some(src.into()); + param = Some(src.into()); Ok(2) - } else if index == 2 { - inc = Some(src.into()); - Ok(3) } else { err!("expected (mode (axis [Name] [command] [command])), got: {tail}") })?; - if let Some(axis) = axis && let Some(dec) = dec && let Some(inc) = inc { - self.keys.push(format!("((dec {axis}) {dec})").into()); - self.keys.push(format!("((inc {axis}) {inc})").into()); + if let Some(axis) = axis && let Some(param) = param { + self.axis.insert(axis.into(), param.into()); Ok(self) } else { - err!("expected (mode (axis X Decrement Increment)), got: {tail}") + err!("mode: expected (axis X Param), got: {tail}") } } diff --git a/src/conf/view.rs b/src/conf/view.rs index 2c41d08f..0bbcbd20 100644 --- a/src/conf/view.rs +++ b/src/conf/view.rs @@ -19,12 +19,6 @@ impl_default!(ClockView: { } }); -/// Collection of custom view definitions. -#[derive(Default, Clone, Debug)] -pub struct Views( - pub Arc, Arc>>>> -); - /// Custom view definition is a boxed closure emitting a [Draw]able from state `S`. pub struct View { pub source: Arc, @@ -75,8 +69,31 @@ pub fn view_debug (perf: &PerfModel, size: &Sizer) -> impl Draw { ) } +/// ``` +/// let x = ""; +/// let _ = tek::view_status(None, x.as_ref(), x.as_ref(), x.as_ref()); +/// let _ = tek::view_status(Some("".into()), x.as_ref(), x.as_ref(), x.as_ref()); +/// ``` +pub fn view_status <'a> (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl Draw { + let theme = ItemTheme::G[96]; + let sr = field_h(theme, "SR", sr); + let buf = field_h(theme, "Buf", buf); + let lat = field_h(theme, "Lat", lat); + bg(Black, east!(above( + sel.map(|sel|field_h(theme, "Selected", sel)).align_w().full_wh(), + east!(sr, buf, lat).align_e().full_wh(), + ))) +} + +pub fn draw_status_bar (to: &mut Tui, state: &App) -> Drawn { + bar(Split::South, 1u16, + dim(true, "Mode"), + bold(true, state.mode_stack().read().get(0)) + ).push_x(1).draw(to) +} + pub fn draw_mode_stack (to: &mut Tui, state: &App) -> Usually<()> { - for name in state.modes.read().iter() { + for name in state.mode_stack().read().iter() { let mut error = false; if let Some(mode) = state.modes_get(name).as_ref().map(Arc::clone) { for (index, view) in mode.view.iter().enumerate() { @@ -107,303 +124,6 @@ pub fn draw_mode_stack (to: &mut Tui, state: &App) -> Usually<()> { Ok(()) } -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!("not word/expr:\n{source:?}") - }; - 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!("invalid split: {split:?}") - }; - 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!("invalid split: {split:?}") - }; - 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!("invalid azimuth: {azimuth:?}") - }; - 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" => { - 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" | "shrink" | "expand" => { - 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 { - "shrink" => Self::boxed(move|state, screen|Shrink::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), - "expand" => Self::boxed(move|state, screen|Expand::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), - "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 { - "shrink" => Self::boxed(move|state, screen|Shrink::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), - "expand" => Self::boxed(move|state, screen|Expand::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)), - "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 { - "shrink" => Self::boxed(move|state, screen|Shrink::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)), - "expand" => Self::boxed(move|state, screen|Expand::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)), - "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!("compile_expr: unexpected: {expr:?}") - - } - - } else { - return err!("compile_expr: invalid expression: {expr:?}") - })) - } - - fn compile_word (word: Arc) - -> UsuallyDrawn + Send + Sync>>> - { - macro_rules! view { - (|$state:ident|$body:expr)=>{Box::new(move|$state, to|$body.draw(to))}; - ($const:expr)=>{Box::new(move|_, to|$const.draw(to))}; - ()=>{Box::new(move|_, __|Ok(None))}; - } - - Ok(Arc::new(match word.split("/").next() { - Some("Beat") => view!(), - Some("Bpm") => view!(), - Some("BrowseTitle") => view!(|app|view_browse_title(app)), - Some("Config") => view!(|app|view_config(app)), - Some("Debug") => Box::new(move|_, to|format!("[{:?}]", to.area()).exact_h(1).draw(to)), - Some("Device") => view!(|app|view_device(app)), - Some("Menu") => view!(|app|view_menu(app)), - Some("Editor") => view!(|app|app.editor()), - Some("MetersIn") => view!(|___|bg(Rgb(30, 30, 30), "Input Meters".align_s().full_h())), - Some("MetersOut") => view!(|___|bg(Rgb(30, 30, 30), "Output Meters".align_s().full_h())), - Some("ModeList") => view!(|app|view_modes(app)), - Some("HistoryCount") => view!(|app|app.history.len()), - Some("HistoryList") => view!(|app|view_history_list(app)), - Some("ScenesClips") => view!(|app|view_scenes_clips(app)), - Some("ScenesNames") => view!(|app|app.view_scenes_names()), - Some("Sessions") => view!(|___|view_sessions()), - Some("Selected") => view!(|app|view_selection(app)), - Some("Templates") => view!(|app|view_templates(app)), - Some("Time") => view!(), - Some("Tracks") => view!("TODO tracks"), - Some("TracksDevices") => view!(|app|app.project.view_track_devices(app.color.clone())), - Some("TracksDevicesAdd") => view!(|app|app.project.view_track_device_add()), - Some("TracksDevicesCells") => view!(|app|app.project.view_track_device_cells(app.tracks_devices_height())), - Some("TracksDevicesCount") => view!(|app|app.project.view_track_device_count()), - Some("TracksInputs") => view!(|app|app.project.view_track_inputs(app.color.clone())), - Some("TracksInputsAdd") => view!(|app|app.project.view_track_input_add()), - Some("TracksInputsCells") => view!(|app|app.project.view_track_input_cells(app.tracks_inputs_height())), - Some("TracksInputsCount") => view!(|app|app.project.view_track_input_count()), - Some("TracksNames") => view!(|app|app.project.view_track_names(app.color.clone())), - Some("TracksNamesAdd") => view!(|app|app.project.view_track_names_add()), - Some("TracksNamesCells") => view!(|app|app.project.view_track_names_cells()), - Some("TracksNamesCount") => view!(|app|app.project.view_track_names_count()), - Some("TracksOutputs") => view!(|app|app.project.view_track_outputs(app.color.clone(), 0)), - Some("TracksOutputsAdd") => view!(|app|app.project.view_track_output_add()), - Some("TracksOutputsCells") => view!(|app|app.project.view_track_output_cells()), - Some("TracksOutputsCount") => view!(|app|app.project.view_track_output_count()), - Some(_) => Box::new(move|state, to|if let Some(view) = state.views_get(word.as_ref()) { - (view.render)(state, to) - } else { - fg(Color::Rgb(128, 32, 32), format!("undefined: {word:?}")).draw(to) - }), - _ => unreachable!() - })) - } -} - pub fn view_menu (app: &App) -> impl Draw { if let Dialog::Menu(selected, items) = &app.dialog { Some(iter_south(move||items.iter().enumerate().map(move|(index, MenuItem(item, _))|{ @@ -442,7 +162,7 @@ pub fn view_config (app: &App) -> impl Draw { let area = to.area(); let XYWH(x0, y0, w, h) = area; let mut y = 0; - for key in app.modes.read().iter().rev() { + for key in app.stack.read().iter().rev() { if let Some(mode) = app.modes_get(key) { to.clip(XYWH(x0 + 1, y0 + y, w, 1), &|to|Split::East.bar(16, key, @@ -933,7 +653,7 @@ fn iter_scenes_with_sizes <'s, T> (state: &'s T) -> impl ScenesSizes<'s> where T }) } -fn view_modes <'a, S: HasModeStack> (state: &'a S) -> impl Draw + 'a { +pub fn view_modes <'a, S: HasModeStack> (state: &'a S) -> impl Draw + 'a { draw(move|to: &mut Tui|{ let XYWH(x, y, w, h) = to.area(); let mut x_next = x; @@ -1081,29 +801,6 @@ pub fn draw_lv2_header (state: &Lv2, to: &mut Tui, x: u16, y: u16, w: u16) { //Ok(Rect { x, y, width: w, height: 1 }) } -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) -} - -/// ``` -/// let x = ""; -/// let _ = tek::view_status(None, x.as_ref(), x.as_ref(), x.as_ref()); -/// let _ = tek::view_status(Some("".into()), x.as_ref(), x.as_ref(), x.as_ref()); -/// ``` -pub fn view_status <'a> (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl Draw { - let theme = ItemTheme::G[96]; - let sr = field_h(theme, "SR", sr); - let buf = field_h(theme, "Buf", buf); - let lat = field_h(theme, "Lat", lat); - bg(Black, east!(above( - sel.map(|sel|field_h(theme, "Selected", sel)).align_w().full_wh(), - east!(sr, buf, lat).align_e().full_wh(), - ))) -} - pub fn view_device <'a> (state: &'a App) -> impl Draw { let selected = state.dialog.device_kind().unwrap(); south( @@ -1157,7 +854,7 @@ pub fn view_scene_name <'a> ( } /// Draw clips per scene -fn view_scenes_clips (state: &S) -> impl Draw { +pub fn view_scenes_clips (state: &S) -> impl Draw { let select = state.selection(); let editor = state.editor(); let editing = state.is_editing(); @@ -1281,10 +978,11 @@ pub fn view_selection <'a> (state: &'a App) -> impl Draw + use<'a> { } pub fn view_templates <'a> (state: &'a App) -> impl Draw + use<'a> { - let height = (state.config.modes_count() * 2) as u16; + let config: &Config = state.config(); + let height = (config.modes_count() * 2) as u16; draw(move |to: &mut Tui|{ let mut index = 0; - state.config.modes_for_each(&mut |id: &str, profile: &Mode| { + 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(""); @@ -1691,39 +1389,6 @@ impl ClockView { } } -pub trait HasViews { - fn views (&self) -> &RwLock, Arc>>>; - /// Load custom view definition. - fn views_add <'a> (&self, expr: impl Language) -> UsuallyRef<'a, ()> { - self.views().write().insert( - expr.head()?.ok_or("view: missing name")?.into(), - View::new(expr.tail()?.ok_or("view: missing body")?)?.into() - ); - Ok(()) - } - fn views_clear (&self) { - *self.views().write() = Default::default(); - } - fn views_get (&self, name: impl AsRef) -> Option>> { - self.views().read().get(name.as_ref()).cloned() - } - fn views_for_each (&self, cb: impl Fn(&Arc, &Arc>)->T) -> () { - for (k, v) in self.views().read().iter() { - cb(k, v); - } - } -} -impl HasViews for App { - fn views (&self) -> &RwLock, Arc>>> { - &self.config.config.views.0 - } -} -impl HasViews for Config { - fn views (&self) -> &RwLock, Arc>>> { - &self.views.0 - } -} - #[cfg(test)] mod test_view { use super::*; use proptest::prelude::*; diff --git a/src/gear/device.rs b/src/gear/device.rs new file mode 100644 index 00000000..50fe8020 --- /dev/null +++ b/src/gear/device.rs @@ -0,0 +1,122 @@ +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!(), + } + } +} + +#[tek_proc::command(Device)] +#[tek_proc::keyword(Device)] +#[derive(Debug)] +pub enum DeviceCmd {} + +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); + +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() + } +} diff --git a/src/gear/mixer.rs b/src/gear/mixer.rs index 86d0c75d..725785b9 100644 --- a/src/gear/mixer.rs +++ b/src/gear/mixer.rs @@ -107,205 +107,3 @@ pub fn to_rms (samples: &[f32]) -> f32 { .unwrap_or(0.0); (sum / samples.len() as f32).sqrt() } - -/// A track consists of a sequencer and zero or more devices chained after it. -/// -/// ``` -/// let track: tek::Track = Default::default(); -/// ``` -#[derive(Debug, Default)] -pub struct Track { - /// Name of track - pub name: Arc, - /// Identifying color of track - pub color: ItemTheme, - /// Preferred width of track column - pub width: usize, - /// MIDI sequencer state - pub sequencer: Sequencer, - /// Device chain - pub devices: Vec, -} - -impl Track { - /// Create a new track with only the default [Sequencer]. - pub fn new ( - name: &impl AsRef, - color: Option, - jack: &Jack<'static>, - clock: Option<&Clock>, - clip: Option<&Arc>>, - midi_from: &[Connect], - midi_to: &[Connect], - ) -> Usually { - Ok(Self { - name: name.as_ref().into(), - color: color.unwrap_or_default(), - sequencer: Sequencer::new( - format!("{}/sequencer", name.as_ref()), - jack, clock, clip, midi_from, midi_to - )?, - ..Default::default() - }) - } - pub fn new_added ( - jack: &Jack<'static>, - clock: Option<&Clock>, - name: &impl AsRef, - color: Option, - clip: Option<&Arc>>, - mins: Arc<[Connect]>, - mouts: Arc<[Connect]>, - ) -> Usually { - let name = name.as_ref(); - Ok(Self { - name: name.into(), - width: (name.len() + 2).max(12), - color: color.unwrap_or_else(ItemTheme::random), - sequencer: Sequencer::new(name, jack, clock, clip, mins.as_ref(), mouts.as_ref())?, - ..Default::default() - }) - } - pub fn audio_ins (&self) -> &[AudioInput] { - self.devices.first().map(|x|x.audio_ins()).unwrap_or_default() - } - pub fn audio_outs (&self) -> &[AudioOutput] { - self.devices.last().map(|x|x.audio_outs()).unwrap_or_default() - } - pub fn stop (&mut self) -> Perhaps { - self.sequencer.enqueue_next(None); - Ok(None) - } -} - -impl HasWidth for Track { - const MIN_WIDTH: usize = 9; - fn width_inc (&mut self) { - self.width += 1; - } - fn width_dec (&mut self) { - if self.width > Track::MIN_WIDTH { self.width -= 1; } - } -} - -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!(), - } - } -} - -#[tek_proc::command(Device)] -#[tek_proc::keyword(Device)] -#[derive(Debug)] -pub enum DeviceCmd {} - -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); - - -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() - } -} diff --git a/src/gear/sample.rs b/src/gear/sample.rs index cd7f121d..f0ae20eb 100644 --- a/src/gear/sample.rs +++ b/src/gear/sample.rs @@ -59,6 +59,14 @@ pub(crate) use symphonia::{ } impl + AsMut> HasSampler for T {} +pub trait HasSampler: AsRef + AsMut { + fn sampler (&self) -> &Sampler { + self.as_ref() + } + fn sampler_mut (&mut self) -> &mut Sampler { + self.as_mut() + } +} impl_audio!(Sampler: sampler_jack_process); diff --git a/src/gear/track.rs b/src/gear/track.rs new file mode 100644 index 00000000..0a206704 --- /dev/null +++ b/src/gear/track.rs @@ -0,0 +1,81 @@ +use crate::*; + +/// A track consists of a sequencer and zero or more devices chained after it. +/// +/// ``` +/// let track: tek::Track = Default::default(); +/// ``` +#[derive(Debug, Default)] +pub struct Track { + /// Name of track + pub name: Arc, + /// Identifying color of track + pub color: ItemTheme, + /// Preferred width of track column + pub width: usize, + /// MIDI sequencer state + pub sequencer: Sequencer, + /// Device chain + pub devices: Vec, +} + +impl Track { + /// Create a new track with only the default [Sequencer]. + pub fn new ( + name: &impl AsRef, + color: Option, + jack: &Jack<'static>, + clock: Option<&Clock>, + clip: Option<&Arc>>, + midi_from: &[Connect], + midi_to: &[Connect], + ) -> Usually { + Ok(Self { + name: name.as_ref().into(), + color: color.unwrap_or_default(), + sequencer: Sequencer::new( + format!("{}/sequencer", name.as_ref()), + jack, clock, clip, midi_from, midi_to + )?, + ..Default::default() + }) + } + pub fn new_added ( + jack: &Jack<'static>, + clock: Option<&Clock>, + name: &impl AsRef, + color: Option, + clip: Option<&Arc>>, + mins: Arc<[Connect]>, + mouts: Arc<[Connect]>, + ) -> Usually { + let name = name.as_ref(); + Ok(Self { + name: name.into(), + width: (name.len() + 2).max(12), + color: color.unwrap_or_else(ItemTheme::random), + sequencer: Sequencer::new(name, jack, clock, clip, mins.as_ref(), mouts.as_ref())?, + ..Default::default() + }) + } + pub fn audio_ins (&self) -> &[AudioInput] { + self.devices.first().map(|x|x.audio_ins()).unwrap_or_default() + } + pub fn audio_outs (&self) -> &[AudioOutput] { + self.devices.last().map(|x|x.audio_outs()).unwrap_or_default() + } + pub fn stop (&mut self) -> Perhaps { + self.sequencer.enqueue_next(None); + Ok(None) + } +} + +impl HasWidth for Track { + const MIN_WIDTH: usize = 9; + fn width_inc (&mut self) { + self.width += 1; + } + fn width_dec (&mut self) { + if self.width > Track::MIN_WIDTH { self.width -= 1; } + } +} diff --git a/src/tek.rs b/src/tek.rs index 46b23e25..9f23cdd6 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -1,119 +1,5 @@ #![allow(clippy::unit_arg)] -/// Define and implement a unit of time -macro_rules! impl_time_unit { - ($T:ident) => { - impl TimeUnit for $T {} - impl_op!($T, Add, add, |a, b|{a + b}); - impl_op!($T, Sub, sub, |a, b|{a - b}); - impl_op!($T, Mul, mul, |a, b|{a * b}); - impl_op!($T, Div, div, |a, b|{a / b}); - impl_op!($T, Rem, rem, |a, b|{a % b}); - impl_from!($T: |value: f64| { Self(value.into()) }); - impl_from!($T: |value: usize| { Self((value as f64).into()) }); - impl From<$T> for f64 { fn from (value: $T) -> Self { value.get() } } - impl From<$T> for usize { fn from (value: $T) -> Self { value.get() as usize } } - impl From<&$T> for f64 { fn from (value: &$T) -> Self { value.get() } } - impl From<&$T> for usize { fn from (value: &$T) -> Self { value.get() as usize } } - impl Clone for $T { fn clone (&self) -> Self { Self(self.get().into()) } } - impl $T { - pub fn get (&self) -> f64 { - self.0.load(Relaxed) - } - pub fn set (&self, value: f64) -> f64 { - let old = self.get(); - self.0.store(value, Relaxed); - old - } - } - } -} - -impl_time_unit!(SampleCount); -impl_time_unit!(SampleRate); -impl_time_unit!(Microsecond); -impl_time_unit!(Quantize); -impl_time_unit!(Ppq); -impl_time_unit!(Pulse); -impl_time_unit!(Bpm); -impl_time_unit!(LaunchSync); - -/// Define a type alias for iterators of sized items (columns). -macro_rules! def_sizes_iter { - ($Type:ident => $($Item:ty),+) => { - pub trait $Type<'a>: Iterator + Send + Sync + 'a {} - impl<'a, T: Iterator + Send + Sync + 'a> $Type<'a> for T {} - } -} - -def_sizes_iter!(ScenesSizes => Scene); -def_sizes_iter!(TracksSizes => Track); -def_sizes_iter!(PortsSizes => Arc, [Connect]); - -impl_has!(Clock: |self: Track|self.sequencer.clock); -impl_default!(Timebase: Self::new(48000f64, 150f64, DEFAULT_PPQ)); - -primitive!(isize: try_to_isize); -primitive!(u16: try_to_u16); -primitive!(u8: try_to_u8); -primitive!(usize: try_to_usize); - -impl_as_ref!(Config: |self: Config| self); -impl_as_ref!(Config: |self: ConfigWatcher| &self.config); -impl_as_mut!(Config: |self: ConfigWatcher| &mut self.config); -impl_as_ref!(ConfigWatcher: |self: ConfigWatcher| &self); - -impl_debug!(MenuItem: |self, w| { write!(w, "{}", &self.0) }); -impl_default!(MenuItem: Self::new("", [])); - -impl_as_mut!(Option: |self: Project| &mut self.editor); -impl_as_mut_opt!(Scene: |self: Project| self.selected_scene_mut()); -impl_as_mut_opt!(Track: |self: Project| self.selected_track_mut()); -impl_as_ref!(Option: |self: Project| &self.editor); -impl_as_ref_opt!(Scene: |self: Project| self.selected_scene()); -impl_as_ref_opt!(Track: |self: Project| self.selected_track()); -impl_has!(Clock: |self: Project| self.clock); -impl_has!(Jack<'static>: |self: Project| self.jack); -impl_has!(Selection: |self: Project| self.selection); -impl_has!(Sizer: |self: Project| self.size); -impl_has!(Vec: |self: Project| self.midi_ins); -impl_has!(Vec: |self: Project| self.midi_outs); - -impl_as_ref!(Config: |self: App|self.config.config.as_ref()); -impl_as_mut!(Option: |self: App|self.project.as_mut()); -impl_as_mut!(Option: |self: App|todo!()); -impl_as_ref!(Option: |self: App|self.project.as_ref()); -impl_as_ref!(Option: |self: App|todo!()); -impl_as_mut_opt!(Scene: |self: App|self.project.as_mut_opt()); -impl_as_mut_opt!(Track: |self: App|self.project.as_mut_opt()); -impl_as_ref_opt!(Scene: |self: App|self.project.as_ref_opt()); -impl_as_ref_opt!(Track: |self: App|self.project.as_ref_opt()); -impl_audio!(App: tek_jack_process, tek_jack_event); - -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.project.clips); -impl_has!(Pool: |self: App|self.project.samples); -impl_has!(Selection: |self: App|self.project.selection); -impl_has!(Sizer: |self: App|self.size); -impl_has!(Vec: |self: App|self.project.midi_ins); -impl_has!(Vec: |self: App|self.project.midi_outs); - -impl_from!(AppCmd: |x: BrowseCmd| AppCmd::Browse { command: x }); -impl_from!(AppCmd: |x: MidiClipCmd| AppCmd::MidiClip { command: x }); -impl_from!(AppCmd: |x: MidiEditCmd| AppCmd::MidiEdit { command: x }); -impl_from!(AppCmd: |x: ProjectCmd| AppCmd::Project { command: x }); -impl_from!(AppCmd: |x: SceneCmd| AppCmd::Scene { command: x }); -impl_from!(AppCmd: |x: ScenesCmd| AppCmd::Scenes { command: x }); -impl_from!(AppCmd: |x: TrackCmd| AppCmd::Track { command: x }); -impl_from!(AppCmd: |x: TracksCmd| AppCmd::Tracks { command: x }); - -tui_keys!(self: App, input { - #[cfg(feature = "prof2")] profiling::scope!("App::tui_keys!"); - self.input_cmds(input) -}); - pub use self::deps::*; mod deps; /// Command-line entrypoint. @@ -122,9 +8,9 @@ pub use self::deps::*; mod deps; fn main () -> Usually<()> { tengri::Tui::setup_panic(); #[cfg(not(feature = "cli"))] - let outcome = ConfigWatcher::run_watched(main_plain, None); + let outcome = Watch::run_watched(main_plain, None); #[cfg(feature = "cli")] - let outcome = ConfigWatcher::run_watched(main_cli, None); + let outcome = Watch::run_watched(main_cli, None); if let Err(e) = outcome { println!("{e:#?}"); std::process::exit(1); @@ -132,8 +18,9 @@ fn main () -> Usually<()> { Ok(()) } +/// Start with empty project. #[cfg(not(feature = "cli"))] -fn main_plain (config: Arc) -> Usually<()> { +fn main_plain (config: Arc) -> Usually<()> { let name = "tek"; tengri::Tui::run_main(Jack::new_run(name, move|jack|{ let mode = "Menu"; @@ -225,9 +112,9 @@ pub struct App { /// Performance counter pub perf: PerfModel, /// Available view modes and input bindings - pub config: Arc, - /// Currently selected mode - pub modes: Arc>>>, + pub config: Arc, + /// Currently active modes + pub stack: Arc>>>, /// Undo history pub history: Vec<(AppCmd, Option)>, /// Dialog overlay @@ -246,7 +133,7 @@ impl App { /// ``` /// # use ::tek::{*, tengri::Jack}; /// let proj = Project::new_jack(&"test_tek").expect("failed to connect to jack"); - /// let tek = config_reload(App::new(None, proj, None, "hello"), stringify!( + /// let tek = config_reload(App::new(None, proj, None, "Arranger"), stringify!( /// (mode Arranger (name Arranger) (info Launch grid.) /// (axis X Track) /// (axis Y Scene) @@ -265,7 +152,7 @@ impl App { pub fn new ( exit: Option, project: Project, - config: impl Into>>, + config: impl Into>>, mode: impl AsRef ) -> Self { App { @@ -273,7 +160,7 @@ impl App { jack: project.jack.clone(), color: ItemTheme::random(), dialog: Dialog::welcome(), - modes: Arc::new(RwLock::new(vec![mode.as_ref().into()])), + stack: Arc::new(RwLock::new(vec![mode.as_ref().into()])), config: config.into().unwrap_or_default(), project, ..Default::default() @@ -748,13 +635,6 @@ impl App { } } -pub trait HasError { - fn error (&self) -> &RwLock>; - fn error_set (&self, e: Option) { - *self.error().write() = e; - } -} - /// Has a MIDI editor. /// /// ``` @@ -887,15 +767,6 @@ pub trait HasTracks: AsRef> + AsMut> + HasClock + HasTrack } } -pub trait HasSampler: AsRef + AsMut { - fn sampler (&self) -> &Sampler { - self.as_ref() - } - fn sampler_mut (&mut self) -> &mut Sampler { - self.as_mut() - } -} - pub trait HasTrackScroll { fn track_scroll (&self) -> usize; fn tracks_last (&self) -> usize; @@ -1037,46 +908,6 @@ impl MenuItem { impl Dialog { - fn eval <'a> (_: &App, src: impl Language) -> Perhaps { - - eval_word(&src, |word|Ok(match word { - - ":none" => Dialog::None, - - ":options" => Dialog::Options, - - ":device" => Dialog::Device(0), - ":device/prev" => Dialog::Device(0), - ":device/next" => Dialog::Device(0), - - ":help" => Dialog::Help(0), - ":save" => Dialog::Browse( - BrowseTarget::SaveProject, - Browse::new(None).unwrap().into()), - ":load" => Dialog::Browse( - BrowseTarget::LoadProject, - Browse::new(None).unwrap().into()), - - ":import/clip" => Dialog::Browse( - BrowseTarget::ImportClip(Default::default()), - Browse::new(None).unwrap().into()), - ":export/clip" => Dialog::Browse( - BrowseTarget::ExportClip(Default::default()), - Browse::new(None).unwrap().into()), - - ":import/sample" => Dialog::Browse( - BrowseTarget::ImportSample(Default::default()), - Browse::new(None).unwrap().into()), - ":export/sample" => Dialog::Browse( - BrowseTarget::ExportSample(Default::default()), - Browse::new(None).unwrap().into()), - - _ => return Err(format!("not a dialog: {}", src.src()?.unwrap_or("")).into()) - - })) - - } - /// ``` /// let _ = tek::Dialog::welcome(); /// ``` @@ -1216,10 +1047,18 @@ impl Browse { } Ok(Self { cwd, dirs, files, ..Default::default() }) } - pub fn to_chdir (&self) -> Usually { Self::new(Some(self.path())) } - pub fn len (&self) -> usize { self.dirs.len() + self.files.len() } - pub fn is_dir (&self) -> bool { self.index < self.dirs.len() } - pub fn is_file (&self) -> bool { self.index >= self.dirs.len() } + pub fn to_chdir (&self) -> Usually { + Self::new(Some(self.path())) + } + pub fn len (&self) -> usize { + self.dirs.len() + self.files.len() + } + pub fn is_dir (&self) -> bool { + self.index < self.dirs.len() + } + pub fn is_file (&self) -> bool { + self.index >= self.dirs.len() + } pub fn path (&self) -> PathBuf { self.cwd.join(if self.is_dir() { &self.dirs[self.index].0 @@ -1269,34 +1108,549 @@ pub fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { Ok((subdirs, files)) } -pub fn print_status (project: &Project) { - println!("Name: {:?}", &project.name); - println!("JACK: {:?}", &project.jack); - println!("Buffer: {:?}", &project.clock.chunk); - println!("Sample rate: {:?}", &project.clock.timebase.sr); - println!("MIDI PPQ: {:?}", &project.clock.timebase.ppq); - println!("Tempo: {:?}", &project.clock.timebase.bpm); - println!("Quantize: {:?}", &project.clock.quant); - println!("Launch: {:?}", &project.clock.sync); - println!("Playhead: {:?}us", &project.clock.playhead.usec); - println!("Playhead: {:?}s", &project.clock.playhead.sample); - println!("Playhead: {:?}p", &project.clock.playhead.pulse); - println!("Started: {:?}", &project.clock.started); - println!("Tracks:"); - for (i, t) in project.tracks.iter().enumerate() { - println!(" Track {i}: {} {} {:?} {:?}", t.name, t.width, - &t.sequencer.play_clip, &t.sequencer.next_clip); +/// Default configuration contents. +const CONFIG_DEFAULT: &'static str = include_str!("tek.edn"); + +pub fn config_init + AsRef> (config: C) -> Usually { + // Write initial contents of configuration: + let watcher: &Watch = config.as_ref(); + if watcher.find_file().is_none() { + std::fs::write(watcher.place_file()?, CONFIG_DEFAULT)?; } - println!("Scenes:"); - for (i, t) in project.scenes.iter().enumerate() { - println!(" Scene {i}: {} {:?}", &t.name, &t.clips); + // Load config from file: + Ok(if let Some(path) = watcher.find_file() { + config_reload(config, std::fs::read_to_string(&path)?)? + } else { + return Err(format!("config_init: not found").into()) + }) +} + +/// Reload configuration from source. +pub fn config_reload , L: Language> (state: C, src: L) -> Usually { + state.as_ref().config_clear(); + src.each(state, |c, s|config_add(c, s.trim())) +} + +/// Load configuration item. +fn config_add , L: Language> (state: C, src: L) -> Usually { + if let Some(expr) = src.expr()? { + match expr.head()? { + Some("mode") => state.as_ref().modes_add(expr.tail()), + Some("view") => state.as_ref().views_add(expr.tail()), + Some("bind") => state.as_ref().binds_def_keys(expr.tail()), + Some("axis") => state.as_ref().binds_def_axis(expr.tail()), + _ => return Err(format!("config_add: unexpected: {src:?}").into()) + }?; + Ok(state) + } else { + Err(format!("Config::add_one: tried to add empty item").into()) } - println!("MIDI Ins: {:?}", &project.midi_ins); - println!("MIDI Outs: {:?}", &project.midi_outs); - println!("Audio Ins: {:?}", &project.audio_ins); - println!("Audio Outs: {:?}", &project.audio_outs); - // TODO git integration - // TODO dawvert integration +} + +/// Contains timestamped mode, view, and bind definitions. +/// +/// ``` +/// # use ::{std::sync::Arc, tek::*}; +/// +/// // Default empty config: +/// let config: Config = Default::default(); +/// +/// // Define a binding: +/// config.binds_def_keys(stringify!(Confirm (@enter confirm))).unwrap(); +/// +/// // Define an axis: +/// config.binds_def_axis(stringify!(Y @up @down)).unwrap(); +/// ``` +#[derive(Default, Debug)] +pub struct Config { + /// Error caught during (re)loading + pub error: RwLock>>, + /// Timestamp + pub stamp: AtomicU64, + /// Active collection of interaction modes. + pub modes: Arc, Arc>>>, + /// Active collection of view definitions. + pub views: Arc, Arc>>>>, + /// Active collection of key bindings. + pub keys: Arc, Arc>>>>>, + /// Active collection of axis bindings. + pub axis: Arc, Arc>>>>, +} + +impl Config { + /// Create a new timestamped config state. + pub fn new () -> Self { + Self { stamp: quanta::Clock::new().raw().into(), ..Default::default() } + } +} + +/// Configuration watcher and manager. +/// +/// ``` +/// let config = tek::Watch::new(None); +/// ``` +#[derive(Default, Debug)] +pub struct Watch { + /// XDG base directories of running user. + pub dirs: BaseDirectories, + /// Watcher + pub watch: RwLock>, + /// Actual configuration + pub config: Config, +} + +impl Watch { + /// Default configuration directory. + const DIR: &'static str = "tek"; + /// Default configuration subdirectory. + const SUB: &'static str = "v0"; + /// Default configuration file name. + const EDN: &'static str = "tek.edn"; + /// Create a new watched app configuration + /// from a set of XDG base directories, + pub fn new (dirs: Option) -> Self { + Self { + dirs: dirs.unwrap_or_else(||BaseDirectories::with_profile(Self::DIR, Self::SUB)), + ..Default::default() + } + } + /// Create, initialize, and watch a new configuration. + pub fn run_watched ( + callback: impl FnOnce(Arc)->T, + dirs: impl Into>, + ) -> Usually { + let watch = Arc::new(Self::new(dirs.into())); + Self::watch(watch.clone(), None)?; + let result = callback(watch); + Ok(result) + } + /// 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.error_set(Some(format!("{e:?}").into())); + panic!("{e:?}"); + } else { + //println!("config updated"); + }, + Err(errors) => { + panic!("{errors:?}"); + } + } + } + }, opts)?; + if let Some(path) = config.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()) + } + } + /// Find the config file. + fn find_file (&self) -> Option { + self.dirs.find_config_file(Self::EDN) + } + /// Place config file in default location. + fn place_file (&self) -> Result { + self.dirs.place_config_file(Self::EDN) + } + /// Get path to config file. + fn get_file (&self) -> Option { + self.dirs.get_config_file(Self::EDN) + } +} + +/// Has a stack of currently active interaction modes. +pub trait HasModeStack { + /// Stack of active modes. + fn mode_stack (&self) -> &RwLock>>; + + fn mode_stack_push (&self, mode: impl AsRef) { + self.mode_stack().write().push(mode.as_ref().into()) + } +} +impl HasModeStack for App { + fn mode_stack (&self) -> &RwLock>> { + &self.stack + } +} + +/// Collection of UI modes. +/// +/// ``` +/// # use tek::*; +/// +/// // Empty mode registry: +/// let state: Config = Default::default(); +/// assert_eq!(state.modes_count(), 0); +/// assert!(state.modes_get("Foo").is_none()); +/// +/// // Add a mode: +/// let _ = state.modes_add(&"Foo (name Foo) (info Bar)").unwrap(); +/// assert_eq!(state.modes_count(), 1); +/// assert!(state.modes_get("Foo").is_some()); +/// ``` +pub trait HasModes { + fn modes (&self) -> &RwLock, Arc>>; + + /// Register a mode. + /// + /// ``` + /// # use ::{std::sync::Arc, tek::*}; + /// let config: Config = Default::default(); + /// + /// config.modes_add(stringify!(Menu + /// (name Menu) (info Mode selector.) (axis Y Index) (bind Confirm) + /// (view (bg (g 30) (bar/s 7 Logo (full/xy Dialog)))) + /// )).unwrap(); + /// + /// let _: Arc = config.modes_get("Menu").unwrap(); + /// ``` + fn modes_add <'a> (&self, expr: impl Language) -> UsuallyRef<'a, ()> { + let name = expr.head()?.ok_or("mode: missing name")?; + let body = expr.tail()?.ok_or("mode: missing body")?; + let mut mode = Mode::default(); + body.each(&mut mode, |c, s|c.add(s))?; + self.modes().write().insert(name.into(), Arc::new(mode)); + Ok(()) + } + /// Get a mode by name. + fn modes_get (&self, name: impl AsRef) -> Option> { + self.modes().read().get(name.as_ref()).cloned() + } + /// Run something for each mode. + fn modes_for_each (&self, mut ator: impl FnMut(&str, &Mode)->T) { + for (k, v) in self.modes().read().iter() { + let _ = ator(k.as_ref(), v.as_ref()); + } + } + /// Count modes. + fn modes_count (&self) -> usize { + self.modes().read().len() + } + /// Clear modes. + fn modes_clear (&self) { + *self.modes().write() = Default::default(); + } +} +impl HasModes for App { + fn modes (&self) -> &RwLock, Arc>> { + &self.config.config.modes + } +} +impl HasModes for Config { + fn modes (&self) -> &RwLock, Arc>> { + &self.modes + } +} +impl HasModes for Mode { + fn modes (&self) -> &RwLock, Arc>> { + &self.modes + } +} + +pub trait HasViews { + fn views (&self) -> &RwLock, Arc>>>; + /// Load custom view definition. + fn views_add <'a> (&self, expr: impl Language) -> UsuallyRef<'a, ()> { + self.views().write().insert( + expr.head()?.ok_or("view: missing name")?.into(), + View::new(expr.tail()?.ok_or("view: missing body")?)?.into() + ); + Ok(()) + } + fn views_clear (&self) { + *self.views().write() = Default::default(); + } + fn views_get (&self, name: impl AsRef) -> Option>> { + self.views().read().get(name.as_ref()).cloned() + } + fn views_for_each (&self, cb: impl Fn(&Arc, &Arc>)->T) -> () { + for (k, v) in self.views().read().iter() { + cb(k, v); + } + } +} +impl HasViews for App { + fn views (&self) -> &RwLock, Arc>>> { + &self.config.config.views + } +} +impl HasViews for Config { + fn views (&self) -> &RwLock, Arc>>> { + &self.views + } +} + +pub trait HasBinds { + fn binds_keys (&self) -> &RwLock, Arc>>>>; + fn binds_axis (&self) -> &RwLock, Arc>>>; + /// Clear the keys and axis bindings collections. + fn binds_clear (&self) { + *self.binds_keys().write() = Default::default(); + *self.binds_axis().write() = Default::default(); + } + /// Find a matching key binding. + fn binds_find (&self, id: impl AsRef) -> Option>>> { + self.binds_keys().read().get(id.as_ref()).cloned() + } + /// Iterate over key bindings. + fn binds_for_each ( + &self, mut cb: impl FnMut(&Arc, &Bind>)->T + ) { + for (k, v) in self.binds_keys().read().iter() { + cb(k, v); + } + } + /// Define a top-level key binding. + /// + /// ``` + /// # use ::{std::sync::Arc, tek::*}; + /// let config: Config = Default::default(); + /// config.binds_def_keys(stringify!(Confirm (@enter confirm))).unwrap(); + /// ``` + fn binds_def_keys (&self, expr: impl Language) -> Usually<()> { + self.binds_keys().write().insert( + expr.head()?.ok_or("bind: missing name")?.into(), + Bind::new(expr.tail()?.unwrap_or(""))?.into() + ); + Ok(()) + } + /// Define an top-level axis binding (keypair). + /// + /// ``` + /// # use ::{std::sync::Arc, tek::*}; + /// let config: Config = Default::default(); + /// config.binds_def_axis(stringify!(Y @up @down)).unwrap(); + /// ``` + fn binds_def_axis (&self, expr: impl Language) -> Usually<()> { + self.binds_axis().write().insert( + expr.head()?.ok_or("axis: missing name")?.into(), + Axis::eval(expr.tail()?.unwrap_or(""))?.into() + ); + Ok(()) + } +} +impl HasBinds for App { + fn binds_keys (&self) -> &RwLock, Arc>>>> { + &self.config.config.keys + } + fn binds_axis (&self) -> &RwLock, Arc>>> { + &self.config.config.axis + } +} +impl HasBinds for Config { + fn binds_keys (&self) -> &RwLock, Arc>>>> { + &self.keys + } + fn binds_axis (&self) -> &RwLock, Arc>>> { + &self.axis + } +} + +pub trait DispatchBind: HasBinds + HasModeStack + HasModes + Namespace { + fn dispatch_bind (&mut self, input: &TuiEvent) -> Usually<()> { + if let Some(bindings) = self.dispatch_bind_match(input) { + return self.dispatch_bind_exec(bindings) + } + Ok(()) + } + /// Find command bindings that match the ,nput. + /// ``` + /// # use ::{std::sync::Arc, tek::*}; + /// // Configure modes: + /// let app = config_reload(App::default(), stringify!( + /// (axis Y @up @down) + /// (bind Confirm (@enter confirm)) + /// (mode Test (axis Y Index) (bind (@escape Cancel) Confirm)) + /// )).unwrap(); + /// println!("Modes: {:#?}", app.config.config.modes); + /// + /// // No mode is active: + /// assert!(app.dispatch_bind_match(&"@left".into()).is_none()); + /// assert!(app.dispatch_bind_match(&"@enter".into()).is_none()); + /// assert!(app.dispatch_bind_match(&"@escape".into()).is_none()); + /// assert!(app.dispatch_bind_match(&"@down".into()).is_none()); + /// assert!(app.dispatch_bind_match(&"@up".into()).is_none()); + /// + /// // Push mode onto stack: + /// app.mode_stack_push("Test"); + /// assert!(app.dispatch_bind_match(&"@left".into()).is_none()); + /// assert!(app.dispatch_bind_match(&"@enter".into()).is_some()); + /// assert!(app.dispatch_bind_match(&"@escape".into()).is_some()); + /// assert!(app.dispatch_bind_match(&"@down".into()).is_some()); + /// assert!(app.dispatch_bind_match(&"@up".into()).is_some()); + /// ``` + fn dispatch_bind_match (&self, input: &TuiEvent) -> Option>]>> { + for name in self.mode_stack().read().iter().rev() { + if let Some(mode) = self.modes_get(name).as_ref().map(Arc::clone) { + if let Some(binds) = self.dispatch_bind_match_axis(mode.as_ref(), input) { + return Some(binds) + } + if let Some(binds) = self.dispatch_bind_match_keys(mode.as_ref(), input) { + return Some(binds) + } + } + } + None + } + /// ``` + /// # use ::{std::sync::Arc, tek::*}; + /// let app = config_reload(App::default(), stringify!( + /// (axis Y @up @down) (mode Test (axis Y Index)) + /// )).unwrap(); + /// assert!(app.dispatch_bind_match(&"@down".into()).is_none()); + /// assert!(app.dispatch_bind_match(&"@up".into()).is_none()); + /// app.mode_stack_push("Test"); + /// assert!(app.dispatch_bind_match(&"@down".into()).is_some()); + /// assert!(app.dispatch_bind_match(&"@up".into()).is_some()); + /// ``` + fn dispatch_bind_match_axis (&self, mode: &Mode, input: &TuiEvent) -> Option>]>> { + let axis = self.binds_axis().read(); + for (ctrl, _param) in mode.axis.iter() { + if let Some(axis) = axis.get(ctrl) { + if *input == axis.0 { + return Some([Binding { commands: [].into(), ..Default::default() }].into()) + } else if *input == axis.1 { + return Some([Binding { commands: [].into(), ..Default::default() }].into()) + } + } + } + None + } + /// ``` + /// # use ::{std::sync::Arc, tek::*}; + /// let app = config_reload(App::default(), stringify!( + /// (bind Confirm (@enter confirm)) (mode Test (bind Confirm (@escape Cancel))) + /// )).unwrap(); + /// assert!(app.dispatch_bind_match(&"@enter".into()).is_none()); + /// assert!(app.dispatch_bind_match(&"@escape".into()).is_none()); + /// app.mode_stack_push("Test"); + /// assert!(app.dispatch_bind_match(&"@enter".into()).is_some()); + /// assert!(app.dispatch_bind_match(&"@escape".into()).is_some()); + /// ``` + fn dispatch_bind_match_keys (&self, mode: &Mode, input: &TuiEvent) -> Option>]>> { + for id in mode.keys.iter() { + println!("{input:?} {id:?}"); + if let Some(event_map) = self.binds_find(id) + && let Some(bindings) = event_map.query(input) { + return Some(bindings.into()) + } + if let Ok(Some(expr)) = id.expr() + && let Ok(Some(head)) = expr.head() + && *input == head.into() + && let Ok(Some(tail)) = expr.tail() { + return Some([Binding { commands: [tail.into()].into(), ..Default::default() }].into()) + } + } + None + } + fn dispatch_bind_exec (&mut self, bindings: Arc<[Binding>]>) -> Usually<()> { + for binding in bindings.iter() { + for src in binding.commands.iter() { + if let Some(command) = Namespace::::namespace(self, &src)? { + self.cmd_dispatch(command)?; + } + } + } + Ok(()) + } + fn cmd_dispatch (&mut self, command: C) -> Usually<()>; +} + +impl DispatchBind for App { + fn cmd_dispatch (&mut self, command: AppCmd) -> Usually<()> { + match command.clone().dispatch(self) { + Ok(undo) => { self.history.push((command, undo)); Ok(()) }, + Err(err) => { self.history.push((command, None)); Err(err) }, + } + } +} + +pub use self::conf::*; mod conf { + use crate::*; + + mod axis; pub use self::axis::*; + mod bind; pub use self::bind::*; + mod lang; pub use self::lang::*; + mod mode; pub use self::mode::*; + mod view; pub use self::view::*; + + pub trait HasError { + fn error (&self) -> &RwLock>; + fn error_set (&self, e: Option) { + *self.error().write() = e; + } + } + impl> HasError> for T { + fn error (&self) -> &RwLock>> { + &self.as_ref().error + } + } + impl HasError> for Arc { + fn error (&self) -> &RwLock>> { + &self.as_ref().config.error + } + } + + pub trait HasConfig { + fn config (&self) -> &Config; + /// Print the configuration. + fn config_print (&self) { + let config = self.config(); + print_config_views(config); + print_config_binds(config); + print_config_modes(config); + } + /// Make this configuration empty. + fn config_clear (&self) { + let config = self.config(); + config.modes_clear(); + config.views_clear(); + config.binds_clear(); + config.stamp.store(quanta::Clock::new().raw(), Relaxed); + } + } + impl HasConfig for App { + fn config (&self) -> &Config { + &self.config.config + } + } + impl HasConfig for Watch { + fn config (&self) -> &Config { + &self.config + } + } + impl HasConfig for Config { + fn config (&self) -> &Config { + self + } + } +} + +pub use self::gear::*; mod gear { + mod device; pub use self::device::*; + mod midi; pub use self::midi::*; + mod mixer; pub use self::mixer::*; + mod pool; pub use self::pool::*; + mod sample; pub use self::sample::*; + mod track; pub use self::track::*; + + #[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::*; } #[cfg(feature = "cli")] pub use self::cli::*; @@ -1313,7 +1667,7 @@ pub fn print_status (project: &Project) { println!("versions aint real man"); } - pub fn main_cli (config: Arc) -> Usually<()> { + pub fn main_cli (config: Arc) -> Usually<()> { Cli::parse().run(Some(config.into())) } @@ -1341,9 +1695,9 @@ pub fn print_status (project: &Project) { /// Command-line configuration. impl Cli { - pub fn run (&self, mut config: Option>) -> Usually<()> { + pub fn run (&self, mut config: Option>) -> Usually<()> { self.action.run( - config.unwrap_or_else(||Arc::new(ConfigWatcher::new(None))), + config.unwrap_or_else(||Arc::new(Watch::new(None))), self.trace ) } @@ -1377,7 +1731,7 @@ pub fn print_status (project: &Project) { } 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(), @@ -1466,233 +1820,148 @@ pub fn print_status (project: &Project) { Ok(proj) } } + + pub fn print_status (project: &Project) { + println!("Name: {:?}", &project.name); + println!("JACK: {:?}", &project.jack); + println!("Buffer: {:?}", &project.clock.chunk); + println!("Sample rate: {:?}", &project.clock.timebase.sr); + println!("MIDI PPQ: {:?}", &project.clock.timebase.ppq); + println!("Tempo: {:?}", &project.clock.timebase.bpm); + println!("Quantize: {:?}", &project.clock.quant); + println!("Launch: {:?}", &project.clock.sync); + println!("Playhead: {:?}us", &project.clock.playhead.usec); + println!("Playhead: {:?}s", &project.clock.playhead.sample); + println!("Playhead: {:?}p", &project.clock.playhead.pulse); + println!("Started: {:?}", &project.clock.started); + println!("Tracks:"); + for (i, t) in project.tracks.iter().enumerate() { + println!(" Track {i}: {} {} {:?} {:?}", t.name, t.width, + &t.sequencer.play_clip, &t.sequencer.next_clip); + } + println!("Scenes:"); + for (i, t) in project.scenes.iter().enumerate() { + println!(" Scene {i}: {} {:?}", &t.name, &t.clips); + } + println!("MIDI Ins: {:?}", &project.midi_ins); + println!("MIDI Outs: {:?}", &project.midi_outs); + println!("Audio Ins: {:?}", &project.audio_ins); + println!("Audio Outs: {:?}", &project.audio_outs); + // TODO git integration + // TODO dawvert integration + } } -pub use self::conf::*; mod conf { - use crate::*; - - mod bind; pub use self::bind::*; - mod lang; pub use self::lang::*; - mod mode; pub use self::mode::*; - mod view; pub use self::view::*; - - /// Default configuration contents. - const CONFIG_DEFAULT: &'static str = include_str!("tek.edn"); - - pub fn config_init + AsRef> (config: C) -> Usually { - // Write initial contents of configuration: - let watcher: &ConfigWatcher = config.as_ref(); - if watcher.find_file().is_none() { - std::fs::write(watcher.place_file()?, CONFIG_DEFAULT)?; - } - // Load config from file: - Ok(if let Some(path) = watcher.find_file() { - config_reload(config, std::fs::read_to_string(&path)?)? - } else { - return Err(format!("config_init: not found").into()) - }) - } - - /// Reload configuration from source. - pub fn config_reload , L: Language> (state: C, src: L) -> Usually { - state.as_ref().config_clear(); - src.each(state, |c, s|config_add(c, s.trim())) - } - - /// Load configuration item. - fn config_add , L: Language> (state: C, src: L) -> Usually { - if let Some(expr) = src.expr()? { - match expr.head()? { - Some("mode") => state.as_ref().modes_add(expr.tail()), - Some("view") => state.as_ref().views_add(expr.tail()), - Some("bind") => state.as_ref().binds_def_keys(expr.tail()), - Some("axis") => state.as_ref().binds_def_axis(expr.tail()), - _ => return Err(format!("config_add: unexpected: {src:?}").into()) - }?; - Ok(state) - } else { - Err(format!("Config::add_one: tried to add empty item").into()) - } - } - - /// Contains timestamped mode, view, and bind definitions. - /// - /// ``` - /// # use ::{std::sync::Arc, tek::*}; - /// - /// // Default empty config: - /// let config: Config = Default::default(); - /// - /// // Define a binding: - /// config.binds_def_keys(stringify!(Confirm (@enter confirm))).unwrap(); - /// - /// // Define an axis: - /// config.binds_def_axis(stringify!(Y @up @down)).unwrap(); - /// ``` - #[derive(Default, Debug)] - pub struct Config { - /// Error caught during (re)loading - pub error: RwLock>>, - /// Timestamp - pub stamp: AtomicU64, - /// Active collection of interaction modes. - pub modes: Modes, - /// Active collection of event bindings. - pub binds: Binds, - /// Active collection of view definitions. - pub views: Views, - } - - impl Config { - /// Create a new timestamped config state. - pub fn new () -> Self { - Self { stamp: quanta::Clock::new().raw().into(), ..Default::default() } - } - } - - /// Configuration watcher and manager. - /// - /// ``` - /// let config = tek::ConfigWatcher::new(None); - /// ``` - #[derive(Default, Debug)] - pub struct ConfigWatcher { - /// XDG base directories of running user. - pub dirs: BaseDirectories, - /// Watcher - pub watch: RwLock>, - /// Actual configuration - pub config: Config, - } - - impl ConfigWatcher { - /// Default configuration directory. - const DIR: &'static str = "tek"; - /// Default configuration subdirectory. - const SUB: &'static str = "v0"; - /// Default configuration file name. - const EDN: &'static str = "tek.edn"; - /// Create a new watched app configuration - /// from a set of XDG base directories, - pub fn new (dirs: Option) -> Self { - Self { - dirs: dirs.unwrap_or_else(||BaseDirectories::with_profile(Self::DIR, Self::SUB)), - ..Default::default() +/// Define and implement a unit of time +macro_rules! impl_time_unit { + ($T:ident) => { + impl TimeUnit for $T {} + impl_op!($T, Add, add, |a, b|{a + b}); + impl_op!($T, Sub, sub, |a, b|{a - b}); + impl_op!($T, Mul, mul, |a, b|{a * b}); + impl_op!($T, Div, div, |a, b|{a / b}); + impl_op!($T, Rem, rem, |a, b|{a % b}); + impl_from!($T: |value: f64| { Self(value.into()) }); + impl_from!($T: |value: usize| { Self((value as f64).into()) }); + impl From<$T> for f64 { fn from (value: $T) -> Self { value.get() } } + impl From<&$T> for f64 { fn from (value: &$T) -> Self { value.get() } } + impl From<$T> for usize { fn from (value: $T) -> Self { value.get() as usize } } + impl From<&$T> for usize { fn from (value: &$T) -> Self { value.get() as usize } } + impl Clone for $T { fn clone (&self) -> Self { Self(self.get().into()) } } + impl $T { + pub fn get (&self) -> f64 { + self.0.load(Relaxed) + } + pub fn set (&self, value: f64) -> f64 { + let old = self.get(); + self.0.store(value, Relaxed); + old } } - /// Create, initialize, and watch a new configuration. - pub fn run_watched ( - callback: impl FnOnce(Arc)->T, - dirs: impl Into>, - ) -> Usually { - let watch = Arc::new(Self::new(dirs.into())); - Self::watch(watch.clone(), None)?; - let result = callback(watch); - Ok(result) - } - /// 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.error_set(Some(format!("{e:?}").into())); - panic!("{e:?}"); - } else { - //println!("config updated"); - }, - Err(errors) => { - panic!("{errors:?}"); - } - } - } - }, opts)?; - if let Some(path) = config.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()) - } - } - /// Find the config file. - fn find_file (&self) -> Option { - self.dirs.find_config_file(Self::EDN) - } - /// Place config file in default location. - fn place_file (&self) -> Result { - self.dirs.place_config_file(Self::EDN) - } - /// Get path to config file. - fn get_file (&self) -> Option { - self.dirs.get_config_file(Self::EDN) - } } - - impl> HasError> for T { - fn error (&self) -> &RwLock>> { - &self.as_ref().error - } - } - impl HasError> for Arc { - fn error (&self) -> &RwLock>> { - &self.as_ref().config.error - } - } - pub trait HasConfig { - fn config (&self) -> &Config; - fn config_mut (&mut self) -> &mut Config; - /// Print the configuration. - fn config_print (&self) { - let config = self.config(); - print_config_views(config); - print_config_binds(config); - print_config_modes(config); - } - /// Make this configuration empty. - fn config_clear (&self) { - let config = self.config(); - config.modes_clear(); - config.views_clear(); - config.binds_clear(); - config.stamp.store(quanta::Clock::new().raw(), Relaxed); - } - } - impl HasConfig for ConfigWatcher { - fn config (&self) -> &Config { - &self.config - } - fn config_mut (&mut self) -> &mut Config { - &mut self.config - } - } - impl HasConfig for Config { - fn config (&self) -> &Config { - self - } - fn config_mut (&mut self) -> &mut Config { - self - } - } - } -pub use self::gear::*; mod gear { - mod pool; pub use self::pool::*; - mod midi; pub use self::midi::*; - mod mixer; pub use self::mixer::*; - mod sample; pub use self::sample::*; +impl_time_unit!(SampleCount); +impl_time_unit!(SampleRate); +impl_time_unit!(Microsecond); +impl_time_unit!(Quantize); +impl_time_unit!(Ppq); +impl_time_unit!(Pulse); +impl_time_unit!(Bpm); +impl_time_unit!(LaunchSync); - #[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::*; +/// Define a type alias for iterators of sized items (columns). +macro_rules! def_sizes_iter { + ($Type:ident => $($Item:ty),+) => { + pub trait $Type<'a>: Iterator + Send + Sync + 'a {} + impl<'a, T: Iterator + Send + Sync + 'a> $Type<'a> for T {} + } } + +def_sizes_iter!(ScenesSizes => Scene); +def_sizes_iter!(TracksSizes => Track); +def_sizes_iter!(PortsSizes => Arc, [Connect]); + +impl_has!(Clock: |self: Track|self.sequencer.clock); +impl_default!(Timebase: Self::new(48000f64, 150f64, DEFAULT_PPQ)); + +primitive!(isize: try_to_isize); +primitive!(u16: try_to_u16); +primitive!(u8: try_to_u8); +primitive!(usize: try_to_usize); + +impl_as_ref!(Config: |self: Config| self); +impl_as_ref!(Config: |self: Watch| &self.config); +impl_as_mut!(Config: |self: Watch| &mut self.config); +impl_as_ref!(Watch: |self: Watch| &self); + +impl_debug!(MenuItem: |self, w| { write!(w, "{}", &self.0) }); +impl_default!(MenuItem: Self::new("", [])); + +impl_as_mut!(Option: |self: Project| &mut self.editor); +impl_as_mut_opt!(Scene: |self: Project| self.selected_scene_mut()); +impl_as_mut_opt!(Track: |self: Project| self.selected_track_mut()); +impl_as_ref!(Option: |self: Project| &self.editor); +impl_as_ref_opt!(Scene: |self: Project| self.selected_scene()); +impl_as_ref_opt!(Track: |self: Project| self.selected_track()); +impl_has!(Clock: |self: Project| self.clock); +impl_has!(Jack<'static>: |self: Project| self.jack); +impl_has!(Selection: |self: Project| self.selection); +impl_has!(Sizer: |self: Project| self.size); +impl_has!(Vec: |self: Project| self.midi_ins); +impl_has!(Vec: |self: Project| self.midi_outs); + +impl_as_ref!(Config: |self: App|self.config.config.as_ref()); +impl_as_mut!(Option: |self: App|self.project.as_mut()); +impl_as_mut!(Option: |self: App|todo!()); +impl_as_ref!(Option: |self: App|self.project.as_ref()); +impl_as_ref!(Option: |self: App|todo!()); +impl_as_mut_opt!(Scene: |self: App|self.project.as_mut_opt()); +impl_as_mut_opt!(Track: |self: App|self.project.as_mut_opt()); +impl_as_ref_opt!(Scene: |self: App|self.project.as_ref_opt()); +impl_as_ref_opt!(Track: |self: App|self.project.as_ref_opt()); +impl_audio!(App: tek_jack_process, tek_jack_event); + +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.project.clips); +impl_has!(Pool: |self: App|self.project.samples); +impl_has!(Selection: |self: App|self.project.selection); +impl_has!(Sizer: |self: App|self.size); +impl_has!(Vec: |self: App|self.project.midi_ins); +impl_has!(Vec: |self: App|self.project.midi_outs); + +impl_from!(AppCmd: |x: BrowseCmd| AppCmd::Browse { command: x }); +impl_from!(AppCmd: |x: MidiClipCmd| AppCmd::MidiClip { command: x }); +impl_from!(AppCmd: |x: MidiEditCmd| AppCmd::MidiEdit { command: x }); +impl_from!(AppCmd: |x: ProjectCmd| AppCmd::Project { command: x }); +impl_from!(AppCmd: |x: SceneCmd| AppCmd::Scene { command: x }); +impl_from!(AppCmd: |x: ScenesCmd| AppCmd::Scenes { command: x }); +impl_from!(AppCmd: |x: TrackCmd| AppCmd::Track { command: x }); +impl_from!(AppCmd: |x: TracksCmd| AppCmd::Tracks { command: x }); + +tui_keys!(self: App, input { + #[cfg(feature = "prof2")] profiling::scope!("App::tui_keys!"); + self.dispatch_bind(input) +}); diff --git a/tengri b/tengri index ab8c8cb1..65f7948e 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit ab8c8cb1c4f2bed9ef4e333d5fc091e586ff0202 +Subproject commit 65f7948e9c0b5ed535d13c354ef5f4150a801f01