This commit is contained in:
i do not exist 2026-09-17 09:49:38 +03:00
parent f743d9fb4d
commit 922c2e1f2f
45 changed files with 1950 additions and 1765 deletions

4
Cargo.lock generated
View file

@ -3893,9 +3893,13 @@ dependencies = [
"midly",
"palette",
"parking_lot 0.12.5",
"profiling",
"quanta",
"rand 0.8.7",
"ratatui",
"tracing",
"tracing-flame",
"tracing-subscriber",
"unicode-width 0.2.0",
]

View file

@ -54,7 +54,7 @@ proptest-derive = { version = "^0.5.1" }
default = ["cli", "arranger", "sampler", "prof"]
prof = []
#prof2 = ["prof", "tengri/prof"]
prof2 = ["prof", "tengri/prof"]
hotpath = ["hotpath/hotpath"]
hotpath-cpu = ["hotpath/hotpath-cpu"]
hotpath-alloc = ["hotpath/hotpath-alloc"]

599
src/app.rs Normal file
View file

@ -0,0 +1,599 @@
use crate::*;
/// Total application state.
///
/// ```
/// use tek::{HasTracks, HasScenes, TracksView, ScenesView};
/// let mut app = tek::App::default();
/// let _ = app.scenes_add_one(None, None).unwrap();
/// let _ = app.update_clock();
/// app.project.editor = Some(Default::default());
/// //let _: Vec<_> = app.project.inputs_with_sizes().collect();
/// //let _: Vec<_> = app.project.outputs_with_sizes().collect();
/// let _: Vec<_> = app.project.tracks_with_sizes().collect();
/// //let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect();
/// //let _: Vec<_> = app.scenes_with_colors(true, 10).collect();
/// //let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect();
/// let _ = app.project.w();
/// //let _ = app.project.w_sidebar();
/// //let _ = app.project.w_tracks_area();
/// let _ = app.project.h();
/// //let _ = app.project.h_tracks_area();
/// //let _ = app.project.h_inputs();
/// //let _ = app.project.h_outputs();
/// let _ = app.project.h_scenes();
/// ```
#[derive(Default, Debug)]
#[namespace(MidiClip)]
#[namespace(ItemColor)]
#[namespace(ItemTheme)]
#[namespace(isize)]
#[namespace(u8)]
#[namespace(bool get_bool)]
#[namespace(u16 get_u16)]
#[namespace(usize get_usize)]
#[namespace(Arc<[Connect]> get_arc_array_connect)]
#[namespace(Arc<str> get_arc_str)]
#[namespace(Color get_color)]
#[namespace(ControlAxis get_axis)]
#[namespace(Dialog get_dialog)]
#[namespace(Option<Arc<[Connect]>> get_opt_arc_array_connect)]
#[namespace(Option<Arc<str>> get_opt_arc_str)]
#[namespace(Option<ItemTheme> get_opt_itemtheme)]
#[namespace(Option<Vec<Option<Arc<RwLock<MidiClip>>>>> get_opt_vec_opt_arc_rwlock_midiclip)]
#[namespace(Option<bool> get_opt_bool)]
#[namespace(Option<u16> get_opt_u16)]
#[namespace(Option<usize> get_opt_usize)]
#[namespace(PathBuf get_path_buf)]
#[namespace(Vec<Arc<str>> get_vec_arc_str)]
pub struct App {
/// Exit flag
pub exit: Exit,
/// Base color.
pub color: ItemTheme,
/// Must not be dropped for the duration of the process
pub jack: Jack<'static>,
/// Display size
pub size: Sizer,
/// Performance counter
pub perf: PerfModel,
/// Available view modes and input bindings
pub config: Arc<Config>,
/// Currently selected mode
pub modes: Arc<RwLock<Vec<Arc<str>>>>,
/// Undo history
pub history: Vec<(AppCommand, Option<AppCommand>)>,
/// Dialog overlay
pub dialog: Dialog,
/// Contains all recently created clips.
pub pool: Pool,
/// Contains the currently edited musical arrangement
pub project: Project,
/// Error, if any
pub error: Arc<RwLock<Option<Arc<str>>>>,
#[cfg(feature = "prof2")]
/// Tracing guard
pub guard: Option<tracing_flame::FlushGuard<std::io::BufWriter<std::fs::File>>>,
}
impl App {
/// Create a new application instance from a backend, project, config, and mode
///
/// ```
/// let mut proj = tek::Project::default();
/// proj.jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack");
/// let mut conf = tek::config_load(tek::Config::default(), "(mode hello)").unwrap();
/// let tek = tek::App::new(None, proj, conf.into(), "hello");
/// ```
pub fn new (
exit: Option<Exit>,
project: Project,
config: Arc<Config>,
mode: impl AsRef<str>
) -> Self {
App {
exit: exit.unwrap_or_default(),
jack: project.jack.clone(),
color: ItemTheme::random(),
dialog: Dialog::welcome(),
modes: Arc::new(RwLock::new(vec![mode.as_ref().into()])),
config,
project,
..Default::default()
}
}
}
fn get_arc_str <'a> (_: &App, src: impl Language) -> Perhaps<Arc<str>> {
Ok(src.src()?.map(|x|x.into()))
}
fn get_u16 <'a> (state: &App, src: impl Language) -> Perhaps<u16> {
Ok(Some(match src.word()? {
Some(":w/sidebar") => state.project.w_sidebar(state.editor().is_some()),
Some(":h/sample-detail") => 6.max(state.size.h() as u16 * 3 / 9),
_ => return try_to_u16(src)
}))
}
fn get_usize <'a> (state: &App, src: impl Language) -> Perhaps<usize> {
Ok(Some(match src.word()? {
Some(":scene-count") => state.scenes().len(),
Some(":track-count") => state.tracks().len(),
Some(":device-kind") => state.dialog.device_kind().unwrap_or(0),
Some(":device-kind/next") => state.dialog.device_kind_next().unwrap_or(0),
Some(":device-kind/prev") => state.dialog.device_kind_prev().unwrap_or(0),
_ => return try_to_usize(src)
}))
}
fn get_bool <'a> (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()
}
#[allow(unused)]
fn get_selection <'a> (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 <'a> (_: &App, src: impl Language) -> Perhaps<Color> {
if let Some(expr) = src.expr()? {
match (expr.head()?, expr.tail()?) {
(Some("g"), Some(tail)) => {
let n = try_to_u8(tail.head().map_err(Into::into))?
.ok_or(LanguageError::domain("not gray"))?;
Ok(Some(Color::Rgb(n, n, n)))
},
(Some("rgb"), Some(tail)) => {
let r = try_to_u8(tail.head().map_err(Into::into))?
.ok_or(LanguageError::domain("not red"))?;
let g = try_to_u8(tail.tail().head().map_err(Into::into))?
.ok_or(LanguageError::domain("not green"))?;
let b = try_to_u8(tail.tail().tail().head().map_err(Into::into))?
.ok_or(LanguageError::domain("not blue"))?;
Ok(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 if let Ok(Some(sym)) = src.word() {
Ok(match sym {
":color/bg" => Some(Color::Rgb(28, 32, 36)),
":color/fg" => Some(Color::Rgb(98, 92, 96)),
_ => return Err(format!("not a color: {sym}").into())
})
} else {
return Err(format!("not a color: {:?}", src.src()?).into())
}
}
#[allow(unused)]
fn get_opt_u7 <'a> (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_usize <'a> (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()
}
#[allow(unused)]
fn get_clip <'a> (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()
}
fn get_axis <'a> (_: &App, src: impl Language) -> Perhaps<ControlAxis> {
Ok(src.word()?.map(|word|Ok(match word {
":x" => ControlAxis::X,
":y" => ControlAxis::Y,
_ => return Err(format!("unknown axis {word}"))
})).transpose()?)
}
fn get_opt_u16 <'a> (_: &App, _src: impl Language) -> Perhaps<Option<u16>> {
Ok(None)
}
fn get_opt_itemtheme <'a> (_: &App, _: impl Language) -> Perhaps<Option<ItemTheme>> {
todo!()
}
fn get_opt_vec_opt_arc_rwlock_midiclip <'a> (_: &App, _: impl Language) -> Perhaps<Option<Vec<Option<Arc<RwLock<MidiClip>>>>>> {
todo!()
}
fn get_opt_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps<Option<Arc<[Connect]>>> {
todo!()
}
fn get_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps<Arc<[Connect]>> {
todo!()
}
fn get_opt_arc_str <'a> (_: &App, _: impl Language) -> Perhaps<Option<Arc<str>>> {
todo!()
}
fn get_opt_bool <'a> (_: &App, _: impl Language) -> Perhaps<Option<bool>> {
todo!()
}
fn get_path_buf <'a> (_: &App, _: impl Language) -> Perhaps<PathBuf> {
todo!()
}
fn get_vec_arc_str <'a> (_: &App, _: impl Language) -> Perhaps<Vec<Arc<str>>> {
todo!()
}
impl_has!(Clock: |self: App|self.project.clock);
impl_has!(Dialog: |self: App|self.dialog);
impl_has!(Jack<'static>: |self: App|self.jack);
impl_has!(Pool: |self: App|self.pool);
impl_has!(Selection: |self: App|self.project.selection);
impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt());
impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt());
impl_has_clips!( |self: App|self.pool.clips);
primitive!(u8: try_to_u8);
primitive!(u16: try_to_u16);
primitive!(usize: try_to_usize);
primitive!(isize: try_to_isize);
tui_keys!(self: App, input {
#[cfg(feature = "prof2")] profiling::scope!("App::tui_keys!");
Self::input_command(self, input)
});
impl App {
fn input_command (state: &mut Self, input: &TuiEvent) -> Usually<()> {
if let Some(bindings) = Self::input_command_find(state, input) {
return Self::input_command_exec(state, bindings)
}
Ok(())
}
fn input_command_find (state: &mut Self, input: &TuiEvent) -> Option<Arc<[Binding<Arc<str>>]>> {
for name in state.modes.read().iter().rev() {
let mode = state.config.get_mode(name).as_ref().map(Arc::clone);
if let Some(mode) = mode {
let binds = state.config.binds.clone();
for id in mode.keys.iter() {
if let Some(event_map) = binds.try_read().unwrap().get(id.as_ref())
&& let Some(bindings) = event_map.query(input) {
return Some(bindings.into())
}
}
}
}
None
}
fn input_command_exec (state: &mut Self, bindings: Arc<[Binding<Arc<str>>]>) -> Usually<()> {
for binding in bindings.iter() {
for command in binding.commands.iter() {
if Self::command::<AppCommand>(state, command)? {
return Ok(())
}
}
}
Ok(())
}
fn command <
T: Dispatch<Self> + Clone + Into<AppCommand>
> (state: &mut Self, src: &str) -> Usually<bool> where
Self: Namespace<T>,
{
if let Some(command) = Namespace::<T>::namespace(state, &src)? {
return Self::command_dispatch(state, command.into())
}
Ok(false)
}
fn command_dispatch (state: &mut Self, command: AppCommand) -> Usually<bool> where
Self: Namespace<AppCommand>,
{
match command.clone().dispatch(state) {
Err(err) => {
state.history.push((command.into(), None));
return Err(err)
},
Ok(undo) => {
state.history.push((command.into(), undo.map(Into::into)));
return Ok(true)
}
}
}
}
#[tek_proc::commands(AppCommand)]
impl App {
/// Exit the application.
#[command(Exit = "exit")]
pub fn command_exit (&mut self) -> Perhaps<AppCommand> {
self.exit.exit();
Ok(None)
}
/// Cancel current selection.
#[command(Cancel = "cancel")]
pub fn command_cancel (&mut self) -> Perhaps<AppCommand> {
todo!("cancel")
}
/// Confirm current selection.
#[command(Confirm = "confirm")]
pub fn command_confirm (&mut self) -> Perhaps<AppCommand> {
Ok(match self.dialog.clone() {
Dialog::Menu(index, items) => {
for command in items[index].1.iter() {
App::command_dispatch(self, command.clone())?;
}
None
}
_ => todo!(),
})
}
/// Swap currently active modal dialog.
///
/// ```
/// let _ = tek::App::default().set_dialog(tek::Dialog::welcome());
/// ```
#[command(Dialog = "dialog")]
pub fn command_dialog (&mut self, dialog: Dialog) -> Perhaps<AppCommand> {
let mut dialog = dialog.clone();
std::mem::swap(&mut self.dialog, &mut dialog);
Ok(Some(AppCommand::Dialog { dialog }))
}
#[command(Mode = "mode")]
pub fn command_mode (&mut self, command: ModeCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Project = "session")]
pub fn command_session (&mut self, command: ProjectCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Axis = "axis")]
pub fn command_axis (&mut self, command: AxisCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Browse = "browse")]
pub fn command_browse (&mut self, command: BrowseCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(MidiClip = "clip")]
pub fn command_midi_clip (&mut self, command: MidiClipCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Pool = "pool")]
pub fn command_pool (&mut self, command: PoolCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Scene = "scene")]
pub fn command_scene (&mut self, command: SceneCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Scenes = "scenes")]
pub fn command_scenes (&mut self, command: ScenesCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Track = "track")]
pub fn command_track (&mut self, command: TrackCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Tracks = "tracks")]
pub fn command_tracks (&mut self, command: TracksCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
}
/// A control axis.
///
/// ```
/// let axis = tek::ControlAxis::X;
/// ```
#[derive(Debug, Copy, Clone)] pub enum ControlAxis {
X, Y, Z, I
}
#[tek_proc::commands(AxisCommand)]
impl App {
/// Increment a given data axis.
#[command(Inc = "inc")]
pub fn inc (&mut self, axis: ControlAxis) -> Perhaps<AxisCommand> {
Ok(match (&self.dialog, axis) {
(Dialog::None, _) => todo!(),
(Dialog::Menu(_, _), ControlAxis::Y) => {
AppCommand::Dialog { dialog: self.dialog.menu_next() }.dispatch(self)?;
None
},
_ => todo!()
})
}
/// Decrement a given data axis.
#[command(Dec = "dec")]
pub fn dec (&mut self, axis: ControlAxis) -> Perhaps<AxisCommand> {
Ok(match (&self.dialog, axis) {
(Dialog::None, _) => None,
(Dialog::Menu(_, _), ControlAxis::Y) => {
AppCommand::Dialog { dialog: self.dialog.menu_prev() }.dispatch(self)?;
None
},
_ => todo!()
})
}
}
impl_from!(AppCommand: |x: AxisCommand| AppCommand::Axis { command: x });
impl_from!(AppCommand: |x: BrowseCommand| AppCommand::Browse { command: x });
impl_from!(AppCommand: |x: MidiClipCommand| AppCommand::MidiClip { command: x });
impl_from!(AppCommand: |x: ModeCommand| AppCommand::Mode { command: x });
impl_from!(AppCommand: |x: PoolCommand| AppCommand::Pool { command: x });
impl_from!(AppCommand: |x: ProjectCommand| AppCommand::Project { command: x });
impl_from!(AppCommand: |x: SceneCommand| AppCommand::Scene { command: x });
impl_from!(AppCommand: |x: ScenesCommand| AppCommand::Scenes { command: x });
impl_from!(AppCommand: |x: TrackCommand| AppCommand::Track { command: x });
impl_from!(AppCommand: |x: TracksCommand| AppCommand::Tracks { command: x });
/// The [Draw] implementation for [App] handles the loaded view,
/// which is defined in terms of [dizzle] DSL.
///
/// If there is an error, the error is displayed. FIXME: overlay it
/// Then, every top-level form of the DSL description is rendered.
impl Draw<Tui> for App {
fn draw (&self, to: &mut Tui) -> Drawn<u16> {
#[cfg(feature = "prof2")] profiling::scope!("App::draw");
//self.perf.cycle(&mut |_|{
self.draw_error(to)?;
self.draw_modes(to)?;
self.draw_debug(to)?;
#[cfg(feature = "prof2")] profiling::finish_frame!();
Ok(Some(to.area().into()))
//})
}
}
impl App {
fn draw_error (&self, to: &mut Tui) -> Usually<()> {
if let Some(e) = self.error.try_read().unwrap().as_ref() {
e.as_ref().align_c().draw(to)?;
}
Ok(())
}
fn draw_modes (&self, to: &mut Tui) -> Usually<()> {
for name in self.modes.read().iter() {
let mut error = false;
if let Some(mode) = self.config.get_mode(name).as_ref().map(Arc::clone) {
for (index, view) in mode.view.iter().enumerate() {
match (view.render)(self, to) {
Ok(None) => {},
Ok(Some(XYWH(.., w, h))) => {
self.size.0.store(w as usize, Relaxed);
self.size.1.store(h as usize, Relaxed);
},
Err(e) => {
let message = format!(
"Mode: {:?}\n\nLayer: #{index}\n\nError: {e}\n\nSource:\n{}",
&mode.name,
&view.source
);
*self.error.try_write().unwrap() = Some(message.into());
error = true;
break;
}
}
}
if error {
break
} else {
*self.error.try_write().unwrap() = None;
}
}
}
Ok(())
}
#[allow(unused)] fn draw_debug (&self, to: &mut Tui) -> Drawn<u16> {
east(
format!("{}x{} ",
self.size.0.load(Relaxed),
self.size.1.load(Relaxed)),
format!("{}/{} {} ",
self.perf.used.load(Relaxed),
self.perf.window.load(Relaxed),
self.perf.clock.raw() / 10000000),
).align_se().draw(to)
}
}
impl_has!(Sizer: |self: App|self.size);
pub trait HasWidth {
const MIN_WIDTH: usize;
/// Increment track width.
fn width_inc (&mut self);
/// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH].
fn width_dec (&mut self);
}
pub fn view_templates <'a> (state: &'a App) -> impl Draw<Tui> + use<'a> {
let height = (state.config.modes.len() * 2) as u16;
draw(move |to: &mut Tui|{
let mut index = 0;
state.config.modes.for_each(&mut |id: &str, profile: &Mode| {
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 info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or("<no info>");
let fg1 = Rgb(224, 192, 128);
let fg2 = Rgb(224, 128, 32);
let field_name = fg(fg1, name).align_w().full_w();
let field_id = fg(fg2, id).align_e().full_w();
let field_info = info.align_w().full_w();
let _ = bg(b, south(above(field_name, field_id), field_info))
.full_w().exact_h(2).push_y((2 * index) as u16).draw(to);
index += 1;
});
Ok(Some(to.area().into()))
}).min_w(30).exact_h(height)
}
pub fn draw_status_bar (to: &mut Tui, state: &App) -> Drawn<u16> {
bar(Split::South, 1u16,
dim(true, "Mode"),
bold(true, state.modes.read().get(0))
).push_x(1).draw(to)
}

92
src/bind.rs Normal file
View file

@ -0,0 +1,92 @@
use crate::*;
/// An map of input events (e.g. [TuiEvent]) to [Binding]s.
///
/// ```
/// use ::{std::sync::Arc, tek::{Bind, tengri::TuiEvent}};
/// let bind = Bind::<TuiEvent, Arc<str>>::load(&stringify!(
/// (@x (nop))
/// (@y (nop) (nop)))).unwrap();
/// assert_eq!(bind.query(&'x'.into()).map(|x|x.len()), Some(1));
/// //assert_eq!(bind.query(&'y'.into()).map(|x|x.len()), Some(2));
/// ```
#[derive(Debug)]
pub struct Bind<E, C>(
/// Map of each event (e.g. key combination) to
/// all command expressions bound to it by
/// all loaded input layers.
pub BTreeMap<E, Vec<Binding<C>>>
);
/// A sequence of zero or more commands (e.g. [AppCommand]),
/// optionally filtered by [Condition] to form layers.
///
/// ```
/// //FIXME: Why does it overflow?
/// //let binding: Binding<()> = tek::Binding { ..Default::default() };
/// ```
#[derive(Debug, Clone)] pub struct Binding<C> {
pub commands: Arc<[C]>,
pub condition: Option<Condition>,
pub description: Option<Arc<str>>,
pub source: Option<Arc<PathBuf>>,
}
/// Condition that must evaluate to true in order to enable an input layer.
///
/// ```
/// let condition = tek::Condition(std::sync::Arc::new(Box::new(||{true})));
/// ```
#[derive(Clone)]
pub struct Condition(pub Arc<Box<dyn Fn()->bool + Send + Sync>>);
/// Default is always empty map regardless if `E` and `C` implement [Default].
impl<E, C> Default for Bind<E, C> {
fn default () -> Self { Self(Default::default()) }
}
impl<C: Default> Default for Binding<C> {
fn default () -> Self {
Self {
commands: Default::default(),
condition: Default::default(),
description: Default::default(),
source: Default::default(),
}
}
}
impl<E: Clone + Ord, C> Bind<E, C> {
/// Create a new event map
pub fn new () -> Self {
Default::default()
}
/// Add a binding to an owned event map.
pub fn def (mut self, event: E, binding: Binding<C>) -> Self {
self.add(event, binding);
self
}
/// Add a binding to an event map.
pub fn add (&mut self, event: E, binding: Binding<C>) -> &mut Self {
if !self.0.contains_key(&event) {
self.0.insert(event.clone(), Default::default());
}
self.0.get_mut(&event).unwrap().push(binding);
self
}
/// Return the binding(s) that correspond to an event.
pub fn query (&self, event: &E) -> Option<&[Binding<C>]> {
self.0.get(event).map(|x|x.as_slice())
}
/// Return the first binding that corresponds to an event, considering conditions.
pub fn dispatch (&self, event: &E) -> Option<&Binding<C>> {
self.query(event)
.map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next())
.flatten()
}
}
impl_debug!(Condition |self, w| { write!(w, "*") });
//take!(DeviceCommand|state: Project, iter|state.selected_device().as_ref()
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));

38
src/binds.rs Normal file
View file

@ -0,0 +1,38 @@
use crate::*;
/// Collection of input bindings.
pub type Binds = Arc<RwLock<BTreeMap<Arc<str>, Bind<TuiEvent, Arc<str>>>>>;
pub fn binds_add <'a> (binds: &Binds, expr: impl Language) -> UsuallyRef<'a, ()> {
//println!("\n\rbinds_add: {expr:?}");
let name = expr.head()?.ok_or("bind: missing name")?;
let body = expr.tail()?.unwrap_or("");
binds.try_write().unwrap().insert(name.into(), {
let mut map = Bind::new();
body.each((), |_, item: &str|if matches!(item.expr().head(), Ok(Some("see"))) {
// TODO
Ok(())
} else if let Ok(Some(_word)) = item.expr().head().word() {
let expr = item.expr()?;
let head = expr.head()?;
if let Some(event) = TuiKey::from_dsl(&head)?.to_crossterm() {
map.add(TuiEvent(event), Binding {
commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(),
condition: None,
description: None,
source: None
});
Ok(())
} else if Some(":char") == item.expr()?.head()? {
// TODO
return Ok(())
} else {
return Err(format!("binds_add: invalid key: {:?}", item.expr()?.head()?).into())
}
} else {
return Err(format!("binds_add: unexpected: {item:?}").into())
})?;
map
});
Ok(())
}

View file

@ -10,32 +10,6 @@ pub fn show_version () {
println!("versions aint real man");
}
#[cfg(not(feature = "cli"))]
fn run_new_plain (config: Config) -> Usually<()> {
let name = "tek";
tengri::Tui::run_main(Jack::new_run(name, move|jack|{
let mode = ":menu";
let title = "untitled!";
let bpm = 74.;
let clock = Clock::new(&jack, Some(bpm))?;
let tracks = [];
let scenes = [];
Ok(App::new(None, Arrangement::new(
&jack,
title.into(),
clock,
tracks.into_iter(),
scenes.into_iter(),
connect_midi_ins(&jack, &"M", &[], None)?.into_iter(),
connect_midi_outs(&jack, &"M", &[], None)?.into_iter(),
[].into_iter().chain(connect_audio_ins(&jack, &"L", &[], None)?.into_iter())
.chain(connect_audio_ins(&jack, &"R", &[], None)?.into_iter()),
[].into_iter().chain(connect_audio_outs(&jack, &"L", &[], None)?.into_iter())
.chain(connect_audio_outs(&jack, &"R", &[], None)?.into_iter()),
), config, mode))
})?)
}
pub fn run_with_config (config: Arc<Config>) -> Usually<()> {
Cli::parse().run(Some(config))
}
@ -99,7 +73,7 @@ pub enum Action {
}
impl Action {
fn run (&self, config: Arc<Config>, trace: bool) -> Usually<()> {
fn run (&self, config: Arc<Config>, _trace: bool) -> Usually<()> {
use Action::*;
match self {
Version => show_version(),
@ -109,6 +83,7 @@ impl Action {
New(sesh) => Exit::run(|exit|Tui::run_main(
exit.clone(),
{
#[allow(unused_mut)]
let mut app = App::new(Some(exit), sesh.init()?, config, ":menu");
#[cfg(feature = "prof2")] {
if trace {
@ -165,7 +140,7 @@ pub struct ProjectInit {
#[arg(short='s', long)] scenes: Option<usize>,
}
impl ProjectInit {
pub fn init (&self) -> Usually<Arrangement> {
pub fn init (&self) -> Usually<Project> {
let Self {
name, bpm, tracks, scenes,
sync_lead: _, sync_follow: _,
@ -175,7 +150,7 @@ impl ProjectInit {
} = self;
let name = name.as_ref().map_or("tek", |x|x.as_str());
let jack = Jack::new(&name)?;
let mut proj = Arrangement::new(
let mut proj = Project::new(
&jack,
name.into(),
Clock::new(&jack, *bpm)?,
@ -207,7 +182,7 @@ impl ProjectInit {
}
}
pub fn print_status (project: &Arrangement) {
pub fn print_status (project: &Project) {
println!("Name: {:?}", &project.name);
println!("JACK: {:?}", &project.jack);
println!("Buffer: {:?}", &project.clock.chunk);

View file

@ -1,6 +1,6 @@
use crate::*;
impl Arrangement {
impl Project {
/// Toggle looping for the active clip
pub fn toggle_loop (&mut self) {
if let Some(clip) = self.selected_clip() {
@ -39,7 +39,7 @@ impl HasClipsSize for App {
fn clips_size (&self) -> &Sizer { &self.project.size_inner }
}
impl HasClipsSize for Arrangement {
impl HasClipsSize for Project {
fn clips_size (&self) -> &Sizer { &self.size_inner }
}

View file

@ -1,11 +1,5 @@
use crate::*;
mod memo; pub use self::memo::*;
mod moment; pub use self::moment::*;
mod ticker; pub use self::ticker::*;
mod timebase; pub use self::timebase::*;
mod clock_view; pub use self::clock_view::*;
impl App {
/// Update memoized render of clock values.
/// ```
@ -508,4 +502,3 @@ pub fn button_play_pause <'a> (playing: bool, compact: bool) -> impl Draw<Tui> {
)
)
}

View file

@ -2,184 +2,12 @@ use crate::*;
use std::path::PathBuf;
use notify_debouncer_full::notify::{RecursiveMode, PollWatcher, Config as NotifyConfig, Watcher};
impl AsRef<Config> for Config {
fn as_ref (&self) -> &Config {
self
}
}
/// Write initial contents of configuration.
pub fn config_init <C: AsRef<Config>> (config: C) -> Usually<C> {
if config.as_ref().find_file().is_none() {
std::fs::write(config.as_ref().place_file()?, Config::DEFAULTS)?;
}
Ok(if let Some(path) = config.as_ref().find_file() {
config_load(config, std::fs::read_to_string(&path)?)?
} else {
return Err(format!("config_init: not found").into())
})
}
pub fn config_load <C: AsRef<Config>, L: Language> (config: C, src: L) -> Usually<C> {
config.as_ref().clear();
config.as_ref().stamp.store(quanta::Clock::new().raw(), Relaxed);
src.each(config, |c, s|config_load_item(c, s.trim()))
}
pub fn config_load_item <C: AsRef<Config>, L: Language> (config: C, src: L) -> Usually<C> {
if let Some(expr) = src.expr()? {
config_load_kind(config, expr)
} else {
Err(format!("Config::add_one: tried to add empty item").into())
}
}
pub fn config_load_kind <C: AsRef<Config>, L: Language> (config: C, src: L) -> Usually<C> {
//if let Some(tail) = src.tail()? {
match src.head()? {
Some("mode") => modes_add(&config.as_ref().modes, src.tail()),
Some("keys") => binds_add(&config.as_ref().binds, src.tail()),
Some("view") => views_add(&config.as_ref().views, src.tail()),
_ => return Err(format!("Config::load: expected view/keys/mode expr, got: {src:?}").into())
}?;
//}
Ok(config)
}
/// Watch a config's file for changes.
pub fn config_watch (
config: Arc<Config>, poll: Option<Duration>
) -> Usually<()> {
let poll = poll.unwrap_or(Duration::from_millis(250));
let opts = NotifyConfig::default().with_poll_interval(poll);
let mut watcher = ::notify_debouncer_full::notify::poll::PollWatcher::new({
let config = config.clone();
move|result|{
match result {
Ok(_events) => if let Err(e) = config_init(config.as_ref()) {
*config.as_ref().error.try_write().unwrap() = Some(format!("{e:?}").into());
panic!("{e:?}");
} else {
//println!("config updated");
},
Err(errors) => {
panic!("{errors:?}");
}
}
}
}, opts)?;
if let Some(path) = config.as_ref().get_file() {
//println!("watching: {path:?}");
watcher.watch(&path, RecursiveMode::NonRecursive)?;
*config.as_ref().watch.try_write().unwrap() = Some(watcher);
Ok(())
} else {
Err(format!("no config path").into())
}
}
/// Register a mode.
pub fn modes_add <'a> (modes: &Modes, expr: impl Language) -> UsuallyRef<'a, ()> {
let name = expr.head()?.ok_or("mode: missing name")?;
let body = expr.tail()?.ok_or("mode: missing body")?;
let mode = Mode::default();
let mode = body.each(mode, |c, s|mode_add(c, s))?;
modes.0.try_write().unwrap().insert(name.into(), Arc::new(mode));
Ok(())
}
impl AsMut<Mode> for Mode {
fn as_mut (&mut self) -> &mut Self {
self
}
}
/// Add a definition to the mode.
///
/// Supported definitions:
///
/// - (name ...) -> name
/// - (info ...) -> description
/// - (keys ...) -> key bindings
/// - (mode ...) -> submode
/// - ... -> view
///
/// ```
/// let mut mode: tek::Mode = tek::mode_add(tek::Mode::default(), "(name hello)").unwrap();
/// ```
pub fn mode_add <T: AsMut<Mode>> (mut mode: T, dsl: impl Language) -> Usually<T> {
Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() {
//println!("Mode::add: {head} {:?}", expr.tail());
let tail = expr.tail()?.map(|x|x.trim()).unwrap_or("");
match head {
"mode" => {
let name = tail.head()?.ok_or("submode: missing name")?;
let body = tail.tail()?.ok_or("submode: missing body")?;
let submode = Mode::default();
let submode = body.each(submode, |c,s|mode_add(c,s))?;
let modes = mode.as_mut().modes.clone();
modes.0.try_write().unwrap().insert(name.into(), Arc::new(submode));
mode
},
"keys" => {
tail.each(mode, |mut mode: T, expr: &str|{
mode.as_mut().keys.push(expr.trim().into());
Ok(mode)
})?
},
"name" => { mode.as_mut().name.push(tail.into()); mode },
"info" => { mode.as_mut().info.push(tail.into()); mode },
"view" => { mode.as_mut().view.push(View::new(tail)?.into()); mode },
_ => { mode.as_mut().view.push(View::new(tail)?.into()); mode },
}
} else if let Ok(Some(word)) = dsl.word() {
mode.as_mut().view.push(View::new(word)?.into());
mode
} else {
return Err(format!("Mode::add: unexpected: {dsl:?}").into());
})
}
pub fn binds_add <'a> (binds: &Binds, expr: impl Language) -> UsuallyRef<'a, ()> {
//println!("\n\rbinds_add: {expr:?}");
let name = expr.head()?.ok_or("bind: missing name")?;
let body = expr.tail()?.unwrap_or("");
binds.try_write().unwrap().insert(name.into(), {
let mut map = Bind::new();
body.each((), |_, item: &str|if matches!(item.expr().head(), Ok(Some("see"))) {
// TODO
Ok(())
} else if let Ok(Some(_word)) = item.expr().head().word() {
let expr = item.expr()?;
let head = expr.head()?;
if let Some(event) = TuiKey::from_dsl(&head)?.to_crossterm() {
map.add(TuiEvent(event), Binding {
commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(),
condition: None,
description: None,
source: None
});
Ok(())
} else if Some(":char") == item.expr()?.head()? {
// TODO
return Ok(())
} else {
return Err(format!("binds_add: invalid key: {:?}", item.expr()?.head()?).into())
}
} else {
return Err(format!("binds_add: unexpected: {item:?}").into())
})?;
map
});
Ok(())
}
/// Configuration: mode, view, and bind definitions.
///
/// ```
/// use ::{std::sync::Arc, tek::{Config, modes_add, Mode}};
/// use ::{std::sync::Arc, tek::{Config, Modes::add, Mode}};
/// let config: Config = Default::default();
/// modes_add(&config.modes, stringify!(
/// Modes::add(&config.modes, stringify!(
/// (mode :menu (name Menu)
/// (info Mode selector.) (keys :axis/y :confirm)
/// (view (bg (g 0) (bsp/s :ports/out
@ -206,337 +34,82 @@ pub struct Config {
pub watch: RwLock<Option<PollWatcher>>
}
/// Collection of UI modes.
///
/// ```
/// let mut modes = tek::Modes::default();
/// assert_eq!(modes.len(), 0);
/// let _ = modes.add(&":foo", &"(mode (name Foo) (info Bar))");
/// assert_eq!(modes.len(), 1);
/// ```
#[derive(Default, Debug, Clone)]
pub struct Modes(Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode>>>>);
/// Group of view and keys definitions.
///
/// ```
/// let mode = tek::Mode::default();
/// ```
#[derive(Default, Debug)]
pub struct Mode {
pub path: PathBuf,
pub name: Vec<Arc<str>>,
pub info: Vec<Arc<str>>,
pub view: Vec<Arc<View<Tui, App>>>,
pub keys: Vec<Arc<str>>,
pub modes: Modes,
impl AsRef<Config> for Config {
fn as_ref (&self) -> &Config {
self
}
}
/// Collection of custom view definitions.
pub type Views = Arc<RwLock<BTreeMap<Arc<str>, Arc<View<Tui, App>>>>>;
/// Custom view definition is a boxed closure emitting a [Draw]able from state `S`.
pub struct View<S: Screen, T> {
pub source: Arc<str>,
pub render: Arc<Box<dyn Fn(&T, &mut S)->Drawn<S::Unit> + Send + Sync>>
}
impl_debug!(<S: Screen, T> View<S, T> |self, w| { write!(w, "View({})", self.source) });
impl_display!(<S: Screen, T> View<S, T> |self, w| { write!(w, "View({})", self.source) });
impl View<Tui, App> {
pub fn new (source: impl AsRef<str>) -> Usually<Self> {
Ok(Self {
source: source.as_ref().into(),
render: Self::compile(source)?
})
impl Config {
/// Write initial contents of configuration.
pub fn init <C: AsRef<Config>> (config: C) -> Usually<C> {
if config.as_ref().find_file().is_none() {
std::fs::write(config.as_ref().place_file()?, Config::DEFAULTS)?;
}
fn boxed <F: Fn(&App, &mut Tui)->Drawn<u16> + Send + Sync + 'static> (f: F)
-> Box<dyn Fn(&App, &mut Tui)->Drawn<u16> + Send + Sync + 'static>
{
Box::new(f)
}
fn compile (source: impl AsRef<str>) ->
Usually<Arc<Box<dyn Fn(&App, &mut Tui)->Drawn<u16> + Send + Sync>>>
{
let source = source.as_ref();
let layer = if let Ok(Some(expr)) = source.expr() {
Self::compile_expr(expr.into())?
} else if let Ok(Some(word)) = source.word() {
Self::compile_word(word.into())?
Ok(if let Some(path) = config.as_ref().find_file() {
Config::load(config, std::fs::read_to_string(&path)?)?
} else {
return Err(format!("not word/expr:\n{source:?}").into())
};
Ok(Arc::new(Box::new(move|state, screen|layer(state, screen))))
}
fn compile_expr (expr: Arc<str>) ->
Usually<Arc<Box<dyn Fn(&App, &mut Tui)->Drawn<u16> + Send + Sync>>>
{
macro_rules! arg {
($src:expr, $head:expr, $index:expr, $name:expr) => {
$src.nth($index)?
.ok_or_else(||Box::<dyn Error>::from(format!(
"{}: no arg #{} ({}) in {}", $head, $index, $name, $src
)))?
};
}
Ok(Arc::new(if let Some(head) = expr.head()? && let Some(ns) = head.split('/').next() {
match ns {
"when" => {
let cond = Arc::from(arg!(expr, head, 1, "condition"));
let cond = move|state: &App|state.namespace(&cond)?.ok_or_else(||Box::<dyn Error>::from("when: no condition value"));
let thunk = Self::compile(arg!(expr, head, 2, "content"))?;
Self::boxed(move|state, screen|{
when(
cond(state)?,
draw(|screen|thunk(state, screen))
).draw(screen)
return Err(format!("config_init: not found").into())
})
},
"either" => {
let cond = Arc::from(arg!(expr, head, 1, "condition"));
let cond = move|state: &App|state.namespace(&cond)?.ok_or_else(||Box::<dyn Error>::from("either: no condition value"));
let a = Self::compile(arg!(expr, head, 2, "content A"))?;
let b = Self::compile(arg!(expr, head, 3, "content B"))?;
Self::boxed(move|state, screen|{
either(
cond(state)?,
draw(|screen|a(state, screen)),
draw(|screen|b(state, screen)),
).draw(screen)
})
},
"bsp" | "split" | "stack" => {
let split = head.split('/').skip(1).next();
let split = match split {
Some("n") => Split::North,
Some("s") => Split::South,
Some("e") => Split::East,
Some("w") => Split::West,
Some("a") => Split::Above,
Some("b") => Split::Below,
_ => return Err(format!("invalid split: {split:?}").into())
};
let a = Self::compile(arg!(expr, head, 1, "content A"))?;
let b = Self::compile(arg!(expr, head, 2, "content B"))?;
Self::boxed(move|state, screen|split.stack(
draw(|screen|a(state, screen)),
draw(|screen|b(state, screen)),
).draw(screen))
},
"bar" => {
let split = head.split('/').skip(1).next();
let split = match split {
Some("n") => Split::North,
Some("s") => Split::South,
Some("e") => Split::East,
Some("w") => Split::West,
Some("a") => Split::Above,
Some("b") => Split::Below,
_ => return Err(format!("invalid split: {split:?}").into())
};
let size = Arc::from(arg!(expr, head, 1, "size"));
let size = move|state: &App|state.namespace(&size)?.ok_or_else(||Box::<dyn Error>::from("bar: no size"));
let a = Self::compile(arg!(expr, head, 2, "content A"))?;
let b = Self::compile(arg!(expr, head, 3, "content B"))?;
Self::boxed(move|state, screen|split.bar(
size(state)?,
draw(|screen|a(state, screen)),
draw(|screen|b(state, screen)),
).draw(screen))
},
"align" => {
let azimuth = head.split('/').skip(1).next();
let azimuth = match azimuth {
Some("n") => Azimuth::N,
Some("s") => Azimuth::S,
Some("e") => Azimuth::E,
Some("w") => Azimuth::W,
Some("ne") => Azimuth::NE,
Some("se") => Azimuth::SE,
Some("nw") => Azimuth::NW,
Some("sw") => Azimuth::SW,
Some("c") => Azimuth::C,
Some("x") => Azimuth::X,
Some("y") => Azimuth::Y,
_ => return Err(format!("invalid azimuth: {azimuth:?}").into())
};
let thunk = Self::compile(arg!(expr, head, 1, "content"))?;
Self::boxed(move|state, screen|{
Align(
Some(azimuth),
draw(|screen|thunk(state, screen))
).draw(screen)
})
},
"full" | "fill" => {
let thunk = Self::compile(arg!(expr, head, 1, "content"))?;
match head.split('/').skip(1).next() {
Some("w") | Some("x") => Self::boxed(move|state, screen|{
Full::W(draw(|screen|thunk(state, screen))).draw(screen)
}),
Some("h") | Some("y") => Self::boxed(move|state, screen|{
Full::H(draw(|screen|thunk(state, screen))).draw(screen)
}),
Some("wh") | Some("xy") | None => Self::boxed(move|state, screen|{
Full::WH(draw(|screen|thunk(state, screen))).draw(screen)
}),
_ => unreachable!()
}
},
"exact" | "min" | "max" | "push" | "pull" | "pad" => {
match head.split('/').skip(1).next() {
Some("w") | Some("x") => {
let value = Arc::from(arg!(expr, head, 1, "value"));
let value = move|state: &App|state.namespace(&value);
let thunk = Self::compile(arg!(expr, head, 2, "content"))?;
match ns {
"exact" => Self::boxed(move|state, screen|Exact::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"push" => Self::boxed(move|state, screen|Push::X(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"pull" => Self::boxed(move|state, screen|Pull::X(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"pad" => Self::boxed(move|state, screen|Pad::X(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"min" => Self::boxed(move|state, screen|Min::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"max" => Self::boxed(move|state, screen|Max::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
_ => unreachable!()
}
},
Some("h") | Some("y") => {
let value = Arc::from(arg!(expr, head, 1, "value"));
let value = move|state: &App|state.namespace(&value);
let thunk = Self::compile(arg!(expr, head, 2, "content"))?;
match ns {
"exact" => Self::boxed(move|state, screen|Exact::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"push" => Self::boxed(move|state, screen|Push::Y(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"pull" => Self::boxed(move|state, screen|Pull::Y(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"pad" => Self::boxed(move|state, screen|Pad::Y(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"min" => Self::boxed(move|state, screen|Min::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"max" => Self::boxed(move|state, screen|Max::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
_ => unreachable!()
}
},
Some("wh") | Some("xy") | None => {
let value1 = Arc::from(arg!(expr, head, 1, "value"));
let value1 = move|state: &App|state.namespace(&value1);
let value2 = Arc::from(arg!(expr, head, 2, "value"));
let value2 = move|state: &App|state.namespace(&value2);
let thunk = Self::compile(arg!(expr, head, 3, "content"))?;
match ns {
"exact" => Self::boxed(move|state, screen|Exact::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
"push" => Self::boxed(move|state, screen|Push::XY(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
"pull" => Self::boxed(move|state, screen|Pull::XY(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
"pad" => Self::boxed(move|state, screen|Pad::XY(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
"min" => Self::boxed(move|state, screen|Min::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
"max" => Self::boxed(move|state, screen|Max::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
_ => unreachable!()
}
},
_ => unreachable!()
}
},
"fg" | "bg" => {
let color = Arc::from(arg!(expr, head, 1, "color"));
let color = move|state: &App|state.namespace(&color);
let thunk = Self::compile(arg!(expr, head, 2, "content"))?;
match ns {
"fg" => Self::boxed(move|state, screen|fg(
color(state)?.unwrap_or_default(), draw(|screen|thunk(state, screen))
).draw(screen)),
"bg" => Self::boxed(move|state, screen|bg(
color(state)?.unwrap_or_default(), draw(|screen|thunk(state, screen))
).draw(screen)),
_ => unreachable!()
}
},
"text" => {
let text: Arc<str> = Arc::from(expr.tail()?.unwrap_or_default());
Self::boxed(move|_state, screen|{
text.draw(screen)
})
},
_ => return Err(format!("compile_expr: unexpected: {expr:?}").into())
}
pub fn load <C: AsRef<Config>, L: Language> (config: C, src: L) -> Usually<C> {
config.as_ref().clear();
config.as_ref().stamp.store(quanta::Clock::new().raw(), Relaxed);
src.each(config, |c, s|Config::load_item(c, s.trim()))
}
pub fn load_item <C: AsRef<Config>, L: Language> (config: C, src: L) -> Usually<C> {
if let Some(expr) = src.expr()? {
Config::load_kind(config, expr)
} else {
return Err(format!("compile_expr: invalid expression: {expr:?}").into())
}))
Err(format!("Config::add_one: tried to add empty item").into())
}
}
fn compile_word (word: Arc<str>)
-> Usually<Arc<Box<dyn Fn(&App, &mut Tui)->Drawn<u16> + Send + Sync>>>
{
macro_rules! draw {
(|$state:ident|$body:expr)=>{Self::boxed(move|$state, to|$body.draw(to))}
pub fn load_kind <C: AsRef<Config>, L: Language> (config: C, src: L) -> Usually<C> {
//if let Some(tail) = src.tail()? {
match src.head()? {
Some("mode") => Modes::add(&config.as_ref().modes, src.tail()),
Some("keys") => binds_add(&config.as_ref().binds, src.tail()),
Some("view") => views_add(&config.as_ref().views, src.tail()),
_ => return Err(format!("Config::load: expected view/keys/mode expr, got: {src:?}").into())
}?;
//}
Ok(config)
}
Ok(Arc::new(match word.split("/").next() {
//Some(":logo") => view_logo().draw(to),
Some(":meters") => match word.split("/").skip(1).next() {
Some("input") => Self::boxed(move|_, to|bg(Rgb(30, 30, 30), "Input Meters".align_s().full_h()).draw(to)),
Some("output") => Self::boxed(move|_, to|bg(Rgb(30, 30, 30), "Output Meters".align_s().full_h()).draw(to)),
_ => panic!()
},
Some(":tracks") => match word.split("/").skip(1).next() {
None => Self::boxed(move|_, to|"TODO tracks".draw(to)),
Some("names") => draw!(|app|app.project.view_track_names(app.color.clone())),
Some("inputs") => draw!(|app|app.project.view_track_inputs(app.color.clone())),
Some("devices") => draw!(|app|app.project.view_track_devices(app.color.clone())),
Some("outputs") => draw!(|app|app.project.view_track_outputs(app.color.clone(), 0)),
_ => panic!()
},
Some(":scenes") => match word.split("/").skip(1).next() {
Some("clips") => draw!(|app|app.view_scenes_clips()),
Some("names") => draw!(|app|app.view_scenes_names()),
_ => panic!()
},
Some(":templates") => draw!(|app|view_templates(app)),
Some(":browse/title") => draw!(|app|view_browse_title(app)),
Some(":device") => draw!(|app|view_device(app)),
Some(":dialog") => Self::boxed(move|state, to|draw_dialog(to, word.split("/").skip(1), state)),
Some(":sessions") => Self::boxed(move|_, to|view_sessions().draw(to)),
Some(":status") => Self::boxed(move|state, to|draw_status_bar(to, state)),
Some(":editor") => Self::boxed(move|_, to|"TODO Editor".draw(to)),
Some(":transport") => Self::boxed(move|_, to|view_transport(true, "", "", "").draw(to)),
Some(":debug") => Self::boxed(move|_, to|format!("[{:?}]", to.area()).exact_h(1).draw(to)),
Some(_) => Self::boxed(move|state, to|if let Some(view) = state.config.get_view(word.as_ref()) {
(view.render)(state, to)
/// Watch a config's file for changes.
pub fn watch (
config: Arc<Config>, poll: Option<Duration>
) -> Usually<()> {
let poll = poll.unwrap_or(Duration::from_millis(250));
let opts = NotifyConfig::default().with_poll_interval(poll);
let mut watcher = ::notify_debouncer_full::notify::poll::PollWatcher::new({
let config = config.clone();
move|result|{
match result {
Ok(_events) => if let Err(e) = Config::init(config.as_ref()) {
*config.as_ref().error.try_write().unwrap() = Some(format!("{e:?}").into());
panic!("{e:?}");
} else {
fg(Color::Rgb(128, 32, 32), format!("undefined: {word:?}")).draw(to)
}),
_ => unreachable!()
}))
//println!("config updated");
},
Err(errors) => {
panic!("{errors:?}");
}
}
pub fn draw_status_bar (to: &mut Tui, state: &App) -> Drawn<u16> {
bar(Split::South, 1u16, dim(true, "Mode"), bold(true, &state.mode)).push_x(1).draw(to)
}
/// Load custom view definition.
pub fn views_add <'a> (views: &Views, expr: impl Language) -> UsuallyRef<'a, ()> {
views.try_write().unwrap().insert(
expr.head()?.ok_or("view: missing name")?.into(),
View::new(expr.tail()?.ok_or("view: missing body")?)?.into()
);
}
}
}, opts)?;
if let Some(path) = config.as_ref().get_file() {
//println!("watching: {path:?}");
watcher.watch(&path, RecursiveMode::NonRecursive)?;
*config.as_ref().watch.try_write().unwrap() = Some(watcher);
Ok(())
} else {
Err(format!("no config path").into())
}
}
}
impl Config {
@ -567,14 +140,14 @@ impl Config {
/// Create, initialize, and watch a new configuration.
pub fn watched <T: 'static> (callback: impl FnOnce(Arc<Config>)->T) -> Usually<T> {
let config = Self::init_new(None)?;
config_watch(config.clone(), None)?;
Config::watch(config.clone(), None)?;
let result = callback(config);
Ok(result)
}
/// Create and initialize a new configuration.
pub fn init_new (_dirs: Option<BaseDirectories>) -> Usually<Arc<Self>> {
config_init(Arc::new(Self::new(None)))
Config::init(Arc::new(Self::new(None)))
}
/// Find the config file.
@ -614,120 +187,3 @@ impl Config {
}
}
impl Modes {
/// Get a mode by name.
pub fn get (&self, name: impl AsRef<str>) -> Option<Arc<Mode>> {
self.0.try_read().unwrap().get(name.as_ref()).cloned()
}
/// Run something for each mode.
pub fn for_each <T> (&self, mut ator: impl FnMut(&str, &Mode)->T) {
for (k, v) in self.0.try_read().unwrap().iter() {
let _ = ator(k.as_ref(), v.as_ref());
}
}
/// Count modes.
pub fn len (&self) -> usize {
self.0.try_read().unwrap().len()
}
}
pub use self::bind::*;
mod bind {
use crate::*;
/// Collection of input bindings.
pub type Binds = Arc<RwLock<BTreeMap<Arc<str>, Bind<TuiEvent, Arc<str>>>>>;
/// An map of input events (e.g. [TuiEvent]) to [Binding]s.
///
/// ```
/// use ::{std::sync::Arc, tek::{Bind, tengri::TuiEvent}};
/// let bind = Bind::<TuiEvent, Arc<str>>::load(&stringify!(
/// (@x (nop))
/// (@y (nop) (nop)))).unwrap();
/// assert_eq!(bind.query(&'x'.into()).map(|x|x.len()), Some(1));
/// //assert_eq!(bind.query(&'y'.into()).map(|x|x.len()), Some(2));
/// ```
#[derive(Debug)]
pub struct Bind<E, C>(
/// Map of each event (e.g. key combination) to
/// all command expressions bound to it by
/// all loaded input layers.
pub BTreeMap<E, Vec<Binding<C>>>
);
/// A sequence of zero or more commands (e.g. [AppCommand]),
/// optionally filtered by [Condition] to form layers.
///
/// ```
/// //FIXME: Why does it overflow?
/// //let binding: Binding<()> = tek::Binding { ..Default::default() };
/// ```
#[derive(Debug, Clone)] pub struct Binding<C> {
pub commands: Arc<[C]>,
pub condition: Option<Condition>,
pub description: Option<Arc<str>>,
pub source: Option<Arc<PathBuf>>,
}
/// Condition that must evaluate to true in order to enable an input layer.
///
/// ```
/// let condition = tek::Condition(std::sync::Arc::new(Box::new(||{true})));
/// ```
#[derive(Clone)]
pub struct Condition(pub Arc<Box<dyn Fn()->bool + Send + Sync>>);
impl Bind<TuiEvent, Arc<str>> {
}
/// Default is always empty map regardless if `E` and `C` implement [Default].
impl<E, C> Default for Bind<E, C> {
fn default () -> Self { Self(Default::default()) }
}
impl<C: Default> Default for Binding<C> {
fn default () -> Self {
Self {
commands: Default::default(),
condition: Default::default(),
description: Default::default(),
source: Default::default(),
}
}
}
impl<E: Clone + Ord, C> Bind<E, C> {
/// Create a new event map
pub fn new () -> Self {
Default::default()
}
/// Add a binding to an owned event map.
pub fn def (mut self, event: E, binding: Binding<C>) -> Self {
self.add(event, binding);
self
}
/// Add a binding to an event map.
pub fn add (&mut self, event: E, binding: Binding<C>) -> &mut Self {
if !self.0.contains_key(&event) {
self.0.insert(event.clone(), Default::default());
}
self.0.get_mut(&event).unwrap().push(binding);
self
}
/// Return the binding(s) that correspond to an event.
pub fn query (&self, event: &E) -> Option<&[Binding<C>]> {
self.0.get(event).map(|x|x.as_slice())
}
/// Return the first binding that corresponds to an event, considering conditions.
pub fn dispatch (&self, event: &E) -> Option<&Binding<C>> {
self.query(event)
.map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next())
.flatten()
}
}
impl_debug!(Condition |self, w| { write!(w, "*") });
}

212
src/device.rs Normal file
View file

@ -0,0 +1,212 @@
use crate::*;
impl App {
/// FIXME: generalize. Set picked device in device pick dialog.
///
/// ```
/// tek::App::default().device_pick(0);
/// ```
pub fn device_pick (&mut self, index: usize) {
self.dialog = Dialog::Device(index);
}
/// FIXME: generalize. Add device to current track.
pub fn add_device (&mut self, index: usize) -> Usually<()> {
match index {
0 => {
let name = self.jack.with_client(|c|c.name().to_string());
let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name();
let track = self.track().expect("no active track");
let port = format!("{}/Sampler", &track.name);
let connect = Connect::exact(format!("{name}:{midi}"));
let sampler = if let Ok(sampler) = Sampler::new(
&self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]]
) {
self.dialog = Dialog::None;
Device::Sampler(sampler)
} else {
self.dialog = Dialog::Message("Failed to add device.".into());
return Err("failed to add device".into())
};
let track = self.track_mut().expect("no active track");
track.devices.push(sampler);
Ok(())
},
1 => {
todo!();
//Ok(())
},
_ => unreachable!(),
}
}
}
impl HasJack<'static> for App {
fn jack (&self) -> &Jack<'static> {
&self.jack
}
}
impl_audio!(App: tek_jack_process, tek_jack_event);
fn tek_jack_process (state: &mut App, client: &Client, scope: &ProcessScope) -> Control {
let t0 = state.perf.get_t0();
state.clock().update_from_scope(scope).unwrap();
let midi_in = state.project.midi_input_collect(scope);
if let Some(editor) = &state.editor() {
let mut pitch: Option<u7> = None;
for port in midi_in.iter() {
for event in port.iter() {
if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..}))
= event
{
pitch = Some(key.clone());
}
}
}
if let Some(pitch) = pitch {
editor.set_note_pos(pitch.as_int() as usize);
}
}
let result = state.project.tracks_jack_process(client, scope);
state.perf.update_from_jack_scope(t0, scope);
result
}
fn tek_jack_event (state: &mut App, event: JackEvent) {
use JackEvent::*;
match event {
SampleRate(sr) => { state.clock().timebase.sr.set(sr as f64); },
PortRegistration(_id, true) => {
//let port = self.jack().port_by_id(id);
//println!("\rport add: {id} {port:?}");
//println!("\rport add: {id}");
},
PortRegistration(_id, false) => {
/*println!("\rport del: {id}")*/
},
PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ },
PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ },
ClientRegistration(_id, true) => {},
ClientRegistration(_id, false) => {},
ThreadInit => {},
XRun => {},
GraphReorder => {},
_ => { panic!("{event:?}"); }
}
}
#[tek_proc::command(Device)]
#[tek_proc::keyword(Device)]
#[derive(Debug)]
pub enum DeviceCommand {}
impl Device {
pub fn name (&self) -> &str {
match self {
Self::Sampler(sampler) => sampler.name.as_ref(),
_ => todo!(),
}
}
pub fn midi_ins (&self) -> &[MidiInput] {
match self {
//Self::Sampler(Sampler { midi_in, .. }) => &[midi_in],
_ => todo!()
}
}
pub fn midi_outs (&self) -> &[MidiOutput] {
match self {
Self::Sampler(_) => &[],
_ => todo!()
}
}
pub fn audio_ins (&self) -> &[AudioInput] {
match self {
Self::Sampler(Sampler { audio_ins, .. }) => audio_ins.as_slice(),
_ => todo!()
}
}
pub fn audio_outs (&self) -> &[AudioOutput] {
match self {
Self::Sampler(Sampler { audio_outs, .. }) => audio_outs.as_slice(),
_ => todo!()
}
}
}
/// A device that can be plugged into the chain.
///
/// ```
/// let device = tek::Device::default();
/// ```
#[derive(Debug, Default)] pub enum Device {
#[default]
Bypass,
Mute,
#[cfg(feature = "sampler")]
Sampler(Sampler),
#[cfg(feature = "lv2")] // TODO
Lv2(Lv2),
#[cfg(feature = "vst2")] // TODO
Vst2,
#[cfg(feature = "vst3")] // TODO
Vst3,
#[cfg(feature = "clap")] // TODO
Clap,
#[cfg(feature = "sf2")] // TODO
Sf2,
}
/// Some sort of wrapper?
pub struct DeviceAudio<'a>(pub &'a mut Device);
impl_audio!(|self: DeviceAudio<'a>, client, scope|{
use Device::*;
match self.0 {
Mute => { Control::Continue },
Bypass => { /*TODO*/ Control::Continue },
#[cfg(feature = "sampler")] Sampler(sampler) => sampler.process(client, scope),
#[cfg(feature = "lv2")] Lv2(lv2) => lv2.process(client, scope),
#[cfg(feature = "vst2")] Vst2 => { todo!() }, // TODO
#[cfg(feature = "vst3")] Vst3 => { todo!() }, // TODO
#[cfg(feature = "clap")] Clap => { todo!() }, // TODO
#[cfg(feature = "sf2")] Sf2 => { todo!() }, // TODO
}
});
pub fn device_kinds () -> &'static [&'static str] {
&[
#[cfg(feature = "sampler")] "Sampler",
#[cfg(feature = "lv2")] "Plugin (LV2)",
]
}
impl<T: AsRef<Vec<Device>> + AsMut<Vec<Device>>> HasDevices for T {
fn devices (&self) -> &Vec<Device> {
self.as_ref()
}
fn devices_mut (&mut self) -> &mut Vec<Device> {
self.as_mut()
}
}
pub trait HasDevices: AsRef<Vec<Device>> + AsMut<Vec<Device>> {
fn devices (&self) -> &Vec<Device> {
self.as_ref()
}
fn devices_mut (&mut self) -> &mut Vec<Device> {
self.as_mut()
}
}
pub fn view_device <'a> (state: &'a App) -> impl Draw<Tui> {
let selected = state.dialog.device_kind().unwrap();
south(
bold(true, "Add device"),
iter_south(move||device_kinds().iter().enumerate().map(move|(i, _label)|{
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 { " " };
bg(b, east(l, west(r, "FIXME device name"))).full_w()
})))
}

View file

@ -1,27 +1,6 @@
use crate::{*, device::*};
use crate::*;
pub fn draw_dialog <'a, I: Debug + Iterator<Item = &'a str>> (to: &mut Tui, mut frags: I, state: &App)
-> Drawn<u16>
{
match frags.next() {
Some("menu") => if let Dialog::Menu(selected, items) = &state.dialog {
Some(iter_south(move||items.0.iter().enumerate().map(move|(index, MenuItem(item, _))|{
let f = if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) };
let b = if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) };
fg_bg(f, b, item).exact_h(2).align_x()
})).exact_w(20).align_x())
} else {
None
}.draw(to),
_ => unimplemented!("draw_dialog: ({frags:?})"),
}
}
impl App {
pub fn get_dialog <'a> (&self, src: impl Language) -> Perhaps<Dialog> {
pub fn get_dialog <'a> (_: &App, src: impl Language) -> Perhaps<Dialog> {
src.word()?.map(|word|Ok(match word {
":dialog/none" => Dialog::None,
":dialog/options" => Dialog::Options,
@ -51,8 +30,6 @@ impl App {
format!("not a dialog: {}", src.src()?.unwrap_or("<???>")).into()
)
})).transpose()
}
}
/// Various possible dialog modes.
@ -63,68 +40,75 @@ impl App {
#[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog {
#[default] None,
Help(usize),
Menu(usize, MenuItems),
Menu(usize, Arc<[MenuItem]>),
Device(usize),
Message(Arc<str>),
Browse(BrowseTarget, Arc<Browse>),
Options,
}
/// List of menu items.
///
/// ```
/// let items: tek::MenuItems = Default::default();
/// ```
#[derive(Debug, Clone, Default, PartialEq)] pub struct MenuItems(
pub Arc<[MenuItem]>
);
pub fn draw_dialog <'a, I: Debug + Iterator<Item = &'a str>> (to: &mut Tui, mut frags: I, state: &App)
-> Drawn<u16>
{
match frags.next() {
/// An item of a menu.
///
/// ```
/// let item: tek::MenuItem = Default::default();
/// ```
#[derive(Clone)] pub struct MenuItem(
/// Label
pub Arc<str>,
/// Callback
pub Arc<Box<dyn Fn(&mut App)->Usually<()> + Send + Sync>>
);
Some("menu") => if let Dialog::Menu(selected, items) = &state.dialog {
Some(iter_south(move||items.iter().enumerate().map(move|(index, MenuItem(item, _))|{
let f = if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) };
let b = if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) };
fg_bg(f, b, item).exact_h(2).align_x()
})).exact_w(20).align_x())
} else {
None
}.draw(to),
impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) });
impl_default!(MenuItem: Self("".into(), Arc::new(Box::new(|_|Ok(())))));
impl PartialEq for MenuItem { fn eq (&self, other: &Self) -> bool { self.0 == other.0 } }
impl AsRef<Arc<[MenuItem]>> for MenuItems { fn as_ref (&self) -> &Arc<[MenuItem]> { &self.0 } }
_ => unimplemented!("draw_dialog: ({frags:?})"),
}
}
impl Dialog {
/// ```
/// let _ = tek::Dialog::welcome();
/// ```
pub fn welcome () -> Self {
Self::Menu(1, MenuItems([
Self::Menu(1, Arc::new([
MenuItem(" Resume session ".into(), Arc::new(Box::new(|_app|{
Ok(())
}))),
MenuItem::new(" Resume session ", [
AppCommand::Project {
command: ProjectCommand::LoadLast
}
]),
MenuItem(" Create session ".into(), Arc::new(Box::new(|app|Ok({
app.dialog = Dialog::None;
app.mode = Some(":arranger".into());
})))),
MenuItem::new(" Create session ", [
AppCommand::Dialog {
dialog: Dialog::None
},
AppCommand::Mode {
command: ModeCommand::Reset {
modes: vec![":arranger".into()]
}
},
]),
MenuItem(" Load session ".into(), Arc::new(Box::new(|_|{
Ok(())
}))),
MenuItem::new(" Load session ", [
AppCommand::Dialog {
dialog: Dialog::Browse(
BrowseTarget::LoadProject,
Browse::new(None).unwrap().into()
)
//dialog: Dialog::Browse {
//title: "Load session".into(),
//filter: "\\.tek$",
//}
}
]),
MenuItem(" Exit ".into(), Arc::new(Box::new(|app|Ok({
app.exit.exit();
println!("Exited cleanly.")
})))),
MenuItem::new(" Exit ", [
AppCommand::Exit
]),
].into()))
]))
}
/// FIXME: generalize
@ -134,26 +118,29 @@ impl Dialog {
pub fn menu_selected (&self) -> Option<usize> {
if let Self::Menu(selected, _) = self { Some(*selected) } else { None }
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().menu_next();
/// ```
pub fn menu_next (&self) -> Self {
match self {
Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.0.len()), items.clone()),
Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.len()), items.clone()),
_ => Self::None
}
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().menu_prev();
/// ```
pub fn menu_prev (&self) -> Self {
match self {
Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.0.len()), items.clone()),
Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.len()), items.clone()),
_ => Self::None
}
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().device_kind();
@ -161,6 +148,7 @@ impl Dialog {
pub fn device_kind (&self) -> Option<usize> {
if let Self::Device(index) = self { Some(*index) } else { None }
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().device_kind_next();
@ -168,6 +156,7 @@ impl Dialog {
pub fn device_kind_next (&self) -> Option<usize> {
self.device_kind().map(|index|(index + 1) % device_kinds().len())
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().device_kind_prev();
@ -175,12 +164,16 @@ impl Dialog {
pub fn device_kind_prev (&self) -> Option<usize> {
self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1)))
}
/// FIXME: implement
pub fn message (&self) -> Option<&str> { todo!() }
/// FIXME: implement
pub fn browser (&self) -> Option<&Arc<Browse>> { todo!() }
/// FIXME: implement
pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() }
}
/// Increment a wrapping counter.

View file

@ -432,8 +432,3 @@ impl Draw<Tui> for MidiEditor {
self.size.of(&self.mode).draw(to)
}
}
mod octave; pub use self::octave::*;
mod piano; pub use self::piano::*;
mod point; pub use self::point::*;
mod range; pub use self::range::*;

81
src/field.rs Normal file
View file

@ -0,0 +1,81 @@
use crate::*;
pub fn field_h <'a, T: Screen> (
_theme: ItemTheme, _head: impl Draw<T>, _body: impl Draw<T>
) -> impl Draw<T> {
}
pub fn field_v <'a, T: Screen> (
_theme: ItemTheme, _head: impl Draw<T>, _body: impl Draw<T>
) -> impl Draw<T> {
}
pub fn view_sessions <'a> () -> impl Draw<Tui> {
let h = 6;
let w = Some(30);
let f = Rgb(224, 192, 128);
draw(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;
bg(b, fg(f, *name).align_w()).full_w().exact_h(h).push_y(y).draw(to)?;
}
Ok(Some(to.area().into()))
}).min_w(w).exact_h(h)
}
/// ```
/// 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 <'a> (bg: Color, fg: Color, content: impl Draw<Tui>)
-> impl Draw<Tui>
{
let left = fg_bg(bg, Reset, y_repeat("▐").exact_w(1));
let right = fg_bg(bg, Reset, y_repeat("▌").exact_w(1));
east(left, west(right, fg_bg(fg, bg, content)))
}
/// ```
/// let _ = tek::button_2("", "", true);
/// let _ = tek::button_2("", "", false);
/// ```
pub fn button_2 <'a> (key: impl Draw<Tui>, label: impl Draw<Tui>, hide: bool)
-> impl Draw<Tui>
{
let c1 = tui_orange();
let c2 = tui_g(0);
let c3 = tui_g(96);
let c4 = tui_g(255);
bold(true, fg_bg(c1, c2,
bar(Split::East, 1u16, fg(c2, "▐"),
bar(Split::East, 1u16, key,
bar(Split::East, 1u16, fg(c3, "▐"),
when(!hide, fg_bg(c4, c3, label)))))))
}
/// ```
/// let _ = tek::button_3("", "", "", true);
/// let _ = tek::button_3("", "", "", false);
/// ```
pub fn button_3 <'a> (
key: impl Draw<Tui>,
label: impl Draw<Tui>,
value: impl Draw<Tui>,
editing: bool,
) -> impl Draw<Tui> {
let c1 = tui_orange();
let c2 = tui_g(0);
let c3 = tui_g(96);
let c4 = tui_g(255);
let c5 = tui_g(128);
bold(true, bar(Split::East, 3u16,
fg_bg(c1, c2,
bar(Split::East, 1u16, fg(c2, "▐"),
bar(Split::East, 1u16, key, fg(if editing { c5 } else { c3 }, "▐")))),
east(
when(!editing, east(fg_bg(c4, c3, label), fg_bg(c5, c3, "▐"),)),
east(fg_bg(tui_g(224), c5, value), fg_bg(c5, Reset, "▌"), ))))
}

View file

@ -1,15 +1,7 @@
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.
#[derive(Debug)] #[cfg(feature = "lv2")] pub struct Lv2 {
#[derive(Debug)] pub struct Lv2 {
/// JACK client handle (needs to not be dropped for standalone mode to work).
pub jack: Jack<'static>,
pub name: Arc<str>,
@ -30,11 +22,6 @@ use crate::*;
pub lv2_ui_thread: Option<JoinHandle<()>>,
}
/// A LV2 plugin's X11 UI.
#[cfg(feature = "lv2_gui")] pub struct LV2PluginUI {
pub window: Option<Window>
}
impl_audio!(Lv2: lv2_jack_process);
impl Lv2 {
@ -183,32 +170,3 @@ fn draw_header (state: &Lv2, to: &mut Tui, x: u16, y: u16, w: u16) {
}
//Ok(Rect { x, y, width: w, height: 1 })
}
#[cfg(feature = "vst2")] impl<E: Engine> ::vst::host::Host for Plugin<E> {}
#[cfg(feature = "vst2")] fn set_vst_plugin <E: Engine> (
host: &Arc<Mutex<Plugin<E>>>, _path: &str
) -> Usually<PluginKind> {
let mut loader = ::vst::host::PluginLoader::load(
&std::path::Path::new("/nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lxvst/helm.so"),
host.clone()
)?;
Ok(PluginKind::VST2 {
instance: loader.instance()?
})
}
#[cfg(feature = "lv2_gui")]
pub fn run_lv2_ui (mut ui: LV2PluginUI) -> Usually<JoinHandle<()>> {
Ok(spawn(move||{
let event_loop = EventLoop::builder().with_x11().with_any_thread(true).build().unwrap();
event_loop.set_control_flow(ControlFlow::Wait);
event_loop.run_app(&mut ui).unwrap()
}))
}
#[cfg(feature = "lv2_gui")]
fn lv2_ui_instantiate (kind: &str) {
//let host = Suil
}

26
src/lv2_gui.rs Normal file
View file

@ -0,0 +1,26 @@
use crate::*;
use ::winit::{
application::ApplicationHandler,
event::WindowEvent,
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
window::{Window, WindowId},
platform::x11::EventLoopBuilderExtX11
};
/// A LV2 plugin's X11 UI.
pub struct LV2PluginUI {
pub window: Option<Window>
}
pub fn run_lv2_ui (mut ui: LV2PluginUI) -> Usually<JoinHandle<()>> {
Ok(spawn(move||{
let event_loop = EventLoop::builder().with_x11().with_any_thread(true).build().unwrap();
event_loop.set_control_flow(ControlFlow::Wait);
event_loop.run_app(&mut ui).unwrap()
}))
}
fn lv2_ui_instantiate (kind: &str) {
//let host = Suil
}

33
src/menu_item.rs Normal file
View file

@ -0,0 +1,33 @@
use crate::*;
/// An item of a menu.
///
/// ```
/// let item: tek::MenuItem = Default::default();
/// ```
#[derive(Clone)] pub struct MenuItem(
pub Arc<str>,
pub Arc<[AppCommand]>,
);
impl MenuItem {
pub fn new <const N: usize> (name: impl AsRef<str>, actions: [AppCommand; N]) -> Self {
Self(name.as_ref().into(), Arc::new(actions))
}
}
impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) });
impl_default!(MenuItem: Self::new("", []));
impl PartialEq for MenuItem {
fn eq (&self, other: &Self) -> bool {
self.0 == other.0
}
}
//impl AsRef<Arc<[MenuItem]>> for MenuItems {
//fn as_ref (&self) -> &Arc<[MenuItem]> {
//&self.0
//}
//}

View file

@ -60,6 +60,40 @@ pub fn to_rms (samples: &[f32]) -> f32 {
(sum / samples.len() as f32).sqrt()
}
/// ```
/// 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>
{
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, bg(c, ()).exact_wh(w, 1))
}
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)
}
#[cfg(test)] mod test_view_meter {
use super::*;
use proptest::prelude::*;

72
src/mode.rs Normal file
View file

@ -0,0 +1,72 @@
use crate::*;
/// Group of view and keys definitions.
///
/// ```
/// let mode = tek::Mode::default();
/// ```
#[derive(Default, Debug)]
pub struct Mode {
pub path: PathBuf,
pub name: Vec<Arc<str>>,
pub info: Vec<Arc<str>>,
pub view: Vec<Arc<View<Tui, App>>>,
pub keys: Vec<Arc<str>>,
pub modes: Modes,
}
impl Mode {
/// Add a definition to the mode.
///
/// Supported definitions:
///
/// - (name ...) -> name
/// - (info ...) -> description
/// - (keys ...) -> key bindings
/// - (mode ...) -> submode
/// - ... -> view
///
/// ```
/// let mut mode: tek::Mode = tek::Mode::add(tek::Mode::default(), "(name hello)").unwrap();
/// ```
pub fn add <T: AsMut<Mode>> (mut mode: T, dsl: impl Language) -> Usually<T> {
Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() {
//println!("Mode::add: {head} {:?}", expr.tail());
let tail = expr.tail()?.map(|x|x.trim()).unwrap_or("");
match head {
"mode" => {
let name = tail.head()?.ok_or("submode: missing name")?;
let body = tail.tail()?.ok_or("submode: missing body")?;
let submode = Mode::default();
let submode = body.each(submode, |c,s|Mode::add(c,s))?;
let modes = mode.as_mut().modes.clone();
modes.0.try_write().unwrap().insert(name.into(), Arc::new(submode));
mode
},
"keys" => {
tail.each(mode, |mut mode: T, expr: &str|{
mode.as_mut().keys.push(expr.trim().into());
Ok(mode)
})?
},
"name" => { mode.as_mut().name.push(tail.into()); mode },
"info" => { mode.as_mut().info.push(tail.into()); mode },
"view" => { mode.as_mut().view.push(View::new(tail)?.into()); mode },
_ => { mode.as_mut().view.push(View::new(tail)?.into()); mode },
}
} else if let Ok(Some(word)) = dsl.word() {
mode.as_mut().view.push(View::new(word)?.into());
mode
} else {
return Err(format!("Mode::add: unexpected: {dsl:?}").into());
})
}
}
impl AsMut<Mode> for Mode {
fn as_mut (&mut self) -> &mut Self {
self
}
}

46
src/mode_stack.rs Normal file
View file

@ -0,0 +1,46 @@
use crate::*;
pub trait HasModeStack {
fn modes (&self) -> RwLockReadGuard<Vec<Arc<str>>>;
fn modes_mut (&mut self) -> RwLockWriteGuard<Vec<Arc<str>>>;
}
impl HasModeStack for App {
fn modes (&self) -> RwLockReadGuard<Vec<Arc<str>>> {
self.modes.try_read().unwrap()
}
fn modes_mut (&mut self) -> RwLockWriteGuard<Vec<Arc<str>>> {
self.modes.try_write().unwrap()
}
}
impl<T> ModeController for T where T: HasModeStack
+ Namespace<Vec<Arc<str>>>
+ Namespace<Arc<str>>
+ Namespace<usize>
{}
#[tek_proc::commands(ModeCommand)]
pub trait ModeController: HasModeStack
+ Namespace<Vec<Arc<str>>>
+ Namespace<Arc<str>>
+ Namespace<usize>
{
#[command(Reset = "reset")]
fn mode_reset (&mut self, modes: Vec<Arc<str>>) -> Perhaps<ModeCommand> {
*self.modes_mut() = modes.into();
Ok(None)
}
#[command(Push = "push")]
fn mode_push (&mut self, mode: Arc<str>) -> Perhaps<ModeCommand> {
self.modes_mut().push(mode);
Ok(Some(ModeCommand::Pop))
}
#[command(Pop = "pop")]
fn mode_pop (&mut self) -> Perhaps<ModeCommand> {
let mode = self.modes_mut().pop();
Ok(mode.map(|mode|ModeCommand::Push { mode }))
}
}

45
src/modes.rs Normal file
View file

@ -0,0 +1,45 @@
use crate::*;
/// Collection of UI modes.
///
/// ```
/// let mut modes = tek::Modes::default();
/// assert_eq!(modes.len(), 0);
/// let _ = modes.add(&":foo", &"(mode (name Foo) (info Bar))");
/// assert_eq!(modes.len(), 1);
/// ```
#[derive(Default, Debug, Clone)]
pub struct Modes(
pub Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode>>>>
);
impl Modes {
/// Register a mode.
pub fn add <'a> (modes: &Modes, expr: impl Language) -> UsuallyRef<'a, ()> {
let name = expr.head()?.ok_or("mode: missing name")?;
let body = expr.tail()?.ok_or("mode: missing body")?;
let mode = Mode::default();
let mode = body.each(mode, |c, s|Mode::add(c, s))?;
modes.0.try_write().unwrap().insert(name.into(), Arc::new(mode));
Ok(())
}
/// Get a mode by name.
pub fn get (&self, name: impl AsRef<str>) -> Option<Arc<Mode>> {
self.0.try_read().unwrap().get(name.as_ref()).cloned()
}
/// Run something for each mode.
pub fn for_each <T> (&self, mut ator: impl FnMut(&str, &Mode)->T) {
for (k, v) in self.0.try_read().unwrap().iter() {
let _ = ator(k.as_ref(), v.as_ref());
}
}
/// Count modes.
pub fn len (&self) -> usize {
self.0.try_read().unwrap().len()
}
}

1
src/plugin.rs Normal file
View file

@ -0,0 +1 @@
use crate::*;

View file

@ -1,7 +1,7 @@
use crate::*;
def_sizes_iter!(PortsSizes => Arc<str>, [Connect]);
impl_has!(Vec<MidiInput>: |self: Arrangement| self.midi_ins);
impl_has!(Vec<MidiOutput>: |self: Arrangement| self.midi_outs);
impl_has!(Vec<MidiInput>: |self: Project| self.midi_ins);
impl_has!(Vec<MidiOutput>: |self: Project| self.midi_outs);
impl_has!(Vec<MidiInput>: |self: App|self.project.midi_ins);
impl_has!(Vec<MidiOutput>: |self: App|self.project.midi_outs);

View file

@ -1,12 +1,45 @@
use crate::*;
impl AsRef<Option<Project>> for App {
fn as_ref (&self) -> &Option<Project> {
todo!()
}
}
impl AsMut<Option<Project>> for App {
fn as_mut (&mut self) -> &mut Option<Project> {
todo!()
}
}
pub trait HasProject: AsRef<Option<Project>> + AsMut<Option<Project>> {}
impl<T: AsRef<Option<Project>> + AsMut<Option<Project>>> HasProject for T {}
impl<T: HasProject> ProjectController for T {}
#[tek_proc::commands(ProjectCommand)]
pub trait ProjectController: HasProject {
#[command(LoadLast = "load-last")]
fn load_last (&mut self) -> Perhaps<ProjectCommand> {
todo!()
}
#[command(Load = "load")]
fn load (&mut self) -> Perhaps<ProjectCommand> {
todo!()
}
}
/// Arranger.
///
/// ```
/// let arranger = tek::Arrangement::default();
/// let arranger = tek::Project::default();
/// ```
#[derive(Default, Debug)]
pub struct Arrangement {
pub struct Project {
/// JACK client handle.
pub jack: Jack<'static>,
/// Project name.
@ -46,7 +79,7 @@ pub struct Arrangement {
#[cfg(feature = "scene")] pub scene_scroll: usize,
}
impl Arrangement {
impl Project {
/// Create a new arrangement.
pub fn new (
@ -104,43 +137,15 @@ impl Arrangement {
}
}
/// Get the first sampler of the active track
#[cfg(feature = "sampler")]
impl Arrangement {
pub fn sampler (&self) -> Option<&Sampler> {
self.selected_track()?.sampler(0)
}
impl HasJack<'static> for Project { fn jack (&self) -> &Jack<'static> { &self.jack } }
impl_has!(Jack<'static>: |self: Project| self.jack);
impl_has!(Sizer: |self: Project| self.size);
impl_has!(Clock: |self: Project| self.clock);
impl_has!(Selection: |self: Project| self.selection);
impl_as_ref_opt!(MidiEditor: |self: Project| self.editor.as_ref());
impl_as_mut_opt!(MidiEditor: |self: Project| self.editor.as_mut());
/// Get the first sampler of the active track
#[cfg(feature = "sampler")]
pub fn sampler_mut (&mut self) -> Option<&mut Sampler> {
self.selected_track_mut()?.sampler_mut(0)
}
}
impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &self.jack } }
impl_has!(Jack<'static>: |self: Arrangement| self.jack);
impl_has!(Sizer: |self: Arrangement| self.size);
impl_has!(Clock: |self: Arrangement| self.clock);
impl_has!(Selection: |self: Arrangement| self.selection);
impl_as_ref_opt!(MidiEditor: |self: Arrangement| self.editor.as_ref());
impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut());
/// Define a type alias for iterators of sized items (columns).
macro_rules! def_sizes_iter {
($Type:ident => $($Item:ty),+) => {
pub trait $Type<'a>: Iterator<Item=(usize, $(&'a $Item,)+ usize, usize)> + Send + Sync + 'a {}
impl<'a, T: Iterator<Item=(usize, $(&'a $Item,)+ usize, usize)> + Send + Sync + 'a> $Type<'a> for T {}
}
}
mod clip; pub use self::clip::*;
mod scene; pub use self::scene::*;
mod select; pub use self::select::*;
mod port; pub use self::port::*;
mod track; pub use self::track::*;
pub(self) 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
) -> Perhaps<U> {
if *target == *value {
@ -152,7 +157,7 @@ pub(self) fn swap_value <T: Clone + PartialEq, U> (
}
}
pub(self) fn toggle_bool <U> (
pub fn toggle_bool <U> (
target: &mut bool, value: &Option<bool>, returned: impl Fn(Option<bool>)->U
) -> Perhaps<U> {
let mut value = value.unwrap_or(!*target);
@ -163,24 +168,3 @@ pub(self) fn toggle_bool <U> (
Ok(Some(returned(Some(value))))
}
}
//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|callback(index, track).full_h().align_y())
//}
//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 {
//bg(Reset, iter_east(||tracks()
//.map(move|(index, track, x1, x2): (usize, &'a Track, usize, usize)|{
//fg_bg(
//track.color.lightest.term,
//track.color.base.term,
//callback(index, track)
//).exact_w((x2 - x1) as u16)
//})).align_x())
//}

View file

@ -8,6 +8,18 @@ pub(crate) use symphonia::{
},
};
/// Get the first sampler of the active track
impl Project {
pub fn sampler (&self) -> Option<&Sampler> {
self.selected_track()?.sampler(0)
}
/// Get the first sampler of the active track
pub fn sampler_mut (&mut self) -> Option<&mut Sampler> {
self.selected_track_mut()?.sampler_mut(0)
}
}
pub trait HasSampler: AsRef<Sampler> + AsMut<Sampler> {
fn sampler (&self) -> &Sampler {
self.as_ref()

View file

@ -57,8 +57,8 @@ impl Scene {
impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt());
impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt());
impl_as_ref_opt!(Scene: |self: Arrangement| self.selected_scene());
impl_as_mut_opt!(Scene: |self: Arrangement| self.selected_scene_mut());
impl_as_ref_opt!(Scene: |self: Project| self.selected_scene());
impl_as_mut_opt!(Scene: |self: Project| self.selected_scene_mut());
impl<T: AsRefOpt<Scene> + AsMutOpt<Scene> + Send + Sync> HasScene for T {}
@ -123,7 +123,7 @@ pub trait SceneController: HasScene
pub type SceneWith<'a, T> = (usize, &'a Scene, usize, usize, T);
def_sizes_iter!(ScenesSizes => Scene);
impl_has!(Vec<Scene>: |self: Arrangement| self.scenes);
impl_has!(Vec<Scene>: |self: Project| self.scenes);
impl_as_ref!(Vec<Scene>: |self: App| self.project.as_ref());
impl_as_mut!(Vec<Scene>: |self: App| self.project.as_mut());
@ -271,7 +271,7 @@ impl ScenesView for App {
}
}
impl ScenesView for Arrangement {
impl ScenesView for Project {
fn h_scenes (&self) -> u16 {
(self.size.h() as u16).saturating_sub(20)
}
@ -311,7 +311,7 @@ pub trait HasSceneScroll: HasScenes {
fn scene_scroll (&self) -> usize;
}
impl HasSceneScroll for Arrangement {
impl HasSceneScroll for Project {
fn scene_scroll (&self) -> usize {
self.scene_scroll
}

View file

@ -1,7 +1,7 @@
use crate::*;
impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track());
impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut());
impl_as_ref_opt!(Track: |self: Project| self.selected_track());
impl_as_mut_opt!(Track: |self: Project| self.selected_track_mut());
impl_as_ref_opt!(Track: |self: App| self.project.as_ref_opt());
impl_as_mut_opt!(Track: |self: App| self.project.as_mut_opt());
impl <T: AsRef<Selection>+AsMut<Selection>> HasSelection for T {}

View file

@ -20,13 +20,13 @@
(bar/s 2 (align/x (bg (g 12) :logo))
:dialog/menu)))))
(keys :back (@escape (back)))
(keys :confirm (@enter (dialog confirm)))
(keys :back (@escape cancel))
(keys :confirm (@enter confirm))
(keys :axis/y (@up (axis (dec :y)))
(@down (axis (inc :y))))
(mode :arranger (name Arranger) (info Launch grid.)
(keys :clock :color :launch :scenes :tracks :global)
(keys :clock :color :launch :scenes :tracks :global :history :saveload)
(mode :editor (keys :editor))
(mode :dialog (keys :dialog))
(mode :message (keys :message))
@ -46,7 +46,7 @@
(bar/n 2 :tracks/inputs
(bar/e 16 :scenes/names :scenes/clips))))))))
(keys :global (see :history :saveload)
(keys :global
(@f1 (dialog :help))
(@f8 (dialog :options))
(@f10 (dialog :quit)))

View file

@ -1,6 +1,59 @@
#![allow(clippy::unit_arg)]
//#![feature(impl_trait_in_assoc_type)] // Used by EntriesIterator; TODO remove
/// Define a type alias for iterators of sized items (columns).
macro_rules! def_sizes_iter {
($Type:ident => $($Item:ty),+) => {
pub trait $Type<'a>: Iterator<Item=(usize, $(&'a $Item,)+ usize, usize)> + Send + Sync + 'a {}
impl<'a, T: Iterator<Item=(usize, $(&'a $Item,)+ usize, usize)> + Send + Sync + 'a> $Type<'a> for T {}
}
}
mod app; pub use self::app::*;
mod bind; pub use self::bind::*;
mod binds; pub use self::binds::*;
mod browse; pub use self::browse::*;
mod cli; pub use self::cli::*;
mod clip; pub use self::clip::*;
mod clock; pub use self::clock::*;
mod clock_view; pub use self::clock_view::*;
mod config; pub use self::config::*;
mod deps; pub use self::deps::*;
mod device; pub use self::device::*;
mod dialog; pub use self::dialog::*;
mod editor; pub use self::editor::*;
mod field; pub use self::field::*;
mod memo; pub use self::memo::*;
mod menu_item; pub use self::menu_item::*;
mod meter; pub use self::meter::*;
mod mix; pub use self::mix::*;
mod mode; pub use self::mode::*;
mod mode_stack; pub use self::mode_stack::*;
mod modes; pub use self::modes::*;
mod moment; pub use self::moment::*;
mod octave; pub use self::octave::*;
mod piano; pub use self::piano::*;
mod point; pub use self::point::*;
mod pool; pub use self::pool::*;
mod port; pub use self::port::*;
mod project; pub use self::project::*;
mod range; pub use self::range::*;
mod sampler; pub use self::sampler::*;
mod scene; pub use self::scene::*;
mod select; pub use self::select::*;
mod sequence; pub use self::sequence::*;
mod ticker; pub use self::ticker::*;
mod timebase; pub use self::timebase::*;
mod track; pub use self::track::*;
mod views; pub use self::views::*;
#[cfg(feature = "lv2")] mod lv2;
#[cfg(feature = "lv2")] pub use self::lv2::*;
#[cfg(feature = "lv2_gui")] mod lv2_gui;
#[cfg(feature = "lv2_gui")] pub use self::lv2_gui::*;
#[cfg(feature = "vst2")] mod vst2;
#[cfg(feature = "vst2")] pub use self::vst2::*;
/// Command-line entrypoint.
#[allow(unused)]
@ -16,891 +69,28 @@ fn main () -> Usually<()> {
Ok(())
}
pub use self::cli::*;
mod cli;
pub use self::config::*;
mod config;
pub use self::app::*;
mod app {
use crate::*;
primitive!(u8: try_to_u8);
primitive!(u16: try_to_u16);
primitive!(usize: try_to_usize);
primitive!(isize: try_to_isize);
impl_has!(Clock: |self: App|self.project.clock);
impl_has!(Dialog: |self: App|self.dialog);
impl_has!(Jack<'static>: |self: App|self.jack);
impl_has!(Pool: |self: App|self.pool);
impl_has!(Selection: |self: App|self.project.selection);
impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt());
impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt());
impl_has_clips!( |self: App|self.pool.clips);
/// Total application state.
///
/// ```
/// use tek::{HasTracks, HasScenes, TracksView, ScenesView};
/// let mut app = tek::App::default();
/// let _ = app.scenes_add_one(None, None).unwrap();
/// let _ = app.update_clock();
/// app.project.editor = Some(Default::default());
/// //let _: Vec<_> = app.project.inputs_with_sizes().collect();
/// //let _: Vec<_> = app.project.outputs_with_sizes().collect();
/// let _: Vec<_> = app.project.tracks_with_sizes().collect();
/// //let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect();
/// //let _: Vec<_> = app.scenes_with_colors(true, 10).collect();
/// //let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect();
/// let _ = app.project.w();
/// //let _ = app.project.w_sidebar();
/// //let _ = app.project.w_tracks_area();
/// let _ = app.project.h();
/// //let _ = app.project.h_tracks_area();
/// //let _ = app.project.h_inputs();
/// //let _ = app.project.h_outputs();
/// let _ = app.project.h_scenes();
/// ```
#[derive(Default, Debug)]
#[namespace(MidiClip)]
#[namespace(ItemColor)]
#[namespace(ItemTheme)]
#[namespace(isize)]
#[namespace(u8)]
#[namespace(Arc<[Connect]> get_arc_array_connect)]
#[namespace(Arc<str> get_arc_str)]
#[namespace(Color get_color)]
#[namespace(ControlAxis get_axis)]
#[namespace(Option<Arc<[Connect]>> get_opt_arc_array_connect)]
#[namespace(Option<Arc<str>> get_opt_arc_str)]
#[namespace(Option<ItemTheme> get_opt_itemtheme)]
#[namespace(Option<Vec<Option<Arc<RwLock<MidiClip>>>>> get_opt_vec_opt_arc_rwlock_midiclip)]
#[namespace(Option<u16> get_opt_u16)]
#[namespace(Option<usize> get_opt_usize)]
#[namespace(Option<bool> get_opt_bool)]
#[namespace(bool get_bool)]
#[namespace(u16 get_u16)]
#[namespace(PathBuf get_path_buf)]
#[namespace(usize get_usize)]
#[namespace(Dialog App::get_dialog)]
pub struct App {
/// Exit flag
pub exit: Exit,
/// Base color.
pub color: ItemTheme,
/// Must not be dropped for the duration of the process
pub jack: Jack<'static>,
/// Display size
pub size: Sizer,
/// Performance counter
pub perf: PerfModel,
/// Available view modes and input bindings
pub config: Arc<Config>,
/// Currently selected mode
pub mode: Option<Arc<str>>,
/// Undo history
pub history: Vec<(AppCommand, Option<AppCommand>)>,
/// Dialog overlay
pub dialog: Dialog,
/// Contains all recently created clips.
pub pool: Pool,
/// Contains the currently edited musical arrangement
pub project: Arrangement,
/// Error, if any
pub error: Arc<RwLock<Option<Arc<str>>>>,
#[cfg(feature = "prof2")]
/// Tracing guard
pub guard: Option<tracing_flame::FlushGuard<std::io::BufWriter<std::fs::File>>>,
}
impl App {
/// Create a new application instance from a backend, project, config, and mode
///
/// ```
/// let mut proj = tek::Arrangement::default();
/// proj.jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack");
/// let mut conf = tek::config_load(tek::Config::default(), "(mode hello)").unwrap();
/// let tek = tek::App::new(None, proj, conf.into(), "hello");
/// ```
pub fn new (
exit: Option<Exit>,
project: Arrangement,
config: Arc<Config>,
mode: impl AsRef<str>
) -> Self {
App {
exit: exit.unwrap_or_default(),
jack: project.jack.clone(),
color: ItemTheme::random(),
dialog: Dialog::welcome(),
mode: Some(mode.as_ref().into()),
config,
project,
..Default::default()
}
}
}
fn get_arc_str <'a> (_: &App, src: impl Language) -> Perhaps<Arc<str>> {
Ok(src.src()?.map(|x|x.into()))
}
fn get_u16 <'a> (state: &App, src: impl Language) -> Perhaps<u16> {
Ok(Some(match src.word()? {
Some(":w/sidebar") => state.project.w_sidebar(state.editor().is_some()),
Some(":h/sample-detail") => 6.max(state.size.h() as u16 * 3 / 9),
_ => return try_to_u16(src)
}))
}
fn get_usize <'a> (state: &App, src: impl Language) -> Perhaps<usize> {
Ok(Some(match src.word()? {
Some(":scene-count") => state.scenes().len(),
Some(":track-count") => state.tracks().len(),
Some(":device-kind") => state.dialog.device_kind().unwrap_or(0),
Some(":device-kind/next") => state.dialog.device_kind_next().unwrap_or(0),
Some(":device-kind/prev") => state.dialog.device_kind_prev().unwrap_or(0),
_ => return try_to_usize(src)
}))
}
fn get_bool <'a> (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()
}
#[allow(unused)]
fn get_selection <'a> (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 <'a> (_: &App, src: impl Language) -> Perhaps<Color> {
if let Some(expr) = src.expr()? {
match (expr.head()?, expr.tail()?) {
(Some("g"), Some(tail)) => {
let n = try_to_u8(tail.head().map_err(Into::into))?
.ok_or(LanguageError::domain("not gray"))?;
Ok(Some(Color::Rgb(n, n, n)))
},
(Some("rgb"), Some(tail)) => {
let r = try_to_u8(tail.head().map_err(Into::into))?
.ok_or(LanguageError::domain("not red"))?;
let g = try_to_u8(tail.tail().head().map_err(Into::into))?
.ok_or(LanguageError::domain("not green"))?;
let b = try_to_u8(tail.tail().tail().head().map_err(Into::into))?
.ok_or(LanguageError::domain("not blue"))?;
Ok(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 if let Ok(Some(sym)) = src.word() {
Ok(match sym {
":color/bg" => Some(Color::Rgb(28, 32, 36)),
":color/fg" => Some(Color::Rgb(98, 92, 96)),
_ => return Err(format!("not a color: {sym}").into())
})
} else {
return Err(format!("not a color: {:?}", src.src()?).into())
}
}
#[allow(unused)]
fn get_opt_u7 <'a> (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_usize <'a> (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()
}
#[allow(unused)]
fn get_clip <'a> (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()
}
fn get_axis <'a> (_: &App, src: impl Language) -> Perhaps<ControlAxis> {
Ok(src.word()?.map(|word|Ok(match word {
":x" => ControlAxis::X,
":y" => ControlAxis::Y,
_ => return Err(format!("unknown axis {word}"))
})).transpose()?)
}
fn get_opt_u16 <'a> (_: &App, _src: impl Language) -> Perhaps<Option<u16>> {
Ok(None)
}
fn get_opt_itemtheme <'a> (_: &App, _: impl Language) -> Perhaps<Option<ItemTheme>> {
todo!()
}
fn get_opt_vec_opt_arc_rwlock_midiclip <'a> (_: &App, _: impl Language) -> Perhaps<Option<Vec<Option<Arc<RwLock<MidiClip>>>>>> {
todo!()
}
fn get_opt_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps<Option<Arc<[Connect]>>> {
todo!()
}
fn get_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps<Arc<[Connect]>> {
todo!()
}
fn get_opt_arc_str <'a> (_: &App, _: impl Language) -> Perhaps<Option<Arc<str>>> {
todo!()
}
fn get_opt_bool <'a> (_: &App, _: impl Language) -> Perhaps<Option<bool>> {
todo!()
}
fn get_path_buf <'a> (_: &App, _: impl Language) -> Perhaps<PathBuf> {
todo!()
}
}
pub use self::bind::*;
mod bind {
use crate::*;
tui_keys!(self: App, input {
#[cfg(feature = "prof2")] profiling::scope!("App::tui_keys!");
let name = self.mode.as_ref();
let mode = name.and_then(|m|self.config.get_mode(m)).as_ref().map(Arc::clone);
if let Some(mode) = mode {
let binds = self.config.binds.clone();
for id in mode.keys.iter() {
if let Some(event_map) = binds.try_read().unwrap().get(id.as_ref())
&& let Some(bindings) = event_map.query(input) {
for binding in bindings {
for command in binding.commands.iter() {
if Self::command::<AppCommand>(self, command)? {
break
}
}
}
}
}
}
Ok(())
});
impl App {
fn command <
T: Dispatch<Self> + Clone + Into<AppCommand>
> (state: &mut Self, src: &str) -> Usually<bool> where
Self: Namespace<T>,
{
if let Some(command) = Namespace::<T>::namespace(state, &src)? {
match command.clone().dispatch(state) {
Err(err) => {
state.history.push((command.into(), None));
return Err(err)
},
Ok(undo) => {
state.history.push((command.into(), undo.map(Into::into)));
return Ok(true)
}
};
}
Ok(false)
}
}
impl_from!(AppCommand: |x: AxisCommand| AppCommand::Axis { command: x });
impl_from!(AppCommand: |x: BrowseCommand| AppCommand::Browse { command: x });
impl_from!(AppCommand: |x: DialogCommand| AppCommand::Dialog { command: x });
impl_from!(AppCommand: |x: MidiClipCommand| AppCommand::MidiClip { command: x });
impl_from!(AppCommand: |x: PoolCommand| AppCommand::Pool { command: x });
impl_from!(AppCommand: |x: SceneCommand| AppCommand::Scene { command: x });
impl_from!(AppCommand: |x: ScenesCommand| AppCommand::Scenes { command: x });
impl_from!(AppCommand: |x: TrackCommand| AppCommand::Track { command: x });
impl_from!(AppCommand: |x: TracksCommand| AppCommand::Tracks { command: x });
#[tek_proc::commands(AppCommand)]
impl App {
#[command(Axis = "axis")]
pub fn command_axis (&mut self, command: AxisCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Browse = "browse")]
pub fn command_browse (&mut self, command: BrowseCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Dialog = "dialog")]
pub fn command_dialog (&mut self, command: DialogCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(MidiClip = "clip")]
pub fn command_midi_clip (&mut self, command: MidiClipCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Pool = "pool")]
pub fn command_pool (&mut self, command: PoolCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Scene = "scene")]
pub fn command_scene (&mut self, command: SceneCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Scenes = "scenes")]
pub fn command_scenes (&mut self, command: ScenesCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Track = "track")]
pub fn command_track (&mut self, command: TrackCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
#[command(Tracks = "tracks")]
pub fn command_tracks (&mut self, command: TracksCommand) -> Perhaps<AppCommand> {
Ok(command.dispatch(self)?.map(Into::into))
}
}
#[tek_proc::commands(DialogCommand)]
impl App {
/// Cancel current dialog
#[command(Cancel = "cancel")]
pub fn cancel (&mut self) -> Perhaps<DialogCommand> {
todo!()
}
/// Confirm current dialog selection.
#[command(Confirm = "confirm")]
pub fn confirm (&mut self) -> Perhaps<DialogCommand> {
Ok(match &self.dialog {
Dialog::Menu(index, items) => {
let callback = items.0[*index].1.clone();
callback(self)?;
None
},
_ => todo!(),
})
}
/// Swap currently active modal dialog.
///
/// ```
/// let _ = tek::App::default().set_dialog(tek::Dialog::welcome());
/// ```
#[command(SetDialog = "seg")]
pub fn set_dialog (&mut self, dialog: Dialog) -> Perhaps<DialogCommand> {
let mut dialog = dialog.clone();
std::mem::swap(&mut self.dialog, &mut dialog);
Ok(Some(DialogCommand::SetDialog { dialog }))
}
}
#[tek_proc::commands(AxisCommand)]
impl App {
/// Increment a given data axis.
#[command(Inc = "inc")]
pub fn inc (&mut self, axis: ControlAxis) -> Perhaps<AxisCommand> {
Ok(match (&self.dialog, axis) {
(Dialog::None, _) => todo!(),
(Dialog::Menu(_, _), ControlAxis::Y) => {
DialogCommand::SetDialog { dialog: self.dialog.menu_next() }.dispatch(self)?;
None
},
_ => todo!()
})
}
/// Decrement a given data axis.
#[command(Dec = "dec")]
pub fn dec (&mut self, axis: ControlAxis) -> Perhaps<AxisCommand> {
Ok(match (&self.dialog, axis) {
(Dialog::None, _) => None,
(Dialog::Menu(_, _), ControlAxis::Y) => {
DialogCommand::SetDialog { dialog: self.dialog.menu_prev() }.dispatch(self)?;
None
},
_ => todo!()
})
}
}
/// A control axis.
///
/// ```
/// let axis = tek::ControlAxis::X;
/// ```
#[derive(Debug, Copy, Clone)] pub enum ControlAxis {
X, Y, Z, I
}
//take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref()
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));
}
pub use self::device::*;
mod device {
use crate::*;
impl App {
/// FIXME: generalize. Set picked device in device pick dialog.
///
/// ```
/// tek::App::default().device_pick(0);
/// ```
pub fn device_pick (&mut self, index: usize) {
self.dialog = Dialog::Device(index);
}
/// FIXME: generalize. Add device to current track.
pub fn add_device (&mut self, index: usize) -> Usually<()> {
match index {
0 => {
let name = self.jack.with_client(|c|c.name().to_string());
let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name();
let track = self.track().expect("no active track");
let port = format!("{}/Sampler", &track.name);
let connect = Connect::exact(format!("{name}:{midi}"));
let sampler = if let Ok(sampler) = Sampler::new(
&self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]]
) {
self.dialog = Dialog::None;
Device::Sampler(sampler)
} else {
self.dialog = Dialog::Message("Failed to add device.".into());
return Err("failed to add device".into())
};
let track = self.track_mut().expect("no active track");
track.devices.push(sampler);
Ok(())
},
1 => {
todo!();
//Ok(())
},
_ => unreachable!(),
}
}
}
impl HasJack<'static> for App {
fn jack (&self) -> &Jack<'static> {
&self.jack
}
}
impl_audio!(App: tek_jack_process, tek_jack_event);
fn tek_jack_process (state: &mut App, client: &Client, scope: &ProcessScope) -> Control {
let t0 = state.perf.get_t0();
state.clock().update_from_scope(scope).unwrap();
let midi_in = state.project.midi_input_collect(scope);
if let Some(editor) = &state.editor() {
let mut pitch: Option<u7> = None;
for port in midi_in.iter() {
for event in port.iter() {
if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..}))
= event
{
pitch = Some(key.clone());
}
}
}
if let Some(pitch) = pitch {
editor.set_note_pos(pitch.as_int() as usize);
}
}
let result = state.project.tracks_jack_process(client, scope);
state.perf.update_from_jack_scope(t0, scope);
result
}
fn tek_jack_event (state: &mut App, event: JackEvent) {
use JackEvent::*;
match event {
SampleRate(sr) => { state.clock().timebase.sr.set(sr as f64); },
PortRegistration(_id, true) => {
//let port = self.jack().port_by_id(id);
//println!("\rport add: {id} {port:?}");
//println!("\rport add: {id}");
},
PortRegistration(_id, false) => {
/*println!("\rport del: {id}")*/
},
PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ },
PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ },
ClientRegistration(_id, true) => {},
ClientRegistration(_id, false) => {},
ThreadInit => {},
XRun => {},
GraphReorder => {},
_ => { panic!("{event:?}"); }
}
}
#[tek_proc::command(Device)]
#[tek_proc::keyword(Device)]
#[derive(Debug)]
pub enum DeviceCommand {}
impl Device {
pub fn name (&self) -> &str {
match self {
Self::Sampler(sampler) => sampler.name.as_ref(),
_ => todo!(),
}
}
pub fn midi_ins (&self) -> &[MidiInput] {
match self {
//Self::Sampler(Sampler { midi_in, .. }) => &[midi_in],
_ => todo!()
}
}
pub fn midi_outs (&self) -> &[MidiOutput] {
match self {
Self::Sampler(_) => &[],
_ => todo!()
}
}
pub fn audio_ins (&self) -> &[AudioInput] {
match self {
Self::Sampler(Sampler { audio_ins, .. }) => audio_ins.as_slice(),
_ => todo!()
}
}
pub fn audio_outs (&self) -> &[AudioOutput] {
match self {
Self::Sampler(Sampler { audio_outs, .. }) => audio_outs.as_slice(),
_ => todo!()
}
}
}
/// A device that can be plugged into the chain.
///
/// ```
/// let device = tek::Device::default();
/// ```
#[derive(Debug, Default)] pub enum Device {
#[default]
Bypass,
Mute,
#[cfg(feature = "sampler")]
Sampler(Sampler),
#[cfg(feature = "lv2")] // TODO
Lv2(Lv2),
#[cfg(feature = "vst2")] // TODO
Vst2,
#[cfg(feature = "vst3")] // TODO
Vst3,
#[cfg(feature = "clap")] // TODO
Clap,
#[cfg(feature = "sf2")] // TODO
Sf2,
}
/// Some sort of wrapper?
pub struct DeviceAudio<'a>(pub &'a mut Device);
impl_audio!(|self: DeviceAudio<'a>, client, scope|{
use Device::*;
match self.0 {
Mute => { Control::Continue },
Bypass => { /*TODO*/ Control::Continue },
#[cfg(feature = "sampler")] Sampler(sampler) => sampler.process(client, scope),
#[cfg(feature = "lv2")] Lv2(lv2) => lv2.process(client, scope),
#[cfg(feature = "vst2")] Vst2 => { todo!() }, // TODO
#[cfg(feature = "vst3")] Vst3 => { todo!() }, // TODO
#[cfg(feature = "clap")] Clap => { todo!() }, // TODO
#[cfg(feature = "sf2")] Sf2 => { todo!() }, // TODO
}
});
pub fn device_kinds () -> &'static [&'static str] {
&[
#[cfg(feature = "sampler")] "Sampler",
#[cfg(feature = "lv2")] "Plugin (LV2)",
]
}
impl<T: AsRef<Vec<Device>> + AsMut<Vec<Device>>> HasDevices for T {
fn devices (&self) -> &Vec<Device> {
self.as_ref()
}
fn devices_mut (&mut self) -> &mut Vec<Device> {
self.as_mut()
}
}
pub trait HasDevices: AsRef<Vec<Device>> + AsMut<Vec<Device>> {
fn devices (&self) -> &Vec<Device> {
self.as_ref()
}
fn devices_mut (&mut self) -> &mut Vec<Device> {
self.as_mut()
}
}
pub fn view_device <'a> (state: &'a App) -> impl Draw<Tui> {
let selected = state.dialog.device_kind().unwrap();
south(
bold(true, "Add device"),
iter_south(move||device_kinds().iter().enumerate().map(move|(i, _label)|{
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 { " " };
bg(b, east(l, west(r, "FIXME device name"))).full_w()
})))
}
pub mod arrange; pub use self::arrange::*;
pub mod browse; pub use self::browse::*;
pub mod pool; pub use self::pool::*;
pub mod clock; pub use self::clock::*;
pub mod dialog; pub use self::dialog::*;
pub mod editor; pub use self::editor::*;
pub mod meter; pub use self::meter::*;
pub mod mix; pub use self::mix::*;
pub mod sampler; pub use self::sampler::*;
pub mod sequence; pub use self::sequence::*;
#[cfg(feature = "plugin")] pub mod plugin;
#[cfg(feature = "plugin")] pub use self::plugin::*;
}
pub use self::draw::*;
mod draw {
use crate::*;
/// The [Draw] implementation for [App] handles the loaded view,
/// which is defined in terms of [dizzle] DSL.
///
/// If there is an error, the error is displayed. FIXME: overlay it
/// Then, every top-level form of the DSL description is rendered.
impl Draw<Tui> for App {
fn draw (&self, to: &mut Tui) -> Drawn<u16> {
#[cfg(feature = "prof2")] profiling::scope!("App::draw");
//self.perf.cycle(&mut |_|{
self.draw_error(to)?;
self.draw_modes(to)?;
self.draw_debug(to)?;
#[cfg(feature = "prof2")] profiling::finish_frame!();
Ok(Some(to.area().into()))
//})
}
}
impl App {
fn draw_error (&self, to: &mut Tui) -> Usually<()> {
if let Some(e) = self.error.try_read().unwrap().as_ref() {
e.as_ref().align_c().draw(to)?;
}
Ok(())
}
fn draw_modes (&self, to: &mut Tui) -> Usually<()> {
if let Some(mode) = self.mode.as_ref().and_then(|m|self.config.get_mode(m)) {
let mut error = false;
for (index, view) in mode.view.iter().enumerate() {
match (view.render)(self, to) {
Ok(None) => {},
Ok(Some(XYWH(.., w, h))) => {
self.size.0.store(w as usize, Relaxed);
self.size.1.store(h as usize, Relaxed);
},
Err(e) => {
let message = format!(
"Mode: {:?}\n\nLayer: #{index}\n\nError: {e}\n\nSource:\n{}",
&mode.name,
&view.source
);
*self.error.try_write().unwrap() = Some(message.into());
error = true;
break;
}
}
}
if !error {
*self.error.try_write().unwrap() = None;
}
}
Ok(())
}
#[allow(unused)] fn draw_debug (&self, to: &mut Tui) -> Drawn<u16> {
east(
format!("{}x{} ",
self.size.0.load(Relaxed),
self.size.1.load(Relaxed)),
format!("{}/{} {} ",
self.perf.used.load(Relaxed),
self.perf.window.load(Relaxed),
self.perf.clock.raw() / 10000000),
).align_se().draw(to)
}
}
impl_has!(Sizer: |self: App|self.size);
pub trait HasWidth {
const MIN_WIDTH: usize;
/// Increment track width.
fn width_inc (&mut self);
/// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH].
fn width_dec (&mut self);
}
pub fn view_templates <'a> (state: &'a App) -> impl Draw<Tui> + use<'a> {
let height = (state.config.modes.len() * 2) as u16;
draw(move |to: &mut Tui|{
let mut index = 0;
state.config.modes.for_each(&mut |id: &str, profile: &Mode| {
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 info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or("<no info>");
let fg1 = Rgb(224, 192, 128);
let fg2 = Rgb(224, 128, 32);
let field_name = fg(fg1, name).align_w().full_w();
let field_id = fg(fg2, id).align_e().full_w();
let field_info = info.align_w().full_w();
let _ = bg(b, south(above(field_name, field_id), field_info))
.full_w().exact_h(2).push_y((2 * index) as u16).draw(to);
index += 1;
});
Ok(Some(to.area().into()))
}).min_w(30).exact_h(height)
}
pub fn field_h <'a, T: Screen> (
_theme: ItemTheme, _head: impl Draw<T>, _body: impl Draw<T>
) -> impl Draw<T> {
}
pub fn field_v <'a, T: Screen> (
_theme: ItemTheme, _head: impl Draw<T>, _body: impl Draw<T>
) -> impl Draw<T> {
}
pub fn view_sessions <'a> () -> impl Draw<Tui> {
let h = 6;
let w = Some(30);
let f = Rgb(224, 192, 128);
draw(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;
bg(b, fg(f, *name).align_w()).full_w().exact_h(h).push_y(y).draw(to)?;
}
Ok(Some(to.area().into()))
}).min_w(w).exact_h(h)
}
/// ```
/// 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 <'a> (bg: Color, fg: Color, content: impl Draw<Tui>)
-> impl Draw<Tui>
{
let left = fg_bg(bg, Reset, y_repeat("▐").exact_w(1));
let right = fg_bg(bg, Reset, y_repeat("▌").exact_w(1));
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>
{
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, bg(c, ()).exact_wh(w, 1))
}
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_track_header <'a> (theme: ItemTheme, content: impl Draw<Tui>)
-> impl Draw<Tui>
{
bg(theme.darker.term, content.align_e().full_w()).exact_w(12)
}
/// ```
/// let _ = tek::button_2("", "", true);
/// let _ = tek::button_2("", "", false);
/// ```
pub fn button_2 <'a> (key: impl Draw<Tui>, label: impl Draw<Tui>, hide: bool)
-> impl Draw<Tui>
{
let c1 = tui_orange();
let c2 = tui_g(0);
let c3 = tui_g(96);
let c4 = tui_g(255);
bold(true, fg_bg(c1, c2,
bar(Split::East, 1u16, fg(c2, "▐"),
bar(Split::East, 1u16, key,
bar(Split::East, 1u16, fg(c3, "▐"),
when(!hide, fg_bg(c4, c3, label)))))))
}
/// ```
/// let _ = tek::button_3("", "", "", true);
/// let _ = tek::button_3("", "", "", false);
/// ```
pub fn button_3 <'a> (
key: impl Draw<Tui>,
label: impl Draw<Tui>,
value: impl Draw<Tui>,
editing: bool,
) -> impl Draw<Tui> {
let c1 = tui_orange();
let c2 = tui_g(0);
let c3 = tui_g(96);
let c4 = tui_g(255);
let c5 = tui_g(128);
bold(true, bar(Split::East, 3u16,
fg_bg(c1, c2,
bar(Split::East, 1u16, fg(c2, "▐"),
bar(Split::East, 1u16, key, fg(if editing { c5 } else { c3 }, "▐")))),
east(
when(!editing, east(fg_bg(c4, c3, label), fg_bg(c5, c3, "▐"),)),
east(fg_bg(tui_g(224), c5, value), fg_bg(c5, Reset, "▌"), ))))
}
#[cfg(not(feature = "cli"))]
fn run_new_plain (config: Config) -> Usually<()> {
let name = "tek";
tengri::Tui::run_main(Jack::new_run(name, move|jack|{
let mode = ":menu";
let title = "untitled!";
let bpm = 74.;
let clock = Clock::new(&jack, Some(bpm))?;
let tracks = [];
let scenes = [];
Ok(App::new(None, Project::new(
&jack,
title.into(),
clock,
tracks.into_iter(),
scenes.into_iter(),
connect_midi_ins(&jack, &"M", &[], None)?.into_iter(),
connect_midi_outs(&jack, &"M", &[], None)?.into_iter(),
[].into_iter().chain(connect_audio_ins(&jack, &"L", &[], None)?.into_iter())
.chain(connect_audio_ins(&jack, &"R", &[], None)?.into_iter()),
[].into_iter().chain(connect_audio_outs(&jack, &"L", &[], None)?.into_iter())
.chain(connect_audio_outs(&jack, &"R", &[], None)?.into_iter()),
), config, mode))
})?)
}

View file

@ -1,4 +1,4 @@
use crate::{*, arrange::*};
use crate::*;
/// A track consists of a sequencer and zero or more devices chained after it.
///
@ -155,7 +155,7 @@ pub(crate) fn track_width (_index: usize, track: &Track) -> u16 {
}
def_sizes_iter!(TracksSizes => Track);
impl_has!(Vec<Track>: |self: Arrangement| self.tracks);
impl_has!(Vec<Track>: |self: Project| self.tracks);
impl_as_ref!(Vec<Track>: |self: App| self.project.as_ref());
impl_as_mut!(Vec<Track>: |self: App| self.project.as_mut());
@ -626,7 +626,7 @@ pub trait HasTrackScroll {
fn tracks_last_mut (&mut self) -> &mut usize;
}
impl HasTrackScroll for &mut Arrangement {
impl HasTrackScroll for &mut Project {
fn track_scroll (&self) -> usize {
self.track_scroll
}
@ -638,7 +638,7 @@ impl HasTrackScroll for &mut Arrangement {
}
}
impl HasTrackScroll for Arrangement {
impl HasTrackScroll for Project {
fn track_scroll (&self) -> usize {
self.track_scroll
}
@ -680,3 +680,30 @@ fn view_track_row_section <'a> (
)
)
}
pub fn view_track_header <'a> (theme: ItemTheme, content: impl Draw<Tui>)
-> impl Draw<Tui>
{
bg(theme.darker.term, content.align_e().full_w()).exact_w(12)
}
//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|callback(index, track).full_h().align_y())
//}
//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 {
//bg(Reset, iter_east(||tracks()
//.map(move|(index, track, x1, x2): (usize, &'a Track, usize, usize)|{
//fg_bg(
//track.color.lightest.term,
//track.color.base.term,
//callback(index, track)
//).exact_w((x2 - x1) as u16)
//})).align_x())
//}

304
src/views.rs Normal file
View file

@ -0,0 +1,304 @@
use crate::*;
/// Collection of custom view definitions.
pub type Views = Arc<RwLock<BTreeMap<Arc<str>, Arc<View<Tui, App>>>>>;
/// Custom view definition is a boxed closure emitting a [Draw]able from state `S`.
pub struct View<S: Screen, T> {
pub source: Arc<str>,
pub render: Arc<Box<dyn Fn(&T, &mut S)->Drawn<S::Unit> + Send + Sync>>
}
impl_debug!(<S: Screen, T> View<S, T> |self, w| { write!(w, "View({})", self.source) });
impl_display!(<S: Screen, T> View<S, T> |self, w| { write!(w, "View({})", self.source) });
impl View<Tui, App> {
pub fn new (source: impl AsRef<str>) -> Usually<Self> {
Ok(Self {
source: source.as_ref().into(),
render: Self::compile(source)?
})
}
fn boxed <F: Fn(&App, &mut Tui)->Drawn<u16> + Send + Sync + 'static> (f: F)
-> Box<dyn Fn(&App, &mut Tui)->Drawn<u16> + Send + Sync + 'static>
{
Box::new(f)
}
fn compile (source: impl AsRef<str>) ->
Usually<Arc<Box<dyn Fn(&App, &mut Tui)->Drawn<u16> + Send + Sync>>>
{
let source = source.as_ref();
let layer = if let Ok(Some(expr)) = source.expr() {
Self::compile_expr(expr.into())?
} else if let Ok(Some(word)) = source.word() {
Self::compile_word(word.into())?
} else {
return Err(format!("not word/expr:\n{source:?}").into())
};
Ok(Arc::new(Box::new(move|state, screen|layer(state, screen))))
}
fn compile_expr (expr: Arc<str>) ->
Usually<Arc<Box<dyn Fn(&App, &mut Tui)->Drawn<u16> + Send + Sync>>>
{
macro_rules! arg {
($src:expr, $head:expr, $index:expr, $name:expr) => {
$src.nth($index)?
.ok_or_else(||Box::<dyn Error>::from(format!(
"{}: no arg #{} ({}) in {}", $head, $index, $name, $src
)))?
};
}
Ok(Arc::new(if let Some(head) = expr.head()? && let Some(ns) = head.split('/').next() {
match ns {
"when" => {
let cond = Arc::from(arg!(expr, head, 1, "condition"));
let cond = move|state: &App|state.namespace(&cond)?.ok_or_else(||Box::<dyn Error>::from("when: no condition value"));
let thunk = Self::compile(arg!(expr, head, 2, "content"))?;
Self::boxed(move|state, screen|{
when(
cond(state)?,
draw(|screen|thunk(state, screen))
).draw(screen)
})
},
"either" => {
let cond = Arc::from(arg!(expr, head, 1, "condition"));
let cond = move|state: &App|state.namespace(&cond)?.ok_or_else(||Box::<dyn Error>::from("either: no condition value"));
let a = Self::compile(arg!(expr, head, 2, "content A"))?;
let b = Self::compile(arg!(expr, head, 3, "content B"))?;
Self::boxed(move|state, screen|{
either(
cond(state)?,
draw(|screen|a(state, screen)),
draw(|screen|b(state, screen)),
).draw(screen)
})
},
"bsp" | "split" | "stack" => {
let split = head.split('/').skip(1).next();
let split = match split {
Some("n") => Split::North,
Some("s") => Split::South,
Some("e") => Split::East,
Some("w") => Split::West,
Some("a") => Split::Above,
Some("b") => Split::Below,
_ => return Err(format!("invalid split: {split:?}").into())
};
let a = Self::compile(arg!(expr, head, 1, "content A"))?;
let b = Self::compile(arg!(expr, head, 2, "content B"))?;
Self::boxed(move|state, screen|split.stack(
draw(|screen|a(state, screen)),
draw(|screen|b(state, screen)),
).draw(screen))
},
"bar" => {
let split = head.split('/').skip(1).next();
let split = match split {
Some("n") => Split::North,
Some("s") => Split::South,
Some("e") => Split::East,
Some("w") => Split::West,
Some("a") => Split::Above,
Some("b") => Split::Below,
_ => return Err(format!("invalid split: {split:?}").into())
};
let size = Arc::from(arg!(expr, head, 1, "size"));
let size = move|state: &App|state.namespace(&size)?.ok_or_else(||Box::<dyn Error>::from("bar: no size"));
let a = Self::compile(arg!(expr, head, 2, "content A"))?;
let b = Self::compile(arg!(expr, head, 3, "content B"))?;
Self::boxed(move|state, screen|split.bar(
size(state)?,
draw(|screen|a(state, screen)),
draw(|screen|b(state, screen)),
).draw(screen))
},
"align" => {
let azimuth = head.split('/').skip(1).next();
let azimuth = match azimuth {
Some("n") => Azimuth::N,
Some("s") => Azimuth::S,
Some("e") => Azimuth::E,
Some("w") => Azimuth::W,
Some("ne") => Azimuth::NE,
Some("se") => Azimuth::SE,
Some("nw") => Azimuth::NW,
Some("sw") => Azimuth::SW,
Some("c") => Azimuth::C,
Some("x") => Azimuth::X,
Some("y") => Azimuth::Y,
_ => return Err(format!("invalid azimuth: {azimuth:?}").into())
};
let thunk = Self::compile(arg!(expr, head, 1, "content"))?;
Self::boxed(move|state, screen|{
Align(
Some(azimuth),
draw(|screen|thunk(state, screen))
).draw(screen)
})
},
"full" | "fill" => {
let thunk = Self::compile(arg!(expr, head, 1, "content"))?;
match head.split('/').skip(1).next() {
Some("w") | Some("x") => Self::boxed(move|state, screen|{
Full::W(draw(|screen|thunk(state, screen))).draw(screen)
}),
Some("h") | Some("y") => Self::boxed(move|state, screen|{
Full::H(draw(|screen|thunk(state, screen))).draw(screen)
}),
Some("wh") | Some("xy") | None => Self::boxed(move|state, screen|{
Full::WH(draw(|screen|thunk(state, screen))).draw(screen)
}),
_ => unreachable!()
}
},
"exact" | "min" | "max" | "push" | "pull" | "pad" => {
match head.split('/').skip(1).next() {
Some("w") | Some("x") => {
let value = Arc::from(arg!(expr, head, 1, "value"));
let value = move|state: &App|state.namespace(&value);
let thunk = Self::compile(arg!(expr, head, 2, "content"))?;
match ns {
"exact" => Self::boxed(move|state, screen|Exact::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"push" => Self::boxed(move|state, screen|Push::X(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"pull" => Self::boxed(move|state, screen|Pull::X(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"pad" => Self::boxed(move|state, screen|Pad::X(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"min" => Self::boxed(move|state, screen|Min::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"max" => Self::boxed(move|state, screen|Max::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
_ => unreachable!()
}
},
Some("h") | Some("y") => {
let value = Arc::from(arg!(expr, head, 1, "value"));
let value = move|state: &App|state.namespace(&value);
let thunk = Self::compile(arg!(expr, head, 2, "content"))?;
match ns {
"exact" => Self::boxed(move|state, screen|Exact::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"push" => Self::boxed(move|state, screen|Push::Y(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"pull" => Self::boxed(move|state, screen|Pull::Y(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"pad" => Self::boxed(move|state, screen|Pad::Y(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"min" => Self::boxed(move|state, screen|Min::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
"max" => Self::boxed(move|state, screen|Max::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
_ => unreachable!()
}
},
Some("wh") | Some("xy") | None => {
let value1 = Arc::from(arg!(expr, head, 1, "value"));
let value1 = move|state: &App|state.namespace(&value1);
let value2 = Arc::from(arg!(expr, head, 2, "value"));
let value2 = move|state: &App|state.namespace(&value2);
let thunk = Self::compile(arg!(expr, head, 3, "content"))?;
match ns {
"exact" => Self::boxed(move|state, screen|Exact::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
"push" => Self::boxed(move|state, screen|Push::XY(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
"pull" => Self::boxed(move|state, screen|Pull::XY(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
"pad" => Self::boxed(move|state, screen|Pad::XY(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
"min" => Self::boxed(move|state, screen|Min::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
"max" => Self::boxed(move|state, screen|Max::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
_ => unreachable!()
}
},
_ => unreachable!()
}
},
"fg" | "bg" => {
let color = Arc::from(arg!(expr, head, 1, "color"));
let color = move|state: &App|state.namespace(&color);
let thunk = Self::compile(arg!(expr, head, 2, "content"))?;
match ns {
"fg" => Self::boxed(move|state, screen|fg(
color(state)?.unwrap_or_default(), draw(|screen|thunk(state, screen))
).draw(screen)),
"bg" => Self::boxed(move|state, screen|bg(
color(state)?.unwrap_or_default(), draw(|screen|thunk(state, screen))
).draw(screen)),
_ => unreachable!()
}
},
"text" => {
let text: Arc<str> = Arc::from(expr.tail()?.unwrap_or_default());
Self::boxed(move|_state, screen|{
text.draw(screen)
})
},
_ => return Err(format!("compile_expr: unexpected: {expr:?}").into())
}
} else {
return Err(format!("compile_expr: invalid expression: {expr:?}").into())
}))
}
fn compile_word (word: Arc<str>)
-> Usually<Arc<Box<dyn Fn(&App, &mut Tui)->Drawn<u16> + Send + Sync>>>
{
macro_rules! draw {
(|$state:ident|$body:expr)=>{Self::boxed(move|$state, to|$body.draw(to))}
}
Ok(Arc::new(match word.split("/").next() {
//Some(":logo") => view_logo().draw(to),
Some(":meters") => match word.split("/").skip(1).next() {
Some("input") => Self::boxed(move|_, to|bg(Rgb(30, 30, 30), "Input Meters".align_s().full_h()).draw(to)),
Some("output") => Self::boxed(move|_, to|bg(Rgb(30, 30, 30), "Output Meters".align_s().full_h()).draw(to)),
_ => panic!()
},
Some(":tracks") => match word.split("/").skip(1).next() {
None => Self::boxed(move|_, to|"TODO tracks".draw(to)),
Some("names") => draw!(|app|app.project.view_track_names(app.color.clone())),
Some("inputs") => draw!(|app|app.project.view_track_inputs(app.color.clone())),
Some("devices") => draw!(|app|app.project.view_track_devices(app.color.clone())),
Some("outputs") => draw!(|app|app.project.view_track_outputs(app.color.clone(), 0)),
_ => panic!()
},
Some(":scenes") => match word.split("/").skip(1).next() {
Some("clips") => draw!(|app|app.view_scenes_clips()),
Some("names") => draw!(|app|app.view_scenes_names()),
_ => panic!()
},
Some(":templates") => draw!(|app|view_templates(app)),
Some(":browse/title") => draw!(|app|view_browse_title(app)),
Some(":device") => draw!(|app|view_device(app)),
Some(":dialog") => Self::boxed(move|state, to|draw_dialog(to, word.split("/").skip(1), state)),
Some(":sessions") => Self::boxed(move|_, to|view_sessions().draw(to)),
Some(":status") => Self::boxed(move|state, to|draw_status_bar(to, state)),
Some(":editor") => Self::boxed(move|_, to|"TODO Editor".draw(to)),
Some(":transport") => Self::boxed(move|_, to|view_transport(true, "", "", "").draw(to)),
Some(":debug") => Self::boxed(move|_, to|format!("[{:?}]", to.area()).exact_h(1).draw(to)),
Some(_) => Self::boxed(move|state, to|if let Some(view) = state.config.get_view(word.as_ref()) {
(view.render)(state, to)
} else {
fg(Color::Rgb(128, 32, 32), format!("undefined: {word:?}")).draw(to)
}),
_ => unreachable!()
}))
}
}
/// Load custom view definition.
pub fn views_add <'a> (views: &Views, expr: impl Language) -> UsuallyRef<'a, ()> {
views.try_write().unwrap().insert(
expr.head()?.ok_or("view: missing name")?.into(),
View::new(expr.tail()?.ok_or("view: missing body")?)?.into()
);
Ok(())
}

15
src/vst2.rs Normal file
View file

@ -0,0 +1,15 @@
use crate::*;
impl<E: Engine> ::vst::host::Host for Plugin<E> {}
fn set_vst_plugin <E: Engine> (
host: &Arc<Mutex<Plugin<E>>>, _path: &str
) -> Usually<PluginKind> {
let mut loader = ::vst::host::PluginLoader::load(
&std::path::Path::new("/nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lxvst/helm.so"),
host.clone()
)?;
Ok(PluginKind::VST2 {
instance: loader.instance()?
})
}

2
tengri

@ -1 +1 @@
Subproject commit 5272b8ec55e48d135c88cbc840eab530afa1ae73
Subproject commit 87d1c0a8111fddb1f2acf64e7ad72c353c363540