mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 11:46:41 +01:00
Compare commits
3 commits
fa5f67f010
...
5ccbb9719f
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ccbb9719f | |||
| 17c6184f0e | |||
| d5865d941f |
12 changed files with 463 additions and 344 deletions
24
Cargo.lock
generated
24
Cargo.lock
generated
|
|
@ -2390,6 +2390,7 @@ dependencies = [
|
||||||
"proptest",
|
"proptest",
|
||||||
"proptest-derive",
|
"proptest-derive",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
"tek_config",
|
||||||
"tek_device",
|
"tek_device",
|
||||||
"tek_engine",
|
"tek_engine",
|
||||||
"tengri",
|
"tengri",
|
||||||
|
|
@ -2406,6 +2407,14 @@ dependencies = [
|
||||||
"tek",
|
"tek",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tek_config"
|
||||||
|
version = "0.2.1"
|
||||||
|
dependencies = [
|
||||||
|
"tengri",
|
||||||
|
"xdg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tek_device"
|
name = "tek_device"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|
@ -2447,7 +2456,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tengri"
|
name = "tengri"
|
||||||
version = "0.13.0"
|
version = "0.14.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"tengri_core",
|
"tengri_core",
|
||||||
"tengri_dsl",
|
"tengri_dsl",
|
||||||
|
|
@ -2458,11 +2467,11 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tengri_core"
|
name = "tengri_core"
|
||||||
version = "0.13.0"
|
version = "0.14.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tengri_dsl"
|
name = "tengri_dsl"
|
||||||
version = "0.13.0"
|
version = "0.14.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"const_panic",
|
"const_panic",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
|
|
@ -2473,15 +2482,14 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tengri_input"
|
name = "tengri_input"
|
||||||
version = "0.13.0"
|
version = "0.14.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"tengri_core",
|
"tengri_core",
|
||||||
"tengri_dsl",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tengri_output"
|
name = "tengri_output"
|
||||||
version = "0.13.0"
|
version = "0.14.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"tengri_core",
|
"tengri_core",
|
||||||
"tengri_dsl",
|
"tengri_dsl",
|
||||||
|
|
@ -2489,7 +2497,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tengri_proc"
|
name = "tengri_proc"
|
||||||
version = "0.13.0"
|
version = "0.14.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
|
@ -2500,7 +2508,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tengri_tui"
|
name = "tengri_tui"
|
||||||
version = "0.13.0"
|
version = "0.14.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic_float",
|
"atomic_float",
|
||||||
"better-panic",
|
"better-panic",
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"./crates/engine",
|
"./crates/engine",
|
||||||
"./crates/device",
|
"./crates/device",
|
||||||
|
"./crates/config",
|
||||||
"./crates/app",
|
"./crates/app",
|
||||||
"./crates/cli",
|
"./crates/cli",
|
||||||
]
|
]
|
||||||
|
|
@ -34,6 +35,7 @@ path = "./deps/rust-jack"
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
tek_device = { path = "./crates/device" }
|
tek_device = { path = "./crates/device" }
|
||||||
tek_engine = { path = "./crates/engine" }
|
tek_engine = { path = "./crates/engine" }
|
||||||
|
tek_config = { path = "./crates/config" }
|
||||||
tek = { path = "./crates/app" }
|
tek = { path = "./crates/app" }
|
||||||
tek_cli = { path = "./crates/cli" }
|
tek_cli = { path = "./crates/cli" }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,9 @@ version = { workspace = true }
|
||||||
tengri = { workspace = true }
|
tengri = { workspace = true }
|
||||||
tengri_proc = { workspace = true }
|
tengri_proc = { workspace = true }
|
||||||
|
|
||||||
tek_engine = { workspace = true }
|
tek_config = { workspace = true }
|
||||||
tek_device = { workspace = true }
|
tek_device = { workspace = true }
|
||||||
|
tek_engine = { workspace = true }
|
||||||
|
|
||||||
backtrace = { workspace = true }
|
backtrace = { workspace = true }
|
||||||
clap = { workspace = true, optional = true }
|
clap = { workspace = true, optional = true }
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,12 @@
|
||||||
// ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
|
||||||
//██Let me play the world's tiniest piano for you. ██
|
|
||||||
//█▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀█
|
|
||||||
//█▙▙█▙▙▙█▙▙█▙▙▙█▙▙█▙▙▙█▙▙█▙▙▙█▙▙█▙▙▙█▙▙█▙▙▙█▙▙█▙▙▙██
|
|
||||||
//█▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄█
|
|
||||||
//███████████████████████████████████████████████████
|
|
||||||
//█ ▀ ▀ ▀ █
|
|
||||||
#![allow(unused, clippy::unit_arg)]
|
#![allow(unused, clippy::unit_arg)]
|
||||||
#![feature(adt_const_params, associated_type_defaults, if_let_guard, impl_trait_in_assoc_type,
|
#![feature(adt_const_params, associated_type_defaults, if_let_guard, impl_trait_in_assoc_type,
|
||||||
type_alias_impl_trait, trait_alias, type_changing_struct_update, closure_lifetime_binder)]
|
type_alias_impl_trait, trait_alias, type_changing_struct_update, closure_lifetime_binder)]
|
||||||
pub use ::{
|
|
||||||
tek_engine::*,
|
|
||||||
tek_device,
|
|
||||||
tek_device::*,
|
|
||||||
tengri::{
|
|
||||||
Usually, Perhaps, Has, MaybeHas, has, maybe_has,
|
|
||||||
dsl::*, input::*, output::*, tui::*,
|
|
||||||
tui::ratatui,
|
|
||||||
tui::ratatui::prelude::buffer::Cell,
|
|
||||||
tui::ratatui::prelude::Color::{self, *},
|
|
||||||
tui::ratatui::prelude::{Style, Stylize, Buffer, Modifier},
|
|
||||||
tui::crossterm,
|
|
||||||
tui::crossterm::event::{Event, KeyCode::{self, *}},
|
|
||||||
},
|
|
||||||
std::{
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
sync::{Arc, RwLock},
|
|
||||||
sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed},
|
|
||||||
error::Error,
|
|
||||||
collections::BTreeMap,
|
|
||||||
fmt::Write,
|
|
||||||
},
|
|
||||||
xdg::BaseDirectories,
|
|
||||||
};
|
|
||||||
mod app_jack; pub use self::app_jack::*;
|
|
||||||
#[cfg(test)] mod app_test;
|
#[cfg(test)] mod app_test;
|
||||||
|
mod app_deps; pub use self::app_deps::*;
|
||||||
|
mod app_jack; pub use self::app_jack::*;
|
||||||
|
mod app_bind; pub use self::app_bind::*;
|
||||||
|
mod app_data; pub use self::app_data::*;
|
||||||
|
mod app_view; pub use self::app_view::*;
|
||||||
/// Total state
|
/// Total state
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
pub struct App {
|
pub struct App {
|
||||||
|
|
@ -58,23 +31,6 @@ pub struct App {
|
||||||
/// Base color.
|
/// Base color.
|
||||||
pub color: ItemTheme,
|
pub color: ItemTheme,
|
||||||
}
|
}
|
||||||
/// Configuration
|
|
||||||
#[derive(Default, Debug)]
|
|
||||||
pub struct Config {
|
|
||||||
pub dirs: BaseDirectories,
|
|
||||||
pub modes: Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode<Arc<str>>>>>>,
|
|
||||||
pub views: Arc<RwLock<BTreeMap<Arc<str>, Arc<str>>>>,
|
|
||||||
pub binds: Arc<RwLock<BTreeMap<Arc<str>, EventMap<TuiEvent, Arc<str>>>>>,
|
|
||||||
}
|
|
||||||
#[derive(Default, Debug)]
|
|
||||||
pub struct Mode<D: Dsl + Ord> {
|
|
||||||
pub path: PathBuf,
|
|
||||||
pub name: Vec<D>,
|
|
||||||
pub info: Vec<D>,
|
|
||||||
pub view: Vec<D>,
|
|
||||||
pub keys: Vec<D>,
|
|
||||||
pub modes: BTreeMap<D, Mode<D>>,
|
|
||||||
}
|
|
||||||
/// Various possible dialog modes.
|
/// Various possible dialog modes.
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub enum Dialog {
|
pub enum Dialog {
|
||||||
|
|
@ -86,107 +42,6 @@ pub enum Dialog {
|
||||||
Browser(BrowserTarget, Arc<Browser>),
|
Browser(BrowserTarget, Arc<Browser>),
|
||||||
Options,
|
Options,
|
||||||
}
|
}
|
||||||
impl Config {
|
|
||||||
const CONFIG: &'static str = "tek.edn";
|
|
||||||
const DEFAULTS: &'static str = include_str!("../../tek.edn");
|
|
||||||
pub fn init () -> Usually<Self> {
|
|
||||||
let mut cfgs: Self = Default::default();
|
|
||||||
cfgs.dirs = BaseDirectories::with_profile("tek", "v0");
|
|
||||||
cfgs.init_file(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|cfgs.load(dsl))?;
|
|
||||||
Ok(cfgs)
|
|
||||||
}
|
|
||||||
pub fn init_file (
|
|
||||||
&mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()>
|
|
||||||
) -> Usually<()> {
|
|
||||||
if self.dirs.find_config_file(path).is_none() {
|
|
||||||
println!("Creating {path:?}");
|
|
||||||
std::fs::write(self.dirs.place_config_file(path)?, defaults);
|
|
||||||
}
|
|
||||||
Ok(if let Some(path) = self.dirs.find_config_file(path) {
|
|
||||||
println!("Loading {path:?}");
|
|
||||||
let src = std::fs::read_to_string(&path)?;
|
|
||||||
src.as_str().each(move|item|each(self, item))?;
|
|
||||||
} else {
|
|
||||||
return Err(format!("{path}: not found").into())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
pub fn load (&mut self, dsl: impl Dsl) -> Usually<()> {
|
|
||||||
dsl.each(|item|if let Some(expr) = item.expr()? {
|
|
||||||
let head = expr.head()?;
|
|
||||||
let tail = expr.tail()?;
|
|
||||||
let name = tail.head()?;
|
|
||||||
let body = tail.tail()?;
|
|
||||||
println!("{} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default());
|
|
||||||
match head {
|
|
||||||
Some("view") if let Some(name) = name => self.load_view(name.into(), body),
|
|
||||||
Some("keys") if let Some(name) = name => self.load_bind(name.into(), body),
|
|
||||||
Some("mode") if let Some(name) = name => self.load_mode(name.into(), body),
|
|
||||||
_ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Err(format!("Config::load: expected expr, got: {item:?}").into())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
pub fn load_view (&mut self, id: Arc<str>, dsl: impl Dsl) -> Usually<()> {
|
|
||||||
self.views.write().unwrap().insert(id, dsl.src()?.unwrap_or_default().into());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
pub fn load_bind (&mut self, id: Arc<str>, dsl: impl Dsl) -> Usually<()> {
|
|
||||||
let mut map = EventMap::new();
|
|
||||||
dsl.each(|item|if item.expr().head() == Ok(Some("see")) {
|
|
||||||
// TODO
|
|
||||||
Ok(())
|
|
||||||
} else if let Ok(Some(word)) = item.expr().head().word() {
|
|
||||||
if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? {
|
|
||||||
map.add(key, Binding {
|
|
||||||
commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(),
|
|
||||||
condition: None,
|
|
||||||
description: None,
|
|
||||||
source: None
|
|
||||||
});
|
|
||||||
Ok(())
|
|
||||||
} else if Some(":char") == item.expr()?.head()? {
|
|
||||||
// TODO
|
|
||||||
return Ok(())
|
|
||||||
} else {
|
|
||||||
return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Err(format!("Config::load_bind: unexpected: {item:?}").into())
|
|
||||||
})?;
|
|
||||||
self.binds.write().unwrap().insert(id, map);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
pub fn load_mode (&mut self, id: Arc<str>, dsl: impl Dsl) -> Usually<()> {
|
|
||||||
let mut mode = Mode::default();
|
|
||||||
dsl.each(|item|Self::load_mode_one(&mut mode, item))?;
|
|
||||||
self.modes.write().unwrap().insert(id.into(), Arc::new(mode));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
pub fn load_mode_one (mode: &mut Mode<Arc<str>>, item: impl Dsl) -> Usually<()> {
|
|
||||||
Ok(if let Ok(Some(key)) = item.expr().head() {
|
|
||||||
match key {
|
|
||||||
"name" => mode.name.push(item.expr()?.tail()?.map(|x|x.trim()).unwrap_or("").into()),
|
|
||||||
"info" => mode.info.push(item.expr()?.tail()?.map(|x|x.trim()).unwrap_or("").into()),
|
|
||||||
"keys" => {
|
|
||||||
item.expr()?.tail()?.each(|item|{mode.keys.push(item.trim().into()); Ok(())})?;
|
|
||||||
},
|
|
||||||
"mode" => if let Some(id) = item.expr()?.tail()?.head()? {
|
|
||||||
let mut submode = Mode::default();
|
|
||||||
Self::load_mode_one(&mut submode, item.expr()?.tail()?.tail()?)?;
|
|
||||||
mode.modes.insert(id.into(), submode);
|
|
||||||
} else {
|
|
||||||
return Err(format!("load_mode_one: incomplete: {item:?}").into());
|
|
||||||
},
|
|
||||||
_ => mode.view.push(item.expr()?.unwrap().into()),
|
|
||||||
}
|
|
||||||
} else if let Ok(Some(word)) = item.word() {
|
|
||||||
mode.view.push(word.into());
|
|
||||||
} else {
|
|
||||||
return Err(format!("load_mode_one: unexpected: {item:?}").into());
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
has!(Jack<'static>: |self: App|self.jack);
|
has!(Jack<'static>: |self: App|self.jack);
|
||||||
has!(Pool: |self: App|self.pool);
|
has!(Pool: |self: App|self.pool);
|
||||||
has!(Dialog: |self: App|self.dialog);
|
has!(Dialog: |self: App|self.dialog);
|
||||||
|
|
@ -228,7 +83,7 @@ fn render_dsl <'t> (
|
||||||
state: &'t impl DslNs<'t, Box<dyn Render<TuiOut>>>,
|
state: &'t impl DslNs<'t, Box<dyn Render<TuiOut>>>,
|
||||||
src: &str
|
src: &str
|
||||||
) -> Box<dyn Render<TuiOut>> {
|
) -> Box<dyn Render<TuiOut>> {
|
||||||
let err: Option<Box<dyn Error>> = match state.from(src) {
|
let err: Option<Box<dyn Error>> = match state.from(&src) {
|
||||||
Ok(Some(value)) => return value, Ok(None) => None, Err(e) => Some(e),
|
Ok(Some(value)) => return value, Ok(None) => None, Err(e) => Some(e),
|
||||||
};
|
};
|
||||||
let (fg_1, bg_1) = (Color::Rgb(240, 160, 100), Color::Rgb(48, 0, 0));
|
let (fg_1, bg_1) = (Color::Rgb(240, 160, 100), Color::Rgb(48, 0, 0));
|
||||||
|
|
@ -265,8 +120,6 @@ handle!(TuiIn:|self: App, input|{
|
||||||
}
|
}
|
||||||
Ok(None)
|
Ok(None)
|
||||||
});
|
});
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum AppCommand { /* TODO */ }
|
|
||||||
impl Dialog {
|
impl Dialog {
|
||||||
pub fn menu_selected (&self) -> Option<usize> {
|
pub fn menu_selected (&self) -> Option<usize> {
|
||||||
if let Self::Menu(selected) = self { Some(*selected) } else { None }
|
if let Self::Menu(selected) = self { Some(*selected) } else { None }
|
||||||
|
|
@ -290,17 +143,6 @@ impl Dialog {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[tengri_proc::command(Option<Dialog>)] impl DialogCommand {
|
|
||||||
fn open (dialog: &mut Option<Dialog>, new: Dialog) -> Perhaps<Self> {
|
|
||||||
*dialog = Some(new);
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
fn close (dialog: &mut Option<Dialog>) -> Perhaps<Self> {
|
|
||||||
*dialog = None;
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub fn editor_focused (&self) -> bool {
|
pub fn editor_focused (&self) -> bool {
|
||||||
false
|
false
|
||||||
|
|
@ -384,107 +226,6 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
dsl_ns! { |app: App|
|
|
||||||
|
|
||||||
bool => {
|
|
||||||
":focused/editor" => app.project.editor.is_some(),
|
|
||||||
":focused/dialog" => !matches!(app.dialog, Dialog::None),
|
|
||||||
":focused/message" => matches!(app.dialog, Dialog::Message(..)),
|
|
||||||
":focused/add_device" => matches!(app.dialog, Dialog::Device(..)),
|
|
||||||
":focused/browser" => app.dialog.browser().is_some(),
|
|
||||||
":focused/pool/import" => matches!(app.pool.mode, Some(PoolMode::Import(..))),
|
|
||||||
":focused/pool/export" => matches!(app.pool.mode, Some(PoolMode::Export(..))),
|
|
||||||
":focused/pool/rename" => matches!(app.pool.mode, Some(PoolMode::Rename(..))),
|
|
||||||
":focused/pool/length" => matches!(app.pool.mode, Some(PoolMode::Length(..))),
|
|
||||||
":focused/clip" => !app.editor_focused() && matches!(app.selection(),
|
|
||||||
Selection::TrackClip{..}),
|
|
||||||
":focused/track" => !app.editor_focused() && matches!(app.selection(),
|
|
||||||
Selection::Track(..)),
|
|
||||||
":focused/scene" => !app.editor_focused() && matches!(app.selection(),
|
|
||||||
Selection::Scene(..)),
|
|
||||||
":focused/mix" => !app.editor_focused() && matches!(app.selection(),
|
|
||||||
Selection::Mix),
|
|
||||||
};
|
|
||||||
|
|
||||||
ItemTheme => {
|
|
||||||
":_theme_stub" => Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
Dialog => {
|
|
||||||
":dialog/none" => Dialog::None,
|
|
||||||
":dialog/options" => Dialog::Options,
|
|
||||||
":dialog/device" => Dialog::Device(0),
|
|
||||||
":dialog/device/prev" => Dialog::Device(0),
|
|
||||||
":dialog/device/next" => Dialog::Device(0),
|
|
||||||
":dialog/help" => Dialog::Help(0),
|
|
||||||
":dialog/menu" => Dialog::Menu(0),
|
|
||||||
":dialog/save" => Dialog::Browser(BrowserTarget::SaveProject,
|
|
||||||
Browser::new(None).unwrap().into()),
|
|
||||||
":dialog/load" => Dialog::Browser(BrowserTarget::LoadProject,
|
|
||||||
Browser::new(None).unwrap().into()),
|
|
||||||
":dialog/import/clip" => Dialog::Browser(BrowserTarget::ImportClip(Default::default()),
|
|
||||||
Browser::new(None).unwrap().into()),
|
|
||||||
":dialog/export/clip" => Dialog::Browser(BrowserTarget::ExportClip(Default::default()),
|
|
||||||
Browser::new(None).unwrap().into()),
|
|
||||||
":dialog/import/sample" => Dialog::Browser(BrowserTarget::ImportSample(Default::default()),
|
|
||||||
Browser::new(None).unwrap().into()),
|
|
||||||
":dialog/export/sample" => Dialog::Browser(BrowserTarget::ExportSample(Default::default()),
|
|
||||||
Browser::new(None).unwrap().into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
Selection => {
|
|
||||||
":select/scene" => app.selection().select_scene(app.tracks().len()),
|
|
||||||
":select/scene/next" => app.selection().select_scene_next(app.scenes().len()),
|
|
||||||
":select/scene/prev" => app.selection().select_scene_prev(),
|
|
||||||
":select/track" => app.selection().select_track(app.tracks().len()),
|
|
||||||
":select/track/next" => app.selection().select_track_next(app.tracks().len()),
|
|
||||||
":select/track/prev" => app.selection().select_track_prev(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Option<u7> => {
|
|
||||||
":editor/pitch" => Some(
|
|
||||||
(app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into()
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
Option<usize> => {
|
|
||||||
":selected/scene" => app.selection().scene(),
|
|
||||||
":selected/track" => app.selection().track(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Option<Arc<RwLock<MidiClip>>> => {
|
|
||||||
":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() {
|
|
||||||
app.scenes()[*scene].clips[*track].clone()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Color => {
|
|
||||||
("g", n: u8) => Color::Rgb(n, n, n),
|
|
||||||
("rgb", red: u8, green: u8, blue: u8) => Color::Rgb(red, green, blue),
|
|
||||||
":color/bg" => Color::Rgb(28, 32, 36),
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
dsl_ns! { num |app: App|
|
|
||||||
u8;
|
|
||||||
u16 => {
|
|
||||||
":w/sidebar" => app.project.w_sidebar(app.editor().is_some()),
|
|
||||||
":h/sample-detail" => 6.max(app.height() as u16 * 3 / 9),
|
|
||||||
};
|
|
||||||
usize => {
|
|
||||||
":scene-count" => app.scenes().len(),
|
|
||||||
":track-count" => app.tracks().len(),
|
|
||||||
":device-kind" => app.dialog.device_kind().unwrap_or(0),
|
|
||||||
":device-kind/next" => app.dialog.device_kind_next().unwrap_or(0),
|
|
||||||
":device-kind/prev" => app.dialog.device_kind_prev().unwrap_or(0),
|
|
||||||
};
|
|
||||||
isize;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn wrap_dialog (dialog: impl Content<TuiOut>) -> impl Content<TuiOut> {
|
fn wrap_dialog (dialog: impl Content<TuiOut>) -> impl Content<TuiOut> {
|
||||||
Fixed::xy(70, 23, Tui::fg_bg(Rgb(255,255,255), Rgb(16,16,16), Bsp::b(
|
Fixed::xy(70, 23, Tui::fg_bg(Rgb(255,255,255), Rgb(16,16,16), Bsp::b(
|
||||||
Repeat(" "), Outer(true, Style::default().fg(Tui::g(96))).enclose(dialog))))
|
Repeat(" "), Outer(true, Style::default().fg(Tui::g(96))).enclose(dialog))))
|
||||||
|
|
@ -495,38 +236,6 @@ impl ScenesView for App {
|
||||||
fn h_scenes (&self) -> u16 { (self.height() as u16).saturating_sub(20) }
|
fn h_scenes (&self) -> u16 { (self.height() as u16).saturating_sub(20) }
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules!dsl_words((|$state:ident|->$Type:ty$({
|
|
||||||
$($word:literal => $body:expr),* $(,)?
|
|
||||||
})?)=>{
|
|
||||||
const WORDS: DslNsMap<'t, fn (&'t Self)->Perhaps<$Type>> = DslNsMap::new(&[$(
|
|
||||||
$(($word, |$state: &Self|{Ok(Some($body))})),*
|
|
||||||
)? ]);
|
|
||||||
});
|
|
||||||
macro_rules!dsl_exprs((|$state:ident|->$Type:ty$({
|
|
||||||
$($name:literal ($($arg:ident:$ty:ty),* $(,)?) => $body:expr),* $(,)?
|
|
||||||
})?)=>{
|
|
||||||
const EXPRS: DslNsMap<'t, fn (&'t Self, &str)->Perhaps<$Type>> = DslNsMap::new(&[$(
|
|
||||||
$(($name, |$state: &Self, tail: &str|{
|
|
||||||
$(
|
|
||||||
let head = tail.head()?.unwrap_or_default();
|
|
||||||
let tail = tail.tail()?.unwrap_or_default();
|
|
||||||
let $arg: $ty = if let Some(arg) = $state.from(&head)? {
|
|
||||||
arg
|
|
||||||
} else {
|
|
||||||
return Err(format!("{}: arg \"{}\" ({}) got: {head}, remaining: {tail}",
|
|
||||||
$name,
|
|
||||||
stringify!($arg),
|
|
||||||
stringify!($ty),
|
|
||||||
).into())
|
|
||||||
};
|
|
||||||
)*
|
|
||||||
Ok(Some($body))
|
|
||||||
})),*
|
|
||||||
)? ]);
|
|
||||||
});
|
|
||||||
|
|
||||||
mod app_view; pub use self::app_view::*;
|
|
||||||
mod app_bind; pub use self::app_bind::*;
|
|
||||||
|
|
||||||
//#[dizzle::ns]
|
//#[dizzle::ns]
|
||||||
//#[dizzle::ns::include(TuiOut)]
|
//#[dizzle::ns::include(TuiOut)]
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
impl<'t> DslNs<'t, AppCommand> for App {
|
impl<'t> DslNs<'t, AppCommand> for App {
|
||||||
dsl_exprs!(|app| -> AppCommand {
|
dsl_exprs!(|app| -> AppCommand { /* TODO */ });
|
||||||
});
|
|
||||||
dsl_words!(|app| -> AppCommand {
|
dsl_words!(|app| -> AppCommand {
|
||||||
"x/inc" => todo!(),
|
"x/inc" => todo!(),
|
||||||
"x/dec" => todo!(),
|
"x/dec" => todo!(),
|
||||||
|
|
@ -11,6 +10,21 @@ impl<'t> DslNs<'t, AppCommand> for App {
|
||||||
"confirm" => todo!(),
|
"confirm" => todo!(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum AppCommand { /* TODO */ }
|
||||||
|
|
||||||
|
#[tengri_proc::command(Option<Dialog>)]
|
||||||
|
impl DialogCommand {
|
||||||
|
fn open (dialog: &mut Option<Dialog>, new: Dialog) -> Perhaps<Self> {
|
||||||
|
*dialog = Some(new);
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
fn close (dialog: &mut Option<Dialog>) -> Perhaps<Self> {
|
||||||
|
*dialog = None;
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//AppCommand => {
|
//AppCommand => {
|
||||||
//("x/inc" /
|
//("x/inc" /
|
||||||
|
|
|
||||||
122
crates/app/app_data.rs
Normal file
122
crates/app/app_data.rs
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
impl<'t> DslNs<'t, Arc<str>> for App {
|
||||||
|
dsl_exprs!(|app| -> Arc<str> {});
|
||||||
|
dsl_words!(|app| -> Arc<str> {});
|
||||||
|
fn from_literal <D: Dsl> (&self, dsl: &D) -> Perhaps<Arc<str>> {
|
||||||
|
Ok(dsl.src()?.map(|x|x.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'t> DslNs<'t, bool> for App {
|
||||||
|
dsl_words!(|app| -> bool {
|
||||||
|
":focused/editor" => app.project.editor.is_some(),
|
||||||
|
":focused/dialog" => !matches!(app.dialog, Dialog::None),
|
||||||
|
":focused/message" => matches!(app.dialog, Dialog::Message(..)),
|
||||||
|
":focused/add_device" => matches!(app.dialog, Dialog::Device(..)),
|
||||||
|
":focused/browser" => app.dialog.browser().is_some(),
|
||||||
|
":focused/pool/import" => matches!(app.pool.mode, Some(PoolMode::Import(..))),
|
||||||
|
":focused/pool/export" => matches!(app.pool.mode, Some(PoolMode::Export(..))),
|
||||||
|
":focused/pool/rename" => matches!(app.pool.mode, Some(PoolMode::Rename(..))),
|
||||||
|
":focused/pool/length" => matches!(app.pool.mode, Some(PoolMode::Length(..))),
|
||||||
|
":focused/clip" => !app.editor_focused() && matches!(app.selection(),
|
||||||
|
Selection::TrackClip{..}),
|
||||||
|
":focused/track" => !app.editor_focused() && matches!(app.selection(),
|
||||||
|
Selection::Track(..)),
|
||||||
|
":focused/scene" => !app.editor_focused() && matches!(app.selection(),
|
||||||
|
Selection::Scene(..)),
|
||||||
|
":focused/mix" => !app.editor_focused() && matches!(app.selection(),
|
||||||
|
Selection::Mix),
|
||||||
|
});
|
||||||
|
//fn from_literal <D: Dsl> (&self, dsl: &D) -> Perhaps<Arc<str>> {
|
||||||
|
//TODO
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'t> DslNs<'t, ItemTheme> for App {}
|
||||||
|
|
||||||
|
impl<'t> DslNs<'t, Dialog> for App {
|
||||||
|
dsl_words!(|app| -> Dialog {
|
||||||
|
":dialog/none" => Dialog::None,
|
||||||
|
":dialog/options" => Dialog::Options,
|
||||||
|
":dialog/device" => Dialog::Device(0),
|
||||||
|
":dialog/device/prev" => Dialog::Device(0),
|
||||||
|
":dialog/device/next" => Dialog::Device(0),
|
||||||
|
":dialog/help" => Dialog::Help(0),
|
||||||
|
":dialog/menu" => Dialog::Menu(0),
|
||||||
|
":dialog/save" => Dialog::Browser(BrowserTarget::SaveProject,
|
||||||
|
Browser::new(None).unwrap().into()),
|
||||||
|
":dialog/load" => Dialog::Browser(BrowserTarget::LoadProject,
|
||||||
|
Browser::new(None).unwrap().into()),
|
||||||
|
":dialog/import/clip" => Dialog::Browser(BrowserTarget::ImportClip(Default::default()),
|
||||||
|
Browser::new(None).unwrap().into()),
|
||||||
|
":dialog/export/clip" => Dialog::Browser(BrowserTarget::ExportClip(Default::default()),
|
||||||
|
Browser::new(None).unwrap().into()),
|
||||||
|
":dialog/import/sample" => Dialog::Browser(BrowserTarget::ImportSample(Default::default()),
|
||||||
|
Browser::new(None).unwrap().into()),
|
||||||
|
":dialog/export/sample" => Dialog::Browser(BrowserTarget::ExportSample(Default::default()),
|
||||||
|
Browser::new(None).unwrap().into()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'t> DslNs<'t, Selection> for App {
|
||||||
|
dsl_words!(|app| -> Selection {
|
||||||
|
":select/scene" => app.selection().select_scene(app.tracks().len()),
|
||||||
|
":select/scene/next" => app.selection().select_scene_next(app.scenes().len()),
|
||||||
|
":select/scene/prev" => app.selection().select_scene_prev(),
|
||||||
|
":select/track" => app.selection().select_track(app.tracks().len()),
|
||||||
|
":select/track/next" => app.selection().select_track_next(app.tracks().len()),
|
||||||
|
":select/track/prev" => app.selection().select_track_prev(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'t> DslNs<'t, Color> for App {
|
||||||
|
dsl_words!(|app| -> Color {
|
||||||
|
":color/bg" => Color::Rgb(28, 32, 36),
|
||||||
|
});
|
||||||
|
dsl_exprs!(|app| -> Color {
|
||||||
|
"g" (n: u8) => Color::Rgb(n, n, n),
|
||||||
|
"rgb" (r: u8, g: u8, b: u8) => Color::Rgb(r, g, b),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'t> DslNs<'t, Option<u7>> for App {
|
||||||
|
dsl_words!(|app| -> Option<u7> {
|
||||||
|
":editor/pitch" => Some(
|
||||||
|
(app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into()
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'t> DslNs<'t, Option<usize>> for App {
|
||||||
|
dsl_words!(|app| -> Option<usize> {
|
||||||
|
":selected/scene" => app.selection().scene(),
|
||||||
|
":selected/track" => app.selection().track(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'t> DslNs<'t, Option<Arc<RwLock<MidiClip>>>> for App {
|
||||||
|
dsl_words!(|app| -> Option<Arc<RwLock<MidiClip>>> {
|
||||||
|
":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() {
|
||||||
|
app.scenes()[*scene].clips[*track].clone()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dsl_ns! { num |app: App|
|
||||||
|
u8;
|
||||||
|
u16 => {
|
||||||
|
":w/sidebar" => app.project.w_sidebar(app.editor().is_some()),
|
||||||
|
":h/sample-detail" => 6.max(app.height() as u16 * 3 / 9),
|
||||||
|
};
|
||||||
|
usize => {
|
||||||
|
":scene-count" => app.scenes().len(),
|
||||||
|
":track-count" => app.tracks().len(),
|
||||||
|
":device-kind" => app.dialog.device_kind().unwrap_or(0),
|
||||||
|
":device-kind/next" => app.dialog.device_kind_next().unwrap_or(0),
|
||||||
|
":device-kind/prev" => app.dialog.device_kind_prev().unwrap_or(0),
|
||||||
|
};
|
||||||
|
isize;
|
||||||
|
}
|
||||||
24
crates/app/app_deps.rs
Normal file
24
crates/app/app_deps.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
pub use ::{
|
||||||
|
tek_engine::*,
|
||||||
|
tek_config::*,
|
||||||
|
tek_device::{self, *},
|
||||||
|
tengri::{
|
||||||
|
Usually, Perhaps, Has, MaybeHas, has, maybe_has,
|
||||||
|
dsl::*, input::*, output::*, tui::*,
|
||||||
|
tui::ratatui,
|
||||||
|
tui::ratatui::prelude::buffer::Cell,
|
||||||
|
tui::ratatui::prelude::Color::{self, *},
|
||||||
|
tui::ratatui::prelude::{Style, Stylize, Buffer, Modifier},
|
||||||
|
tui::crossterm,
|
||||||
|
tui::crossterm::event::{Event, KeyCode::{self, *}},
|
||||||
|
},
|
||||||
|
std::{
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed},
|
||||||
|
error::Error,
|
||||||
|
collections::BTreeMap,
|
||||||
|
fmt::Write,
|
||||||
|
},
|
||||||
|
xdg::BaseDirectories,
|
||||||
|
};
|
||||||
|
|
@ -2,17 +2,21 @@ use crate::*;
|
||||||
|
|
||||||
impl<'t> DslNs<'t, Box<dyn Render<TuiOut>>> for App {
|
impl<'t> DslNs<'t, Box<dyn Render<TuiOut>>> for App {
|
||||||
dsl_exprs!(|app| -> Box<dyn Render<TuiOut>> {
|
dsl_exprs!(|app| -> Box<dyn Render<TuiOut>> {
|
||||||
"fg" (color: Color, x: Box<dyn Render<TuiOut>>) => Box::new(Tui::fg(color, x)),
|
"text" (tail: Arc<str>) => Box::new(tail),
|
||||||
"bg" (color: Color, x: Box<dyn Render<TuiOut>>) => Box::new(Tui::bg(color, x)),
|
|
||||||
"fg/bg" (fg: Color, bg: Color, x: Box<dyn Render<TuiOut>>) => Box::new(Tui::fg_bg(fg, bg, x)),
|
|
||||||
|
|
||||||
"bsp/n" (a: Box<dyn Render<TuiOut>>, b: Box<dyn Render<TuiOut>>) => Box::new(Bsp::n(a, b)),
|
"fg" (color: Color, x: Box<dyn Render<TuiOut>>) => Box::new(Tui::fg(color, x)),
|
||||||
"bsp/s" (a: Box<dyn Render<TuiOut>>, b: Box<dyn Render<TuiOut>>) => Box::new(Bsp::s(a, b)),
|
"bg" (color: Color, x: Box<dyn Render<TuiOut>>) => Box::new(Tui::bg(color, x)),
|
||||||
"bsp/e" (a: Box<dyn Render<TuiOut>>, b: Box<dyn Render<TuiOut>>) => Box::new(Bsp::e(a, b)),
|
"fg/bg" (fg: Color, bg: Color, x: Box<dyn Render<TuiOut>>) => Box::new(Tui::fg_bg(fg, bg, x)),
|
||||||
"bsp/w" (a: Box<dyn Render<TuiOut>>, b: Box<dyn Render<TuiOut>>) => Box::new(Bsp::w(a, b)),
|
|
||||||
"bsp/a" (a: Box<dyn Render<TuiOut>>, b: Box<dyn Render<TuiOut>>) => Box::new(Bsp::a(a, b)),
|
|
||||||
"bsp/b" (a: Box<dyn Render<TuiOut>>, b: Box<dyn Render<TuiOut>>) => Box::new(Bsp::b(a, b)),
|
|
||||||
|
|
||||||
|
"bsp/n" (a: Box<dyn Render<TuiOut>>, b: Box<dyn Render<TuiOut>>) => Box::new(Bsp::n(a, b)),
|
||||||
|
"bsp/s" (a: Box<dyn Render<TuiOut>>, b: Box<dyn Render<TuiOut>>) => Box::new(Bsp::s(a, b)),
|
||||||
|
"bsp/e" (a: Box<dyn Render<TuiOut>>, b: Box<dyn Render<TuiOut>>) => Box::new(Bsp::e(a, b)),
|
||||||
|
"bsp/w" (a: Box<dyn Render<TuiOut>>, b: Box<dyn Render<TuiOut>>) => Box::new(Bsp::w(a, b)),
|
||||||
|
"bsp/a" (a: Box<dyn Render<TuiOut>>, b: Box<dyn Render<TuiOut>>) => Box::new(Bsp::a(a, b)),
|
||||||
|
"bsp/b" (a: Box<dyn Render<TuiOut>>, b: Box<dyn Render<TuiOut>>) => Box::new(Bsp::b(a, b)),
|
||||||
|
|
||||||
|
"align/nw" (x: Box<dyn Render<TuiOut>>) => Box::new(Align::nw(x)),
|
||||||
|
"align/ne" (x: Box<dyn Render<TuiOut>>) => Box::new(Align::ne(x)),
|
||||||
"align/n" (x: Box<dyn Render<TuiOut>>) => Box::new(Align::n(x)),
|
"align/n" (x: Box<dyn Render<TuiOut>>) => Box::new(Align::n(x)),
|
||||||
"align/s" (x: Box<dyn Render<TuiOut>>) => Box::new(Align::s(x)),
|
"align/s" (x: Box<dyn Render<TuiOut>>) => Box::new(Align::s(x)),
|
||||||
"align/e" (x: Box<dyn Render<TuiOut>>) => Box::new(Align::e(x)),
|
"align/e" (x: Box<dyn Render<TuiOut>>) => Box::new(Align::e(x)),
|
||||||
|
|
@ -21,43 +25,46 @@ impl<'t> DslNs<'t, Box<dyn Render<TuiOut>>> for App {
|
||||||
"align/y" (x: Box<dyn Render<TuiOut>>) => Box::new(Align::y(x)),
|
"align/y" (x: Box<dyn Render<TuiOut>>) => Box::new(Align::y(x)),
|
||||||
"align/c" (x: Box<dyn Render<TuiOut>>) => Box::new(Align::c(x)),
|
"align/c" (x: Box<dyn Render<TuiOut>>) => Box::new(Align::c(x)),
|
||||||
|
|
||||||
"fill/x" (x: Box<dyn Render<TuiOut>>) => Box::new(Fill::x(x)),
|
"fill/x" (x: Box<dyn Render<TuiOut>>) => Box::new(Fill::x(x)),
|
||||||
"fill/y" (x: Box<dyn Render<TuiOut>>) => Box::new(Fill::y(x)),
|
"fill/y" (x: Box<dyn Render<TuiOut>>) => Box::new(Fill::y(x)),
|
||||||
"fill/xy" (x: Box<dyn Render<TuiOut>>) => Box::new(Fill::xy(x)),
|
"fill/xy" (x: Box<dyn Render<TuiOut>>) => Box::new(Fill::xy(x)),
|
||||||
|
|
||||||
"fixed/x" (x: u16, c: Box<dyn Render<TuiOut>>) => Box::new(Fixed::x(x, c)),
|
"fixed/x" (x: u16, c: Box<dyn Render<TuiOut>>) => Box::new(Fixed::x(x, c)),
|
||||||
"fixed/y" (y: u16, c: Box<dyn Render<TuiOut>>) => Box::new(Fixed::y(y, c)),
|
"fixed/y" (y: u16, c: Box<dyn Render<TuiOut>>) => Box::new(Fixed::y(y, c)),
|
||||||
"fixed/xy" (x: u16, y: u16, c: Box<dyn Render<TuiOut>>) => Box::new(Fixed::xy(x, y, c)),
|
"fixed/xy" (x: u16, y: u16, c: Box<dyn Render<TuiOut>>) => Box::new(Fixed::xy(x, y, c)),
|
||||||
|
|
||||||
"min/x" (x: u16, c: Box<dyn Render<TuiOut>>) => Box::new(Min::x(x, c)),
|
"min/x" (x: u16, c: Box<dyn Render<TuiOut>>) => Box::new(Min::x(x, c)),
|
||||||
"min/y" (y: u16, c: Box<dyn Render<TuiOut>>) => Box::new(Min::y(y, c)),
|
"min/y" (y: u16, c: Box<dyn Render<TuiOut>>) => Box::new(Min::y(y, c)),
|
||||||
"min/xy" (x: u16, y: u16, c: Box<dyn Render<TuiOut>>) => Box::new(Min::xy(x, y, c)),
|
"min/xy" (x: u16, y: u16, c: Box<dyn Render<TuiOut>>) => Box::new(Min::xy(x, y, c)),
|
||||||
|
|
||||||
"max/x" (x: u16, c: Box<dyn Render<TuiOut>>) => Box::new(Max::x(x, c)),
|
"max/x" (x: u16, c: Box<dyn Render<TuiOut>>) => Box::new(Max::x(x, c)),
|
||||||
"max/y" (y: u16, c: Box<dyn Render<TuiOut>>) => Box::new(Max::y(y, c)),
|
"max/y" (y: u16, c: Box<dyn Render<TuiOut>>) => Box::new(Max::y(y, c)),
|
||||||
"max/xy" (x: u16, y: u16, c: Box<dyn Render<TuiOut>>) => Box::new(Max::xy(x, y, c)),
|
"max/xy" (x: u16, y: u16, c: Box<dyn Render<TuiOut>>) => Box::new(Max::xy(x, y, c)),
|
||||||
});
|
});
|
||||||
dsl_words!(|app| -> Box<dyn Render<TuiOut>> {
|
dsl_words!(|app| -> Box<dyn Render<TuiOut>> {
|
||||||
":templates" => Box::new({
|
":templates" => Box::new({
|
||||||
let modes = app.config.modes.clone();
|
let modes = app.config.modes.clone();
|
||||||
let height = (modes.read().unwrap().len() * 2) as u16;
|
let height = (modes.read().unwrap().len() * 2) as u16;
|
||||||
Min::x(30, Fixed::y(height, Stack::south(move|add: &mut dyn FnMut(&dyn Render<TuiOut>)|{
|
Fixed::y(height, Min::x(30, Stack::south(move|add: &mut dyn FnMut(&dyn Render<TuiOut>)|{
|
||||||
for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() {
|
for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() {
|
||||||
let bg = if index == 0 { Rgb(48,64,32) } else { Rgb(16, 32, 24) };
|
let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) };
|
||||||
let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or("<no name>");
|
let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or("<no name>");
|
||||||
let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or("<no info>");
|
let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or("<no info>");
|
||||||
let fg1 = Rgb(224, 192, 128);
|
let fg1 = Rgb(224, 192, 128);
|
||||||
let fg2 = Rgb(224, 128, 32);
|
let fg2 = Rgb(224, 128, 32);
|
||||||
|
let field_name = Fill::x(Align::w(Tui::fg(fg1, name)));
|
||||||
|
let field_id = Fill::x(Align::e(Tui::fg(fg2, id)));
|
||||||
|
let field_info = Fill::x(Align::w(info));
|
||||||
add(&Fixed::y(2, Fill::x(Tui::bg(bg, Bsp::s(
|
add(&Fixed::y(2, Fill::x(Tui::bg(bg, Bsp::s(
|
||||||
Bsp::a(Fill::x(Align::w(Tui::fg(fg1, name))),
|
Bsp::a(field_name, field_id), field_info)))));
|
||||||
Fill::x(Align::e(Tui::fg(fg2, id)))),
|
}
|
||||||
Align::w(info))))));
|
})))
|
||||||
}})))}),
|
}),
|
||||||
":sessions" => Box::new(Min::x(30, Fixed::y(6, Stack::south(
|
":sessions" => Box::new(Fixed::y(6, Min::x(30, Stack::south(
|
||||||
move|add: &mut dyn FnMut(&dyn Render<TuiOut>)|{
|
move|add: &mut dyn FnMut(&dyn Render<TuiOut>)|{
|
||||||
let fg = Rgb(224, 192, 128);
|
let fg = Rgb(224, 192, 128);
|
||||||
for (index, name) in ["session1", "session2", "session3"].iter().enumerate() {
|
for (index, name) in ["session1", "session2", "session3"].iter().enumerate() {
|
||||||
let bg = if index == 0 { Rgb(48,64,32) } else { Rgb(16, 32, 24) };
|
let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) };
|
||||||
add(&Fixed::y(2, Fill::x(Tui::bg(bg, Align::w(Tui::fg(fg, name))))));
|
add(&Fixed::y(2, Fill::x(Tui::bg(bg, Align::w(Tui::fg(fg, name))))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -82,7 +89,7 @@ impl<'t> DslNs<'t, Box<dyn Render<TuiOut>>> for App {
|
||||||
Fill::x(Tui::bg(bg, Bsp::e(lb, Bsp::w(rb, "FIXME device name")))) }))) },
|
Fill::x(Tui::bg(bg, Bsp::e(lb, Bsp::w(rb, "FIXME device name")))) }))) },
|
||||||
});
|
});
|
||||||
/// Resolve an expression if known.
|
/// Resolve an expression if known.
|
||||||
fn from_expr <D: Dsl> (&'t self, dsl: D) -> Perhaps<Box<dyn Render<TuiOut>>> {
|
fn from_expr <D: Dsl> (&self, dsl: &D) -> Perhaps<Box<dyn Render<TuiOut>>> {
|
||||||
if let Some(head) = dsl.expr().head()? {
|
if let Some(head) = dsl.expr().head()? {
|
||||||
for (key, value) in Self::EXPRS.0.iter() {
|
for (key, value) in Self::EXPRS.0.iter() {
|
||||||
if head == *key {
|
if head == *key {
|
||||||
|
|
@ -93,7 +100,7 @@ impl<'t> DslNs<'t, Box<dyn Render<TuiOut>>> for App {
|
||||||
return Ok(None)
|
return Ok(None)
|
||||||
}
|
}
|
||||||
/// Resolve a symbol if known.
|
/// Resolve a symbol if known.
|
||||||
fn from_word <D: Dsl> (&'t self, dsl: D) -> Perhaps<Box<dyn Render<TuiOut>>> {
|
fn from_word <D: Dsl> (&self, dsl: &D) -> Perhaps<Box<dyn Render<TuiOut>>> {
|
||||||
if let Some(dsl) = dsl.word()? {
|
if let Some(dsl) = dsl.word()? {
|
||||||
let views = self.config.views.read().unwrap();
|
let views = self.config.views.read().unwrap();
|
||||||
if let Some(view) = views.get(dsl) {
|
if let Some(view) = views.get(dsl) {
|
||||||
|
|
@ -101,7 +108,11 @@ impl<'t> DslNs<'t, Box<dyn Render<TuiOut>>> for App {
|
||||||
std::mem::drop(views);
|
std::mem::drop(views);
|
||||||
return Ok(Some(render_dsl(self, view.as_ref())))
|
return Ok(Some(render_dsl(self, view.as_ref())))
|
||||||
}
|
}
|
||||||
for (word, get) in Self::WORDS.0 { if dsl == *word { return get(self) } }
|
for (word, get) in Self::WORDS.0 {
|
||||||
|
if dsl == *word {
|
||||||
|
return get(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return Ok(None)
|
return Ok(None)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11
crates/config/Cargo.toml
Normal file
11
crates/config/Cargo.toml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[package]
|
||||||
|
name = "tek_config"
|
||||||
|
edition = { workspace = true }
|
||||||
|
version = { workspace = true }
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "config.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
xdg = { workspace = true }
|
||||||
|
tengri = { workspace = true }
|
||||||
213
crates/config/config.rs
Normal file
213
crates/config/config.rs
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
#![feature(if_let_guard)]
|
||||||
|
|
||||||
|
use ::{
|
||||||
|
std::{
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
collections::BTreeMap,
|
||||||
|
path::PathBuf
|
||||||
|
},
|
||||||
|
tengri::{
|
||||||
|
Usually, impl_debug,
|
||||||
|
tui::TuiEvent,
|
||||||
|
dsl::{Dsl, DslWord, DslExpr}
|
||||||
|
},
|
||||||
|
xdg::BaseDirectories,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Configuration.
|
||||||
|
///
|
||||||
|
/// Contains mode, view, and bind definitions.
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct Config {
|
||||||
|
pub dirs: BaseDirectories,
|
||||||
|
pub modes: Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode<Arc<str>>>>>>,
|
||||||
|
pub views: Arc<RwLock<BTreeMap<Arc<str>, Arc<str>>>>,
|
||||||
|
pub binds: Arc<RwLock<BTreeMap<Arc<str>, EventMap<TuiEvent, Arc<str>>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A set of currently active view and keys definitions,
|
||||||
|
/// with optional name and description.
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct Mode<D: Dsl + Ord> {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub name: Vec<D>,
|
||||||
|
pub info: Vec<D>,
|
||||||
|
pub view: Vec<D>,
|
||||||
|
pub keys: Vec<D>,
|
||||||
|
pub modes: BTreeMap<D, Mode<D>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A collection of input bindings.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct EventMap<E, C>(
|
||||||
|
/// Map of each event (e.g. key combination) to
|
||||||
|
/// all command expressions bound to it by
|
||||||
|
/// all loaded input layers.
|
||||||
|
pub BTreeMap<E, Vec<Binding<C>>>
|
||||||
|
);
|
||||||
|
|
||||||
|
/// An input binding.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Binding<C> {
|
||||||
|
pub commands: Arc<[C]>,
|
||||||
|
pub condition: Option<Condition>,
|
||||||
|
pub description: Option<Arc<str>>,
|
||||||
|
pub source: Option<Arc<PathBuf>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Input bindings are only returned if this evaluates to true
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Condition(Arc<Box<dyn Fn()->bool + Send + Sync>>);
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
const CONFIG: &'static str = "tek.edn";
|
||||||
|
const DEFAULTS: &'static str = include_str!("../../tek.edn");
|
||||||
|
pub fn init () -> Usually<Self> {
|
||||||
|
let mut cfgs: Self = Default::default();
|
||||||
|
cfgs.dirs = BaseDirectories::with_profile("tek", "v0");
|
||||||
|
cfgs.init_file(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|cfgs.load(dsl))?;
|
||||||
|
Ok(cfgs)
|
||||||
|
}
|
||||||
|
pub fn init_file (
|
||||||
|
&mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()>
|
||||||
|
) -> Usually<()> {
|
||||||
|
if self.dirs.find_config_file(path).is_none() {
|
||||||
|
println!("Creating {path:?}");
|
||||||
|
std::fs::write(self.dirs.place_config_file(path)?, defaults);
|
||||||
|
}
|
||||||
|
Ok(if let Some(path) = self.dirs.find_config_file(path) {
|
||||||
|
println!("Loading {path:?}");
|
||||||
|
let src = std::fs::read_to_string(&path)?;
|
||||||
|
src.as_str().each(move|item|each(self, item))?;
|
||||||
|
} else {
|
||||||
|
return Err(format!("{path}: not found").into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn load (&mut self, dsl: impl Dsl) -> Usually<()> {
|
||||||
|
dsl.each(|item|if let Some(expr) = item.expr()? {
|
||||||
|
let head = expr.head()?;
|
||||||
|
let tail = expr.tail()?;
|
||||||
|
let name = tail.head()?;
|
||||||
|
let body = tail.tail()?;
|
||||||
|
println!("{} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default());
|
||||||
|
match head {
|
||||||
|
Some("view") if let Some(name) = name => self.load_view(name.into(), body),
|
||||||
|
Some("keys") if let Some(name) = name => self.load_bind(name.into(), body),
|
||||||
|
Some("mode") if let Some(name) = name => self.load_mode(name.into(), body),
|
||||||
|
_ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(format!("Config::load: expected expr, got: {item:?}").into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn load_view (&mut self, id: Arc<str>, dsl: impl Dsl) -> Usually<()> {
|
||||||
|
self.views.write().unwrap().insert(id, dsl.src()?.unwrap_or_default().into());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn load_bind (&mut self, id: Arc<str>, dsl: impl Dsl) -> Usually<()> {
|
||||||
|
let mut map = EventMap::new();
|
||||||
|
dsl.each(|item|if item.expr().head() == Ok(Some("see")) {
|
||||||
|
// TODO
|
||||||
|
Ok(())
|
||||||
|
} else if let Ok(Some(word)) = item.expr().head().word() {
|
||||||
|
if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? {
|
||||||
|
map.add(key, Binding {
|
||||||
|
commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(),
|
||||||
|
condition: None,
|
||||||
|
description: None,
|
||||||
|
source: None
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
} else if Some(":char") == item.expr()?.head()? {
|
||||||
|
// TODO
|
||||||
|
return Ok(())
|
||||||
|
} else {
|
||||||
|
return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(format!("Config::load_bind: unexpected: {item:?}").into())
|
||||||
|
})?;
|
||||||
|
self.binds.write().unwrap().insert(id, map);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn load_mode (&mut self, id: Arc<str>, dsl: impl Dsl) -> Usually<()> {
|
||||||
|
let mut mode = Mode::default();
|
||||||
|
dsl.each(|item|Self::load_mode_one(&mut mode, item))?;
|
||||||
|
self.modes.write().unwrap().insert(id.into(), Arc::new(mode));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn load_mode_one (mode: &mut Mode<Arc<str>>, item: impl Dsl) -> Usually<()> {
|
||||||
|
Ok(if let Ok(Some(key)) = item.expr().head() {
|
||||||
|
match key {
|
||||||
|
"name" => mode.name.push(item.expr()?.tail()?.map(|x|x.trim()).unwrap_or("").into()),
|
||||||
|
"info" => mode.info.push(item.expr()?.tail()?.map(|x|x.trim()).unwrap_or("").into()),
|
||||||
|
"keys" => {
|
||||||
|
item.expr()?.tail()?.each(|item|{mode.keys.push(item.trim().into()); Ok(())})?;
|
||||||
|
},
|
||||||
|
"mode" => if let Some(id) = item.expr()?.tail()?.head()? {
|
||||||
|
let mut submode = Mode::default();
|
||||||
|
Self::load_mode_one(&mut submode, item.expr()?.tail()?.tail()?)?;
|
||||||
|
mode.modes.insert(id.into(), submode);
|
||||||
|
} else {
|
||||||
|
return Err(format!("load_mode_one: incomplete: {item:?}").into());
|
||||||
|
},
|
||||||
|
_ => mode.view.push(item.expr()?.unwrap().into()),
|
||||||
|
}
|
||||||
|
} else if let Ok(Some(word)) = item.word() {
|
||||||
|
mode.view.push(word.into());
|
||||||
|
} else {
|
||||||
|
return Err(format!("load_mode_one: unexpected: {item:?}").into());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default is always empty map regardless if `E` and `C` implement [Default].
|
||||||
|
impl<E, C> Default for EventMap<E, C> {
|
||||||
|
fn default () -> Self { Self(Default::default()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: Clone + Ord, C> EventMap<E, C> {
|
||||||
|
/// Create a new event map
|
||||||
|
pub fn new () -> Self {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
/// Add a binding to an owned event map.
|
||||||
|
pub fn def (mut self, event: E, binding: Binding<C>) -> Self {
|
||||||
|
self.add(event, binding);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Add a binding to an event map.
|
||||||
|
pub fn add (&mut self, event: E, binding: Binding<C>) -> &mut Self {
|
||||||
|
if !self.0.contains_key(&event) {
|
||||||
|
self.0.insert(event.clone(), Default::default());
|
||||||
|
}
|
||||||
|
self.0.get_mut(&event).unwrap().push(binding);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// Return the binding(s) that correspond to an event.
|
||||||
|
pub fn query (&self, event: &E) -> Option<&[Binding<C>]> {
|
||||||
|
self.0.get(event).map(|x|x.as_slice())
|
||||||
|
}
|
||||||
|
/// Return the first binding that corresponds to an event, considering conditions.
|
||||||
|
pub fn dispatch (&self, event: &E) -> Option<&Binding<C>> {
|
||||||
|
self.query(event)
|
||||||
|
.map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next())
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> Binding<C> {
|
||||||
|
pub fn from_dsl (dsl: impl Dsl) -> Usually<Self> {
|
||||||
|
let command: Option<C> = None;
|
||||||
|
let condition: Option<Condition> = None;
|
||||||
|
let description: Option<Arc<str>> = None;
|
||||||
|
let source: Option<Arc<PathBuf>> = None;
|
||||||
|
if let Some(command) = command {
|
||||||
|
Ok(Self { commands: [command].into(), condition, description, source })
|
||||||
|
} else {
|
||||||
|
Err(format!("no command in {dsl:?}").into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_debug!(Condition |self, w| { write!(w, "*") });
|
||||||
2
deps/tengri
vendored
2
deps/tengri
vendored
|
|
@ -1 +1 @@
|
||||||
Subproject commit 2557a0d253dfe45eab001dcc08ebc66c2c6715d3
|
Subproject commit e3e3c163da02165e77a259eb715749b7f0097498
|
||||||
16
tek.edn
16
tek.edn
|
|
@ -1,12 +1,16 @@
|
||||||
(mode :menu (keys :x :y :confirm) :menu)
|
(mode :menu (keys :x :y :confirm) :menu)
|
||||||
(keys :x (@left x/dec) (@right x/inc))
|
(keys :x (@left x/dec) (@right x/inc))
|
||||||
(keys :y (@up y/dec) (@down y/inc))
|
(keys :y (@up y/dec) (@down y/inc))
|
||||||
(keys :confirm (@enter confirm))
|
(keys :confirm (@enter confirm))
|
||||||
(view :menu (bg :color/bg (bsp/s :ports/out (bsp/n :ports/in (bsp/e :sessions :templates)))))
|
(view :menu (bg (g 40) (bsp/s :ports/out (bsp/n :ports/in
|
||||||
(view :ports/out (fill/x (fixed/y 3 (bsp/a (fill/x (align/w "L AUDIO OUT")
|
(bg (g 30) (fill/xy (align/c (bg (g 40) (bsp/s
|
||||||
(bsp/a "MIDI OUT" (fill/x (align/e "AUDIO OUT R"))))))))
|
(text CONTINUE-SESSION)
|
||||||
(view :ports/in (fill/x (fixed/y 3 (bsp/a (fill/x (align/w "L AUDIO IN ")
|
(bsp/s (text LOAD-OTHER-SESSION)
|
||||||
(bsp/a "MIDI IN " (fill/x (align/e "AUDIO IN R"))))))))
|
(text BEGIN-NEW-SESSION)))))))))))
|
||||||
|
(view :ports/out (fill/x (fixed/y 3 (bsp/a (fill/x (align/w (text L-AUDIO-OUT)))
|
||||||
|
(bsp/a (text MIDI-OUT) (fill/x (align/e (text AUDIO-OUT R))))))))
|
||||||
|
(view :ports/in (fill/x (fixed/y 3 (bsp/a (fill/x (align/w (text L-AUDIO-IN)))
|
||||||
|
(bsp/a (text MIDI-IN) (fill/x (align/e (text AUDIO-IN-R))))))))
|
||||||
(view :browse (bsp/s (padding/xy 3 1 :browse-title) (enclose (fg (g 96)) browser)))
|
(view :browse (bsp/s (padding/xy 3 1 :browse-title) (enclose (fg (g 96)) browser)))
|
||||||
|
|
||||||
(keys :help (@f1 dialog :help))
|
(keys :help (@f1 dialog :help))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue