diff --git a/src/conf/bind.rs b/src/conf/bind.rs index 30d6d2f0..d15c47ca 100644 --- a/src/conf/bind.rs +++ b/src/conf/bind.rs @@ -1,5 +1,71 @@ use crate::*; +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.watch.config.keys + } + fn binds_axis (&self) -> &RwLock, Arc>>> { + &self.watch.config.axis + } +} +impl HasBinds for Config { + fn binds_keys (&self) -> &RwLock, Arc>>>> { + &self.keys + } + fn binds_axis (&self) -> &RwLock, Arc>>> { + &self.axis + } +} + /// A map of input events (e.g. [TuiEvent]) to [Binding]s. /// /// ``` diff --git a/src/conf/mode.rs b/src/conf/mode.rs index a6125915..7662dd81 100644 --- a/src/conf/mode.rs +++ b/src/conf/mode.rs @@ -1,5 +1,94 @@ use crate::*; +/// 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.watch.config.modes + } +} +impl HasModes for Config { + fn modes (&self) -> &RwLock, Arc>> { + &self.modes + } +} +impl HasModes for Mode { + fn modes (&self) -> &RwLock, Arc>> { + &self.modes + } +} + impl_as_mut!(Mode: |self: Mode| self); /// Group of view and keys definitions. diff --git a/src/conf/view.rs b/src/conf/view.rs index 0bbcbd20..3b309509 100644 --- a/src/conf/view.rs +++ b/src/conf/view.rs @@ -1,5 +1,38 @@ use crate::*; +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.watch.config.views + } +} +impl HasViews for Config { + fn views (&self) -> &RwLock, Arc>>> { + &self.views + } +} + impl_debug!( View: |self, w| { write!(w, "View({})", self.source) }); impl_display!( View: |self, w| { write!(w, "View({})", self.source) }); impl_default!(ClockView: { @@ -25,24 +58,6 @@ pub struct View { pub render: ArcDrawn + Send + Sync>> } -/// The [Draw] implementation for [App] handles the loaded view, -/// which is defined in terms of [dizzle] DSL. -/// -/// If there is an error, the error is displayed. FIXME: overlay it -/// Then, every top-level form of the DSL description is rendered. -impl Draw for App { - fn draw (&self, to: &mut Tui) -> Drawn { - #[cfg(feature = "prof2")] profiling::scope!("App::draw"); - self.perf.cycle(&mut |_|{ - draw_error(to, self.error.as_ref())?; - draw_mode_stack(to, &self)?; - view_debug(&self.perf, &self.size).draw(to)?; - #[cfg(feature = "prof2")] profiling::finish_frame!(); - Ok(Some(to.area().into())) - }) - } -} - pub fn draw_error (to: &mut Tui, error: &RwLock>>) -> Usually<()> { if let Some(e) = error.read().as_ref() { e.as_ref().align_c().draw(to)?; @@ -50,22 +65,23 @@ pub fn draw_error (to: &mut Tui, error: &RwLock>>) -> Usually<() Ok(()) } -pub fn view_debug (perf: &PerfModel, size: &Sizer) -> impl Draw { +pub fn view_debug ( + stack: &Vec>, + perf: &PerfModel, + size: &Sizer, +) -> impl Draw { Split::Above.bar(0, Split::West.bar(16, format!("{}/{} {} ", perf.used.load(Relaxed), perf.window.load(Relaxed), perf.clock.raw() / 10000000).align_e(), - format!("{}x{} ", + format!("{:03}x{:03} ", size.0.load(Relaxed), size.1.load(Relaxed)).align_w(), ).align_se(), - () - //Split::East.bar(16, - //format!("{}", self.modes.read()[0]), - //format!("{}", self.modes.read()[0]) - //).align_sw() + //() + format!("{:?}", stack).align_sw(), ) } @@ -95,6 +111,9 @@ pub fn draw_status_bar (to: &mut Tui, state: &App) -> Drawn { pub fn draw_mode_stack (to: &mut Tui, state: &App) -> Usually<()> { for name in state.mode_stack().read().iter() { let mut error = false; + //if state.watch.config.modes_count() > 0 { + //panic!("{name:?} {:?}", state.watch.config.modes); + //} if let Some(mode) = state.modes_get(name).as_ref().map(Arc::clone) { for (index, view) in mode.view.iter().enumerate() { match (view.render)(state, to) { diff --git a/src/tek.rs b/src/tek.rs index 9f23cdd6..e3aa82bc 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -112,7 +112,7 @@ pub struct App { /// Performance counter pub perf: PerfModel, /// Available view modes and input bindings - pub config: Arc, + pub watch: Arc, /// Currently active modes pub stack: Arc>>>, /// Undo history @@ -152,7 +152,7 @@ impl App { pub fn new ( exit: Option, project: Project, - config: impl Into>>, + watch: impl Into>>, mode: impl AsRef ) -> Self { App { @@ -161,57 +161,93 @@ impl App { color: ItemTheme::random(), dialog: Dialog::welcome(), stack: Arc::new(RwLock::new(vec![mode.as_ref().into()])), - config: config.into().unwrap_or_default(), + watch: watch.into().unwrap_or_default(), project, ..Default::default() } } } -fn tek_jack_process (state: &mut App, client: &Client, scope: &ProcessScope) -> Control { - let t0 = state.perf.get_t0(); - state.clock().update_from_scope(scope).unwrap(); - let midi_in = state.project.midi_input_collect(scope); - if let Some(editor) = &state.editor() { - let mut pitch: Option = None; - for port in midi_in.iter() { - for event in port.iter() { - if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..})) - = event - { - pitch = Some(key.clone()); - } - } - } - if let Some(pitch) = pitch { - editor.set_note_pos(pitch.as_int() as usize); - } - } - let result = state.project.tracks_jack_process(client, scope); - state.perf.update_from_jack_scope(t0, scope); - result +/// 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>, + /// App configuration + pub config: Config, } -fn tek_jack_event (state: &mut App, event: JackEvent) { - use JackEvent::*; - match event { - SampleRate(sr) => { state.clock().timebase.sr.set(sr as f64); }, - PortRegistration(_id, true) => { - //let port = self.jack().port_by_id(id); - //println!("\rport add: {id} {port:?}"); - //println!("\rport add: {id}"); - }, - PortRegistration(_id, false) => { - /*println!("\rport del: {id}")*/ - }, - PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ }, - PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ }, - ClientRegistration(_id, true) => {}, - ClientRegistration(_id, false) => {}, - ThreadInit => {}, - XRun => {}, - GraphReorder => {}, - _ => { panic!("{event:?}"); } +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) } } @@ -266,6 +302,53 @@ pub struct Project { #[cfg(feature = "scene")] pub scenes_scroll: usize, } +fn tek_jack_process (state: &mut App, client: &Client, scope: &ProcessScope) -> Control { + let t0 = state.perf.get_t0(); + state.clock().update_from_scope(scope).unwrap(); + let midi_in = state.project.midi_input_collect(scope); + if let Some(editor) = &state.editor() { + let mut pitch: Option = None; + for port in midi_in.iter() { + for event in port.iter() { + if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..})) + = event + { + pitch = Some(key.clone()); + } + } + } + if let Some(pitch) = pitch { + editor.set_note_pos(pitch.as_int() as usize); + } + } + let result = state.project.tracks_jack_process(client, scope); + state.perf.update_from_jack_scope(t0, scope); + result +} + +fn tek_jack_event (state: &mut App, event: JackEvent) { + use JackEvent::*; + match event { + SampleRate(sr) => { state.clock().timebase.sr.set(sr as f64); }, + PortRegistration(_id, true) => { + //let port = self.jack().port_by_id(id); + //println!("\rport add: {id} {port:?}"); + //println!("\rport add: {id}"); + }, + PortRegistration(_id, false) => { + /*println!("\rport del: {id}")*/ + }, + PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ }, + PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ }, + ClientRegistration(_id, true) => {}, + ClientRegistration(_id, false) => {}, + ThreadInit => {}, + XRun => {}, + GraphReorder => {}, + _ => { panic!("{event:?}"); } + } +} + impl Project { pub fn new (jack: &Jack<'static>) -> Self { Self { @@ -600,7 +683,6 @@ impl Scene { } } -pub type SceneWith<'a, T> = (usize, &'a Scene, usize, usize, T); impl ScenesView for App { fn w_mid (&self) -> u16 { (self.size.w() as u16).saturating_sub(self.w_side()) @@ -612,6 +694,7 @@ impl ScenesView for App { (self.size.h() as u16).saturating_sub(20) } } + impl HasScenes for App { fn scenes_scroll (&self) -> usize { self.project.scenes_scroll() @@ -1108,353 +1191,6 @@ pub fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { Ok((subdirs, files)) } -/// 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)?; - } - // 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: 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) { @@ -1471,7 +1207,7 @@ pub trait DispatchBind: HasBinds + HasModeStack + HasModes + Namespace /// (bind Confirm (@enter confirm)) /// (mode Test (axis Y Index) (bind (@escape Cancel) Confirm)) /// )).unwrap(); - /// println!("Modes: {:#?}", app.config.config.modes); + /// println!("Modes: {:#?}", app.watch.config.modes); /// /// // No mode is active: /// assert!(app.dispatch_bind_match(&"@left".into()).is_none()); @@ -1620,7 +1356,7 @@ pub use self::conf::*; mod conf { } impl HasConfig for App { fn config (&self) -> &Config { - &self.config.config + &self.watch.config } } impl HasConfig for Watch { @@ -1633,6 +1369,82 @@ pub use self::conf::*; mod conf { self } } + + /// 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)?; + } + // 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: 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() } + } + } } pub use self::gear::*; mod gear { @@ -1731,18 +1543,18 @@ pub use self::gear::*; mod gear { } impl Action { - fn run (&self, config: Arc, _trace: bool) -> Usually<()> { + fn run (&self, watch: Arc, _trace: bool) -> Usually<()> { use Action::*; match self { Version => show_version(), - Config => config.config_print(), + Config => watch.config_print(), Resume => todo!("resume session"), List => todo!("list sessions"), New(sesh) => Exit::run(|exit|Tui::run_main( exit.clone(), { #[allow(unused_mut)] - let mut app = App::new(Some(exit), sesh.init()?, config, "Menu"); + let mut app = App::new(Some(exit), sesh.init()?, watch, "Menu"); app.dialog = Dialog::welcome(); #[cfg(feature = "prof2")] { if trace { @@ -1931,7 +1743,7 @@ 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_ref!(Config: |self: App|self.watch.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()); @@ -1965,3 +1777,21 @@ tui_keys!(self: App, input { #[cfg(feature = "prof2")] profiling::scope!("App::tui_keys!"); self.dispatch_bind(input) }); + +/// The [Draw] implementation for [App] handles the loaded view, +/// which is defined in terms of [dizzle] DSL. +/// +/// If there is an error, the error is displayed. FIXME: overlay it +/// Then, every top-level form of the DSL description is rendered. +impl Draw for App { + fn draw (&self, to: &mut Tui) -> Drawn { + #[cfg(feature = "prof2")] profiling::scope!("App::draw"); + self.perf.cycle(&mut |_|{ + draw_error(to, self.error.as_ref())?; + draw_mode_stack(to, &self)?; + view_debug(&*self.stack.read(), &self.perf, &self.size).draw(to)?; + #[cfg(feature = "prof2")] profiling::finish_frame!(); + Ok(Some(to.area().into())) + }) + } +}