refactor: Mode, View, Bind

This commit is contained in:
i do not exist 2026-07-02 09:21:47 +03:00
parent 51faa82d74
commit 701a651fbd
21 changed files with 901 additions and 880 deletions

View file

@ -3,9 +3,7 @@ pub mod audio; #[allow(unused)] pub use self::audio::*;
pub mod bind; pub use self::bind::*; pub mod bind; pub use self::bind::*;
pub mod cli; pub use self::cli::*; pub mod cli; pub use self::cli::*;
pub mod config; pub use self::config::*; pub mod config; pub use self::config::*;
pub mod view; pub use self::view::*;
pub mod draw; pub use self::draw::*; pub mod draw; pub use self::draw::*;
pub mod mode; pub use self::mode::*;
pub mod size; pub use self::size::*; pub mod size; pub use self::size::*;
/// Total application state. /// Total application state.

View file

@ -1,16 +1,41 @@
use crate::*; use crate::*;
tui_keys!(|self: App, input| { tui_keys!(|self: App, input| {
todo!() let commands = tek_commands_collect(self, input)?;
let results = tek_commands_execute(self, commands)?;
self.history.extend(results.into_iter());
Ok(())
}); });
/// A control axis. fn tek_commands_collect (app: &App, input: &TuiEvent)
/// -> Usually<Vec<AppCommand>>
/// ``` {
/// let axis = tek::ControlAxis::X; let mut commands = vec![];
/// ``` for id in app.mode.keys.iter() {
#[derive(Debug, Copy, Clone)] pub enum ControlAxis { if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref())
X, Y, Z, I && let Some(bindings) = event_map.query(input) {
for binding in bindings {
for command in binding.commands.iter() {
if let Some(command) = app.namespace(command)? as Option<AppCommand> {
commands.push(command)
}
}
}
}
}
Ok(commands)
}
fn tek_commands_execute (app: &mut App, commands: Vec<AppCommand>)
-> Usually<Vec<(AppCommand, Option<AppCommand>)>>
{
let mut history = vec![];
for command in commands.into_iter() {
let result = command.act(app);
match result { Err(err) => { history.push((command, None)); return Err(err) }
Ok(undo) => { history.push((command, undo)); } };
}
Ok(history)
} }
/// Collection of input bindings. /// Collection of input bindings.
@ -91,6 +116,7 @@ impl Bind<TuiEvent, Arc<str>> {
impl<E, C> Default for Bind<E, C> { impl<E, C> Default for Bind<E, C> {
fn default () -> Self { Self(Default::default()) } fn default () -> Self { Self(Default::default()) }
} }
impl<C: Default> Default for Binding<C> { impl<C: Default> Default for Binding<C> {
fn default () -> Self { fn default () -> Self {
Self { Self {
@ -158,38 +184,14 @@ impl<'a> Namespace<'a, AppCommand> for App {
}); });
} }
//impl_handle!(TuiIn: |self: App, input|{ /// A control axis.
//let commands = tek_collect_commands(self, input)?; ///
//let history = tek_execute_commands(self, commands)?; /// ```
//self.history.extend(history.into_iter()); /// let axis = tek::ControlAxis::X;
//Ok(None) /// ```
//}); #[derive(Debug, Copy, Clone)] pub enum ControlAxis {
X, Y, Z, I
}
//fn tek_collect_commands (app: &App, input: &TuiIn) -> Usually<Vec<AppCommand>> { //take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref()
//let mut commands = vec![]; //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));
//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<AppCommand> {
//commands.push(command)
//}
//}
//}
//}
//}
//Ok(commands)
//}
//fn tek_execute_commands (
//app: &mut App, commands: Vec<AppCommand>
//) -> Usually<Vec<(AppCommand, Option<AppCommand>)>> {
//let mut history = vec![];
//for command in commands.into_iter() {
//let result = command.act(app);
//match result { Err(err) => { history.push((command, None)); return Err(err) }
//Ok(undo) => { history.push((command, undo)); } };
//}
//Ok(history)
//}

View file

@ -208,7 +208,7 @@ pub fn tek_print_config (config: &Config) {
} }
} }
} }
for (k, v) in config.modes.read().unwrap().iter() { config.modes.for_each(|k, v|{
println!(); println!();
for v in v.name.iter() { print!("{}", Green.bold().paint(format!("{v} "))); } for v in v.name.iter() { print!("{}", Green.bold().paint(format!("{v} "))); }
for v in v.info.iter() { print!("\n{}", Green.paint(format!("{v}"))); } for v in v.info.iter() { print!("\n{}", Green.paint(format!("{v}"))); }
@ -216,22 +216,16 @@ pub fn tek_print_config (config: &Config) {
print!("\n{}", Blue.paint("KEYS")); print!("\n{}", Blue.paint("KEYS"));
for v in v.keys.iter() { print!("{}", Green.paint(format!(" {v}"))); } for v in v.keys.iter() { print!("{}", Green.paint(format!(" {v}"))); }
println!(); println!();
for (k, v) in v.modes.read().unwrap().iter() { v.modes.for_each(|k, v|{
print!("{} {} {:?}", print!("{} {} {:?}", Blue.paint("MODE"), Green.bold().paint(format!("{k:<16}")), v.name);
Blue.paint("MODE"), print!( " INFO={:?}", v.info);
Green.bold().paint(format!("{k:<16}")), print!( " VIEW={:?}", v.view);
v.name); println!(" KEYS={:?}", v.keys);
print!(" INFO={:?}", });
v.info);
print!(" VIEW={:?}",
v.view);
println!(" KEYS={:?}",
v.keys);
}
print!("{}", Blue.paint("VIEW")); print!("{}", Blue.paint("VIEW"));
for v in v.view.iter() { print!("{}", Green.paint(format!(" {v}"))); } for v in v.view.iter() { print!("{}", Green.paint(format!(" {v}"))); }
println!(); println!();
} });
} }
pub fn tek_print_status (project: &Arrangement) { pub fn tek_print_status (project: &Arrangement) {

View file

@ -1,4 +1,4 @@
use crate::{*, bind::*, view::*}; use crate::*;
/// Configuration: mode, view, and bind definitions. /// Configuration: mode, view, and bind definitions.
/// ///
@ -48,12 +48,14 @@ impl Config {
config.init()?; config.init()?;
Ok(config) Ok(config)
} }
/// Create a new app configuration from a set of XDG base directories, /// Create a new app configuration from a set of XDG base directories,
pub fn new (dirs: Option<BaseDirectories>) -> Self { pub fn new (dirs: Option<BaseDirectories>) -> Self {
let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB); let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB);
let dirs = dirs.unwrap_or_else(default); let dirs = dirs.unwrap_or_else(default);
Self { dirs, ..Default::default() } Self { dirs, ..Default::default() }
} }
/// Write initial contents of configuration. /// Write initial contents of configuration.
pub fn init (&mut self) -> Usually<()> { pub fn init (&mut self) -> Usually<()> {
self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|{ self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|{
@ -62,6 +64,7 @@ impl Config {
})?; })?;
Ok(()) Ok(())
} }
/// Write initial contents of a configuration file. /// Write initial contents of a configuration file.
pub fn init_one ( pub fn init_one (
&mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()> &mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()>
@ -78,11 +81,13 @@ impl Config {
return Err(format!("{path}: not found").into()) return Err(format!("{path}: not found").into())
}) })
} }
/// Add statements to configuration from [Dsl] source. /// Add statements to configuration from [Dsl] source.
pub fn add (&mut self, dsl: impl Language) -> Usually<&mut Self> { pub fn add (&mut self, dsl: impl Language) -> Usually<&mut Self> {
dsl.each(|item|self.add_one(item))?; dsl.each(|item|self.add_one(item))?;
Ok(self) Ok(self)
} }
fn add_one (&self, item: impl Language) -> Usually<()> { fn add_one (&self, item: impl Language) -> Usually<()> {
if let Some(expr) = item.expr()? { if let Some(expr) = item.expr()? {
let head = expr.head()?; let head = expr.head()?;
@ -91,7 +96,7 @@ impl Config {
let body = tail.tail()?; let body = tail.tail()?;
//println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default()); //println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default());
match head { match head {
Some("mode") if let Some(name) = name => load_mode(&self.modes, &name, &body)?, Some("mode") if let Some(name) = name => self.modes.add(&name, &body)?,
Some("keys") if let Some(name) = name => load_bind(&self.binds, &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)?, 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()) _ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into())
@ -101,7 +106,12 @@ impl Config {
return Err(format!("Config::load: expected expr, got: {item:?}").into()) return Err(format!("Config::load: expected expr, got: {item:?}").into())
} }
} }
pub fn get_mode (&self, mode: impl AsRef<str>) -> Option<Arc<Mode<Arc<str>>>> { pub fn get_mode (&self, mode: impl AsRef<str>) -> Option<Arc<Mode<Arc<str>>>> {
self.modes.clone().read().unwrap().get(mode.as_ref()).cloned() self.modes.get(mode)
} }
} }
mod views; pub use self::views::*;
mod modes; pub use self::modes::*;
mod mode; pub use self::mode::*;

View file

@ -1,15 +1,19 @@
use crate::*; use crate::*;
pub(crate) fn load_mode (modes: &Modes, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> { /// Group of view and keys definitions.
let mut mode = Mode::default(); ///
body.each(|item|mode.add(item))?; /// ```
modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode)); /// let mode = tek::Mode::<std::sync::Arc<str>>::default();
Ok(()) /// ```
#[derive(Default, Debug)] pub struct Mode<D: Language + Ord> {
pub path: PathBuf,
pub name: Vec<D>,
pub info: Vec<D>,
pub view: Vec<D>,
pub keys: Vec<D>,
pub modes: Modes,
} }
/// Collection of interaction modes.
pub type Modes = Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode<Arc<str>>>>>>;
impl Mode<Arc<str>> { impl Mode<Arc<str>> {
/// Add a definition to the mode. /// Add a definition to the mode.
/// ///
@ -71,23 +75,9 @@ impl Mode<Arc<str>> {
} }
fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> { fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> {
Ok(Some(if let Some(id) = dsl.head()? { Ok(Some(if let Some(id) = dsl.head()? {
load_mode(&self.modes, &id, &dsl.tail())?; self.modes.add(&id, &dsl.tail())?;
} else { } else {
return Err(format!("Mode::add: self: incomplete: {dsl:?}").into()); return Err(format!("Mode::add: self: incomplete: {dsl:?}").into());
})) }))
} }
} }
/// Group of view and keys definitions.
///
/// ```
/// let mode = tek::Mode::<std::sync::Arc<str>>::default();
/// ```
#[derive(Default, Debug)] pub struct Mode<D: Language + Ord> {
pub path: PathBuf,
pub name: Vec<D>,
pub info: Vec<D>,
pub view: Vec<D>,
pub keys: Vec<D>,
pub modes: Modes,
}

27
src/app/config/modes.rs Normal file
View file

@ -0,0 +1,27 @@
use crate::*;
/// Collection of UI modes.
#[derive(Default, Debug, Clone)]
pub struct Modes(
Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode<Arc<str>>>>>>
);
impl Modes {
pub fn add (&self, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
let mut mode = Mode::default();
body.each(|item|mode.add(item))?;
self.0.write().unwrap().insert(name.as_ref().into(), Arc::new(mode));
Ok(())
}
pub fn get (&self, name: impl AsRef<str>) -> Option<Arc<Mode<Arc<str>>>> {
self.0.read().unwrap().get(name.as_ref()).cloned()
}
pub fn for_each <T> (&self, mut ator: impl FnMut(&str, &Mode<Arc<str>>)->T) {
for (k, v) in self.0.read().unwrap().iter() {
let _ = ator(k.as_ref(), v.as_ref());
}
}
pub fn len (&self) -> usize {
self.0.read().unwrap().len()
}
}

10
src/app/config/views.rs Normal file
View file

@ -0,0 +1,10 @@
use crate::*;
/// Collection of custom view definitions.
pub type Views = Arc<RwLock<BTreeMap<Arc<str>, Arc<str>>>>;
/// Load custom view definition.
pub(crate) fn load_view (views: &Views, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
views.write().unwrap().insert(name.as_ref().into(), body.src()?.unwrap_or_default().into());
Ok(())
}

View file

@ -1,5 +1,9 @@
use crate::*; use crate::*;
tui_view!(|self: App| {
""
});
/// The [Draw] implementation for [App] handles the loaded view, /// The [Draw] implementation for [App] handles the loaded view,
/// which is defined in terms of [dizzle] DSL. /// which is defined in terms of [dizzle] DSL.
/// ///
@ -137,10 +141,10 @@ pub fn draw_dialog (
pub fn draw_templates (to: &mut Tui, frags: std::str::Split<&str>, state: &App) pub fn draw_templates (to: &mut Tui, frags: std::str::Split<&str>, state: &App)
-> Usually<XYWH<u16>> -> Usually<XYWH<u16>>
{ {
let modes = state.config.modes.clone(); let height = (state.config.modes.len() * 2) as u16;
let height = (modes.read().unwrap().len() * 2) as u16;
h_exact(height, w_min(Some(30), thunk(move |to: &mut Tui|{ h_exact(height, w_min(Some(30), thunk(move |to: &mut Tui|{
for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { let mut index = 0;
state.config.modes.for_each(|id, profile| {
let b = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; let b = 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("<no name>"); let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or("<no name>");
let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or("<no info>"); let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or("<no info>");
@ -149,12 +153,613 @@ pub fn draw_templates (to: &mut Tui, frags: std::str::Split<&str>, state: &App)
let field_name = w_full(origin_w(fg(fg1, name))); let field_name = w_full(origin_w(fg(fg1, name)));
let field_id = w_full(origin_e(fg(fg2, id))); let field_id = w_full(origin_e(fg(fg2, id)));
let field_info = w_full(origin_w(info)); let field_info = w_full(origin_w(info));
y_push((2 * index) as u16, let _ = y_push((2 * index) as u16,
h_exact(2, w_full(bg(b, south( h_exact(2, w_full(bg(b, south(
above(field_name, field_id), above(field_name, field_id),
field_info field_info
))))).draw(to); ))))).draw(to);
} index += 1;
});
Ok(to.area().into()) Ok(to.area().into())
}))).draw(to) }))).draw(to)
} }
/// ```
/// let _ = tek::view_logo();
/// ```
pub fn view_logo () -> impl Draw<Tui> {
wh_exact(Some(32), Some(7), bold(true, fg(Rgb(240, 200, 180), south!{
h_exact(1, ""),
h_exact(1, ""),
h_exact(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"),
h_exact(1, east("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", east(fg(Rgb(230,100,40), "v0.3.0"), " ~~"))),
h_exact(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"),
})))
}
/// ```
/// let x = "";
/// let _ = tek::view_transport(true, x.as_ref(), x.as_ref(), x.as_ref());
/// let _ = tek::view_transport(false, x.as_ref(), x.as_ref(), x.as_ref());
/// ```
pub fn view_transport (play: bool, bpm: &str, beat: &str, time: &str) -> impl Draw<Tui> {
let theme = ItemTheme::G[96];
bg(Black, east!(above(
wh_full(origin_w(button_play_pause(play, false))),
wh_full(origin_e(east!(
field_h(theme, "BPM", bpm),
field_h(theme, "Beat", beat),
field_h(theme, "Time", time),
)))
)))
}
/// ```
/// let x = "";
/// let _ = tek::view_status(None, x.as_ref(), x.as_ref(), x.as_ref());
/// let _ = tek::view_status(Some("".into()), x.as_ref(), x.as_ref(), x.as_ref());
/// ```
pub fn view_status (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl Draw<Tui> {
let theme = ItemTheme::G[96];
let sr = field_h(theme, "SR", sr);
let buf = field_h(theme, "Buf", buf);
let lat = field_h(theme, "Lat", lat);
bg(Black, east!(above(
wh_full(origin_w(sel.map(|sel|field_h(theme, "Selected", sel)))),
wh_full(origin_e(east!(sr, buf, lat))),
)))
}
/// ```
/// let _ = tek::button_play_pause(true, true);
/// let _ = tek::button_play_pause(true, false);
/// let _ = tek::button_play_pause(false, true);
/// let _ = tek::button_play_pause(false, false);
/// ```
pub fn button_play_pause (playing: bool, compact: bool) -> impl Draw<Tui> {
bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) },
either(compact,
thunk(move|to: &mut Tui|w_exact(9, either(playing,
fg(Rgb(0, 255, 0), " PLAYING "),
fg(Rgb(255, 128, 0), " STOPPED "))
).draw(to)),
thunk(move|to: &mut Tui|w_exact(5, either(playing,
fg(Rgb(0, 255, 0), south(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)),
fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",)))
).draw(to)),
)
)
}
#[cfg(feature = "track")] pub fn view_track_row_section (
_theme: ItemTheme,
button: impl Draw<Tui>,
button_add: impl Draw<Tui>,
content: impl Draw<Tui>,
) -> impl Draw<Tui> {
west(h_full(w_exact(4, origin_nw(button_add))),
east(w_exact(20, h_full(origin_nw(button))), wh_full(origin_c(content))))
}
/// ```
/// let bg = tengri::ratatui::style::Color::Red;
/// let fg = tengri::ratatui::style::Color::Green;
/// let _ = tek::view_wrap(bg, fg, "and then blue, too!");
/// ```
pub fn view_wrap (bg: Color, fg: Color, content: impl Draw<Tui>) -> impl Draw<Tui> {
let left = fg_bg(bg, Reset, w_exact(1, y_repeat("")));
let right = fg_bg(bg, Reset, w_exact(1, y_repeat("")));
east(left, west(right, fg_bg(fg, bg, content)))
}
/// ```
/// let _ = tek::view_meter("", 0.0);
/// let _ = tek::view_meters(&[0.0, 0.0]);
/// ```
pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Draw<Tui> + 'a {
let f = field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value));
let w = if value >= 0.0 { 13 }
else if value >= -1.0 { 12 }
else if value >= -2.0 { 11 }
else if value >= -3.0 { 10 }
else if value >= -4.0 { 9 }
else if value >= -6.0 { 8 }
else if value >= -9.0 { 7 }
else if value >= -12.0 { 6 }
else if value >= -15.0 { 5 }
else if value >= -20.0 { 4 }
else if value >= -25.0 { 3 }
else if value >= -30.0 { 2 }
else if value >= -40.0 { 1 }
else { 0 };
let c = if value >= 0.0 { Red }
else if value >= -3.0 { Yellow }
else { Green };
south!(f, wh_exact(Some(w), Some(1), bg(c, ())))
}
pub fn view_meters (values: &[f32;2]) -> impl Draw<Tui> + use<'_> {
let left = format!("L/{:>+9.3}", values[0]);
let right = format!("R/{:>+9.3}", values[1]);
south(left, right)
}
pub fn view_sample_info (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
when(sample.is_some(), thunk(move|to: &mut Tui|{
let sample = sample.unwrap().read().unwrap();
let theme = sample.color;
east!(
field_h(theme, "Name", format!("{:<10}", sample.name.clone())),
field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())),
field_h(theme, "Start", format!("{:<8}", sample.start)),
field_h(theme, "End", format!("{:<8}", sample.end)),
field_h(theme, "Trans", "0"),
field_h(theme, "Gain", format!("{}", sample.gain)),
).draw(to)
}))
}
pub fn view_sample_info_v (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
let a = thunk(move|to: &mut Tui|{
let sample = sample.unwrap().read().unwrap();
let theme = sample.color;
w_exact(20, south!(
w_full(origin_w(field_h(theme, "Name ", format!("{:<10}", sample.name.clone())))),
w_full(origin_w(field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())))),
w_full(origin_w(field_h(theme, "Start ", format!("{:<8}", sample.start)))),
w_full(origin_w(field_h(theme, "End ", format!("{:<8}", sample.end)))),
w_full(origin_w(field_h(theme, "Trans ", "0"))),
w_full(origin_w(field_h(theme, "Gain ", format!("{}", sample.gain)))),
)).draw(to)
});
let b = thunk(|to: &mut Tui|fg(Red, south!(
bold(true, "× No sample."),
"[r] record",
"[Shift-F9] import",
)).draw(to));
either(sample.is_some(), a, b)
}
pub fn view_sample_status (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> {
bold(true, fg(g(224), sample
.map(|sample|{
let sample = sample.read().unwrap();
format!("Sample {}-{}", sample.start, sample.end)
})
.unwrap_or_else(||"No sample".to_string())))
}
pub fn view_track_header (theme: ItemTheme, content: impl Draw<Tui>) -> impl Draw<Tui> {
w_exact(12, bg(theme.darker.term, w_full(origin_e(content))))
}
pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T])
-> impl Draw<Tui> + use<'a, T>
{
let ins = ports.len() as u16;
let frame = Outer(true, Style::default().fg(g(96)));
let iter = move||ports.iter();
let names = iter_south(iter, move|port, index|h_full(origin_w(format!(" {index} {}", port.port_name()))));
let field = field_v(theme, title, names);
wh_exact(Some(20), Some(1 + ins), border(true, frame, wh_exact(Some(20), Some(1 + ins), field)))
}
pub fn view_io_ports <'a, T: PortsSizes<'a>> (
fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a
) -> impl Draw<Tui> + 'a {
type Item<'a> = (usize, &'a Arc<str>, &'a [Connect], usize, usize);
iter(items, move|(_index, name, connections, y, y2): Item<'a>, _| {
y_push(y as u16, h_exact((y2-y) as u16,
south(h_full(bold(true, fg_bg(fg, bg, origin_w(east(" 󰣲 ", name))))),
iter(||connections.iter(), move|connect: &'a Connect, index|y_push(
index as u16, h_exact(1, origin_w(bold(false, fg_bg(fg, bg, &connect.info))))
)))))
})
}
pub fn view_scenes_clips <'a, S: ScenesSizes<'a>> (
scenes: impl Fn()->S,
tracks: impl TracksSizes<'a>,
select: &Selection,
editor: Option<&MidiEditor>,
size: &Size,
editing: bool,
) -> impl Draw<Tui> {
let status = wh_full(origin_se(fg(Green, format!("{}x{}", size.w(), size.h()))));
let tracks = iter_once(tracks, move|(track_index, track, _, _), _| {
let scenes = iter_once(scenes(), move|(scene_index, scene, _, _), _| {
let (name, theme): (Arc<str>, ItemTheme) = scene_name_theme(scene, track_index);
let f = theme.lightest.term;
let (b, o) = scene_bg(theme, select, track_index, scene_index);
let w = scene_w(track, select, track_index, editor);
let y = scene_y(select, scene_index, editor);
let is_selected = scene_sel(select, track_index, scene_index, editing);
wh_exact(Some(w), Some(y), below(
wh_full(Outer(true, Style::default().fg(o))),
wh_full(below(
below(
fg_bg(o, b, wh_full("")),
wh_full(origin_nw(fg_bg(f, b, bold(true, name)))),
),
wh_full(when(is_selected, editor.map(|e|e.view())))))))
});
w_exact(track.width as u16, h_full(scenes))
});
return size.of(wh_full(above(status, tracks)));
fn scene_name_theme (scene: &Scene, track_index: usize) -> (Arc<str>, ItemTheme) {
if let Some(Some(clip)) = &scene.clips.get(track_index) {
let clip = clip.read().unwrap();
(format!("{}", &clip.name).into(), clip.color)
} else {
(" ⏹ -- ".into(), ItemTheme::G[32])
}
}
fn scene_bg (
theme: ItemTheme, select: &Selection, track_index: usize, scene_index: usize
) -> (Color, Color) {
let mut outline = theme.base.term;
(if select.track() == Some(track_index) && select.scene() == Some(scene_index) {
outline = theme.lighter.term;
theme.light.term
} else if select.track() == Some(track_index) || select.scene() == Some(scene_index) {
outline = theme.darkest.term;
theme.base.term
} else {
theme.dark.term
}, outline)
}
fn scene_w (
track: &Track, select: &Selection, track_index: usize, editor: Option<&MidiEditor>
) -> u16 {
if select.track() == Some(track_index) && let Some(editor) = editor {
(editor.size.w() as usize).max(24).max(track.width) as u16
} else {
track.width as u16
}
}
fn scene_y (
select: &Selection, scene_index: usize, editor: Option<&MidiEditor>
) -> u16 {
if select.scene() == Some(scene_index) && let Some(editor) = editor {
editor.size.h().max(12)
} else {
H_SCENE as u16
}
}
fn scene_sel (select: &Selection, track_index: usize, scene_index: usize, editing: bool) -> bool {
editing && select.track() == Some(track_index) && select.scene() == Some(scene_index)
}
}
pub fn view_track_names (
theme: ItemTheme,
tracks: impl TracksSizes<'_>,
track_count: usize,
scene_count: usize,
selected: &Selection,
) -> impl Draw<Tui> {
let button = south(
button_3("t", "rack ", format!("{}{track_count}", selected.track()
.map(|track|format!("{track}/")).unwrap_or_default()), false),
button_3("s", "cene ", format!("{}{scene_count}", selected.scene()
.map(|scene|format!("{scene}/")).unwrap_or_default()), false));
let button_2 = south(
button_2("T", "+", false),
button_2("S", "+", false));
view_track_row_section(theme, button, button_2, bg(theme.darker.term,
h_exact(2, thunk(|to: &mut Tui|{
for (index, track, x1, _x2) in tracks {
x_push(x1 as u16, w_exact(track_width(index, track),
bg(if selected.track() == Some(index) {
track.color.light.term
} else {
track.color.base.term
}, south(w_full(origin_nw(east(
format!("·t{index:02} "),
fg(Rgb(255, 255, 255), bold(true, &track.name))
))), ""))) ).draw(to);
}
Ok(XYWH(0, 0, 0, 0))
}))))
}
pub fn view_track_outputs (
theme: ItemTheme, tracks: impl TracksSizes<'_>, midi_outs: impl Iterator<Item = &MidiOutput>,
) -> impl Draw<Tui> {
view_track_row_section(theme,
south(w_full(origin_w(button_2("o", "utput", false))),
thunk(|to: &mut Tui|{
for port in midi_outs {
w_full(origin_w(port.port_name())).draw(to);
}
Ok(XYWH(0, 0, 0, 0))
})),
button_2("O", "+", false),
bg(theme.darker.term, origin_w(thunk(|to: &mut Tui|{
for (index, track, _x1, _x2) in tracks {
let iter = ||track.sequencer.midi_outs.iter();
let draw = |port: &MidiOutput, _|fg(Rgb(255, 255, 255),
h_exact(1, bg(track.color.dark.term, w_full(origin_w(
format!("·o{index:02} {}", port.port_name()))))));
w_exact(track_width(index, track),
origin_nw(h_full(iter_south(iter, draw)))).draw(to);
}
Ok(XYWH(0, 0, 0, 0))
}))))
}
pub fn view_track_inputs (
theme: ItemTheme, tracks: impl TracksSizes<'_>, height: u16,
) -> impl Draw<Tui> {
view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false),
bg(theme.darker.term, origin_w(thunk(move|to: &mut Tui|{
for (index, track, _x1, _x2) in tracks {
wh_exact(Some(track_width(index, track)), Some(height + 1),
origin_nw(south(
bg(track.color.base.term,
w_full(origin_w(east!(
either(track.sequencer.monitoring, fg(Green, "●mon "), "·mon "),
either(track.sequencer.recording, fg(Red, "●rec "), "·rec "),
either(track.sequencer.overdub, fg(Yellow, "●dub "), "·dub "),
)))),
iter_south(||track.sequencer.midi_ins.iter(),
|port, _|fg_bg(Rgb(255, 255, 255), track.color.dark.term,
w_full(origin_w(format!("·i{index:02} {}", port.port_name()))))))))
.draw(to);
}
Ok(XYWH(0, 0, 0, 0))
}))))
}
pub fn view_scenes_names (
scenes: impl ScenesSizes<'_>,
select: &Selection,
editor: Option<&MidiEditor>,
editing: bool,
) -> impl Draw<Tui> {
w_exact(20, thunk(move |to: &mut Tui|{
for (index, scene, ..) in scenes {
view_scene_name(select, editor, index, scene, editing).draw(to);
}
Ok(XYWH(1, 1, 1, 1))
}))
}
pub fn view_scene_name (
select: &Selection,
editor: Option<&MidiEditor>,
index: usize,
scene: &Scene,
editing: bool
) -> impl Draw<Tui> {
let h = if select.scene() == Some(index) && let Some(_editor) = editor {
7
} else {
H_SCENE as u16
};
let a = w_full(origin_w(east(format!("·s{index:02} "),
fg(g(255), bold(true, &scene.name)))));
let b = when(select.scene() == Some(index) && editing,
wh_full(origin_nw(south(
editor.as_ref().map(|e|e.clip_status()),
editor.as_ref().map(|e|e.edit_status())))));
let c = if select.scene() == Some(index) {
scene.color.light.term
} else {
scene.color.base.term
};
wh_exact(Some(20), Some(h), bg(c, origin_nw(south(a, b))))
}
pub fn view_midi_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
track.map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins))
}
pub fn view_midi_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
track.map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs))
}
pub fn view_audio_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
track.map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins()))
}
pub fn view_audio_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
track.map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs()))
}
pub fn view_track_per <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
tracks: impl Fn() -> U + Send + Sync + 'a,
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
) -> impl Draw<Tui> {
iter_east(tracks, move|(index, track, x1, x2): (usize, &Track, usize, usize), _|{
w_exact((x2 - x1) as u16, fg_bg(
track.color.lightest.term,
track.color.base.term,
callback(index, track)))
})
}
pub fn view_per_track () -> impl Draw<Tui> {}
pub fn view_per_track_top () -> impl Draw<Tui> {}
pub fn view_inputs (tracks: impl TracksSizes<'_>, midi_ins: &[MidiInput]) -> impl Draw<Tui> {
let title_1 = wh_exact(Some(20), Some(1), origin_w(button_3("i", "nput ", format!("{}", midi_ins.len()), false)));
let title_2 = wh_exact(Some(4), Some(1), w_exact(4, button_2("I", "+", false)));
east(title_1, west(title_2, thunk(move|to: &mut Tui|{
for (_index, track, x1, _x2) in tracks {
south(
x_push(x1 as u16, bg(track.color.dark.term, origin_w(w_exact(track.width as u16, east!(
either(track.sequencer.monitoring, fg(Green, "mon "), "mon "),
either(track.sequencer.recording, fg(Red, "rec "), "rec "),
either(track.sequencer.overdub, fg(Yellow, "dub "), "dub "),
))))),
thunk(move |to: &mut Tui|{
for (index, port) in midi_ins.iter().enumerate() {
x_push(index as u16 * 10, h_exact(1, east(w_exact(20, origin_w(east("", bold(true, fg(Rgb(255,255,255), port.port_name()))))),
west(w_exact(4, ()), thunk(move|to: &mut Tui|{
bg(track.color.darker.term, origin_w(w_exact(track.width as u16, east!(
either(track.sequencer.monitoring, fg(Green, ""), " · "),
either(track.sequencer.recording, fg(Red, ""), " · "),
either(track.sequencer.overdub, fg(Yellow, ""), " · "),
)))).draw(to);
todo!()
}))))).draw(to);
}
todo!()
})
).draw(to);
}
todo!()
})))
}
pub fn view_outputs (
theme: ItemTheme,
tracks: impl TracksSizes<'_>,
midi_outs: &[MidiOutput],
height: u16,
) -> impl Draw<Tui> {
let list = south(
h_exact(1, w_full(origin_w(button_3(
"o", "utput", format!("{}", midi_outs.len()), false
)))),
h_exact(height - 1, wh_full(origin_nw(thunk(|to: &mut Tui|{
for (_index, port) in midi_outs.iter().enumerate() {
h_exact(1,w_full(east(
origin_w(east("", fg(Rgb(255,255,255), bold(true, port.port_name())))),
w_full(origin_e(format!("{}/{} ",
port.port().get_connections().len(),
port.connections.len())))))).draw(to);
for (index, conn) in port.connections.iter().enumerate() {
h_exact(1, w_full(origin_w(format!(" c{index:02}{}", conn.info()))))
.draw(to);
}
}
todo!();
}))))
);
h_exact(height, view_track_row_section(theme, list, button_2("O", "+", false),
bg(theme.darker.term, origin_w(w_full(
thunk(|to: &mut Tui|{
for (index, track, _x1, _x2) in tracks {
w_exact(track_width(index, track),
thunk(|to: &mut Tui|{
h_exact(1, origin_w(east(
either(true, fg(Green, "play "), "play "),
either(false, fg(Yellow, "solo "), "solo "),
))).draw(to);
for (_index, port) in midi_outs.iter().enumerate() {
h_exact(1, origin_w(east(
either(true, fg(Green, ""), " · "),
either(false, fg(Yellow, ""), " · "),
))).draw(to);
for (_index, _conn) in port.connections.iter().enumerate() {
h_exact(1, w_full("")).draw(to);
}
}
todo!()
})
).draw(to);
}
todo!()
})
)))
))
}
pub fn view_track_devices (
theme: ItemTheme,
tracks: impl TracksSizes<'_>,
track: Option<&Track>,
h: u16,
) -> impl Draw<Tui> {
view_track_row_section(theme,
button_3("d", "evice", format!("{}", track.map(|t|t.devices.len()).unwrap_or(0)), false),
button_2("D", "+", false),
iter_once(tracks, move|(_, track, _x1, _x2), index| wh_exact(
Some(track_width(index, track)),
Some(h + 1),
bg(track.color.dark.term, origin_nw(iter_south(move||0..h,
|_, _index|wh_exact(Some(track.width as u16), Some(2),
fg_bg(
ItemTheme::G[32].lightest.term,
ItemTheme::G[32].dark.term,
origin_nw(format!(" · {}", "--"))
)
)))))))
}
pub fn per_track <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
tracks: impl Fn() -> U + Send + Sync + 'a,
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
) -> impl Draw<Tui> + 'a {
per_track_top(tracks, move|index, track|h_full(origin_y(callback(index, track))))
}
pub fn per_track_top <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
tracks: impl Fn() -> U + Send + Sync + 'a,
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
) -> impl Draw<Tui> + 'a {
origin_x(bg(Reset, iter_east(tracks,
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{
w_exact((x2 - x1) as u16, fg_bg(
track.color.lightest.term,
track.color.base.term,
callback(index, track))) })))
}
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 view_sessions () -> impl Draw<Tui> {
let h = 6;
let w = Some(30);
let f = Rgb(224, 192, 128);
h_exact(h, w_min(w, thunk(move |to: &mut Tui|{
for (index, name) in ["session1", "session2", "session3"].iter().enumerate() {
let b = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) };
let y = (2 * index) as u16;
let h = 2;
y_push(y, &h_exact(h, w_full(bg(b, origin_w(fg(f, name)))))).draw(to);
}
Ok(to.area().into())
})))
}
pub fn view_browse_title (state: &App) -> impl Draw<Tui> {
w_full(origin_w(field_v(ItemTheme::default(),
match state.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:",
}, h_exact(1, fg(g(96), x_repeat("🭻")))
)))
}
pub fn view_device (state: &App) -> impl Draw<Tui> {
let selected = state.dialog.device_kind().unwrap();
south(bold(true, "Add device"), iter_south(
move||device_kinds().iter(),
move|_label: &&'static str, i|{
let b = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) };
let l = if i == selected { "[ " } else { " " };
let r = if i == selected { " ]" } else { " " };
w_full(bg(b, east(l, west(r, "FIXME device name")))) }))
}

View file

@ -16,10 +16,6 @@ def_sizes_iter!(PortsSizes => Arc<str>, [Connect]);
def_sizes_iter!(ScenesSizes => Scene); def_sizes_iter!(ScenesSizes => Scene);
def_sizes_iter!(TracksSizes => Track); def_sizes_iter!(TracksSizes => Track);
pub(crate) fn track_width (_index: usize, track: &Track) -> u16 {
track.width as u16
}
pub trait HasWidth { pub trait HasWidth {
const MIN_WIDTH: usize; const MIN_WIDTH: usize;
/// Increment track width. /// Increment track width.
@ -27,31 +23,3 @@ pub trait HasWidth {
/// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH]. /// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH].
fn width_dec (&mut self); fn width_dec (&mut self);
} }
impl HasClipsSize for App {
fn clips_size (&self) -> &Size { &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 ScenesView for App {
fn w_mid (&self) -> u16 {
(self.size.w() as u16).saturating_sub(self.w_side())
}
fn w_side (&self) -> u16 {
20
}
fn h_scenes (&self) -> u16 {
(self.size.h() as u16).saturating_sub(20)
}
}

View file

@ -1,614 +0,0 @@
use crate::*;
tui_view!(|self: App| {
""
});
/// Collection of custom view definitions.
pub type Views = Arc<RwLock<BTreeMap<Arc<str>, Arc<str>>>>;
/// Load custom view definition.
pub(crate) fn load_view (views: &Views, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
views.write().unwrap().insert(name.as_ref().into(), body.src()?.unwrap_or_default().into());
Ok(())
}
/// ```
/// let _ = tek::view_logo();
/// ```
pub fn view_logo () -> impl Draw<Tui> {
wh_exact(Some(32), Some(7), bold(true, fg(Rgb(240, 200, 180), south!{
h_exact(1, ""),
h_exact(1, ""),
h_exact(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"),
h_exact(1, east("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", east(fg(Rgb(230,100,40), "v0.3.0"), " ~~"))),
h_exact(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"),
})))
}
/// ```
/// let x = "";
/// let _ = tek::view_transport(true, x.as_ref(), x.as_ref(), x.as_ref());
/// let _ = tek::view_transport(false, x.as_ref(), x.as_ref(), x.as_ref());
/// ```
pub fn view_transport (play: bool, bpm: &str, beat: &str, time: &str) -> impl Draw<Tui> {
let theme = ItemTheme::G[96];
bg(Black, east!(above(
wh_full(origin_w(button_play_pause(play, false))),
wh_full(origin_e(east!(
field_h(theme, "BPM", bpm),
field_h(theme, "Beat", beat),
field_h(theme, "Time", time),
)))
)))
}
/// ```
/// let x = "";
/// let _ = tek::view_status(None, x.as_ref(), x.as_ref(), x.as_ref());
/// let _ = tek::view_status(Some("".into()), x.as_ref(), x.as_ref(), x.as_ref());
/// ```
pub fn view_status (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl Draw<Tui> {
let theme = ItemTheme::G[96];
let sr = field_h(theme, "SR", sr);
let buf = field_h(theme, "Buf", buf);
let lat = field_h(theme, "Lat", lat);
bg(Black, east!(above(
wh_full(origin_w(sel.map(|sel|field_h(theme, "Selected", sel)))),
wh_full(origin_e(east!(sr, buf, lat))),
)))
}
/// ```
/// let _ = tek::button_play_pause(true, true);
/// let _ = tek::button_play_pause(true, false);
/// let _ = tek::button_play_pause(false, true);
/// let _ = tek::button_play_pause(false, false);
/// ```
pub fn button_play_pause (playing: bool, compact: bool) -> impl Draw<Tui> {
bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) },
either(compact,
thunk(move|to: &mut Tui|w_exact(9, either(playing,
fg(Rgb(0, 255, 0), " PLAYING "),
fg(Rgb(255, 128, 0), " STOPPED "))
).draw(to)),
thunk(move|to: &mut Tui|w_exact(5, either(playing,
fg(Rgb(0, 255, 0), south(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)),
fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",)))
).draw(to)),
)
)
}
#[cfg(feature = "track")] pub fn view_track_row_section (
_theme: ItemTheme,
button: impl Draw<Tui>,
button_add: impl Draw<Tui>,
content: impl Draw<Tui>,
) -> impl Draw<Tui> {
west(h_full(w_exact(4, origin_nw(button_add))),
east(w_exact(20, h_full(origin_nw(button))), wh_full(origin_c(content))))
}
/// ```
/// let bg = tengri::ratatui::style::Color::Red;
/// let fg = tengri::ratatui::style::Color::Green;
/// let _ = tek::view_wrap(bg, fg, "and then blue, too!");
/// ```
pub fn view_wrap (bg: Color, fg: Color, content: impl Draw<Tui>) -> impl Draw<Tui> {
let left = fg_bg(bg, Reset, w_exact(1, y_repeat("")));
let right = fg_bg(bg, Reset, w_exact(1, y_repeat("")));
east(left, west(right, fg_bg(fg, bg, content)))
}
/// ```
/// let _ = tek::view_meter("", 0.0);
/// let _ = tek::view_meters(&[0.0, 0.0]);
/// ```
pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Draw<Tui> + 'a {
let f = field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value));
let w = if value >= 0.0 { 13 }
else if value >= -1.0 { 12 }
else if value >= -2.0 { 11 }
else if value >= -3.0 { 10 }
else if value >= -4.0 { 9 }
else if value >= -6.0 { 8 }
else if value >= -9.0 { 7 }
else if value >= -12.0 { 6 }
else if value >= -15.0 { 5 }
else if value >= -20.0 { 4 }
else if value >= -25.0 { 3 }
else if value >= -30.0 { 2 }
else if value >= -40.0 { 1 }
else { 0 };
let c = if value >= 0.0 { Red }
else if value >= -3.0 { Yellow }
else { Green };
south!(f, wh_exact(Some(w), Some(1), bg(c, ())))
}
pub fn view_meters (values: &[f32;2]) -> impl Draw<Tui> + use<'_> {
let left = format!("L/{:>+9.3}", values[0]);
let right = format!("R/{:>+9.3}", values[1]);
south(left, right)
}
pub fn view_sample_info (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
when(sample.is_some(), thunk(move|to: &mut Tui|{
let sample = sample.unwrap().read().unwrap();
let theme = sample.color;
east!(
field_h(theme, "Name", format!("{:<10}", sample.name.clone())),
field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())),
field_h(theme, "Start", format!("{:<8}", sample.start)),
field_h(theme, "End", format!("{:<8}", sample.end)),
field_h(theme, "Trans", "0"),
field_h(theme, "Gain", format!("{}", sample.gain)),
).draw(to)
}))
}
pub fn view_sample_info_v (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> + use<'_> {
let a = thunk(move|to: &mut Tui|{
let sample = sample.unwrap().read().unwrap();
let theme = sample.color;
w_exact(20, south!(
w_full(origin_w(field_h(theme, "Name ", format!("{:<10}", sample.name.clone())))),
w_full(origin_w(field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())))),
w_full(origin_w(field_h(theme, "Start ", format!("{:<8}", sample.start)))),
w_full(origin_w(field_h(theme, "End ", format!("{:<8}", sample.end)))),
w_full(origin_w(field_h(theme, "Trans ", "0"))),
w_full(origin_w(field_h(theme, "Gain ", format!("{}", sample.gain)))),
)).draw(to)
});
let b = thunk(|to: &mut Tui|fg(Red, south!(
bold(true, "× No sample."),
"[r] record",
"[Shift-F9] import",
)).draw(to));
either(sample.is_some(), a, b)
}
pub fn view_sample_status (sample: Option<&Arc<RwLock<Sample>>>) -> impl Draw<Tui> {
bold(true, fg(g(224), sample
.map(|sample|{
let sample = sample.read().unwrap();
format!("Sample {}-{}", sample.start, sample.end)
})
.unwrap_or_else(||"No sample".to_string())))
}
pub fn view_track_header (theme: ItemTheme, content: impl Draw<Tui>) -> impl Draw<Tui> {
w_exact(12, bg(theme.darker.term, w_full(origin_e(content))))
}
pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T])
-> impl Draw<Tui> + use<'a, T>
{
let ins = ports.len() as u16;
let frame = Outer(true, Style::default().fg(g(96)));
let iter = move||ports.iter();
let names = iter_south(iter, move|port, index|h_full(origin_w(format!(" {index} {}", port.port_name()))));
let field = field_v(theme, title, names);
wh_exact(Some(20), Some(1 + ins), border(true, frame, wh_exact(Some(20), Some(1 + ins), field)))
}
pub fn view_io_ports <'a, T: PortsSizes<'a>> (
fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a
) -> impl Draw<Tui> + 'a {
type Item<'a> = (usize, &'a Arc<str>, &'a [Connect], usize, usize);
iter(items, move|(_index, name, connections, y, y2): Item<'a>, _| {
y_push(y as u16, h_exact((y2-y) as u16,
south(h_full(bold(true, fg_bg(fg, bg, origin_w(east(" 󰣲 ", name))))),
iter(||connections.iter(), move|connect: &'a Connect, index|y_push(
index as u16, h_exact(1, origin_w(bold(false, fg_bg(fg, bg, &connect.info))))
)))))
})
}
pub fn view_scenes_clips <'a, S: ScenesSizes<'a>> (
scenes: impl Fn()->S,
tracks: impl TracksSizes<'a>,
select: &Selection,
editor: Option<&MidiEditor>,
size: &Size,
editing: bool,
) -> impl Draw<Tui> {
let status = wh_full(origin_se(fg(Green, format!("{}x{}", size.w(), size.h()))));
let tracks = iter_once(tracks, move|(track_index, track, _, _), _| {
let scenes = iter_once(scenes(), move|(scene_index, scene, _, _), _| {
let (name, theme): (Arc<str>, ItemTheme) = scene_name_theme(scene, track_index);
let f = theme.lightest.term;
let (b, o) = scene_bg(theme, select, track_index, scene_index);
let w = scene_w(track, select, track_index, editor);
let y = scene_y(select, scene_index, editor);
let is_selected = scene_sel(select, track_index, scene_index, editing);
wh_exact(Some(w), Some(y), below(
wh_full(Outer(true, Style::default().fg(o))),
wh_full(below(
below(
fg_bg(o, b, wh_full("")),
wh_full(origin_nw(fg_bg(f, b, bold(true, name)))),
),
wh_full(when(is_selected, editor.map(|e|e.view())))))))
});
w_exact(track.width as u16, h_full(scenes))
});
return size.of(wh_full(above(status, tracks)));
fn scene_name_theme (scene: &Scene, track_index: usize) -> (Arc<str>, ItemTheme) {
if let Some(Some(clip)) = &scene.clips.get(track_index) {
let clip = clip.read().unwrap();
(format!("{}", &clip.name).into(), clip.color)
} else {
(" ⏹ -- ".into(), ItemTheme::G[32])
}
}
fn scene_bg (
theme: ItemTheme, select: &Selection, track_index: usize, scene_index: usize
) -> (Color, Color) {
let mut outline = theme.base.term;
(if select.track() == Some(track_index) && select.scene() == Some(scene_index) {
outline = theme.lighter.term;
theme.light.term
} else if select.track() == Some(track_index) || select.scene() == Some(scene_index) {
outline = theme.darkest.term;
theme.base.term
} else {
theme.dark.term
}, outline)
}
fn scene_w (
track: &Track, select: &Selection, track_index: usize, editor: Option<&MidiEditor>
) -> u16 {
if select.track() == Some(track_index) && let Some(editor) = editor {
(editor.size.w() as usize).max(24).max(track.width) as u16
} else {
track.width as u16
}
}
fn scene_y (
select: &Selection, scene_index: usize, editor: Option<&MidiEditor>
) -> u16 {
if select.scene() == Some(scene_index) && let Some(editor) = editor {
editor.size.h().max(12)
} else {
H_SCENE as u16
}
}
fn scene_sel (select: &Selection, track_index: usize, scene_index: usize, editing: bool) -> bool {
editing && select.track() == Some(track_index) && select.scene() == Some(scene_index)
}
}
pub fn view_track_names (
theme: ItemTheme,
tracks: impl TracksSizes<'_>,
track_count: usize,
scene_count: usize,
selected: &Selection,
) -> impl Draw<Tui> {
let button = south(
button_3("t", "rack ", format!("{}{track_count}", selected.track()
.map(|track|format!("{track}/")).unwrap_or_default()), false),
button_3("s", "cene ", format!("{}{scene_count}", selected.scene()
.map(|scene|format!("{scene}/")).unwrap_or_default()), false));
let button_2 = south(
button_2("T", "+", false),
button_2("S", "+", false));
view_track_row_section(theme, button, button_2, bg(theme.darker.term,
h_exact(2, thunk(|to: &mut Tui|{
for (index, track, x1, _x2) in tracks {
x_push(x1 as u16, w_exact(track_width(index, track),
bg(if selected.track() == Some(index) {
track.color.light.term
} else {
track.color.base.term
}, south(w_full(origin_nw(east(
format!("·t{index:02} "),
fg(Rgb(255, 255, 255), bold(true, &track.name))
))), ""))) ).draw(to);
}
Ok(XYWH(0, 0, 0, 0))
}))))
}
pub fn view_track_outputs (
theme: ItemTheme, tracks: impl TracksSizes<'_>, midi_outs: impl Iterator<Item = &MidiOutput>,
) -> impl Draw<Tui> {
view_track_row_section(theme,
south(w_full(origin_w(button_2("o", "utput", false))),
thunk(|to: &mut Tui|{
for port in midi_outs {
w_full(origin_w(port.port_name())).draw(to);
}
Ok(XYWH(0, 0, 0, 0))
})),
button_2("O", "+", false),
bg(theme.darker.term, origin_w(thunk(|to: &mut Tui|{
for (index, track, _x1, _x2) in tracks {
let iter = ||track.sequencer.midi_outs.iter();
let draw = |port: &MidiOutput, _|fg(Rgb(255, 255, 255),
h_exact(1, bg(track.color.dark.term, w_full(origin_w(
format!("·o{index:02} {}", port.port_name()))))));
w_exact(track_width(index, track),
origin_nw(h_full(iter_south(iter, draw)))).draw(to);
}
Ok(XYWH(0, 0, 0, 0))
}))))
}
pub fn view_track_inputs (
theme: ItemTheme, tracks: impl TracksSizes<'_>, height: u16,
) -> impl Draw<Tui> {
view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false),
bg(theme.darker.term, origin_w(thunk(move|to: &mut Tui|{
for (index, track, _x1, _x2) in tracks {
wh_exact(Some(track_width(index, track)), Some(height + 1),
origin_nw(south(
bg(track.color.base.term,
w_full(origin_w(east!(
either(track.sequencer.monitoring, fg(Green, "●mon "), "·mon "),
either(track.sequencer.recording, fg(Red, "●rec "), "·rec "),
either(track.sequencer.overdub, fg(Yellow, "●dub "), "·dub "),
)))),
iter_south(||track.sequencer.midi_ins.iter(),
|port, _|fg_bg(Rgb(255, 255, 255), track.color.dark.term,
w_full(origin_w(format!("·i{index:02} {}", port.port_name()))))))))
.draw(to);
}
Ok(XYWH(0, 0, 0, 0))
}))))
}
pub fn view_scenes_names (
scenes: impl ScenesSizes<'_>,
select: &Selection,
editor: Option<&MidiEditor>,
editing: bool,
) -> impl Draw<Tui> {
w_exact(20, thunk(move |to: &mut Tui|{
for (index, scene, ..) in scenes {
view_scene_name(select, editor, index, scene, editing).draw(to);
}
Ok(XYWH(1, 1, 1, 1))
}))
}
pub fn view_scene_name (
select: &Selection,
editor: Option<&MidiEditor>,
index: usize,
scene: &Scene,
editing: bool
) -> impl Draw<Tui> {
let h = if select.scene() == Some(index) && let Some(_editor) = editor {
7
} else {
H_SCENE as u16
};
let a = w_full(origin_w(east(format!("·s{index:02} "),
fg(g(255), bold(true, &scene.name)))));
let b = when(select.scene() == Some(index) && editing,
wh_full(origin_nw(south(
editor.as_ref().map(|e|e.clip_status()),
editor.as_ref().map(|e|e.edit_status())))));
let c = if select.scene() == Some(index) {
scene.color.light.term
} else {
scene.color.base.term
};
wh_exact(Some(20), Some(h), bg(c, origin_nw(south(a, b))))
}
pub fn view_midi_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
track.map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins))
}
pub fn view_midi_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
track.map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs))
}
pub fn view_audio_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
track.map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins()))
}
pub fn view_audio_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw<Tui> {
track.map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs()))
}
pub fn view_track_per <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
tracks: impl Fn() -> U + Send + Sync + 'a,
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
) -> impl Draw<Tui> {
iter_east(tracks, move|(index, track, x1, x2): (usize, &Track, usize, usize), _|{
w_exact((x2 - x1) as u16, fg_bg(
track.color.lightest.term,
track.color.base.term,
callback(index, track)))
})
}
pub fn view_per_track () -> impl Draw<Tui> {}
pub fn view_per_track_top () -> impl Draw<Tui> {}
pub fn view_inputs (tracks: impl TracksSizes<'_>, midi_ins: &[MidiInput]) -> impl Draw<Tui> {
let title_1 = wh_exact(Some(20), Some(1), origin_w(button_3("i", "nput ", format!("{}", midi_ins.len()), false)));
let title_2 = wh_exact(Some(4), Some(1), w_exact(4, button_2("I", "+", false)));
east(title_1, west(title_2, thunk(move|to: &mut Tui|{
for (_index, track, x1, _x2) in tracks {
south(
x_push(x1 as u16, bg(track.color.dark.term, origin_w(w_exact(track.width as u16, east!(
either(track.sequencer.monitoring, fg(Green, "mon "), "mon "),
either(track.sequencer.recording, fg(Red, "rec "), "rec "),
either(track.sequencer.overdub, fg(Yellow, "dub "), "dub "),
))))),
thunk(move |to: &mut Tui|{
for (index, port) in midi_ins.iter().enumerate() {
x_push(index as u16 * 10, h_exact(1, east(w_exact(20, origin_w(east("", bold(true, fg(Rgb(255,255,255), port.port_name()))))),
west(w_exact(4, ()), thunk(move|to: &mut Tui|{
bg(track.color.darker.term, origin_w(w_exact(track.width as u16, east!(
either(track.sequencer.monitoring, fg(Green, ""), " · "),
either(track.sequencer.recording, fg(Red, ""), " · "),
either(track.sequencer.overdub, fg(Yellow, ""), " · "),
)))).draw(to);
todo!()
}))))).draw(to);
}
todo!()
})
).draw(to);
}
todo!()
})))
}
pub fn view_outputs (
theme: ItemTheme,
tracks: impl TracksSizes<'_>,
midi_outs: &[MidiOutput],
height: u16,
) -> impl Draw<Tui> {
let list = south(
h_exact(1, w_full(origin_w(button_3(
"o", "utput", format!("{}", midi_outs.len()), false
)))),
h_exact(height - 1, wh_full(origin_nw(thunk(|to: &mut Tui|{
for (_index, port) in midi_outs.iter().enumerate() {
h_exact(1,w_full(east(
origin_w(east("", fg(Rgb(255,255,255), bold(true, port.port_name())))),
w_full(origin_e(format!("{}/{} ",
port.port().get_connections().len(),
port.connections.len())))))).draw(to);
for (index, conn) in port.connections.iter().enumerate() {
h_exact(1, w_full(origin_w(format!(" c{index:02}{}", conn.info()))))
.draw(to);
}
}
todo!();
}))))
);
h_exact(height, view_track_row_section(theme, list, button_2("O", "+", false),
bg(theme.darker.term, origin_w(w_full(
thunk(|to: &mut Tui|{
for (index, track, _x1, _x2) in tracks {
w_exact(track_width(index, track),
thunk(|to: &mut Tui|{
h_exact(1, origin_w(east(
either(true, fg(Green, "play "), "play "),
either(false, fg(Yellow, "solo "), "solo "),
))).draw(to);
for (_index, port) in midi_outs.iter().enumerate() {
h_exact(1, origin_w(east(
either(true, fg(Green, ""), " · "),
either(false, fg(Yellow, ""), " · "),
))).draw(to);
for (_index, _conn) in port.connections.iter().enumerate() {
h_exact(1, w_full("")).draw(to);
}
}
todo!()
})
).draw(to);
}
todo!()
})
)))
))
}
pub fn view_track_devices (
theme: ItemTheme,
tracks: impl TracksSizes<'_>,
track: Option<&Track>,
h: u16,
) -> impl Draw<Tui> {
view_track_row_section(theme,
button_3("d", "evice", format!("{}", track.map(|t|t.devices.len()).unwrap_or(0)), false),
button_2("D", "+", false),
iter_once(tracks, move|(_, track, _x1, _x2), index| wh_exact(
Some(track_width(index, track)),
Some(h + 1),
bg(track.color.dark.term, origin_nw(iter_south(move||0..h,
|_, _index|wh_exact(Some(track.width as u16), Some(2),
fg_bg(
ItemTheme::G[32].lightest.term,
ItemTheme::G[32].dark.term,
origin_nw(format!(" · {}", "--"))
)
)))))))
}
pub fn per_track <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
tracks: impl Fn() -> U + Send + Sync + 'a,
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
) -> impl Draw<Tui> + 'a {
per_track_top(tracks, move|index, track|h_full(origin_y(callback(index, track))))
}
pub fn per_track_top <'a, T: Draw<Tui> + 'a, U: TracksSizes<'a>> (
tracks: impl Fn() -> U + Send + Sync + 'a,
callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
) -> impl Draw<Tui> + 'a {
origin_x(bg(Reset, iter_east(tracks,
move|(index, track, x1, x2): (usize, &'a Track, usize, usize), _|{
w_exact((x2 - x1) as u16, fg_bg(
track.color.lightest.term,
track.color.base.term,
callback(index, track))) })))
}
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 view_sessions () -> impl Draw<Tui> {
let h = 6;
let w = Some(30);
let f = Rgb(224, 192, 128);
h_exact(h, w_min(w, thunk(move |to: &mut Tui|{
for (index, name) in ["session1", "session2", "session3"].iter().enumerate() {
let b = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) };
let y = (2 * index) as u16;
let h = 2;
y_push(y, &h_exact(h, w_full(bg(b, origin_w(fg(f, name)))))).draw(to);
}
Ok(to.area().into())
})))
}
pub fn view_browse_title (state: &App) -> impl Draw<Tui> {
w_full(origin_w(field_v(ItemTheme::default(),
match state.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:",
}, h_exact(1, fg(g(96), x_repeat("🭻")))
)))
}
pub fn view_device (state: &App) -> impl Draw<Tui> {
let selected = state.dialog.device_kind().unwrap();
south(bold(true, "Add device"), iter_south(
move||device_kinds().iter(),
move|_label: &&'static str, i|{
let b = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) };
let l = if i == selected { "[ " } else { " " };
let r = if i == selected { " ]" } else { " " };
w_full(bg(b, east(l, west(r, "FIXME device name")))) }))
}

33
src/deps.rs Normal file
View file

@ -0,0 +1,33 @@
#[cfg(feature = "cli")]
pub(crate) use ::clap::{self, Parser, Subcommand};
pub(crate) use ::xdg::BaseDirectories;
pub(crate) use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*};
pub(crate) use tengri::{
*, lang::*, exit::*, eval::*, sing::*, time::*, draw::*, term::*, color::*, task::*,
crossterm::event::{Event, KeyEvent},
ratatui::{
self,
prelude::{Rect, Style, Stylize, Buffer, Color::{self, *}},
widgets::{Widget, canvas::{Canvas, Line}},
},
};
#[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}},
time::Duration,
thread::{spawn, JoinHandle},
},
};

View file

@ -26,7 +26,7 @@ use crate::*;
pub trait ClipsView: TracksView + ScenesView { pub trait ClipsView: TracksView + ScenesView {
/// Draw clips per scene /// Draw clips per scene
fn view_scenes_clips <'a> (&'a self) -> impl Draw<Tui> + 'a { fn view_scenes_clips <'a> (&'a self) -> impl Draw<Tui> + 'a {
crate::view::view_scenes_clips( view_scenes_clips(
||self.scenes_with_sizes(), ||self.scenes_with_sizes(),
self.tracks_with_sizes(), self.tracks_with_sizes(),
self.selection(), self.selection(),
@ -87,3 +87,7 @@ impl Arrangement {
} }
pub trait HasClipsSize { fn clips_size (&self) -> &Size; } pub trait HasClipsSize { fn clips_size (&self) -> &Size; }
impl HasClipsSize for App {
fn clips_size (&self) -> &Size { &self.project.size_inner }
}

View file

@ -62,7 +62,7 @@ pub trait ScenesView: HasEditor + HasSelection + HasSceneScroll + HasClipsSize +
fn w_mid (&self) -> u16; fn w_mid (&self) -> u16;
fn view_scenes_names (&self) -> impl Draw<Tui> { fn view_scenes_names (&self) -> impl Draw<Tui> {
crate::view::view_scenes_names( view_scenes_names(
self.scenes_with_sizes(), self.selection(), self.editor(), self.is_editing() self.scenes_with_sizes(), self.selection(), self.editor(), self.is_editing()
) )
} }
@ -130,16 +130,14 @@ impl HasSceneScroll for Arrangement {
fn scene_scroll (&self) -> usize { self.scene_scroll } fn scene_scroll (&self) -> usize { self.scene_scroll }
} }
impl HasSceneScroll for App {
fn scene_scroll (&self) -> usize { self.project.scene_scroll() }
}
impl ScenesView for Arrangement { impl ScenesView for Arrangement {
fn h_scenes (&self) -> u16 { fn h_scenes (&self) -> u16 { (self.size.h() as u16).saturating_sub(20) }
(self.size.h() as u16).saturating_sub(20) fn w_side (&self) -> u16 { (self.size.w() as u16 * 2 / 10).max(20) }
} fn w_mid (&self) -> u16 { (self.size.w() as u16).saturating_sub(2 * self.w_side()).max(40) }
fn w_side (&self) -> u16 {
(self.size.w() as u16 * 2 / 10).max(20)
}
fn w_mid (&self) -> u16 {
(self.size.w() as u16).saturating_sub(2 * self.w_side()).max(40)
}
} }
pub type SceneWith<'a, T> = pub type SceneWith<'a, T> =
@ -158,7 +156,19 @@ def_command!(SceneCommand: |scene: Scene| {
#[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt()); #[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt());
#[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: Arrangement| self.selected_scene()); #[cfg(all(feature = "select"))] impl_as_ref_opt!(Scene: |self: Arrangement| self.selected_scene());
#[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: Arrangement| self.selected_scene_mut()); #[cfg(all(feature = "select"))] impl_as_mut_opt!(Scene: |self: Arrangement| self.selected_scene_mut());
impl<T: TracksView+ScenesView+Send+Sync> ClipsView for T {}
impl_has!(Vec<Scene>: |self: Arrangement| self.scenes); impl_has!(Vec<Scene>: |self: Arrangement| self.scenes);
impl<T: TracksView+ScenesView+Send+Sync> ClipsView for T {}
impl<T: AsRef<Vec<Scene>>+AsMut<Vec<Scene>>> HasScenes for T {} impl<T: AsRef<Vec<Scene>>+AsMut<Vec<Scene>>> HasScenes for T {}
impl<T: AsRefOpt<Scene>+AsMutOpt<Scene>+Send+Sync> HasScene for T {} impl<T: AsRefOpt<Scene>+AsMutOpt<Scene>+Send+Sync> HasScene for T {}
impl ScenesView for App {
fn w_mid (&self) -> u16 {
(self.size.w() as u16).saturating_sub(self.w_side())
}
fn w_side (&self) -> u16 {
20
}
fn h_scenes (&self) -> u16 {
(self.size.h() as u16).saturating_sub(20)
}
}

View file

@ -55,9 +55,13 @@ impl Track {
) -> impl Draw<Tui> + 'a { ) -> impl Draw<Tui> + 'a {
view_track_per(tracks, callback) view_track_per(tracks, callback)
} }
}
#[cfg(feature = "sampler")]
impl Track {
/// Create a new track connecting the [Sequencer] to a [Sampler]. /// Create a new track connecting the [Sequencer] to a [Sampler].
#[cfg(feature = "sampler")] pub fn new_with_sampler ( pub fn new_with_sampler (
name: &impl AsRef<str>, name: &impl AsRef<str>,
color: Option<ItemTheme>, color: Option<ItemTheme>,
jack: &Jack<'static>, jack: &Jack<'static>,
@ -78,7 +82,7 @@ impl Track {
Ok(track) Ok(track)
} }
#[cfg(feature = "sampler")] pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> { pub fn sampler (&self, mut nth: usize) -> Option<&Sampler> {
for device in self.devices.iter() { for device in self.devices.iter() {
match device { match device {
Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; },
@ -88,7 +92,7 @@ impl Track {
None None
} }
#[cfg(feature = "sampler")] pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> { pub fn sampler_mut (&mut self, mut nth: usize) -> Option<&mut Sampler> {
for device in self.devices.iter_mut() { for device in self.devices.iter_mut() {
match device { match device {
Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; }, Device::Sampler(s) => if nth == 0 { return Some(s); } else { nth -= 1; },
@ -153,22 +157,22 @@ pub trait HasTrack: AsRefOpt<Track> + AsMutOpt<Track> {
#[cfg(feature = "port")] #[cfg(feature = "port")]
fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Draw<Tui> + 'a { fn view_midi_ins_status <'a> (&'a self, theme: ItemTheme) -> impl Draw<Tui> + 'a {
crate::view::view_midi_ins_status(theme, self.track()) view_midi_ins_status(theme, self.track())
} }
#[cfg(feature = "port")] #[cfg(feature = "port")]
fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Draw<Tui> + '_ { fn view_midi_outs_status (&self, theme: ItemTheme) -> impl Draw<Tui> + '_ {
crate::view::view_midi_outs_status(theme, self.track()) view_midi_outs_status(theme, self.track())
} }
#[cfg(feature = "port")] #[cfg(feature = "port")]
fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Draw<Tui> { fn view_audio_ins_status (&self, theme: ItemTheme) -> impl Draw<Tui> {
crate::view::view_audio_ins_status(theme, self.track()) view_audio_ins_status(theme, self.track())
} }
#[cfg(feature = "port")] #[cfg(feature = "port")]
fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Draw<Tui> { fn view_audio_outs_status (&self, theme: ItemTheme) -> impl Draw<Tui> {
crate::view::view_audio_outs_status(theme, self.track()) view_audio_outs_status(theme, self.track())
} }
} }
@ -179,13 +183,13 @@ pub trait HasTrackScroll: HasTracks {
pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll { pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll {
/// Draw name of each track /// Draw name of each track
fn view_track_names (&self, theme: ItemTheme) -> impl Draw<Tui> { fn view_track_names (&self, theme: ItemTheme) -> impl Draw<Tui> {
crate::view::view_track_names( view_track_names(
theme, self.tracks_with_sizes(), self.tracks().len(), self.scenes().len(), self.selection() theme, self.tracks_with_sizes(), self.tracks().len(), self.scenes().len(), self.selection()
) )
} }
/// Draw outputs per track /// Draw outputs per track
fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw<Tui> { fn view_track_outputs <'a> (&'a self, theme: ItemTheme, _h: u16) -> impl Draw<Tui> {
crate::view::view_track_outputs( view_track_outputs(
theme, self.tracks_with_sizes(), self.midi_outs().iter() theme, self.tracks_with_sizes(), self.midi_outs().iter()
) )
} }
@ -195,7 +199,7 @@ pub trait TracksView: ScenesView + HasMidiIns + HasMidiOuts + HasTrackScroll {
for track in self.tracks().iter() { for track in self.tracks().iter() {
h = h.max(track.sequencer.midi_ins.len() as u16); h = h.max(track.sequencer.midi_ins.len() as u16);
} }
crate::view::view_track_inputs(theme, self.tracks_with_sizes(), h) view_track_inputs(theme, self.tracks_with_sizes(), h)
} }
/// Iterate over tracks with their corresponding sizes. /// Iterate over tracks with their corresponding sizes.
fn tracks_with_sizes (&self) -> impl TracksSizes<'_> { fn tracks_with_sizes (&self) -> impl TracksSizes<'_> {
@ -247,19 +251,19 @@ impl_as_mut!(Vec<Track>: |self: App| self.project.as_mut());
impl Arrangement { impl Arrangement {
pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw<Tui> + '_ { pub fn view_inputs (&self, _theme: ItemTheme) -> impl Draw<Tui> + '_ {
crate::view::view_inputs( view_inputs(
self.tracks_with_sizes(), self.midi_ins().as_slice() self.tracks_with_sizes(), self.midi_ins().as_slice()
) )
} }
pub fn view_outputs (&self, theme: ItemTheme) -> impl Draw<Tui> { pub fn view_outputs (&self, theme: ItemTheme) -> impl Draw<Tui> {
crate::view::view_outputs( view_outputs(
theme, self.tracks_with_sizes(), self.midi_outs(), self.outputs_height() theme, self.tracks_with_sizes(), self.midi_outs(), self.outputs_height()
) )
} }
pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw<Tui> { pub fn view_track_devices (&self, theme: ItemTheme) -> impl Draw<Tui> {
crate::view::view_track_devices( view_track_devices(
theme, self.tracks_with_sizes(), self.track(), self.devices_height() theme, self.tracks_with_sizes(), self.track(), self.devices_height()
) )
} }
@ -334,3 +338,13 @@ impl Arrangement {
Ok((index, &mut self.tracks_mut()[index])) Ok((index, &mut self.tracks_mut()[index]))
} }
} }
impl HasTrackScroll for App {
fn track_scroll (&self) -> usize {
self.project.track_scroll()
}
}
pub(crate) fn track_width (_index: usize, track: &Track) -> u16 {
track.width as u16
}

View file

@ -145,6 +145,27 @@ impl Quantize {
} }
} }
/// Implement an arithmetic operation for a unit of time
#[macro_export] macro_rules! impl_op {
($T:ident, $Op:ident, $method:ident, |$a:ident,$b:ident|{$impl:expr}) => {
impl $Op<Self> for $T {
type Output = Self; #[inline] fn $method (self, other: Self) -> Self::Output {
let $a = self.get(); let $b = other.get(); Self($impl.into())
}
}
impl $Op<usize> for $T {
type Output = Self; #[inline] fn $method (self, other: usize) -> Self::Output {
let $a = self.get(); let $b = other as f64; Self($impl.into())
}
}
impl $Op<f64> for $T {
type Output = Self; #[inline] fn $method (self, other: f64) -> Self::Output {
let $a = self.get(); let $b = other; Self($impl.into())
}
}
}
}
/// Define and implement a unit of time /// Define and implement a unit of time
#[macro_export] macro_rules! impl_time_unit { #[macro_export] macro_rules! impl_time_unit {
($T:ident) => { ($T:ident) => {

View file

@ -43,7 +43,7 @@ impl Dialog {
MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))), MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))),
MenuItem("Create new session".into(), Arc::new(Box::new(|app|Ok({ MenuItem("Create new session".into(), Arc::new(Box::new(|app|Ok({
app.dialog = Dialog::None; app.dialog = Dialog::None;
app.mode = app.config.modes.clone().read().unwrap().get(":arranger").cloned().unwrap(); app.mode = app.config.modes.get(":arranger").unwrap();
})))), })))),
MenuItem("Load old session".into(), Arc::new(Box::new(|_|Ok(())))), MenuItem("Load old session".into(), Arc::new(Box::new(|_|Ok(())))),
].into())) ].into()))

View file

@ -1,5 +1,13 @@
use crate::*; use crate::*;
#[cfg(feature = "lv2_gui")] use ::winit::{
application::ApplicationHandler,
event::WindowEvent,
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
window::{Window, WindowId},
platform::x11::EventLoopBuilderExtX11
};
/// A LV2 plugin. /// A LV2 plugin.
#[derive(Debug)] #[cfg(feature = "lv2")] pub struct Lv2 { #[derive(Debug)] #[cfg(feature = "lv2")] pub struct Lv2 {
/// JACK client handle (needs to not be dropped for standalone mode to work). /// JACK client handle (needs to not be dropped for standalone mode to work).

View file

@ -3,14 +3,13 @@ use ConnectName::*;
use ConnectScope::*; use ConnectScope::*;
use ConnectStatus::*; use ConnectStatus::*;
pub mod audio; pub use self::audio::*; mod audio; pub use self::audio::*;
pub mod midi; pub use self::midi::*; mod midi; pub use self::midi::*;
pub mod connect; pub use self::connect::*; mod connect; pub use self::connect::*;
mod register; pub use self::register::*;
pub trait JackPort: HasJack<'static> { pub trait JackPort: HasJack<'static> {
type Port: PortSpec + Default; type Port: PortSpec + Default;
type Pair: PortSpec + Default; type Pair: PortSpec + Default;
fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect]) fn new (jack: &Jack<'static>, name: &impl AsRef<str>, connect: &[Connect])
@ -135,29 +134,3 @@ pub trait JackPort: HasJack<'static> {
})) }))
} }
} }
pub trait RegisterPorts: HasJack<'static> {
/// Register a MIDI input port.
fn midi_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiInput>;
/// Register a MIDI output port.
fn midi_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiOutput>;
/// Register an audio input port.
fn audio_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioInput>;
/// Register an audio output port.
fn audio_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioOutput>;
}
impl<J: HasJack<'static>> RegisterPorts for J {
fn midi_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiInput> {
MidiInput::new(self.jack(), name, connect)
}
fn midi_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiOutput> {
MidiOutput::new(self.jack(), name, connect)
}
fn audio_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioInput> {
AudioInput::new(self.jack(), name, connect)
}
fn audio_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioOutput> {
AudioOutput::new(self.jack(), name, connect)
}
}

View file

@ -0,0 +1,27 @@
use crate::*;
pub trait RegisterPorts: HasJack<'static> {
/// Register a MIDI input port.
fn midi_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiInput>;
/// Register a MIDI output port.
fn midi_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiOutput>;
/// Register an audio input port.
fn audio_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioInput>;
/// Register an audio output port.
fn audio_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioOutput>;
}
impl<J: HasJack<'static>> RegisterPorts for J {
fn midi_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiInput> {
MidiInput::new(self.jack(), name, connect)
}
fn midi_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<MidiOutput> {
MidiOutput::new(self.jack(), name, connect)
}
fn audio_in (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioInput> {
AudioInput::new(self.jack(), name, connect)
}
fn audio_out (&self, name: &impl AsRef<str>, connect: &[Connect]) -> Usually<AudioOutput> {
AudioOutput::new(self.jack(), name, connect)
}
}

View file

@ -1,5 +1,13 @@
use crate::{*, device::*, browse::*, mix::*}; use crate::{*, device::*, browse::*, mix::*};
pub(crate) use symphonia::{
default::get_codecs,
core::{//errors::Error as SymphoniaError,
audio::SampleBuffer, formats::Packet, io::MediaSourceStream, probe::Hint,
codecs::{Decoder, CODEC_TYPE_NULL},
},
};
mod voice; pub use self::voice::*; mod voice; pub use self::voice::*;
mod sample; pub use self::sample::*; mod sample; pub use self::sample::*;
mod sample_add; pub use self::sample_add::*; mod sample_add; pub use self::sample_add::*;

View file

@ -1,92 +1,19 @@
#![allow(clippy::unit_arg)] #![allow(clippy::unit_arg)]
#![feature(adt_const_params, associated_type_defaults, anonymous_lifetime_in_impl_trait, #![feature(
closure_lifetime_binder, impl_trait_in_assoc_type, trait_alias, type_alias_impl_trait, adt_const_params,
type_changing_struct_update)] anonymous_lifetime_in_impl_trait,
impl_trait_in_assoc_type,
/// CLI banner. trait_alias,
pub(crate) const HEADER: &'static str = r#" type_changing_struct_update
)]
~ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~
~ v0.4.0, 2026 heatwave edition ~
~ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#;
#[cfg(feature = "cli")] use clap::{self, Parser, Subcommand};
pub extern crate atomic_float; pub extern crate atomic_float;
pub extern crate xdg; pub extern crate xdg;
pub(crate) use ::xdg::BaseDirectories;
pub extern crate midly; pub extern crate midly;
pub(crate) use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*};
pub extern crate tengri; pub extern crate tengri;
pub(crate) use tengri::{
*, lang::*, exit::*, eval::*, sing::*, time::*, draw::*, term::*, color::*, task::*,
crossterm::event::{Event, KeyEvent},
ratatui::{
self,
prelude::{Rect, Style, Stylize, Buffer, Color::{self, *}},
widgets::{Widget, canvas::{Canvas, Line}},
},
};
/// Implement an arithmetic operation for a unit of time
#[macro_export] macro_rules! impl_op {
($T:ident, $Op:ident, $method:ident, |$a:ident,$b:ident|{$impl:expr}) => {
impl $Op<Self> for $T {
type Output = Self; #[inline] fn $method (self, other: Self) -> Self::Output {
let $a = self.get(); let $b = other.get(); Self($impl.into())
}
}
impl $Op<usize> for $T {
type Output = Self; #[inline] fn $method (self, other: usize) -> Self::Output {
let $a = self.get(); let $b = other as f64; Self($impl.into())
}
}
impl $Op<f64> for $T {
type Output = Self; #[inline] fn $method (self, other: f64) -> Self::Output {
let $a = self.get(); let $b = other; Self($impl.into())
}
}
}
}
#[cfg(feature = "sampler")] pub(crate) use symphonia::{
default::get_codecs,
core::{//errors::Error as SymphoniaError,
audio::SampleBuffer, formats::Packet, io::MediaSourceStream, probe::Hint,
codecs::{Decoder, CODEC_TYPE_NULL},
},
};
#[cfg(feature = "lv2_gui")] use ::winit::{
application::ApplicationHandler,
event::WindowEvent,
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
window::{Window, WindowId},
platform::x11::EventLoopBuilderExtX11
};
#[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}},
time::Duration,
thread::{spawn, JoinHandle},
},
};
/// Command-line entrypoint. /// Command-line entrypoint.
#[cfg(feature = "cli")] #[cfg(feature = "cli")] pub fn main () -> Usually<()> {
pub fn main () -> Usually<()> {
tui_setup_panic(); tui_setup_panic();
Config::watch(|config|{ Config::watch(|config|{
tui_run_main(Jack::new_run("tek", move|jack|{ tui_run_main(Jack::new_run("tek", move|jack|{
@ -98,8 +25,14 @@ pub fn main () -> Usually<()> {
}).map(|_|()) }).map(|_|())
} }
//take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref() pub mod deps; pub use self::deps::*;
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));
pub mod device; pub use self::device::*; pub mod device; pub use self::device::*;
pub mod app; pub use self::app::*; pub mod app; pub use self::app::*;
/// CLI banner.
pub(crate) const HEADER: &'static str = r#"
~ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~
~ v0.4.0, 2026 heatwave edition ~
~ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#;