wip: namespaces

This commit is contained in:
🪞👃🪞 2025-08-10 02:36:48 +03:00
parent 525a455f7a
commit f2d6e7724b
4 changed files with 174 additions and 169 deletions

View file

@ -40,6 +40,12 @@ use std::collections::BTreeMap;
use std::fmt::Write;
use ::tengri::tui::ratatui::prelude::Position;
use xdg::BaseDirectories;
mod app_dsl; pub use self::app_dsl::*;
macro_rules!dsl_sym(
(|$state:ident:$State:ty| -> $type:ty {$($lit:literal => $exp:expr),* $(,)?})=>{
impl<'t> DslSymNs<'t, $type> for $State {
const NS: DslNs<'t, fn (&'t $State)->$type> =
DslNs(&[$(($lit, |$state: &$State|$exp)),*]); } });
mod app_view; pub use self::app_view::*;
mod app_ctrl; pub use self::app_ctrl::*;
mod app_jack; pub use self::app_jack::*;
@ -202,20 +208,13 @@ impl Profile {
}
}
/// Various possible dialog modes.
#[derive(Debug, Clone, Default)]
pub enum Dialog {
#[default] None,
Help(usize),
Menu(usize),
Device(usize),
Message(Arc<str>),
Browser(BrowserTarget, Arc<Browser>),
Options,
}
impl App {
pub fn update_clock (&self) {
ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80)
}
pub fn focused_editor (&self) -> bool {
false
}
}
has!(Jack<'static>: |self: App|self.jack);
has!(Pool: |self: App|self.pool);
@ -242,12 +241,30 @@ fn unquote (x: &str) -> &str {
//chars.next_back();
chars.as_str()
}
#[derive(Debug, Clone, Default)]
pub enum Dialog {
#[default] None,
Help(usize),
Menu(usize),
Device(usize),
Message(Arc<str>),
Browser(BrowserTarget, Arc<Browser>),
Options,
}
impl Dialog {
fn menu_selected (&self) -> Option<usize> {
if let Self::Menu(selected) = self { Some(*selected) } else { None }
}
fn device_selected (&self) -> Option<usize> {
if let Self::Device(selected) = self { Some(*selected) } else { None }
fn device_kind (&self) -> Option<usize> {
if let Self::Device(index) = self { Some(*index) } else { None }
}
fn device_kind_next (&self) -> Option<usize> {
self.device_kind().map(|index|(index + 1) % device_kinds().len())
}
fn device_kind_prev (&self) -> Option<usize> {
self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1)))
}
fn message (&self) -> Option<&str> {
todo!()

View file

@ -1,9 +1,5 @@
use crate::*;
//handle!(TuiIn:|self: App, input|self.
impl App {
fn handle_tui_key_with_history (&mut self, input: &TuiIn) -> Perhaps<bool> {
handle!(TuiIn:|self: App, input|{
panic!("{input:?}");
//Ok(if let Some(binding) = self.profile.as_ref()
//.map(|c|c.keys.dispatch(input.event())).flatten()
@ -16,78 +12,76 @@ impl App {
//} else {
//None
//})
}
}
type MaybeClip = Option<Arc<RwLock<MidiClip>>>;
macro_rules!dsl_expose(($Struct:ident { $($fn:ident: $ret:ty = |$self:ident|$body:expr);* $(;)? })=>{
#[tengri_proc::expose] impl $Struct { $(fn $fn (&$self) -> $ret { $body })* }
})
dsl_sym!(|app: App| -> isize {
":_isize_stub" => -1
});
dsl_expose!(App {
_isize_stub: isize = |self|todo!();
_item_theme_stub: ItemTheme = |self|todo!();
w_sidebar: u16 = |self|self.project.w_sidebar(self.editor().is_some());
h_sample_detail: u16 = |self|6.max(self.height() as u16 * 3 / 9);
focus_editor: bool = |self|self.project.editor.is_some();
focus_dialog: bool = |self|!matches!(self.dialog, Dialog::None);
focus_message: bool = |self|matches!(self.dialog, Dialog::Message(..));
focus_add_device: bool = |self|matches!(self.dialog, Dialog::Device(..));
focus_browser: bool = |self|self.dialog.browser().is_some();
focus_clip: bool = |self|!self.focus_editor() && matches!(self.selection(), Selection::TrackClip{..});
focus_track: bool = |self|!self.focus_editor() && matches!(self.selection(), Selection::Track(..));
focus_scene: bool = |self|!self.focus_editor() && matches!(self.selection(), Selection::Scene(..));
focus_mix: bool = |self|!self.focus_editor() && matches!(self.selection(), Selection::Mix);
focus_pool_import: bool = |self|matches!(self.pool.mode, Some(PoolMode::Import(..)));
focus_pool_export: bool = |self|matches!(self.pool.mode, Some(PoolMode::Export(..)));
focus_pool_rename: bool = |self|matches!(self.pool.mode, Some(PoolMode::Rename(..)));
focus_pool_length: bool = |self|matches!(self.pool.mode, Some(PoolMode::Length(..)));
dialog_none: Option<Dialog> = |self|None;
dialog_device: Option<Dialog> = |self|Some(Dialog::Device(0)); // TODO
dialog_device_prev: Option<Dialog> = |self|Some(Dialog::Device(0)); // TODO
dialog_device_next: Option<Dialog> = |self|Some(Dialog::Device(0)); // TODO
dialog_help: Option<Dialog> = |self|Some(Dialog::Help(0));
dialog_menu: Option<Dialog> = |self|Some(Dialog::Menu(0));
dialog_save: Option<Dialog> = |self|Some(Dialog::Browser(BrowserTarget::SaveProject, Browser::new(None).unwrap().into()));
dialog_load: Option<Dialog> = |self|Some(Dialog::Browser(BrowserTarget::LoadProject, Browser::new(None).unwrap().into()));
dialog_import_clip: Option<Dialog> = |self|Some(Dialog::Browser(BrowserTarget::ImportClip(Default::default()), Browser::new(None).unwrap().into()));
dialog_export_clip: Option<Dialog> = |self|Some(Dialog::Browser(BrowserTarget::ExportClip(Default::default()), Browser::new(None).unwrap().into()));
dialog_import_sample: Option<Dialog> = |self|Some(Dialog::Browser(BrowserTarget::ImportSample(Default::default()), Browser::new(None).unwrap().into()));
dialog_export_sample: Option<Dialog> = |self|Some(Dialog::Browser(BrowserTarget::ExportSample(Default::default()), Browser::new(None).unwrap().into()));
dialog_options: Option<Dialog> = |self|Some(Dialog::Options);
editor_pitch: Option<u7> = |self|Some((self.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into());
scene_count: usize = |self|self.scenes().len();
scene_selected: Option<usize> = |self|self.selection().scene();
track_count: usize = |self|self.tracks().len();
track_selected: Option<usize> = |self|self.selection().track();
select_scene: Selection = |self|self.selection().select_scene(self.tracks().len());
select_scene_next: Selection = |self|self.selection().select_scene_next(self.scenes().len());
select_scene_prev: Selection = |self|self.selection().select_scene_prev();
select_track: Selection = |self|self.selection().select_track(self.tracks().len());
select_track_next: Selection = |self|self.selection().select_track_next(self.tracks().len());
select_track_prev: Selection = |self|self.selection().select_track_prev();
clip_selected: Option<Arc<RwLock<MidiClip>>> = |self|match self.selection() {
Selection::TrackClip { track, scene } => self.scenes()[*scene].clips[*track].clone(),
_ => None };
device_kind: usize = |self|if let Dialog::Device(index) = self.dialog {
index } else { 0 };
device_kind_next: usize = |self|if let Dialog::Device(index) = self.dialog {
(index + 1) % device_kinds().len() } else { 0 };
device_kind_prev: usize = |self|if let Dialog::Device(index) = self.dialog {
index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1))
} else { 0 };
dsl_sym!(|app: App| -> ItemTheme {
":_theme_stub" => Default::default()
});
macro_rules!dsl_bind(($Command:ident: $State:ident {
$($fn:ident = |$state:ident $(, $arg:ident:$ty:ty)*|$body:expr);* $(;)?
})=>{
handle!(TuiIn: |self: $State, input|self.handle_tui_key_with_history(input));
#[tengri_proc::command(App)] impl $Command {
$(fn $fn ($state: &mut $State $(, $arg:$ty)*) -> Perhaps<Self> { $body })*
dsl_sym!(|app: App| -> u16{
":w/sidebar" => app.project.w_sidebar(app.editor().is_some()),
":h/sample-detail" => 6.max(app.height() as u16 * 3 / 9),
});
dsl_sym!(|app: App| -> usize {
":scene-count" => app.scenes().len(),
":track-count" => app.tracks().len(),
":device-kind" => app.dialog.device_kind().unwrap_or(0),
":device-kind/next" => app.dialog.device_kind_next().unwrap_or(0),
":device-kind/prev" => app.dialog.device_kind_prev().unwrap_or(0),
});
dsl_sym!(|app: App| -> bool {
":focused/editor" => app.project.editor.is_some(),
":focused/dialog" => !matches!(app.dialog, Dialog::None),
":focused/message" => matches!(app.dialog, Dialog::Message(..)),
":focused/add_device" => matches!(app.dialog, Dialog::Device(..)),
":focused/browser" => app.dialog.browser().is_some(),
":focused/clip" => !app.focused_editor() && matches!(app.selection(), Selection::TrackClip{..}),
":focused/track" => !app.focused_editor() && matches!(app.selection(), Selection::Track(..)),
":focused/scene" => !app.focused_editor() && matches!(app.selection(), Selection::Scene(..)),
":focused/mix" => !app.focused_editor() && matches!(app.selection(), Selection::Mix),
":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(..))),
});
dsl_sym!(|app: App| -> Dialog {
":dialog/none" => Dialog::None,
":dialog/options" => Dialog::Options,
":dialog/device" => Dialog::Device(0),
":dialog/device/prev" => Dialog::Device(0),
":dialog/device/next" => Dialog::Device(0),
":dialog/help" => Dialog::Help(0),
":dialog/menu" => Dialog::Menu(0),
":dialog/save" => Dialog::Browser(BrowserTarget::SaveProject, Browser::new(None).unwrap().into()),
":dialog/load" => Dialog::Browser(BrowserTarget::LoadProject, Browser::new(None).unwrap().into()),
":dialog/import/clip" => Dialog::Browser(BrowserTarget::ImportClip(Default::default()), Browser::new(None).unwrap().into()),
":dialog/export/clip" => Dialog::Browser(BrowserTarget::ExportClip(Default::default()), Browser::new(None).unwrap().into()),
":dialog/import/sample" => Dialog::Browser(BrowserTarget::ImportSample(Default::default()), Browser::new(None).unwrap().into()),
":dialog/export/sample" => Dialog::Browser(BrowserTarget::ExportSample(Default::default()), Browser::new(None).unwrap().into()),
});
dsl_sym!(|app: App| -> Selection {
":select/scene" => app.selection().select_scene(app.tracks().len()),
":select/scene/next" => app.selection().select_scene_next(app.scenes().len()),
":select/scene/prev" => app.selection().select_scene_prev(),
":select/track" => app.selection().select_track(app.tracks().len()),
":select/track/next" => app.selection().select_track_next(app.tracks().len()),
":select/track/prev" => app.selection().select_track_prev(),
});
dsl_sym!(|app: App| -> Option<u7> {
":editor/pitch" => Some((app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into())
})
dsl_sym!(|app: App| -> Option<usize> {
":selected/scene" => app.selection().scene(),
":selected/track" => app.selection().track(),
});
dsl_sym!(|app: App| -> Option<Arc<RwLock<MidiClip>>> {
":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() {
app.scenes()[*scene].clips[*track].clone(),
} else {
None
}
});
dsl_bind!(AppCommand: App {
enqueue = |app, clip: Option<Arc<RwLock<MidiClip>>>| { todo!() };
history = |app, delta: isize| { todo!() };

32
crates/app/app_dsl.rs Normal file
View file

@ -0,0 +1,32 @@
use crate::*;
macro_rules!dsl_sym(
(|$state:ident:$State:ty| -> $type:ty {$($lit:literal => $exp:expr),* $(,)?})=>{
impl<'t> DslSymNs<'t, $type> for $State {
const NS: DslNs<'t, fn (&'t $State)->$type> =
DslNs(&[$(($lit, |$state: &$State|$exp)),*]); } });
pub trait DslSymNs<'t, T: 't>: 't {
const NS: DslNs<'t, fn (&'t Self)->T>;
}
pub struct DslNs<'t, T: 't>(pub &'t [(&'t str, T)]);
pub type DslCb = fn (&App) -> Box<dyn Render<TuiOut>>;
impl<'t, D: Dsl> std::ops::Index<D> for DslNs<'t, DslCb> {
type Output = DslCb;
fn index (&self, index: D) -> &Self::Output {
if let Ok(Some(symbol)) = index.src() {
for (key, value) in self.0.iter() {
if symbol == *key {
return value
}
}
}
&(view_nil as DslCb)
}
}
fn view_nil (_: &App) -> Box<dyn Render<TuiOut>> {
Box::new(Fill::xy("·"))
}

View file

@ -1,78 +1,21 @@
use crate::*;
content!(TuiOut:|self: App|VIEW[":view"](self));//if let Ok(Some(view)) = VIEW[":view"] { view(self) } else { panic!() });
struct DslNs<'t, T: 't>(&'t [(&'t str, T)]);
type DslCb = fn (&App) -> Box<dyn Render<TuiOut>>;
impl<'t, D: Dsl> std::ops::Index<D> for DslNs<'t, DslCb> {
type Output = DslCb;
fn index (&self, index: D) -> &Self::Output {
if let Ok(Some(symbol)) = index.src() {
for (key, value) in self.0.iter() {
if symbol == *key {
return value
}
}
}
&(view_nil as DslCb)
}
}
pub const VIEW: DslNs<'static, DslCb> = DslNs(&[
(":view", |state|Box::new(
Fill::xy(Bsp::a(VIEW[":view/content"](state), VIEW[":view/overlay"](state))))),
(":view/content", |state|Box::new(
Fill::xy(ErrorBoundary::new(Ok(Some("·")))))),
(":view/overlay", |state|match &state.dialog {
Dialog::Menu(_) => VIEW[":view/menu"](state),
Dialog::Device(_) => VIEW[":view/device"](state),
Dialog::Browser(_, _) => VIEW[":view/browse"](state),
//Dialog::Help(_) => view_help,
//Dialog::Message(_) => view_message,
//Dialog::Options => view_options,
_ => unimplemented!() }),
(":view/browse", |state|{
let browser = state.dialog.browser().cloned().unwrap();
Box::new(Bsp::s(
Padding::xy(3, 1, VIEW[":view/browse-title"](state)),
Outer(true, Style::default().fg(Tui::g(96)))
.enclose(Fill::xy(browser)))) }),
(":view/browse-title", |state: &App|{
let target = state.dialog.browser_target().unwrap();
Box::new(Fill::x(Align::w(FieldV(Default::default(), match target {
BrowserTarget::SaveProject => "Save project:",
BrowserTarget::LoadProject => "Load project:",
BrowserTarget::ImportSample(_) => "Import sample:",
BrowserTarget::ExportSample(_) => "Export sample:",
BrowserTarget::ImportClip(_) => "Import clip:",
BrowserTarget::ExportClip(_) => "Export clip:",
}, Shrink::x(3, Fixed::y(1, Tui::fg(Tui::g(96), RepeatH("🭻"))))))))}),
(":view/device", |state: &App|{
let selected = state.dialog.device_selected().unwrap();
Box::new(Bsp::s(Tui::bold(true, "Add device"), Map::south(1,
move||device_kinds().iter(),
move|label: &&'static str, i|{
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")))) }))) }),
(":view", |state|VIEW[":view/menu"](state)),
(":view/menu", |state|{
let selected = state.dialog.menu_selected();
let outputs = Fill::x(Fixed::y(3,
Bsp::a(Fill::x(Align::w(" L AUDIO OUTS")), Bsp::a("MIDI OUT", Fill::x(Align::e("AUDIO OUTS R "))))));
let inputs = Fill::x(Fixed::y(3,
Bsp::a(Fill::x(Align::w(" L AUDIO INS")), Bsp::a("MIDI INS", Fill::x(Align::e("AUDIO INS R "))))));
let outputs = VIEW[":view/ports/outs"](state);
let inputs = VIEW[":view/ports/ins"](state);
Box::new(Tui::bg(Rgb(0,0,0), Bsp::s(outputs, Bsp::s(
Fill::x(Fixed::y(3, Tui::bg(Rgb(33,33,33), Tui::bold(true, "tek 0.3.0-rc0")))),
Bsp::n(inputs, Bsp::n(
Fill::x(Fixed::y(3, Tui::bg(Rgb(33,33,33), Bsp::e(Tui::fg(Rgb(255,192,48), "[Enter]"), " new session")))),
Fill::y(Align::n(Fill::x(VIEW[":view/profiles"](state)))))))))) }),
(":view/profiles", |state|Box::new({
(":view/ports/outs", |state|Box::new(Fill::x(Fixed::y(3,
Bsp::a(Fill::x(Align::w(" L AUDIO OUTS")), Bsp::a("MIDI OUT", Fill::x(Align::e("AUDIO OUTS R ")))))))),
(":view/ports/ins", |state|Box::new(Fill::x(Fixed::y(3,
Bsp::a(Fill::x(Align::w(" L AUDIO INS")), Bsp::a("MIDI INS", Fill::x(Align::e("AUDIO INS R ")))))))),
(":view/profiles", |state: &App|Box::new({
let views = state.config.views.clone();
Stack::south(move|add: &mut dyn FnMut(&dyn Render<TuiOut>)|{
for (index, (id, profile)) in views.read().unwrap().iter().enumerate() {
@ -83,15 +26,34 @@ pub const VIEW: DslNs<'static, DslCb> = DslNs(&[
Fill::x(Bsp::a(
Fill::x(Align::w(Tui::fg(Rgb(224,192,128), name))),
Fill::x(Align::e(Tui::fg(Rgb(224,128,32), &id))))),
Fill::x(Align::w(info)))))); } })}))
Fill::x(Align::w(info)))))); } })})),
(":view/browse", |state: &App|{
let browser = state.dialog.browser().cloned().unwrap();
Box::new(Bsp::s(
Padding::xy(3, 1, VIEW[":view/browse-title"](state)),
Outer(true, Style::default().fg(Tui::g(96)))
.enclose(Fill::xy(browser)))) }),
(":view/browse-title", |state: &App|{
let target = state.dialog.browser_target().unwrap();
Box::new(Fill::x(Align::w(FieldV(Default::default(), match target {
BrowserTarget::SaveProject => "Save project:",
BrowserTarget::LoadProject => "Load project:",
BrowserTarget::ImportSample(_) => "Import sample:",
BrowserTarget::ExportSample(_) => "Export sample:",
BrowserTarget::ImportClip(_) => "Import clip:",
BrowserTarget::ExportClip(_) => "Export clip:",
}, Shrink::x(3, Fixed::y(1, Tui::fg(Tui::g(96), RepeatH("🭻"))))))))}),
(":view/device", |state: &App|{
let selected = state.dialog.device_kind().unwrap();
Box::new(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")))) }))) }),
//(":view/options", view_options),
]);
fn view_nil (_: &App) -> Box<dyn Render<TuiOut>> {
"nil".boxed()
}
fn wrap_dialog (dialog: impl Content<TuiOut>) -> impl Content<TuiOut> {
Fixed::xy(70, 23, Tui::fg_bg(Rgb(255,255,255), Rgb(16,16,16), Bsp::b(
Repeat(" "), Outer(true, Style::default().fg(Tui::g(96))).enclose(dialog))))