mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-10-08 18:56:42 +02:00
big flat
This commit is contained in:
parent
f743d9fb4d
commit
922c2e1f2f
45 changed files with 1950 additions and 1765 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
|
@ -3893,9 +3893,13 @@ dependencies = [
|
||||||
"midly",
|
"midly",
|
||||||
"palette",
|
"palette",
|
||||||
"parking_lot 0.12.5",
|
"parking_lot 0.12.5",
|
||||||
|
"profiling",
|
||||||
"quanta",
|
"quanta",
|
||||||
"rand 0.8.7",
|
"rand 0.8.7",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
|
"tracing",
|
||||||
|
"tracing-flame",
|
||||||
|
"tracing-subscriber",
|
||||||
"unicode-width 0.2.0",
|
"unicode-width 0.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ proptest-derive = { version = "^0.5.1" }
|
||||||
default = ["cli", "arranger", "sampler", "prof"]
|
default = ["cli", "arranger", "sampler", "prof"]
|
||||||
|
|
||||||
prof = []
|
prof = []
|
||||||
#prof2 = ["prof", "tengri/prof"]
|
prof2 = ["prof", "tengri/prof"]
|
||||||
hotpath = ["hotpath/hotpath"]
|
hotpath = ["hotpath/hotpath"]
|
||||||
hotpath-cpu = ["hotpath/hotpath-cpu"]
|
hotpath-cpu = ["hotpath/hotpath-cpu"]
|
||||||
hotpath-alloc = ["hotpath/hotpath-alloc"]
|
hotpath-alloc = ["hotpath/hotpath-alloc"]
|
||||||
|
|
|
||||||
599
src/app.rs
Normal file
599
src/app.rs
Normal 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
92
src/bind.rs
Normal 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
38
src/binds.rs
Normal 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(())
|
||||||
|
}
|
||||||
35
src/cli.rs
35
src/cli.rs
|
|
@ -10,32 +10,6 @@ pub fn show_version () {
|
||||||
println!("versions aint real man");
|
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<()> {
|
pub fn run_with_config (config: Arc<Config>) -> Usually<()> {
|
||||||
Cli::parse().run(Some(config))
|
Cli::parse().run(Some(config))
|
||||||
}
|
}
|
||||||
|
|
@ -99,7 +73,7 @@ pub enum Action {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Action {
|
impl Action {
|
||||||
fn run (&self, config: Arc<Config>, trace: bool) -> Usually<()> {
|
fn run (&self, config: Arc<Config>, _trace: bool) -> Usually<()> {
|
||||||
use Action::*;
|
use Action::*;
|
||||||
match self {
|
match self {
|
||||||
Version => show_version(),
|
Version => show_version(),
|
||||||
|
|
@ -109,6 +83,7 @@ impl Action {
|
||||||
New(sesh) => Exit::run(|exit|Tui::run_main(
|
New(sesh) => Exit::run(|exit|Tui::run_main(
|
||||||
exit.clone(),
|
exit.clone(),
|
||||||
{
|
{
|
||||||
|
#[allow(unused_mut)]
|
||||||
let mut app = App::new(Some(exit), sesh.init()?, config, ":menu");
|
let mut app = App::new(Some(exit), sesh.init()?, config, ":menu");
|
||||||
#[cfg(feature = "prof2")] {
|
#[cfg(feature = "prof2")] {
|
||||||
if trace {
|
if trace {
|
||||||
|
|
@ -165,7 +140,7 @@ pub struct ProjectInit {
|
||||||
#[arg(short='s', long)] scenes: Option<usize>,
|
#[arg(short='s', long)] scenes: Option<usize>,
|
||||||
}
|
}
|
||||||
impl ProjectInit {
|
impl ProjectInit {
|
||||||
pub fn init (&self) -> Usually<Arrangement> {
|
pub fn init (&self) -> Usually<Project> {
|
||||||
let Self {
|
let Self {
|
||||||
name, bpm, tracks, scenes,
|
name, bpm, tracks, scenes,
|
||||||
sync_lead: _, sync_follow: _,
|
sync_lead: _, sync_follow: _,
|
||||||
|
|
@ -175,7 +150,7 @@ impl ProjectInit {
|
||||||
} = self;
|
} = self;
|
||||||
let name = name.as_ref().map_or("tek", |x|x.as_str());
|
let name = name.as_ref().map_or("tek", |x|x.as_str());
|
||||||
let jack = Jack::new(&name)?;
|
let jack = Jack::new(&name)?;
|
||||||
let mut proj = Arrangement::new(
|
let mut proj = Project::new(
|
||||||
&jack,
|
&jack,
|
||||||
name.into(),
|
name.into(),
|
||||||
Clock::new(&jack, *bpm)?,
|
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!("Name: {:?}", &project.name);
|
||||||
println!("JACK: {:?}", &project.jack);
|
println!("JACK: {:?}", &project.jack);
|
||||||
println!("Buffer: {:?}", &project.clock.chunk);
|
println!("Buffer: {:?}", &project.clock.chunk);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
impl Arrangement {
|
impl Project {
|
||||||
/// Toggle looping for the active clip
|
/// Toggle looping for the active clip
|
||||||
pub fn toggle_loop (&mut self) {
|
pub fn toggle_loop (&mut self) {
|
||||||
if let Some(clip) = self.selected_clip() {
|
if let Some(clip) = self.selected_clip() {
|
||||||
|
|
@ -39,7 +39,7 @@ impl HasClipsSize for App {
|
||||||
fn clips_size (&self) -> &Sizer { &self.project.size_inner }
|
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 }
|
fn clips_size (&self) -> &Sizer { &self.size_inner }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,11 +1,5 @@
|
||||||
use crate::*;
|
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 {
|
impl App {
|
||||||
/// Update memoized render of clock values.
|
/// Update memoized render of clock values.
|
||||||
/// ```
|
/// ```
|
||||||
|
|
@ -508,4 +502,3 @@ pub fn button_play_pause <'a> (playing: bool, compact: bool) -> impl Draw<Tui> {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
670
src/config.rs
670
src/config.rs
|
|
@ -2,184 +2,12 @@ use crate::*;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use notify_debouncer_full::notify::{RecursiveMode, PollWatcher, Config as NotifyConfig, Watcher};
|
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.
|
/// 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();
|
/// let config: Config = Default::default();
|
||||||
/// modes_add(&config.modes, stringify!(
|
/// Modes::add(&config.modes, stringify!(
|
||||||
/// (mode :menu (name Menu)
|
/// (mode :menu (name Menu)
|
||||||
/// (info Mode selector.) (keys :axis/y :confirm)
|
/// (info Mode selector.) (keys :axis/y :confirm)
|
||||||
/// (view (bg (g 0) (bsp/s :ports/out
|
/// (view (bg (g 0) (bsp/s :ports/out
|
||||||
|
|
@ -206,337 +34,82 @@ pub struct Config {
|
||||||
pub watch: RwLock<Option<PollWatcher>>
|
pub watch: RwLock<Option<PollWatcher>>
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Collection of UI modes.
|
impl AsRef<Config> for Config {
|
||||||
///
|
fn as_ref (&self) -> &Config {
|
||||||
/// ```
|
self
|
||||||
/// 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,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Collection of custom view definitions.
|
impl Config {
|
||||||
pub type Views = Arc<RwLock<BTreeMap<Arc<str>, Arc<View<Tui, App>>>>>;
|
/// Write initial contents of configuration.
|
||||||
|
pub fn init <C: AsRef<Config>> (config: C) -> Usually<C> {
|
||||||
/// Custom view definition is a boxed closure emitting a [Draw]able from state `S`.
|
if config.as_ref().find_file().is_none() {
|
||||||
pub struct View<S: Screen, T> {
|
std::fs::write(config.as_ref().place_file()?, Config::DEFAULTS)?;
|
||||||
pub source: Arc<str>,
|
|
||||||
pub render: Arc<Box<dyn Fn(&T, &mut S)->Drawn<S::Unit> + Send + Sync>>
|
|
||||||
}
|
}
|
||||||
|
Ok(if let Some(path) = config.as_ref().find_file() {
|
||||||
impl_debug!(<S: Screen, T> View<S, T> |self, w| { write!(w, "View({})", self.source) });
|
Config::load(config, std::fs::read_to_string(&path)?)?
|
||||||
|
|
||||||
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 {
|
} else {
|
||||||
return Err(format!("not word/expr:\n{source:?}").into())
|
return Err(format!("config_init: not found").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())
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
} 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>)
|
pub fn load_kind <C: AsRef<Config>, L: Language> (config: C, src: L) -> Usually<C> {
|
||||||
-> Usually<Arc<Box<dyn Fn(&App, &mut Tui)->Drawn<u16> + Send + Sync>>>
|
//if let Some(tail) = src.tail()? {
|
||||||
{
|
match src.head()? {
|
||||||
macro_rules! draw {
|
Some("mode") => Modes::add(&config.as_ref().modes, src.tail()),
|
||||||
(|$state:ident|$body:expr)=>{Self::boxed(move|$state, to|$body.draw(to))}
|
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() {
|
/// Watch a config's file for changes.
|
||||||
//Some(":logo") => view_logo().draw(to),
|
pub fn watch (
|
||||||
Some(":meters") => match word.split("/").skip(1).next() {
|
config: Arc<Config>, poll: Option<Duration>
|
||||||
Some("input") => Self::boxed(move|_, to|bg(Rgb(30, 30, 30), "Input Meters".align_s().full_h()).draw(to)),
|
) -> Usually<()> {
|
||||||
Some("output") => Self::boxed(move|_, to|bg(Rgb(30, 30, 30), "Output Meters".align_s().full_h()).draw(to)),
|
let poll = poll.unwrap_or(Duration::from_millis(250));
|
||||||
_ => panic!()
|
let opts = NotifyConfig::default().with_poll_interval(poll);
|
||||||
},
|
let mut watcher = ::notify_debouncer_full::notify::poll::PollWatcher::new({
|
||||||
Some(":tracks") => match word.split("/").skip(1).next() {
|
let config = config.clone();
|
||||||
None => Self::boxed(move|_, to|"TODO tracks".draw(to)),
|
move|result|{
|
||||||
Some("names") => draw!(|app|app.project.view_track_names(app.color.clone())),
|
match result {
|
||||||
Some("inputs") => draw!(|app|app.project.view_track_inputs(app.color.clone())),
|
Ok(_events) => if let Err(e) = Config::init(config.as_ref()) {
|
||||||
Some("devices") => draw!(|app|app.project.view_track_devices(app.color.clone())),
|
*config.as_ref().error.try_write().unwrap() = Some(format!("{e:?}").into());
|
||||||
Some("outputs") => draw!(|app|app.project.view_track_outputs(app.color.clone(), 0)),
|
panic!("{e:?}");
|
||||||
_ => 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 {
|
} else {
|
||||||
fg(Color::Rgb(128, 32, 32), format!("undefined: {word:?}")).draw(to)
|
//println!("config updated");
|
||||||
}),
|
},
|
||||||
_ => unreachable!()
|
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)
|
|
||||||
}
|
}
|
||||||
|
}, opts)?;
|
||||||
/// Load custom view definition.
|
if let Some(path) = config.as_ref().get_file() {
|
||||||
pub fn views_add <'a> (views: &Views, expr: impl Language) -> UsuallyRef<'a, ()> {
|
//println!("watching: {path:?}");
|
||||||
views.try_write().unwrap().insert(
|
watcher.watch(&path, RecursiveMode::NonRecursive)?;
|
||||||
expr.head()?.ok_or("view: missing name")?.into(),
|
*config.as_ref().watch.try_write().unwrap() = Some(watcher);
|
||||||
View::new(expr.tail()?.ok_or("view: missing body")?)?.into()
|
|
||||||
);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(format!("no config path").into())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
|
@ -567,14 +140,14 @@ impl Config {
|
||||||
/// Create, initialize, and watch a new configuration.
|
/// Create, initialize, and watch a new configuration.
|
||||||
pub fn watched <T: 'static> (callback: impl FnOnce(Arc<Config>)->T) -> Usually<T> {
|
pub fn watched <T: 'static> (callback: impl FnOnce(Arc<Config>)->T) -> Usually<T> {
|
||||||
let config = Self::init_new(None)?;
|
let config = Self::init_new(None)?;
|
||||||
config_watch(config.clone(), None)?;
|
Config::watch(config.clone(), None)?;
|
||||||
let result = callback(config);
|
let result = callback(config);
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create and initialize a new configuration.
|
/// Create and initialize a new configuration.
|
||||||
pub fn init_new (_dirs: Option<BaseDirectories>) -> Usually<Arc<Self>> {
|
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.
|
/// 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
212
src/device.rs
Normal 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()
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
@ -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)
|
pub fn get_dialog <'a> (_: &App, src: impl Language) -> Perhaps<Dialog> {
|
||||||
-> 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> {
|
|
||||||
src.word()?.map(|word|Ok(match word {
|
src.word()?.map(|word|Ok(match word {
|
||||||
":dialog/none" => Dialog::None,
|
":dialog/none" => Dialog::None,
|
||||||
":dialog/options" => Dialog::Options,
|
":dialog/options" => Dialog::Options,
|
||||||
|
|
@ -53,8 +32,6 @@ impl App {
|
||||||
})).transpose()
|
})).transpose()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Various possible dialog modes.
|
/// Various possible dialog modes.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
|
@ -63,68 +40,75 @@ impl App {
|
||||||
#[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog {
|
#[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog {
|
||||||
#[default] None,
|
#[default] None,
|
||||||
Help(usize),
|
Help(usize),
|
||||||
Menu(usize, MenuItems),
|
Menu(usize, Arc<[MenuItem]>),
|
||||||
Device(usize),
|
Device(usize),
|
||||||
Message(Arc<str>),
|
Message(Arc<str>),
|
||||||
Browse(BrowseTarget, Arc<Browse>),
|
Browse(BrowseTarget, Arc<Browse>),
|
||||||
Options,
|
Options,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List of menu items.
|
pub fn draw_dialog <'a, I: Debug + Iterator<Item = &'a str>> (to: &mut Tui, mut frags: I, state: &App)
|
||||||
///
|
-> Drawn<u16>
|
||||||
/// ```
|
{
|
||||||
/// let items: tek::MenuItems = Default::default();
|
match frags.next() {
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone, Default, PartialEq)] pub struct MenuItems(
|
|
||||||
pub Arc<[MenuItem]>
|
|
||||||
);
|
|
||||||
|
|
||||||
/// An item of a menu.
|
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 item: tek::MenuItem = Default::default();
|
let b = if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) };
|
||||||
/// ```
|
fg_bg(f, b, item).exact_h(2).align_x()
|
||||||
#[derive(Clone)] pub struct MenuItem(
|
})).exact_w(20).align_x())
|
||||||
/// Label
|
} else {
|
||||||
pub Arc<str>,
|
None
|
||||||
/// Callback
|
}.draw(to),
|
||||||
pub Arc<Box<dyn Fn(&mut App)->Usually<()> + Send + Sync>>
|
|
||||||
);
|
|
||||||
|
|
||||||
impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) });
|
_ => unimplemented!("draw_dialog: ({frags:?})"),
|
||||||
|
}
|
||||||
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 } }
|
|
||||||
|
|
||||||
impl Dialog {
|
impl Dialog {
|
||||||
|
|
||||||
/// ```
|
/// ```
|
||||||
/// let _ = tek::Dialog::welcome();
|
/// let _ = tek::Dialog::welcome();
|
||||||
/// ```
|
/// ```
|
||||||
pub fn welcome () -> Self {
|
pub fn welcome () -> Self {
|
||||||
Self::Menu(1, MenuItems([
|
Self::Menu(1, Arc::new([
|
||||||
|
|
||||||
MenuItem(" Resume session ".into(), Arc::new(Box::new(|_app|{
|
MenuItem::new(" Resume session ", [
|
||||||
Ok(())
|
AppCommand::Project {
|
||||||
}))),
|
command: ProjectCommand::LoadLast
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
|
||||||
MenuItem(" Create session ".into(), Arc::new(Box::new(|app|Ok({
|
MenuItem::new(" Create session ", [
|
||||||
app.dialog = Dialog::None;
|
AppCommand::Dialog {
|
||||||
app.mode = Some(":arranger".into());
|
dialog: Dialog::None
|
||||||
})))),
|
},
|
||||||
|
AppCommand::Mode {
|
||||||
|
command: ModeCommand::Reset {
|
||||||
|
modes: vec![":arranger".into()]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
|
||||||
MenuItem(" Load session ".into(), Arc::new(Box::new(|_|{
|
MenuItem::new(" Load session ", [
|
||||||
Ok(())
|
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({
|
MenuItem::new(" Exit ", [
|
||||||
app.exit.exit();
|
AppCommand::Exit
|
||||||
println!("Exited cleanly.")
|
]),
|
||||||
})))),
|
|
||||||
|
|
||||||
].into()))
|
]))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// FIXME: generalize
|
/// FIXME: generalize
|
||||||
|
|
@ -134,26 +118,29 @@ impl Dialog {
|
||||||
pub fn menu_selected (&self) -> Option<usize> {
|
pub fn menu_selected (&self) -> Option<usize> {
|
||||||
if let Self::Menu(selected, _) = self { Some(*selected) } else { None }
|
if let Self::Menu(selected, _) = self { Some(*selected) } else { None }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// FIXME: generalize
|
/// FIXME: generalize
|
||||||
/// ```
|
/// ```
|
||||||
/// let _ = tek::Dialog::welcome().menu_next();
|
/// let _ = tek::Dialog::welcome().menu_next();
|
||||||
/// ```
|
/// ```
|
||||||
pub fn menu_next (&self) -> Self {
|
pub fn menu_next (&self) -> Self {
|
||||||
match 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
|
_ => Self::None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// FIXME: generalize
|
/// FIXME: generalize
|
||||||
/// ```
|
/// ```
|
||||||
/// let _ = tek::Dialog::welcome().menu_prev();
|
/// let _ = tek::Dialog::welcome().menu_prev();
|
||||||
/// ```
|
/// ```
|
||||||
pub fn menu_prev (&self) -> Self {
|
pub fn menu_prev (&self) -> Self {
|
||||||
match 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
|
_ => Self::None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// FIXME: generalize
|
/// FIXME: generalize
|
||||||
/// ```
|
/// ```
|
||||||
/// let _ = tek::Dialog::welcome().device_kind();
|
/// let _ = tek::Dialog::welcome().device_kind();
|
||||||
|
|
@ -161,6 +148,7 @@ impl Dialog {
|
||||||
pub fn device_kind (&self) -> Option<usize> {
|
pub fn device_kind (&self) -> Option<usize> {
|
||||||
if let Self::Device(index) = self { Some(*index) } else { None }
|
if let Self::Device(index) = self { Some(*index) } else { None }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// FIXME: generalize
|
/// FIXME: generalize
|
||||||
/// ```
|
/// ```
|
||||||
/// let _ = tek::Dialog::welcome().device_kind_next();
|
/// let _ = tek::Dialog::welcome().device_kind_next();
|
||||||
|
|
@ -168,6 +156,7 @@ impl Dialog {
|
||||||
pub fn device_kind_next (&self) -> Option<usize> {
|
pub fn device_kind_next (&self) -> Option<usize> {
|
||||||
self.device_kind().map(|index|(index + 1) % device_kinds().len())
|
self.device_kind().map(|index|(index + 1) % device_kinds().len())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// FIXME: generalize
|
/// FIXME: generalize
|
||||||
/// ```
|
/// ```
|
||||||
/// let _ = tek::Dialog::welcome().device_kind_prev();
|
/// let _ = tek::Dialog::welcome().device_kind_prev();
|
||||||
|
|
@ -175,12 +164,16 @@ impl Dialog {
|
||||||
pub fn device_kind_prev (&self) -> Option<usize> {
|
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)))
|
self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// FIXME: implement
|
/// FIXME: implement
|
||||||
pub fn message (&self) -> Option<&str> { todo!() }
|
pub fn message (&self) -> Option<&str> { todo!() }
|
||||||
|
|
||||||
/// FIXME: implement
|
/// FIXME: implement
|
||||||
pub fn browser (&self) -> Option<&Arc<Browse>> { todo!() }
|
pub fn browser (&self) -> Option<&Arc<Browse>> { todo!() }
|
||||||
|
|
||||||
/// FIXME: implement
|
/// FIXME: implement
|
||||||
pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() }
|
pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Increment a wrapping counter.
|
/// Increment a wrapping counter.
|
||||||
|
|
@ -432,8 +432,3 @@ impl Draw<Tui> for MidiEditor {
|
||||||
self.size.of(&self.mode).draw(to)
|
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
81
src/field.rs
Normal 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, "▌"), ))))
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,7 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
#[cfg(feature = "lv2_gui")] use ::winit::{
|
|
||||||
application::ApplicationHandler,
|
|
||||||
event::WindowEvent,
|
|
||||||
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
|
|
||||||
window::{Window, WindowId},
|
|
||||||
platform::x11::EventLoopBuilderExtX11
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A LV2 plugin.
|
/// A LV2 plugin.
|
||||||
#[derive(Debug)] #[cfg(feature = "lv2")] pub struct Lv2 {
|
#[derive(Debug)] pub struct Lv2 {
|
||||||
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
||||||
pub jack: Jack<'static>,
|
pub jack: Jack<'static>,
|
||||||
pub name: Arc<str>,
|
pub name: Arc<str>,
|
||||||
|
|
@ -30,11 +22,6 @@ use crate::*;
|
||||||
pub lv2_ui_thread: Option<JoinHandle<()>>,
|
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_audio!(Lv2: lv2_jack_process);
|
||||||
|
|
||||||
impl Lv2 {
|
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 })
|
//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
26
src/lv2_gui.rs
Normal 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
33
src/menu_item.rs
Normal 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
|
||||||
|
//}
|
||||||
|
//}
|
||||||
|
|
@ -60,6 +60,40 @@ pub fn to_rms (samples: &[f32]) -> f32 {
|
||||||
(sum / samples.len() as f32).sqrt()
|
(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 {
|
#[cfg(test)] mod test_view_meter {
|
||||||
use super::*;
|
use super::*;
|
||||||
use proptest::prelude::*;
|
use proptest::prelude::*;
|
||||||
72
src/mode.rs
Normal file
72
src/mode.rs
Normal 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
46
src/mode_stack.rs
Normal 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
45
src/modes.rs
Normal 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
1
src/plugin.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
use crate::*;
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
def_sizes_iter!(PortsSizes => Arc<str>, [Connect]);
|
def_sizes_iter!(PortsSizes => Arc<str>, [Connect]);
|
||||||
impl_has!(Vec<MidiInput>: |self: Arrangement| self.midi_ins);
|
impl_has!(Vec<MidiInput>: |self: Project| self.midi_ins);
|
||||||
impl_has!(Vec<MidiOutput>: |self: Arrangement| self.midi_outs);
|
impl_has!(Vec<MidiOutput>: |self: Project| self.midi_outs);
|
||||||
impl_has!(Vec<MidiInput>: |self: App|self.project.midi_ins);
|
impl_has!(Vec<MidiInput>: |self: App|self.project.midi_ins);
|
||||||
impl_has!(Vec<MidiOutput>: |self: App|self.project.midi_outs);
|
impl_has!(Vec<MidiOutput>: |self: App|self.project.midi_outs);
|
||||||
|
|
||||||
|
|
@ -1,12 +1,45 @@
|
||||||
use crate::*;
|
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.
|
/// Arranger.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let arranger = tek::Arrangement::default();
|
/// let arranger = tek::Project::default();
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
pub struct Arrangement {
|
pub struct Project {
|
||||||
/// JACK client handle.
|
/// JACK client handle.
|
||||||
pub jack: Jack<'static>,
|
pub jack: Jack<'static>,
|
||||||
/// Project name.
|
/// Project name.
|
||||||
|
|
@ -46,7 +79,7 @@ pub struct Arrangement {
|
||||||
#[cfg(feature = "scene")] pub scene_scroll: usize,
|
#[cfg(feature = "scene")] pub scene_scroll: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Arrangement {
|
impl Project {
|
||||||
|
|
||||||
/// Create a new arrangement.
|
/// Create a new arrangement.
|
||||||
pub fn new (
|
pub fn new (
|
||||||
|
|
@ -104,43 +137,15 @@ impl Arrangement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the first sampler of the active track
|
impl HasJack<'static> for Project { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
||||||
#[cfg(feature = "sampler")]
|
impl_has!(Jack<'static>: |self: Project| self.jack);
|
||||||
impl Arrangement {
|
impl_has!(Sizer: |self: Project| self.size);
|
||||||
pub fn sampler (&self) -> Option<&Sampler> {
|
impl_has!(Clock: |self: Project| self.clock);
|
||||||
self.selected_track()?.sampler(0)
|
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
|
pub fn swap_value <T: Clone + PartialEq, U> (
|
||||||
#[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> (
|
|
||||||
target: &mut T, value: &T, returned: impl Fn(T)->U
|
target: &mut T, value: &T, returned: impl Fn(T)->U
|
||||||
) -> Perhaps<U> {
|
) -> Perhaps<U> {
|
||||||
if *target == *value {
|
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
|
target: &mut bool, value: &Option<bool>, returned: impl Fn(Option<bool>)->U
|
||||||
) -> Perhaps<U> {
|
) -> Perhaps<U> {
|
||||||
let mut value = value.unwrap_or(!*target);
|
let mut value = value.unwrap_or(!*target);
|
||||||
|
|
@ -163,24 +168,3 @@ pub(self) fn toggle_bool <U> (
|
||||||
Ok(Some(returned(Some(value))))
|
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())
|
|
||||||
//}
|
|
||||||
|
|
@ -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> {
|
pub trait HasSampler: AsRef<Sampler> + AsMut<Sampler> {
|
||||||
fn sampler (&self) -> &Sampler {
|
fn sampler (&self) -> &Sampler {
|
||||||
self.as_ref()
|
self.as_ref()
|
||||||
|
|
@ -57,8 +57,8 @@ impl Scene {
|
||||||
|
|
||||||
impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt());
|
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_mut_opt!(Scene: |self: App| self.project.as_mut_opt());
|
||||||
impl_as_ref_opt!(Scene: |self: Arrangement| self.selected_scene());
|
impl_as_ref_opt!(Scene: |self: Project| self.selected_scene());
|
||||||
impl_as_mut_opt!(Scene: |self: Arrangement| self.selected_scene_mut());
|
impl_as_mut_opt!(Scene: |self: Project| self.selected_scene_mut());
|
||||||
|
|
||||||
impl<T: AsRefOpt<Scene> + AsMutOpt<Scene> + Send + Sync> HasScene for T {}
|
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);
|
pub type SceneWith<'a, T> = (usize, &'a Scene, usize, usize, T);
|
||||||
|
|
||||||
def_sizes_iter!(ScenesSizes => Scene);
|
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_ref!(Vec<Scene>: |self: App| self.project.as_ref());
|
||||||
impl_as_mut!(Vec<Scene>: |self: App| self.project.as_mut());
|
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 {
|
fn h_scenes (&self) -> u16 {
|
||||||
(self.size.h() as u16).saturating_sub(20)
|
(self.size.h() as u16).saturating_sub(20)
|
||||||
}
|
}
|
||||||
|
|
@ -311,7 +311,7 @@ pub trait HasSceneScroll: HasScenes {
|
||||||
fn scene_scroll (&self) -> usize;
|
fn scene_scroll (&self) -> usize;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HasSceneScroll for Arrangement {
|
impl HasSceneScroll for Project {
|
||||||
fn scene_scroll (&self) -> usize {
|
fn scene_scroll (&self) -> usize {
|
||||||
self.scene_scroll
|
self.scene_scroll
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track());
|
impl_as_ref_opt!(Track: |self: Project| self.selected_track());
|
||||||
impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut());
|
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_ref_opt!(Track: |self: App| self.project.as_ref_opt());
|
||||||
impl_as_mut_opt!(Track: |self: App| self.project.as_mut_opt());
|
impl_as_mut_opt!(Track: |self: App| self.project.as_mut_opt());
|
||||||
impl <T: AsRef<Selection>+AsMut<Selection>> HasSelection for T {}
|
impl <T: AsRef<Selection>+AsMut<Selection>> HasSelection for T {}
|
||||||
|
|
@ -20,13 +20,13 @@
|
||||||
(bar/s 2 (align/x (bg (g 12) :logo))
|
(bar/s 2 (align/x (bg (g 12) :logo))
|
||||||
:dialog/menu)))))
|
:dialog/menu)))))
|
||||||
|
|
||||||
(keys :back (@escape (back)))
|
(keys :back (@escape cancel))
|
||||||
(keys :confirm (@enter (dialog confirm)))
|
(keys :confirm (@enter confirm))
|
||||||
(keys :axis/y (@up (axis (dec :y)))
|
(keys :axis/y (@up (axis (dec :y)))
|
||||||
(@down (axis (inc :y))))
|
(@down (axis (inc :y))))
|
||||||
|
|
||||||
(mode :arranger (name Arranger) (info Launch grid.)
|
(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 :editor (keys :editor))
|
||||||
(mode :dialog (keys :dialog))
|
(mode :dialog (keys :dialog))
|
||||||
(mode :message (keys :message))
|
(mode :message (keys :message))
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
(bar/n 2 :tracks/inputs
|
(bar/n 2 :tracks/inputs
|
||||||
(bar/e 16 :scenes/names :scenes/clips))))))))
|
(bar/e 16 :scenes/names :scenes/clips))))))))
|
||||||
|
|
||||||
(keys :global (see :history :saveload)
|
(keys :global
|
||||||
(@f1 (dialog :help))
|
(@f1 (dialog :help))
|
||||||
(@f8 (dialog :options))
|
(@f8 (dialog :options))
|
||||||
(@f10 (dialog :quit)))
|
(@f10 (dialog :quit)))
|
||||||
|
|
|
||||||
966
src/tek.rs
966
src/tek.rs
|
|
@ -1,6 +1,59 @@
|
||||||
#![allow(clippy::unit_arg)]
|
#![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 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.
|
/// Command-line entrypoint.
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
|
|
@ -16,891 +69,28 @@ fn main () -> Usually<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub use self::cli::*;
|
#[cfg(not(feature = "cli"))]
|
||||||
mod cli;
|
fn run_new_plain (config: Config) -> Usually<()> {
|
||||||
|
let name = "tek";
|
||||||
pub use self::config::*;
|
tengri::Tui::run_main(Jack::new_run(name, move|jack|{
|
||||||
mod config;
|
let mode = ":menu";
|
||||||
|
let title = "untitled!";
|
||||||
pub use self::app::*;
|
let bpm = 74.;
|
||||||
mod app {
|
let clock = Clock::new(&jack, Some(bpm))?;
|
||||||
use crate::*;
|
let tracks = [];
|
||||||
primitive!(u8: try_to_u8);
|
let scenes = [];
|
||||||
primitive!(u16: try_to_u16);
|
Ok(App::new(None, Project::new(
|
||||||
primitive!(usize: try_to_usize);
|
&jack,
|
||||||
primitive!(isize: try_to_isize);
|
title.into(),
|
||||||
impl_has!(Clock: |self: App|self.project.clock);
|
clock,
|
||||||
impl_has!(Dialog: |self: App|self.dialog);
|
tracks.into_iter(),
|
||||||
impl_has!(Jack<'static>: |self: App|self.jack);
|
scenes.into_iter(),
|
||||||
impl_has!(Pool: |self: App|self.pool);
|
connect_midi_ins(&jack, &"M", &[], None)?.into_iter(),
|
||||||
impl_has!(Selection: |self: App|self.project.selection);
|
connect_midi_outs(&jack, &"M", &[], None)?.into_iter(),
|
||||||
impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt());
|
[].into_iter().chain(connect_audio_ins(&jack, &"L", &[], None)?.into_iter())
|
||||||
impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt());
|
.chain(connect_audio_ins(&jack, &"R", &[], None)?.into_iter()),
|
||||||
impl_has_clips!( |self: App|self.pool.clips);
|
[].into_iter().chain(connect_audio_outs(&jack, &"L", &[], None)?.into_iter())
|
||||||
/// Total application state.
|
.chain(connect_audio_outs(&jack, &"R", &[], None)?.into_iter()),
|
||||||
///
|
), config, mode))
|
||||||
/// ```
|
})?)
|
||||||
/// 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, "▌"), ))))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{*, arrange::*};
|
use crate::*;
|
||||||
|
|
||||||
/// A track consists of a sequencer and zero or more devices chained after it.
|
/// 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);
|
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_ref!(Vec<Track>: |self: App| self.project.as_ref());
|
||||||
impl_as_mut!(Vec<Track>: |self: App| self.project.as_mut());
|
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;
|
fn tracks_last_mut (&mut self) -> &mut usize;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HasTrackScroll for &mut Arrangement {
|
impl HasTrackScroll for &mut Project {
|
||||||
fn track_scroll (&self) -> usize {
|
fn track_scroll (&self) -> usize {
|
||||||
self.track_scroll
|
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 {
|
fn track_scroll (&self) -> usize {
|
||||||
self.track_scroll
|
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
304
src/views.rs
Normal 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
15
src/vst2.rs
Normal 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
2
tengri
|
|
@ -1 +1 @@
|
||||||
Subproject commit 5272b8ec55e48d135c88cbc840eab530afa1ae73
|
Subproject commit 87d1c0a8111fddb1f2acf64e7ad72c353c363540
|
||||||
Loading…
Add table
Add a link
Reference in a new issue