Compare commits

...

2 commits

Author SHA1 Message Date
d10107684d init stub arranger
Some checks are pending
/ build (push) Waiting to run
2025-08-24 03:22:06 +03:00
f81f16b47b working main menu 2025-08-24 02:42:30 +03:00
10 changed files with 160 additions and 100 deletions

View file

@ -2,10 +2,11 @@
#![feature(adt_const_params, associated_type_defaults, if_let_guard, impl_trait_in_assoc_type,
type_alias_impl_trait, trait_alias, type_changing_struct_update, closure_lifetime_binder)]
#[cfg(test)] mod app_test;
mod app_deps; pub use self::app_deps::*;
mod app_jack; pub use self::app_jack::*;
mod app_bind; pub use self::app_bind::*;
mod app_data; pub use self::app_data::*;
mod app_deps; pub use self::app_deps::*;
mod app_jack; pub use self::app_jack::*;
mod app_menu; pub use self::app_menu::*;
mod app_view; pub use self::app_view::*;
/// Total state
#[derive(Default, Debug)]
@ -31,23 +32,6 @@ pub struct App {
/// Contains the currently edited musical arrangement
pub project: Arrangement,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Axis {
min: usize,
max: usize,
step: usize,
}
/// Various possible dialog modes.
#[derive(Debug, Clone, Default, PartialEq)]
pub enum Dialog {
#[default] None,
Help(usize),
Menu(usize, Arc<[Arc<str>]>),
Device(usize),
Message(Arc<str>),
Browse(BrowseTarget, Arc<Browse>),
Options,
}
has!(Jack<'static>: |self: App|self.jack);
has!(Pool: |self: App|self.pool);
has!(Dialog: |self: App|self.dialog);
@ -67,48 +51,6 @@ maybe_has!(Scene: |self: App| { MaybeHas::<Scene>::get(&self.project) };
impl HasClipsSize for App { fn clips_size (&self) -> &Measure<TuiOut> { &self.project.inner_size } }
impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() } }
impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } }
impl Dialog {
pub fn welcome () -> Self {
Self::Menu(0, [
"Continue session".into(),
"Load old session".into(),
"Begin new session".into(),
].into())
}
pub fn menu_next (&self) -> Self {
match self {
Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.len()), items.clone()),
_ => Self::None
}
}
pub fn menu_prev (&self) -> Self {
match self {
Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.len()), items.clone()),
_ => Self::None
}
}
pub fn menu_selected (&self) -> Option<usize> {
if let Self::Menu(selected, _) = self { Some(*selected) } else { None }
}
pub fn device_kind (&self) -> Option<usize> {
if let Self::Device(index) = self { Some(*index) } else { None }
}
pub fn device_kind_next (&self) -> Option<usize> {
self.device_kind().map(|index|(index + 1) % device_kinds().len())
}
pub fn device_kind_prev (&self) -> Option<usize> {
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<Browse>> {
todo!()
}
pub fn browser_target (&self) -> Option<&BrowseTarget> {
todo!()
}
}
impl App {
pub fn editor_focused (&self) -> bool {
false

View file

@ -49,8 +49,15 @@ impl Default for AppCommand { fn default () -> Self { Self::Nop } }
def_command!(AppCommand: |app: App| {
Nop => Ok(None),
Confirm => todo!(),
Cancel => todo!(), // TODO delegate:
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() }

View file

@ -10,7 +10,8 @@ impl<'t> DslNs<'t, Arc<str>> for App {
impl<'t> DslNs<'t, bool> for App {
dsl_words!(|app| -> bool {
":focused/editor" => app.project.editor.is_some(),
":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(..)),

View file

@ -3,14 +3,17 @@ pub use ::{
tek_config::*,
tek_device::{self, *},
tengri::{
Usually, Perhaps, Has, MaybeHas, has, maybe_has,
Usually, Perhaps, Has, MaybeHas, has, maybe_has, impl_debug,
dsl::*, input::*, output::*, tui::*,
tui::ratatui,
tui::ratatui::prelude::buffer::Cell,
tui::ratatui::prelude::Color::{self, *},
tui::ratatui::prelude::{Style, Stylize, Buffer, Modifier},
tui::crossterm,
tui::crossterm::event::{Event, KeyCode::{self, *}},
tui::{
ratatui::{
self, prelude::{Style, Stylize, Buffer, Modifier, buffer::Cell, Color::{self, *}}
},
crossterm::{
self,
event::{Event, KeyCode::{self, *}},
},
}
},
std::{
path::{Path, PathBuf},

88
crates/app/app_menu.rs Normal file
View file

@ -0,0 +1,88 @@
use crate::*;
/// Various possible dialog modes.
#[derive(Debug, Clone, Default, PartialEq)]
pub enum Dialog {
#[default] None,
Help(usize),
Menu(usize, MenuItems),
Device(usize),
Message(Arc<str>),
Browse(BrowseTarget, Arc<Browse>),
Options,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct MenuItems(pub Arc<[MenuItem]>);
impl AsRef<Arc<[MenuItem]>> for MenuItems {
fn as_ref (&self) -> &Arc<[MenuItem]> {
&self.0
}
}
#[derive(Clone)]
pub struct MenuItem(
/// Label
pub Arc<str>,
/// Callback
pub Arc<Box<dyn Fn(&mut App)->Usually<()> + Send + Sync>>
);
impl Default for MenuItem {
fn default () -> Self { Self("".into(), Arc::new(Box::new(|_|Ok(())))) }
}
impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) });
impl PartialEq for MenuItem {
fn eq (&self, other: &Self) -> bool {
self.0 == other.0
}
}
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<usize> {
if let Self::Menu(selected, _) = self { Some(*selected) } else { None }
}
pub fn device_kind (&self) -> Option<usize> {
if let Self::Device(index) = self { Some(*index) } else { None }
}
pub fn device_kind_next (&self) -> Option<usize> {
self.device_kind().map(|index|(index + 1) % device_kinds().len())
}
pub fn device_kind_prev (&self) -> Option<usize> {
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<Browse>> {
todo!()
}
pub fn browser_target (&self) -> Option<&BrowseTarget> {
todo!()
}
}

View file

@ -49,6 +49,9 @@ impl<'t> DslNs<'t, Box<dyn Render<TuiOut>>> for App {
"bg" (color: Color, x: Box<dyn Render<TuiOut>>) => Box::new(Tui::bg(color, x)),
"fg/bg" (fg: Color, bg: Color, x: Box<dyn Render<TuiOut>>) => Box::new(Tui::fg_bg(fg, bg, x)),
"either" (cond: bool, a: Box<dyn Render<TuiOut>>, b: Box<dyn Render<TuiOut>>) =>
Box::new(Either(cond, a, b)),
"bsp/n" (a: Box<dyn Render<TuiOut>>, b: Box<dyn Render<TuiOut>>) => Box::new(Bsp::n(a, b)),
"bsp/s" (a: Box<dyn Render<TuiOut>>, b: Box<dyn Render<TuiOut>>) => Box::new(Bsp::s(a, b)),
"bsp/e" (a: Box<dyn Render<TuiOut>>, b: Box<dyn Render<TuiOut>>) => Box::new(Bsp::e(a, b)),
@ -83,18 +86,35 @@ impl<'t> DslNs<'t, Box<dyn Render<TuiOut>>> for App {
"max/xy" (x: u16, y: u16, c: Box<dyn Render<TuiOut>>) => Box::new(Max::xy(x, y, c)),
});
dsl_words!(|app| -> Box<dyn Render<TuiOut>> {
":logo" => Box::new(Fixed::xy(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), Stack::south(|add|{
add(&Fixed::y(1, ""));
add(&Fixed::y(1, ""));
add(&Fixed::y(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"));
add(&Fixed::y(1, Bsp::e("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", Bsp::e(Tui::fg(Rgb(230,100,40), "v0.3.0"), " ~~"))));
add(&Fixed::y(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"));
}))))),
":meters/input" => Box::new("Input Meters"),
":meters/output" => Box::new("Output Meters"),
":status" => Box::new("Status Bar"),
":tracks/names" => Box::new("Track Names"),
":tracks/inputs" => Box::new("Track Inputs"),
":tracks/devices" => Box::new("Track Devices"),
":tracks/outputs" => Box::new("Track Outputs"),
":scenes/names" => Box::new("Scene Names"),
":editor" => Box::new("Editor"),
":scenes" => Box::new("Editor"),
":dialog/menu" => Box::new(if let Dialog::Menu(selected, items) = &app.dialog {
let items = items.clone();
let selected = *selected;
Some(Fill::xy(Align::c(Tui::bg(Red, Fill::x(Stack::south(move|add|{
for (index, item) in items.iter().enumerate() {
Some(Fill::x(Stack::south(move|add|{
for (index, MenuItem(item, _)) in items.0.iter().enumerate() {
add(&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
}),
@ -218,14 +238,6 @@ pub fn view_nil (_: &App) -> Box<dyn Render<TuiOut>> {
////.enclose(Fill::xy(browser)))
//},
//
//pub fn view_meters_input (&self) -> impl Content<TuiOut> + use<'_> {
//self.project.sampler().map(|s|
//s.view_meters_input())
//}
//pub fn view_meters_output (&self) -> impl Content<TuiOut> + use<'_> {
//self.project.sampler().map(|s|
//s.view_meters_output())
//}
//pub fn view_history (&self) -> impl Content<TuiOut> {
//Fixed::y(1, Fill::x(Align::w(FieldH(self.color,
//format!("History ({})", self.history.len()),

View file

@ -115,12 +115,15 @@ impl Cli {
/// CLI header
const HEADER: &'static str = r#"
~ ~~~~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~~~~~~
~ ~ ~ ~< ~ v0.3.0-rc.0 "no, i insist that i am not a dj ~
~ ~ ~ ~ 2025, summer, the nose of the cat. J ~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
On first run, Tek will create configuration and state dirs. ~
On subsequent runs, Tek should resume from where you left off. ~"#;
~ ~~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ ~~~~~~ ~ ~~~
~~ ~ ~< ~ v0.3.0, 2025 sum(m)er @ the nose of the cat. ~
~~~ ~ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ ~~ ~~ ~ ~~
On first run, Tek will create configuration and state dirs:
* [x] ~/.config/tek - config
* [ ] ~/.local/share/tek - projects
* [ ] ~/.local/lib/tek - plugins
* [ ] ~/.cache/tek - cache
~"#;
#[cfg(test)] #[test] fn test_cli () {
use clap::CommandFactory;

View file

@ -125,10 +125,10 @@ impl Mode<Arc<str>> {
Ok(())
}
pub fn load_one (&mut self, dsl: impl Dsl) -> Usually<()> {
Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(key)) = expr.head() {
println!("Mode::load_one: {key} {:?}", expr.tail()?);
Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() {
println!("Mode::load_one: {head} {:?}", expr.tail());
let tail = expr.tail()?.map(|x|x.trim()).unwrap_or("");
match key {
match head {
"name" => self.name.push(tail.into()),
"info" => self.info.push(tail.into()),
"view" => self.view.push(tail.into()),
@ -139,7 +139,7 @@ impl Mode<Arc<str>> {
return Err(format!("Mode::load_one: self: incomplete: {expr:?}").into());
},
_ => {
return Err(format!("Mode::load_one: unexpected expr: {key:?} {tail:?}").into())
return Err(format!("Mode::load_one: unexpected expr: {head:?} {tail:?}").into())
},
};
} else if let Ok(Some(word)) = dsl.word() {

2
deps/tengri vendored

@ -1 +1 @@
Subproject commit 446ec7a71477e1b9ca117b7a23759d6318eb2cf0
Subproject commit d9081087ecc41ad8514c17614daab4f34d3e876d

18
tek.edn
View file

@ -12,9 +12,9 @@
(mode :menu (keys :axis/y :confirm) :menu)
(keys :confirm (@enter confirm))
(view :menu (bg (g 40) (bsp/s :ports/out (bsp/n :ports/in
(bg (g 30) (fill/xy (align/c :dialog/menu)))))))
(bg (g 30) (bsp/s (fixed/y 7 :logo) (fill/xy :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))))))))
(bsp/a (text MIDI-OUT) (fill/x (align/e (text AUDIO-OUT-R))))))))
(view :ports/in (fill/x (fixed/y 3 (bsp/a (fill/x (align/w (text L-AUDIO-IN)))
(bsp/a (text MIDI-IN) (fill/x (align/e (text AUDIO-IN-R))))))))
(view :browse (bsp/s (padding/xy 3 1 :browse-title) (enclose (fg (g 96)) browser)))
@ -55,12 +55,16 @@
(mode :scene (keys :scene))
(mode :mix (keys :mix))
(keys :clock :arranger :global)
(view :arranger))
:arranger)
(view :arranger (bsp/w :meters/output (bsp/e :meters/input (stack/n
(fixed/y 2 :status/h2) :tracks/inputs (stack/s
:tracks/devices :tracks/outputs :tracks/names
(fill/xy (either :mode/editor (bsp/e :scenes/names :editor) :scenes)))))))
(view :arranger (bsp/w :meters/output (bsp/e :meters/input
(bsp/n (fixed/y 2 :status) (bsp/n :tracks/inputs
(bsp/s :tracks/devices (bsp/s :tracks/outputs (bsp/s :tracks/names
(fill/xy (either :mode/editor (bsp/e :scenes/names :editor) :scenes))))))))))
(view :arranger (bsp/w :meters/output (bsp/e :meters/input
(bsp/n (fixed/y 2 :status) (bsp/n :tracks/inputs
(bsp/s :tracks/devices (bsp/s :tracks/outputs (bsp/s :tracks/names
(fill/xy (either :mode/editor (bsp/e :scenes/names :editor) :scenes))))))))))
(keys :arranger (see :color :launch :scenes :tracks)
(@tab project/edit) (@enter project/edit)