diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..326cbf0c --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.'cfg(target_os = "linux")'] +rustflags = ["-C", "link-arg=-fuse-ld=mold"] diff --git a/Cargo.lock b/Cargo.lock index 8361e92f..23cc9338 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -250,6 +250,28 @@ dependencies = [ "objc2", ] +[[package]] +name = "builder-pattern" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85376b93d8efe18dd819f56505e33e7a9c0f93fb02bd761f8690026178ed6e5" +dependencies = [ + "builder-pattern-macro", + "futures", +] + +[[package]] +name = "builder-pattern-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d624ef88b39588d113f807ffb38ee968aafc388ca57dd7a9a7b82d3de1f5f4" +dependencies = [ + "bitflags 1.3.2", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -376,7 +398,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -454,7 +476,7 @@ checksum = "eb0240417fe20ccf13397fa25e6f0a987dbbfaf27d9e13532419df7f593e65e8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", "unicode-xid", ] @@ -591,7 +613,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -620,7 +642,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.116", ] [[package]] @@ -631,7 +653,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -653,7 +675,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 2.0.116", ] [[package]] @@ -793,7 +815,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -802,12 +824,71 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + [[package]] name = "futures-task" version = "0.3.32" @@ -820,8 +901,13 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "slab", ] @@ -960,7 +1046,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -1330,7 +1416,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -1598,7 +1684,7 @@ dependencies = [ "by_address", "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -1691,7 +1777,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -1720,7 +1806,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -1765,7 +1851,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.116", ] [[package]] @@ -1813,7 +1899,7 @@ checksum = "4ee1c9ac207483d5e7db4940700de86a9aae46ef90c48b57f99fe7edb8345e49" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -2167,7 +2253,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -2318,7 +2404,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 2.0.116", ] [[package]] @@ -2516,6 +2602,17 @@ dependencies = [ "symphonia-metadata", ] +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.116" @@ -2534,6 +2631,7 @@ dependencies = [ "ansi_term", "atomic_float", "backtrace", + "builder-pattern", "clap", "jack", "konst", @@ -2545,6 +2643,7 @@ dependencies = [ "rand 0.8.5", "symphonia", "tek_device", + "tek_engine", "tengri", "toml", "uuid", @@ -2672,7 +2771,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -2683,7 +2782,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] @@ -2958,7 +3057,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.116", "wasm-bindgen-shared", ] @@ -3432,7 +3531,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn", + "syn 2.0.116", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -3448,7 +3547,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.116", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -3570,7 +3669,7 @@ checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.116", ] [[package]] diff --git a/app/.scratch.rs b/app/.scratch.rs new file mode 100644 index 00000000..b3e36a2f --- /dev/null +++ b/app/.scratch.rs @@ -0,0 +1,379 @@ + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +//pub fn view_nil (_: &App) -> TuiCb { + //|to|to.place(&Fill::XY("ยท")) +//} + + //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::Browse(BrowseTarget::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::Browse(BrowseTarget::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::Browse(BrowseTarget::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))) + //}, +// + //pub fn view_history (&self) -> impl Content { + //Fixed::Y(1, Fill::X(Align::w(FieldH(self.color, + //format!("History ({})", self.history.len()), + //self.history.last().map(|last|Fill::X(Align::w(format!("{:?}", last.0)))))))) + //} + //pub fn view_status_h2 (&self) -> impl Content { + //self.update_clock(); + //let theme = self.color; + //let clock = self.clock(); + //let playing = clock.is_rolling(); + //let cache = clock.view_cache.clone(); + ////let selection = self.selection().describe(self.tracks(), self.scenes()); + //let hist_len = self.history.len(); + //let hist_last = self.history.last(); + //Fixed::Y(2, Stack::east(move|add: &mut dyn FnMut(&dyn Draw)|{ + //add(&Fixed::X(5, Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, + //Either::new(false, // TODO + //Thunk::new(move||Fixed::X(9, Either::new(playing, + //Tui::fg(Rgb(0, 255, 0), " PLAYING "), + //Tui::fg(Rgb(255, 128, 0), " STOPPED "))) + //), + //Thunk::new(move||Fixed::X(5, Either::new(playing, + //Tui::fg(Rgb(0, 255, 0), Bsp::s(" ๐Ÿญ๐Ÿญ‘๐Ÿฌฝ ", " ๐Ÿญž๐Ÿญœ๐Ÿญ˜ ",)), + //Tui::fg(Rgb(255, 128, 0), Bsp::s(" โ–—โ–„โ–– ", " โ–โ–€โ–˜ ",)))) + //) + //) + //))); + //add(&" "); + //{ + //let cache = cache.read().unwrap(); + //add(&Fixed::X(15, Align::w(Bsp::s( + //FieldH(theme, "Beat", cache.beat.view.clone()), + //FieldH(theme, "Time", cache.time.view.clone()), + //)))); + //add(&Fixed::X(13, Align::w(Bsp::s( + //Fill::X(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), + //Fill::X(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), + //)))); + //add(&Fixed::X(12, Align::w(Bsp::s( + //Fill::X(Align::w(FieldH(theme, "Buf", cache.buf.view.clone()))), + //Fill::X(Align::w(FieldH(theme, "Lat", cache.lat.view.clone()))), + //)))); + ////add(&Bsp::s( + //////Fill::X(Align::w(FieldH(theme, "Selected", Align::w(selection)))), + ////Fill::X(Align::w(FieldH(theme, format!("History ({})", hist_len), + ////hist_last.map(|last|Fill::X(Align::w(format!("{:?}", last.0))))))), + ////"" + ////)); + //////if let Some(last) = self.history.last() { + //////add(&FieldV(theme, format!("History ({})", self.history.len()), + //////Fill::X(Align::w(format!("{:?}", last.0))))); + //////} + //} + //})) + //} + //pub fn view_status_v (&self) -> impl Content + use<'_> { + //self.update_clock(); + //let cache = self.project.clock.view_cache.read().unwrap(); + //let theme = self.color; + //let playing = self.clock().is_rolling(); + //Tui::bg(theme.darker.rgb, Fixed::XY(20, 5, Outer(true, Style::default().fg(Tui::g(96))).enclose( + //col!( + //Fill::X(Align::w(Bsp::e( + //Align::w(Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, + //Either::new(false, // TODO + //Thunk::new(move||Fixed::X(9, Either::new(playing, + //Tui::fg(Rgb(0, 255, 0), " PLAYING "), + //Tui::fg(Rgb(255, 128, 0), " STOPPED "))) + //), + //Thunk::new(move||Fixed::X(5, Either::new(playing, + //Tui::fg(Rgb(0, 255, 0), Bsp::s(" ๐Ÿญ๐Ÿญ‘๐Ÿฌฝ ", " ๐Ÿญž๐Ÿญœ๐Ÿญ˜ ",)), + //Tui::fg(Rgb(255, 128, 0), Bsp::s(" โ–—โ–„โ–– ", " โ–โ–€โ–˜ ",)))) + //) + //) + //)), + //Bsp::s( + //FieldH(theme, "Beat", cache.beat.view.clone()), + //FieldH(theme, "Time", cache.time.view.clone()), + //), + //))), + //Fill::X(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), + //Fill::X(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), + //Fill::X(Align::w(FieldH(theme, "Buf", Bsp::e(cache.buf.view.clone(), Bsp::e(" = ", cache.lat.view.clone()))))), + //)))) + //} + //pub fn view_status (&self) -> impl Content + use<'_> { + //self.update_clock(); + //let cache = self.project.clock.view_cache.read().unwrap(); + //view_status(Some(self.project.selection.describe(self.tracks(), self.scenes())), + //cache.sr.view.clone(), cache.buf.view.clone(), cache.lat.view.clone()) + //} + //pub fn view_transport (&self) -> impl Content + use<'_> { + //self.update_clock(); + //let cache = self.project.clock.view_cache.read().unwrap(); + //view_transport(self.project.clock.is_rolling(), + //cache.bpm.view.clone(), cache.beat.view.clone(), cache.time.view.clone()) + //} + //pub fn view_editor (&self) -> impl Content + use<'_> { + //let bg = self.editor() + //.and_then(|editor|editor.clip().clone()) + //.map(|clip|clip.read().unwrap().color.darker) + //.unwrap_or(self.color.darker); + //Fill::XY(Tui::bg(bg.rgb, self.editor())) + //} + //pub fn view_editor_status (&self) -> impl Content + use<'_> { + //self.editor().map(|e|Fixed::X(20, Outer(true, Style::default().fg(Tui::g(96))).enclose( + //Fill::Y(Align::n(Bsp::s(e.clip_status(), e.edit_status())))))) + //} + //pub fn view_midi_ins_status (&self) -> impl Content + use<'_> { + //self.project.view_midi_ins_status(self.color) + //} + //pub fn view_midi_outs_status (&self) -> impl Content + use<'_> { + //self.project.view_midi_outs_status(self.color) + //} + //pub fn view_audio_ins_status (&self) -> impl Content + use<'_> { + //self.project.view_audio_ins_status(self.color) + //} + //pub fn view_audio_outs_status (&self) -> impl Content + use<'_> { + //self.project.view_audio_outs_status(self.color) + //} + //pub fn view_scenes (&self) -> impl Content + use<'_> { + //Bsp::e( + //Fixed::X(20, Align::nw(self.project.view_scenes_names())), + //self.project.view_scenes_clips(), + //) + //} + //pub fn view_scenes_names (&self) -> impl Content + use<'_> { + //self.project.view_scenes_names() + //} + //pub fn view_scenes_clips (&self) -> impl Content + use<'_> { + //self.project.view_scenes_clips() + //} + //pub fn view_tracks_inputs <'a> (&'a self) -> impl Content + use<'a> { + //Fixed::Y(1 + self.project.midi_ins.len() as u16, + //self.project.view_inputs(self.color)) + //} + //pub fn view_tracks_outputs <'a> (&'a self) -> impl Content + use<'a> { + //self.project.view_outputs(self.color) + //} + //pub fn view_tracks_devices <'a> (&'a self) -> impl Content + use<'a> { + //Fixed::Y(4, self.project.view_track_devices(self.color)) + //} + //pub fn view_tracks_names <'a> (&'a self) -> impl Content + use<'a> { + //Fixed::Y(2, self.project.view_track_names(self.color)) + //} + //pub fn view_pool (&self) -> impl Content + use<'_> { + //Fixed::X(20, Bsp::s( + //Fill::X(Align::w(FieldH(self.color, "Clip pool:", ""))), + //Fill::Y(Align::n(Tui::bg(Rgb(0, 0, 0), Outer(true, Style::default().fg(Tui::g(96))) + //.enclose(PoolView(&self.pool))))))) + //} + //pub fn view_samples_keys (&self) -> impl Content + use<'_> { + //self.project.sampler().map(|s|s.view_list(true, self.editor().unwrap())) + //} + //pub fn view_samples_grid (&self) -> impl Content + use<'_> { + //self.project.sampler().map(|s|s.view_grid()) + //} + //pub fn view_sample_viewer (&self) -> impl Content + use<'_> { + //self.project.sampler().map(|s|s.view_sample(self.editor().unwrap().get_note_pos())) + //} + //pub fn view_sample_info (&self) -> impl Content + use<'_> { + //self.project.sampler().map(|s|s.view_sample_info(self.editor().unwrap().get_note_pos())) + //} + //pub fn view_sample_status (&self) -> impl Content + use<'_> { + //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()))))) + //} + ////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))) + + //AppCommand => { + //("x/inc" / + //("stop-all") => todo!(),//app.project.stop_all(), + //("enqueue", clip: Option>>) => todo!(), + //("history", delta: isize) => todo!(), + //("zoom", zoom: usize) => todo!(), + //("select", selection: Selection) => todo!(), + //("dialog" / command: DialogCommand) => todo!(), + //("project" / command: ArrangementCommand) => todo!(), + //("clock" / command: ClockCommand) => todo!(), + //("sampler" / command: SamplerCommand) => todo!(), + //("pool" / command: PoolCommand) => todo!(), + //("edit" / editor: MidiEditCommand) => todo!(), + //}; + + //DialogCommand; + + //ArrangementCommand; + + //ClockCommand; + + //SamplerCommand; + + //PoolCommand; + + //MidiEditCommand; + + +//take!(DialogCommand |state: App, iter|Take::take(&state.dialog, iter)); +//#[derive(Clone, Debug)] +//pub enum DialogCommand { + //Open { dialog: Dialog }, + //Close +//} + +//impl Command> for DialogCommand { + //fn execute (self, state: &mut Option) -> Perhaps { + //match self { + //Self::Open { dialog } => { + //*state = Some(dialog); + //}, + //Self::Close => { + //*state = None; + //} + //}; + //Ok(None) + //} +//} + +//dsl!(DialogCommand: |self: Dialog, iter|todo!()); +//Dsl::take(&mut self.dialog, iter)); + +//#[tengri_proc::command(Option)]//Nope. +//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) + //} +//} +// +//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, |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 { + //// autoselect: automatically load selected clip in editor + //PoolCommand::Select { .. } | + //// autocolor: update color in all places simultaneously + //PoolCommand::Clip { command: PoolClipCommand::SetColor { .. } } => { + //let clip = app.pool.clip().clone(); + //app.editor_mut().map(|editor|editor.set_clip(clip.as_ref())) + //}, + //_ => None + //}; + //Ok(undo) + //}; + //select = |app, selection: Selection| { + //*app.project.selection_mut() = selection; + ////todo! + ////if let Some(ref mut editor) = app.editor_mut() { + ////editor.set_clip(match selection { + ////Selection::TrackClip { track, scene } if let Some(Some(Some(clip))) = app + ////.project + ////.scenes.get(scene) + ////.map(|s|s.clips.get(track)) + ////=> + ////Some(clip), + ////_ => + ////None + ////}); + ////} + //Ok(None) + ////("select" [t: usize, s: usize] Some(match (t.expect("no track"), s.expect("no scene")) { + ////(0, 0) => Self::Select(Selection::Mix), + ////(t, 0) => Self::Select(Selection::Track(t)), + ////(0, s) => Self::Select(Selection::Scene(s)), + ////(t, s) => Self::Select(Selection::TrackClip { track: t, scene: s }) }))) + //// autoedit: load focused clip in editor. + //}; + ////fn color (app: &mut App, theme: ItemTheme) -> Perhaps { + ////Ok(app.set_color(Some(theme)).map(|theme|Self::Color{theme})) + ////} + ////fn launch (app: &mut App) -> Perhaps { + ////app.project.launch(); + ////Ok(None) + ////} + //toggle_editor = |app, value: bool|{ app.toggle_editor(Some(value)); Ok(None) }; + //editor = |app, command: MidiEditCommand| Ok(if let Some(editor) = app.editor_mut() { + //let undo = command.clone().delegate(editor, |command|AppCommand::Editor{command})?; + //// update linked sampler after editor action + //app.project.sampler_mut().map(|sampler|match command { + //// autoselect: automatically select sample in sampler + //MidiEditCommand::SetNotePos { pos } => { sampler.set_note_pos(pos); }, + //_ => {} + //}); + //undo + //} else { + //None + //}); +//}); +//take!(ClockCommand |state: App, iter|Take::take(state.clock(), iter)); +//take!(MidiEditCommand |state: App, iter|Ok(state.editor().map(|x|Take::take(x, iter)).transpose()?.flatten())); +//take!(PoolCommand |state: App, iter|Take::take(&state.pool, iter)); +//take!(SamplerCommand |state: App, iter|Ok(state.project.sampler().map(|x|Take::take(x, iter)).transpose()?.flatten())); +//take!(ArrangementCommand |state: App, iter|Take::take(&state.project, iter)); + + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +//has_editor!(|self: App|{ + //editor = self.editor; + //editor_w = { + //let size = self.size.w(); + //let editor = self.editor.as_ref().expect("missing editor"); + //let time_len = editor.time_len().get(); + //let time_zoom = editor.time_zoom().get().max(1); + //(5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16) + //}; + //editor_h = 15; + //is_editing = self.editor.is_some(); +//}); + + + diff --git a/app/Cargo.toml b/app/Cargo.toml index 5d48eabd..c1b01a53 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -32,11 +32,13 @@ uuid = { workspace = true, optional = true } wavers = { workspace = true, optional = true } winit = { workspace = true, optional = true } xdg = { workspace = true } -ansi_term = "0.12.1" +ansi_term = "0.12.1" +builder-pattern = "0.4.2" [dev-dependencies] proptest = { workspace = true } proptest-derive = { workspace = true } +tek_engine = { path = "../engine" } [features] arranger = ["port", "editor", "sequencer", "editor"] diff --git a/app/tek.edn b/app/tek.edn index 5ecbdcff..9020ef91 100644 --- a/app/tek.edn +++ b/app/tek.edn @@ -10,8 +10,19 @@ (keys :axis/i2 (@lt i2/dec) (@gt z2/inc)) (keys :axis/w (@openbracket w/dec) (@closebracket w/inc)) (keys :axis/w2 (@openbrace w2/dec) (@closebrace w2/inc)) -(mode :menu (name Menu) (info Mode selector.) (keys :axis/y :confirm) - (bg (g 0) (bsp/s :ports/out (bsp/n :ports/in (bg (g 30) (bsp/s (fixed/y 7 :logo) (fill :dialog/menu))))))) + +(mode :menu + (name Menu) + (info Mode selector.) + (keys :axis/y :confirm) + (view (bg (g 0) (bsp/s + :ports/out + (bsp/n + :ports/in + (bg (g 30) (bsp/s + (fixed/y 7 :logo) + (fill :dialog/menu)))))))) + (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)))))))) diff --git a/app/tek.rs b/app/tek.rs index cdb057e9..8a910220 100644 --- a/app/tek.rs +++ b/app/tek.rs @@ -8,23 +8,21 @@ type_alias_impl_trait, type_changing_struct_update)] #[cfg(test)] mod tek_test; +#[allow(unused)] pub(crate) use ::std::{ + cmp::Ord, + collections::BTreeMap, + error::Error, + ffi::OsString, + fmt::{Write, Debug, Formatter}, + fs::File, + ops::{Add, Sub, Mul, Div, Rem}, + path::{Path, PathBuf}, + sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}}, + thread::JoinHandle, +}; #[allow(unused)] pub(crate) use ::{ - std::path::{Path, PathBuf}, - std::sync::{Arc, RwLock}, - std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}, - std::error::Error, - std::collections::BTreeMap, - std::fmt::Write, - std::cmp::Ord, - std::ffi::OsString, - std::fmt::{Debug, Formatter}, - std::fs::File, - std::ops::{Add, Sub, Mul, Div, Rem}, - std::thread::JoinHandle, xdg::BaseDirectories, atomic_float::*, - tek_device::{*, tek_engine::*}, - tengri::{*, input::*, output::*, tui::*}, tengri::tui::ratatui::{ self, prelude::{Rect, Style, Stylize, Buffer, Modifier, buffer::Cell, Color::{self, *}}, @@ -35,1409 +33,300 @@ event::{Event, KeyEvent, KeyCode::{self, *}}, }, }; -pub mod model { - use super::{*, gui::*}; - /// Total state - #[derive(Default, Debug)] pub struct App { - /// Base color. - pub color: ItemTheme, - /// Must not be dropped for the duration of the process - pub jack: Jack<'static>, - /// Display size - pub size: Measure, - /// Performance counter - pub perf: PerfModel, - /// Available view modes and input bindings - pub config: Config, - /// Currently selected mode - pub mode: Arc>>, - /// Undo history - pub history: Vec<(AppCommand, Option)>, - /// Dialog overlay - pub dialog: Dialog, - /// Contains all recently created clips. - pub pool: Pool, - /// Contains the currently edited musical arrangement - pub project: Arrangement, - /// Error, if any - pub error: Arc>>> + +pub extern crate tengri; pub(crate) use tengri::{*, input::*, output::*, tui::*}; +pub extern crate tek_device; pub(crate) use tek_device::{*, tek_engine::*}; + +mod tek_struct; pub use self::tek_struct::*; +mod tek_impls; + +/// Command-line entrypoint. +#[cfg(feature = "cli")] pub fn main () -> Usually<()> { + use clap::Parser; + Cli::parse().run() +} + +/// Create a new application from a backend, project, config, and mode +/// +/// ``` +/// let jack = tek_engine::Jack::new(&"test_tek").expect("failed to connect to jack"); +/// let proj = Default::default(); +/// let conf = Default::default(); +/// let tek = tek::tek(&jack, proj, conf, ""); +/// ``` +pub fn tek ( + jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef +) -> App { + App { + color: ItemTheme::random(), + dialog: Dialog::welcome(), + jack: jack.clone(), + mode: config.get_mode(mode).expect("failed to find mode"), + config, + project, + ..Default::default() } - /// Configuration: mode, view, and bind definitions. - #[derive(Default, Debug)] pub struct Config { - /// XDG base directories of running user. - pub dirs: BaseDirectories, - /// Active collection of interaction modes. - pub modes: Modes, - /// Active collection of event bindings. - pub binds: Binds, - /// Active collection of view definitions. - pub views: Views, - } - /// Collection of interaction modes. - pub type Modes = Arc, Arc>>>>>; - /// Collection of input bindings. - pub type Binds = Arc, Bind>>>>; - /// Collection of view definitions. - pub type Views = Arc, Arc>>>; - /// Group of view and keys definitions. - #[derive(Default, Debug)] - pub struct Mode { - pub path: PathBuf, - pub name: Vec, - pub info: Vec, - pub view: Vec, - pub keys: Vec, - pub modes: Modes, - } - /// An nput binding. - #[derive(Debug)] - pub struct Bind( - /// 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( - pub Arcbool + Send + Sync>> - ); - def_command!(AppCommand: |app: App| { - Nop => Ok(None), - Confirm => Ok(match &app.dialog { - Dialog::Menu(index, items) => { - let callback = items.0[*index].1.clone(); - callback(app)?; - None - }, - _ => todo!(), - }), - Cancel => todo!(), // TODO delegate: - Inc { axis: Axis } => Ok(match (&app.dialog, axis) { - (Dialog::None, _) => todo!(), - (Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_next() } - .execute(app)?, - _ => todo!() - }), - Dec { axis: Axis } => Ok(match (&app.dialog, axis) { - (Dialog::None, _) => None, - (Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_prev() } - .execute(app)?, - _ => todo!() - }), - SetDialog { dialog: Dialog } => { - swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }) +} + +/// Collection of interaction modes. +pub type Modes = Arc, Arc>>>>>; + +/// Collection of input bindings. +pub type Binds = Arc, Bind>>>>; + +/// Collection of view definitions. +pub type Views = Arc, Arc>>>; + +def_command!(AppCommand: |app: App| { + Nop => Ok(None), + Confirm => tek_confirm(app), + Cancel => todo!(), // TODO delegate: + Inc { axis: ControlAxis } => tek_inc(app, axis), + Dec { axis: ControlAxis } => tek_dec(app, axis), + SetDialog { dialog: Dialog } => { + swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }) + }, +}); + +fn tek_confirm (state: &mut App) -> Perhaps { + Ok(match &state.dialog { + Dialog::Menu(index, items) => { + let callback = items.0[*index].1.clone(); + callback(state)?; + None }, - }); + _ => todo!(), + }) } -pub mod core { - use super::{*, model::*, gui::*}; - impl App { - /// Create a new application from a project, config, and mode - pub fn new ( - jack: &Jack<'static>, - project: Arrangement, - config: Config, - mode: impl AsRef - ) -> Self { - let color = ItemTheme::random(); - let dialog = Dialog::welcome(); - let jack = jack.clone(); - let mode = config.get_mode(mode).unwrap(); - Self { color, dialog, jack, mode, config, project, ..Default::default() } - } - } - impl Config { - const CONFIG_DIR: &'static str = "tek"; - const CONFIG_SUB: &'static str = "v0"; - const CONFIG: &'static str = "tek.edn"; - const DEFAULTS: &'static str = include_str!("./tek.edn"); - /// Create a new app configuration from a set of XDG base directories, - pub fn new (dirs: Option) -> Self { - let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB); - let dirs = dirs.unwrap_or_else(default); - Self { dirs, ..Default::default() } - } - /// Write initial contents of configuration. - pub fn init (&mut self) -> Usually<()> { - self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|cfgs.add(&dsl))?; + +fn tek_inc (state: &mut App, axis: &ControlAxis) -> Perhaps { + Ok(match (&state.dialog, axis) { + (Dialog::None, _) => todo!(), + (Dialog::Menu(_, _), ControlAxis::Y) => AppCommand::SetDialog { dialog: state.dialog.menu_next() } + .execute(state)?, + _ => todo!() + }) +} + +fn tek_dec (state: &mut App, axis: &ControlAxis) -> Perhaps { + Ok(match (&state.dialog, axis) { + (Dialog::None, _) => None, + (Dialog::Menu(_, _), ControlAxis::Y) => AppCommand::SetDialog { dialog: state.dialog.menu_prev() } + .execute(state)?, + _ => todo!() + }) +} + +pub fn load_view (views: &Views, name: &impl AsRef, body: &impl Language) -> Usually<()> { + views.write().unwrap().insert(name.as_ref().into(), body.src()?.unwrap_or_default().into()); + Ok(()) +} + +pub fn load_mode (modes: &Modes, name: &impl AsRef, body: &impl Language) -> Usually<()> { + let mut mode = Mode::default(); + body.each(|item|mode.add(item))?; + modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode)); + Ok(()) +} + +pub fn load_bind (binds: &Binds, name: &impl AsRef, body: &impl Language) -> Usually<()> { + let mut map = Bind::new(); + body.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(()) - } - /// Write initial contents of a configuration file. - pub fn init_one ( - &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()) - }) - } - /// Add statements to configuration from [Dsl] source. - pub fn add (&mut self, dsl: impl Language) -> Usually<()> { - dsl.each(|item|self.add_one(item)) - } - fn add_one (&self, item: impl Language) -> Usually<()> { - if let Some(expr) = item.expr()? { - let head = expr.head()?; - let tail = expr.tail()?; - let name = tail.head()?; - let body = tail.tail()?; - //println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default()); - match head { - Some("mode") if let Some(name) = name => load_mode(&self.modes, &name, &body)?, - Some("keys") if let Some(name) = name => load_bind(&self.binds, &name, &body)?, - Some("view") if let Some(name) = name => load_view(&self.views, &name, &body)?, - _ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into()) - } - Ok(()) - } else { - return Err(format!("Config::load: expected expr, got: {item:?}").into()) - } - } - fn get_mode (&self, mode: impl AsRef) -> Option>>> { - self.modes.clone().read().unwrap().get(mode.as_ref()).cloned() - } - } - pub fn load_view (views: &Views, name: &impl AsRef, body: &impl Language) -> Usually<()> { - views.write().unwrap().insert(name.as_ref().into(), body.src()?.unwrap_or_default().into()); - Ok(()) - } - pub fn load_mode (modes: &Modes, name: &impl AsRef, body: &impl Language) -> Usually<()> { - let mut mode = Mode::default(); - body.each(|item|mode.add(item))?; - modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode)); - Ok(()) - } - pub fn load_bind (binds: &Binds, name: &impl AsRef, body: &impl Language) -> Usually<()> { - let mut map = Bind::new(); - body.each(|item|if item.expr().head() == Ok(Some("see")) { + } else if Some(":char") == item.expr()?.head()? { // 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()) - } + return Ok(()) } else { - return Err(format!("Config::load_bind: unexpected: {item:?}").into()) - })?; - binds.write().unwrap().insert(name.as_ref().into(), map); - Ok(()) - } - impl Mode> { - fn add (&mut self, dsl: impl Language) -> Usually<()> { - Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() { - //println!("Mode::add: {head} {:?}", expr.tail()); - let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); - match head { - "name" => self.name.push(tail.into()), - "info" => self.info.push(tail.into()), - "keys" => tail.each(|expr|{ - self.keys.push(expr.trim().into()); - Ok(()) - })?, - "mode" => if let Some(id) = tail.head()? { - load_mode(&self.modes, &id, &tail.tail())?; - } else { - return Err(format!("Mode::add: self: incomplete: {expr:?}").into()); - }, - _ => self.view.push(tail.into()) - }; - } else if let Ok(Some(word)) = dsl.word() { - self.view.push(word.into()); - } else { - return Err(format!("Mode::add: unexpected: {dsl:?}").into()); - }) + return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into()) } - } - impl Bind { - /// 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 Language) -> 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()) - } - } - } + } else { + return Err(format!("Config::load_bind: unexpected: {item:?}").into()) + })?; + binds.write().unwrap().insert(name.as_ref().into(), map); + Ok(()) } -pub mod tui { - use super::{*, model::*, gui::*}; - impl Draw for App { - fn draw (&self, to: &mut TuiOut) { - if let Some(e) = self.error.read().unwrap().as_ref() { - to.place_at(to.area(), e); - } - for (_index, dsl) in self.mode.view.iter().enumerate() { - if let Err(e) = self.view(to, dsl) { - *self.error.write().unwrap() = Some(format!("{e}").into()); - break; - } - } - } - } - impl Draw for Mode { - fn draw (&self, _to: &mut TuiOut) { - //self.content().draw(to) - } - } - impl View for App { - fn view_expr <'a> (&'a self, to: &mut TuiOut, expr: &'a impl Expression) -> Usually<()> { - if evaluate_output_expression(self, to, expr)? - || evaluate_output_expression_tui(self, to, expr)? { - Ok(()) - } else { - Err(format!("App::view_expr: unexpected: {expr:?}").into()) - } - } - fn view_word <'a> (&'a self, to: &mut TuiOut, dsl: &'a impl Expression) -> Usually<()> { - let mut frags = dsl.src()?.unwrap().split("/"); - match frags.next() { - Some(":logo") => to.place(&view_logo()), - Some(":status") => to.place(&Fixed::Y(1, "TODO: Status Bar")), - Some(":meters") => match frags.next() { - Some("input") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::Y(Align::s("Input Meters")))), - Some("output") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::Y(Align::s("Output Meters")))), - _ => panic!() - }, - Some(":tracks") => match frags.next() { - None => to.place(&"TODO tracks"), - Some("names") => to.place(&self.project.view_track_names(self.color.clone())),//Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Names")))), - Some("inputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Inputs")))), - Some("devices") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Devices")))), - Some("outputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Outputs")))), - _ => panic!() - }, - Some(":scenes") => match frags.next() { - None => to.place(&"TODO scenes"), - Some(":scenes/names") => to.place(&"TODO Scene Names"), - _ => panic!() - }, - Some(":editor") => to.place(&"TODO Editor"), - Some(":dialog") => match frags.next() { - Some("menu") => to.place(&if let Dialog::Menu(selected, items) = &self.dialog { - let items = items.clone(); - let selected = selected; - Some(Fill::XY(Thunk::new(move|to: &mut TuiOut|{ - for (index, MenuItem(item, _)) in items.0.iter().enumerate() { - to.place(&Push::Y((2 * index) as u16, - Tui::fg_bg( - if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, - if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, - Fixed::Y(2, Align::n(Fill::X(item))) - ))); - } - }))) - } else { - None - }), - _ => unimplemented!("App::view_word: {dsl:?} ({frags:?})"), - }, - Some(":templates") => to.place(&{ - let modes = self.config.modes.clone(); - let height = (modes.read().unwrap().len() * 2) as u16; - Fixed::Y(height, Min::X(30, Thunk::new(move |to: &mut 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 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)); - to.place(&Push::Y((2 * index) as u16, - Fixed::Y(2, Fill::X(Tui::bg(bg, Bsp::s( - Bsp::a(field_name, field_id), field_info)))))); - } - }))) - }), - Some(":sessions") => to.place(&Fixed::Y(6, Min::X(30, Thunk::new(|to: &mut 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) }; - to.place(&Push::Y((2 * index) as u16, - &Fixed::Y(2, Fill::X(Tui::bg(bg, Align::w(Tui::fg(fg, name))))))); - } - })))), - Some(":browse/title") => to.place(&Fill::X(Align::w(FieldV(ItemColor::default(), - match self.dialog.browser_target().unwrap() { - BrowseTarget::SaveProject => "Save project:", - BrowseTarget::LoadProject => "Load project:", - BrowseTarget::ImportSample(_) => "Import sample:", - BrowseTarget::ExportSample(_) => "Export sample:", - BrowseTarget::ImportClip(_) => "Import clip:", - BrowseTarget::ExportClip(_) => "Export clip:", - }, Shrink::X(3, Fixed::Y(1, Tui::fg(Tui::g(96), Repeat::X("๐Ÿญป")))))))), - Some(":device") => { - let selected = self.dialog.device_kind().unwrap(); - to.place(&Bsp::s(Tui::bold(true, "Add device"), Map::south(1, - move||device_kinds().iter(), - move|_label: &&'static str, i|{ - let bg = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; - let lb = if i == selected { "[ " } else { " " }; - let rb = if i == selected { " ]" } else { " " }; - Fill::X(Tui::bg(bg, Bsp::e(lb, Bsp::w(rb, "FIXME device name")))) }))) - }, - Some(":debug") => to.place(&Fixed::Y(1, format!("[{:?}]", to.area()))), - Some(_) => { - let views = self.config.views.read().unwrap(); - if let Some(dsl) = views.get(dsl.src()?.unwrap()) { - let dsl = dsl.clone(); - std::mem::drop(views); - self.view(to, &dsl)? - } else { - unimplemented!("{dsl:?}"); - } - }, - _ => unreachable!() - } - Ok(()) - } - } - fn view_logo () -> impl Content { - Fixed::XY(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), col!{ - Fixed::Y(1, ""), - Fixed::Y(1, ""), - Fixed::Y(1, "~~ โ•“โ”€โ•ฅโ”€โ•– โ•“โ”€โ”€โ•– โ•ฅ โ•– ~~~~~~~~~~~~"), - Fixed::Y(1, Bsp::e("~~~~ โ•‘ ~ โ•Ÿโ”€โ•Œ ~โ•Ÿโ”€< ~~ ", Bsp::e(Tui::fg(Rgb(230,100,40), "v0.3.0"), " ~~"))), - Fixed::Y(1, "~~~~ โ•จ ~ โ•™โ”€โ”€โ•œ โ•จ โ•œ ~~~~~~~~~~~~"), - }))) - } - handle!(TuiIn: |self: App, input|{ - let commands = collect_commands(self, input)?; - let history = execute_commands(self, commands)?; - self.history.extend(history.into_iter()); - Ok(None) - }); - fn collect_commands ( - app: &App, input: &TuiIn - ) -> Usually> { - let mut commands = vec![]; - for id in app.mode.keys.iter() { - if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref()) - && let Some(bindings) = event_map.query(input.event()) { - for binding in bindings { - for command in binding.commands.iter() { - if let Some(command) = app.resolve(command)? as Option { - commands.push(command) - } - } - } - } - } - Ok(commands) - } - fn execute_commands ( - app: &mut App, commands: Vec - ) -> Usually)>> { - let mut history = vec![]; - for command in commands.into_iter() { - let result = command.execute(app); - match result { Err(err) => { history.push((command, None)); return Err(err) } - Ok(undo) => { history.push((command, undo)); } }; - } - Ok(history) - } -} -pub mod ns { - use super::{*, model::*, gui::*}; - macro_rules!primitive(($T:ty: $name:ident)=>{ - fn $name (src: impl Language) -> Perhaps<$T> { - Ok(if let Some(src) = src.src()? { Some(to_number(src)? as $T) } else { None }) } }); - primitive!(u8: try_to_u8); - primitive!(u16: try_to_u16); - primitive!(usize: try_to_usize); - primitive!(isize: try_to_isize); - namespace!(App: Arc { literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); }); - namespace!(App: u8 { literal = |dsl|try_to_u8(dsl); }); - namespace!(App: u16 { literal = |dsl|try_to_u16(dsl); symbol = |app| { - ":w/sidebar" => app.project.w_sidebar(app.editor().is_some()), - ":h/sample-detail" => 6.max(app.measure_height() as u16 * 3 / 9), }; }); - namespace!(App: isize { literal = |dsl|try_to_isize(dsl); }); - namespace!(App: usize { literal = |dsl|try_to_usize(dsl); symbol = |app| { - ":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), }; }); - // Provide boolean values. - namespace!(App: bool { symbol = |app| { - ":mode/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), - }; }); - // TODO: provide colors here - namespace!(App: ItemTheme {}); - namespace!(App: Dialog { symbol = |app| { - ":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/save" => Dialog::Browse(BrowseTarget::SaveProject, - Browse::new(None).unwrap().into()), - ":dialog/load" => Dialog::Browse(BrowseTarget::LoadProject, - Browse::new(None).unwrap().into()), - ":dialog/import/clip" => Dialog::Browse(BrowseTarget::ImportClip(Default::default()), - Browse::new(None).unwrap().into()), - ":dialog/export/clip" => Dialog::Browse(BrowseTarget::ExportClip(Default::default()), - Browse::new(None).unwrap().into()), - ":dialog/import/sample" => Dialog::Browse(BrowseTarget::ImportSample(Default::default()), - Browse::new(None).unwrap().into()), - ":dialog/export/sample" => Dialog::Browse(BrowseTarget::ExportSample(Default::default()), - Browse::new(None).unwrap().into()), - }; }); - namespace!(App: Selection { symbol = |app| { - ":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(), - }; }); - namespace!(App: Color { - symbol = |app| { - ":color/bg" => Color::Rgb(28, 32, 36), - }; - expression = |app| { - "g" (n: u8) => Color::Rgb(n, n, n), - "rgb" (r: u8, g: u8, b: u8) => Color::Rgb(r, g, b), - }; - }); - namespace!(App: Option { symbol = |app| { - ":editor/pitch" => Some((app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into()) - }; }); - namespace!(App: Option { symbol = |app| { - ":selected/scene" => app.selection().scene(), - ":selected/track" => app.selection().track(), - }; }); - namespace!(App: Option>> { - symbol = |app| { ":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() { app.scenes()[*scene].clips[*track].clone() } else { None } }; - }); - impl<'a> Namespace<'a, AppCommand> for App { symbols!('a |app| -> AppCommand { - "x/inc" => AppCommand::Inc { axis: Axis::X }, - "x/dec" => AppCommand::Dec { axis: Axis::X }, - "y/inc" => AppCommand::Inc { axis: Axis::Y }, - "y/dec" => AppCommand::Dec { axis: Axis::Y }, - "confirm" => AppCommand::Confirm, - "cancel" => AppCommand::Cancel, }); } -} -pub mod gui { - use super::{*, model::*}; - #[derive(Debug, Copy, Clone)] - pub enum Axis { X, Y, Z, I } - /// Various possible dialog modes. - #[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog { - #[default] None, - Help(usize), - Menu(usize, MenuItems), - Device(usize), - Message(Arc), - Browse(BrowseTarget, Arc), - Options, - } - #[derive(Debug, Clone, Default, PartialEq)] pub struct MenuItems( - pub Arc<[MenuItem]> - ); - #[derive(Clone)] pub struct MenuItem( - /// Label - pub Arc, - /// Callback - pub ArcUsually<()> + Send + Sync>> - ); - impl App { - pub fn update_clock (&self) { - ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) - } - /// Set modal dialog. - pub fn set_dialog (&mut self, mut dialog: Dialog) -> Dialog { - std::mem::swap(&mut self.dialog, &mut dialog); - dialog - } - /// Set picked device in device pick dialog. - pub fn device_pick (&mut self, index: usize) { - self.dialog = Dialog::Device(index); - } - pub fn add_device (&mut self, index: usize) -> Usually<()> { - match index { - 0 => { - let name = self.jack.with_client(|c|c.name().to_string()); - let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name(); - let track = self.track().expect("no active track"); - let port = format!("{}/Sampler", &track.name); - let connect = Connect::exact(format!("{name}:{midi}")); - let sampler = if let Ok(sampler) = Sampler::new( - &self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]] - ) { - self.dialog = Dialog::None; - Device::Sampler(sampler) - } else { - 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"); - track.devices.push(sampler); - Ok(()) - }, - 1 => { - todo!(); - Ok(()) - }, - _ => unreachable!(), - } - } - /// Return reference to content browser if open. - pub fn browser (&self) -> Option<&Browse> { - if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } - } - /// Is a MIDI editor currently focused? - pub fn editor_focused (&self) -> bool { false } - /// Toggle MIDI editor. - pub fn toggle_editor (&mut self, value: Option) { - //FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed); - let value = value.unwrap_or_else(||!self.editor().is_some()); - if value { - // Create new clip in pool when entering empty cell - if let Selection::TrackClip { track, scene } = *self.selection() - && let Some(scene) = self.project.scenes.get_mut(scene) - && let Some(slot) = scene.clips.get_mut(track) - && slot.is_none() - && let Some(track) = self.project.tracks.get_mut(track) - { - let (index, mut clip) = self.pool.add_new_clip(); - // autocolor: new clip colors from scene and track color - let color = track.color.base.mix(scene.color.base, 0.5); - clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); - if let Some(editor) = &mut self.project.editor { - editor.set_clip(Some(&clip)); - } - *slot = Some(clip.clone()); - //Some(clip) - } else { - //None - } - } else if let Selection::TrackClip { track, scene } = *self.selection() - && let Some(scene) = self.project.scenes.get_mut(scene) - && let Some(slot) = scene.clips.get_mut(track) - && let Some(clip) = slot.as_mut() - { - // Remove clip from arrangement when exiting empty clip editor - let mut swapped = None; - if clip.read().unwrap().count_midi_messages() == 0 { - std::mem::swap(&mut swapped, slot); - } - if let Some(clip) = swapped { - self.pool.delete_clip(&clip.read().unwrap()); - } - } - } - } - impl Dialog { - pub fn welcome () -> Self { - Self::Menu(1, MenuItems([ - MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))), - MenuItem("Create new session".into(), Arc::new(Box::new(|app|Ok({ - app.dialog = Dialog::None; - app.mode = app.config.modes.clone().read().unwrap().get(":arranger").cloned().unwrap(); - })))), - MenuItem("Load old session".into(), Arc::new(Box::new(|_|Ok(())))), - ].into())) - } - pub fn menu_next (&self) -> Self { - match self { - Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.0.len()), items.clone()), - _ => Self::None - } - } - pub fn menu_prev (&self) -> Self { - match self { - Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.0.len()), items.clone()), - _ => Self::None - } - } - pub fn menu_selected (&self) -> Option { if let Self::Menu(selected, _) = self { Some(*selected) } else { None } } - pub fn device_kind (&self) -> Option { if let Self::Device(index) = self { Some(*index) } else { None } } - pub fn device_kind_next (&self) -> Option { self.device_kind().map(|index|(index + 1) % device_kinds().len()) } - pub fn device_kind_prev (&self) -> Option { self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1))) } - pub fn message (&self) -> Option<&str> { todo!() } - pub fn browser (&self) -> Option<&Arc> { todo!() } - pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() } - } -} -pub mod audio { - use super::{*, model::*}; - audio!( - |self: App, client, scope|{ - let t0 = self.perf.get_t0(); - self.clock().update_from_scope(scope).unwrap(); - let midi_in = self.project.midi_input_collect(scope); - if let Some(editor) = &self.editor() { - let mut pitch: Option = None; - for port in midi_in.iter() { - for event in port.iter() { - if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..})) - = event - { - pitch = Some(key.clone()); - } - } - } - if let Some(pitch) = pitch { - editor.set_note_pos(pitch.as_int() as usize); - } - } - let result = self.project.process_tracks(client, scope); - self.perf.update_from_jack_scope(t0, scope); - result - }; - |self, event|{ - use JackEvent::*; - match event { - SampleRate(sr) => { self.clock().timebase.sr.set(sr as f64); }, - PortRegistration(_id, true) => { - //let port = self.jack().port_by_id(id); - //println!("\rport add: {id} {port:?}"); - //println!("\rport add: {id}"); - }, - PortRegistration(_id, false) => { - /*println!("\rport del: {id}")*/ - }, - PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ }, - PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ }, - ClientRegistration(_id, true) => {}, - ClientRegistration(_id, false) => {}, - ThreadInit => {}, - XRun => {}, - GraphReorder => {}, - _ => { panic!("{event:?}"); } - } - } - ); -} -pub mod glue { - use super::{*, model::*, gui::*}; - has!(Jack<'static>: |self: App|self.jack); - has!(Pool: |self: App|self.pool); - 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); - has!(Vec: |self: App|self.project.midi_ins); - has!(Vec: |self: App|self.project.midi_outs); - has!(Vec: |self: App|self.project.scenes); - has!(Vec: |self: App|self.project.tracks); - has!(Measure: |self: App|self.size); - has_clips!( |self: App|self.pool.clips); - maybe_has!(Track: |self: App| { MaybeHas::::get(&self.project) }; - { MaybeHas::::get_mut(&mut self.project) }); - maybe_has!(Scene: |self: App| { MaybeHas::::get(&self.project) }; - { MaybeHas::::get_mut(&mut self.project) }); - impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } } - impl HasClipsSize for App { fn clips_size (&self) -> &Measure { &self.project.size_inner } } - 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 Default for AppCommand { fn default () -> Self { Self::Nop } } - impl ScenesView for App { - fn w_side (&self) -> u16 { 20 } - fn w_mid (&self) -> u16 { (self.measure_width() as u16).saturating_sub(self.w_side()) } - fn h_scenes (&self) -> u16 { (self.measure_height() as u16).saturating_sub(20) } - } - impl Default for MenuItem { fn default () -> Self { Self("".into(), Arc::new(Box::new(|_|Ok(())))) } } - impl PartialEq for MenuItem { fn eq (&self, other: &Self) -> bool { self.0 == other.0 } } - impl AsRef> for MenuItems { fn as_ref (&self) -> &Arc<[MenuItem]> { &self.0 } } - impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) }); - impl_debug!(Condition |self, w| { write!(w, "*") }); - /// Default is always empty map regardless if `E` and `C` implement [Default]. - impl Default for Bind { fn default () -> Self { Self(Default::default()) } } -} -/// Command-line configuration. -#[cfg(feature = "cli")] pub mod cli { - use super::{*, model::*}; - use clap::{self, Parser, Subcommand}; - /// CLI banner. - const HEADER: &'static str = r#" + +/// CLI banner. +pub(crate) const HEADER: &'static str = r#" ~ โ–ˆโ–€โ–ˆโ–€โ–ˆ โ–ˆโ–€โ–€โ–ˆ โ–ˆ โ–ˆ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ โ–ˆ โ–ˆโ–€ โ–ˆโ–€โ–€โ–„ ~ v0.4.0, 2026 winter (or is it) ~ ~ โ–€ โ–ˆโ–€โ–€โ–ˆ โ–€ โ–€ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#; - #[derive(Debug, Parser)] - #[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))] - pub struct Cli { - /// Pre-defined configuration modes. - /// - /// TODO: Replace these with scripted configurations. - #[command(subcommand)] action: Action, + +fn view_logo () -> impl Content { + Fixed::XY(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), col!{ + Fixed::Y(1, ""), + Fixed::Y(1, ""), + Fixed::Y(1, "~~ โ•“โ”€โ•ฅโ”€โ•– โ•“โ”€โ”€โ•– โ•ฅ โ•– ~~~~~~~~~~~~"), + Fixed::Y(1, Bsp::e("~~~~ โ•‘ ~ โ•Ÿโ”€โ•Œ ~โ•Ÿโ”€< ~~ ", Bsp::e(Tui::fg(Rgb(230,100,40), "v0.3.0"), " ~~"))), + Fixed::Y(1, "~~~~ โ•จ ~ โ•™โ”€โ”€โ•œ โ•จ โ•œ ~~~~~~~~~~~~"), + }))) +} +fn collect_commands ( + app: &App, input: &TuiIn +) -> Usually> { + let mut commands = vec![]; + for id in app.mode.keys.iter() { + if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref()) + && let Some(bindings) = event_map.query(input.event()) { + for binding in bindings { + for command in binding.commands.iter() { + if let Some(command) = app.namespace(command)? as Option { + commands.push(command) + } + } + } + } } - /// Application modes - #[derive(Debug, Clone, Subcommand, Default)] - enum Action { - /// Continue where you left off - #[default] Resume, - /// Run headlessly in current session. - Headless, - /// Show status of current session. - Status, - /// List known sessions. - List, - /// Continue work in a copy of the current session. - Fork, - /// Create a new empty session. - New { - /// Name of JACK client - #[arg(short='n', long)] name: Option, - /// Whether to attempt to become transport master - #[arg(short='Y', long, default_value_t = false)] sync_lead: bool, - /// Whether to sync to external transport master - #[arg(short='y', long, default_value_t = true)] sync_follow: bool, - /// Initial tempo in beats per minute - #[arg(short='b', long, default_value = None)] bpm: Option, - /// Whether to include a transport toolbar (default: true) - #[arg(short='c', long, default_value_t = true)] show_clock: bool, - /// MIDI outs to connect to (multiple instances accepted) - #[arg(short='I', long)] midi_from: Vec, - /// MIDI outs to connect to (multiple instances accepted) - #[arg(short='i', long)] midi_from_re: Vec, - /// MIDI ins to connect to (multiple instances accepted) - #[arg(short='O', long)] midi_to: Vec, - /// MIDI ins to connect to (multiple instances accepted) - #[arg(short='o', long)] midi_to_re: Vec, - /// Audio outs to connect to left input - #[arg(short='l', long)] left_from: Vec, - /// Audio outs to connect to right input - #[arg(short='r', long)] right_from: Vec, - /// Audio ins to connect from left output - #[arg(short='L', long)] left_to: Vec, - /// Audio ins to connect from right output - #[arg(short='R', long)] right_to: Vec, - /// Tracks to create - #[arg(short='t', long)] tracks: Option, - /// Scenes to create - #[arg(short='s', long)] scenes: Option, + Ok(commands) +} +fn execute_commands ( + app: &mut App, commands: Vec +) -> Usually)>> { + let mut history = vec![]; + for command in commands.into_iter() { + let result = command.execute(app); + match result { Err(err) => { history.push((command, None)); return Err(err) } + Ok(undo) => { history.push((command, undo)); } }; + } + Ok(history) +} +pub fn tek_jack_process (app: &mut App, client: &Client, scope: &ProcessScope) -> Control { + let t0 = app.perf.get_t0(); + app.clock().update_from_scope(scope).unwrap(); + let midi_in = app.project.midi_input_collect(scope); + if let Some(editor) = &app.editor() { + let mut pitch: Option = None; + for port in midi_in.iter() { + for event in port.iter() { + if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..})) + = event + { + pitch = Some(key.clone()); + } + } + } + if let Some(pitch) = pitch { + editor.set_note_pos(pitch.as_int() as usize); + } + } + let result = app.project.process_tracks(client, scope); + app.perf.update_from_jack_scope(t0, scope); + result +} +pub fn tek_jack_event (app: &mut App, event: JackEvent) { + use JackEvent::*; + match event { + SampleRate(sr) => { app.clock().timebase.sr.set(sr as f64); }, + PortRegistration(_id, true) => { + //let port = app.jack().port_by_id(id); + //println!("\rport add: {id} {port:?}"); + //println!("\rport add: {id}"); }, - /// Import media as new session. - Import, - /// Show configuration. - Config, - /// Show version. - Version, - } - impl Cli { - pub fn run (&self) -> Usually<()> { - if let Action::Version = self.action { - return Ok(self.show_version()) - } - let mut config = Config::new(None); - config.init()?; - if let Action::Config = self.action { - self.show_config(&config); - } else if let Action::List = self.action { - todo!("list sessions") - } else if let Action::Resume = self.action { - todo!("resume session") - } else if let Action::New { - name, bpm, tracks, scenes, sync_lead, sync_follow, - midi_from, midi_from_re, midi_to, midi_to_re, - left_from, right_from, left_to, right_to, .. - } = &self.action { - - // Connect to JACK - let name = name.as_ref().map_or("tek", |x|x.as_str()); - let jack = Jack::new(&name)?; - - // Collect MIDI IO: - let midi_ins = Connect::collect(&midi_from, &[] as &[&str], &midi_from_re).iter().enumerate() - .map(|(index, connect)|jack.midi_in(&format!("M/{index}"), &[connect.clone()])) - .collect::, _>>()?; - let midi_outs = Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate() - .map(|(index, connect)|jack.midi_out(&format!("{index}/M"), &[connect.clone()])) - .collect::, _>>()?; - - // TODO: Collect audio IO: - let empty = &[] as &[&str]; - let left_froms = Connect::collect(&left_from, empty, empty); - let left_tos = Connect::collect(&left_to, empty, empty); - let right_froms = Connect::collect(&right_from, empty, empty); - let right_tos = Connect::collect(&right_to, empty, empty); - let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; - let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()]; - - // Create initial project: - let clock = Clock::new(&jack, *bpm)?; - let mut project = Arrangement::new(&jack, None, clock, vec![], vec![], - midi_ins, midi_outs); - project.tracks_add(tracks.unwrap_or(0), None, &[], &[])?; - project.scenes_add(scenes.unwrap_or(0))?; - - if matches!(self.action, Action::Status) { - - // Show status and exit - self.show_status(&project); - - } else { - - // Initialize the app state - let app = App::new(&jack, project, config, ":menu"); - - if matches!(self.action, Action::Headless) { - - // TODO: Headless mode (daemon + client over IPC, then over network...) - println!("todo headless"); - - } else { - - // Run the [Tui] and [Jack] threads with the [App] state. - Tui::run(true, jack.run(move|jack|{ - jack.sync_lead(*sync_lead, |mut state|{ - let clock = app.clock(); - clock.playhead.update_from_sample(state.position.frame() as f64); - state.position.bbt = Some(clock.bbt()); - state.position - })?; - jack.sync_follow(*sync_follow)?; - Ok(app) - })?)?; - - } - - } - } - Ok(()) - } - fn show_version (&self) { - println!("todo version"); - } - fn show_config (&self, config: &Config) { - use ::ansi_term::Color::*; - println!("{:?}", config.dirs); - for (k, v) in config.views.read().unwrap().iter() { - println!("{} {} {v}", Green.paint("VIEW"), Green.bold().paint(format!("{k:<16}"))); - } - for (k, v) in config.binds.read().unwrap().iter() { - println!("{} {}", Green.paint("BIND"), Green.bold().paint(format!("{k:<16}"))); - for (k, v) in v.0.iter() { - print!("{} ", &Yellow.paint(match &k.0 { - Event::Key(KeyEvent { modifiers, .. }) => - format!("{:>16}", format!("{modifiers}")), - _ => unimplemented!() - })); - print!("{}", &Yellow.bold().paint(match &k.0 { - Event::Key(KeyEvent { code, .. }) => - format!("{:<10}", format!("{code}")), - _ => unimplemented!() - })); - for v in v.iter() { - print!(" => {:?}", v.commands); - print!(" {}", v.condition.as_ref().map(|x|format!("{x:?}")).unwrap_or_default()); - println!(" {}", v.description.as_ref().map(|x|x.as_ref()).unwrap_or_default()); - //println!(" {:?}", v.source); - } - } - } - for (k, v) in config.modes.read().unwrap().iter() { - println!(); - for v in v.name.iter() { print!("{}", Green.bold().paint(format!("{v} "))); } - for v in v.info.iter() { print!("\n{}", Green.paint(format!("{v}"))); } - print!("\n{} {}", Blue.paint("TOOL"), Green.bold().paint(format!("{k:<16}"))); - print!("\n{}", Blue.paint("KEYS")); - for v in v.keys.iter() { print!("{}", Green.paint(format!(" {v}"))); } - println!(); - for (k, v) in v.modes.read().unwrap().iter() { - print!("{} {} {:?}", - Blue.paint("MODE"), - Green.bold().paint(format!("{k:<16}")), - v.name); - print!(" INFO={:?}", - v.info); - print!(" VIEW={:?}", - v.view); - println!(" KEYS={:?}", - v.keys); - } - print!("{}", Blue.paint("VIEW")); - for v in v.view.iter() { print!("{}", Green.paint(format!(" {v}"))); } - println!(); - } - } - fn show_status (&self, project: &Arrangement) { - println!("Name: {:?}", &project.name); - println!("JACK: {:?}", &project.jack); - println!("Buffer: {:?}", &project.clock.chunk); - println!("Sample rate: {:?}", &project.clock.timebase.sr); - println!("MIDI PPQ: {:?}", &project.clock.timebase.ppq); - println!("Tempo: {:?}", &project.clock.timebase.bpm); - println!("Quantize: {:?}", &project.clock.quant); - println!("Launch: {:?}", &project.clock.sync); - println!("Playhead: {:?}us", &project.clock.playhead.usec); - println!("Playhead: {:?}s", &project.clock.playhead.sample); - println!("Playhead: {:?}p", &project.clock.playhead.pulse); - println!("Started: {:?}", &project.clock.started); - println!("Tracks:"); - for (i, t) in project.tracks.iter().enumerate() { - println!(" Track {i}: {} {} {:?} {:?}", t.name, t.width, - &t.sequencer.play_clip, &t.sequencer.next_clip); - } - println!("Scenes:"); - for (i, t) in project.scenes.iter().enumerate() { - println!(" Scene {i}: {} {:?}", &t.name, &t.clips); - } - println!("MIDI Ins: {:?}", &project.midi_ins); - println!("MIDI Outs: {:?}", &project.midi_outs); - println!("Audio Ins: {:?}", &project.audio_ins); - println!("Audio Outs: {:?}", &project.audio_outs); - // TODO git integration - // TODO dawvert integration - } + PortRegistration(_id, false) => { + /*println!("\rport del: {id}")*/ + }, + PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ }, + PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ }, + ClientRegistration(_id, true) => {}, + ClientRegistration(_id, false) => {}, + ThreadInit => {}, + XRun => {}, + GraphReorder => {}, + _ => { panic!("{event:?}"); } } } -/// Command-line entrypoint. -#[cfg(feature = "cli")] pub fn main () -> Usually<()> { - use clap::Parser; - crate::cli::Cli::parse().run() +pub fn tek_show_version () { + println!("todo version"); +} +pub fn tek_print_config (config: &Config) { + use ::ansi_term::Color::*; + println!("{:?}", config.dirs); + for (k, v) in config.views.read().unwrap().iter() { + println!("{} {} {v}", Green.paint("VIEW"), Green.bold().paint(format!("{k:<16}"))); + } + for (k, v) in config.binds.read().unwrap().iter() { + println!("{} {}", Green.paint("BIND"), Green.bold().paint(format!("{k:<16}"))); + for (k, v) in v.0.iter() { + print!("{} ", &Yellow.paint(match &k.0 { + Event::Key(KeyEvent { modifiers, .. }) => + format!("{:>16}", format!("{modifiers}")), + _ => unimplemented!() + })); + print!("{}", &Yellow.bold().paint(match &k.0 { + Event::Key(KeyEvent { code, .. }) => + format!("{:<10}", format!("{code}")), + _ => unimplemented!() + })); + for v in v.iter() { + print!(" => {:?}", v.commands); + print!(" {}", v.condition.as_ref().map(|x|format!("{x:?}")).unwrap_or_default()); + println!(" {}", v.description.as_ref().map(|x|x.as_ref()).unwrap_or_default()); + //println!(" {:?}", v.source); + } + } + } + for (k, v) in config.modes.read().unwrap().iter() { + println!(); + for v in v.name.iter() { print!("{}", Green.bold().paint(format!("{v} "))); } + for v in v.info.iter() { print!("\n{}", Green.paint(format!("{v}"))); } + print!("\n{} {}", Blue.paint("TOOL"), Green.bold().paint(format!("{k:<16}"))); + print!("\n{}", Blue.paint("KEYS")); + for v in v.keys.iter() { print!("{}", Green.paint(format!(" {v}"))); } + println!(); + for (k, v) in v.modes.read().unwrap().iter() { + print!("{} {} {:?}", + Blue.paint("MODE"), + Green.bold().paint(format!("{k:<16}")), + v.name); + print!(" INFO={:?}", + v.info); + print!(" VIEW={:?}", + v.view); + println!(" KEYS={:?}", + v.keys); + } + print!("{}", Blue.paint("VIEW")); + for v in v.view.iter() { print!("{}", Green.paint(format!(" {v}"))); } + println!(); + } +} +pub fn tek_print_status (project: &Arrangement) { + println!("Name: {:?}", &project.name); + println!("JACK: {:?}", &project.jack); + println!("Buffer: {:?}", &project.clock.chunk); + println!("Sample rate: {:?}", &project.clock.timebase.sr); + println!("MIDI PPQ: {:?}", &project.clock.timebase.ppq); + println!("Tempo: {:?}", &project.clock.timebase.bpm); + println!("Quantize: {:?}", &project.clock.quant); + println!("Launch: {:?}", &project.clock.sync); + println!("Playhead: {:?}us", &project.clock.playhead.usec); + println!("Playhead: {:?}s", &project.clock.playhead.sample); + println!("Playhead: {:?}p", &project.clock.playhead.pulse); + println!("Started: {:?}", &project.clock.started); + println!("Tracks:"); + for (i, t) in project.tracks.iter().enumerate() { + println!(" Track {i}: {} {} {:?} {:?}", t.name, t.width, + &t.sequencer.play_clip, &t.sequencer.next_clip); + } + println!("Scenes:"); + for (i, t) in project.scenes.iter().enumerate() { + println!(" Scene {i}: {} {:?}", &t.name, &t.clips); + } + println!("MIDI Ins: {:?}", &project.midi_ins); + println!("MIDI Outs: {:?}", &project.midi_outs); + println!("Audio Ins: {:?}", &project.audio_ins); + println!("Audio Outs: {:?}", &project.audio_outs); + // TODO git integration + // TODO dawvert integration } - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -//pub fn view_nil (_: &App) -> TuiCb { - //|to|to.place(&Fill::XY("ยท")) -//} - - //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::Browse(BrowseTarget::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::Browse(BrowseTarget::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::Browse(BrowseTarget::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))) - //}, -// - //pub fn view_history (&self) -> impl Content { - //Fixed::Y(1, Fill::X(Align::w(FieldH(self.color, - //format!("History ({})", self.history.len()), - //self.history.last().map(|last|Fill::X(Align::w(format!("{:?}", last.0)))))))) - //} - //pub fn view_status_h2 (&self) -> impl Content { - //self.update_clock(); - //let theme = self.color; - //let clock = self.clock(); - //let playing = clock.is_rolling(); - //let cache = clock.view_cache.clone(); - ////let selection = self.selection().describe(self.tracks(), self.scenes()); - //let hist_len = self.history.len(); - //let hist_last = self.history.last(); - //Fixed::Y(2, Stack::east(move|add: &mut dyn FnMut(&dyn Draw)|{ - //add(&Fixed::X(5, Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, - //Either::new(false, // TODO - //Thunk::new(move||Fixed::X(9, Either::new(playing, - //Tui::fg(Rgb(0, 255, 0), " PLAYING "), - //Tui::fg(Rgb(255, 128, 0), " STOPPED "))) - //), - //Thunk::new(move||Fixed::X(5, Either::new(playing, - //Tui::fg(Rgb(0, 255, 0), Bsp::s(" ๐Ÿญ๐Ÿญ‘๐Ÿฌฝ ", " ๐Ÿญž๐Ÿญœ๐Ÿญ˜ ",)), - //Tui::fg(Rgb(255, 128, 0), Bsp::s(" โ–—โ–„โ–– ", " โ–โ–€โ–˜ ",)))) - //) - //) - //))); - //add(&" "); - //{ - //let cache = cache.read().unwrap(); - //add(&Fixed::X(15, Align::w(Bsp::s( - //FieldH(theme, "Beat", cache.beat.view.clone()), - //FieldH(theme, "Time", cache.time.view.clone()), - //)))); - //add(&Fixed::X(13, Align::w(Bsp::s( - //Fill::X(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), - //Fill::X(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), - //)))); - //add(&Fixed::X(12, Align::w(Bsp::s( - //Fill::X(Align::w(FieldH(theme, "Buf", cache.buf.view.clone()))), - //Fill::X(Align::w(FieldH(theme, "Lat", cache.lat.view.clone()))), - //)))); - ////add(&Bsp::s( - //////Fill::X(Align::w(FieldH(theme, "Selected", Align::w(selection)))), - ////Fill::X(Align::w(FieldH(theme, format!("History ({})", hist_len), - ////hist_last.map(|last|Fill::X(Align::w(format!("{:?}", last.0))))))), - ////"" - ////)); - //////if let Some(last) = self.history.last() { - //////add(&FieldV(theme, format!("History ({})", self.history.len()), - //////Fill::X(Align::w(format!("{:?}", last.0))))); - //////} - //} - //})) - //} - //pub fn view_status_v (&self) -> impl Content + use<'_> { - //self.update_clock(); - //let cache = self.project.clock.view_cache.read().unwrap(); - //let theme = self.color; - //let playing = self.clock().is_rolling(); - //Tui::bg(theme.darker.rgb, Fixed::XY(20, 5, Outer(true, Style::default().fg(Tui::g(96))).enclose( - //col!( - //Fill::X(Align::w(Bsp::e( - //Align::w(Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, - //Either::new(false, // TODO - //Thunk::new(move||Fixed::X(9, Either::new(playing, - //Tui::fg(Rgb(0, 255, 0), " PLAYING "), - //Tui::fg(Rgb(255, 128, 0), " STOPPED "))) - //), - //Thunk::new(move||Fixed::X(5, Either::new(playing, - //Tui::fg(Rgb(0, 255, 0), Bsp::s(" ๐Ÿญ๐Ÿญ‘๐Ÿฌฝ ", " ๐Ÿญž๐Ÿญœ๐Ÿญ˜ ",)), - //Tui::fg(Rgb(255, 128, 0), Bsp::s(" โ–—โ–„โ–– ", " โ–โ–€โ–˜ ",)))) - //) - //) - //)), - //Bsp::s( - //FieldH(theme, "Beat", cache.beat.view.clone()), - //FieldH(theme, "Time", cache.time.view.clone()), - //), - //))), - //Fill::X(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), - //Fill::X(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), - //Fill::X(Align::w(FieldH(theme, "Buf", Bsp::e(cache.buf.view.clone(), Bsp::e(" = ", cache.lat.view.clone()))))), - //)))) - //} - //pub fn view_status (&self) -> impl Content + use<'_> { - //self.update_clock(); - //let cache = self.project.clock.view_cache.read().unwrap(); - //view_status(Some(self.project.selection.describe(self.tracks(), self.scenes())), - //cache.sr.view.clone(), cache.buf.view.clone(), cache.lat.view.clone()) - //} - //pub fn view_transport (&self) -> impl Content + use<'_> { - //self.update_clock(); - //let cache = self.project.clock.view_cache.read().unwrap(); - //view_transport(self.project.clock.is_rolling(), - //cache.bpm.view.clone(), cache.beat.view.clone(), cache.time.view.clone()) - //} - //pub fn view_editor (&self) -> impl Content + use<'_> { - //let bg = self.editor() - //.and_then(|editor|editor.clip().clone()) - //.map(|clip|clip.read().unwrap().color.darker) - //.unwrap_or(self.color.darker); - //Fill::XY(Tui::bg(bg.rgb, self.editor())) - //} - //pub fn view_editor_status (&self) -> impl Content + use<'_> { - //self.editor().map(|e|Fixed::X(20, Outer(true, Style::default().fg(Tui::g(96))).enclose( - //Fill::Y(Align::n(Bsp::s(e.clip_status(), e.edit_status())))))) - //} - //pub fn view_midi_ins_status (&self) -> impl Content + use<'_> { - //self.project.view_midi_ins_status(self.color) - //} - //pub fn view_midi_outs_status (&self) -> impl Content + use<'_> { - //self.project.view_midi_outs_status(self.color) - //} - //pub fn view_audio_ins_status (&self) -> impl Content + use<'_> { - //self.project.view_audio_ins_status(self.color) - //} - //pub fn view_audio_outs_status (&self) -> impl Content + use<'_> { - //self.project.view_audio_outs_status(self.color) - //} - //pub fn view_scenes (&self) -> impl Content + use<'_> { - //Bsp::e( - //Fixed::X(20, Align::nw(self.project.view_scenes_names())), - //self.project.view_scenes_clips(), - //) - //} - //pub fn view_scenes_names (&self) -> impl Content + use<'_> { - //self.project.view_scenes_names() - //} - //pub fn view_scenes_clips (&self) -> impl Content + use<'_> { - //self.project.view_scenes_clips() - //} - //pub fn view_tracks_inputs <'a> (&'a self) -> impl Content + use<'a> { - //Fixed::Y(1 + self.project.midi_ins.len() as u16, - //self.project.view_inputs(self.color)) - //} - //pub fn view_tracks_outputs <'a> (&'a self) -> impl Content + use<'a> { - //self.project.view_outputs(self.color) - //} - //pub fn view_tracks_devices <'a> (&'a self) -> impl Content + use<'a> { - //Fixed::Y(4, self.project.view_track_devices(self.color)) - //} - //pub fn view_tracks_names <'a> (&'a self) -> impl Content + use<'a> { - //Fixed::Y(2, self.project.view_track_names(self.color)) - //} - //pub fn view_pool (&self) -> impl Content + use<'_> { - //Fixed::X(20, Bsp::s( - //Fill::X(Align::w(FieldH(self.color, "Clip pool:", ""))), - //Fill::Y(Align::n(Tui::bg(Rgb(0, 0, 0), Outer(true, Style::default().fg(Tui::g(96))) - //.enclose(PoolView(&self.pool))))))) - //} - //pub fn view_samples_keys (&self) -> impl Content + use<'_> { - //self.project.sampler().map(|s|s.view_list(true, self.editor().unwrap())) - //} - //pub fn view_samples_grid (&self) -> impl Content + use<'_> { - //self.project.sampler().map(|s|s.view_grid()) - //} - //pub fn view_sample_viewer (&self) -> impl Content + use<'_> { - //self.project.sampler().map(|s|s.view_sample(self.editor().unwrap().get_note_pos())) - //} - //pub fn view_sample_info (&self) -> impl Content + use<'_> { - //self.project.sampler().map(|s|s.view_sample_info(self.editor().unwrap().get_note_pos())) - //} - //pub fn view_sample_status (&self) -> impl Content + use<'_> { - //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()))))) - //} - ////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))) - - //AppCommand => { - //("x/inc" / - //("stop-all") => todo!(),//app.project.stop_all(), - //("enqueue", clip: Option>>) => todo!(), - //("history", delta: isize) => todo!(), - //("zoom", zoom: usize) => todo!(), - //("select", selection: Selection) => todo!(), - //("dialog" / command: DialogCommand) => todo!(), - //("project" / command: ArrangementCommand) => todo!(), - //("clock" / command: ClockCommand) => todo!(), - //("sampler" / command: SamplerCommand) => todo!(), - //("pool" / command: PoolCommand) => todo!(), - //("edit" / editor: MidiEditCommand) => todo!(), - //}; - - //DialogCommand; - - //ArrangementCommand; - - //ClockCommand; - - //SamplerCommand; - - //PoolCommand; - - //MidiEditCommand; - - -//take!(DialogCommand |state: App, iter|Take::take(&state.dialog, iter)); -//#[derive(Clone, Debug)] -//pub enum DialogCommand { - //Open { dialog: Dialog }, - //Close -//} - -//impl Command> for DialogCommand { - //fn execute (self, state: &mut Option) -> Perhaps { - //match self { - //Self::Open { dialog } => { - //*state = Some(dialog); - //}, - //Self::Close => { - //*state = None; - //} - //}; - //Ok(None) - //} -//} - -//dsl!(DialogCommand: |self: Dialog, iter|todo!()); -//Dsl::take(&mut self.dialog, iter)); - -//#[tengri_proc::command(Option)]//Nope. -//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) - //} -//} -// -//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, |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 { - //// autoselect: automatically load selected clip in editor - //PoolCommand::Select { .. } | - //// autocolor: update color in all places simultaneously - //PoolCommand::Clip { command: PoolClipCommand::SetColor { .. } } => { - //let clip = app.pool.clip().clone(); - //app.editor_mut().map(|editor|editor.set_clip(clip.as_ref())) - //}, - //_ => None - //}; - //Ok(undo) - //}; - //select = |app, selection: Selection| { - //*app.project.selection_mut() = selection; - ////todo! - ////if let Some(ref mut editor) = app.editor_mut() { - ////editor.set_clip(match selection { - ////Selection::TrackClip { track, scene } if let Some(Some(Some(clip))) = app - ////.project - ////.scenes.get(scene) - ////.map(|s|s.clips.get(track)) - ////=> - ////Some(clip), - ////_ => - ////None - ////}); - ////} - //Ok(None) - ////("select" [t: usize, s: usize] Some(match (t.expect("no track"), s.expect("no scene")) { - ////(0, 0) => Self::Select(Selection::Mix), - ////(t, 0) => Self::Select(Selection::Track(t)), - ////(0, s) => Self::Select(Selection::Scene(s)), - ////(t, s) => Self::Select(Selection::TrackClip { track: t, scene: s }) }))) - //// autoedit: load focused clip in editor. - //}; - ////fn color (app: &mut App, theme: ItemTheme) -> Perhaps { - ////Ok(app.set_color(Some(theme)).map(|theme|Self::Color{theme})) - ////} - ////fn launch (app: &mut App) -> Perhaps { - ////app.project.launch(); - ////Ok(None) - ////} - //toggle_editor = |app, value: bool|{ app.toggle_editor(Some(value)); Ok(None) }; - //editor = |app, command: MidiEditCommand| Ok(if let Some(editor) = app.editor_mut() { - //let undo = command.clone().delegate(editor, |command|AppCommand::Editor{command})?; - //// update linked sampler after editor action - //app.project.sampler_mut().map(|sampler|match command { - //// autoselect: automatically select sample in sampler - //MidiEditCommand::SetNotePos { pos } => { sampler.set_note_pos(pos); }, - //_ => {} - //}); - //undo - //} else { - //None - //}); -//}); -//take!(ClockCommand |state: App, iter|Take::take(state.clock(), iter)); -//take!(MidiEditCommand |state: App, iter|Ok(state.editor().map(|x|Take::take(x, iter)).transpose()?.flatten())); -//take!(PoolCommand |state: App, iter|Take::take(&state.pool, iter)); -//take!(SamplerCommand |state: App, iter|Ok(state.project.sampler().map(|x|Take::take(x, iter)).transpose()?.flatten())); -//take!(ArrangementCommand |state: App, iter|Take::take(&state.project, iter)); - - - -/////////////////////////////////////////////////////////////////////////////////////////////////// -//has_editor!(|self: App|{ - //editor = self.editor; - //editor_w = { - //let size = self.size.w(); - //let editor = self.editor.as_ref().expect("missing editor"); - //let time_len = editor.time_len().get(); - //let time_zoom = editor.time_zoom().get().max(1); - //(5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16) - //}; - //editor_h = 15; - //is_editing = self.editor.is_some(); -//}); - - diff --git a/app/tek_impls.rs b/app/tek_impls.rs new file mode 100644 index 00000000..035a0923 --- /dev/null +++ b/app/tek_impls.rs @@ -0,0 +1,716 @@ +use crate::*; + +audio!(App: tek_jack_process, tek_jack_event); + +/// Command-line configuration. +#[cfg(feature = "cli")] impl Cli { + + pub fn run (&self) -> Usually<()> { + + if let Action::Version = self.action { + return Ok(tek_show_version()) + } + + let mut config = Config::new(None); + config.init()?; + + if let Action::Config = self.action { + tek_print_config(&config); + } else if let Action::List = self.action { + todo!("list sessions") + } else if let Action::Resume = self.action { + todo!("resume session") + } else if let Action::New { + name, bpm, tracks, scenes, sync_lead, sync_follow, + midi_from, midi_from_re, midi_to, midi_to_re, + left_from, right_from, left_to, right_to, .. + } = &self.action { + + // Connect to JACK + let name = name.as_ref().map_or("tek", |x|x.as_str()); + let jack = Jack::new(&name)?; + + // Collect MIDI IO: + let midi_ins = Connect::collect(&midi_from, &[] as &[&str], &midi_from_re).iter().enumerate() + .map(|(index, connect)|jack.midi_in(&format!("M/{index}"), &[connect.clone()])) + .collect::, _>>()?; + let midi_outs = Connect::collect(&midi_to, &[] as &[&str], &midi_to_re).iter().enumerate() + .map(|(index, connect)|jack.midi_out(&format!("{index}/M"), &[connect.clone()])) + .collect::, _>>()?; + + // TODO: Collect audio IO: + let empty = &[] as &[&str]; + let left_froms = Connect::collect(&left_from, empty, empty); + let left_tos = Connect::collect(&left_to, empty, empty); + let right_froms = Connect::collect(&right_from, empty, empty); + let right_tos = Connect::collect(&right_to, empty, empty); + let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; + let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()]; + + // Create initial project: + let clock = Clock::new(&jack, *bpm)?; + let mut project = Arrangement::new(&jack, None, clock, vec![], vec![], + midi_ins, midi_outs); + project.tracks_add(tracks.unwrap_or(0), None, &[], &[])?; + project.scenes_add(scenes.unwrap_or(0))?; + + if matches!(self.action, Action::Status) { + // Show status and exit + tek_print_status(&project); + return Ok(()) + } + + // Initialize the app state + let app = tek(&jack, project, config, ":menu"); + if matches!(self.action, Action::Headless) { + // TODO: Headless mode (daemon + client over IPC, then over network...) + println!("todo headless"); + } else { + // Run the [Tui] and [Jack] threads with the [App] state. + Tui::run(true, jack.run(move|jack|{ + jack.sync_lead(*sync_lead, |mut state|{ + let clock = app.clock(); + clock.playhead.update_from_sample(state.position.frame() as f64); + state.position.bbt = Some(clock.bbt()); + state.position + })?; + jack.sync_follow(*sync_follow)?; + Ok(app) + })?)?; + } + } + Ok(()) + } + +} + +impl HasJack<'static> for App { + fn jack (&self) -> &Jack<'static> { &self.jack } +} + +impl HasClipsSize for App { + fn clips_size (&self) -> &Measure { &self.project.size_inner } +} + +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 Default for AppCommand { + fn default () -> Self { + Self::Nop + } +} + +impl Default for Binding { + fn default () -> Self { + Self { + ..Default::default() + } + } +} + +impl ScenesView for App { + fn w_side (&self) -> u16 { 20 } + fn w_mid (&self) -> u16 { (self.measure_width() as u16).saturating_sub(self.w_side()) } + fn h_scenes (&self) -> u16 { (self.measure_height() as u16).saturating_sub(20) } +} +impl Default for MenuItem { fn default () -> Self { Self("".into(), Arc::new(Box::new(|_|Ok(())))) } } +impl PartialEq for MenuItem { fn eq (&self, other: &Self) -> bool { self.0 == other.0 } } +impl AsRef> for MenuItems { fn as_ref (&self) -> &Arc<[MenuItem]> { &self.0 } } +/// Default is always empty map regardless if `E` and `C` implement [Default]. +impl Default for Bind { fn default () -> Self { Self(Default::default()) } } +has!(Jack<'static>: |self: App|self.jack); +has!(Pool: |self: App|self.pool); +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); +has!(Vec: |self: App|self.project.midi_ins); +has!(Vec: |self: App|self.project.midi_outs); +has!(Vec: |self: App|self.project.scenes); +has!(Vec: |self: App|self.project.tracks); +has!(Measure: |self: App|self.size); +has_clips!( |self: App|self.pool.clips); +maybe_has!(Track: |self: App| { MaybeHas::::get(&self.project) }; + { MaybeHas::::get_mut(&mut self.project) }); +maybe_has!(Scene: |self: App| { MaybeHas::::get(&self.project) }; + { MaybeHas::::get_mut(&mut self.project) }); +impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) }); +impl_debug!(Condition |self, w| { write!(w, "*") }); +macro_rules!primitive(($T:ty: $name:ident)=>{ + fn $name (src: impl Language) -> Perhaps<$T> { + Ok(if let Some(src) = src.src()? { Some(to_number(src)? as $T) } else { None }) } }); +primitive!(u8: try_to_u8); +primitive!(u16: try_to_u16); +primitive!(usize: try_to_usize); +primitive!(isize: try_to_isize); +namespace!(App: Arc { literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); }); +namespace!(App: u8 { literal = |dsl|try_to_u8(dsl); }); +namespace!(App: u16 { literal = |dsl|try_to_u16(dsl); symbol = |app| { + ":w/sidebar" => app.project.w_sidebar(app.editor().is_some()), + ":h/sample-detail" => 6.max(app.measure_height() as u16 * 3 / 9), }; }); +namespace!(App: isize { literal = |dsl|try_to_isize(dsl); }); +namespace!(App: usize { literal = |dsl|try_to_usize(dsl); symbol = |app| { + ":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), }; }); +// Provide boolean values. +namespace!(App: bool { symbol = |app| { + ":mode/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), +}; }); +// TODO: provide colors here +namespace!(App: ItemTheme {}); +namespace!(App: Dialog { symbol = |app| { + ":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/save" => Dialog::Browse(BrowseTarget::SaveProject, + Browse::new(None).unwrap().into()), + ":dialog/load" => Dialog::Browse(BrowseTarget::LoadProject, + Browse::new(None).unwrap().into()), + ":dialog/import/clip" => Dialog::Browse(BrowseTarget::ImportClip(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/export/clip" => Dialog::Browse(BrowseTarget::ExportClip(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/import/sample" => Dialog::Browse(BrowseTarget::ImportSample(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/export/sample" => Dialog::Browse(BrowseTarget::ExportSample(Default::default()), + Browse::new(None).unwrap().into()), +}; }); +namespace!(App: Selection { symbol = |app| { + ":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(), +}; }); +namespace!(App: Color { + symbol = |app| { + ":color/bg" => Color::Rgb(28, 32, 36), + }; + expression = |app| { + "g" (n: u8) => Color::Rgb(n, n, n), + "rgb" (r: u8, g: u8, b: u8) => Color::Rgb(r, g, b), + }; +}); +namespace!(App: Option { symbol = |app| { + ":editor/pitch" => Some((app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into()) +}; }); +namespace!(App: Option { symbol = |app| { + ":selected/scene" => app.selection().scene(), + ":selected/track" => app.selection().track(), +}; }); +namespace!(App: Option>> { + symbol = |app| { + ":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() { + app.scenes()[*scene].clips[*track].clone() + } else { + None + } + }; +}); +handle!(TuiIn: |self: App, input|{ + let commands = collect_commands(self, input)?; + let history = execute_commands(self, commands)?; + self.history.extend(history.into_iter()); + Ok(None) +}); + +impl Config { + const CONFIG_DIR: &'static str = "tek"; + const CONFIG_SUB: &'static str = "v0"; + const CONFIG: &'static str = "tek.edn"; + const DEFAULTS: &'static str = include_str!("./tek.edn"); + /// Create a new app configuration from a set of XDG base directories, + pub fn new (dirs: Option) -> Self { + let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB); + let dirs = dirs.unwrap_or_else(default); + Self { dirs, ..Default::default() } + } + /// Write initial contents of configuration. + pub fn init (&mut self) -> Usually<()> { + self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|{ + cfgs.add(&dsl)?; + Ok(()) + })?; + Ok(()) + } + /// Write initial contents of a configuration file. + pub fn init_one ( + &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()) + }) + } + /// Add statements to configuration from [Dsl] source. + pub fn add (&mut self, dsl: impl Language) -> Usually<&mut Self> { + dsl.each(|item|self.add_one(item))?; + Ok(self) + } + fn add_one (&self, item: impl Language) -> Usually<()> { + if let Some(expr) = item.expr()? { + let head = expr.head()?; + let tail = expr.tail()?; + let name = tail.head()?; + let body = tail.tail()?; + //println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default()); + match head { + Some("mode") if let Some(name) = name => load_mode(&self.modes, &name, &body)?, + Some("keys") if let Some(name) = name => load_bind(&self.binds, &name, &body)?, + Some("view") if let Some(name) = name => load_view(&self.views, &name, &body)?, + _ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into()) + } + Ok(()) + } else { + return Err(format!("Config::load: expected expr, got: {item:?}").into()) + } + } + pub fn get_mode (&self, mode: impl AsRef) -> Option>>> { + self.modes.clone().read().unwrap().get(mode.as_ref()).cloned() + } +} + +// Each mode contains a view, so here we should be drawing it. +// I'm not sure what's going on with this code, though. +impl Draw for Mode { + + fn draw (&self, _to: &mut TuiOut) { + //self.content().draw(to) + } + +} + +impl Mode> { + + /// Add a definition to the mode. + /// + /// Supported definitions: + /// + /// - (name ...) -> name + /// - (info ...) -> description + /// - (keys ...) -> key bindings + /// - (mode ...) -> submode + /// - ... -> view + /// + /// ``` + /// let mut mode: tek::Mode> = Default::default(); + /// mode.add("(name hello)").unwrap(); + /// ``` + pub fn add (&mut self, dsl: impl Language) -> Usually<()> { + Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() { + //println!("Mode::add: {head} {:?}", expr.tail()); + let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); + match head { + "name" => self.add_name(tail)?, + "info" => self.add_info(tail)?, + "keys" => self.add_keys(tail)?, + "mode" => self.add_mode(tail)?, + _ => self.add_view(tail)?, + }; + } else if let Ok(Some(word)) = dsl.word() { + self.add_view(word); + } else { + return Err(format!("Mode::add: unexpected: {dsl:?}").into()); + }) + + //DslParse(dsl, ||Err(format!("Mode::add: unexpected: {dsl:?}").into())) + //.word(|word|self.add_view(word)) + //.expr(|expr|expr.head(|head|{ + ////println!("Mode::add: {head} {:?}", expr.tail()); + //let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); + //match head { + //"name" => self.add_name(tail), + //"info" => self.add_info(tail), + //"keys" => self.add_keys(tail)?, + //"mode" => self.add_mode(tail)?, + //_ => self.add_view(tail), + //}; + //})) + } + + fn add_name (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.name.push(src.into()))) + } + fn add_info (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.info.push(src.into()))) + } + fn add_view (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.view.push(src.into()))) + } + fn add_keys (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(Some(dsl.each(|expr|{ + self.keys.push(expr.trim().into()); + Ok(()) + })?)) + } + fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(Some(if let Some(id) = dsl.head()? { + load_mode(&self.modes, &id, &dsl.tail())?; + } else { + return Err(format!("Mode::add: self: incomplete: {dsl:?}").into()); + })) + } + +} + +impl Bind { + + /// 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 { + + /// FIXME: Load an individual command binding from a dizzle. + /// + /// ``` + /// let binding = tek::Binding::<()>::from_dsl("foo bar").unwrap(); + /// ``` + pub fn from_dsl (dsl: impl Language) -> 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 App { + + pub fn update_clock (&self) { + ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) + } + + /// Set modal dialog. + pub fn set_dialog (&mut self, mut dialog: Dialog) -> Dialog { + std::mem::swap(&mut self.dialog, &mut dialog); + dialog + } + + /// Set picked device in device pick dialog. + pub fn device_pick (&mut self, index: usize) { + self.dialog = Dialog::Device(index); + } + + pub fn add_device (&mut self, index: usize) -> Usually<()> { + match index { + 0 => { + let name = self.jack.with_client(|c|c.name().to_string()); + let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name(); + let track = self.track().expect("no active track"); + let port = format!("{}/Sampler", &track.name); + let connect = Connect::exact(format!("{name}:{midi}")); + let sampler = if let Ok(sampler) = Sampler::new( + &self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]] + ) { + self.dialog = Dialog::None; + Device::Sampler(sampler) + } else { + 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"); + track.devices.push(sampler); + Ok(()) + }, + 1 => { + todo!(); + Ok(()) + }, + _ => unreachable!(), + } + } + + /// Return reference to content browser if open. + pub fn browser (&self) -> Option<&Browse> { + if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } + } + + /// Is a MIDI editor currently focused? + pub fn editor_focused (&self) -> bool { + false + } + + /// Toggle MIDI editor. + pub fn toggle_editor (&mut self, value: Option) { + //FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed); + let value = value.unwrap_or_else(||!self.editor().is_some()); + if value { + // Create new clip in pool when entering empty cell + if let Selection::TrackClip { track, scene } = *self.selection() + && let Some(scene) = self.project.scenes.get_mut(scene) + && let Some(slot) = scene.clips.get_mut(track) + && slot.is_none() + && let Some(track) = self.project.tracks.get_mut(track) + { + let (index, mut clip) = self.pool.add_new_clip(); + // autocolor: new clip colors from scene and track color + let color = track.color.base.mix(scene.color.base, 0.5); + clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); + if let Some(editor) = &mut self.project.editor { + editor.set_clip(Some(&clip)); + } + *slot = Some(clip.clone()); + //Some(clip) + } else { + //None + } + } else if let Selection::TrackClip { track, scene } = *self.selection() + && let Some(scene) = self.project.scenes.get_mut(scene) + && let Some(slot) = scene.clips.get_mut(track) + && let Some(clip) = slot.as_mut() + { + // Remove clip from arrangement when exiting empty clip editor + let mut swapped = None; + if clip.read().unwrap().count_midi_messages() == 0 { + std::mem::swap(&mut swapped, slot); + } + if let Some(clip) = swapped { + self.pool.delete_clip(&clip.read().unwrap()); + } + } + } +} + +impl Dialog { + pub fn welcome () -> Self { + Self::Menu(1, MenuItems([ + MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))), + MenuItem("Create new session".into(), Arc::new(Box::new(|app|Ok({ + app.dialog = Dialog::None; + app.mode = app.config.modes.clone().read().unwrap().get(":arranger").cloned().unwrap(); + })))), + MenuItem("Load old session".into(), Arc::new(Box::new(|_|Ok(())))), + ].into())) + } + pub fn menu_next (&self) -> Self { + match self { + Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.0.len()), items.clone()), + _ => Self::None + } + } + pub fn menu_prev (&self) -> Self { + match self { + Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.0.len()), items.clone()), + _ => Self::None + } + } + pub fn menu_selected (&self) -> Option { if let Self::Menu(selected, _) = self { Some(*selected) } else { None } } + pub fn device_kind (&self) -> Option { if let Self::Device(index) = self { Some(*index) } else { None } } + pub fn device_kind_next (&self) -> Option { self.device_kind().map(|index|(index + 1) % device_kinds().len()) } + pub fn device_kind_prev (&self) -> Option { self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1))) } + pub fn message (&self) -> Option<&str> { todo!() } + pub fn browser (&self) -> Option<&Arc> { todo!() } + pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() } +} + +impl<'a> Namespace<'a, AppCommand> for App { + symbols!('a |app| -> AppCommand { + "x/inc" => AppCommand::Inc { axis: ControlAxis::X }, + "x/dec" => AppCommand::Dec { axis: ControlAxis::X }, + "y/inc" => AppCommand::Inc { axis: ControlAxis::Y }, + "y/dec" => AppCommand::Dec { axis: ControlAxis::Y }, + "confirm" => AppCommand::Confirm, + "cancel" => AppCommand::Cancel, + }); +} + +impl Draw for App { + fn draw (&self, to: &mut TuiOut) { + if let Some(e) = self.error.read().unwrap().as_ref() { + to.place_at(to.area(), e); + } + for (_index, dsl) in self.mode.view.iter().enumerate() { + if let Err(e) = self.view(to, dsl) { + *self.error.write().unwrap() = Some(format!("{e}").into()); + break; + } + } + } +} + +impl View for App { + + fn view_expr <'a> (&'a self, to: &mut TuiOut, expr: &'a impl Expression) -> Usually<()> { + if evaluate_output_expression(self, to, expr)? + || evaluate_output_expression_tui(self, to, expr)? { + Ok(()) + } else { + Err(format!("App::view_expr: unexpected: {expr:?}").into()) + } + } + + fn view_word <'a> (&'a self, to: &mut TuiOut, dsl: &'a impl Expression) -> Usually<()> { + let mut frags = dsl.src()?.unwrap().split("/"); + match frags.next() { + Some(":logo") => to.place(&view_logo()), + Some(":status") => to.place(&Fixed::Y(1, "TODO: Status Bar")), + Some(":meters") => match frags.next() { + Some("input") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::Y(Align::s("Input Meters")))), + Some("output") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::Y(Align::s("Output Meters")))), + _ => panic!() + }, + Some(":tracks") => match frags.next() { + None => to.place(&"TODO tracks"), + Some("names") => to.place(&self.project.view_track_names(self.color.clone())),//Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Names")))), + Some("inputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Inputs")))), + Some("devices") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Devices")))), + Some("outputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Outputs")))), + _ => panic!() + }, + Some(":scenes") => match frags.next() { + None => to.place(&"TODO scenes"), + Some(":scenes/names") => to.place(&"TODO Scene Names"), + _ => panic!() + }, + Some(":editor") => to.place(&"TODO Editor"), + Some(":dialog") => match frags.next() { + Some("menu") => to.place(&if let Dialog::Menu(selected, items) = &self.dialog { + let items = items.clone(); + let selected = selected; + Some(Fill::XY(Thunk::new(move|to: &mut TuiOut|{ + for (index, MenuItem(item, _)) in items.0.iter().enumerate() { + to.place(&Push::Y((2 * index) as u16, + Tui::fg_bg( + if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, + if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, + Fixed::Y(2, Align::n(Fill::X(item))) + ))); + } + }))) + } else { + None + }), + _ => unimplemented!("App::view_word: {dsl:?} ({frags:?})"), + }, + Some(":templates") => to.place(&{ + let modes = self.config.modes.clone(); + let height = (modes.read().unwrap().len() * 2) as u16; + Fixed::Y(height, Min::X(30, Thunk::new(move |to: &mut 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 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)); + to.place(&Push::Y((2 * index) as u16, + Fixed::Y(2, Fill::X(Tui::bg(bg, Bsp::s( + Bsp::a(field_name, field_id), field_info)))))); + } + }))) + }), + Some(":sessions") => to.place(&Fixed::Y(6, Min::X(30, Thunk::new(|to: &mut 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) }; + to.place(&Push::Y((2 * index) as u16, + &Fixed::Y(2, Fill::X(Tui::bg(bg, Align::w(Tui::fg(fg, name))))))); + } + })))), + Some(":browse/title") => to.place(&Fill::X(Align::w(FieldV(ItemColor::default(), + match self.dialog.browser_target().unwrap() { + BrowseTarget::SaveProject => "Save project:", + BrowseTarget::LoadProject => "Load project:", + BrowseTarget::ImportSample(_) => "Import sample:", + BrowseTarget::ExportSample(_) => "Export sample:", + BrowseTarget::ImportClip(_) => "Import clip:", + BrowseTarget::ExportClip(_) => "Export clip:", + }, Shrink::X(3, Fixed::Y(1, Tui::fg(Tui::g(96), Repeat::X("๐Ÿญป")))))))), + Some(":device") => { + let selected = self.dialog.device_kind().unwrap(); + to.place(&Bsp::s(Tui::bold(true, "Add device"), Map::south(1, + move||device_kinds().iter(), + move|_label: &&'static str, i|{ + let bg = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; + let lb = if i == selected { "[ " } else { " " }; + let rb = if i == selected { " ]" } else { " " }; + Fill::X(Tui::bg(bg, Bsp::e(lb, Bsp::w(rb, "FIXME device name")))) }))) + }, + Some(":debug") => to.place(&Fixed::Y(1, format!("[{:?}]", to.area()))), + Some(_) => { + let views = self.config.views.read().unwrap(); + if let Some(dsl) = views.get(dsl.src()?.unwrap()) { + let dsl = dsl.clone(); + std::mem::drop(views); + self.view(to, &dsl)? + } else { + unimplemented!("{dsl:?}"); + } + }, + _ => unreachable!() + } + Ok(()) + } + +} + diff --git a/app/tek_struct.rs b/app/tek_struct.rs new file mode 100644 index 00000000..98679693 --- /dev/null +++ b/app/tek_struct.rs @@ -0,0 +1,213 @@ +use crate::*; +use clap::{self, Parser, Subcommand}; +use builder_pattern::Builder; + +/// Total state +/// +/// ``` +/// let app: tek::App = Default::default(); +/// ``` +#[derive(Default, Debug)] pub struct App { + /// Base color. + pub color: ItemTheme, + /// Must not be dropped for the duration of the process + pub jack: Jack<'static>, + /// Display size + pub size: Measure, + /// Performance counter + pub perf: PerfModel, + /// Available view modes and input bindings + pub config: Config, + /// Currently selected mode + pub mode: Arc>>, + /// Undo history + pub history: Vec<(AppCommand, Option)>, + /// Dialog overlay + pub dialog: Dialog, + /// Contains all recently created clips. + pub pool: Pool, + /// Contains the currently edited musical arrangement + pub project: Arrangement, + /// Error, if any + pub error: Arc>>> +} + +/// Configuration: mode, view, and bind definitions. +/// +/// ``` +/// let conf: tek::Config = Default::default(); +/// ``` +#[derive(Default, Debug)] pub struct Config { + /// XDG base directories of running user. + pub dirs: BaseDirectories, + /// Active collection of interaction modes. + pub modes: Modes, + /// Active collection of event bindings. + pub binds: Binds, + /// Active collection of view definitions. + pub views: Views, +} + +/// Group of view and keys definitions. +/// +/// ``` +/// let mode: tek::Mode> = Default::default(); +/// ``` +#[derive(Default, Debug)] pub struct Mode { + pub path: PathBuf, + pub name: Vec, + pub info: Vec, + pub view: Vec, + pub keys: Vec, + pub modes: Modes, +} + +/// An input binding. +/// +/// ``` +/// let bind: tek::Bind<(), ()> = Default::default(); +/// ``` +#[derive(Debug)] pub struct Bind( + /// 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. +/// +/// ``` +/// let binding: tek::Binding<()> = Default::default(); +/// ``` +#[derive(Debug, Clone)] pub struct Binding { + pub commands: Arc<[C]>, + pub condition: Option, + pub description: Option>, + pub source: Option>, +} + +/// Condition that must evaluate to true in order to enable an input layer. +/// +/// ``` +/// let condition = tek::Condition(std::sync::Arc::new(Box::new(||{true}))); +/// ``` +#[derive(Clone)] pub struct Condition( + pub Arcbool + Send + Sync>> +); + +/// List of menu items. +/// +/// ``` +/// let items: tek::MenuItems = Default::default(); +/// ``` +#[derive(Debug, Clone, Default, PartialEq)] pub struct MenuItems( + pub Arc<[MenuItem]> +); + +/// An item of a menu. +/// +/// ``` +/// let item: tek::MenuItem = Default::default(); +/// ``` +#[derive(Clone)] pub struct MenuItem( + /// Label + pub Arc, + /// Callback + pub ArcUsually<()> + Send + Sync>> +); + +/// The command-line interface descriptor. +/// +/// ``` +/// let cli: tek::Cli = Default::default(); +/// ``` +#[derive(Debug, Parser, Default)] +#[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))] +pub struct Cli { + /// Pre-defined configuration modes. + /// + /// TODO: Replace these with scripted configurations. + #[command(subcommand)] pub action: Action, +} + +/// Application modes that can be passed to the mommand line interface. +/// +/// ``` +/// let action: tek::Action = Default::default(); +/// ``` +#[derive(Debug, Clone, Subcommand, Default)] pub enum Action { + /// Continue where you left off + #[default] Resume, + /// Run headlessly in current session. + Headless, + /// Show status of current session. + Status, + /// List known sessions. + List, + /// Continue work in a copy of the current session. + Fork, + /// Create a new empty session. + New { + /// Name of JACK client + #[arg(short='n', long)] name: Option, + /// Whether to attempt to become transport master + #[arg(short='Y', long, default_value_t = false)] sync_lead: bool, + /// Whether to sync to external transport master + #[arg(short='y', long, default_value_t = true)] sync_follow: bool, + /// Initial tempo in beats per minute + #[arg(short='b', long, default_value = None)] bpm: Option, + /// Whether to include a transport toolbar (default: true) + #[arg(short='c', long, default_value_t = true)] show_clock: bool, + /// MIDI outs to connect to (multiple instances accepted) + #[arg(short='I', long)] midi_from: Vec, + /// MIDI outs to connect to (multiple instances accepted) + #[arg(short='i', long)] midi_from_re: Vec, + /// MIDI ins to connect to (multiple instances accepted) + #[arg(short='O', long)] midi_to: Vec, + /// MIDI ins to connect to (multiple instances accepted) + #[arg(short='o', long)] midi_to_re: Vec, + /// Audio outs to connect to left input + #[arg(short='l', long)] left_from: Vec, + /// Audio outs to connect to right input + #[arg(short='r', long)] right_from: Vec, + /// Audio ins to connect from left output + #[arg(short='L', long)] left_to: Vec, + /// Audio ins to connect from right output + #[arg(short='R', long)] right_to: Vec, + /// Tracks to create + #[arg(short='t', long)] tracks: Option, + /// Scenes to create + #[arg(short='s', long)] scenes: Option, + }, + /// Import media as new session. + Import, + /// Show configuration. + Config, + /// Show version. + Version, +} + +/// A control axis. +/// +/// ``` +/// let axis = tek::ControlAxis::X; +/// ``` +#[derive(Debug, Copy, Clone)] +pub enum ControlAxis { + X, Y, Z, I +} + +/// Various possible dialog modes. +/// +/// ``` +/// let dialog: tek::Dialog = Default::default(); +/// ``` +#[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog { + #[default] None, + Help(usize), + Menu(usize, MenuItems), + Device(usize), + Message(Arc), + Browse(BrowseTarget, Arc), + Options, +} diff --git a/app/tek_test.rs b/app/tek_test.rs index c5d05cb7..16c4a66d 100644 --- a/app/tek_test.rs +++ b/app/tek_test.rs @@ -1,13 +1,38 @@ use crate::*; +#[cfg(test)] #[test] fn test_menu () -> Usually<()> { + + // Some dizzle. + // What indentation to use here lol? + let source = stringify!((mode :menu + (name Menu) + (info Mode selector.) + (keys :axis/y :confirm) + (view (bg (g 0) (bsp/s + :ports/out + (bsp/n + :ports/in + (bg (g 30) (bsp/s + (fixed/y 7 :logo) + (fill :dialog/menu))))))))); + + // Load this definition into the config. + // A "mode" is basically a state machine + // with associated input and output definitions. + let mode = Config::new(None).add(&source)?.get_mode(":menu"); + + Ok(()) + +} + #[cfg(test)] #[test] fn test_cli () { use clap::CommandFactory; - cli::Cli::command().debug_assert(); + Cli::command().debug_assert(); //let jack = Jack::default(); } #[cfg(test)] #[test] fn test_app () -> Usually<()> { - let mut app = model::App::default(); + let mut app = App::default(); let _ = app.scene_add(None, None)?; let _ = app.update_clock(); Ok(()) @@ -59,7 +84,7 @@ use crate::*; } #[cfg(test)] #[test] fn test_view_iter () { - let mut app = model::App::default(); + let mut app = App::default(); app.project.editor = Some(Default::default()); //let _: Vec<_> = app.project.inputs_with_sizes().collect(); //let _: Vec<_> = app.project.outputs_with_sizes().collect(); @@ -70,7 +95,7 @@ use crate::*; } #[cfg(test)] #[test] fn test_view_sizes () { - let app = model::App::default(); + let app = App::default(); let _ = app.project.w(); //let _ = app.project.w_sidebar(); //let _ = app.project.w_tracks_area(); diff --git a/dizzle b/dizzle index 5af7c8b0..be9503a1 160000 --- a/dizzle +++ b/dizzle @@ -1 +1 @@ -Subproject commit 5af7c8b09b1469f95073092fb3dc23e636018e64 +Subproject commit be9503a119722a3b3573563c4bae2b0d52978bcd diff --git a/engine/jack.rs b/engine/jack.rs index 9b611752..aa2ae22d 100644 --- a/engine/jack.rs +++ b/engine/jack.rs @@ -145,7 +145,17 @@ pub trait Audio { #[inline] fn process (&mut $self1, $c: &Client, $s: &ProcessScope) -> Control { $cb } $(#[inline] fn handle (&mut $self2, $e: JackEvent) { $cb2 })? } - } + }; + ($Struct:ident: $process:ident, $handle:ident) => { + impl Audio for $Struct { + #[inline] fn process (&mut self, c: &Client, s: &ProcessScope) -> Control { + $process(self, c, s) + } + #[inline] fn handle (&mut self, e: JackEvent) { + $handle(self, e) + } + } + }; } /// Event enum for JACK events. #[derive(Debug, Clone, PartialEq)] pub enum JackEvent { diff --git a/tengri b/tengri index 04db6f4a..7a8365c0 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit 04db6f4af549012a4ffab3f06af3d2d6694cf811 +Subproject commit 7a8365c0174ddee65bd15ccca5dd7b03f110e002