mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-10-08 10:46:43 +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",
|
||||
"palette",
|
||||
"parking_lot 0.12.5",
|
||||
"profiling",
|
||||
"quanta",
|
||||
"rand 0.8.7",
|
||||
"ratatui",
|
||||
"tracing",
|
||||
"tracing-flame",
|
||||
"tracing-subscriber",
|
||||
"unicode-width 0.2.0",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ proptest-derive = { version = "^0.5.1" }
|
|||
default = ["cli", "arranger", "sampler", "prof"]
|
||||
|
||||
prof = []
|
||||
#prof2 = ["prof", "tengri/prof"]
|
||||
prof2 = ["prof", "tengri/prof"]
|
||||
hotpath = ["hotpath/hotpath"]
|
||||
hotpath-cpu = ["hotpath/hotpath-cpu"]
|
||||
hotpath-alloc = ["hotpath/hotpath-alloc"]
|
||||
|
|
|
|||
599
src/app.rs
Normal file
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");
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "cli"))]
|
||||
fn run_new_plain (config: Config) -> Usually<()> {
|
||||
let name = "tek";
|
||||
tengri::Tui::run_main(Jack::new_run(name, move|jack|{
|
||||
let mode = ":menu";
|
||||
let title = "untitled!";
|
||||
let bpm = 74.;
|
||||
let clock = Clock::new(&jack, Some(bpm))?;
|
||||
let tracks = [];
|
||||
let scenes = [];
|
||||
Ok(App::new(None, Arrangement::new(
|
||||
&jack,
|
||||
title.into(),
|
||||
clock,
|
||||
tracks.into_iter(),
|
||||
scenes.into_iter(),
|
||||
connect_midi_ins(&jack, &"M", &[], None)?.into_iter(),
|
||||
connect_midi_outs(&jack, &"M", &[], None)?.into_iter(),
|
||||
[].into_iter().chain(connect_audio_ins(&jack, &"L", &[], None)?.into_iter())
|
||||
.chain(connect_audio_ins(&jack, &"R", &[], None)?.into_iter()),
|
||||
[].into_iter().chain(connect_audio_outs(&jack, &"L", &[], None)?.into_iter())
|
||||
.chain(connect_audio_outs(&jack, &"R", &[], None)?.into_iter()),
|
||||
), config, mode))
|
||||
})?)
|
||||
}
|
||||
|
||||
pub fn run_with_config (config: Arc<Config>) -> Usually<()> {
|
||||
Cli::parse().run(Some(config))
|
||||
}
|
||||
|
|
@ -99,7 +73,7 @@ pub enum Action {
|
|||
}
|
||||
|
||||
impl Action {
|
||||
fn run (&self, config: Arc<Config>, trace: bool) -> Usually<()> {
|
||||
fn run (&self, config: Arc<Config>, _trace: bool) -> Usually<()> {
|
||||
use Action::*;
|
||||
match self {
|
||||
Version => show_version(),
|
||||
|
|
@ -109,6 +83,7 @@ impl Action {
|
|||
New(sesh) => Exit::run(|exit|Tui::run_main(
|
||||
exit.clone(),
|
||||
{
|
||||
#[allow(unused_mut)]
|
||||
let mut app = App::new(Some(exit), sesh.init()?, config, ":menu");
|
||||
#[cfg(feature = "prof2")] {
|
||||
if trace {
|
||||
|
|
@ -165,7 +140,7 @@ pub struct ProjectInit {
|
|||
#[arg(short='s', long)] scenes: Option<usize>,
|
||||
}
|
||||
impl ProjectInit {
|
||||
pub fn init (&self) -> Usually<Arrangement> {
|
||||
pub fn init (&self) -> Usually<Project> {
|
||||
let Self {
|
||||
name, bpm, tracks, scenes,
|
||||
sync_lead: _, sync_follow: _,
|
||||
|
|
@ -175,7 +150,7 @@ impl ProjectInit {
|
|||
} = self;
|
||||
let name = name.as_ref().map_or("tek", |x|x.as_str());
|
||||
let jack = Jack::new(&name)?;
|
||||
let mut proj = Arrangement::new(
|
||||
let mut proj = Project::new(
|
||||
&jack,
|
||||
name.into(),
|
||||
Clock::new(&jack, *bpm)?,
|
||||
|
|
@ -207,7 +182,7 @@ impl ProjectInit {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn print_status (project: &Arrangement) {
|
||||
pub fn print_status (project: &Project) {
|
||||
println!("Name: {:?}", &project.name);
|
||||
println!("JACK: {:?}", &project.jack);
|
||||
println!("Buffer: {:?}", &project.clock.chunk);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::*;
|
||||
|
||||
impl Arrangement {
|
||||
impl Project {
|
||||
/// Toggle looping for the active clip
|
||||
pub fn toggle_loop (&mut self) {
|
||||
if let Some(clip) = self.selected_clip() {
|
||||
|
|
@ -39,7 +39,7 @@ impl HasClipsSize for App {
|
|||
fn clips_size (&self) -> &Sizer { &self.project.size_inner }
|
||||
}
|
||||
|
||||
impl HasClipsSize for Arrangement {
|
||||
impl HasClipsSize for Project {
|
||||
fn clips_size (&self) -> &Sizer { &self.size_inner }
|
||||
}
|
||||
|
||||
|
|
@ -1,11 +1,5 @@
|
|||
use crate::*;
|
||||
|
||||
mod memo; pub use self::memo::*;
|
||||
mod moment; pub use self::moment::*;
|
||||
mod ticker; pub use self::ticker::*;
|
||||
mod timebase; pub use self::timebase::*;
|
||||
mod clock_view; pub use self::clock_view::*;
|
||||
|
||||
impl App {
|
||||
/// Update memoized render of clock values.
|
||||
/// ```
|
||||
|
|
@ -508,4 +502,3 @@ pub fn button_play_pause <'a> (playing: bool, compact: bool) -> impl Draw<Tui> {
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
674
src/config.rs
674
src/config.rs
|
|
@ -2,184 +2,12 @@ use crate::*;
|
|||
use std::path::PathBuf;
|
||||
use notify_debouncer_full::notify::{RecursiveMode, PollWatcher, Config as NotifyConfig, Watcher};
|
||||
|
||||
impl AsRef<Config> for Config {
|
||||
fn as_ref (&self) -> &Config {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Write initial contents of configuration.
|
||||
pub fn config_init <C: AsRef<Config>> (config: C) -> Usually<C> {
|
||||
if config.as_ref().find_file().is_none() {
|
||||
std::fs::write(config.as_ref().place_file()?, Config::DEFAULTS)?;
|
||||
}
|
||||
Ok(if let Some(path) = config.as_ref().find_file() {
|
||||
config_load(config, std::fs::read_to_string(&path)?)?
|
||||
} else {
|
||||
return Err(format!("config_init: not found").into())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn config_load <C: AsRef<Config>, L: Language> (config: C, src: L) -> Usually<C> {
|
||||
config.as_ref().clear();
|
||||
config.as_ref().stamp.store(quanta::Clock::new().raw(), Relaxed);
|
||||
src.each(config, |c, s|config_load_item(c, s.trim()))
|
||||
}
|
||||
|
||||
pub fn config_load_item <C: AsRef<Config>, L: Language> (config: C, src: L) -> Usually<C> {
|
||||
if let Some(expr) = src.expr()? {
|
||||
config_load_kind(config, expr)
|
||||
} else {
|
||||
Err(format!("Config::add_one: tried to add empty item").into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn config_load_kind <C: AsRef<Config>, L: Language> (config: C, src: L) -> Usually<C> {
|
||||
//if let Some(tail) = src.tail()? {
|
||||
match src.head()? {
|
||||
Some("mode") => modes_add(&config.as_ref().modes, src.tail()),
|
||||
Some("keys") => binds_add(&config.as_ref().binds, src.tail()),
|
||||
Some("view") => views_add(&config.as_ref().views, src.tail()),
|
||||
_ => return Err(format!("Config::load: expected view/keys/mode expr, got: {src:?}").into())
|
||||
}?;
|
||||
//}
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Watch a config's file for changes.
|
||||
pub fn config_watch (
|
||||
config: Arc<Config>, poll: Option<Duration>
|
||||
) -> Usually<()> {
|
||||
let poll = poll.unwrap_or(Duration::from_millis(250));
|
||||
let opts = NotifyConfig::default().with_poll_interval(poll);
|
||||
let mut watcher = ::notify_debouncer_full::notify::poll::PollWatcher::new({
|
||||
let config = config.clone();
|
||||
move|result|{
|
||||
match result {
|
||||
Ok(_events) => if let Err(e) = config_init(config.as_ref()) {
|
||||
*config.as_ref().error.try_write().unwrap() = Some(format!("{e:?}").into());
|
||||
panic!("{e:?}");
|
||||
} else {
|
||||
//println!("config updated");
|
||||
},
|
||||
Err(errors) => {
|
||||
panic!("{errors:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}, opts)?;
|
||||
if let Some(path) = config.as_ref().get_file() {
|
||||
//println!("watching: {path:?}");
|
||||
watcher.watch(&path, RecursiveMode::NonRecursive)?;
|
||||
*config.as_ref().watch.try_write().unwrap() = Some(watcher);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("no config path").into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a mode.
|
||||
pub fn modes_add <'a> (modes: &Modes, expr: impl Language) -> UsuallyRef<'a, ()> {
|
||||
let name = expr.head()?.ok_or("mode: missing name")?;
|
||||
let body = expr.tail()?.ok_or("mode: missing body")?;
|
||||
let mode = Mode::default();
|
||||
let mode = body.each(mode, |c, s|mode_add(c, s))?;
|
||||
modes.0.try_write().unwrap().insert(name.into(), Arc::new(mode));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl AsMut<Mode> for Mode {
|
||||
fn as_mut (&mut self) -> &mut Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a definition to the mode.
|
||||
///
|
||||
/// Supported definitions:
|
||||
///
|
||||
/// - (name ...) -> name
|
||||
/// - (info ...) -> description
|
||||
/// - (keys ...) -> key bindings
|
||||
/// - (mode ...) -> submode
|
||||
/// - ... -> view
|
||||
///
|
||||
/// ```
|
||||
/// let mut mode: tek::Mode = tek::mode_add(tek::Mode::default(), "(name hello)").unwrap();
|
||||
/// ```
|
||||
pub fn mode_add <T: AsMut<Mode>> (mut mode: T, dsl: impl Language) -> Usually<T> {
|
||||
Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() {
|
||||
//println!("Mode::add: {head} {:?}", expr.tail());
|
||||
let tail = expr.tail()?.map(|x|x.trim()).unwrap_or("");
|
||||
match head {
|
||||
"mode" => {
|
||||
let name = tail.head()?.ok_or("submode: missing name")?;
|
||||
let body = tail.tail()?.ok_or("submode: missing body")?;
|
||||
let submode = Mode::default();
|
||||
let submode = body.each(submode, |c,s|mode_add(c,s))?;
|
||||
let modes = mode.as_mut().modes.clone();
|
||||
modes.0.try_write().unwrap().insert(name.into(), Arc::new(submode));
|
||||
mode
|
||||
},
|
||||
"keys" => {
|
||||
tail.each(mode, |mut mode: T, expr: &str|{
|
||||
mode.as_mut().keys.push(expr.trim().into());
|
||||
Ok(mode)
|
||||
})?
|
||||
},
|
||||
"name" => { mode.as_mut().name.push(tail.into()); mode },
|
||||
"info" => { mode.as_mut().info.push(tail.into()); mode },
|
||||
"view" => { mode.as_mut().view.push(View::new(tail)?.into()); mode },
|
||||
_ => { mode.as_mut().view.push(View::new(tail)?.into()); mode },
|
||||
}
|
||||
} else if let Ok(Some(word)) = dsl.word() {
|
||||
mode.as_mut().view.push(View::new(word)?.into());
|
||||
mode
|
||||
} else {
|
||||
return Err(format!("Mode::add: unexpected: {dsl:?}").into());
|
||||
})
|
||||
}
|
||||
|
||||
pub fn binds_add <'a> (binds: &Binds, expr: impl Language) -> UsuallyRef<'a, ()> {
|
||||
//println!("\n\rbinds_add: {expr:?}");
|
||||
let name = expr.head()?.ok_or("bind: missing name")?;
|
||||
let body = expr.tail()?.unwrap_or("");
|
||||
binds.try_write().unwrap().insert(name.into(), {
|
||||
let mut map = Bind::new();
|
||||
body.each((), |_, item: &str|if matches!(item.expr().head(), Ok(Some("see"))) {
|
||||
// TODO
|
||||
Ok(())
|
||||
} else if let Ok(Some(_word)) = item.expr().head().word() {
|
||||
let expr = item.expr()?;
|
||||
let head = expr.head()?;
|
||||
if let Some(event) = TuiKey::from_dsl(&head)?.to_crossterm() {
|
||||
map.add(TuiEvent(event), Binding {
|
||||
commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(),
|
||||
condition: None,
|
||||
description: None,
|
||||
source: None
|
||||
});
|
||||
Ok(())
|
||||
} else if Some(":char") == item.expr()?.head()? {
|
||||
// TODO
|
||||
return Ok(())
|
||||
} else {
|
||||
return Err(format!("binds_add: invalid key: {:?}", item.expr()?.head()?).into())
|
||||
}
|
||||
} else {
|
||||
return Err(format!("binds_add: unexpected: {item:?}").into())
|
||||
})?;
|
||||
map
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Configuration: mode, view, and bind definitions.
|
||||
///
|
||||
/// ```
|
||||
/// use ::{std::sync::Arc, tek::{Config, modes_add, Mode}};
|
||||
/// use ::{std::sync::Arc, tek::{Config, Modes::add, Mode}};
|
||||
/// let config: Config = Default::default();
|
||||
/// modes_add(&config.modes, stringify!(
|
||||
/// Modes::add(&config.modes, stringify!(
|
||||
/// (mode :menu (name Menu)
|
||||
/// (info Mode selector.) (keys :axis/y :confirm)
|
||||
/// (view (bg (g 0) (bsp/s :ports/out
|
||||
|
|
@ -206,337 +34,82 @@ pub struct Config {
|
|||
pub watch: RwLock<Option<PollWatcher>>
|
||||
}
|
||||
|
||||
/// Collection of UI modes.
|
||||
///
|
||||
/// ```
|
||||
/// let mut modes = tek::Modes::default();
|
||||
/// assert_eq!(modes.len(), 0);
|
||||
/// let _ = modes.add(&":foo", &"(mode (name Foo) (info Bar))");
|
||||
/// assert_eq!(modes.len(), 1);
|
||||
/// ```
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct Modes(Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode>>>>);
|
||||
|
||||
/// Group of view and keys definitions.
|
||||
///
|
||||
/// ```
|
||||
/// let mode = tek::Mode::default();
|
||||
/// ```
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Mode {
|
||||
pub path: PathBuf,
|
||||
pub name: Vec<Arc<str>>,
|
||||
pub info: Vec<Arc<str>>,
|
||||
pub view: Vec<Arc<View<Tui, App>>>,
|
||||
pub keys: Vec<Arc<str>>,
|
||||
pub modes: Modes,
|
||||
impl AsRef<Config> for Config {
|
||||
fn as_ref (&self) -> &Config {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Collection of custom view definitions.
|
||||
pub type Views = Arc<RwLock<BTreeMap<Arc<str>, Arc<View<Tui, App>>>>>;
|
||||
|
||||
/// Custom view definition is a boxed closure emitting a [Draw]able from state `S`.
|
||||
pub struct View<S: Screen, T> {
|
||||
pub source: Arc<str>,
|
||||
pub render: Arc<Box<dyn Fn(&T, &mut S)->Drawn<S::Unit> + Send + Sync>>
|
||||
}
|
||||
|
||||
impl_debug!(<S: Screen, T> View<S, T> |self, w| { write!(w, "View({})", self.source) });
|
||||
|
||||
impl_display!(<S: Screen, T> View<S, T> |self, w| { write!(w, "View({})", self.source) });
|
||||
|
||||
impl View<Tui, App> {
|
||||
|
||||
pub fn new (source: impl AsRef<str>) -> Usually<Self> {
|
||||
Ok(Self {
|
||||
source: source.as_ref().into(),
|
||||
render: Self::compile(source)?
|
||||
})
|
||||
impl Config {
|
||||
/// Write initial contents of configuration.
|
||||
pub fn init <C: AsRef<Config>> (config: C) -> Usually<C> {
|
||||
if config.as_ref().find_file().is_none() {
|
||||
std::fs::write(config.as_ref().place_file()?, Config::DEFAULTS)?;
|
||||
}
|
||||
|
||||
fn boxed <F: Fn(&App, &mut Tui)->Drawn<u16> + Send + Sync + 'static> (f: F)
|
||||
-> Box<dyn Fn(&App, &mut Tui)->Drawn<u16> + Send + Sync + 'static>
|
||||
{
|
||||
Box::new(f)
|
||||
}
|
||||
|
||||
fn compile (source: impl AsRef<str>) ->
|
||||
Usually<Arc<Box<dyn Fn(&App, &mut Tui)->Drawn<u16> + Send + Sync>>>
|
||||
{
|
||||
let source = source.as_ref();
|
||||
let layer = if let Ok(Some(expr)) = source.expr() {
|
||||
Self::compile_expr(expr.into())?
|
||||
} else if let Ok(Some(word)) = source.word() {
|
||||
Self::compile_word(word.into())?
|
||||
Ok(if let Some(path) = config.as_ref().find_file() {
|
||||
Config::load(config, std::fs::read_to_string(&path)?)?
|
||||
} else {
|
||||
return Err(format!("not word/expr:\n{source:?}").into())
|
||||
};
|
||||
Ok(Arc::new(Box::new(move|state, screen|layer(state, screen))))
|
||||
}
|
||||
|
||||
fn compile_expr (expr: Arc<str>) ->
|
||||
Usually<Arc<Box<dyn Fn(&App, &mut Tui)->Drawn<u16> + Send + Sync>>>
|
||||
{
|
||||
|
||||
macro_rules! arg {
|
||||
($src:expr, $head:expr, $index:expr, $name:expr) => {
|
||||
$src.nth($index)?
|
||||
.ok_or_else(||Box::<dyn Error>::from(format!(
|
||||
"{}: no arg #{} ({}) in {}", $head, $index, $name, $src
|
||||
)))?
|
||||
};
|
||||
}
|
||||
|
||||
Ok(Arc::new(if let Some(head) = expr.head()? && let Some(ns) = head.split('/').next() {
|
||||
|
||||
match ns {
|
||||
|
||||
"when" => {
|
||||
let cond = Arc::from(arg!(expr, head, 1, "condition"));
|
||||
let cond = move|state: &App|state.namespace(&cond)?.ok_or_else(||Box::<dyn Error>::from("when: no condition value"));
|
||||
let thunk = Self::compile(arg!(expr, head, 2, "content"))?;
|
||||
Self::boxed(move|state, screen|{
|
||||
when(
|
||||
cond(state)?,
|
||||
draw(|screen|thunk(state, screen))
|
||||
).draw(screen)
|
||||
return Err(format!("config_init: not found").into())
|
||||
})
|
||||
},
|
||||
|
||||
"either" => {
|
||||
let cond = Arc::from(arg!(expr, head, 1, "condition"));
|
||||
let cond = move|state: &App|state.namespace(&cond)?.ok_or_else(||Box::<dyn Error>::from("either: no condition value"));
|
||||
let a = Self::compile(arg!(expr, head, 2, "content A"))?;
|
||||
let b = Self::compile(arg!(expr, head, 3, "content B"))?;
|
||||
Self::boxed(move|state, screen|{
|
||||
either(
|
||||
cond(state)?,
|
||||
draw(|screen|a(state, screen)),
|
||||
draw(|screen|b(state, screen)),
|
||||
).draw(screen)
|
||||
})
|
||||
},
|
||||
|
||||
"bsp" | "split" | "stack" => {
|
||||
let split = head.split('/').skip(1).next();
|
||||
let split = match split {
|
||||
Some("n") => Split::North,
|
||||
Some("s") => Split::South,
|
||||
Some("e") => Split::East,
|
||||
Some("w") => Split::West,
|
||||
Some("a") => Split::Above,
|
||||
Some("b") => Split::Below,
|
||||
_ => return Err(format!("invalid split: {split:?}").into())
|
||||
};
|
||||
let a = Self::compile(arg!(expr, head, 1, "content A"))?;
|
||||
let b = Self::compile(arg!(expr, head, 2, "content B"))?;
|
||||
Self::boxed(move|state, screen|split.stack(
|
||||
draw(|screen|a(state, screen)),
|
||||
draw(|screen|b(state, screen)),
|
||||
).draw(screen))
|
||||
},
|
||||
|
||||
"bar" => {
|
||||
let split = head.split('/').skip(1).next();
|
||||
let split = match split {
|
||||
Some("n") => Split::North,
|
||||
Some("s") => Split::South,
|
||||
Some("e") => Split::East,
|
||||
Some("w") => Split::West,
|
||||
Some("a") => Split::Above,
|
||||
Some("b") => Split::Below,
|
||||
_ => return Err(format!("invalid split: {split:?}").into())
|
||||
};
|
||||
let size = Arc::from(arg!(expr, head, 1, "size"));
|
||||
let size = move|state: &App|state.namespace(&size)?.ok_or_else(||Box::<dyn Error>::from("bar: no size"));
|
||||
let a = Self::compile(arg!(expr, head, 2, "content A"))?;
|
||||
let b = Self::compile(arg!(expr, head, 3, "content B"))?;
|
||||
Self::boxed(move|state, screen|split.bar(
|
||||
size(state)?,
|
||||
draw(|screen|a(state, screen)),
|
||||
draw(|screen|b(state, screen)),
|
||||
).draw(screen))
|
||||
},
|
||||
|
||||
"align" => {
|
||||
let azimuth = head.split('/').skip(1).next();
|
||||
let azimuth = match azimuth {
|
||||
Some("n") => Azimuth::N,
|
||||
Some("s") => Azimuth::S,
|
||||
Some("e") => Azimuth::E,
|
||||
Some("w") => Azimuth::W,
|
||||
Some("ne") => Azimuth::NE,
|
||||
Some("se") => Azimuth::SE,
|
||||
Some("nw") => Azimuth::NW,
|
||||
Some("sw") => Azimuth::SW,
|
||||
Some("c") => Azimuth::C,
|
||||
Some("x") => Azimuth::X,
|
||||
Some("y") => Azimuth::Y,
|
||||
_ => return Err(format!("invalid azimuth: {azimuth:?}").into())
|
||||
};
|
||||
let thunk = Self::compile(arg!(expr, head, 1, "content"))?;
|
||||
Self::boxed(move|state, screen|{
|
||||
Align(
|
||||
Some(azimuth),
|
||||
draw(|screen|thunk(state, screen))
|
||||
).draw(screen)
|
||||
})
|
||||
},
|
||||
|
||||
"full" | "fill" => {
|
||||
let thunk = Self::compile(arg!(expr, head, 1, "content"))?;
|
||||
match head.split('/').skip(1).next() {
|
||||
Some("w") | Some("x") => Self::boxed(move|state, screen|{
|
||||
Full::W(draw(|screen|thunk(state, screen))).draw(screen)
|
||||
}),
|
||||
Some("h") | Some("y") => Self::boxed(move|state, screen|{
|
||||
Full::H(draw(|screen|thunk(state, screen))).draw(screen)
|
||||
}),
|
||||
Some("wh") | Some("xy") | None => Self::boxed(move|state, screen|{
|
||||
Full::WH(draw(|screen|thunk(state, screen))).draw(screen)
|
||||
}),
|
||||
_ => unreachable!()
|
||||
}
|
||||
},
|
||||
|
||||
"exact" | "min" | "max" | "push" | "pull" | "pad" => {
|
||||
match head.split('/').skip(1).next() {
|
||||
Some("w") | Some("x") => {
|
||||
let value = Arc::from(arg!(expr, head, 1, "value"));
|
||||
let value = move|state: &App|state.namespace(&value);
|
||||
let thunk = Self::compile(arg!(expr, head, 2, "content"))?;
|
||||
match ns {
|
||||
"exact" => Self::boxed(move|state, screen|Exact::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||||
"push" => Self::boxed(move|state, screen|Push::X(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||||
"pull" => Self::boxed(move|state, screen|Pull::X(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||||
"pad" => Self::boxed(move|state, screen|Pad::X(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||||
"min" => Self::boxed(move|state, screen|Min::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||||
"max" => Self::boxed(move|state, screen|Max::W(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||||
_ => unreachable!()
|
||||
}
|
||||
},
|
||||
Some("h") | Some("y") => {
|
||||
let value = Arc::from(arg!(expr, head, 1, "value"));
|
||||
let value = move|state: &App|state.namespace(&value);
|
||||
let thunk = Self::compile(arg!(expr, head, 2, "content"))?;
|
||||
match ns {
|
||||
"exact" => Self::boxed(move|state, screen|Exact::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||||
"push" => Self::boxed(move|state, screen|Push::Y(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||||
"pull" => Self::boxed(move|state, screen|Pull::Y(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||||
"pad" => Self::boxed(move|state, screen|Pad::Y(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||||
"min" => Self::boxed(move|state, screen|Min::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||||
"max" => Self::boxed(move|state, screen|Max::H(draw(|screen|thunk(state, screen)), value(state)?).draw(screen)),
|
||||
_ => unreachable!()
|
||||
}
|
||||
},
|
||||
Some("wh") | Some("xy") | None => {
|
||||
let value1 = Arc::from(arg!(expr, head, 1, "value"));
|
||||
let value1 = move|state: &App|state.namespace(&value1);
|
||||
let value2 = Arc::from(arg!(expr, head, 2, "value"));
|
||||
let value2 = move|state: &App|state.namespace(&value2);
|
||||
let thunk = Self::compile(arg!(expr, head, 3, "content"))?;
|
||||
match ns {
|
||||
"exact" => Self::boxed(move|state, screen|Exact::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
|
||||
"push" => Self::boxed(move|state, screen|Push::XY(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
|
||||
"pull" => Self::boxed(move|state, screen|Pull::XY(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
|
||||
"pad" => Self::boxed(move|state, screen|Pad::XY(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
|
||||
"min" => Self::boxed(move|state, screen|Min::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
|
||||
"max" => Self::boxed(move|state, screen|Max::WH(draw(|screen|thunk(state, screen)), value1(state)?, value2(state)?).draw(screen)),
|
||||
_ => unreachable!()
|
||||
}
|
||||
},
|
||||
_ => unreachable!()
|
||||
}
|
||||
},
|
||||
|
||||
"fg" | "bg" => {
|
||||
let color = Arc::from(arg!(expr, head, 1, "color"));
|
||||
let color = move|state: &App|state.namespace(&color);
|
||||
let thunk = Self::compile(arg!(expr, head, 2, "content"))?;
|
||||
match ns {
|
||||
"fg" => Self::boxed(move|state, screen|fg(
|
||||
color(state)?.unwrap_or_default(), draw(|screen|thunk(state, screen))
|
||||
).draw(screen)),
|
||||
"bg" => Self::boxed(move|state, screen|bg(
|
||||
color(state)?.unwrap_or_default(), draw(|screen|thunk(state, screen))
|
||||
).draw(screen)),
|
||||
_ => unreachable!()
|
||||
}
|
||||
},
|
||||
|
||||
"text" => {
|
||||
let text: Arc<str> = Arc::from(expr.tail()?.unwrap_or_default());
|
||||
Self::boxed(move|_state, screen|{
|
||||
text.draw(screen)
|
||||
})
|
||||
},
|
||||
|
||||
_ => return Err(format!("compile_expr: unexpected: {expr:?}").into())
|
||||
|
||||
}
|
||||
|
||||
pub fn load <C: AsRef<Config>, L: Language> (config: C, src: L) -> Usually<C> {
|
||||
config.as_ref().clear();
|
||||
config.as_ref().stamp.store(quanta::Clock::new().raw(), Relaxed);
|
||||
src.each(config, |c, s|Config::load_item(c, s.trim()))
|
||||
}
|
||||
|
||||
pub fn load_item <C: AsRef<Config>, L: Language> (config: C, src: L) -> Usually<C> {
|
||||
if let Some(expr) = src.expr()? {
|
||||
Config::load_kind(config, expr)
|
||||
} else {
|
||||
return Err(format!("compile_expr: invalid expression: {expr:?}").into())
|
||||
}))
|
||||
Err(format!("Config::add_one: tried to add empty item").into())
|
||||
}
|
||||
}
|
||||
|
||||
fn compile_word (word: Arc<str>)
|
||||
-> Usually<Arc<Box<dyn Fn(&App, &mut Tui)->Drawn<u16> + Send + Sync>>>
|
||||
{
|
||||
macro_rules! draw {
|
||||
(|$state:ident|$body:expr)=>{Self::boxed(move|$state, to|$body.draw(to))}
|
||||
pub fn load_kind <C: AsRef<Config>, L: Language> (config: C, src: L) -> Usually<C> {
|
||||
//if let Some(tail) = src.tail()? {
|
||||
match src.head()? {
|
||||
Some("mode") => Modes::add(&config.as_ref().modes, src.tail()),
|
||||
Some("keys") => binds_add(&config.as_ref().binds, src.tail()),
|
||||
Some("view") => views_add(&config.as_ref().views, src.tail()),
|
||||
_ => return Err(format!("Config::load: expected view/keys/mode expr, got: {src:?}").into())
|
||||
}?;
|
||||
//}
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
Ok(Arc::new(match word.split("/").next() {
|
||||
//Some(":logo") => view_logo().draw(to),
|
||||
Some(":meters") => match word.split("/").skip(1).next() {
|
||||
Some("input") => Self::boxed(move|_, to|bg(Rgb(30, 30, 30), "Input Meters".align_s().full_h()).draw(to)),
|
||||
Some("output") => Self::boxed(move|_, to|bg(Rgb(30, 30, 30), "Output Meters".align_s().full_h()).draw(to)),
|
||||
_ => panic!()
|
||||
},
|
||||
Some(":tracks") => match word.split("/").skip(1).next() {
|
||||
None => Self::boxed(move|_, to|"TODO tracks".draw(to)),
|
||||
Some("names") => draw!(|app|app.project.view_track_names(app.color.clone())),
|
||||
Some("inputs") => draw!(|app|app.project.view_track_inputs(app.color.clone())),
|
||||
Some("devices") => draw!(|app|app.project.view_track_devices(app.color.clone())),
|
||||
Some("outputs") => draw!(|app|app.project.view_track_outputs(app.color.clone(), 0)),
|
||||
_ => panic!()
|
||||
},
|
||||
Some(":scenes") => match word.split("/").skip(1).next() {
|
||||
Some("clips") => draw!(|app|app.view_scenes_clips()),
|
||||
Some("names") => draw!(|app|app.view_scenes_names()),
|
||||
_ => panic!()
|
||||
},
|
||||
Some(":templates") => draw!(|app|view_templates(app)),
|
||||
Some(":browse/title") => draw!(|app|view_browse_title(app)),
|
||||
Some(":device") => draw!(|app|view_device(app)),
|
||||
Some(":dialog") => Self::boxed(move|state, to|draw_dialog(to, word.split("/").skip(1), state)),
|
||||
Some(":sessions") => Self::boxed(move|_, to|view_sessions().draw(to)),
|
||||
Some(":status") => Self::boxed(move|state, to|draw_status_bar(to, state)),
|
||||
Some(":editor") => Self::boxed(move|_, to|"TODO Editor".draw(to)),
|
||||
Some(":transport") => Self::boxed(move|_, to|view_transport(true, "", "", "").draw(to)),
|
||||
Some(":debug") => Self::boxed(move|_, to|format!("[{:?}]", to.area()).exact_h(1).draw(to)),
|
||||
Some(_) => Self::boxed(move|state, to|if let Some(view) = state.config.get_view(word.as_ref()) {
|
||||
(view.render)(state, to)
|
||||
/// Watch a config's file for changes.
|
||||
pub fn watch (
|
||||
config: Arc<Config>, poll: Option<Duration>
|
||||
) -> Usually<()> {
|
||||
let poll = poll.unwrap_or(Duration::from_millis(250));
|
||||
let opts = NotifyConfig::default().with_poll_interval(poll);
|
||||
let mut watcher = ::notify_debouncer_full::notify::poll::PollWatcher::new({
|
||||
let config = config.clone();
|
||||
move|result|{
|
||||
match result {
|
||||
Ok(_events) => if let Err(e) = Config::init(config.as_ref()) {
|
||||
*config.as_ref().error.try_write().unwrap() = Some(format!("{e:?}").into());
|
||||
panic!("{e:?}");
|
||||
} else {
|
||||
fg(Color::Rgb(128, 32, 32), format!("undefined: {word:?}")).draw(to)
|
||||
}),
|
||||
_ => unreachable!()
|
||||
}))
|
||||
//println!("config updated");
|
||||
},
|
||||
Err(errors) => {
|
||||
panic!("{errors:?}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_status_bar (to: &mut Tui, state: &App) -> Drawn<u16> {
|
||||
bar(Split::South, 1u16, dim(true, "Mode"), bold(true, &state.mode)).push_x(1).draw(to)
|
||||
}
|
||||
|
||||
/// Load custom view definition.
|
||||
pub fn views_add <'a> (views: &Views, expr: impl Language) -> UsuallyRef<'a, ()> {
|
||||
views.try_write().unwrap().insert(
|
||||
expr.head()?.ok_or("view: missing name")?.into(),
|
||||
View::new(expr.tail()?.ok_or("view: missing body")?)?.into()
|
||||
);
|
||||
}
|
||||
}
|
||||
}, opts)?;
|
||||
if let Some(path) = config.as_ref().get_file() {
|
||||
//println!("watching: {path:?}");
|
||||
watcher.watch(&path, RecursiveMode::NonRecursive)?;
|
||||
*config.as_ref().watch.try_write().unwrap() = Some(watcher);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("no config path").into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
|
@ -567,14 +140,14 @@ impl Config {
|
|||
/// Create, initialize, and watch a new configuration.
|
||||
pub fn watched <T: 'static> (callback: impl FnOnce(Arc<Config>)->T) -> Usually<T> {
|
||||
let config = Self::init_new(None)?;
|
||||
config_watch(config.clone(), None)?;
|
||||
Config::watch(config.clone(), None)?;
|
||||
let result = callback(config);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Create and initialize a new configuration.
|
||||
pub fn init_new (_dirs: Option<BaseDirectories>) -> Usually<Arc<Self>> {
|
||||
config_init(Arc::new(Self::new(None)))
|
||||
Config::init(Arc::new(Self::new(None)))
|
||||
}
|
||||
|
||||
/// Find the config file.
|
||||
|
|
@ -614,120 +187,3 @@ impl Config {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
impl Modes {
|
||||
/// Get a mode by name.
|
||||
pub fn get (&self, name: impl AsRef<str>) -> Option<Arc<Mode>> {
|
||||
self.0.try_read().unwrap().get(name.as_ref()).cloned()
|
||||
}
|
||||
/// Run something for each mode.
|
||||
pub fn for_each <T> (&self, mut ator: impl FnMut(&str, &Mode)->T) {
|
||||
for (k, v) in self.0.try_read().unwrap().iter() {
|
||||
let _ = ator(k.as_ref(), v.as_ref());
|
||||
}
|
||||
}
|
||||
/// Count modes.
|
||||
pub fn len (&self) -> usize {
|
||||
self.0.try_read().unwrap().len()
|
||||
}
|
||||
}
|
||||
|
||||
pub use self::bind::*;
|
||||
mod bind {
|
||||
use crate::*;
|
||||
|
||||
/// Collection of input bindings.
|
||||
pub type Binds = Arc<RwLock<BTreeMap<Arc<str>, Bind<TuiEvent, Arc<str>>>>>;
|
||||
|
||||
/// An map of input events (e.g. [TuiEvent]) to [Binding]s.
|
||||
///
|
||||
/// ```
|
||||
/// use ::{std::sync::Arc, tek::{Bind, tengri::TuiEvent}};
|
||||
/// let bind = Bind::<TuiEvent, Arc<str>>::load(&stringify!(
|
||||
/// (@x (nop))
|
||||
/// (@y (nop) (nop)))).unwrap();
|
||||
/// assert_eq!(bind.query(&'x'.into()).map(|x|x.len()), Some(1));
|
||||
/// //assert_eq!(bind.query(&'y'.into()).map(|x|x.len()), Some(2));
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct Bind<E, C>(
|
||||
/// Map of each event (e.g. key combination) to
|
||||
/// all command expressions bound to it by
|
||||
/// all loaded input layers.
|
||||
pub BTreeMap<E, Vec<Binding<C>>>
|
||||
);
|
||||
|
||||
/// A sequence of zero or more commands (e.g. [AppCommand]),
|
||||
/// optionally filtered by [Condition] to form layers.
|
||||
///
|
||||
/// ```
|
||||
/// //FIXME: Why does it overflow?
|
||||
/// //let binding: Binding<()> = tek::Binding { ..Default::default() };
|
||||
/// ```
|
||||
#[derive(Debug, Clone)] pub struct Binding<C> {
|
||||
pub commands: Arc<[C]>,
|
||||
pub condition: Option<Condition>,
|
||||
pub description: Option<Arc<str>>,
|
||||
pub source: Option<Arc<PathBuf>>,
|
||||
}
|
||||
|
||||
/// Condition that must evaluate to true in order to enable an input layer.
|
||||
///
|
||||
/// ```
|
||||
/// let condition = tek::Condition(std::sync::Arc::new(Box::new(||{true})));
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct Condition(pub Arc<Box<dyn Fn()->bool + Send + Sync>>);
|
||||
|
||||
impl Bind<TuiEvent, Arc<str>> {
|
||||
|
||||
}
|
||||
|
||||
/// Default is always empty map regardless if `E` and `C` implement [Default].
|
||||
impl<E, C> Default for Bind<E, C> {
|
||||
fn default () -> Self { Self(Default::default()) }
|
||||
}
|
||||
|
||||
impl<C: Default> Default for Binding<C> {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
commands: Default::default(),
|
||||
condition: Default::default(),
|
||||
description: Default::default(),
|
||||
source: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Clone + Ord, C> Bind<E, C> {
|
||||
/// Create a new event map
|
||||
pub fn new () -> Self {
|
||||
Default::default()
|
||||
}
|
||||
/// Add a binding to an owned event map.
|
||||
pub fn def (mut self, event: E, binding: Binding<C>) -> Self {
|
||||
self.add(event, binding);
|
||||
self
|
||||
}
|
||||
/// Add a binding to an event map.
|
||||
pub fn add (&mut self, event: E, binding: Binding<C>) -> &mut Self {
|
||||
if !self.0.contains_key(&event) {
|
||||
self.0.insert(event.clone(), Default::default());
|
||||
}
|
||||
self.0.get_mut(&event).unwrap().push(binding);
|
||||
self
|
||||
}
|
||||
/// Return the binding(s) that correspond to an event.
|
||||
pub fn query (&self, event: &E) -> Option<&[Binding<C>]> {
|
||||
self.0.get(event).map(|x|x.as_slice())
|
||||
}
|
||||
/// Return the first binding that corresponds to an event, considering conditions.
|
||||
pub fn dispatch (&self, event: &E) -> Option<&Binding<C>> {
|
||||
self.query(event)
|
||||
.map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next())
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
impl_debug!(Condition |self, w| { write!(w, "*") });
|
||||
}
|
||||
|
|
|
|||
212
src/device.rs
Normal file
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)
|
||||
-> Drawn<u16>
|
||||
{
|
||||
match frags.next() {
|
||||
|
||||
Some("menu") => if let Dialog::Menu(selected, items) = &state.dialog {
|
||||
Some(iter_south(move||items.0.iter().enumerate().map(move|(index, MenuItem(item, _))|{
|
||||
let f = if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) };
|
||||
let b = if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) };
|
||||
fg_bg(f, b, item).exact_h(2).align_x()
|
||||
})).exact_w(20).align_x())
|
||||
} else {
|
||||
None
|
||||
}.draw(to),
|
||||
|
||||
_ => unimplemented!("draw_dialog: ({frags:?})"),
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
|
||||
pub fn get_dialog <'a> (&self, src: impl Language) -> Perhaps<Dialog> {
|
||||
pub fn get_dialog <'a> (_: &App, src: impl Language) -> Perhaps<Dialog> {
|
||||
src.word()?.map(|word|Ok(match word {
|
||||
":dialog/none" => Dialog::None,
|
||||
":dialog/options" => Dialog::Options,
|
||||
|
|
@ -51,8 +30,6 @@ impl App {
|
|||
format!("not a dialog: {}", src.src()?.unwrap_or("<???>")).into()
|
||||
)
|
||||
})).transpose()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Various possible dialog modes.
|
||||
|
|
@ -63,68 +40,75 @@ impl App {
|
|||
#[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog {
|
||||
#[default] None,
|
||||
Help(usize),
|
||||
Menu(usize, MenuItems),
|
||||
Menu(usize, Arc<[MenuItem]>),
|
||||
Device(usize),
|
||||
Message(Arc<str>),
|
||||
Browse(BrowseTarget, Arc<Browse>),
|
||||
Options,
|
||||
}
|
||||
|
||||
/// List of menu items.
|
||||
///
|
||||
/// ```
|
||||
/// let items: tek::MenuItems = Default::default();
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default, PartialEq)] pub struct MenuItems(
|
||||
pub Arc<[MenuItem]>
|
||||
);
|
||||
pub fn draw_dialog <'a, I: Debug + Iterator<Item = &'a str>> (to: &mut Tui, mut frags: I, state: &App)
|
||||
-> Drawn<u16>
|
||||
{
|
||||
match frags.next() {
|
||||
|
||||
/// An item of a menu.
|
||||
///
|
||||
/// ```
|
||||
/// let item: tek::MenuItem = Default::default();
|
||||
/// ```
|
||||
#[derive(Clone)] pub struct MenuItem(
|
||||
/// Label
|
||||
pub Arc<str>,
|
||||
/// Callback
|
||||
pub Arc<Box<dyn Fn(&mut App)->Usually<()> + Send + Sync>>
|
||||
);
|
||||
Some("menu") => if let Dialog::Menu(selected, items) = &state.dialog {
|
||||
Some(iter_south(move||items.iter().enumerate().map(move|(index, MenuItem(item, _))|{
|
||||
let f = if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) };
|
||||
let b = if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) };
|
||||
fg_bg(f, b, item).exact_h(2).align_x()
|
||||
})).exact_w(20).align_x())
|
||||
} else {
|
||||
None
|
||||
}.draw(to),
|
||||
|
||||
impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) });
|
||||
|
||||
impl_default!(MenuItem: Self("".into(), Arc::new(Box::new(|_|Ok(())))));
|
||||
|
||||
impl PartialEq for MenuItem { fn eq (&self, other: &Self) -> bool { self.0 == other.0 } }
|
||||
|
||||
impl AsRef<Arc<[MenuItem]>> for MenuItems { fn as_ref (&self) -> &Arc<[MenuItem]> { &self.0 } }
|
||||
_ => unimplemented!("draw_dialog: ({frags:?})"),
|
||||
}
|
||||
}
|
||||
|
||||
impl Dialog {
|
||||
|
||||
/// ```
|
||||
/// let _ = tek::Dialog::welcome();
|
||||
/// ```
|
||||
pub fn welcome () -> Self {
|
||||
Self::Menu(1, MenuItems([
|
||||
Self::Menu(1, Arc::new([
|
||||
|
||||
MenuItem(" Resume session ".into(), Arc::new(Box::new(|_app|{
|
||||
Ok(())
|
||||
}))),
|
||||
MenuItem::new(" Resume session ", [
|
||||
AppCommand::Project {
|
||||
command: ProjectCommand::LoadLast
|
||||
}
|
||||
]),
|
||||
|
||||
MenuItem(" Create session ".into(), Arc::new(Box::new(|app|Ok({
|
||||
app.dialog = Dialog::None;
|
||||
app.mode = Some(":arranger".into());
|
||||
})))),
|
||||
MenuItem::new(" Create session ", [
|
||||
AppCommand::Dialog {
|
||||
dialog: Dialog::None
|
||||
},
|
||||
AppCommand::Mode {
|
||||
command: ModeCommand::Reset {
|
||||
modes: vec![":arranger".into()]
|
||||
}
|
||||
},
|
||||
]),
|
||||
|
||||
MenuItem(" Load session ".into(), Arc::new(Box::new(|_|{
|
||||
Ok(())
|
||||
}))),
|
||||
MenuItem::new(" Load session ", [
|
||||
AppCommand::Dialog {
|
||||
dialog: Dialog::Browse(
|
||||
BrowseTarget::LoadProject,
|
||||
Browse::new(None).unwrap().into()
|
||||
)
|
||||
//dialog: Dialog::Browse {
|
||||
//title: "Load session".into(),
|
||||
//filter: "\\.tek$",
|
||||
//}
|
||||
}
|
||||
]),
|
||||
|
||||
MenuItem(" Exit ".into(), Arc::new(Box::new(|app|Ok({
|
||||
app.exit.exit();
|
||||
println!("Exited cleanly.")
|
||||
})))),
|
||||
MenuItem::new(" Exit ", [
|
||||
AppCommand::Exit
|
||||
]),
|
||||
|
||||
].into()))
|
||||
]))
|
||||
}
|
||||
|
||||
/// FIXME: generalize
|
||||
|
|
@ -134,26 +118,29 @@ impl Dialog {
|
|||
pub fn menu_selected (&self) -> Option<usize> {
|
||||
if let Self::Menu(selected, _) = self { Some(*selected) } else { None }
|
||||
}
|
||||
|
||||
/// FIXME: generalize
|
||||
/// ```
|
||||
/// let _ = tek::Dialog::welcome().menu_next();
|
||||
/// ```
|
||||
pub fn menu_next (&self) -> Self {
|
||||
match self {
|
||||
Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.0.len()), items.clone()),
|
||||
Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.len()), items.clone()),
|
||||
_ => Self::None
|
||||
}
|
||||
}
|
||||
|
||||
/// FIXME: generalize
|
||||
/// ```
|
||||
/// let _ = tek::Dialog::welcome().menu_prev();
|
||||
/// ```
|
||||
pub fn menu_prev (&self) -> Self {
|
||||
match self {
|
||||
Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.0.len()), items.clone()),
|
||||
Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.len()), items.clone()),
|
||||
_ => Self::None
|
||||
}
|
||||
}
|
||||
|
||||
/// FIXME: generalize
|
||||
/// ```
|
||||
/// let _ = tek::Dialog::welcome().device_kind();
|
||||
|
|
@ -161,6 +148,7 @@ impl Dialog {
|
|||
pub fn device_kind (&self) -> Option<usize> {
|
||||
if let Self::Device(index) = self { Some(*index) } else { None }
|
||||
}
|
||||
|
||||
/// FIXME: generalize
|
||||
/// ```
|
||||
/// let _ = tek::Dialog::welcome().device_kind_next();
|
||||
|
|
@ -168,6 +156,7 @@ impl Dialog {
|
|||
pub fn device_kind_next (&self) -> Option<usize> {
|
||||
self.device_kind().map(|index|(index + 1) % device_kinds().len())
|
||||
}
|
||||
|
||||
/// FIXME: generalize
|
||||
/// ```
|
||||
/// let _ = tek::Dialog::welcome().device_kind_prev();
|
||||
|
|
@ -175,12 +164,16 @@ impl Dialog {
|
|||
pub fn device_kind_prev (&self) -> Option<usize> {
|
||||
self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1)))
|
||||
}
|
||||
|
||||
/// FIXME: implement
|
||||
pub fn message (&self) -> Option<&str> { todo!() }
|
||||
|
||||
/// FIXME: implement
|
||||
pub fn browser (&self) -> Option<&Arc<Browse>> { todo!() }
|
||||
|
||||
/// FIXME: implement
|
||||
pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() }
|
||||
|
||||
}
|
||||
|
||||
/// Increment a wrapping counter.
|
||||
|
|
@ -432,8 +432,3 @@ impl Draw<Tui> for MidiEditor {
|
|||
self.size.of(&self.mode).draw(to)
|
||||
}
|
||||
}
|
||||
|
||||
mod octave; pub use self::octave::*;
|
||||
mod piano; pub use self::piano::*;
|
||||
mod point; pub use self::point::*;
|
||||
mod range; pub use self::range::*;
|
||||
81
src/field.rs
Normal file
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::*;
|
||||
|
||||
#[cfg(feature = "lv2_gui")] use ::winit::{
|
||||
application::ApplicationHandler,
|
||||
event::WindowEvent,
|
||||
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
|
||||
window::{Window, WindowId},
|
||||
platform::x11::EventLoopBuilderExtX11
|
||||
};
|
||||
|
||||
/// A LV2 plugin.
|
||||
#[derive(Debug)] #[cfg(feature = "lv2")] pub struct Lv2 {
|
||||
#[derive(Debug)] pub struct Lv2 {
|
||||
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
||||
pub jack: Jack<'static>,
|
||||
pub name: Arc<str>,
|
||||
|
|
@ -30,11 +22,6 @@ use crate::*;
|
|||
pub lv2_ui_thread: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
/// A LV2 plugin's X11 UI.
|
||||
#[cfg(feature = "lv2_gui")] pub struct LV2PluginUI {
|
||||
pub window: Option<Window>
|
||||
}
|
||||
|
||||
impl_audio!(Lv2: lv2_jack_process);
|
||||
|
||||
impl Lv2 {
|
||||
|
|
@ -183,32 +170,3 @@ fn draw_header (state: &Lv2, to: &mut Tui, x: u16, y: u16, w: u16) {
|
|||
}
|
||||
//Ok(Rect { x, y, width: w, height: 1 })
|
||||
}
|
||||
|
||||
#[cfg(feature = "vst2")] impl<E: Engine> ::vst::host::Host for Plugin<E> {}
|
||||
|
||||
|
||||
#[cfg(feature = "vst2")] fn set_vst_plugin <E: Engine> (
|
||||
host: &Arc<Mutex<Plugin<E>>>, _path: &str
|
||||
) -> Usually<PluginKind> {
|
||||
let mut loader = ::vst::host::PluginLoader::load(
|
||||
&std::path::Path::new("/nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lxvst/helm.so"),
|
||||
host.clone()
|
||||
)?;
|
||||
Ok(PluginKind::VST2 {
|
||||
instance: loader.instance()?
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "lv2_gui")]
|
||||
pub fn run_lv2_ui (mut ui: LV2PluginUI) -> Usually<JoinHandle<()>> {
|
||||
Ok(spawn(move||{
|
||||
let event_loop = EventLoop::builder().with_x11().with_any_thread(true).build().unwrap();
|
||||
event_loop.set_control_flow(ControlFlow::Wait);
|
||||
event_loop.run_app(&mut ui).unwrap()
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(feature = "lv2_gui")]
|
||||
fn lv2_ui_instantiate (kind: &str) {
|
||||
//let host = Suil
|
||||
}
|
||||
26
src/lv2_gui.rs
Normal file
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()
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let _ = tek::view_meter("", 0.0);
|
||||
/// let _ = tek::view_meters(&[0.0, 0.0]);
|
||||
/// ```
|
||||
pub fn view_meter <'a> (label: &'a str, value: f32)
|
||||
-> impl Draw<Tui>
|
||||
{
|
||||
let f = field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value));
|
||||
let w = if value >= 0.0 { 13 }
|
||||
else if value >= -1.0 { 12 }
|
||||
else if value >= -2.0 { 11 }
|
||||
else if value >= -3.0 { 10 }
|
||||
else if value >= -4.0 { 9 }
|
||||
else if value >= -6.0 { 8 }
|
||||
else if value >= -9.0 { 7 }
|
||||
else if value >= -12.0 { 6 }
|
||||
else if value >= -15.0 { 5 }
|
||||
else if value >= -20.0 { 4 }
|
||||
else if value >= -25.0 { 3 }
|
||||
else if value >= -30.0 { 2 }
|
||||
else if value >= -40.0 { 1 }
|
||||
else { 0 };
|
||||
let c = if value >= 0.0 { Red }
|
||||
else if value >= -3.0 { Yellow }
|
||||
else { Green };
|
||||
south!(f, bg(c, ()).exact_wh(w, 1))
|
||||
}
|
||||
|
||||
pub fn view_meters (values: &[f32;2]) -> impl Draw<Tui> + use<'_> {
|
||||
let left = format!("L/{:>+9.3}", values[0]);
|
||||
let right = format!("R/{:>+9.3}", values[1]);
|
||||
south(left, right)
|
||||
}
|
||||
|
||||
#[cfg(test)] mod test_view_meter {
|
||||
use super::*;
|
||||
use proptest::prelude::*;
|
||||
72
src/mode.rs
Normal file
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::*;
|
||||
def_sizes_iter!(PortsSizes => Arc<str>, [Connect]);
|
||||
impl_has!(Vec<MidiInput>: |self: Arrangement| self.midi_ins);
|
||||
impl_has!(Vec<MidiOutput>: |self: Arrangement| self.midi_outs);
|
||||
impl_has!(Vec<MidiInput>: |self: Project| self.midi_ins);
|
||||
impl_has!(Vec<MidiOutput>: |self: Project| self.midi_outs);
|
||||
impl_has!(Vec<MidiInput>: |self: App|self.project.midi_ins);
|
||||
impl_has!(Vec<MidiOutput>: |self: App|self.project.midi_outs);
|
||||
|
||||
|
|
@ -1,12 +1,45 @@
|
|||
use crate::*;
|
||||
|
||||
impl AsRef<Option<Project>> for App {
|
||||
fn as_ref (&self) -> &Option<Project> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsMut<Option<Project>> for App {
|
||||
fn as_mut (&mut self) -> &mut Option<Project> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HasProject: AsRef<Option<Project>> + AsMut<Option<Project>> {}
|
||||
|
||||
impl<T: AsRef<Option<Project>> + AsMut<Option<Project>>> HasProject for T {}
|
||||
|
||||
impl<T: HasProject> ProjectController for T {}
|
||||
|
||||
#[tek_proc::commands(ProjectCommand)]
|
||||
pub trait ProjectController: HasProject {
|
||||
|
||||
#[command(LoadLast = "load-last")]
|
||||
fn load_last (&mut self) -> Perhaps<ProjectCommand> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
#[command(Load = "load")]
|
||||
fn load (&mut self) -> Perhaps<ProjectCommand> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Arranger.
|
||||
///
|
||||
/// ```
|
||||
/// let arranger = tek::Arrangement::default();
|
||||
/// let arranger = tek::Project::default();
|
||||
/// ```
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Arrangement {
|
||||
pub struct Project {
|
||||
/// JACK client handle.
|
||||
pub jack: Jack<'static>,
|
||||
/// Project name.
|
||||
|
|
@ -46,7 +79,7 @@ pub struct Arrangement {
|
|||
#[cfg(feature = "scene")] pub scene_scroll: usize,
|
||||
}
|
||||
|
||||
impl Arrangement {
|
||||
impl Project {
|
||||
|
||||
/// Create a new arrangement.
|
||||
pub fn new (
|
||||
|
|
@ -104,43 +137,15 @@ impl Arrangement {
|
|||
}
|
||||
}
|
||||
|
||||
/// Get the first sampler of the active track
|
||||
#[cfg(feature = "sampler")]
|
||||
impl Arrangement {
|
||||
pub fn sampler (&self) -> Option<&Sampler> {
|
||||
self.selected_track()?.sampler(0)
|
||||
}
|
||||
impl HasJack<'static> for Project { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
||||
impl_has!(Jack<'static>: |self: Project| self.jack);
|
||||
impl_has!(Sizer: |self: Project| self.size);
|
||||
impl_has!(Clock: |self: Project| self.clock);
|
||||
impl_has!(Selection: |self: Project| self.selection);
|
||||
impl_as_ref_opt!(MidiEditor: |self: Project| self.editor.as_ref());
|
||||
impl_as_mut_opt!(MidiEditor: |self: Project| self.editor.as_mut());
|
||||
|
||||
/// Get the first sampler of the active track
|
||||
#[cfg(feature = "sampler")]
|
||||
pub fn sampler_mut (&mut self) -> Option<&mut Sampler> {
|
||||
self.selected_track_mut()?.sampler_mut(0)
|
||||
}
|
||||
}
|
||||
|
||||
impl HasJack<'static> for Arrangement { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
||||
impl_has!(Jack<'static>: |self: Arrangement| self.jack);
|
||||
impl_has!(Sizer: |self: Arrangement| self.size);
|
||||
impl_has!(Clock: |self: Arrangement| self.clock);
|
||||
impl_has!(Selection: |self: Arrangement| self.selection);
|
||||
impl_as_ref_opt!(MidiEditor: |self: Arrangement| self.editor.as_ref());
|
||||
impl_as_mut_opt!(MidiEditor: |self: Arrangement| self.editor.as_mut());
|
||||
|
||||
/// Define a type alias for iterators of sized items (columns).
|
||||
macro_rules! def_sizes_iter {
|
||||
($Type:ident => $($Item:ty),+) => {
|
||||
pub trait $Type<'a>: Iterator<Item=(usize, $(&'a $Item,)+ usize, usize)> + Send + Sync + 'a {}
|
||||
impl<'a, T: Iterator<Item=(usize, $(&'a $Item,)+ usize, usize)> + Send + Sync + 'a> $Type<'a> for T {}
|
||||
}
|
||||
}
|
||||
|
||||
mod clip; pub use self::clip::*;
|
||||
mod scene; pub use self::scene::*;
|
||||
mod select; pub use self::select::*;
|
||||
mod port; pub use self::port::*;
|
||||
mod track; pub use self::track::*;
|
||||
|
||||
pub(self) fn swap_value <T: Clone + PartialEq, U> (
|
||||
pub fn swap_value <T: Clone + PartialEq, U> (
|
||||
target: &mut T, value: &T, returned: impl Fn(T)->U
|
||||
) -> Perhaps<U> {
|
||||
if *target == *value {
|
||||
|
|
@ -152,7 +157,7 @@ pub(self) fn swap_value <T: Clone + PartialEq, U> (
|
|||
}
|
||||
}
|
||||
|
||||
pub(self) fn toggle_bool <U> (
|
||||
pub fn toggle_bool <U> (
|
||||
target: &mut bool, value: &Option<bool>, returned: impl Fn(Option<bool>)->U
|
||||
) -> Perhaps<U> {
|
||||
let mut value = value.unwrap_or(!*target);
|
||||
|
|
@ -163,24 +168,3 @@ pub(self) fn toggle_bool <U> (
|
|||
Ok(Some(returned(Some(value))))
|
||||
}
|
||||
}
|
||||
|
||||
//pub fn per_track <'a, T: Draw<'_, Tui> + 'a, U: TracksSizes<'a>> (
|
||||
//tracks: impl Fn() -> U + Send + Sync + 'a,
|
||||
//callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||||
//) -> impl Draw<'_, Tui> + 'a {
|
||||
//per_track_top(tracks, move|index, track|callback(index, track).full_h().align_y())
|
||||
//}
|
||||
|
||||
//pub fn per_track_top <'a, T: Draw<'_, Tui> + 'a, U: TracksSizes<'a>> (
|
||||
//tracks: impl Fn() -> U + Send + Sync + 'a,
|
||||
//callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||||
//) -> impl Draw<'_, Tui> + 'a {
|
||||
//bg(Reset, iter_east(||tracks()
|
||||
//.map(move|(index, track, x1, x2): (usize, &'a Track, usize, usize)|{
|
||||
//fg_bg(
|
||||
//track.color.lightest.term,
|
||||
//track.color.base.term,
|
||||
//callback(index, track)
|
||||
//).exact_w((x2 - x1) as u16)
|
||||
//})).align_x())
|
||||
//}
|
||||
|
|
@ -8,6 +8,18 @@ pub(crate) use symphonia::{
|
|||
},
|
||||
};
|
||||
|
||||
/// Get the first sampler of the active track
|
||||
impl Project {
|
||||
pub fn sampler (&self) -> Option<&Sampler> {
|
||||
self.selected_track()?.sampler(0)
|
||||
}
|
||||
|
||||
/// Get the first sampler of the active track
|
||||
pub fn sampler_mut (&mut self) -> Option<&mut Sampler> {
|
||||
self.selected_track_mut()?.sampler_mut(0)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HasSampler: AsRef<Sampler> + AsMut<Sampler> {
|
||||
fn sampler (&self) -> &Sampler {
|
||||
self.as_ref()
|
||||
|
|
@ -57,8 +57,8 @@ impl Scene {
|
|||
|
||||
impl_as_ref_opt!(Scene: |self: App| self.project.as_ref_opt());
|
||||
impl_as_mut_opt!(Scene: |self: App| self.project.as_mut_opt());
|
||||
impl_as_ref_opt!(Scene: |self: Arrangement| self.selected_scene());
|
||||
impl_as_mut_opt!(Scene: |self: Arrangement| self.selected_scene_mut());
|
||||
impl_as_ref_opt!(Scene: |self: Project| self.selected_scene());
|
||||
impl_as_mut_opt!(Scene: |self: Project| self.selected_scene_mut());
|
||||
|
||||
impl<T: AsRefOpt<Scene> + AsMutOpt<Scene> + Send + Sync> HasScene for T {}
|
||||
|
||||
|
|
@ -123,7 +123,7 @@ pub trait SceneController: HasScene
|
|||
pub type SceneWith<'a, T> = (usize, &'a Scene, usize, usize, T);
|
||||
|
||||
def_sizes_iter!(ScenesSizes => Scene);
|
||||
impl_has!(Vec<Scene>: |self: Arrangement| self.scenes);
|
||||
impl_has!(Vec<Scene>: |self: Project| self.scenes);
|
||||
impl_as_ref!(Vec<Scene>: |self: App| self.project.as_ref());
|
||||
impl_as_mut!(Vec<Scene>: |self: App| self.project.as_mut());
|
||||
|
||||
|
|
@ -271,7 +271,7 @@ impl ScenesView for App {
|
|||
}
|
||||
}
|
||||
|
||||
impl ScenesView for Arrangement {
|
||||
impl ScenesView for Project {
|
||||
fn h_scenes (&self) -> u16 {
|
||||
(self.size.h() as u16).saturating_sub(20)
|
||||
}
|
||||
|
|
@ -311,7 +311,7 @@ pub trait HasSceneScroll: HasScenes {
|
|||
fn scene_scroll (&self) -> usize;
|
||||
}
|
||||
|
||||
impl HasSceneScroll for Arrangement {
|
||||
impl HasSceneScroll for Project {
|
||||
fn scene_scroll (&self) -> usize {
|
||||
self.scene_scroll
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::*;
|
||||
|
||||
impl_as_ref_opt!(Track: |self: Arrangement| self.selected_track());
|
||||
impl_as_mut_opt!(Track: |self: Arrangement| self.selected_track_mut());
|
||||
impl_as_ref_opt!(Track: |self: Project| self.selected_track());
|
||||
impl_as_mut_opt!(Track: |self: Project| self.selected_track_mut());
|
||||
impl_as_ref_opt!(Track: |self: App| self.project.as_ref_opt());
|
||||
impl_as_mut_opt!(Track: |self: App| self.project.as_mut_opt());
|
||||
impl <T: AsRef<Selection>+AsMut<Selection>> HasSelection for T {}
|
||||
|
|
@ -20,13 +20,13 @@
|
|||
(bar/s 2 (align/x (bg (g 12) :logo))
|
||||
:dialog/menu)))))
|
||||
|
||||
(keys :back (@escape (back)))
|
||||
(keys :confirm (@enter (dialog confirm)))
|
||||
(keys :back (@escape cancel))
|
||||
(keys :confirm (@enter confirm))
|
||||
(keys :axis/y (@up (axis (dec :y)))
|
||||
(@down (axis (inc :y))))
|
||||
|
||||
(mode :arranger (name Arranger) (info Launch grid.)
|
||||
(keys :clock :color :launch :scenes :tracks :global)
|
||||
(keys :clock :color :launch :scenes :tracks :global :history :saveload)
|
||||
(mode :editor (keys :editor))
|
||||
(mode :dialog (keys :dialog))
|
||||
(mode :message (keys :message))
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
(bar/n 2 :tracks/inputs
|
||||
(bar/e 16 :scenes/names :scenes/clips))))))))
|
||||
|
||||
(keys :global (see :history :saveload)
|
||||
(keys :global
|
||||
(@f1 (dialog :help))
|
||||
(@f8 (dialog :options))
|
||||
(@f10 (dialog :quit)))
|
||||
|
|
|
|||
966
src/tek.rs
966
src/tek.rs
|
|
@ -1,6 +1,59 @@
|
|||
#![allow(clippy::unit_arg)]
|
||||
//#![feature(impl_trait_in_assoc_type)] // Used by EntriesIterator; TODO remove
|
||||
|
||||
/// Define a type alias for iterators of sized items (columns).
|
||||
macro_rules! def_sizes_iter {
|
||||
($Type:ident => $($Item:ty),+) => {
|
||||
pub trait $Type<'a>: Iterator<Item=(usize, $(&'a $Item,)+ usize, usize)> + Send + Sync + 'a {}
|
||||
impl<'a, T: Iterator<Item=(usize, $(&'a $Item,)+ usize, usize)> + Send + Sync + 'a> $Type<'a> for T {}
|
||||
}
|
||||
}
|
||||
|
||||
mod app; pub use self::app::*;
|
||||
mod bind; pub use self::bind::*;
|
||||
mod binds; pub use self::binds::*;
|
||||
mod browse; pub use self::browse::*;
|
||||
mod cli; pub use self::cli::*;
|
||||
mod clip; pub use self::clip::*;
|
||||
mod clock; pub use self::clock::*;
|
||||
mod clock_view; pub use self::clock_view::*;
|
||||
mod config; pub use self::config::*;
|
||||
mod deps; pub use self::deps::*;
|
||||
mod device; pub use self::device::*;
|
||||
mod dialog; pub use self::dialog::*;
|
||||
mod editor; pub use self::editor::*;
|
||||
mod field; pub use self::field::*;
|
||||
mod memo; pub use self::memo::*;
|
||||
mod menu_item; pub use self::menu_item::*;
|
||||
mod meter; pub use self::meter::*;
|
||||
mod mix; pub use self::mix::*;
|
||||
mod mode; pub use self::mode::*;
|
||||
mod mode_stack; pub use self::mode_stack::*;
|
||||
mod modes; pub use self::modes::*;
|
||||
mod moment; pub use self::moment::*;
|
||||
mod octave; pub use self::octave::*;
|
||||
mod piano; pub use self::piano::*;
|
||||
mod point; pub use self::point::*;
|
||||
mod pool; pub use self::pool::*;
|
||||
mod port; pub use self::port::*;
|
||||
mod project; pub use self::project::*;
|
||||
mod range; pub use self::range::*;
|
||||
mod sampler; pub use self::sampler::*;
|
||||
mod scene; pub use self::scene::*;
|
||||
mod select; pub use self::select::*;
|
||||
mod sequence; pub use self::sequence::*;
|
||||
mod ticker; pub use self::ticker::*;
|
||||
mod timebase; pub use self::timebase::*;
|
||||
mod track; pub use self::track::*;
|
||||
mod views; pub use self::views::*;
|
||||
|
||||
#[cfg(feature = "lv2")] mod lv2;
|
||||
#[cfg(feature = "lv2")] pub use self::lv2::*;
|
||||
|
||||
#[cfg(feature = "lv2_gui")] mod lv2_gui;
|
||||
#[cfg(feature = "lv2_gui")] pub use self::lv2_gui::*;
|
||||
|
||||
#[cfg(feature = "vst2")] mod vst2;
|
||||
#[cfg(feature = "vst2")] pub use self::vst2::*;
|
||||
|
||||
/// Command-line entrypoint.
|
||||
#[allow(unused)]
|
||||
|
|
@ -16,891 +69,28 @@ fn main () -> Usually<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub use self::cli::*;
|
||||
mod cli;
|
||||
|
||||
pub use self::config::*;
|
||||
mod config;
|
||||
|
||||
pub use self::app::*;
|
||||
mod app {
|
||||
use crate::*;
|
||||
primitive!(u8: try_to_u8);
|
||||
primitive!(u16: try_to_u16);
|
||||
primitive!(usize: try_to_usize);
|
||||
primitive!(isize: try_to_isize);
|
||||
impl_has!(Clock: |self: App|self.project.clock);
|
||||
impl_has!(Dialog: |self: App|self.dialog);
|
||||
impl_has!(Jack<'static>: |self: App|self.jack);
|
||||
impl_has!(Pool: |self: App|self.pool);
|
||||
impl_has!(Selection: |self: App|self.project.selection);
|
||||
impl_as_ref_opt!(MidiEditor: |self: App|self.project.as_ref_opt());
|
||||
impl_as_mut_opt!(MidiEditor: |self: App|self.project.as_mut_opt());
|
||||
impl_has_clips!( |self: App|self.pool.clips);
|
||||
/// Total application state.
|
||||
///
|
||||
/// ```
|
||||
/// use tek::{HasTracks, HasScenes, TracksView, ScenesView};
|
||||
/// let mut app = tek::App::default();
|
||||
/// let _ = app.scenes_add_one(None, None).unwrap();
|
||||
/// let _ = app.update_clock();
|
||||
/// app.project.editor = Some(Default::default());
|
||||
/// //let _: Vec<_> = app.project.inputs_with_sizes().collect();
|
||||
/// //let _: Vec<_> = app.project.outputs_with_sizes().collect();
|
||||
/// let _: Vec<_> = app.project.tracks_with_sizes().collect();
|
||||
/// //let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect();
|
||||
/// //let _: Vec<_> = app.scenes_with_colors(true, 10).collect();
|
||||
/// //let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect();
|
||||
/// let _ = app.project.w();
|
||||
/// //let _ = app.project.w_sidebar();
|
||||
/// //let _ = app.project.w_tracks_area();
|
||||
/// let _ = app.project.h();
|
||||
/// //let _ = app.project.h_tracks_area();
|
||||
/// //let _ = app.project.h_inputs();
|
||||
/// //let _ = app.project.h_outputs();
|
||||
/// let _ = app.project.h_scenes();
|
||||
/// ```
|
||||
#[derive(Default, Debug)]
|
||||
#[namespace(MidiClip)]
|
||||
#[namespace(ItemColor)]
|
||||
#[namespace(ItemTheme)]
|
||||
#[namespace(isize)]
|
||||
#[namespace(u8)]
|
||||
#[namespace(Arc<[Connect]> get_arc_array_connect)]
|
||||
#[namespace(Arc<str> get_arc_str)]
|
||||
#[namespace(Color get_color)]
|
||||
#[namespace(ControlAxis get_axis)]
|
||||
#[namespace(Option<Arc<[Connect]>> get_opt_arc_array_connect)]
|
||||
#[namespace(Option<Arc<str>> get_opt_arc_str)]
|
||||
#[namespace(Option<ItemTheme> get_opt_itemtheme)]
|
||||
#[namespace(Option<Vec<Option<Arc<RwLock<MidiClip>>>>> get_opt_vec_opt_arc_rwlock_midiclip)]
|
||||
#[namespace(Option<u16> get_opt_u16)]
|
||||
#[namespace(Option<usize> get_opt_usize)]
|
||||
#[namespace(Option<bool> get_opt_bool)]
|
||||
#[namespace(bool get_bool)]
|
||||
#[namespace(u16 get_u16)]
|
||||
#[namespace(PathBuf get_path_buf)]
|
||||
#[namespace(usize get_usize)]
|
||||
#[namespace(Dialog App::get_dialog)]
|
||||
pub struct App {
|
||||
/// Exit flag
|
||||
pub exit: Exit,
|
||||
/// Base color.
|
||||
pub color: ItemTheme,
|
||||
/// Must not be dropped for the duration of the process
|
||||
pub jack: Jack<'static>,
|
||||
/// Display size
|
||||
pub size: Sizer,
|
||||
/// Performance counter
|
||||
pub perf: PerfModel,
|
||||
/// Available view modes and input bindings
|
||||
pub config: Arc<Config>,
|
||||
/// Currently selected mode
|
||||
pub mode: Option<Arc<str>>,
|
||||
/// Undo history
|
||||
pub history: Vec<(AppCommand, Option<AppCommand>)>,
|
||||
/// Dialog overlay
|
||||
pub dialog: Dialog,
|
||||
/// Contains all recently created clips.
|
||||
pub pool: Pool,
|
||||
/// Contains the currently edited musical arrangement
|
||||
pub project: Arrangement,
|
||||
/// Error, if any
|
||||
pub error: Arc<RwLock<Option<Arc<str>>>>,
|
||||
|
||||
#[cfg(feature = "prof2")]
|
||||
/// Tracing guard
|
||||
pub guard: Option<tracing_flame::FlushGuard<std::io::BufWriter<std::fs::File>>>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// Create a new application instance from a backend, project, config, and mode
|
||||
///
|
||||
/// ```
|
||||
/// let mut proj = tek::Arrangement::default();
|
||||
/// proj.jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack");
|
||||
/// let mut conf = tek::config_load(tek::Config::default(), "(mode hello)").unwrap();
|
||||
/// let tek = tek::App::new(None, proj, conf.into(), "hello");
|
||||
/// ```
|
||||
pub fn new (
|
||||
exit: Option<Exit>,
|
||||
project: Arrangement,
|
||||
config: Arc<Config>,
|
||||
mode: impl AsRef<str>
|
||||
) -> Self {
|
||||
App {
|
||||
exit: exit.unwrap_or_default(),
|
||||
jack: project.jack.clone(),
|
||||
color: ItemTheme::random(),
|
||||
dialog: Dialog::welcome(),
|
||||
mode: Some(mode.as_ref().into()),
|
||||
config,
|
||||
project,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_arc_str <'a> (_: &App, src: impl Language) -> Perhaps<Arc<str>> {
|
||||
Ok(src.src()?.map(|x|x.into()))
|
||||
}
|
||||
fn get_u16 <'a> (state: &App, src: impl Language) -> Perhaps<u16> {
|
||||
Ok(Some(match src.word()? {
|
||||
Some(":w/sidebar") => state.project.w_sidebar(state.editor().is_some()),
|
||||
Some(":h/sample-detail") => 6.max(state.size.h() as u16 * 3 / 9),
|
||||
_ => return try_to_u16(src)
|
||||
}))
|
||||
}
|
||||
fn get_usize <'a> (state: &App, src: impl Language) -> Perhaps<usize> {
|
||||
Ok(Some(match src.word()? {
|
||||
Some(":scene-count") => state.scenes().len(),
|
||||
Some(":track-count") => state.tracks().len(),
|
||||
Some(":device-kind") => state.dialog.device_kind().unwrap_or(0),
|
||||
Some(":device-kind/next") => state.dialog.device_kind_next().unwrap_or(0),
|
||||
Some(":device-kind/prev") => state.dialog.device_kind_prev().unwrap_or(0),
|
||||
_ => return try_to_usize(src)
|
||||
}))
|
||||
}
|
||||
fn get_bool <'a> (state: &App, src: impl Language) -> Perhaps<bool> {
|
||||
src.word()?.map(|word|Ok(match word {
|
||||
"Y" => true,
|
||||
"N" => false,
|
||||
":mode/editor" => state.project.editor.is_some(),
|
||||
":focused/dialog" => !matches!(state.dialog, Dialog::None),
|
||||
":focused/message" => matches!(state.dialog, Dialog::Message(..)),
|
||||
":focused/add_device" => matches!(state.dialog, Dialog::Device(..)),
|
||||
":focused/browser" => state.dialog.browser().is_some(),
|
||||
":focused/pool/import" => matches!(state.pool.mode, Some(PoolMode::Import(..))),
|
||||
":focused/pool/export" => matches!(state.pool.mode, Some(PoolMode::Export(..))),
|
||||
":focused/pool/rename" => matches!(state.pool.mode, Some(PoolMode::Rename(..))),
|
||||
":focused/pool/length" => matches!(state.pool.mode, Some(PoolMode::Length(..))),
|
||||
":focused/clip" => !state.editor_focused() && matches!(state.selection(), Selection::TrackClip{..}),
|
||||
":focused/track" => !state.editor_focused() && matches!(state.selection(), Selection::Track(..)),
|
||||
":focused/scene" => !state.editor_focused() && matches!(state.selection(), Selection::Scene(..)),
|
||||
":focused/mix" => !state.editor_focused() && matches!(state.selection(), Selection::Mix),
|
||||
_ => return Err(format!("not bool: {word}").into())
|
||||
})).transpose()
|
||||
}
|
||||
#[allow(unused)]
|
||||
fn get_selection <'a> (state: &App, src: impl Language) -> Perhaps<Selection> {
|
||||
src.word()?.map(|word|Ok(match word {
|
||||
":select/scene" => state.selection().select_scene(state.tracks().len()),
|
||||
":select/scene/next" => state.selection().select_scene_next(state.scenes().len()),
|
||||
":select/scene/prev" => state.selection().select_scene_prev(),
|
||||
":select/track" => state.selection().select_track(state.tracks().len()),
|
||||
":select/track/next" => state.selection().select_track_next(state.tracks().len()),
|
||||
":select/track/prev" => state.selection().select_track_prev(),
|
||||
_ => return Err(format!("not selection: {word}").into())
|
||||
})).transpose()
|
||||
}
|
||||
fn get_color <'a> (_: &App, src: impl Language) -> Perhaps<Color> {
|
||||
if let Some(expr) = src.expr()? {
|
||||
match (expr.head()?, expr.tail()?) {
|
||||
(Some("g"), Some(tail)) => {
|
||||
let n = try_to_u8(tail.head().map_err(Into::into))?
|
||||
.ok_or(LanguageError::domain("not gray"))?;
|
||||
Ok(Some(Color::Rgb(n, n, n)))
|
||||
},
|
||||
(Some("rgb"), Some(tail)) => {
|
||||
let r = try_to_u8(tail.head().map_err(Into::into))?
|
||||
.ok_or(LanguageError::domain("not red"))?;
|
||||
let g = try_to_u8(tail.tail().head().map_err(Into::into))?
|
||||
.ok_or(LanguageError::domain("not green"))?;
|
||||
let b = try_to_u8(tail.tail().tail().head().map_err(Into::into))?
|
||||
.ok_or(LanguageError::domain("not blue"))?;
|
||||
Ok(Some(Color::Rgb(r, g, b)))
|
||||
},
|
||||
(Some(_), _) => return Err(format!("not a color expression: {expr}").into()),
|
||||
(None, _) => return Err(format!("not a color expression: {expr}").into()),
|
||||
}
|
||||
} else if let Ok(Some(sym)) = src.word() {
|
||||
Ok(match sym {
|
||||
":color/bg" => Some(Color::Rgb(28, 32, 36)),
|
||||
":color/fg" => Some(Color::Rgb(98, 92, 96)),
|
||||
_ => return Err(format!("not a color: {sym}").into())
|
||||
})
|
||||
} else {
|
||||
return Err(format!("not a color: {:?}", src.src()?).into())
|
||||
}
|
||||
}
|
||||
#[allow(unused)]
|
||||
fn get_opt_u7 <'a> (state: &App, src: impl Language) -> Perhaps<Option<u7>> {
|
||||
src.word()?.map(|word|Ok(match word {
|
||||
":editor/pitch" => Some((
|
||||
state.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8
|
||||
).into()),
|
||||
_ => return Err(format!("unknown midi note: {word}").into())
|
||||
})).transpose()
|
||||
}
|
||||
fn get_opt_usize <'a> (state: &App, src: impl Language) -> Perhaps<Option<usize>> {
|
||||
src.word()?.map(|word|Ok(match word {
|
||||
":selected/scene" => state.selection().scene(),
|
||||
":selected/track" => state.selection().track(),
|
||||
_ => return Err(format!("unknown opt<usize>: {word}").into())
|
||||
})).transpose()
|
||||
}
|
||||
#[allow(unused)]
|
||||
fn get_clip <'a> (state: &App, src: impl Language) -> Perhaps<Option<Arc<RwLock<MidiClip>>>> {
|
||||
src.word()?.map(|word|Ok(match word {
|
||||
":selected/clip" if let Selection::TrackClip { track, scene } = state.selection() =>
|
||||
state.scenes()[*scene].clips[*track].clone(),
|
||||
_ => return Err(format!("not a clip: {word}").into())
|
||||
})).transpose()
|
||||
}
|
||||
fn get_axis <'a> (_: &App, src: impl Language) -> Perhaps<ControlAxis> {
|
||||
Ok(src.word()?.map(|word|Ok(match word {
|
||||
":x" => ControlAxis::X,
|
||||
":y" => ControlAxis::Y,
|
||||
_ => return Err(format!("unknown axis {word}"))
|
||||
})).transpose()?)
|
||||
}
|
||||
fn get_opt_u16 <'a> (_: &App, _src: impl Language) -> Perhaps<Option<u16>> {
|
||||
Ok(None)
|
||||
}
|
||||
fn get_opt_itemtheme <'a> (_: &App, _: impl Language) -> Perhaps<Option<ItemTheme>> {
|
||||
todo!()
|
||||
}
|
||||
fn get_opt_vec_opt_arc_rwlock_midiclip <'a> (_: &App, _: impl Language) -> Perhaps<Option<Vec<Option<Arc<RwLock<MidiClip>>>>>> {
|
||||
todo!()
|
||||
}
|
||||
fn get_opt_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps<Option<Arc<[Connect]>>> {
|
||||
todo!()
|
||||
}
|
||||
fn get_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps<Arc<[Connect]>> {
|
||||
todo!()
|
||||
}
|
||||
fn get_opt_arc_str <'a> (_: &App, _: impl Language) -> Perhaps<Option<Arc<str>>> {
|
||||
todo!()
|
||||
}
|
||||
fn get_opt_bool <'a> (_: &App, _: impl Language) -> Perhaps<Option<bool>> {
|
||||
todo!()
|
||||
}
|
||||
fn get_path_buf <'a> (_: &App, _: impl Language) -> Perhaps<PathBuf> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
pub use self::bind::*;
|
||||
mod bind {
|
||||
use crate::*;
|
||||
|
||||
tui_keys!(self: App, input {
|
||||
#[cfg(feature = "prof2")] profiling::scope!("App::tui_keys!");
|
||||
let name = self.mode.as_ref();
|
||||
let mode = name.and_then(|m|self.config.get_mode(m)).as_ref().map(Arc::clone);
|
||||
if let Some(mode) = mode {
|
||||
let binds = self.config.binds.clone();
|
||||
for id in mode.keys.iter() {
|
||||
if let Some(event_map) = binds.try_read().unwrap().get(id.as_ref())
|
||||
&& let Some(bindings) = event_map.query(input) {
|
||||
for binding in bindings {
|
||||
for command in binding.commands.iter() {
|
||||
if Self::command::<AppCommand>(self, command)? {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
impl App {
|
||||
fn command <
|
||||
T: Dispatch<Self> + Clone + Into<AppCommand>
|
||||
> (state: &mut Self, src: &str) -> Usually<bool> where
|
||||
Self: Namespace<T>,
|
||||
{
|
||||
if let Some(command) = Namespace::<T>::namespace(state, &src)? {
|
||||
match command.clone().dispatch(state) {
|
||||
Err(err) => {
|
||||
state.history.push((command.into(), None));
|
||||
return Err(err)
|
||||
},
|
||||
Ok(undo) => {
|
||||
state.history.push((command.into(), undo.map(Into::into)));
|
||||
return Ok(true)
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl_from!(AppCommand: |x: AxisCommand| AppCommand::Axis { command: x });
|
||||
impl_from!(AppCommand: |x: BrowseCommand| AppCommand::Browse { command: x });
|
||||
impl_from!(AppCommand: |x: DialogCommand| AppCommand::Dialog { command: x });
|
||||
impl_from!(AppCommand: |x: MidiClipCommand| AppCommand::MidiClip { command: x });
|
||||
impl_from!(AppCommand: |x: PoolCommand| AppCommand::Pool { command: x });
|
||||
impl_from!(AppCommand: |x: SceneCommand| AppCommand::Scene { command: x });
|
||||
impl_from!(AppCommand: |x: ScenesCommand| AppCommand::Scenes { command: x });
|
||||
impl_from!(AppCommand: |x: TrackCommand| AppCommand::Track { command: x });
|
||||
impl_from!(AppCommand: |x: TracksCommand| AppCommand::Tracks { command: x });
|
||||
#[tek_proc::commands(AppCommand)]
|
||||
impl App {
|
||||
#[command(Axis = "axis")]
|
||||
pub fn command_axis (&mut self, command: AxisCommand) -> Perhaps<AppCommand> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
#[command(Browse = "browse")]
|
||||
pub fn command_browse (&mut self, command: BrowseCommand) -> Perhaps<AppCommand> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
#[command(Dialog = "dialog")]
|
||||
pub fn command_dialog (&mut self, command: DialogCommand) -> Perhaps<AppCommand> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
#[command(MidiClip = "clip")]
|
||||
pub fn command_midi_clip (&mut self, command: MidiClipCommand) -> Perhaps<AppCommand> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
#[command(Pool = "pool")]
|
||||
pub fn command_pool (&mut self, command: PoolCommand) -> Perhaps<AppCommand> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
#[command(Scene = "scene")]
|
||||
pub fn command_scene (&mut self, command: SceneCommand) -> Perhaps<AppCommand> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
#[command(Scenes = "scenes")]
|
||||
pub fn command_scenes (&mut self, command: ScenesCommand) -> Perhaps<AppCommand> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
#[command(Track = "track")]
|
||||
pub fn command_track (&mut self, command: TrackCommand) -> Perhaps<AppCommand> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
#[command(Tracks = "tracks")]
|
||||
pub fn command_tracks (&mut self, command: TracksCommand) -> Perhaps<AppCommand> {
|
||||
Ok(command.dispatch(self)?.map(Into::into))
|
||||
}
|
||||
}
|
||||
|
||||
#[tek_proc::commands(DialogCommand)]
|
||||
impl App {
|
||||
/// Cancel current dialog
|
||||
#[command(Cancel = "cancel")]
|
||||
pub fn cancel (&mut self) -> Perhaps<DialogCommand> {
|
||||
todo!()
|
||||
}
|
||||
/// Confirm current dialog selection.
|
||||
#[command(Confirm = "confirm")]
|
||||
pub fn confirm (&mut self) -> Perhaps<DialogCommand> {
|
||||
Ok(match &self.dialog {
|
||||
Dialog::Menu(index, items) => {
|
||||
let callback = items.0[*index].1.clone();
|
||||
callback(self)?;
|
||||
None
|
||||
},
|
||||
_ => todo!(),
|
||||
})
|
||||
}
|
||||
/// Swap currently active modal dialog.
|
||||
///
|
||||
/// ```
|
||||
/// let _ = tek::App::default().set_dialog(tek::Dialog::welcome());
|
||||
/// ```
|
||||
#[command(SetDialog = "seg")]
|
||||
pub fn set_dialog (&mut self, dialog: Dialog) -> Perhaps<DialogCommand> {
|
||||
let mut dialog = dialog.clone();
|
||||
std::mem::swap(&mut self.dialog, &mut dialog);
|
||||
Ok(Some(DialogCommand::SetDialog { dialog }))
|
||||
}
|
||||
}
|
||||
|
||||
#[tek_proc::commands(AxisCommand)]
|
||||
impl App {
|
||||
/// Increment a given data axis.
|
||||
#[command(Inc = "inc")]
|
||||
pub fn inc (&mut self, axis: ControlAxis) -> Perhaps<AxisCommand> {
|
||||
Ok(match (&self.dialog, axis) {
|
||||
(Dialog::None, _) => todo!(),
|
||||
(Dialog::Menu(_, _), ControlAxis::Y) => {
|
||||
DialogCommand::SetDialog { dialog: self.dialog.menu_next() }.dispatch(self)?;
|
||||
None
|
||||
},
|
||||
_ => todo!()
|
||||
})
|
||||
}
|
||||
/// Decrement a given data axis.
|
||||
#[command(Dec = "dec")]
|
||||
pub fn dec (&mut self, axis: ControlAxis) -> Perhaps<AxisCommand> {
|
||||
Ok(match (&self.dialog, axis) {
|
||||
(Dialog::None, _) => None,
|
||||
(Dialog::Menu(_, _), ControlAxis::Y) => {
|
||||
DialogCommand::SetDialog { dialog: self.dialog.menu_prev() }.dispatch(self)?;
|
||||
None
|
||||
},
|
||||
_ => todo!()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A control axis.
|
||||
///
|
||||
/// ```
|
||||
/// let axis = tek::ControlAxis::X;
|
||||
/// ```
|
||||
#[derive(Debug, Copy, Clone)] pub enum ControlAxis {
|
||||
X, Y, Z, I
|
||||
}
|
||||
|
||||
//take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref()
|
||||
//.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten()));
|
||||
}
|
||||
|
||||
pub use self::device::*;
|
||||
mod device {
|
||||
use crate::*;
|
||||
|
||||
impl App {
|
||||
/// FIXME: generalize. Set picked device in device pick dialog.
|
||||
///
|
||||
/// ```
|
||||
/// tek::App::default().device_pick(0);
|
||||
/// ```
|
||||
pub fn device_pick (&mut self, index: usize) {
|
||||
self.dialog = Dialog::Device(index);
|
||||
}
|
||||
|
||||
/// FIXME: generalize. Add device to current track.
|
||||
pub fn add_device (&mut self, index: usize) -> Usually<()> {
|
||||
match index {
|
||||
0 => {
|
||||
let name = self.jack.with_client(|c|c.name().to_string());
|
||||
let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name();
|
||||
let track = self.track().expect("no active track");
|
||||
let port = format!("{}/Sampler", &track.name);
|
||||
let connect = Connect::exact(format!("{name}:{midi}"));
|
||||
let sampler = if let Ok(sampler) = Sampler::new(
|
||||
&self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]]
|
||||
) {
|
||||
self.dialog = Dialog::None;
|
||||
Device::Sampler(sampler)
|
||||
} else {
|
||||
self.dialog = Dialog::Message("Failed to add device.".into());
|
||||
return Err("failed to add device".into())
|
||||
};
|
||||
let track = self.track_mut().expect("no active track");
|
||||
track.devices.push(sampler);
|
||||
Ok(())
|
||||
},
|
||||
1 => {
|
||||
todo!();
|
||||
//Ok(())
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HasJack<'static> for App {
|
||||
fn jack (&self) -> &Jack<'static> {
|
||||
&self.jack
|
||||
}
|
||||
}
|
||||
|
||||
impl_audio!(App: tek_jack_process, tek_jack_event);
|
||||
|
||||
fn tek_jack_process (state: &mut App, client: &Client, scope: &ProcessScope) -> Control {
|
||||
let t0 = state.perf.get_t0();
|
||||
state.clock().update_from_scope(scope).unwrap();
|
||||
let midi_in = state.project.midi_input_collect(scope);
|
||||
if let Some(editor) = &state.editor() {
|
||||
let mut pitch: Option<u7> = None;
|
||||
for port in midi_in.iter() {
|
||||
for event in port.iter() {
|
||||
if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..}))
|
||||
= event
|
||||
{
|
||||
pitch = Some(key.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(pitch) = pitch {
|
||||
editor.set_note_pos(pitch.as_int() as usize);
|
||||
}
|
||||
}
|
||||
let result = state.project.tracks_jack_process(client, scope);
|
||||
state.perf.update_from_jack_scope(t0, scope);
|
||||
result
|
||||
}
|
||||
|
||||
fn tek_jack_event (state: &mut App, event: JackEvent) {
|
||||
use JackEvent::*;
|
||||
match event {
|
||||
SampleRate(sr) => { state.clock().timebase.sr.set(sr as f64); },
|
||||
PortRegistration(_id, true) => {
|
||||
//let port = self.jack().port_by_id(id);
|
||||
//println!("\rport add: {id} {port:?}");
|
||||
//println!("\rport add: {id}");
|
||||
},
|
||||
PortRegistration(_id, false) => {
|
||||
/*println!("\rport del: {id}")*/
|
||||
},
|
||||
PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ },
|
||||
PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ },
|
||||
ClientRegistration(_id, true) => {},
|
||||
ClientRegistration(_id, false) => {},
|
||||
ThreadInit => {},
|
||||
XRun => {},
|
||||
GraphReorder => {},
|
||||
_ => { panic!("{event:?}"); }
|
||||
}
|
||||
}
|
||||
|
||||
#[tek_proc::command(Device)]
|
||||
#[tek_proc::keyword(Device)]
|
||||
#[derive(Debug)]
|
||||
pub enum DeviceCommand {}
|
||||
|
||||
impl Device {
|
||||
pub fn name (&self) -> &str {
|
||||
match self {
|
||||
Self::Sampler(sampler) => sampler.name.as_ref(),
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
pub fn midi_ins (&self) -> &[MidiInput] {
|
||||
match self {
|
||||
//Self::Sampler(Sampler { midi_in, .. }) => &[midi_in],
|
||||
_ => todo!()
|
||||
}
|
||||
}
|
||||
pub fn midi_outs (&self) -> &[MidiOutput] {
|
||||
match self {
|
||||
Self::Sampler(_) => &[],
|
||||
_ => todo!()
|
||||
}
|
||||
}
|
||||
pub fn audio_ins (&self) -> &[AudioInput] {
|
||||
match self {
|
||||
Self::Sampler(Sampler { audio_ins, .. }) => audio_ins.as_slice(),
|
||||
_ => todo!()
|
||||
}
|
||||
}
|
||||
pub fn audio_outs (&self) -> &[AudioOutput] {
|
||||
match self {
|
||||
Self::Sampler(Sampler { audio_outs, .. }) => audio_outs.as_slice(),
|
||||
_ => todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A device that can be plugged into the chain.
|
||||
///
|
||||
/// ```
|
||||
/// let device = tek::Device::default();
|
||||
/// ```
|
||||
#[derive(Debug, Default)] pub enum Device {
|
||||
#[default]
|
||||
Bypass,
|
||||
Mute,
|
||||
#[cfg(feature = "sampler")]
|
||||
Sampler(Sampler),
|
||||
#[cfg(feature = "lv2")] // TODO
|
||||
Lv2(Lv2),
|
||||
#[cfg(feature = "vst2")] // TODO
|
||||
Vst2,
|
||||
#[cfg(feature = "vst3")] // TODO
|
||||
Vst3,
|
||||
#[cfg(feature = "clap")] // TODO
|
||||
Clap,
|
||||
#[cfg(feature = "sf2")] // TODO
|
||||
Sf2,
|
||||
}
|
||||
|
||||
/// Some sort of wrapper?
|
||||
pub struct DeviceAudio<'a>(pub &'a mut Device);
|
||||
|
||||
impl_audio!(|self: DeviceAudio<'a>, client, scope|{
|
||||
use Device::*;
|
||||
match self.0 {
|
||||
Mute => { Control::Continue },
|
||||
Bypass => { /*TODO*/ Control::Continue },
|
||||
#[cfg(feature = "sampler")] Sampler(sampler) => sampler.process(client, scope),
|
||||
#[cfg(feature = "lv2")] Lv2(lv2) => lv2.process(client, scope),
|
||||
#[cfg(feature = "vst2")] Vst2 => { todo!() }, // TODO
|
||||
#[cfg(feature = "vst3")] Vst3 => { todo!() }, // TODO
|
||||
#[cfg(feature = "clap")] Clap => { todo!() }, // TODO
|
||||
#[cfg(feature = "sf2")] Sf2 => { todo!() }, // TODO
|
||||
}
|
||||
});
|
||||
|
||||
pub fn device_kinds () -> &'static [&'static str] {
|
||||
&[
|
||||
#[cfg(feature = "sampler")] "Sampler",
|
||||
#[cfg(feature = "lv2")] "Plugin (LV2)",
|
||||
]
|
||||
}
|
||||
|
||||
impl<T: AsRef<Vec<Device>> + AsMut<Vec<Device>>> HasDevices for T {
|
||||
fn devices (&self) -> &Vec<Device> {
|
||||
self.as_ref()
|
||||
}
|
||||
fn devices_mut (&mut self) -> &mut Vec<Device> {
|
||||
self.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HasDevices: AsRef<Vec<Device>> + AsMut<Vec<Device>> {
|
||||
fn devices (&self) -> &Vec<Device> {
|
||||
self.as_ref()
|
||||
}
|
||||
fn devices_mut (&mut self) -> &mut Vec<Device> {
|
||||
self.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view_device <'a> (state: &'a App) -> impl Draw<Tui> {
|
||||
let selected = state.dialog.device_kind().unwrap();
|
||||
south(
|
||||
bold(true, "Add device"),
|
||||
iter_south(move||device_kinds().iter().enumerate().map(move|(i, _label)|{
|
||||
let b = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) };
|
||||
let l = if i == selected { "[ " } else { " " };
|
||||
let r = if i == selected { " ]" } else { " " };
|
||||
bg(b, east(l, west(r, "FIXME device name"))).full_w()
|
||||
})))
|
||||
}
|
||||
|
||||
pub mod arrange; pub use self::arrange::*;
|
||||
pub mod browse; pub use self::browse::*;
|
||||
pub mod pool; pub use self::pool::*;
|
||||
pub mod clock; pub use self::clock::*;
|
||||
pub mod dialog; pub use self::dialog::*;
|
||||
pub mod editor; pub use self::editor::*;
|
||||
pub mod meter; pub use self::meter::*;
|
||||
pub mod mix; pub use self::mix::*;
|
||||
pub mod sampler; pub use self::sampler::*;
|
||||
pub mod sequence; pub use self::sequence::*;
|
||||
|
||||
#[cfg(feature = "plugin")] pub mod plugin;
|
||||
#[cfg(feature = "plugin")] pub use self::plugin::*;
|
||||
}
|
||||
|
||||
pub use self::draw::*;
|
||||
mod draw {
|
||||
use crate::*;
|
||||
|
||||
/// The [Draw] implementation for [App] handles the loaded view,
|
||||
/// which is defined in terms of [dizzle] DSL.
|
||||
///
|
||||
/// If there is an error, the error is displayed. FIXME: overlay it
|
||||
/// Then, every top-level form of the DSL description is rendered.
|
||||
impl Draw<Tui> for App {
|
||||
fn draw (&self, to: &mut Tui) -> Drawn<u16> {
|
||||
#[cfg(feature = "prof2")] profiling::scope!("App::draw");
|
||||
//self.perf.cycle(&mut |_|{
|
||||
self.draw_error(to)?;
|
||||
self.draw_modes(to)?;
|
||||
self.draw_debug(to)?;
|
||||
#[cfg(feature = "prof2")] profiling::finish_frame!();
|
||||
Ok(Some(to.area().into()))
|
||||
//})
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn draw_error (&self, to: &mut Tui) -> Usually<()> {
|
||||
if let Some(e) = self.error.try_read().unwrap().as_ref() {
|
||||
e.as_ref().align_c().draw(to)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_modes (&self, to: &mut Tui) -> Usually<()> {
|
||||
if let Some(mode) = self.mode.as_ref().and_then(|m|self.config.get_mode(m)) {
|
||||
let mut error = false;
|
||||
for (index, view) in mode.view.iter().enumerate() {
|
||||
match (view.render)(self, to) {
|
||||
Ok(None) => {},
|
||||
Ok(Some(XYWH(.., w, h))) => {
|
||||
self.size.0.store(w as usize, Relaxed);
|
||||
self.size.1.store(h as usize, Relaxed);
|
||||
},
|
||||
Err(e) => {
|
||||
let message = format!(
|
||||
"Mode: {:?}\n\nLayer: #{index}\n\nError: {e}\n\nSource:\n{}",
|
||||
&mode.name,
|
||||
&view.source
|
||||
);
|
||||
*self.error.try_write().unwrap() = Some(message.into());
|
||||
error = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !error {
|
||||
*self.error.try_write().unwrap() = None;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(unused)] fn draw_debug (&self, to: &mut Tui) -> Drawn<u16> {
|
||||
east(
|
||||
format!("{}x{} ",
|
||||
self.size.0.load(Relaxed),
|
||||
self.size.1.load(Relaxed)),
|
||||
format!("{}/{} {} ",
|
||||
self.perf.used.load(Relaxed),
|
||||
self.perf.window.load(Relaxed),
|
||||
self.perf.clock.raw() / 10000000),
|
||||
).align_se().draw(to)
|
||||
}
|
||||
}
|
||||
|
||||
impl_has!(Sizer: |self: App|self.size);
|
||||
|
||||
pub trait HasWidth {
|
||||
const MIN_WIDTH: usize;
|
||||
/// Increment track width.
|
||||
fn width_inc (&mut self);
|
||||
/// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH].
|
||||
fn width_dec (&mut self);
|
||||
}
|
||||
|
||||
pub fn view_templates <'a> (state: &'a App) -> impl Draw<Tui> + use<'a> {
|
||||
let height = (state.config.modes.len() * 2) as u16;
|
||||
draw(move |to: &mut Tui|{
|
||||
let mut index = 0;
|
||||
state.config.modes.for_each(&mut |id: &str, profile: &Mode| {
|
||||
let b = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) };
|
||||
let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or("<no name>");
|
||||
let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or("<no info>");
|
||||
let fg1 = Rgb(224, 192, 128);
|
||||
let fg2 = Rgb(224, 128, 32);
|
||||
let field_name = fg(fg1, name).align_w().full_w();
|
||||
let field_id = fg(fg2, id).align_e().full_w();
|
||||
let field_info = info.align_w().full_w();
|
||||
let _ = bg(b, south(above(field_name, field_id), field_info))
|
||||
.full_w().exact_h(2).push_y((2 * index) as u16).draw(to);
|
||||
index += 1;
|
||||
});
|
||||
Ok(Some(to.area().into()))
|
||||
}).min_w(30).exact_h(height)
|
||||
}
|
||||
|
||||
pub fn field_h <'a, T: Screen> (
|
||||
_theme: ItemTheme, _head: impl Draw<T>, _body: impl Draw<T>
|
||||
) -> impl Draw<T> {
|
||||
}
|
||||
|
||||
pub fn field_v <'a, T: Screen> (
|
||||
_theme: ItemTheme, _head: impl Draw<T>, _body: impl Draw<T>
|
||||
) -> impl Draw<T> {
|
||||
}
|
||||
|
||||
pub fn view_sessions <'a> () -> impl Draw<Tui> {
|
||||
let h = 6;
|
||||
let w = Some(30);
|
||||
let f = Rgb(224, 192, 128);
|
||||
draw(move |to: &mut Tui|{
|
||||
for (index, name) in ["session1", "session2", "session3"].iter().enumerate() {
|
||||
let b = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) };
|
||||
let y = (2 * index) as u16;
|
||||
let h = 2;
|
||||
bg(b, fg(f, *name).align_w()).full_w().exact_h(h).push_y(y).draw(to)?;
|
||||
}
|
||||
Ok(Some(to.area().into()))
|
||||
}).min_w(w).exact_h(h)
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let bg = tengri::ratatui::style::Color::Red;
|
||||
/// let fg = tengri::ratatui::style::Color::Green;
|
||||
/// let _ = tek::view_wrap(bg, fg, "and then blue, too!");
|
||||
/// ```
|
||||
pub fn view_wrap <'a> (bg: Color, fg: Color, content: impl Draw<Tui>)
|
||||
-> impl Draw<Tui>
|
||||
{
|
||||
let left = fg_bg(bg, Reset, y_repeat("▐").exact_w(1));
|
||||
let right = fg_bg(bg, Reset, y_repeat("▌").exact_w(1));
|
||||
east(left, west(right, fg_bg(fg, bg, content)))
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let _ = tek::view_meter("", 0.0);
|
||||
/// let _ = tek::view_meters(&[0.0, 0.0]);
|
||||
/// ```
|
||||
pub fn view_meter <'a> (label: &'a str, value: f32)
|
||||
-> impl Draw<Tui>
|
||||
{
|
||||
let f = field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value));
|
||||
let w = if value >= 0.0 { 13 }
|
||||
else if value >= -1.0 { 12 }
|
||||
else if value >= -2.0 { 11 }
|
||||
else if value >= -3.0 { 10 }
|
||||
else if value >= -4.0 { 9 }
|
||||
else if value >= -6.0 { 8 }
|
||||
else if value >= -9.0 { 7 }
|
||||
else if value >= -12.0 { 6 }
|
||||
else if value >= -15.0 { 5 }
|
||||
else if value >= -20.0 { 4 }
|
||||
else if value >= -25.0 { 3 }
|
||||
else if value >= -30.0 { 2 }
|
||||
else if value >= -40.0 { 1 }
|
||||
else { 0 };
|
||||
let c = if value >= 0.0 { Red }
|
||||
else if value >= -3.0 { Yellow }
|
||||
else { Green };
|
||||
south!(f, bg(c, ()).exact_wh(w, 1))
|
||||
}
|
||||
|
||||
pub fn view_meters (values: &[f32;2]) -> impl Draw<Tui> + use<'_> {
|
||||
let left = format!("L/{:>+9.3}", values[0]);
|
||||
let right = format!("R/{:>+9.3}", values[1]);
|
||||
south(left, right)
|
||||
}
|
||||
|
||||
pub fn view_track_header <'a> (theme: ItemTheme, content: impl Draw<Tui>)
|
||||
-> impl Draw<Tui>
|
||||
{
|
||||
bg(theme.darker.term, content.align_e().full_w()).exact_w(12)
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let _ = tek::button_2("", "", true);
|
||||
/// let _ = tek::button_2("", "", false);
|
||||
/// ```
|
||||
pub fn button_2 <'a> (key: impl Draw<Tui>, label: impl Draw<Tui>, hide: bool)
|
||||
-> impl Draw<Tui>
|
||||
{
|
||||
let c1 = tui_orange();
|
||||
let c2 = tui_g(0);
|
||||
let c3 = tui_g(96);
|
||||
let c4 = tui_g(255);
|
||||
bold(true, fg_bg(c1, c2,
|
||||
bar(Split::East, 1u16, fg(c2, "▐"),
|
||||
bar(Split::East, 1u16, key,
|
||||
bar(Split::East, 1u16, fg(c3, "▐"),
|
||||
when(!hide, fg_bg(c4, c3, label)))))))
|
||||
}
|
||||
|
||||
/// ```
|
||||
/// let _ = tek::button_3("", "", "", true);
|
||||
/// let _ = tek::button_3("", "", "", false);
|
||||
/// ```
|
||||
pub fn button_3 <'a> (
|
||||
key: impl Draw<Tui>,
|
||||
label: impl Draw<Tui>,
|
||||
value: impl Draw<Tui>,
|
||||
editing: bool,
|
||||
) -> impl Draw<Tui> {
|
||||
let c1 = tui_orange();
|
||||
let c2 = tui_g(0);
|
||||
let c3 = tui_g(96);
|
||||
let c4 = tui_g(255);
|
||||
let c5 = tui_g(128);
|
||||
bold(true, bar(Split::East, 3u16,
|
||||
fg_bg(c1, c2,
|
||||
bar(Split::East, 1u16, fg(c2, "▐"),
|
||||
bar(Split::East, 1u16, key, fg(if editing { c5 } else { c3 }, "▐")))),
|
||||
east(
|
||||
when(!editing, east(fg_bg(c4, c3, label), fg_bg(c5, c3, "▐"),)),
|
||||
east(fg_bg(tui_g(224), c5, value), fg_bg(c5, Reset, "▌"), ))))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "cli"))]
|
||||
fn run_new_plain (config: Config) -> Usually<()> {
|
||||
let name = "tek";
|
||||
tengri::Tui::run_main(Jack::new_run(name, move|jack|{
|
||||
let mode = ":menu";
|
||||
let title = "untitled!";
|
||||
let bpm = 74.;
|
||||
let clock = Clock::new(&jack, Some(bpm))?;
|
||||
let tracks = [];
|
||||
let scenes = [];
|
||||
Ok(App::new(None, Project::new(
|
||||
&jack,
|
||||
title.into(),
|
||||
clock,
|
||||
tracks.into_iter(),
|
||||
scenes.into_iter(),
|
||||
connect_midi_ins(&jack, &"M", &[], None)?.into_iter(),
|
||||
connect_midi_outs(&jack, &"M", &[], None)?.into_iter(),
|
||||
[].into_iter().chain(connect_audio_ins(&jack, &"L", &[], None)?.into_iter())
|
||||
.chain(connect_audio_ins(&jack, &"R", &[], None)?.into_iter()),
|
||||
[].into_iter().chain(connect_audio_outs(&jack, &"L", &[], None)?.into_iter())
|
||||
.chain(connect_audio_outs(&jack, &"R", &[], None)?.into_iter()),
|
||||
), config, mode))
|
||||
})?)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{*, arrange::*};
|
||||
use crate::*;
|
||||
|
||||
/// A track consists of a sequencer and zero or more devices chained after it.
|
||||
///
|
||||
|
|
@ -155,7 +155,7 @@ pub(crate) fn track_width (_index: usize, track: &Track) -> u16 {
|
|||
}
|
||||
|
||||
def_sizes_iter!(TracksSizes => Track);
|
||||
impl_has!(Vec<Track>: |self: Arrangement| self.tracks);
|
||||
impl_has!(Vec<Track>: |self: Project| self.tracks);
|
||||
impl_as_ref!(Vec<Track>: |self: App| self.project.as_ref());
|
||||
impl_as_mut!(Vec<Track>: |self: App| self.project.as_mut());
|
||||
|
||||
|
|
@ -626,7 +626,7 @@ pub trait HasTrackScroll {
|
|||
fn tracks_last_mut (&mut self) -> &mut usize;
|
||||
}
|
||||
|
||||
impl HasTrackScroll for &mut Arrangement {
|
||||
impl HasTrackScroll for &mut Project {
|
||||
fn track_scroll (&self) -> usize {
|
||||
self.track_scroll
|
||||
}
|
||||
|
|
@ -638,7 +638,7 @@ impl HasTrackScroll for &mut Arrangement {
|
|||
}
|
||||
}
|
||||
|
||||
impl HasTrackScroll for Arrangement {
|
||||
impl HasTrackScroll for Project {
|
||||
fn track_scroll (&self) -> usize {
|
||||
self.track_scroll
|
||||
}
|
||||
|
|
@ -680,3 +680,30 @@ fn view_track_row_section <'a> (
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn view_track_header <'a> (theme: ItemTheme, content: impl Draw<Tui>)
|
||||
-> impl Draw<Tui>
|
||||
{
|
||||
bg(theme.darker.term, content.align_e().full_w()).exact_w(12)
|
||||
}
|
||||
|
||||
//pub fn per_track <'a, T: Draw<'_, Tui> + 'a, U: TracksSizes<'a>> (
|
||||
//tracks: impl Fn() -> U + Send + Sync + 'a,
|
||||
//callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||||
//) -> impl Draw<'_, Tui> + 'a {
|
||||
//per_track_top(tracks, move|index, track|callback(index, track).full_h().align_y())
|
||||
//}
|
||||
|
||||
//pub fn per_track_top <'a, T: Draw<'_, Tui> + 'a, U: TracksSizes<'a>> (
|
||||
//tracks: impl Fn() -> U + Send + Sync + 'a,
|
||||
//callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a
|
||||
//) -> impl Draw<'_, Tui> + 'a {
|
||||
//bg(Reset, iter_east(||tracks()
|
||||
//.map(move|(index, track, x1, x2): (usize, &'a Track, usize, usize)|{
|
||||
//fg_bg(
|
||||
//track.color.lightest.term,
|
||||
//track.color.base.term,
|
||||
//callback(index, track)
|
||||
//).exact_w((x2 - x1) as u16)
|
||||
//})).align_x())
|
||||
//}
|
||||
304
src/views.rs
Normal file
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