mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-10-08 18:56:42 +02:00
This commit is contained in:
parent
02dc7bc9f8
commit
914e3e094b
4 changed files with 428 additions and 424 deletions
|
|
@ -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.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
628
src/tek.rs
628
src/tek.rs
|
|
@ -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,
|
||||||
PortRegistration(_id, false) => {
|
pub fn new (dirs: Option<BaseDirectories>) -> Self {
|
||||||
/*println!("\rport del: {id}")*/
|
Self {
|
||||||
},
|
dirs: dirs.unwrap_or_else(||BaseDirectories::with_profile(Self::DIR, Self::SUB)),
|
||||||
PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ },
|
..Default::default()
|
||||||
PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ },
|
}
|
||||||
ClientRegistration(_id, true) => {},
|
}
|
||||||
ClientRegistration(_id, false) => {},
|
/// Create, initialize, and watch a new configuration.
|
||||||
ThreadInit => {},
|
pub fn run_watched <T: 'static> (
|
||||||
XRun => {},
|
callback: impl FnOnce(Arc<Self>)->T,
|
||||||
GraphReorder => {},
|
dirs: impl Into<Option<BaseDirectories>>,
|
||||||
_ => { panic!("{event:?}"); }
|
) -> 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,
|
#[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()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue