no first load of config?
Some checks are pending
/ build (push) Waiting to run

This commit is contained in:
i do not exist 2026-10-05 02:48:44 +03:00
parent 02dc7bc9f8
commit 914e3e094b
4 changed files with 428 additions and 424 deletions

View file

@ -112,7 +112,7 @@ pub struct App {
/// Performance counter
pub perf: PerfModel,
/// Available view modes and input bindings
pub config: Arc<Watch>,
pub watch: Arc<Watch>,
/// Currently active modes
pub stack: Arc<RwLock<Vec<Arc<str>>>>,
/// Undo history
@ -152,7 +152,7 @@ impl App {
pub fn new (
exit: Option<Exit>,
project: Project,
config: impl Into<Option<Arc<Watch>>>,
watch: impl Into<Option<Arc<Watch>>>,
mode: impl AsRef<str>
) -> 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<u7> = 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<Option<PollWatcher>>,
/// 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<BaseDirectories>) -> 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 <T: 'static> (
callback: impl FnOnce(Arc<Self>)->T,
dirs: impl Into<Option<BaseDirectories>>,
) -> Usually<T> {
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<Self>, poll: Option<Duration>) -> 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<PathBuf> {
self.dirs.find_config_file(Self::EDN)
}
/// Place config file in default location.
fn place_file (&self) -> Result<PathBuf, std::io::Error> {
self.dirs.place_config_file(Self::EDN)
}
/// Get path to config file.
fn get_file (&self) -> Option<PathBuf> {
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<u7> = 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<OsString>, Vec<OsString>)> {
Ok((subdirs, files))
}
/// Default configuration contents.
const CONFIG_DEFAULT: &'static str = include_str!("tek.edn");
pub fn config_init <C: AsRef<Watch> + AsRef<Config>> (config: C) -> Usually<C> {
// 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 <C: AsRef<Config>, L: Language> (state: C, src: L) -> Usually<C> {
state.as_ref().config_clear();
src.each(state, |c, s|config_add(c, s.trim()))
}
/// Load configuration item.
fn config_add <C: AsRef<Config>, L: Language> (state: C, src: L) -> Usually<C> {
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<Option<Arc<str>>>,
/// Timestamp
pub stamp: AtomicU64,
/// Active collection of interaction modes.
pub modes: Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode>>>>,
/// Active collection of view definitions.
pub views: Arc<RwLock<BTreeMap<Arc<str>, Arc<View<Tui, App>>>>>,
/// Active collection of key bindings.
pub keys: Arc<RwLock<BTreeMap<Arc<str>, Arc<Bind<TuiEvent, Arc<str>>>>>>,
/// Active collection of axis bindings.
pub axis: Arc<RwLock<BTreeMap<Arc<str>, Arc<Axis<TuiEvent>>>>>,
}
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<Option<PollWatcher>>,
/// 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<BaseDirectories>) -> 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 <T: 'static> (
callback: impl FnOnce(Arc<Self>)->T,
dirs: impl Into<Option<BaseDirectories>>,
) -> Usually<T> {
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<Self>, poll: Option<Duration>) -> 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<PathBuf> {
self.dirs.find_config_file(Self::EDN)
}
/// Place config file in default location.
fn place_file (&self) -> Result<PathBuf, std::io::Error> {
self.dirs.place_config_file(Self::EDN)
}
/// Get path to config file.
fn get_file (&self) -> Option<PathBuf> {
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<Vec<Arc<str>>>;
fn mode_stack_push (&self, mode: impl AsRef<str>) {
self.mode_stack().write().push(mode.as_ref().into())
}
}
impl HasModeStack for App {
fn mode_stack (&self) -> &RwLock<Vec<Arc<str>>> {
&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<BTreeMap<Arc<str>, Arc<Mode>>>;
/// 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<Mode> = 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<str>) -> Option<Arc<Mode>> {
self.modes().read().get(name.as_ref()).cloned()
}
/// Run something for each mode.
fn modes_for_each <T> (&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<BTreeMap<Arc<str>, Arc<Mode>>> {
&self.config.config.modes
}
}
impl HasModes for Config {
fn modes (&self) -> &RwLock<BTreeMap<Arc<str>, Arc<Mode>>> {
&self.modes
}
}
impl HasModes for Mode {
fn modes (&self) -> &RwLock<BTreeMap<Arc<str>, Arc<Mode>>> {
&self.modes
}
}
pub trait HasViews {
fn views (&self) -> &RwLock<BTreeMap<Arc<str>, Arc<View<Tui, App>>>>;
/// 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<str>) -> Option<Arc<View<Tui, App>>> {
self.views().read().get(name.as_ref()).cloned()
}
fn views_for_each <T> (&self, cb: impl Fn(&Arc<str>, &Arc<View<Tui, App>>)->T) -> () {
for (k, v) in self.views().read().iter() {
cb(k, v);
}
}
}
impl HasViews for App {
fn views (&self) -> &RwLock<BTreeMap<Arc<str>, Arc<View<Tui, App>>>> {
&self.config.config.views
}
}
impl HasViews for Config {
fn views (&self) -> &RwLock<BTreeMap<Arc<str>, Arc<View<Tui, App>>>> {
&self.views
}
}
pub trait HasBinds {
fn binds_keys (&self) -> &RwLock<BTreeMap<Arc<str>, Arc<Bind<TuiEvent, Arc<str>>>>>;
fn binds_axis (&self) -> &RwLock<BTreeMap<Arc<str>, Arc<Axis<TuiEvent>>>>;
/// 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<str>) -> Option<Arc<Bind<TuiEvent, Arc<str>>>> {
self.binds_keys().read().get(id.as_ref()).cloned()
}
/// Iterate over key bindings.
fn binds_for_each <T> (
&self, mut cb: impl FnMut(&Arc<str>, &Bind<TuiEvent, Arc<str>>)->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<BTreeMap<Arc<str>, Arc<Bind<TuiEvent, Arc<str>>>>> {
&self.config.config.keys
}
fn binds_axis (&self) -> &RwLock<BTreeMap<Arc<str>, Arc<Axis<TuiEvent>>>> {
&self.config.config.axis
}
}
impl HasBinds for Config {
fn binds_keys (&self) -> &RwLock<BTreeMap<Arc<str>, Arc<Bind<TuiEvent, Arc<str>>>>> {
&self.keys
}
fn binds_axis (&self) -> &RwLock<BTreeMap<Arc<str>, Arc<Axis<TuiEvent>>>> {
&self.axis
}
}
pub trait DispatchBind<C: Clone>: HasBinds + HasModeStack + HasModes + Namespace<C> {
fn dispatch_bind (&mut self, input: &TuiEvent) -> Usually<()> {
if let Some(bindings) = self.dispatch_bind_match(input) {
@ -1471,7 +1207,7 @@ pub trait DispatchBind<C: Clone>: 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 <C: AsRef<Watch> + AsRef<Config>> (config: C) -> Usually<C> {
// 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 <C: AsRef<Config>, L: Language> (state: C, src: L) -> Usually<C> {
state.as_ref().config_clear();
src.each(state, |c, s|config_add(c, s.trim()))
}
/// Load configuration item.
fn config_add <C: AsRef<Config>, L: Language> (state: C, src: L) -> Usually<C> {
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<Option<Arc<str>>>,
/// Timestamp
pub stamp: AtomicU64,
/// Active collection of interaction modes.
pub modes: Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode>>>>,
/// Active collection of view definitions.
pub views: Arc<RwLock<BTreeMap<Arc<str>, Arc<View<Tui, App>>>>>,
/// Active collection of key bindings.
pub keys: Arc<RwLock<BTreeMap<Arc<str>, Arc<Bind<TuiEvent, Arc<str>>>>>>,
/// Active collection of axis bindings.
pub axis: Arc<RwLock<BTreeMap<Arc<str>, Arc<Axis<TuiEvent>>>>>,
}
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<Watch>, _trace: bool) -> Usually<()> {
fn run (&self, watch: Arc<Watch>, _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<MidiInput>: |self: Project| self.midi_ins);
impl_has!(Vec<MidiOutput>: |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<MidiEditor>: |self: App|self.project.as_mut());
impl_as_mut!(Option<Project>: |self: App|todo!());
impl_as_ref!(Option<MidiEditor>: |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<Tui> for App {
fn draw (&self, to: &mut Tui) -> Drawn<u16> {
#[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()))
})
}
}