mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 04:06:45 +01:00
Compare commits
No commits in common. "5ccbb9719f11ad7370cf612715b6bd239c91b57a" and "fa5f67f010245eec1291ed84dad8ca126341b66b" have entirely different histories.
5ccbb9719f
...
fa5f67f010
12 changed files with 344 additions and 463 deletions
24
Cargo.lock
generated
24
Cargo.lock
generated
|
|
@ -2390,7 +2390,6 @@ dependencies = [
|
|||
"proptest",
|
||||
"proptest-derive",
|
||||
"rand 0.8.5",
|
||||
"tek_config",
|
||||
"tek_device",
|
||||
"tek_engine",
|
||||
"tengri",
|
||||
|
|
@ -2407,14 +2406,6 @@ dependencies = [
|
|||
"tek",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tek_config"
|
||||
version = "0.2.1"
|
||||
dependencies = [
|
||||
"tengri",
|
||||
"xdg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tek_device"
|
||||
version = "0.2.1"
|
||||
|
|
@ -2456,7 +2447,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tengri"
|
||||
version = "0.14.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"tengri_core",
|
||||
"tengri_dsl",
|
||||
|
|
@ -2467,11 +2458,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tengri_core"
|
||||
version = "0.14.0"
|
||||
version = "0.13.0"
|
||||
|
||||
[[package]]
|
||||
name = "tengri_dsl"
|
||||
version = "0.14.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"const_panic",
|
||||
"itertools 0.14.0",
|
||||
|
|
@ -2482,14 +2473,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tengri_input"
|
||||
version = "0.14.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"tengri_core",
|
||||
"tengri_dsl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tengri_output"
|
||||
version = "0.14.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"tengri_core",
|
||||
"tengri_dsl",
|
||||
|
|
@ -2497,7 +2489,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tengri_proc"
|
||||
version = "0.14.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
|
|
@ -2508,7 +2500,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tengri_tui"
|
||||
version = "0.14.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"atomic_float",
|
||||
"better-panic",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ resolver = "2"
|
|||
members = [
|
||||
"./crates/engine",
|
||||
"./crates/device",
|
||||
"./crates/config",
|
||||
"./crates/app",
|
||||
"./crates/cli",
|
||||
]
|
||||
|
|
@ -35,7 +34,6 @@ path = "./deps/rust-jack"
|
|||
[workspace.dependencies]
|
||||
tek_device = { path = "./crates/device" }
|
||||
tek_engine = { path = "./crates/engine" }
|
||||
tek_config = { path = "./crates/config" }
|
||||
tek = { path = "./crates/app" }
|
||||
tek_cli = { path = "./crates/cli" }
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,8 @@ version = { workspace = true }
|
|||
tengri = { workspace = true }
|
||||
tengri_proc = { workspace = true }
|
||||
|
||||
tek_config = { workspace = true }
|
||||
tek_device = { workspace = true }
|
||||
tek_engine = { workspace = true }
|
||||
tek_device = { workspace = true }
|
||||
|
||||
backtrace = { workspace = true }
|
||||
clap = { workspace = true, optional = true }
|
||||
|
|
|
|||
|
|
@ -1,12 +1,39 @@
|
|||
// ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
//██Let me play the world's tiniest piano for you. ██
|
||||
//█▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀█
|
||||
//█▙▙█▙▙▙█▙▙█▙▙▙█▙▙█▙▙▙█▙▙█▙▙▙█▙▙█▙▙▙█▙▙█▙▙▙█▙▙█▙▙▙██
|
||||
//█▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄█
|
||||
//███████████████████████████████████████████████████
|
||||
//█ ▀ ▀ ▀ █
|
||||
#![allow(unused, clippy::unit_arg)]
|
||||
#![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)]
|
||||
#[cfg(test)] mod app_test;
|
||||
mod app_deps; pub use self::app_deps::*;
|
||||
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::*;
|
||||
mod app_bind; pub use self::app_bind::*;
|
||||
mod app_data; pub use self::app_data::*;
|
||||
mod app_view; pub use self::app_view::*;
|
||||
#[cfg(test)] mod app_test;
|
||||
/// Total state
|
||||
#[derive(Default, Debug)]
|
||||
pub struct App {
|
||||
|
|
@ -31,6 +58,23 @@ pub struct App {
|
|||
/// Base color.
|
||||
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.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub enum Dialog {
|
||||
|
|
@ -42,6 +86,107 @@ pub enum Dialog {
|
|||
Browser(BrowserTarget, Arc<Browser>),
|
||||
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!(Pool: |self: App|self.pool);
|
||||
has!(Dialog: |self: App|self.dialog);
|
||||
|
|
@ -83,7 +228,7 @@ fn render_dsl <'t> (
|
|||
state: &'t impl DslNs<'t, Box<dyn Render<TuiOut>>>,
|
||||
src: &str
|
||||
) -> 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),
|
||||
};
|
||||
let (fg_1, bg_1) = (Color::Rgb(240, 160, 100), Color::Rgb(48, 0, 0));
|
||||
|
|
@ -120,6 +265,8 @@ handle!(TuiIn:|self: App, input|{
|
|||
}
|
||||
Ok(None)
|
||||
});
|
||||
#[derive(Debug)]
|
||||
pub enum AppCommand { /* TODO */ }
|
||||
impl Dialog {
|
||||
pub fn menu_selected (&self) -> Option<usize> {
|
||||
if let Self::Menu(selected) = self { Some(*selected) } else { None }
|
||||
|
|
@ -143,6 +290,17 @@ impl Dialog {
|
|||
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 {
|
||||
pub fn editor_focused (&self) -> bool {
|
||||
false
|
||||
|
|
@ -226,6 +384,107 @@ 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> {
|
||||
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))))
|
||||
|
|
@ -236,6 +495,38 @@ impl ScenesView for App {
|
|||
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::include(TuiOut)]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
use crate::*;
|
||||
|
||||
impl<'t> DslNs<'t, AppCommand> for App {
|
||||
dsl_exprs!(|app| -> AppCommand { /* TODO */ });
|
||||
dsl_exprs!(|app| -> AppCommand {
|
||||
});
|
||||
dsl_words!(|app| -> AppCommand {
|
||||
"x/inc" => todo!(),
|
||||
"x/dec" => todo!(),
|
||||
|
|
@ -10,21 +11,6 @@ impl<'t> DslNs<'t, AppCommand> for App {
|
|||
"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 => {
|
||||
//("x/inc" /
|
||||
|
|
|
|||
|
|
@ -1,122 +0,0 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
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,21 +2,17 @@ use crate::*;
|
|||
|
||||
impl<'t> DslNs<'t, Box<dyn Render<TuiOut>>> for App {
|
||||
dsl_exprs!(|app| -> Box<dyn Render<TuiOut>> {
|
||||
"text" (tail: Arc<str>) => Box::new(tail),
|
||||
"fg" (color: Color, x: Box<dyn Render<TuiOut>>) => Box::new(Tui::fg(color, x)),
|
||||
"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)),
|
||||
|
||||
"fg" (color: Color, x: Box<dyn Render<TuiOut>>) => Box::new(Tui::fg(color, x)),
|
||||
"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)),
|
||||
"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)),
|
||||
|
||||
"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/s" (x: Box<dyn Render<TuiOut>>) => Box::new(Align::s(x)),
|
||||
"align/e" (x: Box<dyn Render<TuiOut>>) => Box::new(Align::e(x)),
|
||||
|
|
@ -25,46 +21,43 @@ impl<'t> DslNs<'t, Box<dyn Render<TuiOut>>> for App {
|
|||
"align/y" (x: Box<dyn Render<TuiOut>>) => Box::new(Align::y(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/y" (x: Box<dyn Render<TuiOut>>) => Box::new(Fill::y(x)),
|
||||
"fill/xy" (x: Box<dyn Render<TuiOut>>) => Box::new(Fill::xy(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/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/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)),
|
||||
|
||||
"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/xy" (x: u16, y: u16, c: Box<dyn Render<TuiOut>>) => Box::new(Min::xy(x, y, 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/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/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/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/xy" (x: u16, y: u16, c: Box<dyn Render<TuiOut>>) => Box::new(Max::xy(x, y, c)),
|
||||
});
|
||||
dsl_words!(|app| -> Box<dyn Render<TuiOut>> {
|
||||
":templates" => Box::new({
|
||||
let modes = app.config.modes.clone();
|
||||
let height = (modes.read().unwrap().len() * 2) as u16;
|
||||
Fixed::y(height, Min::x(30, Stack::south(move|add: &mut dyn FnMut(&dyn Render<TuiOut>)|{
|
||||
Min::x(30, Fixed::y(height, Stack::south(move|add: &mut dyn FnMut(&dyn Render<TuiOut>)|{
|
||||
for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() {
|
||||
let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) };
|
||||
let bg = if index == 0 { Rgb(48,64,32) } else { Rgb(16, 32, 24) };
|
||||
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 fg1 = Rgb(224, 192, 128);
|
||||
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(
|
||||
Bsp::a(field_name, field_id), field_info)))));
|
||||
}
|
||||
})))
|
||||
}),
|
||||
":sessions" => Box::new(Fixed::y(6, Min::x(30, Stack::south(
|
||||
Bsp::a(Fill::x(Align::w(Tui::fg(fg1, name))),
|
||||
Fill::x(Align::e(Tui::fg(fg2, id)))),
|
||||
Align::w(info))))));
|
||||
}})))}),
|
||||
":sessions" => Box::new(Min::x(30, Fixed::y(6, Stack::south(
|
||||
move|add: &mut dyn FnMut(&dyn Render<TuiOut>)|{
|
||||
let fg = Rgb(224, 192, 128);
|
||||
for (index, name) in ["session1", "session2", "session3"].iter().enumerate() {
|
||||
let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) };
|
||||
let bg = if index == 0 { Rgb(48,64,32) } else { Rgb(16, 32, 24) };
|
||||
add(&Fixed::y(2, Fill::x(Tui::bg(bg, Align::w(Tui::fg(fg, name))))));
|
||||
}
|
||||
}
|
||||
|
|
@ -89,7 +82,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")))) }))) },
|
||||
});
|
||||
/// Resolve an expression if known.
|
||||
fn from_expr <D: Dsl> (&self, dsl: &D) -> Perhaps<Box<dyn Render<TuiOut>>> {
|
||||
fn from_expr <D: Dsl> (&'t self, dsl: D) -> Perhaps<Box<dyn Render<TuiOut>>> {
|
||||
if let Some(head) = dsl.expr().head()? {
|
||||
for (key, value) in Self::EXPRS.0.iter() {
|
||||
if head == *key {
|
||||
|
|
@ -100,7 +93,7 @@ impl<'t> DslNs<'t, Box<dyn Render<TuiOut>>> for App {
|
|||
return Ok(None)
|
||||
}
|
||||
/// Resolve a symbol if known.
|
||||
fn from_word <D: Dsl> (&self, dsl: &D) -> Perhaps<Box<dyn Render<TuiOut>>> {
|
||||
fn from_word <D: Dsl> (&'t self, dsl: D) -> Perhaps<Box<dyn Render<TuiOut>>> {
|
||||
if let Some(dsl) = dsl.word()? {
|
||||
let views = self.config.views.read().unwrap();
|
||||
if let Some(view) = views.get(dsl) {
|
||||
|
|
@ -108,11 +101,7 @@ impl<'t> DslNs<'t, Box<dyn Render<TuiOut>>> for App {
|
|||
std::mem::drop(views);
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
[package]
|
||||
name = "tek_config"
|
||||
edition = { workspace = true }
|
||||
version = { workspace = true }
|
||||
|
||||
[lib]
|
||||
path = "config.rs"
|
||||
|
||||
[dependencies]
|
||||
xdg = { workspace = true }
|
||||
tengri = { workspace = true }
|
||||
|
|
@ -1,213 +0,0 @@
|
|||
#![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 e3e3c163da02165e77a259eb715749b7f0097498
|
||||
Subproject commit 2557a0d253dfe45eab001dcc08ebc66c2c6715d3
|
||||
16
tek.edn
16
tek.edn
|
|
@ -1,16 +1,12 @@
|
|||
(mode :menu (keys :x :y :confirm) :menu)
|
||||
(keys :x (@left x/dec) (@right x/inc))
|
||||
(keys :y (@up y/dec) (@down y/inc))
|
||||
(keys :confirm (@enter confirm))
|
||||
(view :menu (bg (g 40) (bsp/s :ports/out (bsp/n :ports/in
|
||||
(bg (g 30) (fill/xy (align/c (bg (g 40) (bsp/s
|
||||
(text CONTINUE-SESSION)
|
||||
(bsp/s (text LOAD-OTHER-SESSION)
|
||||
(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))))))))
|
||||
(keys :confirm (@enter confirm))
|
||||
(view :menu (bg :color/bg (bsp/s :ports/out (bsp/n :ports/in (bsp/e :sessions :templates)))))
|
||||
(view :ports/out (fill/x (fixed/y 3 (bsp/a (fill/x (align/w "L AUDIO OUT")
|
||||
(bsp/a "MIDI OUT" (fill/x (align/e "AUDIO OUT R"))))))))
|
||||
(view :ports/in (fill/x (fixed/y 3 (bsp/a (fill/x (align/w "L AUDIO IN ")
|
||||
(bsp/a "MIDI IN " (fill/x (align/e "AUDIO IN R"))))))))
|
||||
(view :browse (bsp/s (padding/xy 3 1 :browse-title) (enclose (fg (g 96)) browser)))
|
||||
|
||||
(keys :help (@f1 dialog :help))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue