use #[namespace]

This commit is contained in:
i do not exist 2026-07-25 07:14:09 +03:00
parent 34bd2eeacf
commit b19d5bef24
10 changed files with 641 additions and 697 deletions

996
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,14 @@
# https://dystroy.org/bacon/config/ # https://dystroy.org/bacon/config/
default_job = "test" default_job = "test"
env.CARGO_TERM_COLOR = "always" env.CARGO_TERM_COLOR = "always"
[keybindings] [keybindings]
c = "job:check" c = "job:check"
t = "job:test" t = "job:test"
n = "job:nextest" n = "job:nextest"
l = "job:clippy" l = "job:clippy"
r = "job:run"
[jobs] [jobs]
[jobs.check] [jobs.check]
command = ["cargo", "check"] command = ["cargo", "check"]
@ -47,9 +50,9 @@ command = [
"cargo", "run", "cargo", "run",
# put launch parameters for your program behind a `--` separator # put launch parameters for your program behind a `--` separator
] ]
need_stdout = true need_stdout = false
allow_warnings = true allow_warnings = true
background = true background = false
[jobs.run-long] [jobs.run-long]
watch = ["dizzle", "tengri", "app"] watch = ["dizzle", "tengri", "app"]
command = [ "cargo", "run", ] command = [ "cargo", "run", ]

View file

@ -9,6 +9,7 @@
pkgs.libclang pkgs.libclang
pkgs.mold pkgs.mold
pkgs.cloc pkgs.cloc
pkgs.watchexec
]; ];
buildInputs = [ buildInputs = [
pkgs.libclang pkgs.libclang

View file

@ -29,7 +29,21 @@ pub mod size; pub use self::size::*;
/// //let _ = app.project.h_outputs(); /// //let _ = app.project.h_outputs();
/// let _ = app.project.h_scenes(); /// let _ = app.project.h_scenes();
/// ``` /// ```
#[derive(Default, Debug)] pub struct App { #[derive(Default, Debug)]
#[namespace(u8)]
#[namespace(isize)]
#[namespace(ItemTheme)]
#[namespace(Arc<str> get_arc_str)]
#[namespace(u16 get_u16)]
#[namespace(usize get_usize)]
#[namespace(bool get_bool)]
#[namespace(Selection get_selection)]
#[namespace(Color get_color)]
#[namespace(Option<u7> get_opt_u7)]
#[namespace(Option<u16> get_opt_u16)]
#[namespace(Option<usize> get_opt_usize)]
#[namespace(Option<Arc<RwLock<MidiClip>>> get_clip)]
pub struct App {
/// Base color. /// Base color.
pub color: ItemTheme, pub color: ItemTheme,
/// Must not be dropped for the duration of the process /// Must not be dropped for the duration of the process
@ -54,6 +68,129 @@ pub mod size; pub use self::size::*;
pub error: Arc<RwLock<Option<Arc<str>>>> pub error: Arc<RwLock<Option<Arc<str>>>>
} }
fn get_arc_str (_state: &App, src: impl Language) -> Perhaps<Arc<str>> {
Ok(src.src()?.map(|x|x.into()))
}
fn get_u16 (state: &App, src: impl Language) -> Perhaps<u16> {
if let Some(sym) = src.word()? {
Ok(Some(match sym {
":w/sidebar" => state.project.w_sidebar(state.editor().is_some()),
":h/sample-detail" => 6.max(state.size.h() as u16 * 3 / 9),
_ => return Err(format!("not a u16: {sym}").into())
}))
} else {
try_to_u16(src)
}
}
fn get_usize (state: &App, src: impl Language) -> Perhaps<usize> {
if let Some(sym) = src.word()? {
Ok(Some(match sym {
":scene-count" => state.scenes().len(),
":track-count" => state.tracks().len(),
":device-kind" => state.dialog.device_kind().unwrap_or(0),
":device-kind/next" => state.dialog.device_kind_next().unwrap_or(0),
":device-kind/prev" => state.dialog.device_kind_prev().unwrap_or(0),
_ => return Err(format!("not usize: {sym}").into())
}))
} else {
try_to_usize(src)
}
}
fn get_bool (state: &App, src: impl Language) -> Perhaps<bool> {
src.word()?.map(|word|Ok(match word {
"Y" => true,
"N" => false,
":mode/editor" => state.project.editor.is_some(),
":focused/dialog" => !matches!(state.dialog, Dialog::None),
":focused/message" => matches!(state.dialog, Dialog::Message(..)),
":focused/add_device" => matches!(state.dialog, Dialog::Device(..)),
":focused/browser" => state.dialog.browser().is_some(),
":focused/pool/import" => matches!(state.pool.mode, Some(PoolMode::Import(..))),
":focused/pool/export" => matches!(state.pool.mode, Some(PoolMode::Export(..))),
":focused/pool/rename" => matches!(state.pool.mode, Some(PoolMode::Rename(..))),
":focused/pool/length" => matches!(state.pool.mode, Some(PoolMode::Length(..))),
":focused/clip" => !state.editor_focused() && matches!(state.selection(), Selection::TrackClip{..}),
":focused/track" => !state.editor_focused() && matches!(state.selection(), Selection::Track(..)),
":focused/scene" => !state.editor_focused() && matches!(state.selection(), Selection::Scene(..)),
":focused/mix" => !state.editor_focused() && matches!(state.selection(), Selection::Mix),
_ => return Err(format!("not bool: {word}").into())
})).transpose()
}
fn get_selection (state: &App, src: impl Language) -> Perhaps<Selection> {
src.word()?.map(|word|Ok(match word {
":select/scene" => state.selection().select_scene(state.tracks().len()),
":select/scene/next" => state.selection().select_scene_next(state.scenes().len()),
":select/scene/prev" => state.selection().select_scene_prev(),
":select/track" => state.selection().select_track(state.tracks().len()),
":select/track/next" => state.selection().select_track_next(state.tracks().len()),
":select/track/prev" => state.selection().select_track_prev(),
_ => return Err(format!("not selection: {word}").into())
})).transpose()
}
fn get_color (state: &App, src: impl Language) -> Perhaps<Color> {
Ok(if let Some(sym) = src.word()? {
match sym {
":color/bg" => Some(Color::Rgb(28, 32, 36)),
_ => return Err(format!("not a color: {sym}").into())
}
} else if let Some(expr) = src.expr()? {
match expr.head()? {
Some("g") => {
let n = try_to_u8(expr.tail())?
.ok_or::<Box<dyn Error>>(format!("not gray: {expr}").into())?;
Some(Color::Rgb(n, n, n))
},
Some("rgb") => {
let r = try_to_u8(expr.tail().head())?
.ok_or::<Box<dyn Error>>(format!("not red: {expr}").into())?;
let g = try_to_u8(expr.tail().tail().head())?
.ok_or::<Box<dyn Error>>(format!("not green: {expr}").into())?;
let b = try_to_u8(expr.tail().tail().tail().head())?
.ok_or::<Box<dyn Error>>(format!("not blue: {expr}").into())?;
Some(Color::Rgb(r, g, b))
},
Some(_) => return Err(format!("not a color expression: {expr}").into()),
None => return Err(format!("not a color expression: {expr}").into())
}
} else {
None
})
}
fn get_opt_u7 (state: &App, src: impl Language) -> Perhaps<Option<u7>> {
src.word()?.map(|word|Ok(match word {
":editor/pitch" => Some((
state.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8
).into()),
_ => return Err(format!("unknown midi note: {word}").into())
})).transpose()
}
fn get_opt_u16 (_state: &App, _src: impl Language) -> Perhaps<Option<u16>> {
Ok(None)
}
fn get_opt_usize (state: &App, src: impl Language) -> Perhaps<Option<usize>> {
src.word()?.map(|word|Ok(match word {
":selected/scene" => state.selection().scene(),
":selected/track" => state.selection().track(),
_ => return Err(format!("unknown opt<usize>: {word}").into())
})).transpose()
}
fn get_clip (state: &App, src: impl Language) -> Perhaps<Option<Arc<RwLock<MidiClip>>>> {
src.word()?.map(|word|Ok(match word {
":selected/clip" if let Selection::TrackClip { track, scene } = state.selection() =>
state.scenes()[*scene].clips[*track].clone(),
_ => return Err(format!("not a clip: {word}").into())
})).transpose()
}
impl App { impl App {
/// Create a new application instance from a backend, project, config, and mode /// Create a new application instance from a backend, project, config, and mode
@ -247,78 +384,6 @@ primitive!(u16: try_to_u16);
primitive!(usize: try_to_usize); primitive!(usize: try_to_usize);
primitive!(isize: try_to_isize); primitive!(isize: try_to_isize);
namespace!(App: Arc<str> { 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.size.h() 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), }; });
namespace!(App: bool { symbol = |app| { // Provide boolean values.
":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),
}; });
namespace!(App: ItemTheme {}); // TODO: provide colors here
//
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<u7> { symbol = |app| {
":editor/pitch" => Some((app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into())
}; });
namespace!(App: Option<u16> { symbol = |app| {}; });
namespace!(App: Option<usize> { symbol = |app| {
":selected/scene" => app.selection().scene(),
":selected/track" => app.selection().track(),
}; });
namespace!(App: Option<Arc<RwLock<MidiClip>>> { symbol = |app| {
":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() {
app.scenes()[*scene].clips[*track].clone()
} else {
None
}
}; });
pub fn swap_value <T: Clone + PartialEq, U> ( pub fn swap_value <T: Clone + PartialEq, U> (
target: &mut T, value: &T, returned: impl Fn(T)->U target: &mut T, value: &T, returned: impl Fn(T)->U

View file

@ -145,13 +145,12 @@ pub fn draw_templates (to: &mut Tui, frags: std::str::Split<&str>, state: &App)
/// let _ = tek::view_logo(); /// let _ = tek::view_logo();
/// ``` /// ```
pub fn view_logo () -> impl Draw<Tui> { pub fn view_logo () -> impl Draw<Tui> {
bold(true, fg(Rgb(240, 200, 180), south!{ fg(Rgb(240, 200, 180), south!{
"".exact_h(1),
"".exact_h(1),
"~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~".exact_h(1), "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~".exact_h(1),
east("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", east(fg(Rgb(230,100,40), "v0.3.0"), " ~~")).exact_h(1), "~~~~ ║ ~ ╟─╌ ~╟─< ~~ v0.3.0 ~~".exact_h(1),
//east("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", east(fg(Rgb(230,100,40), "v0.3.0"), " ~~")).exact_h(1),
"~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~".exact_h(1), "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~".exact_h(1),
})).exact_wh(32, 7) }.exact_w(32))
} }
/// ``` /// ```
@ -465,7 +464,7 @@ pub fn view_track_outputs (
south(button_2("o", "utput", false).align_w().full_w(), south(button_2("o", "utput", false).align_w().full_w(),
thunk(|to: &mut Tui|{ thunk(|to: &mut Tui|{
for port in midi_outs { for port in midi_outs {
port.port_name().align_w().full_w().draw(to); let _ = port.port_name().align_w().full_w().draw(to)?;
} }
Ok(Some(XYWH(0, 0, 0, 0))) Ok(Some(XYWH(0, 0, 0, 0)))
})), })),
@ -584,7 +583,7 @@ pub fn view_inputs (tracks: impl TracksSizes<'_>, midi_ins: &[MidiInput]) -> imp
let title_2 = button_2("I", "+", false).exact_wh(4, 1); let title_2 = button_2("I", "+", false).exact_wh(4, 1);
east(title_1, west(title_2, thunk(move|to: &mut Tui|{ east(title_1, west(title_2, thunk(move|to: &mut Tui|{
for (_index, track, x1, _x2) in tracks { for (_index, track, x1, _x2) in tracks {
south( let _ = south(
bg(track.color.dark.term, east!( bg(track.color.dark.term, east!(
either(track.sequencer.monitoring, fg(Green, "mon "), "mon "), either(track.sequencer.monitoring, fg(Green, "mon "), "mon "),
either(track.sequencer.recording, fg(Red, "rec "), "rec "), either(track.sequencer.recording, fg(Red, "rec "), "rec "),
@ -592,26 +591,24 @@ pub fn view_inputs (tracks: impl TracksSizes<'_>, midi_ins: &[MidiInput]) -> imp
).exact_w(track.width as u16)).align_w().push_x(x1 as u16), ).exact_w(track.width as u16)).align_w().push_x(x1 as u16),
thunk(move |to: &mut Tui|{ thunk(move |to: &mut Tui|{
for (index, port) in midi_ins.iter().enumerate() { for (index, port) in midi_ins.iter().enumerate() {
east( let _ = east(
east( east(
"", "",
bold(true, fg(Rgb(255,255,255), port.port_name())) bold(true, fg(Rgb(255,255,255), port.port_name()))
).align_w().exact_w(20), ).align_w().exact_w(20),
west( west(
().exact_w(4), ().exact_w(4),
thunk(move|to: &mut Tui|{
bg(track.color.darker.term, east!( bg(track.color.darker.term, east!(
either(track.sequencer.monitoring, fg(Green, ""), " · "), either(track.sequencer.monitoring, fg(Green, ""), " · "),
either(track.sequencer.recording, fg(Red, ""), " · "), either(track.sequencer.recording, fg(Red, ""), " · "),
either(track.sequencer.overdub, fg(Yellow, ""), " · "), either(track.sequencer.overdub, fg(Yellow, ""), " · "),
).exact_w(track.width as u16).align_w()).draw(to); ).exact_w(track.width as u16).align_w())
todo!() )
})) ).push_x(index as u16 * 10).exact_h(1).draw(to)?;
).push_x(index as u16 * 10).exact_h(1).draw(to);
} }
todo!() todo!()
}) })
).draw(to); ).draw(to)?;
} }
todo!() todo!()
}))) })))
@ -646,7 +643,7 @@ pub fn view_outputs (
view_track_row_section(theme, list, button_2("O", "+", false), view_track_row_section(theme, list, button_2("O", "+", false),
bg(theme.darker.term, thunk(|to: &mut Tui|{ bg(theme.darker.term, thunk(|to: &mut Tui|{
for (index, track, _x1, _x2) in tracks { for (index, track, _x1, _x2) in tracks {
thunk(|to: &mut Tui|{ let _ = thunk(|to: &mut Tui|{
east( east(
either(true, fg(Green, "play "), "play "), either(true, fg(Green, "play "), "play "),
either(false, fg(Yellow, "solo "), "solo "), either(false, fg(Yellow, "solo "), "solo "),
@ -661,7 +658,7 @@ pub fn view_outputs (
} }
} }
todo!() todo!()
}).exact_w(track_width(index, track)).draw(to); }).exact_w(track_width(index, track)).draw(to)?;
} }
todo!() todo!()
}).align_w().full_w())).exact_h(height) }).align_w().full_w())).exact_h(height)
@ -711,10 +708,14 @@ pub fn per_track_top <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
}).align_x()) }).align_x())
} }
pub fn field_h <T: Screen> (theme: ItemTheme, head: impl Draw<T>, body: impl Draw<T>) -> impl Draw<T> { pub fn field_h <T: Screen> (
theme: ItemTheme, head: impl Draw<T>, body: impl Draw<T>
) -> impl Draw<T> {
} }
pub fn field_v <T: Screen> (theme: ItemTheme, head: impl Draw<T>, body: impl Draw<T>) -> impl Draw<T> { pub fn field_v <T: Screen> (
theme: ItemTheme, head: impl Draw<T>, body: impl Draw<T>
) -> impl Draw<T> {
} }
pub fn view_sessions () -> impl Draw<Tui> { pub fn view_sessions () -> impl Draw<Tui> {

View file

@ -6,7 +6,8 @@ pub(crate) use ::xdg::BaseDirectories;
pub(crate) use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*}; pub(crate) use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*};
pub(crate) use tengri::{ pub(crate) use tengri::{
*, lang::*, *,
lang::*,
crossterm::event::{Event, KeyEvent}, crossterm::event::{Event, KeyEvent},
ratatui::{ ratatui::{
self, self,

View file

@ -14,26 +14,41 @@ use crate::{*, browse::*, device::*, menu::*};
Browse(BrowseTarget, Arc<Browse>), Browse(BrowseTarget, Arc<Browse>),
Options, Options,
} }
namespace!(App: Dialog { symbol = |app| {
impl<'n> Namespace<'n, Dialog> for App {
fn namespace (&self, src: impl Language) -> Perhaps<Dialog> {
src.word()?.map(|word|Ok(match word {
":dialog/none" => Dialog::None, ":dialog/none" => Dialog::None,
":dialog/options" => Dialog::Options, ":dialog/options" => Dialog::Options,
":dialog/device" => Dialog::Device(0), ":dialog/device" => Dialog::Device(0),
":dialog/device/prev" => Dialog::Device(0), ":dialog/device/prev" => Dialog::Device(0),
":dialog/device/next" => Dialog::Device(0), ":dialog/device/next" => Dialog::Device(0),
":dialog/help" => Dialog::Help(0), ":dialog/help" => Dialog::Help(0),
":dialog/save" => Dialog::Browse(BrowseTarget::SaveProject, ":dialog/save" => Dialog::Browse(
BrowseTarget::SaveProject,
Browse::new(None).unwrap().into()), Browse::new(None).unwrap().into()),
":dialog/load" => Dialog::Browse(BrowseTarget::LoadProject, ":dialog/load" => Dialog::Browse(
BrowseTarget::LoadProject,
Browse::new(None).unwrap().into()), Browse::new(None).unwrap().into()),
":dialog/import/clip" => Dialog::Browse(BrowseTarget::ImportClip(Default::default()), ":dialog/import/clip" => Dialog::Browse(
BrowseTarget::ImportClip(Default::default()),
Browse::new(None).unwrap().into()), Browse::new(None).unwrap().into()),
":dialog/export/clip" => Dialog::Browse(BrowseTarget::ExportClip(Default::default()), ":dialog/export/clip" => Dialog::Browse(
BrowseTarget::ExportClip(Default::default()),
Browse::new(None).unwrap().into()), Browse::new(None).unwrap().into()),
":dialog/import/sample" => Dialog::Browse(BrowseTarget::ImportSample(Default::default()), ":dialog/import/sample" => Dialog::Browse(
BrowseTarget::ImportSample(Default::default()),
Browse::new(None).unwrap().into()), Browse::new(None).unwrap().into()),
":dialog/export/sample" => Dialog::Browse(BrowseTarget::ExportSample(Default::default()), ":dialog/export/sample" => Dialog::Browse(
BrowseTarget::ExportSample(Default::default()),
Browse::new(None).unwrap().into()), Browse::new(None).unwrap().into()),
}; }); _ => return Err(
format!("not a dialog: {}", src.src()?.unwrap_or("<???>")).into()
)
})).transpose()
}
}
impl Dialog { impl Dialog {
/// ``` /// ```
/// let _ = tek::Dialog::welcome(); /// let _ = tek::Dialog::welcome();

View file

@ -16,12 +16,12 @@
(info Mode selector.) (info Mode selector.)
(keys :axis/y :confirm) (keys :axis/y :confirm)
(view (bg (g 0) (bsp/s (view (bg (g 0) (bsp/s
:ports/out (bg (g 10) :ports/out)
(bsp/n (bsp/n
:ports/in (bg (g 10) :ports/in)
(bg (g 30) (bsp/s (bg (g 30) (bsp/s
(fixed/y 7 :logo) :logo
(fill :dialog/menu)))))))) :dialog/menu)))))))
(view :ports/out (fill/x (fixed/y 3 (bsp/a (view :ports/out (fill/x (fixed/y 3 (bsp/a
(fill/x (align/w (text L-AUDIO-OUT))) (fill/x (align/w (text L-AUDIO-OUT)))

View file

@ -25,10 +25,14 @@ pub extern crate tengri;
}).map(|_|()) }).map(|_|())
} }
pub mod deps; pub use self::deps::*; pub mod deps;
pub mod device; pub use self::device::*; pub(crate) use self::deps::*;
pub mod app; pub use self::app::*;
pub mod device;
pub use self::device::*;
pub mod app;
pub use self::app::*;
/// CLI banner. /// CLI banner.
pub(crate) const HEADER: &'static str = r#" pub(crate) const HEADER: &'static str = r#"

2
tengri

@ -1 +1 @@
Subproject commit 482b6241690f7b116b07d06fb4950754f6fdf584 Subproject commit be98e666f53eedf95618396239641ad846a772bd