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

@ -1,5 +1,71 @@
use crate::*; use crate::*;
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.watch.config.keys
}
fn binds_axis (&self) -> &RwLock<BTreeMap<Arc<str>, Arc<Axis<TuiEvent>>>> {
&self.watch.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
}
}
/// A map of input events (e.g. [TuiEvent]) to [Binding]s. /// A map of input events (e.g. [TuiEvent]) to [Binding]s.
/// ///
/// ``` /// ```

View file

@ -1,5 +1,94 @@
use crate::*; use crate::*;
/// 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.watch.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
}
}
impl_as_mut!(Mode: |self: Mode| self); impl_as_mut!(Mode: |self: Mode| self);
/// Group of view and keys definitions. /// Group of view and keys definitions.

View file

@ -1,5 +1,38 @@
use crate::*; use crate::*;
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.watch.config.views
}
}
impl HasViews for Config {
fn views (&self) -> &RwLock<BTreeMap<Arc<str>, Arc<View<Tui, App>>>> {
&self.views
}
}
impl_debug!(<S: Screen, T> View<S, T>: |self, w| { write!(w, "View({})", self.source) }); impl_debug!(<S: Screen, T> View<S, T>: |self, w| { write!(w, "View({})", self.source) });
impl_display!(<S: Screen, T> View<S, T>: |self, w| { write!(w, "View({})", self.source) }); impl_display!(<S: Screen, T> View<S, T>: |self, w| { write!(w, "View({})", self.source) });
impl_default!(ClockView: { impl_default!(ClockView: {
@ -25,24 +58,6 @@ pub struct View<S: Screen, T> {
pub render: Arc<Box<dyn Fn(&T, &mut S)->Drawn<S::Unit> + Send + Sync>> pub render: Arc<Box<dyn Fn(&T, &mut S)->Drawn<S::Unit> + 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<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.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<Option<Arc<str>>>) -> Usually<()> { pub fn draw_error (to: &mut Tui, error: &RwLock<Option<Arc<str>>>) -> Usually<()> {
if let Some(e) = error.read().as_ref() { if let Some(e) = error.read().as_ref() {
e.as_ref().align_c().draw(to)?; e.as_ref().align_c().draw(to)?;
@ -50,22 +65,23 @@ pub fn draw_error (to: &mut Tui, error: &RwLock<Option<Arc<str>>>) -> Usually<()
Ok(()) Ok(())
} }
pub fn view_debug (perf: &PerfModel, size: &Sizer) -> impl Draw<Tui> { pub fn view_debug (
stack: &Vec<Arc<str>>,
perf: &PerfModel,
size: &Sizer,
) -> impl Draw<Tui> {
Split::Above.bar(0, Split::Above.bar(0,
Split::West.bar(16, Split::West.bar(16,
format!("{}/{} {} ", format!("{}/{} {} ",
perf.used.load(Relaxed), perf.used.load(Relaxed),
perf.window.load(Relaxed), perf.window.load(Relaxed),
perf.clock.raw() / 10000000).align_e(), perf.clock.raw() / 10000000).align_e(),
format!("{}x{} ", format!("{:03}x{:03} ",
size.0.load(Relaxed), size.0.load(Relaxed),
size.1.load(Relaxed)).align_w(), size.1.load(Relaxed)).align_w(),
).align_se(), ).align_se(),
() //()
//Split::East.bar(16, format!("{:?}", stack).align_sw(),
//format!("{}", self.modes.read()[0]),
//format!("{}", self.modes.read()[0])
//).align_sw()
) )
} }
@ -95,6 +111,9 @@ pub fn draw_status_bar (to: &mut Tui, state: &App) -> Drawn<u16> {
pub fn draw_mode_stack (to: &mut Tui, state: &App) -> Usually<()> { pub fn draw_mode_stack (to: &mut Tui, state: &App) -> Usually<()> {
for name in state.mode_stack().read().iter() { for name in state.mode_stack().read().iter() {
let mut error = false; 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) { if let Some(mode) = state.modes_get(name).as_ref().map(Arc::clone) {
for (index, view) in mode.view.iter().enumerate() { for (index, view) in mode.view.iter().enumerate() {
match (view.render)(state, to) { match (view.render)(state, to) {

View file

@ -112,7 +112,7 @@ pub struct App {
/// Performance counter /// Performance counter
pub perf: PerfModel, pub perf: PerfModel,
/// Available view modes and input bindings /// Available view modes and input bindings
pub config: Arc<Watch>, pub watch: Arc<Watch>,
/// Currently active modes /// Currently active modes
pub stack: Arc<RwLock<Vec<Arc<str>>>>, pub stack: Arc<RwLock<Vec<Arc<str>>>>,
/// Undo history /// Undo history
@ -152,7 +152,7 @@ impl App {
pub fn new ( pub fn new (
exit: Option<Exit>, exit: Option<Exit>,
project: Project, project: Project,
config: impl Into<Option<Arc<Watch>>>, watch: impl Into<Option<Arc<Watch>>>,
mode: impl AsRef<str> mode: impl AsRef<str>
) -> Self { ) -> Self {
App { App {
@ -161,57 +161,93 @@ impl App {
color: ItemTheme::random(), color: ItemTheme::random(),
dialog: Dialog::welcome(), dialog: Dialog::welcome(),
stack: 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(), watch: watch.into().unwrap_or_default(),
project, project,
..Default::default() ..Default::default()
} }
} }
} }
fn tek_jack_process (state: &mut App, client: &Client, scope: &ProcessScope) -> Control { /// Configuration watcher and manager.
let t0 = state.perf.get_t0(); ///
state.clock().update_from_scope(scope).unwrap(); /// ```
let midi_in = state.project.midi_input_collect(scope); /// let config = tek::Watch::new(None);
if let Some(editor) = &state.editor() { /// ```
let mut pitch: Option<u7> = None; #[derive(Default, Debug)]
for port in midi_in.iter() { pub struct Watch {
for event in port.iter() { /// XDG base directories of running user.
if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..})) pub dirs: BaseDirectories,
= event /// Watcher
{ pub watch: RwLock<Option<PollWatcher>>,
pitch = Some(key.clone()); /// App configuration
} pub config: Config,
}
}
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) { impl Watch {
use JackEvent::*; /// Default configuration directory.
match event { const DIR: &'static str = "tek";
SampleRate(sr) => { state.clock().timebase.sr.set(sr as f64); }, /// Default configuration subdirectory.
PortRegistration(_id, true) => { const SUB: &'static str = "v0";
//let port = self.jack().port_by_id(id); /// Default configuration file name.
//println!("\rport add: {id} {port:?}"); const EDN: &'static str = "tek.edn";
//println!("\rport add: {id}"); /// 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");
}, },
PortRegistration(_id, false) => { Err(errors) => {
/*println!("\rport del: {id}")*/ panic!("{errors:?}");
}, }
PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ }, }
PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ }, }
ClientRegistration(_id, true) => {}, }, opts)?;
ClientRegistration(_id, false) => {}, if let Some(path) = config.get_file() {
ThreadInit => {}, //println!("watching: {path:?}");
XRun => {}, watcher.watch(&path, RecursiveMode::NonRecursive)?;
GraphReorder => {}, *config.as_ref().watch.try_write().unwrap() = Some(watcher);
_ => { panic!("{event:?}"); } 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, #[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 { impl Project {
pub fn new (jack: &Jack<'static>) -> Self { pub fn new (jack: &Jack<'static>) -> Self {
Self { Self {
@ -600,7 +683,6 @@ impl Scene {
} }
} }
pub type SceneWith<'a, T> = (usize, &'a Scene, usize, usize, T);
impl ScenesView for App { impl ScenesView for App {
fn w_mid (&self) -> u16 { fn w_mid (&self) -> u16 {
(self.size.w() as u16).saturating_sub(self.w_side()) (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) (self.size.h() as u16).saturating_sub(20)
} }
} }
impl HasScenes for App { impl HasScenes for App {
fn scenes_scroll (&self) -> usize { fn scenes_scroll (&self) -> usize {
self.project.scenes_scroll() self.project.scenes_scroll()
@ -1108,353 +1191,6 @@ pub fn scan (dir: &PathBuf) -> Usually<(Vec<OsString>, Vec<OsString>)> {
Ok((subdirs, files)) 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> { pub trait DispatchBind<C: Clone>: HasBinds + HasModeStack + HasModes + Namespace<C> {
fn dispatch_bind (&mut self, input: &TuiEvent) -> Usually<()> { fn dispatch_bind (&mut self, input: &TuiEvent) -> Usually<()> {
if let Some(bindings) = self.dispatch_bind_match(input) { 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)) /// (bind Confirm (@enter confirm))
/// (mode Test (axis Y Index) (bind (@escape Cancel) Confirm)) /// (mode Test (axis Y Index) (bind (@escape Cancel) Confirm))
/// )).unwrap(); /// )).unwrap();
/// println!("Modes: {:#?}", app.config.config.modes); /// println!("Modes: {:#?}", app.watch.config.modes);
/// ///
/// // No mode is active: /// // No mode is active:
/// assert!(app.dispatch_bind_match(&"@left".into()).is_none()); /// assert!(app.dispatch_bind_match(&"@left".into()).is_none());
@ -1620,7 +1356,7 @@ pub use self::conf::*; mod conf {
} }
impl HasConfig for App { impl HasConfig for App {
fn config (&self) -> &Config { fn config (&self) -> &Config {
&self.config.config &self.watch.config
} }
} }
impl HasConfig for Watch { impl HasConfig for Watch {
@ -1633,6 +1369,82 @@ pub use self::conf::*; mod conf {
self 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 { pub use self::gear::*; mod gear {
@ -1731,18 +1543,18 @@ pub use self::gear::*; mod gear {
} }
impl Action { impl Action {
fn run (&self, config: Arc<Watch>, _trace: bool) -> Usually<()> { fn run (&self, watch: Arc<Watch>, _trace: bool) -> Usually<()> {
use Action::*; use Action::*;
match self { match self {
Version => show_version(), Version => show_version(),
Config => config.config_print(), Config => watch.config_print(),
Resume => todo!("resume session"), Resume => todo!("resume session"),
List => todo!("list sessions"), List => todo!("list sessions"),
New(sesh) => Exit::run(|exit|Tui::run_main( New(sesh) => Exit::run(|exit|Tui::run_main(
exit.clone(), exit.clone(),
{ {
#[allow(unused_mut)] #[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(); app.dialog = Dialog::welcome();
#[cfg(feature = "prof2")] { #[cfg(feature = "prof2")] {
if trace { 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<MidiInput>: |self: Project| self.midi_ins);
impl_has!(Vec<MidiOutput>: |self: Project| self.midi_outs); 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<MidiEditor>: |self: App|self.project.as_mut());
impl_as_mut!(Option<Project>: |self: App|todo!()); impl_as_mut!(Option<Project>: |self: App|todo!());
impl_as_ref!(Option<MidiEditor>: |self: App|self.project.as_ref()); 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!"); #[cfg(feature = "prof2")] profiling::scope!("App::tui_keys!");
self.dispatch_bind(input) 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()))
})
}
}