diff --git a/Cargo.lock b/Cargo.lock index f0a12eed..18fb7be7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2386,6 +2386,7 @@ version = "0.2.1" dependencies = [ "backtrace", "clap", + "konst", "palette", "proptest", "proptest-derive", diff --git a/Cargo.toml b/Cargo.toml index 35b9e4ac..c9386a0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ atomic_float = { version = "1.0.0" } backtrace = { version = "0.3.72" } clap = { version = "4.5.4", features = [ "derive" ] } gtk = { version = "0.18.1" } +konst = { version = "0.3.16", features = [ "rust_1_83" ] } livi = { version = "0.7.4" } midly = { version = "0.5" } palette = { version = "0.7.6", features = [ "random" ] } diff --git a/bacon.toml b/bacon.toml new file mode 100644 index 00000000..e2eeae40 --- /dev/null +++ b/bacon.toml @@ -0,0 +1,55 @@ +# https://dystroy.org/bacon/config/ +default_job = "check" +env.CARGO_TERM_COLOR = "always" +[keybindings] +c = "job:check" +l = "job:clippy" +[jobs] +[jobs.check] +command = ["cargo", "check"] +need_stdout = false +watch = ["crates", "deps"] +[jobs.check-all] +command = ["cargo", "check", "--all-targets"] +need_stdout = false +[jobs.clippy] +command = ["cargo", "clippy"] +need_stdout = false +[jobs.clippy-all] +command = ["cargo", "clippy", "--all-targets"] +need_stdout = false +[jobs.test] +command = ["cargo", "test"] +need_stdout = true +[jobs.nextest] +command = [ + "cargo", "nextest", "run", + "--hide-progress-bar", "--failure-output", "final" +] +need_stdout = true +analyzer = "nextest" +[jobs.doc] +command = ["cargo", "doc", "--no-deps"] +need_stdout = false +[jobs.doc-open] +command = ["cargo", "doc", "--no-deps", "--open"] +need_stdout = false +on_success = "back" # so that we don't open the browser at each change +[jobs.run] +command = [ + "cargo", "run", + # put launch parameters for your program behind a `--` separator +] +need_stdout = true +allow_warnings = true +background = true +[jobs.run-long] +command = [ "cargo", "run", ] +need_stdout = true +allow_warnings = true +background = false +on_change_strategy = "kill_then_restart" +[jobs.ex] +command = ["cargo", "run", "--example"] +need_stdout = true +allow_warnings = true diff --git a/config/bindings.edn b/config/binds.edn similarity index 100% rename from config/bindings.edn rename to config/binds.edn diff --git a/config/profiles.edn b/config/views.edn similarity index 90% rename from config/profiles.edn rename to config/views.edn index 918d4d60..435a11e9 100644 --- a/config/profiles.edn +++ b/config/views.edn @@ -11,7 +11,7 @@ (mode :editor (keys :editor)) (mode :dialog (keys :dialog)) (mode :message (keys :message)) - (mode :device-add (keys :device-add)) + (mode :add-device (keys :add-device)) (mode :browser (keys :browser)) (mode :rename (keys :pool-rename)) (mode :length (keys :pool-length)) @@ -22,7 +22,6 @@ (keys :clock) (keys :arranger) (keys :global) - :view/dialog (bsp/w :view/meters/output (bsp/e :view/meters/input (stack/n (fixed/y 2 :view/status/h2) :view/tracks/inputs @@ -41,7 +40,6 @@ (keys :editor) (keys :sampler) (keys :global) - :view/dialog (bsp/w :view/meters/output (bsp/e :view/meters/input (bsp/w @@ -61,7 +59,6 @@ (info "A sampling soundboard.") (keys :sampler) (keys :global) - :view/dialog (bsp/s (fixed/y 1 :view/transport) (bsp/n (fixed/y 1 :view/status) (fill/xy :view/samples/grid)))) @@ -75,6 +72,7 @@ (keys :editor) (keys :clock) (keys :global) - :view/dialog - (bsp/s (fixed/y 1 :view/transport) (bsp/n (fixed/y 1 :view/status) - (fill/xy (bsp/a (fill/xy (align/e :view/pool)) :view/editor))))) + (bsp/s (fixed/y 1 :view/transport) + (bsp/n (fixed/y 1 :view/status) + (fill/xy (bsp/a (fill/xy (align/e :view/pool)) + :view/editor))))) diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 4283e9a9..06ce641b 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -12,6 +12,7 @@ tek_device = { workspace = true } backtrace = { workspace = true } clap = { workspace = true, optional = true } +konst = { workspace = true } palette = { workspace = true } rand = { workspace = true } toml = { workspace = true } diff --git a/crates/app/app.rs b/crates/app/app.rs index 176514ec..a3e195c4 100644 --- a/crates/app/app.rs +++ b/crates/app/app.rs @@ -16,6 +16,8 @@ #![feature(type_changing_struct_update)] #![feature(let_chains)] #![feature(closure_lifetime_binder)] +#![feature(generic_const_exprs)] +#![feature(generic_arg_infer)] pub use ::tek_engine:: *; pub use ::tek_device::{self, *}; pub use ::tengri::{Usually, Perhaps, Has, MaybeHas}; @@ -38,6 +40,7 @@ use std::collections::BTreeMap; use std::fmt::Write; use ::tengri::tui::ratatui::prelude::Position; use xdg::BaseDirectories; +mod app_view; pub use self::app_view::*; #[cfg(test)] mod app_test; /// Total state #[derive(Default, Debug)] @@ -51,7 +54,7 @@ pub struct App { /// Available view profiles and input bindings pub config: Config, /// Currently selected profile - pub profile: Option, + pub profile: Profile, /// Contains the currently edited musical arrangement pub project: Arrangement, /// Contains all recently created clips. @@ -59,34 +62,10 @@ pub struct App { /// Undo history pub history: Vec<(AppCommand, Option)>, /// Dialog overlay - pub dialog: Option, + pub dialog: Dialog, /// Base color. pub color: ItemTheme, } -/// Configuration -#[derive(Default, Debug)] -pub struct Config { - /// XDG basedirs - pub dirs: BaseDirectories, - /// Available view profiles - pub profiles: Arc, Profile>>>, - /// Available input bindings - pub bindings: Arc, Binding>>>, -} -/// Profile -#[derive(Default, Debug)] -pub struct Profile { - /// Path of configuration entrypoint - pub path: PathBuf, - /// Name of configuration - pub name: Option>, - /// Description of configuration - pub info: Option>, - /// View definition - pub view: Arc, - // Input keymap - pub keys: EventMap, -} macro_rules!dsl_expose(($Struct:ident { $($fn:ident: $ret:ty = |$self:ident|$body:expr);* $(;)? })=>{ #[tengri_proc::expose] impl $Struct { $(fn $fn (&$self) -> $ret { $body })* } }); @@ -96,9 +75,9 @@ dsl_expose!(App { w_sidebar: u16 = |self|self.project.w_sidebar(self.editor().is_some()); h_sample_detail: u16 = |self|6.max(self.height() as u16 * 3 / 9); focus_editor: bool = |self|self.project.editor.is_some(); - focus_dialog: bool = |self|self.dialog.is_some(); - focus_message: bool = |self|matches!(self.dialog, Some(Dialog::Message(..))); - focus_device_add: bool = |self|matches!(self.dialog, Some(Dialog::Device(..))); + focus_dialog: bool = |self|!matches!(self.dialog, Dialog::None); + focus_message: bool = |self|matches!(self.dialog, Dialog::Message(..)); + focus_add_device: bool = |self|matches!(self.dialog, Dialog::Device(..)); focus_browser: bool = |self|self.browser().is_some(); focus_clip: bool = |self|!self.focus_editor() && matches!(self.selection(), Selection::TrackClip{..}); focus_track: bool = |self|!self.focus_editor() && matches!(self.selection(), Selection::Track(..)); @@ -134,19 +113,152 @@ dsl_expose!(App { select_track_prev: Selection = |self|self.selection().select_track_prev(); clip_selected: Option>> = |self|match self.selection() { Selection::TrackClip { track, scene } => self.scenes()[*scene].clips[*track].clone(), - _ => None - }; - device_kind: usize = |self|if let Some(Dialog::Device(index)) = self.dialog { + _ => None }; + device_kind: usize = |self|if let Dialog::Device(index) = self.dialog { index } else { 0 }; - device_kind_next: usize = |self|if let Some(Dialog::Device(index)) = self.dialog { + device_kind_next: usize = |self|if let Dialog::Device(index) = self.dialog { (index + 1) % device_kinds().len() } else { 0 }; - device_kind_prev: usize = |self|if let Some(Dialog::Device(index)) = self.dialog { + device_kind_prev: usize = |self|if let Dialog::Device(index) = self.dialog { index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1)) } else { 0 }; }); +type ModuleMap = Arc, T>>>; +/// Configuration +#[derive(Default, Debug)] +pub struct Config { + /// XDG basedirs + pub dirs: BaseDirectories, + /// Available view profiles + pub views: ModuleMap, + /// Available input bindings + pub binds: ModuleMap, Arc>>, +} +impl Config { + const VIEWS: &'static str = "views.edn"; + const BINDS: &'static str = "binds.edn"; + const DEFAULT_VIEWS: &'static str = include_str!("../../config/views.edn"); + const DEFAULT_BINDS: &'static str = include_str!("../../config/binds.edn"); + pub fn init () -> Usually { + let mut cfgs: Self = Default::default(); + cfgs.dirs = BaseDirectories::with_profile("tek", "v0"); + cfgs.load(Self::VIEWS, Self::DEFAULT_VIEWS, |cfgs, dsl|cfgs.load_view(dsl))?; + cfgs.load(Self::BINDS, Self::DEFAULT_BINDS, |cfgs, dsl|cfgs.load_bind(dsl))?; + println!("{cfgs:#?}"); + Ok(cfgs) + } + pub fn load ( + &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!("init: {path}"); + std::fs::write(self.dirs.place_config_file(path)?, defaults); + } + Ok(if let Some(path) = self.dirs.find_config_file(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_view (&mut self, dsl: D) -> Usually<()> { + load_mod(dsl, |id, tail|Ok(self.views.write().unwrap().insert(id.into(), + Profile::from_dsl(tail)?))) + } + pub fn load_bind (&mut self, dsl: D) -> Usually<()> { + load_mod(dsl, |id, tail|Ok(self.binds.write().unwrap().insert(id.into(), { + let mut map = EventMap::new(); + tail.each(|item|{ + println!("{item:?}"); + map.add(TuiEvent::from_dsl(item.exp()?.head()?)?, Binding { + command: item.exp()?.tail()?.unwrap_or_default().into(), + condition: None, + description: None, + source: None + }); + Ok(()) + })?; + map + }))) + } +} +fn load_mod (dsl: D, cb: impl Fn(&str, &str)->Usually) -> Usually<()> { + //dsl!(dsl|module :id ..body|dsl!(body|@bind #info? ..commands|) + if let Some(exp) = dsl.exp()? + && Some("module") == exp.head().key()? + && let Some(tail) = exp.tail()? + && let Some(id) = tail.head().sym()? + && let Some(body) = tail.tail()? + { + let _ = cb(id, body)?; + Ok(()) + } else { + return Err("unexpected: {exp:?}".into()); + } +} +/// Profile +#[derive(Default, Debug)] +pub struct Profile { + /// Path of configuration entrypoint + pub path: PathBuf, + /// Name of configuration + pub name: Option>, + /// Description of configuration + pub info: Option>, + /// View definition + pub view: Arc, + // Input keymap + pub keys: EventMap, +} +impl Profile { + fn from_dsl (dsl: impl Dsl) -> Usually { + let mut profile = Self { ..Default::default() }; + dsl.each(|dsl|{ + let head = dsl.head(); + let exp = dsl.exp(); + Ok(if exp.head().key() == Ok(Some("name")) { + profile.name = Some(exp.tail()?.unwrap_or_default().into()); + } else if exp.head().key() == Ok(Some("info")) { + profile.info = Some(exp.tail()?.unwrap_or_default().into()); + }) + })?; + Ok(profile) + } +} +impl Profile { + fn load_template (&mut self, dsl: impl Dsl) -> Usually<&mut Self> { + //dsl.src()?.unwrap_or_default().each(|item|Ok(match () { + //_ if let Some(exp) = dsl.exp()? => match exp.head()?.key()? { + //Some("name") => match exp.tail()?.text()? { + //Some(name) => self.name = Some(name.into()), + //_ => return Err(format!("missing name definition").into()) + //}, + //Some("info") => match exp.tail()?.text()? { + //Some(info) => self.info = Some(info.into()), + //_ => return Err(format!("missing info definition").into()) + //}, + //Some("bind") => match exp.tail()? { + //Some(keys) => self.keys = EventMap::from_dsl(&mut &keys)?, + //_ => return Err(format!("missing keys definition").into()) + //}, + //Some("view") => match exp.tail()? { + //Some(tail) => self.view = tail.src()?.unwrap_or_default().into(), + //_ => return Err(format!("missing view definition").into()) + //}, + //dsl => return Err(format!("unexpected: {dsl:?}").into()) + //}, + //_ => return Err(format!("unexpected: {dsl:?}").into()) + //})); + Ok(self) + } + fn load_binding (&mut self, dsl: impl Dsl) -> Usually<&mut Self> { + todo!(); + Ok(self) + } +} /// Various possible dialog modes. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub enum Dialog { + #[default] None, Help(usize), Menu(usize), Device(usize), @@ -155,129 +267,13 @@ pub enum Dialog { Options, } impl App { - pub fn view <'a> (&'a self) -> impl Content + 'a { - Fill::xy(Bsp::a(self.view_overlay(), self.view_content())) - } pub fn update_clock (&self) { ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) } } -macro_rules!dsl_view(($Struct:ident { $($fn:ident = |$self:ident $(, $arg:ident:$ty:ty)*|$body:expr);* $(;)? })=>{ - content!(TuiOut: |self: $Struct| { - ErrorBoundary::new(Ok(Some(Tui::bg(Black, self.view())))) - }); - #[tengri_proc::view(TuiOut)] impl $Struct { $( - fn $fn (&$self $(, $arg: $ty)*) -> impl Content + '_ { $body } - )* } -}); -dsl_view!(App { - view_nil = |self|"·"; - view_content = |self|Fill::xy(ErrorBoundary::new(Ok(Some(self.view_nil())))); - view_overlay = |self|Fill::xy(ErrorBoundary::new(Ok(Some(Tui::bg(Black, ThunkRender::new(|to| - match self.dialog.as_ref() { - Some(Dialog::Help(scrolled)) => Render::render(&self.view_help(*scrolled), to), - Some(Dialog::Menu(selected)) => Render::render(&self.view_menu(*selected), to), - Some(Dialog::Device(selected)) => Render::render(&self.view_device(*selected), to), - Some(Dialog::Message(message)) => Render::render(&self.view_message(message.clone()), to), - Some(Dialog::Browser(target, browser)) => Render::render(&self.view_browser(target.clone(), browser.clone()), to), - Some(Dialog::Options) => Render::render(&self.view_options(), to), - _ => unimplemented!() })))))); - view_menu = |self, selected: usize|{ - let profiles = self.config.profiles.clone(); - wrap_dialog(wrap_dialog_menu(Stack::south(move|add: &mut dyn FnMut(&dyn Render)|{ - ////let options = ||["Projects", "Settings", "Help", "Quit"].iter(); - ////let option = |a,i|Tui::fg(Rgb(255,255,255), format!("{}", a)); - ////Bsp::s(Tui::bold(true, "tek!"), Bsp::s("", Map::south(1, options, option))) - for (index, (id, profile)) in profiles.read().unwrap().iter().enumerate() { - let bg = if index == 0 { Rgb(64,64,64) } else { Rgb(32,32,32) }; - let name = profile.name.as_ref().map(|x|unquote(unquote(x.as_ref()))); - let info = profile.info.as_ref().map(|x|unquote(unquote(x.as_ref()))); - add(&Fixed::y(3, Tui::bg(bg, Bsp::s( - Fill::x(Bsp::a( - Fill::x(Align::w(Tui::fg(Rgb(224,192,128), name))), - Fill::x(Align::e(Tui::fg(Rgb(224,128,32), &id))) - )), - Fill::x(Align::w(info)))))); } }))) }; - view_help = |self, scrolled: usize|wrap_dialog(Bsp::s(Tui::bold(true, "Help"), "TODO")); - view_options = |self|wrap_dialog("TODO"); - view_message = |self, message: Arc|wrap_dialog(Bsp::s(message.clone(), Bsp::s("", "[ OK ]"))); - view_browser = |self, target: BrowserTarget, browser: Browser|wrap_dialog(Bsp::s( - Padding::xy(3, 1, Fill::x(Align::w(FieldV( - Default::default(), - match target { - BrowserTarget::SaveProject => "Save project:", - BrowserTarget::LoadProject => "Load project:", - BrowserTarget::ImportSample(_) => "Import sample:", - BrowserTarget::ExportSample(_) => "Export sample:", - BrowserTarget::ImportClip(_) => "Import clip:", - BrowserTarget::ExportClip(_) => "Export clip:", - }, - Shrink::x(3, Fixed::y(1, Tui::fg(Tui::g(96), RepeatH("🭻")))))))), - Outer(true, Style::default().fg(Tui::g(96))) - .enclose(Fill::xy(browser)))); - view_device = |self, selected: usize|wrap_dialog(Bsp::s( - Tui::bold(true, "Add device"), - Map::south(1, - move||device_kinds().iter(), - move|label: &&'static str, i| - Fill::x(Tui::bg(if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }, - Bsp::e(if i == selected { "[ " } else { " " }, - Bsp::w(if i == selected { " ]" } else { " " }, - "FIXME device name"))))))); - //Bsp::s("", - //Map::south(1, - //move||self.config.bindings.layers.iter() - //.filter_map(|a|(a.0)(self).then_some(a.1)) - //.flat_map(|a|a) - //.filter_map(|x|if let Value::Exp(_, iter)=x.value{ Some(iter) } else { None }) - //.skip(offset) - //.take(20), - //|mut b,i|Fixed::x(60, Align::w(Bsp::e("(", Bsp::e( - //b.next().map(|t|Fixed::x(16, Align::w(Tui::fg(Rgb(64,224,0), format!("{}", t.value))))), - //Bsp::e(" ", Align::w(format!("{}", b.0.0.trim()))))))))))), - - //Dialog::Browser(BrowserTarget::Load, browser) => { - //"bobcat".boxed() - ////Bsp::s( - ////Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( - ////Tui::bold(true, " Load project: "), - ////Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))), - ////Outer(true, Style::default().fg(Tui::g(96))) - ////.enclose(Fill::xy(browser))) - //}, - //Dialog::Browser(BrowserTarget::Export, browser) => { - //"bobcat".boxed() - ////Bsp::s( - ////Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( - ////Tui::bold(true, " Export: "), - ////Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))), - ////Outer(true, Style::default().fg(Tui::g(96))) - ////.enclose(Fill::xy(browser))) - //}, - //Dialog::Browser(BrowserTarget::Import, browser) => { - //"bobcat".boxed() - ////Bsp::s( - ////Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( - ////Tui::bold(true, " Import: "), - ////Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))), - ////Outer(true, Style::default().fg(Tui::g(96))) - ////.enclose(Fill::xy(browser))) - //}, -}); -fn wrap_dialog <'a> (dialog: impl Content + 'a) -> Box + 'a> { - 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)))).boxed() -} -fn wrap_dialog_menu <'a> (content: impl Content + 'a) -> Box + 'a> { - Tui::bg(Rgb(0,0,0), Bsp::s( - Fill::x(Fixed::y(3, Tui::bg(Rgb(33,33,33), Tui::bold(true, "tek 0.3.0-rc0")))), - Bsp::n( - Fill::x(Fixed::y(3, Tui::bg(Rgb(33,33,33), Bsp::e(Tui::fg(Rgb(255,192,48), "[Enter]"), " new session")))), - Fill::y(Align::n(Fill::x(content)))))).boxed() -} has!(Jack<'static>: |self: App|self.jack); has!(Pool: |self: App|self.pool); -has!(Option: |self: App|self.dialog); +has!(Dialog: |self: App|self.dialog); has!(Clock: |self: App|self.project.clock); has!(Option: |self: App|self.project.editor); has!(Selection: |self: App|self.project.selection); @@ -294,114 +290,6 @@ maybe_has!(Scene: |self: App| { MaybeHas::::get(&self.project) }; impl HasClipsSize for App { fn clips_size (&self) -> &Measure { &self.project.inner_size } } impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() } } impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } } -impl Profile { - fn from_dsl (dsl: impl Dsl) -> Usually { - let mut profile = Self { ..Default::default() }; - dsl.each(|dsl|{ - let head = dsl.head(); - let exp = dsl.exp(); - Ok(if exp.head().key() == Ok(Some("name")) { - profile.name = Some(exp.tail()?.unwrap_or_default().into()); - } else if exp.head().key() == Ok(Some("info")) { - profile.info = Some(exp.tail()?.unwrap_or_default().into()); - }) - })?; - Ok(profile) - } -} -impl Config { - const PROFILES: &'static str = "profiles.edn"; - const BINDINGS: &'static str = "bindings.edn"; - const DEFAULT_PROFILES: &'static str = include_str!("../../config/profiles.edn"); - const DEFAULT_BINDINGS: &'static str = include_str!("../../config/bindings.edn"); - pub fn init () -> Usually { - let mut dirs = BaseDirectories::with_profile("tek", "v0"); - let mut cfgs = Self { dirs, ..Default::default() }; - cfgs.init_file(Self::PROFILES, Self::DEFAULT_PROFILES)?; - cfgs.load_file(Self::PROFILES, |cfgs, dsl|Ok( - if dsl.exp().head().key() == Ok(Some("module")) { - let exp = dsl.exp()?; - let tail = exp.tail()?; - let head = tail.head()?; - if let Some(id) = head.sym()? { - cfgs.profiles.write().unwrap().insert( - id.into(), - Profile::from_dsl(tail.tail()?)? - ); - } - } else { - return Err("unexpected: {exp:?}".into()); - } - ))?; - cfgs.init_file(Self::BINDINGS, Self::DEFAULT_BINDINGS)?; - cfgs.load_file(Self::BINDINGS, |cfgs, dsl|Ok( - if dsl.exp().head().key() == Ok(Some("module")) { - let exp = dsl.exp()?; - let tail = exp.tail()?; - let head = tail.head()?; - if let Some(id) = head.sym()? { - cfgs.bindings.write().unwrap().insert( - id.into(), - Binding::from_dsl(tail.tail()?)? - ); - } - } else { - return Err("unexpected: {exp:?}".into()); - } - ))?; - println!("{cfgs:#?}"); - Ok(cfgs) - } - fn init_file (&mut self, path: &str, val: &str) -> Usually<()> { - if self.dirs.find_config_file(path).is_none() { - std::fs::write(self.dirs.place_config_file(path)?, val); - } - Ok(()) - } - fn load_file ( - &mut self, - path: &str, - mut each: impl FnMut(&mut Self, &str)->Usually<()> - ) -> Usually<()> { - Ok(if let Some(path) = self.dirs.find_config_file(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()) - }) - } -} -impl Profile { - fn load_template (&mut self, dsl: impl Dsl) -> Usually<&mut Self> { - dsl.src()?.unwrap_or_default().each(|item|Ok(match () { - _ if let Some(exp) = dsl.exp()? => match exp.head()?.key()? { - Some("name") => match exp.tail()?.text()? { - Some(name) => self.name = Some(name.into()), - _ => return Err(format!("missing name definition").into()) - }, - Some("info") => match exp.tail()?.text()? { - Some(info) => self.info = Some(info.into()), - _ => return Err(format!("missing info definition").into()) - }, - Some("bind") => match exp.tail()? { - Some(keys) => self.keys = EventMap::from_dsl(&mut &keys)?, - _ => return Err(format!("missing keys definition").into()) - }, - Some("view") => match exp.tail()? { - Some(tail) => self.view = tail.src()?.unwrap_or_default().into(), - _ => return Err(format!("missing view definition").into()) - }, - dsl => return Err(format!("unexpected: {dsl:?}").into()) - }, - _ => return Err(format!("unexpected: {dsl:?}").into()) - })); - Ok(self) - } - fn load_binding (&mut self, dsl: impl Dsl) -> Usually<&mut Self> { - todo!(); - Ok(self) - } -} fn unquote (x: &str) -> &str { let mut chars = x.chars(); chars.next(); @@ -423,17 +311,17 @@ impl ScenesView for App { impl App { fn handle_tui_key_with_history (&mut self, input: &TuiIn) -> Perhaps { panic!("{input:?}"); - Ok(if let Some(binding) = self.profile.as_ref() - .map(|c|c.keys.dispatch(input.event())).flatten() - { - let binding = binding.clone(); - let undo = binding.command.clone().execute(self)?; - // FIXME failed commands are not persisted in undo history - //self.history.push((binding.command.clone(), undo)); - Some(true) - } else { - None - }) + //Ok(if let Some(binding) = self.profile.as_ref() + //.map(|c|c.keys.dispatch(input.event())).flatten() + //{ + //let binding = binding.clone(); + //let undo = binding.command.clone().execute(self)?; + //// FIXME failed commands are not persisted in undo history + ////self.history.push((binding.command.clone(), undo)); + //Some(true) + //} else { + //None + //}) } } @@ -452,25 +340,20 @@ macro_rules!dsl_bind(($Command:ident: $State:ident { }); dsl_bind!(AppCommand: App { - enqueue = |app, clip: Option>>| { - todo!() }; - history = |app, delta: isize| { - todo!() }; - zoom = |app, zoom: usize| { - todo!() }; - stop_all = |app| { - app.tracks_stop_all(); Ok(None) }; - dialog = |app, command: DialogCommand|Ok( - command.delegate(&mut app.dialog, |command|Self::Dialog{command})?); - project = |app, command: ArrangementCommand|Ok( - command.delegate(&mut app.project, |command|Self::Project{command})?); - clock = |app, command: ClockCommand|Ok( - command.execute(app.clock_mut())?.map(|command|Self::Clock{command})); - sampler = |app, command: SamplerCommand|Ok(app.project.sampler_mut() - .map(|s|command.delegate(s, |command|Self::Sampler{command})) - .transpose()? - .flatten()); - pool = |app, command: PoolCommand| { + enqueue = |app, clip: Option>>| { todo!() }; + history = |app, delta: isize| { todo!() }; + zoom = |app, zoom: usize| { todo!() }; + stop_all = |app| { app.tracks_stop_all(); Ok(None) }; + //dialog = |app, command: DialogCommand| + //Ok(command.delegate(&mut app.dialog, |c|Self::Dialog{command: c})?); + project = |app, command: ArrangementCommand| + Ok(command.delegate(&mut app.project, |c|Self::Project{command: c})?); + clock = |app, command: ClockCommand| + Ok(command.execute(app.clock_mut())?.map(|c|Self::Clock{command: c})); + sampler = |app, command: SamplerCommand| + Ok(app.project.sampler_mut().map(|s|command.delegate(s, |command|Self::Sampler{command})) + .transpose()?.flatten()); + pool = |app, command: PoolCommand| { let undo = command.clone().delegate(&mut app.pool, |command|AppCommand::Pool{command})?; // update linked editor after pool action match command { @@ -547,6 +430,13 @@ dsl_bind!(AppCommand: App { //editor_h = 15; //is_editing = self.editor.is_some(); //}); +impl Dialog { + fn menu_selected (&self) -> Option { todo!() } + fn device_selected (&self) -> Option { todo!() } + fn message (&self) -> Option<&str> { todo!() } + fn browser (&self) -> Option<&Browser> { todo!() } + fn browser_target (&self) -> Option<&BrowserTarget> { todo!() } +} #[tengri_proc::command(Option)] impl DialogCommand { fn open (dialog: &mut Option, new: Dialog) -> Perhaps { *dialog = Some(new); @@ -613,7 +503,7 @@ audio!( ); impl App { - pub fn toggle_dialog (&mut self, mut dialog: Option) -> Option { + pub fn toggle_dialog (&mut self, mut dialog: Dialog) -> Dialog { std::mem::swap(&mut self.dialog, &mut dialog); dialog } @@ -656,15 +546,12 @@ impl App { } } pub fn browser (&self) -> Option<&Browser> { - self.dialog.as_ref().and_then(|dialog|match dialog { - Dialog::Browser(_, b) => Some(b), - _ => None - }) + if let Dialog::Browser(_, ref b) = self.dialog { Some(b) } else { None } } pub fn device_pick (&mut self, index: usize) { - self.dialog = Some(Dialog::Device(index)); + self.dialog = Dialog::Device(index); } - pub fn device_add (&mut self, index: usize) -> Usually<()> { + pub fn add_device (&mut self, index: usize) -> Usually<()> { match index { 0 => { let name = self.jack.with_client(|c|c.name().to_string()); @@ -675,10 +562,10 @@ impl App { let sampler = if let Ok(sampler) = Sampler::new( &self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]] ) { - self.dialog = None; + self.dialog = Dialog::None; Device::Sampler(sampler) } else { - self.dialog = Some(Dialog::Message("Failed to add device.".into())); + self.dialog = Dialog::Message("Failed to add device.".into()); return Err("failed to add device".into()) }; let track = self.track_mut().expect("no active track"); @@ -908,4 +795,3 @@ impl App { //self.project.sampler().map(|s|Outer(true, Style::default().fg(Tui::g(96))).enclose( //Fill::y(Align::n(s.view_sample_status(self.editor().unwrap().get_note_pos()))))) //} -//} diff --git a/crates/app/app_view.rs b/crates/app/app_view.rs new file mode 100644 index 00000000..0f2244d6 --- /dev/null +++ b/crates/app/app_view.rs @@ -0,0 +1,159 @@ +use crate::*; +pub(crate) use std::marker::PhantomData; +content!(TuiOut:|self: App|"kyp");//VIEW.content(self, "(bg :color/black (fill/xy (bsp/a overlay content)))")); +struct DslView<'s, O: Output, T> { + symbol: &'s str, + callback: Option fn (&'t T) -> Box + 't>>, + next: Option<&'s Self> +} +pub const VIEW: DslView<'static, TuiOut, App> = DslView::new() + .def("browser", view_browser) + .def("content", view_content) + .def("device", view_device) + .def("menu", view_menu) + //.def("options", view_options) + .def("overlay", view_overlay); +impl<'s, O: Output, T> DslView<'s, O, T> { + pub const fn new () -> Self { + DslView { symbol: "", callback: None, next: None } + } + pub const fn def ( + &'static self, + symbol: &'static str, + callback: fn (&T) -> Box + '_> + ) -> Self { + DslView { symbol, callback: Some(callback), next: Some(self) } + } + pub fn content <'t> ( + &'static self, state: &'t T, src: impl Dsl) -> Usually + 't>> { + Ok(if src.sym()? == Some(self.symbol) && let Some(callback) = self.callback { + callback(&state) + } else if let Some(next) = self.next { + next.content(state, src)? + } else { + return Err("DslView::content: undefined".into()) + }) + } + //fn render (&self, state: &T, output: &mut O, src: impl Dsl) -> Usually<()> { + //Ok(if src.sym()? == Some(self.0) && let Some(callback) = self.1 { + //let view = callback(&state); + //output.place(output.area(), &view) + //} else if let Some(next) = self.2 { + //next.render(state, output, src)? + //} else { + //return Err("DslView::render: undefined".into()) + //}) + //} +} + + +fn view_content (app: &App) -> Box + '_> { + Fill::xy(ErrorBoundary::new(Ok(Some("·")))).boxed() +} +fn view_overlay <'s> (state: &'s App) -> Box + 's> { + match &state.dialog { + Dialog::Menu(_) => view_menu(state), + Dialog::Device(_) => view_device(state), + Dialog::Browser(_, _) => view_browser(state), + //Dialog::Help(_) => view_help, + //Dialog::Message(_) => view_message, + //Dialog::Options => view_options, + _ => unimplemented!() + } +} +fn wrap_dialog <'s> (dialog: impl Content + 's) -> impl Content + 's { + 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)))) +} +fn wrap_dialog_menu <'a> (content: impl Content + 'a) -> Box + 'a> { + Box::new(Tui::bg(Rgb(0,0,0), Bsp::s( + Fill::x(Fixed::y(3, Tui::bg(Rgb(33,33,33), Tui::bold(true, "tek 0.3.0-rc0")))), + Bsp::n( + Fill::x(Fixed::y(3, Tui::bg(Rgb(33,33,33), Bsp::e(Tui::fg(Rgb(255,192,48), "[Enter]"), " new session")))), + Fill::y(Align::n(Fill::x(content))))))) +} +fn view_menu <'s> (state: &'s App) -> Box + 's> { + let views = state.config.views.clone(); + let selected = state.dialog.menu_selected(); + wrap_dialog_menu(Stack::south(move|add: &mut dyn FnMut(&dyn Render)|{ + ////let options = ||["Projects", "Settings", "Help", "Quit"].iter(); + ////let option = |a,i|Tui::fg(Rgb(255,255,255), format!("{}", a)); + ////Bsp::s(Tui::bold(true, "tek!"), Bsp::s("", Map::south(1, options, option))) + for (index, (id, profile)) in views.read().unwrap().iter().enumerate() { + let bg = if index == 0 { Rgb(64,64,64) } else { Rgb(32,32,32) }; + let name = profile.name.as_ref().map(|x|unquote(unquote(x.as_ref()))); + let info = profile.info.as_ref().map(|x|unquote(unquote(x.as_ref()))); + add(&Fixed::y(3, Tui::bg(bg, Bsp::s( + Fill::x(Bsp::a( + Fill::x(Align::w(Tui::fg(Rgb(224,192,128), name))), + Fill::x(Align::e(Tui::fg(Rgb(224,128,32), &id))) + )), + Fill::x(Align::w(info)))))); } })) +} +fn view_browser <'s> (state: &'s App) -> Box + 's> { + let target = &state.dialog.browser_target().clone().unwrap(); + Box::new(Bsp::s( + Padding::xy(3, 1, Fill::x(Align::w(FieldV( + Default::default(), + match &target { + BrowserTarget::SaveProject => "Save project:", + BrowserTarget::LoadProject => "Load project:", + BrowserTarget::ImportSample(_) => "Import sample:", + BrowserTarget::ExportSample(_) => "Export sample:", + BrowserTarget::ImportClip(_) => "Import clip:", + BrowserTarget::ExportClip(_) => "Export clip:", + }, + Shrink::x(3, Fixed::y(1, Tui::fg(Tui::g(96), RepeatH("🭻")))))))), + Outer(true, Style::default().fg(Tui::g(96))) + .enclose(Fill::xy(state.dialog.browser())))) +} +fn view_device <'s> (state: &'s App) -> Box + 's> { + let selected = state.dialog.device_selected().unwrap(); + Box::new(Bsp::s(Tui::bold(true, "Add device"), Map::south(1, + move||device_kinds().iter(), + move|label: &&'static str, i| + Fill::x(Tui::bg(if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }, + Bsp::e(if i == state.dialog.device_selected().unwrap() { "[ " } else { " " }, + Bsp::w(if i == state.dialog.device_selected().unwrap() { " ]" } else { " " }, + "FIXME device name"))))))) +} + + //Bsp::s("", + //Map::south(1, + //move||app.config.binds.layers.iter() + //.filter_map(|a|(a.0)(app).then_some(a.1)) + //.flat_map(|a|a) + //.filter_map(|x|if let Value::Exp(_, iter)=x.value{ Some(iter) } else { None }) + //.skip(offset) + //.take(20), + //|mut b,i|Fixed::x(60, Align::w(Bsp::e("(", Bsp::e( + //b.next().map(|t|Fixed::x(16, Align::w(Tui::fg(Rgb(64,224,0), format!("{}", t.value))))), + //Bsp::e(" ", Align::w(format!("{}", b.0.0.trim()))))))))))), + + //Dialog::Browser(BrowserTarget::Load, browser) => { + //"bobcat".boxed() + ////Bsp::s( + ////Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( + ////Tui::bold(true, " Load project: "), + ////Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))), + ////Outer(true, Style::default().fg(Tui::g(96))) + ////.enclose(Fill::xy(browser))) + //}, + //Dialog::Browser(BrowserTarget::Export, browser) => { + //"bobcat".boxed() + ////Bsp::s( + ////Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( + ////Tui::bold(true, " Export: "), + ////Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))), + ////Outer(true, Style::default().fg(Tui::g(96))) + ////.enclose(Fill::xy(browser))) + //}, + //Dialog::Browser(BrowserTarget::Import, browser) => { + //"bobcat".boxed() + ////Bsp::s( + ////Fill::x(Align::w(Margin::xy(1, 1, Bsp::e( + ////Tui::bold(true, " Import: "), + ////Shrink::x(3, Fixed::y(1, RepeatH("🭻"))))))), + ////Outer(true, Style::default().fg(Tui::g(96))) + ////.enclose(Fill::xy(browser))) + //}, diff --git a/crates/cli/tek.rs b/crates/cli/tek.rs index fbd67aeb..529eb63a 100644 --- a/crates/cli/tek.rs +++ b/crates/cli/tek.rs @@ -78,7 +78,7 @@ impl Cli { jack: jack.clone(), config: Config::init()?, color: ItemTheme::random(), - dialog: Some(Dialog::Menu(0)), + dialog: Dialog::Menu(0), project: Arrangement { name: Default::default(), color: ItemTheme::random(), diff --git a/deps/tengri b/deps/tengri index b52c1f58..4515d2cc 160000 --- a/deps/tengri +++ b/deps/tengri @@ -1 +1 @@ -Subproject commit b52c1f582880d08e663411e9238d3fdacfda9473 +Subproject commit 4515d2cc6f9d8d67782c175ebc5b560923295314