From d5865d941f7b0442dbae8340bacfa0a685649e70 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 23 Aug 2025 12:19:35 +0300 Subject: [PATCH 1/3] almost back in commission --- crates/app/app.rs | 184 ++--------------------------------------- crates/app/app_bind.rs | 18 +++- crates/app/app_data.rs | 122 +++++++++++++++++++++++++++ crates/app/app_deps.rs | 24 ++++++ crates/app/app_view.rs | 69 +++++++++------- deps/tengri | 2 +- tek.edn | 16 ++-- 7 files changed, 219 insertions(+), 216 deletions(-) create mode 100644 crates/app/app_data.rs create mode 100644 crates/app/app_deps.rs diff --git a/crates/app/app.rs b/crates/app/app.rs index 5c3fed5f..5f9cac56 100644 --- a/crates/app/app.rs +++ b/crates/app/app.rs @@ -1,38 +1,12 @@ -// ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ -//██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)] -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_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::*; + #[cfg(test)] mod app_test; /// Total state #[derive(Default, Debug)] @@ -228,7 +202,7 @@ fn render_dsl <'t> ( state: &'t impl DslNs<'t, Box>>, src: &str ) -> Box> { - let err: Option> = match state.from(src) { + let err: Option> = 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)); @@ -265,8 +239,6 @@ handle!(TuiIn:|self: App, input|{ } Ok(None) }); -#[derive(Debug)] -pub enum AppCommand { /* TODO */ } impl Dialog { pub fn menu_selected (&self) -> Option { if let Self::Menu(selected) = self { Some(*selected) } else { None } @@ -290,17 +262,6 @@ impl Dialog { todo!() } } -#[tengri_proc::command(Option)] impl DialogCommand { - fn open (dialog: &mut Option, new: Dialog) -> Perhaps { - *dialog = Some(new); - Ok(None) - } - fn close (dialog: &mut Option) -> Perhaps { - *dialog = None; - Ok(None) - } -} - impl App { pub fn editor_focused (&self) -> bool { false @@ -384,107 +345,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 => { - ":editor/pitch" => Some( - (app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into() - ) - }; - - Option => { - ":selected/scene" => app.selection().scene(), - ":selected/track" => app.selection().track(), - }; - - Option>> => { - ":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) -> impl Content { 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)))) @@ -495,38 +355,6 @@ 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)] diff --git a/crates/app/app_bind.rs b/crates/app/app_bind.rs index 9acaa5ca..2a13994b 100644 --- a/crates/app/app_bind.rs +++ b/crates/app/app_bind.rs @@ -1,8 +1,7 @@ use crate::*; impl<'t> DslNs<'t, AppCommand> for App { - dsl_exprs!(|app| -> AppCommand { - }); + dsl_exprs!(|app| -> AppCommand { /* TODO */ }); dsl_words!(|app| -> AppCommand { "x/inc" => todo!(), "x/dec" => todo!(), @@ -11,6 +10,21 @@ impl<'t> DslNs<'t, AppCommand> for App { "confirm" => todo!(), }); } +#[derive(Debug)] +pub enum AppCommand { /* TODO */ } + +#[tengri_proc::command(Option)] +impl DialogCommand { + fn open (dialog: &mut Option, new: Dialog) -> Perhaps { + *dialog = Some(new); + Ok(None) + } + fn close (dialog: &mut Option) -> Perhaps { + *dialog = None; + Ok(None) + } +} + //AppCommand => { //("x/inc" / diff --git a/crates/app/app_data.rs b/crates/app/app_data.rs new file mode 100644 index 00000000..e44ada9f --- /dev/null +++ b/crates/app/app_data.rs @@ -0,0 +1,122 @@ +use crate::*; + +impl<'t> DslNs<'t, Arc> for App { + dsl_exprs!(|app| -> Arc {}); + dsl_words!(|app| -> Arc {}); + fn from_literal (&self, dsl: &D) -> Perhaps> { + 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 (&self, dsl: &D) -> Perhaps> { + //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> for App { + dsl_words!(|app| -> Option { + ":editor/pitch" => Some( + (app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into() + ) + }); +} + +impl<'t> DslNs<'t, Option> for App { + dsl_words!(|app| -> Option { + ":selected/scene" => app.selection().scene(), + ":selected/track" => app.selection().track(), + }); +} + +impl<'t> DslNs<'t, Option>>> for App { + dsl_words!(|app| -> Option>> { + ":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; +} diff --git a/crates/app/app_deps.rs b/crates/app/app_deps.rs new file mode 100644 index 00000000..508feda5 --- /dev/null +++ b/crates/app/app_deps.rs @@ -0,0 +1,24 @@ +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, +}; diff --git a/crates/app/app_view.rs b/crates/app/app_view.rs index 9d9a4a72..6d7faef1 100644 --- a/crates/app/app_view.rs +++ b/crates/app/app_view.rs @@ -2,17 +2,21 @@ use crate::*; impl<'t> DslNs<'t, Box>> for App { dsl_exprs!(|app| -> Box> { - "fg" (color: Color, x: Box>) => Box::new(Tui::fg(color, x)), - "bg" (color: Color, x: Box>) => Box::new(Tui::bg(color, x)), - "fg/bg" (fg: Color, bg: Color, x: Box>) => Box::new(Tui::fg_bg(fg, bg, x)), + "text" (tail: Arc) => Box::new(tail), - "bsp/n" (a: Box>, b: Box>) => Box::new(Bsp::n(a, b)), - "bsp/s" (a: Box>, b: Box>) => Box::new(Bsp::s(a, b)), - "bsp/e" (a: Box>, b: Box>) => Box::new(Bsp::e(a, b)), - "bsp/w" (a: Box>, b: Box>) => Box::new(Bsp::w(a, b)), - "bsp/a" (a: Box>, b: Box>) => Box::new(Bsp::a(a, b)), - "bsp/b" (a: Box>, b: Box>) => Box::new(Bsp::b(a, b)), + "fg" (color: Color, x: Box>) => Box::new(Tui::fg(color, x)), + "bg" (color: Color, x: Box>) => Box::new(Tui::bg(color, x)), + "fg/bg" (fg: Color, bg: Color, x: Box>) => Box::new(Tui::fg_bg(fg, bg, x)), + "bsp/n" (a: Box>, b: Box>) => Box::new(Bsp::n(a, b)), + "bsp/s" (a: Box>, b: Box>) => Box::new(Bsp::s(a, b)), + "bsp/e" (a: Box>, b: Box>) => Box::new(Bsp::e(a, b)), + "bsp/w" (a: Box>, b: Box>) => Box::new(Bsp::w(a, b)), + "bsp/a" (a: Box>, b: Box>) => Box::new(Bsp::a(a, b)), + "bsp/b" (a: Box>, b: Box>) => Box::new(Bsp::b(a, b)), + + "align/nw" (x: Box>) => Box::new(Align::nw(x)), + "align/ne" (x: Box>) => Box::new(Align::ne(x)), "align/n" (x: Box>) => Box::new(Align::n(x)), "align/s" (x: Box>) => Box::new(Align::s(x)), "align/e" (x: Box>) => Box::new(Align::e(x)), @@ -21,43 +25,46 @@ impl<'t> DslNs<'t, Box>> for App { "align/y" (x: Box>) => Box::new(Align::y(x)), "align/c" (x: Box>) => Box::new(Align::c(x)), - "fill/x" (x: Box>) => Box::new(Fill::x(x)), - "fill/y" (x: Box>) => Box::new(Fill::y(x)), - "fill/xy" (x: Box>) => Box::new(Fill::xy(x)), + "fill/x" (x: Box>) => Box::new(Fill::x(x)), + "fill/y" (x: Box>) => Box::new(Fill::y(x)), + "fill/xy" (x: Box>) => Box::new(Fill::xy(x)), "fixed/x" (x: u16, c: Box>) => Box::new(Fixed::x(x, c)), "fixed/y" (y: u16, c: Box>) => Box::new(Fixed::y(y, c)), "fixed/xy" (x: u16, y: u16, c: Box>) => Box::new(Fixed::xy(x, y, c)), - "min/x" (x: u16, c: Box>) => Box::new(Min::x(x, c)), - "min/y" (y: u16, c: Box>) => Box::new(Min::y(y, c)), - "min/xy" (x: u16, y: u16, c: Box>) => Box::new(Min::xy(x, y, c)), + "min/x" (x: u16, c: Box>) => Box::new(Min::x(x, c)), + "min/y" (y: u16, c: Box>) => Box::new(Min::y(y, c)), + "min/xy" (x: u16, y: u16, c: Box>) => Box::new(Min::xy(x, y, c)), - "max/x" (x: u16, c: Box>) => Box::new(Max::x(x, c)), - "max/y" (y: u16, c: Box>) => Box::new(Max::y(y, c)), - "max/xy" (x: u16, y: u16, c: Box>) => Box::new(Max::xy(x, y, c)), + "max/x" (x: u16, c: Box>) => Box::new(Max::x(x, c)), + "max/y" (y: u16, c: Box>) => Box::new(Max::y(y, c)), + "max/xy" (x: u16, y: u16, c: Box>) => Box::new(Max::xy(x, y, c)), }); dsl_words!(|app| -> Box> { ":templates" => Box::new({ let modes = app.config.modes.clone(); let height = (modes.read().unwrap().len() * 2) as u16; - Min::x(30, Fixed::y(height, Stack::south(move|add: &mut dyn FnMut(&dyn Render)|{ + Fixed::y(height, Min::x(30, Stack::south(move|add: &mut dyn FnMut(&dyn Render)|{ 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(""); let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); 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(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( + Bsp::a(field_name, field_id), field_info))))); + } + }))) + }), + ":sessions" => Box::new(Fixed::y(6, Min::x(30, Stack::south( move|add: &mut dyn FnMut(&dyn Render)|{ let fg = Rgb(224, 192, 128); 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)))))); } } @@ -82,7 +89,7 @@ impl<'t> DslNs<'t, Box>> for App { Fill::x(Tui::bg(bg, Bsp::e(lb, Bsp::w(rb, "FIXME device name")))) }))) }, }); /// Resolve an expression if known. - fn from_expr (&'t self, dsl: D) -> Perhaps>> { + fn from_expr (&self, dsl: &D) -> Perhaps>> { if let Some(head) = dsl.expr().head()? { for (key, value) in Self::EXPRS.0.iter() { if head == *key { @@ -93,7 +100,7 @@ impl<'t> DslNs<'t, Box>> for App { return Ok(None) } /// Resolve a symbol if known. - fn from_word (&'t self, dsl: D) -> Perhaps>> { + fn from_word (&self, dsl: &D) -> Perhaps>> { if let Some(dsl) = dsl.word()? { let views = self.config.views.read().unwrap(); if let Some(view) = views.get(dsl) { @@ -101,7 +108,11 @@ impl<'t> DslNs<'t, Box>> 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) } diff --git a/deps/tengri b/deps/tengri index 2557a0d2..d92e5efd 160000 --- a/deps/tengri +++ b/deps/tengri @@ -1 +1 @@ -Subproject commit 2557a0d253dfe45eab001dcc08ebc66c2c6715d3 +Subproject commit d92e5efdd0e009ba0492a75b3aad0b3c142b9056 diff --git a/tek.edn b/tek.edn index 3ae28b53..14b11ae6 100644 --- a/tek.edn +++ b/tek.edn @@ -1,12 +1,16 @@ (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 :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")))))))) +(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)))))))) (view :browse (bsp/s (padding/xy 3 1 :browse-title) (enclose (fg (g 96)) browser))) (keys :help (@f1 dialog :help)) From 17c6184f0ea2e8574002482d846b6777fdca926a Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 23 Aug 2025 13:25:26 +0300 Subject: [PATCH 2/3] refactor: config crate --- Cargo.lock | 9 +++ Cargo.toml | 2 + crates/app/Cargo.toml | 3 +- crates/app/app.rs | 121 +--------------------------------- crates/app/app_deps.rs | 4 +- crates/config/Cargo.toml | 11 ++++ crates/config/config.rs | 137 +++++++++++++++++++++++++++++++++++++++ deps/tengri | 2 +- 8 files changed, 165 insertions(+), 124 deletions(-) create mode 100644 crates/config/Cargo.toml create mode 100644 crates/config/config.rs diff --git a/Cargo.lock b/Cargo.lock index 9d6c381f..cf2b08e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2390,6 +2390,7 @@ dependencies = [ "proptest", "proptest-derive", "rand 0.8.5", + "tek_config", "tek_device", "tek_engine", "tengri", @@ -2406,6 +2407,14 @@ dependencies = [ "tek", ] +[[package]] +name = "tek_config" +version = "0.2.1" +dependencies = [ + "tengri", + "xdg", +] + [[package]] name = "tek_device" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 5d15805f..1506700f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ resolver = "2" members = [ "./crates/engine", "./crates/device", + "./crates/config", "./crates/app", "./crates/cli", ] @@ -34,6 +35,7 @@ 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" } diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 06ce641b..397f379b 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -7,8 +7,9 @@ version = { workspace = true } tengri = { workspace = true } tengri_proc = { workspace = true } -tek_engine = { workspace = true } +tek_config = { workspace = true } tek_device = { workspace = true } +tek_engine = { workspace = true } backtrace = { workspace = true } clap = { workspace = true, optional = true } diff --git a/crates/app/app.rs b/crates/app/app.rs index 5f9cac56..bdd705c7 100644 --- a/crates/app/app.rs +++ b/crates/app/app.rs @@ -1,13 +1,12 @@ #![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::*; 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 { @@ -32,23 +31,6 @@ pub struct App { /// Base color. pub color: ItemTheme, } -/// Configuration -#[derive(Default, Debug)] -pub struct Config { - pub dirs: BaseDirectories, - pub modes: Arc, Arc>>>>>, - pub views: Arc, Arc>>>, - pub binds: Arc, EventMap>>>>, -} -#[derive(Default, Debug)] -pub struct Mode { - pub path: PathBuf, - pub name: Vec, - pub info: Vec, - pub view: Vec, - pub keys: Vec, - pub modes: BTreeMap>, -} /// Various possible dialog modes. #[derive(Debug, Clone, Default)] pub enum Dialog { @@ -60,107 +42,6 @@ pub enum Dialog { Browser(BrowserTarget, Arc), Options, } -impl Config { - const CONFIG: &'static str = "tek.edn"; - const DEFAULTS: &'static str = include_str!("../../tek.edn"); - pub fn init () -> Usually { - 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, 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, 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, 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>, 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); diff --git a/crates/app/app_deps.rs b/crates/app/app_deps.rs index 508feda5..40bdf08f 100644 --- a/crates/app/app_deps.rs +++ b/crates/app/app_deps.rs @@ -1,7 +1,7 @@ pub use ::{ tek_engine::*, - tek_device, - tek_device::*, + tek_config::*, + tek_device::{self, *}, tengri::{ Usually, Perhaps, Has, MaybeHas, has, maybe_has, dsl::*, input::*, output::*, tui::*, diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml new file mode 100644 index 00000000..b5605dcd --- /dev/null +++ b/crates/config/Cargo.toml @@ -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 } diff --git a/crates/config/config.rs b/crates/config/config.rs new file mode 100644 index 00000000..ac485ef7 --- /dev/null +++ b/crates/config/config.rs @@ -0,0 +1,137 @@ +#![feature(if_let_guard)] + +use ::{ + std::{ + sync::{Arc, RwLock}, + collections::BTreeMap, + path::PathBuf + }, + tengri::{ + Usually, + input::{EventMap, Binding}, + tui::TuiEvent, + dsl::{Dsl, DslWord, DslExpr} + }, + xdg::BaseDirectories, +}; + +/// Configuration +#[derive(Default, Debug)] +pub struct Config { + pub dirs: BaseDirectories, + pub modes: Arc, Arc>>>>>, + pub views: Arc, Arc>>>, + pub binds: Arc, EventMap>>>>, +} + +#[derive(Default, Debug)] +pub struct Mode { + pub path: PathBuf, + pub name: Vec, + pub info: Vec, + pub view: Vec, + pub keys: Vec, + pub modes: BTreeMap>, +} + +impl Config { + const CONFIG: &'static str = "tek.edn"; + const DEFAULTS: &'static str = include_str!("../../tek.edn"); + pub fn init () -> Usually { + 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, 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, 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, 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>, 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()); + }) + } +} diff --git a/deps/tengri b/deps/tengri index d92e5efd..06217939 160000 --- a/deps/tengri +++ b/deps/tengri @@ -1 +1 @@ -Subproject commit d92e5efdd0e009ba0492a75b3aad0b3c142b9056 +Subproject commit 062179393037dc948a09aa24cb0000b6ef1dea8e From 5ccbb9719f11ad7370cf612715b6bd239c91b57a Mon Sep 17 00:00:00 2001 From: unspeaker Date: Sat, 23 Aug 2025 13:47:56 +0300 Subject: [PATCH 3/3] config: extract --- Cargo.lock | 15 ++++---- crates/config/config.rs | 82 +++++++++++++++++++++++++++++++++++++++-- deps/tengri | 2 +- 3 files changed, 87 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cf2b08e0..b6669678 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2456,7 +2456,7 @@ dependencies = [ [[package]] name = "tengri" -version = "0.13.0" +version = "0.14.0" dependencies = [ "tengri_core", "tengri_dsl", @@ -2467,11 +2467,11 @@ dependencies = [ [[package]] name = "tengri_core" -version = "0.13.0" +version = "0.14.0" [[package]] name = "tengri_dsl" -version = "0.13.0" +version = "0.14.0" dependencies = [ "const_panic", "itertools 0.14.0", @@ -2482,15 +2482,14 @@ dependencies = [ [[package]] name = "tengri_input" -version = "0.13.0" +version = "0.14.0" dependencies = [ "tengri_core", - "tengri_dsl", ] [[package]] name = "tengri_output" -version = "0.13.0" +version = "0.14.0" dependencies = [ "tengri_core", "tengri_dsl", @@ -2498,7 +2497,7 @@ dependencies = [ [[package]] name = "tengri_proc" -version = "0.13.0" +version = "0.14.0" dependencies = [ "heck", "proc-macro2", @@ -2509,7 +2508,7 @@ dependencies = [ [[package]] name = "tengri_tui" -version = "0.13.0" +version = "0.14.0" dependencies = [ "atomic_float", "better-panic", diff --git a/crates/config/config.rs b/crates/config/config.rs index ac485ef7..a44e955f 100644 --- a/crates/config/config.rs +++ b/crates/config/config.rs @@ -7,15 +7,16 @@ use ::{ path::PathBuf }, tengri::{ - Usually, - input::{EventMap, Binding}, + Usually, impl_debug, tui::TuiEvent, dsl::{Dsl, DslWord, DslExpr} }, xdg::BaseDirectories, }; -/// Configuration +/// Configuration. +/// +/// Contains mode, view, and bind definitions. #[derive(Default, Debug)] pub struct Config { pub dirs: BaseDirectories, @@ -24,6 +25,8 @@ pub struct Config { pub binds: Arc, EventMap>>>>, } +/// A set of currently active view and keys definitions, +/// with optional name and description. #[derive(Default, Debug)] pub struct Mode { pub path: PathBuf, @@ -34,6 +37,28 @@ pub struct Mode { pub modes: BTreeMap>, } +/// A collection of input bindings. +#[derive(Debug)] +pub struct EventMap( + /// Map of each event (e.g. key combination) to + /// all command expressions bound to it by + /// all loaded input layers. + pub BTreeMap>> +); + +/// An input binding. +#[derive(Debug, Clone)] +pub struct Binding { + pub commands: Arc<[C]>, + pub condition: Option, + pub description: Option>, + pub source: Option>, +} + +/// Input bindings are only returned if this evaluates to true +#[derive(Clone)] +pub struct Condition(Arcbool + Send + Sync>>); + impl Config { const CONFIG: &'static str = "tek.edn"; const DEFAULTS: &'static str = include_str!("../../tek.edn"); @@ -135,3 +160,54 @@ impl Config { }) } } + +/// Default is always empty map regardless if `E` and `C` implement [Default]. +impl Default for EventMap { + fn default () -> Self { Self(Default::default()) } +} + +impl EventMap { + /// 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) -> Self { + self.add(event, binding); + self + } + /// Add a binding to an event map. + pub fn add (&mut self, event: E, binding: Binding) -> &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]> { + 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> { + self.query(event) + .map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next()) + .flatten() + } +} + +impl Binding { + pub fn from_dsl (dsl: impl Dsl) -> Usually { + let command: Option = None; + let condition: Option = None; + let description: Option> = None; + let source: Option> = 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, "*") }); diff --git a/deps/tengri b/deps/tengri index 06217939..e3e3c163 160000 --- a/deps/tengri +++ b/deps/tengri @@ -1 +1 @@ -Subproject commit 062179393037dc948a09aa24cb0000b6ef1dea8e +Subproject commit e3e3c163da02165e77a259eb715749b7f0097498