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
628
src/tek.rs
628
src/tek.rs
|
|
@ -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()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue