mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-08-07 22:17:07 +02:00
multiline view error
This commit is contained in:
parent
07f2290017
commit
b4424fcb69
15 changed files with 979 additions and 949 deletions
398
src/app.rs
398
src/app.rs
|
|
@ -1,398 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
pub mod audio; #[allow(unused)] pub use self::audio::*;
|
|
||||||
pub mod bind; pub use self::bind::*;
|
|
||||||
pub mod config; pub use self::config::*;
|
|
||||||
pub mod draw; pub use self::draw::*;
|
|
||||||
pub mod size; pub use self::size::*;
|
|
||||||
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!(Vec<MidiInput>: |self: App|self.project.midi_ins);
|
|
||||||
impl_has!(Vec<MidiOutput>: |self: App|self.project.midi_outs);
|
|
||||||
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!(Vec<Scene>: |self: App|self.project.as_ref());
|
|
||||||
impl_as_mut!(Vec<Scene>: |self: App|self.project.as_mut());
|
|
||||||
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.scene_add(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(u8)]
|
|
||||||
#[namespace(isize)]
|
|
||||||
#[namespace(ItemTheme)]
|
|
||||||
#[namespace(Arc<str> App::get_arc_str)]
|
|
||||||
#[namespace(u16 App::get_u16)]
|
|
||||||
#[namespace(usize App::get_usize)]
|
|
||||||
#[namespace(bool App::get_bool)]
|
|
||||||
#[namespace(Selection App::get_selection)]
|
|
||||||
#[namespace(Color App::get_color)]
|
|
||||||
#[namespace(Option<u7> App::get_opt_u7)]
|
|
||||||
#[namespace(Option<u16> App::get_opt_u16)]
|
|
||||||
#[namespace(Option<usize> App::get_opt_usize)]
|
|
||||||
#[namespace(Option<Arc<RwLock<MidiClip>>> App::get_clip)]
|
|
||||||
pub struct App {
|
|
||||||
/// 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: Config,
|
|
||||||
/// Currently selected mode
|
|
||||||
pub mode: Arc<Mode<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>>>>
|
|
||||||
}
|
|
||||||
impl App {
|
|
||||||
/// Create a new application instance from a backend, project, config, and mode
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack");
|
|
||||||
/// let proj = tek::Arrangement::default();
|
|
||||||
/// let mut conf = tek::Config::default();
|
|
||||||
/// conf.add("(mode hello)");
|
|
||||||
/// let tek = tek::App::new(&jack, proj, conf, "hello");
|
|
||||||
/// ```
|
|
||||||
pub fn new (
|
|
||||||
jack: &Jack<'static>, project: Arrangement, config: Config, mode: impl AsRef<str>
|
|
||||||
) -> Self {
|
|
||||||
let mode: &str = mode.as_ref();
|
|
||||||
App {
|
|
||||||
color: ItemTheme::random(),
|
|
||||||
dialog: Dialog::welcome(),
|
|
||||||
jack: jack.clone(),
|
|
||||||
mode: config.get_mode(mode).expect(&format!("failed to find mode '{mode}'")),
|
|
||||||
config,
|
|
||||||
project,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Update memoized render of clock values.
|
|
||||||
/// ```
|
|
||||||
/// tek::App::default().update_clock();
|
|
||||||
/// ```
|
|
||||||
pub fn update_clock (&self) {
|
|
||||||
ClockView::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set modal dialog.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let previous: tek::Dialog = tek::App::default().set_dialog(tek::Dialog::welcome());
|
|
||||||
/// ```
|
|
||||||
pub fn set_dialog (&mut self, mut dialog: Dialog) -> Dialog {
|
|
||||||
std::mem::swap(&mut self.dialog, &mut dialog);
|
|
||||||
dialog
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return reference to content browser if open.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// assert_eq!(tek::App::default().browser(), None);
|
|
||||||
/// ```
|
|
||||||
pub fn browser (&self) -> Option<&Browse> {
|
|
||||||
if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Is a MIDI editor currently focused?
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// tek::App::default().editor_focused();
|
|
||||||
/// ```
|
|
||||||
pub fn editor_focused (&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Toggle MIDI editor.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// tek::App::default().toggle_editor(None);
|
|
||||||
/// ```
|
|
||||||
pub fn toggle_editor (&mut self, value: Option<bool>) {
|
|
||||||
//FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed);
|
|
||||||
let value = value.unwrap_or_else(||!self.editor().is_some());
|
|
||||||
if value {
|
|
||||||
// Create new clip in pool when entering empty cell
|
|
||||||
if let Selection::TrackClip { track, scene } = *self.selection()
|
|
||||||
&& let Some(scene) = self.project.scenes.get_mut(scene)
|
|
||||||
&& let Some(slot) = scene.clips.get_mut(track)
|
|
||||||
&& slot.is_none()
|
|
||||||
&& let Some(track) = self.project.tracks.get_mut(track)
|
|
||||||
{
|
|
||||||
let (_index, clip) = self.pool.add_new_clip();
|
|
||||||
// autocolor: new clip colors from scene and track color
|
|
||||||
let color = track.color.base.mix(scene.color.base, 0.5);
|
|
||||||
clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into();
|
|
||||||
if let Some(editor) = &mut self.project.editor {
|
|
||||||
editor.set_clip(Some(&clip));
|
|
||||||
}
|
|
||||||
*slot = Some(clip.clone());
|
|
||||||
//Some(clip)
|
|
||||||
} else {
|
|
||||||
//None
|
|
||||||
}
|
|
||||||
} else if let Selection::TrackClip { track, scene } = *self.selection()
|
|
||||||
&& let Some(scene) = self.project.scenes.get_mut(scene)
|
|
||||||
&& let Some(slot) = scene.clips.get_mut(track)
|
|
||||||
&& let Some(clip) = slot.as_mut()
|
|
||||||
{
|
|
||||||
// Remove clip from arrangement when exiting empty clip editor
|
|
||||||
let mut swapped = None;
|
|
||||||
if clip.read().unwrap().count_midi_messages() == 0 {
|
|
||||||
std::mem::swap(&mut swapped, slot);
|
|
||||||
}
|
|
||||||
if let Some(clip) = swapped {
|
|
||||||
self.pool.delete_clip(&clip.read().unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn get_arc_str (&self, src: impl Language) -> Perhaps<Arc<str>> {
|
|
||||||
Ok(src.src()?.map(|x|x.into()))
|
|
||||||
}
|
|
||||||
fn get_u16 (&self, src: impl Language) -> Perhaps<u16> {
|
|
||||||
Ok(Some(match src.word()? {
|
|
||||||
Some(":w/sidebar") => self.project.w_sidebar(self.editor().is_some()),
|
|
||||||
Some(":h/sample-detail") => 6.max(self.size.h() as u16 * 3 / 9),
|
|
||||||
_ => return try_to_u16(src)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
fn get_usize (&self, src: impl Language) -> Perhaps<usize> {
|
|
||||||
Ok(Some(match src.word()? {
|
|
||||||
Some(":scene-count") => self.scenes().len(),
|
|
||||||
Some(":track-count") => self.tracks().len(),
|
|
||||||
Some(":device-kind") => self.dialog.device_kind().unwrap_or(0),
|
|
||||||
Some(":device-kind/next") => self.dialog.device_kind_next().unwrap_or(0),
|
|
||||||
Some(":device-kind/prev") => self.dialog.device_kind_prev().unwrap_or(0),
|
|
||||||
_ => return try_to_usize(src)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
fn get_bool (&self, src: impl Language) -> Perhaps<bool> {
|
|
||||||
src.word()?.map(|word|Ok(match word {
|
|
||||||
"Y" => true,
|
|
||||||
"N" => false,
|
|
||||||
":mode/editor" => self.project.editor.is_some(),
|
|
||||||
":focused/dialog" => !matches!(self.dialog, Dialog::None),
|
|
||||||
":focused/message" => matches!(self.dialog, Dialog::Message(..)),
|
|
||||||
":focused/add_device" => matches!(self.dialog, Dialog::Device(..)),
|
|
||||||
":focused/browser" => self.dialog.browser().is_some(),
|
|
||||||
":focused/pool/import" => matches!(self.pool.mode, Some(PoolMode::Import(..))),
|
|
||||||
":focused/pool/export" => matches!(self.pool.mode, Some(PoolMode::Export(..))),
|
|
||||||
":focused/pool/rename" => matches!(self.pool.mode, Some(PoolMode::Rename(..))),
|
|
||||||
":focused/pool/length" => matches!(self.pool.mode, Some(PoolMode::Length(..))),
|
|
||||||
":focused/clip" => !self.editor_focused() && matches!(self.selection(), Selection::TrackClip{..}),
|
|
||||||
":focused/track" => !self.editor_focused() && matches!(self.selection(), Selection::Track(..)),
|
|
||||||
":focused/scene" => !self.editor_focused() && matches!(self.selection(), Selection::Scene(..)),
|
|
||||||
":focused/mix" => !self.editor_focused() && matches!(self.selection(), Selection::Mix),
|
|
||||||
_ => return Err(format!("not bool: {word}").into())
|
|
||||||
})).transpose()
|
|
||||||
}
|
|
||||||
fn get_selection (&self, src: impl Language) -> Perhaps<Selection> {
|
|
||||||
src.word()?.map(|word|Ok(match word {
|
|
||||||
":select/scene" => self.selection().select_scene(self.tracks().len()),
|
|
||||||
":select/scene/next" => self.selection().select_scene_next(self.scenes().len()),
|
|
||||||
":select/scene/prev" => self.selection().select_scene_prev(),
|
|
||||||
":select/track" => self.selection().select_track(self.tracks().len()),
|
|
||||||
":select/track/next" => self.selection().select_track_next(self.tracks().len()),
|
|
||||||
":select/track/prev" => self.selection().select_track_prev(),
|
|
||||||
_ => return Err(format!("not selection: {word}").into())
|
|
||||||
})).transpose()
|
|
||||||
}
|
|
||||||
fn get_color (&self, 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(expr.tail().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(expr.tail().map_err(Into::into))?
|
|
||||||
.ok_or(LanguageError::Domain("not red"))?;
|
|
||||||
let g = try_to_u8(expr.tail().tail().head().map_err(Into::into))?
|
|
||||||
.ok_or(LanguageError::Domain("not green"))?;
|
|
||||||
let b = try_to_u8(expr.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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn get_opt_u7 (&self, src: impl Language) -> Perhaps<Option<u7>> {
|
|
||||||
src.word()?.map(|word|Ok(match word {
|
|
||||||
":editor/pitch" => Some((
|
|
||||||
self.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8
|
|
||||||
).into()),
|
|
||||||
_ => return Err(format!("unknown midi note: {word}").into())
|
|
||||||
})).transpose()
|
|
||||||
}
|
|
||||||
fn get_opt_u16 (&self, _src: impl Language) -> Perhaps<Option<u16>> {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
fn get_opt_usize (&self, src: impl Language) -> Perhaps<Option<usize>> {
|
|
||||||
src.word()?.map(|word|Ok(match word {
|
|
||||||
":selected/scene" => self.selection().scene(),
|
|
||||||
":selected/track" => self.selection().track(),
|
|
||||||
_ => return Err(format!("unknown opt<usize>: {word}").into())
|
|
||||||
})).transpose()
|
|
||||||
}
|
|
||||||
fn get_clip (&self, src: impl Language) -> Perhaps<Option<Arc<RwLock<MidiClip>>>> {
|
|
||||||
src.word()?.map(|word|Ok(match word {
|
|
||||||
":selected/clip" if let Selection::TrackClip { track, scene } = self.selection() =>
|
|
||||||
self.scenes()[*scene].clips[*track].clone(),
|
|
||||||
_ => return Err(format!("not a clip: {word}").into())
|
|
||||||
})).transpose()
|
|
||||||
}
|
|
||||||
pub fn inc (&mut self, axis: &ControlAxis) -> Perhaps<AppCommand> {
|
|
||||||
Ok(match (&self.dialog, axis) {
|
|
||||||
(Dialog::None, _) => todo!(),
|
|
||||||
(Dialog::Menu(_, _), ControlAxis::Y) =>
|
|
||||||
AppCommand::SetDialog { dialog: self.dialog.menu_next() }.act(self)?,
|
|
||||||
_ => todo!()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
pub fn dec (&mut self, axis: &ControlAxis) -> Perhaps<AppCommand> {
|
|
||||||
Ok(match (&self.dialog, axis) {
|
|
||||||
(Dialog::None, _) => None,
|
|
||||||
(Dialog::Menu(_, _), ControlAxis::Y) =>
|
|
||||||
AppCommand::SetDialog { dialog: self.dialog.menu_prev() }.act(self)?,
|
|
||||||
_ => todo!()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
pub fn confirm (&mut self) -> Perhaps<AppCommand> {
|
|
||||||
Ok(match &self.dialog {
|
|
||||||
Dialog::Menu(index, items) => {
|
|
||||||
let callback = items.0[*index].1.clone();
|
|
||||||
callback(self)?;
|
|
||||||
None
|
|
||||||
},
|
|
||||||
_ => todo!(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn swap_value <T: Clone + PartialEq, U> (
|
|
||||||
target: &mut T, value: &T, returned: impl Fn(T)->U
|
|
||||||
) -> Perhaps<U> {
|
|
||||||
if *target == *value {
|
|
||||||
Ok(None)
|
|
||||||
} else {
|
|
||||||
let mut value = value.clone();
|
|
||||||
std::mem::swap(target, &mut value);
|
|
||||||
Ok(Some(returned(value)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
if value == *target {
|
|
||||||
Ok(None)
|
|
||||||
} else {
|
|
||||||
std::mem::swap(target, &mut value);
|
|
||||||
Ok(Some(returned(Some(value))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn scan (dir: &PathBuf) -> Usually<(Vec<OsString>, Vec<OsString>)> {
|
|
||||||
let (mut subdirs, mut files) = std::fs::read_dir(dir)?
|
|
||||||
.fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{
|
|
||||||
let entry = entry.expect("failed to read drectory entry");
|
|
||||||
let meta = entry.metadata().expect("failed to read entry metadata");
|
|
||||||
if meta.is_file() {
|
|
||||||
files.push(entry.file_name());
|
|
||||||
} else if meta.is_dir() {
|
|
||||||
subdirs.push(entry.file_name());
|
|
||||||
}
|
|
||||||
(subdirs, files)
|
|
||||||
});
|
|
||||||
subdirs.sort();
|
|
||||||
files.sort();
|
|
||||||
Ok((subdirs, files))
|
|
||||||
}
|
|
||||||
|
|
@ -11,7 +11,8 @@ fn tek_commands_collect (app: &App, input: &TuiEvent)
|
||||||
-> Usually<Vec<AppCommand>>
|
-> Usually<Vec<AppCommand>>
|
||||||
{
|
{
|
||||||
let mut commands = vec![];
|
let mut commands = vec![];
|
||||||
for id in app.mode.keys.iter() {
|
if let Some(ref mode) = app.mode {
|
||||||
|
for id in mode.keys.iter() {
|
||||||
if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref())
|
if let Some(event_map) = app.config.binds.clone().read().unwrap().get(id.as_ref())
|
||||||
&& let Some(bindings) = event_map.query(input) {
|
&& let Some(bindings) = event_map.query(input) {
|
||||||
for binding in bindings {
|
for binding in bindings {
|
||||||
|
|
@ -23,6 +24,7 @@ fn tek_commands_collect (app: &App, input: &TuiEvent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Ok(commands)
|
Ok(commands)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,49 @@ impl Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mod views; pub use self::views::*;
|
pub fn print_config (config: &Config) {
|
||||||
mod modes; pub use self::modes::*;
|
use ::ansi_term::Color::*;
|
||||||
mod mode; pub use self::mode::*;
|
println!("{:?}", config.dirs);
|
||||||
|
for (k, v) in config.views.read().unwrap().iter() {
|
||||||
|
println!("{} {} {v}", Green.paint("VIEW"), Green.bold().paint(format!("{k:<16}")));
|
||||||
|
}
|
||||||
|
for (k, v) in config.binds.read().unwrap().iter() {
|
||||||
|
println!("{} {}", Green.paint("BIND"), Green.bold().paint(format!("{k:<16}")));
|
||||||
|
for (k, v) in v.0.iter() {
|
||||||
|
print!("{} ", &Yellow.paint(match &k.0 {
|
||||||
|
Event::Key(KeyEvent { modifiers, .. }) =>
|
||||||
|
format!("{:>16}", format!("{modifiers}")),
|
||||||
|
_ => unimplemented!()
|
||||||
|
}));
|
||||||
|
print!("{}", &Yellow.bold().paint(match &k.0 {
|
||||||
|
Event::Key(KeyEvent { code, .. }) =>
|
||||||
|
format!("{:<10}", format!("{code}")),
|
||||||
|
_ => unimplemented!()
|
||||||
|
}));
|
||||||
|
for v in v.iter() {
|
||||||
|
print!(" => {:?}", v.commands);
|
||||||
|
print!(" {}", v.condition.as_ref().map(|x|format!("{x:?}")).unwrap_or_default());
|
||||||
|
println!(" {}", v.description.as_ref().map(|x|x.as_ref()).unwrap_or_default());
|
||||||
|
//println!(" {:?}", v.source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config.modes.for_each(|k, v|{
|
||||||
|
println!();
|
||||||
|
for v in v.name.iter() { print!("{}", Green.bold().paint(format!("{v} "))); }
|
||||||
|
for v in v.info.iter() { print!("\n{}", Green.paint(format!("{v}"))); }
|
||||||
|
print!("\n{} {}", Blue.paint("TOOL"), Green.bold().paint(format!("{k:<16}")));
|
||||||
|
print!("\n{}", Blue.paint("KEYS"));
|
||||||
|
for v in v.keys.iter() { print!("{}", Green.paint(format!(" {v}"))); }
|
||||||
|
println!();
|
||||||
|
v.modes.for_each(|k, v|{
|
||||||
|
print!("{} {} {:?}", Blue.paint("MODE"), Green.bold().paint(format!("{k:<16}")), v.name);
|
||||||
|
print!( " INFO={:?}", v.info);
|
||||||
|
print!( " VIEW={:?}", v.view);
|
||||||
|
println!(" KEYS={:?}", v.keys);
|
||||||
|
});
|
||||||
|
print!("{}", Blue.paint("VIEW"));
|
||||||
|
for v in v.view.iter() { print!("{}", Green.paint(format!(" {v}"))); }
|
||||||
|
println!();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
/// Collection of UI modes.
|
|
||||||
#[derive(Default, Debug, Clone)]
|
|
||||||
pub struct Modes(
|
|
||||||
Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode<Arc<str>>>>>>
|
|
||||||
);
|
|
||||||
|
|
||||||
impl Modes {
|
|
||||||
pub fn add (&self, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
|
|
||||||
let mut mode = Mode::default();
|
|
||||||
body.each(|item|mode.add(item))?;
|
|
||||||
self.0.write().unwrap().insert(name.as_ref().into(), Arc::new(mode));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
pub fn get (&self, name: impl AsRef<str>) -> Option<Arc<Mode<Arc<str>>>> {
|
|
||||||
self.0.read().unwrap().get(name.as_ref()).cloned()
|
|
||||||
}
|
|
||||||
pub fn for_each <T> (&self, mut ator: impl FnMut(&str, &Mode<Arc<str>>)->T) {
|
|
||||||
for (k, v) in self.0.read().unwrap().iter() {
|
|
||||||
let _ = ator(k.as_ref(), v.as_ref());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn len (&self) -> usize {
|
|
||||||
self.0.read().unwrap().len()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
/// Collection of custom view definitions.
|
|
||||||
pub type Views = Arc<RwLock<BTreeMap<Arc<str>, Arc<str>>>>;
|
|
||||||
|
|
||||||
/// Load custom view definition.
|
|
||||||
pub(crate) fn load_view (views: &Views, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
|
|
||||||
views.write().unwrap().insert(name.as_ref().into(), body.src()?.unwrap_or_default().into());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
175
src/app/draw.rs
175
src/app/draw.rs
|
|
@ -1,5 +1,21 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
|
/// Collection of custom view definitions.
|
||||||
|
pub type Views = Arc<RwLock<BTreeMap<Arc<str>, Arc<str>>>>;
|
||||||
|
|
||||||
|
/// Load custom view definition.
|
||||||
|
pub(crate) fn load_view (
|
||||||
|
views: &Views,
|
||||||
|
name: &impl AsRef<str>,
|
||||||
|
body: &impl Language,
|
||||||
|
) -> Usually<()> {
|
||||||
|
views.write().unwrap().insert(
|
||||||
|
name.as_ref().into(),
|
||||||
|
body.src()?.unwrap_or_default().into()
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// The [Draw] implementation for [App] handles the loaded view,
|
/// The [Draw] implementation for [App] handles the loaded view,
|
||||||
/// which is defined in terms of [dizzle] DSL.
|
/// which is defined in terms of [dizzle] DSL.
|
||||||
///
|
///
|
||||||
|
|
@ -9,17 +25,24 @@ impl View<Tui> for App {
|
||||||
fn view (&self) -> impl Draw<Tui> {
|
fn view (&self) -> impl Draw<Tui> {
|
||||||
thunk(|to: &mut Tui|{
|
thunk(|to: &mut Tui|{
|
||||||
let xywh = to.area().into();
|
let xywh = to.area().into();
|
||||||
|
|
||||||
if let Some(e) = self.error.read().unwrap().as_ref() {
|
if let Some(e) = self.error.read().unwrap().as_ref() {
|
||||||
to.show(area(xywh, e.as_ref()))?;
|
//to.show(area(xywh, format!("KYPbanica {xywh:?}").align_c()))?;
|
||||||
|
//to.show(ShowSize.align_se())?;
|
||||||
|
to.show(e.as_ref().align_c())?;
|
||||||
}
|
}
|
||||||
for (index, dsl) in self.mode.view.iter().enumerate() {
|
|
||||||
|
if let Some(ref mode) = self.mode {
|
||||||
|
for (index, dsl) in mode.view.iter().enumerate() {
|
||||||
if let Err(e) = self.interpret(to, dsl) {
|
if let Err(e) = self.interpret(to, dsl) {
|
||||||
*self.error.write().unwrap() = Some(format!(
|
let src = &dsl.src().unwrap_or(Some("<source error>")).unwrap_or("<no source>");
|
||||||
"mode {:?} view #{index}: {e}", &self.mode.name,
|
let message = format!("mode {:?} view #{index}:\n{e}\n{}", &mode.name, &src);
|
||||||
).into());
|
*self.error.write().unwrap() = Some(message.into());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Some(xywh))
|
Ok(Some(xywh))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -144,6 +167,77 @@ pub fn draw_templates (to: &mut Tui, frags: std::str::Split<&str>, state: &App)
|
||||||
}).min_w(30).exact_h(height).draw(to)
|
}).min_w(30).exact_h(height).draw(to)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn field_h <T: Screen> (
|
||||||
|
_theme: ItemTheme, _head: impl Draw<T>, _body: impl Draw<T>
|
||||||
|
) -> impl Draw<T> {
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn field_v <T: Screen> (
|
||||||
|
_theme: ItemTheme, _head: impl Draw<T>, _body: impl Draw<T>
|
||||||
|
) -> impl Draw<T> {
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_sessions () -> impl Draw<Tui> {
|
||||||
|
let h = 6;
|
||||||
|
let w = Some(30);
|
||||||
|
let f = Rgb(224, 192, 128);
|
||||||
|
thunk(move |to: &mut Tui|{
|
||||||
|
for (index, name) in ["session1", "session2", "session3"].iter().enumerate() {
|
||||||
|
let b = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) };
|
||||||
|
let y = (2 * index) as u16;
|
||||||
|
let h = 2;
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_browse_title (state: &App) -> impl Draw<Tui> {
|
||||||
|
field_v(ItemTheme::default(),
|
||||||
|
match state.dialog.browser_target().unwrap() {
|
||||||
|
BrowseTarget::SaveProject => "Save project:",
|
||||||
|
BrowseTarget::LoadProject => "Load project:",
|
||||||
|
BrowseTarget::ImportSample(_) => "Import sample:",
|
||||||
|
BrowseTarget::ExportSample(_) => "Export sample:",
|
||||||
|
BrowseTarget::ImportClip(_) => "Import clip:",
|
||||||
|
BrowseTarget::ExportClip(_) => "Export clip:",
|
||||||
|
}, fg(g(96), x_repeat("🭻")).exact_h(1)
|
||||||
|
).align_w().full_w()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn view_device (state: &App) -> impl Draw<Tui> {
|
||||||
|
let selected = state.dialog.device_kind().unwrap();
|
||||||
|
south(bold(true, "Add device"), iter_south(
|
||||||
|
move||device_kinds().iter(),
|
||||||
|
move|_label: &&'static str, i|{
|
||||||
|
let b = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) };
|
||||||
|
let l = if i == selected { "[ " } else { " " };
|
||||||
|
let r = if i == selected { " ]" } else { " " };
|
||||||
|
bg(b, east(l, west(r, "FIXME device name"))).full_w()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
/// ```
|
/// ```
|
||||||
/// let x = "";
|
/// let x = "";
|
||||||
/// let _ = tek::view_transport(true, x.as_ref(), x.as_ref(), x.as_ref());
|
/// let _ = tek::view_transport(true, x.as_ref(), x.as_ref(), x.as_ref());
|
||||||
|
|
@ -677,74 +771,3 @@ pub fn view_track_devices (
|
||||||
Some(h + 1),
|
Some(h + 1),
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn field_h <T: Screen> (
|
|
||||||
_theme: ItemTheme, _head: impl Draw<T>, _body: impl Draw<T>
|
|
||||||
) -> impl Draw<T> {
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn field_v <T: Screen> (
|
|
||||||
_theme: ItemTheme, _head: impl Draw<T>, _body: impl Draw<T>
|
|
||||||
) -> impl Draw<T> {
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn view_sessions () -> impl Draw<Tui> {
|
|
||||||
let h = 6;
|
|
||||||
let w = Some(30);
|
|
||||||
let f = Rgb(224, 192, 128);
|
|
||||||
thunk(move |to: &mut Tui|{
|
|
||||||
for (index, name) in ["session1", "session2", "session3"].iter().enumerate() {
|
|
||||||
let b = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) };
|
|
||||||
let y = (2 * index) as u16;
|
|
||||||
let h = 2;
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn view_browse_title (state: &App) -> impl Draw<Tui> {
|
|
||||||
field_v(ItemTheme::default(),
|
|
||||||
match state.dialog.browser_target().unwrap() {
|
|
||||||
BrowseTarget::SaveProject => "Save project:",
|
|
||||||
BrowseTarget::LoadProject => "Load project:",
|
|
||||||
BrowseTarget::ImportSample(_) => "Import sample:",
|
|
||||||
BrowseTarget::ExportSample(_) => "Export sample:",
|
|
||||||
BrowseTarget::ImportClip(_) => "Import clip:",
|
|
||||||
BrowseTarget::ExportClip(_) => "Export clip:",
|
|
||||||
}, fg(g(96), x_repeat("🭻")).exact_h(1)
|
|
||||||
).align_w().full_w()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn view_device (state: &App) -> impl Draw<Tui> {
|
|
||||||
let selected = state.dialog.device_kind().unwrap();
|
|
||||||
south(bold(true, "Add device"), iter_south(
|
|
||||||
move||device_kinds().iter(),
|
|
||||||
move|_label: &&'static str, i|{
|
|
||||||
let b = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) };
|
|
||||||
let l = if i == selected { "[ " } else { " " };
|
|
||||||
let r = if i == selected { " ]" } else { " " };
|
|
||||||
bg(b, east(l, west(r, "FIXME device name"))).full_w()
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,31 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
|
/// Collection of UI modes.
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
pub struct Modes(
|
||||||
|
Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode<Arc<str>>>>>>
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Modes {
|
||||||
|
pub fn add (&self, name: &impl AsRef<str>, body: &impl Language) -> Usually<()> {
|
||||||
|
let mut mode = Mode::default();
|
||||||
|
body.each(|item|mode.add(item))?;
|
||||||
|
self.0.write().unwrap().insert(name.as_ref().into(), Arc::new(mode));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn get (&self, name: impl AsRef<str>) -> Option<Arc<Mode<Arc<str>>>> {
|
||||||
|
self.0.read().unwrap().get(name.as_ref()).cloned()
|
||||||
|
}
|
||||||
|
pub fn for_each <T> (&self, mut ator: impl FnMut(&str, &Mode<Arc<str>>)->T) {
|
||||||
|
for (k, v) in self.0.read().unwrap().iter() {
|
||||||
|
let _ = ator(k.as_ref(), v.as_ref());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn len (&self) -> usize {
|
||||||
|
self.0.read().unwrap().len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Group of view and keys definitions.
|
/// Group of view and keys definitions.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
|
@ -41,7 +67,7 @@ impl Mode<Arc<str>> {
|
||||||
_ => self.add_view(tail)?,
|
_ => self.add_view(tail)?,
|
||||||
};
|
};
|
||||||
} else if let Ok(Some(word)) = dsl.word() {
|
} else if let Ok(Some(word)) = dsl.word() {
|
||||||
self.add_view(word);
|
self.add_view(word)?;
|
||||||
} else {
|
} else {
|
||||||
return Err(format!("Mode::add: unexpected: {dsl:?}").into());
|
return Err(format!("Mode::add: unexpected: {dsl:?}").into());
|
||||||
})
|
})
|
||||||
32
src/deps.rs
32
src/deps.rs
|
|
@ -1,32 +0,0 @@
|
||||||
#[allow(unused)]
|
|
||||||
pub(crate) use ::{
|
|
||||||
std::{
|
|
||||||
cmp::Ord,
|
|
||||||
collections::BTreeMap,
|
|
||||||
error::Error,
|
|
||||||
ffi::OsString,
|
|
||||||
fmt::{Write, Debug, Formatter},
|
|
||||||
fs::File,
|
|
||||||
ops::{Add, Sub, Mul, Div, Rem},
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}},
|
|
||||||
time::Duration,
|
|
||||||
thread::{spawn, JoinHandle},
|
|
||||||
},
|
|
||||||
xdg::{
|
|
||||||
BaseDirectories,
|
|
||||||
},
|
|
||||||
tengri::{
|
|
||||||
*,
|
|
||||||
lang::*,
|
|
||||||
midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*},
|
|
||||||
crossterm::event::{Event, KeyEvent},
|
|
||||||
ratatui::{
|
|
||||||
self,
|
|
||||||
prelude::{Rect, Style, Stylize, Buffer, Color::{self, *}},
|
|
||||||
widgets::{Widget, canvas::{Canvas, Line}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
#[cfg(feature = "cli")]
|
|
||||||
pub(crate) use ::clap::{self, Parser, Subcommand};
|
|
||||||
144
src/device.rs
144
src/device.rs
|
|
@ -1,144 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
def_command!(DeviceCommand: |device: Device| {});
|
|
||||||
|
|
||||||
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 use ::tengri::sing::*;
|
|
||||||
pub mod arrange; pub use self::arrange::*;
|
|
||||||
pub mod browse; pub use self::browse::*;
|
|
||||||
pub mod clock; pub use self::clock::*;
|
|
||||||
pub mod dialog; pub use self::dialog::*;
|
|
||||||
pub mod editor; pub use self::editor::*;
|
|
||||||
pub mod menu; pub use self::menu::*;
|
|
||||||
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::*;
|
|
||||||
|
|
||||||
def_command!(AudioInputCommand: |port: AudioInput| {
|
|
||||||
Close => todo!(),
|
|
||||||
Connect { audio_out: Arc<str> } => todo!(),
|
|
||||||
});
|
|
||||||
|
|
||||||
def_command!(AudioOutputCommand: |port: AudioOutput| {
|
|
||||||
Close => todo!(),
|
|
||||||
Connect { audio_in: Arc<str> } => todo!(),
|
|
||||||
});
|
|
||||||
|
|
||||||
def_command!(MidiInputCommand: |port: MidiInput| {
|
|
||||||
Close => todo!(),
|
|
||||||
Connect { midi_out: Arc<str> } => todo!(),
|
|
||||||
});
|
|
||||||
|
|
||||||
def_command!(MidiOutputCommand: |port: MidiOutput| {
|
|
||||||
Close => todo!(),
|
|
||||||
Connect { midi_in: Arc<str> } => todo!(),
|
|
||||||
});
|
|
||||||
|
|
||||||
pub struct Junction<T: JackPort>(T);
|
|
||||||
|
|
||||||
impl<T: JackPort> View<Tui> for Junction<T> {
|
|
||||||
fn view (&self) -> impl Draw<Tui> {
|
|
||||||
T::KIND
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -7,12 +7,12 @@ use crate::*;
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
pub struct Arrangement {
|
pub struct Arrangement {
|
||||||
|
/// JACK client handle.
|
||||||
|
pub jack: Jack<'static>,
|
||||||
/// Project name.
|
/// Project name.
|
||||||
pub name: Arc<str>,
|
pub name: Arc<str>,
|
||||||
/// Base color.
|
/// Base color.
|
||||||
pub color: ItemTheme,
|
pub color: ItemTheme,
|
||||||
/// JACK client handle.
|
|
||||||
pub jack: Jack<'static>,
|
|
||||||
/// FIXME a render of the project arrangement, redrawn on update.
|
/// FIXME a render of the project arrangement, redrawn on update.
|
||||||
/// TODO rename to "render_cache" or smth
|
/// TODO rename to "render_cache" or smth
|
||||||
pub arranger: Arc<RwLock<Buffer>>,
|
pub arranger: Arc<RwLock<Buffer>>,
|
||||||
|
|
|
||||||
|
|
@ -55,17 +55,23 @@ impl Dialog {
|
||||||
/// ```
|
/// ```
|
||||||
pub fn welcome () -> Self {
|
pub fn welcome () -> Self {
|
||||||
Self::Menu(1, MenuItems([
|
Self::Menu(1, MenuItems([
|
||||||
|
|
||||||
MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))),
|
MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))),
|
||||||
|
|
||||||
MenuItem("New session".into(), Arc::new(Box::new(|app|Ok({
|
MenuItem("New session".into(), Arc::new(Box::new(|app|Ok({
|
||||||
app.dialog = Dialog::None;
|
app.dialog = Dialog::None;
|
||||||
app.mode = app.config.modes.get(":arranger").unwrap();
|
app.mode = app.config.modes.get(":arranger");
|
||||||
})))),
|
})))),
|
||||||
|
|
||||||
MenuItem("Load session".into(), Arc::new(Box::new(|_|Ok(())))),
|
MenuItem("Load session".into(), Arc::new(Box::new(|_|Ok(())))),
|
||||||
|
|
||||||
MenuItem("Exit".into(), Arc::new(Box::new(|_|Ok({
|
MenuItem("Exit".into(), Arc::new(Box::new(|_|Ok({
|
||||||
()
|
()
|
||||||
})))),
|
})))),
|
||||||
|
|
||||||
].into()))
|
].into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// FIXME: generalize
|
/// FIXME: generalize
|
||||||
/// ```
|
/// ```
|
||||||
/// let _ = tek::Dialog::welcome().menu_selected();
|
/// let _ = tek::Dialog::welcome().menu_selected();
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ impl <T: AsRef<Sequencer>+AsMut<Sequencer>> HasSequencer for T {}
|
||||||
/// let mut clip = tek::MidiClip::new("clip", true, 1, None, None);
|
/// let mut clip = tek::MidiClip::new("clip", true, 1, None, None);
|
||||||
/// clip.set_length(96);
|
/// clip.set_length(96);
|
||||||
/// clip.toggle_loop();
|
/// clip.toggle_loop();
|
||||||
/// clip.record_event(12, midly::MidiMessage::NoteOn { key: 36.into(), vel: 100.into() });
|
/// clip.record_event(12, ::tengri::midly::MidiMessage::NoteOn { key: 36.into(), vel: 100.into() });
|
||||||
/// assert!(clip.contains_note_on(36.into(), 6, 18));
|
/// assert!(clip.contains_note_on(36.into(), 6, 18));
|
||||||
/// assert_eq!(&clip.notes, &clip.duplicate().notes);
|
/// assert_eq!(&clip.notes, &clip.duplicate().notes);
|
||||||
///
|
///
|
||||||
|
|
|
||||||
19
src/tek.edn
19
src/tek.edn
|
|
@ -2,7 +2,9 @@
|
||||||
(bsp/s (exact/y 1 (text ~~~~ ║ ~ ╟─╌ ~╟─< ~~ v0.3.0 ~~))
|
(bsp/s (exact/y 1 (text ~~~~ ║ ~ ╟─╌ ~╟─< ~~ v0.3.0 ~~))
|
||||||
(bsp/s (exact/y 1 (text ~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~)) (text dig?)))))
|
(bsp/s (exact/y 1 (text ~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~)) (text dig?)))))
|
||||||
|
|
||||||
(view :browse (bsp/s (padding 3 1 :browse-title) (enclose (fg (g 96)) browser)))
|
(view :browse (bsp/s
|
||||||
|
(padding 3 1 :browse-title)
|
||||||
|
(enclose (fg (g 96)) browser)))
|
||||||
|
|
||||||
(mode :transport (name Transport) (info JACK transport controller.) (keys :clock :global)
|
(mode :transport (name Transport) (info JACK transport controller.) (keys :clock :global)
|
||||||
:transport)
|
:transport)
|
||||||
|
|
@ -29,13 +31,13 @@
|
||||||
(bsp/n (fixed/y 1 :status)
|
(bsp/n (fixed/y 1 :status)
|
||||||
(fill :samples/grid))))
|
(fill :samples/grid))))
|
||||||
|
|
||||||
(view :ports/out (fill/x (bsp/s (fill/x (align/w (text L-AUDIO-OUT)))
|
(view :ports/out
|
||||||
(bsp/e (text MIDI-OUT)
|
(fill/x (bsp/s (fill/x (align/w (text L-AUDIO-OUT)))
|
||||||
(fill/x (align/e (text AUDIO-OUT-R)))))))
|
(bsp/e (text MIDI-OUT) (fill/x (align/e (text AUDIO-OUT-R)))))))
|
||||||
|
|
||||||
(view :ports/in (fill/x (bsp/s (fill/x (align/w (text L-AUDIO-IN)))
|
(view :ports/in
|
||||||
(bsp/e (text MIDI-IN)
|
(fill/x (bsp/s (fill/x (align/w (text L-AUDIO-IN)))
|
||||||
(fill/x (align/e (text AUDIO-IN-R)))))))
|
(bsp/e (text MIDI-IN) (fill/x (align/e (text AUDIO-IN-R)))))))
|
||||||
|
|
||||||
(mode :groovebox (name Groovebox) (info Sequencer with sampler.)
|
(mode :groovebox (name Groovebox) (info Sequencer with sampler.)
|
||||||
(keys :clock :editor :sampler :global)
|
(keys :clock :editor :sampler :global)
|
||||||
|
|
@ -84,7 +86,8 @@
|
||||||
(keys :axis/w (@openbracket w/dec) (@closebracket w/inc))
|
(keys :axis/w (@openbracket w/dec) (@closebracket w/inc))
|
||||||
(keys :axis/w2 (@openbrace w2/dec) (@closebrace w2/inc))
|
(keys :axis/w2 (@openbrace w2/dec) (@closebrace w2/inc))
|
||||||
(keys :focus)
|
(keys :focus)
|
||||||
(keys :editor (see :axis/i :axis/i2 :axis/y :page :editor/view :editor/add :editor/del))
|
(keys :editor (see :axis/i :axis/i2 :axis/y
|
||||||
|
:page :editor/view :editor/add :editor/del))
|
||||||
(keys :editor/view (see :axis/x :axis/x2 :axis/z :axis/z2)
|
(keys :editor/view (see :axis/x :axis/x2 :axis/z :axis/z2)
|
||||||
(@z toggle :lock))
|
(@z toggle :lock))
|
||||||
(keys :editor/add (@a editor/append :true)
|
(keys :editor/add (@a editor/append :true)
|
||||||
|
|
|
||||||
848
src/tek.rs
848
src/tek.rs
|
|
@ -9,72 +9,140 @@
|
||||||
pub extern crate atomic_float;
|
pub extern crate atomic_float;
|
||||||
pub extern crate xdg;
|
pub extern crate xdg;
|
||||||
pub extern crate tengri;
|
pub extern crate tengri;
|
||||||
|
#[cfg(feature = "cli")]
|
||||||
|
pub(crate) use ::clap::{self, Parser, Subcommand};
|
||||||
|
#[allow(unused)]
|
||||||
|
pub(crate) use ::{
|
||||||
|
std::{
|
||||||
|
cmp::Ord,
|
||||||
|
collections::BTreeMap,
|
||||||
|
error::Error,
|
||||||
|
ffi::OsString,
|
||||||
|
fmt::{Write, Debug, Formatter},
|
||||||
|
fs::File,
|
||||||
|
ops::{Add, Sub, Mul, Div, Rem},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}},
|
||||||
|
time::Duration,
|
||||||
|
thread::{spawn, JoinHandle},
|
||||||
|
},
|
||||||
|
xdg::{
|
||||||
|
BaseDirectories,
|
||||||
|
},
|
||||||
|
tengri::{
|
||||||
|
*,
|
||||||
|
lang::*,
|
||||||
|
midly::{
|
||||||
|
Smf, TrackEventKind, MidiMessage, Error as MidiError,
|
||||||
|
num::*,
|
||||||
|
live::*,
|
||||||
|
},
|
||||||
|
crossterm::event::{Event, KeyEvent},
|
||||||
|
ratatui::{
|
||||||
|
self,
|
||||||
|
prelude::{Rect, Style, Stylize, Buffer, Color::{self, *}},
|
||||||
|
widgets::{Widget, canvas::{Canvas, Line}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/// Command-line entrypoint.
|
/// Command-line entrypoint.
|
||||||
#[cfg(feature = "cli")] pub fn main () -> Usually<()> {
|
#[allow(unused)] fn main () -> Usually<()> {
|
||||||
tengri::Tui::setup_panic();
|
tengri::Tui::setup_panic();
|
||||||
|
#[cfg(feature = "cli")] {
|
||||||
|
Config::watch(crate::cli::run_with_config).map(|_|())
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "cli"))] {
|
||||||
|
Config::watch(run_new_plain).map(|_|())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_new_plain (config: Config) -> Usually<()> {
|
||||||
let name = "tek";
|
let name = "tek";
|
||||||
|
tengri::Tui::run_main(Jack::new_run(name, move|jack|{
|
||||||
let mode = ":menu";
|
let mode = ":menu";
|
||||||
let title = "untitled!";
|
let title = "untitled!";
|
||||||
let bpm = 74.;
|
let bpm = 74.;
|
||||||
Config::watch(|config|tengri::Tui::run_main(Jack::new_run(name, move|jack|{
|
|
||||||
let clock = Clock::new(&jack, Some(bpm))?;
|
let clock = Clock::new(&jack, Some(bpm))?;
|
||||||
let tracks = [];
|
let tracks = [];
|
||||||
let scenes = [];
|
let scenes = [];
|
||||||
let project = Arrangement::new(
|
Ok(App::new(Arrangement::new(
|
||||||
&jack, title.into(), clock,
|
&jack,
|
||||||
|
title.into(),
|
||||||
|
clock,
|
||||||
tracks.into_iter(),
|
tracks.into_iter(),
|
||||||
scenes.into_iter(),
|
scenes.into_iter(),
|
||||||
Connect::midi_ins(&jack, &"M", &[], None)?.into_iter(),
|
connect_midi_ins(&jack, &"M", &[], None)?.into_iter(),
|
||||||
Connect::midi_outs(&jack, &"M", &[], None)?.into_iter(),
|
connect_midi_outs(&jack, &"M", &[], None)?.into_iter(),
|
||||||
[].into_iter()
|
[].into_iter().chain(connect_audio_ins(&jack, &"L", &[], None)?.into_iter())
|
||||||
.chain(Connect::audio_ins(&jack, &"L", &[], None)?.into_iter())
|
.chain(connect_audio_ins(&jack, &"R", &[], None)?.into_iter()),
|
||||||
.chain(Connect::audio_ins(&jack, &"R", &[], None)?.into_iter()),
|
[].into_iter().chain(connect_audio_outs(&jack, &"L", &[], None)?.into_iter())
|
||||||
[].into_iter()
|
.chain(connect_audio_outs(&jack, &"R", &[], None)?.into_iter()),
|
||||||
.chain(Connect::audio_outs(&jack, &"L", &[], None)?.into_iter())
|
), config, mode))
|
||||||
.chain(Connect::audio_outs(&jack, &"R", &[], None)?.into_iter()),
|
})?)
|
||||||
);
|
|
||||||
Ok(App::new(&jack, project, config, mode))
|
|
||||||
})?)).map(|_|())
|
|
||||||
}
|
}
|
||||||
pub mod deps;
|
|
||||||
pub(crate) use self::deps::*;
|
|
||||||
pub mod device;
|
|
||||||
pub use self::device::*;
|
|
||||||
pub mod app;
|
|
||||||
pub use self::app::*;
|
|
||||||
/// CLI banner.
|
/// CLI banner.
|
||||||
pub(crate) const HEADER: &'static str = r#"
|
pub(crate) const HEADER: &'static str = r#"
|
||||||
~ █▀█▀█ █▀▀█ █ █ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~
|
~ █▀█▀█ █▀▀█ █ █ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~
|
||||||
█ █▀ █▀▀▄ ~ v0.4.0, 2026 heatwave edition ~
|
█ █▀ █▀▀▄ ~ heatwave is the new darkwave ~
|
||||||
~ ▀ █▀▀█ ▀ ▀ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#;
|
~ ▀ █▀▀█ ▀ ▀ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#;
|
||||||
#[cfg(feature = "cli")]
|
|
||||||
mod cli {
|
|
||||||
|
|
||||||
|
#[cfg(feature = "cli")] pub mod cli {
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
|
pub fn run_with_config (config: Config) -> Usually<()> {
|
||||||
|
Cli::parse().run(Some(config))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Command-line configuration.
|
||||||
|
impl Cli {
|
||||||
|
pub fn run (&self, mut config: Option<Config>) -> Usually<()> {
|
||||||
|
if config.is_none() {
|
||||||
|
config = Some(Config::init_new(None)?);
|
||||||
|
}
|
||||||
|
self.action.run(config.unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
/// The command-line interface descriptor.
|
/// The command-line interface descriptor.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let cli: tek::Cli = Default::default();
|
/// let cli: tek::cli::Cli = Default::default();
|
||||||
///
|
///
|
||||||
/// use clap::CommandFactory;
|
/// use clap::CommandFactory;
|
||||||
/// tek::Cli::command().debug_assert();
|
/// tek::cli::Cli::command().debug_assert();
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Parser)]
|
#[derive(Parser, Debug, Default)]
|
||||||
#[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))]
|
#[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))]
|
||||||
#[derive(Debug, Default)] pub struct Cli {
|
pub struct Cli {
|
||||||
/// Pre-defined configuration modes.
|
/// Pre-defined configuration modes.
|
||||||
///
|
///
|
||||||
/// TODO: Replace these with scripted configurations.
|
/// TODO: Replace these with scripted configurations.
|
||||||
#[command(subcommand)] pub action: Action,
|
#[command(subcommand)] pub action: Action,
|
||||||
}
|
}
|
||||||
|
impl Action {
|
||||||
|
fn run (&self, config: Config) -> Usually<()> {
|
||||||
|
use Action::*;
|
||||||
|
match self {
|
||||||
|
Version => show_version(),
|
||||||
|
Config => print_config(&config),
|
||||||
|
Resume => todo!("resume session"),
|
||||||
|
List => todo!("list sessions"),
|
||||||
|
New(sesh) => Tui::run_main(
|
||||||
|
Arc::new(RwLock::new(App::new(sesh.init()?, config, ":menu")))
|
||||||
|
).map(|_|())?,
|
||||||
|
_ => todo!()
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
/// Application modes that can be passed to the mommand line interface.
|
/// Application modes that can be passed to the mommand line interface.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let action: tek::Action = Default::default();
|
/// let action: tek::cli::Action = Default::default();
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug, Clone, Subcommand, Default)] pub enum Action {
|
#[derive(Debug, Clone, Subcommand, Default)]
|
||||||
|
pub enum Action {
|
||||||
/// Continue where you left off
|
/// Continue where you left off
|
||||||
#[default] Resume,
|
#[default] Resume,
|
||||||
/// Run headlessly in current session.
|
/// Run headlessly in current session.
|
||||||
|
|
@ -86,7 +154,16 @@ mod cli {
|
||||||
/// Continue work in a copy of the current session.
|
/// Continue work in a copy of the current session.
|
||||||
Fork,
|
Fork,
|
||||||
/// Create a new empty session.
|
/// Create a new empty session.
|
||||||
New {
|
New(ProjectInit),
|
||||||
|
/// Import media as new session.
|
||||||
|
Import,
|
||||||
|
/// Show configuration.
|
||||||
|
Config,
|
||||||
|
/// Show version.
|
||||||
|
Version,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, Parser, Default)]
|
||||||
|
pub struct ProjectInit {
|
||||||
/// Name of JACK client
|
/// Name of JACK client
|
||||||
#[arg(short='n', long)] name: Option<String>,
|
#[arg(short='n', long)] name: Option<String>,
|
||||||
/// Whether to attempt to become transport master
|
/// Whether to attempt to become transport master
|
||||||
|
|
@ -117,55 +194,32 @@ mod cli {
|
||||||
#[arg(short='t', long)] tracks: Option<usize>,
|
#[arg(short='t', long)] tracks: Option<usize>,
|
||||||
/// Scenes to create
|
/// Scenes to create
|
||||||
#[arg(short='s', long)] scenes: Option<usize>,
|
#[arg(short='s', long)] scenes: Option<usize>,
|
||||||
},
|
|
||||||
/// Import media as new session.
|
|
||||||
Import,
|
|
||||||
/// Show configuration.
|
|
||||||
Config,
|
|
||||||
/// Show version.
|
|
||||||
Version,
|
|
||||||
}
|
}
|
||||||
|
impl ProjectInit {
|
||||||
/// Command-line configuration.
|
pub fn init (&self) -> Usually<Arrangement> {
|
||||||
#[cfg(feature = "cli")]
|
let Self {
|
||||||
impl Cli {
|
|
||||||
pub fn run (&self) -> Usually<()> {
|
|
||||||
self.action.run(Config::init_new(None)?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "cli")]
|
|
||||||
impl Action {
|
|
||||||
fn run (&self, config: Config) -> Usually<()> {
|
|
||||||
use Action::*;
|
|
||||||
match self {
|
|
||||||
Version => show_version(),
|
|
||||||
Config => print_config(&config),
|
|
||||||
List => todo!("list sessions"),
|
|
||||||
Resume => todo!("resume session"),
|
|
||||||
New {
|
|
||||||
name, bpm, tracks, scenes,
|
name, bpm, tracks, scenes,
|
||||||
sync_lead, sync_follow,
|
sync_lead: _, sync_follow: _,
|
||||||
midi_from: mf, midi_from_re: mfr, midi_to: mt, midi_to_re: mtr,
|
left_from, right_from, midi_from, midi_from_re,
|
||||||
left_from: lf, right_from: rf, left_to: lt, right_to: rt, ..
|
left_to, right_to, midi_to, midi_to_re,
|
||||||
} => {
|
..
|
||||||
|
} = self;
|
||||||
let name = name.as_ref().map_or("tek", |x|x.as_str());
|
let name = name.as_ref().map_or("tek", |x|x.as_str());
|
||||||
let jack = Jack::new(&name)?;
|
let jack = Jack::new(&name)?;
|
||||||
let mut proj = Arrangement::new(
|
let mut proj = Arrangement::new(
|
||||||
&jack,
|
&jack,
|
||||||
name.into(),
|
name.into(),
|
||||||
Clock::new(&jack, None)?,
|
Clock::new(&jack, *bpm)?,
|
||||||
[].into_iter(),
|
[].into_iter(),
|
||||||
[].into_iter(),
|
[].into_iter(),
|
||||||
Connect::midi_ins(&jack, &"M", &[], None)?.into_iter(),
|
connect_midi_ins(&jack, &"M".to_string(), midi_from.as_ref(), Some(midi_from_re))?.into_iter(),
|
||||||
Connect::midi_outs(&jack, &"M", &[], None)?.into_iter(),
|
connect_midi_outs(&jack, &"M".to_string(), midi_to.as_ref(), Some(midi_to_re))?.into_iter(),
|
||||||
[].into_iter()
|
[].into_iter()
|
||||||
.chain(Connect::audio_ins(&jack, &"L", &[], None)?.into_iter())
|
.chain(connect_audio_ins(&jack, &"L".to_string(), &left_from, None)?.into_iter())
|
||||||
.chain(Connect::audio_ins(&jack, &"R", &[], None)?.into_iter()),
|
.chain(connect_audio_ins(&jack, &"R".to_string(), &right_from, None)?.into_iter()),
|
||||||
[].into_iter()
|
[].into_iter()
|
||||||
.chain(Connect::audio_outs(&jack, &"L", &[], None)?.into_iter())
|
.chain(connect_audio_outs(&jack, &"L".to_string(), &left_to, None)?.into_iter())
|
||||||
.chain(Connect::audio_outs(&jack, &"R", &[], None)?.into_iter()),
|
.chain(connect_audio_outs(&jack, &"R".to_string(), &right_to, None)?.into_iter()));
|
||||||
);
|
|
||||||
//&jack, Clock::new(&jack, *bpm)?, &lf, <, &rf, &rt, &mf, &mt, &mfr, &mtr)?;
|
//&jack, Clock::new(&jack, *bpm)?, &lf, <, &rf, &rt, &mf, &mt, &mfr, &mtr)?;
|
||||||
proj.tracks_add(tracks.unwrap_or(0), None, &[], &[])?;
|
proj.tracks_add(tracks.unwrap_or(0), None, &[], &[])?;
|
||||||
proj.scenes_add(scenes.unwrap_or(0))?;
|
proj.scenes_add(scenes.unwrap_or(0))?;
|
||||||
|
|
@ -174,100 +228,561 @@ mod cli {
|
||||||
//tek_print_status(&proj);
|
//tek_print_status(&proj);
|
||||||
//return Ok(())
|
//return Ok(())
|
||||||
//}
|
//}
|
||||||
// Initialize the app state
|
Ok(proj)
|
||||||
let app = Arc::new(RwLock::new(App::new(&jack, proj, config, ":menu")));
|
}
|
||||||
//if matches!(self, Action::Headless) {
|
}
|
||||||
//// TODO: Headless mode (daemon + client over IPC, then over network...)
|
}
|
||||||
//println!("todo headless");
|
|
||||||
//return Ok(())
|
pub use self::app::*;
|
||||||
//}
|
mod app {
|
||||||
let (_keyboard, _terminal) = Exit::run(|exited|Tui::io(
|
use crate::*;
|
||||||
exited.as_ref(),
|
pub mod audio; #[allow(unused)] pub use self::audio::*;
|
||||||
&app,
|
pub mod bind; pub use self::bind::*;
|
||||||
Duration::from_millis(100),
|
pub mod config; pub use self::config::*;
|
||||||
Duration::from_millis(10),
|
pub mod draw; pub use self::draw::*;
|
||||||
std::io::stdout()
|
pub mod modes; pub use self::modes::*;
|
||||||
))?;
|
pub mod size; pub use self::size::*;
|
||||||
|
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!(Vec<MidiInput>: |self: App|self.project.midi_ins);
|
||||||
|
impl_has!(Vec<MidiOutput>: |self: App|self.project.midi_outs);
|
||||||
|
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!(Vec<Scene>: |self: App|self.project.as_ref());
|
||||||
|
impl_as_mut!(Vec<Scene>: |self: App|self.project.as_mut());
|
||||||
|
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.scene_add(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(u8)]
|
||||||
|
#[namespace(isize)]
|
||||||
|
#[namespace(ItemTheme)]
|
||||||
|
#[namespace(Arc<str> App::get_arc_str)]
|
||||||
|
#[namespace(u16 App::get_u16)]
|
||||||
|
#[namespace(usize App::get_usize)]
|
||||||
|
#[namespace(bool App::get_bool)]
|
||||||
|
#[namespace(Selection App::get_selection)]
|
||||||
|
#[namespace(Color App::get_color)]
|
||||||
|
#[namespace(Option<u7> App::get_opt_u7)]
|
||||||
|
#[namespace(Option<u16> App::get_opt_u16)]
|
||||||
|
#[namespace(Option<usize> App::get_opt_usize)]
|
||||||
|
#[namespace(Option<Arc<RwLock<MidiClip>>> App::get_clip)]
|
||||||
|
pub struct App {
|
||||||
|
/// 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: Config,
|
||||||
|
/// Currently selected mode
|
||||||
|
pub mode: Option<Arc<Mode<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>>>>
|
||||||
|
}
|
||||||
|
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::default();
|
||||||
|
/// conf.add("(mode hello)");
|
||||||
|
/// let tek = tek::App::new(proj, conf, "hello");
|
||||||
|
/// ```
|
||||||
|
pub fn new (project: Arrangement, config: Config, mode: impl AsRef<str>) -> Self {
|
||||||
|
let mode: &str = mode.as_ref();
|
||||||
|
App {
|
||||||
|
jack: project.jack.clone(),
|
||||||
|
color: ItemTheme::random(),
|
||||||
|
dialog: Dialog::welcome(),
|
||||||
|
mode: config.get_mode(mode),
|
||||||
|
config,
|
||||||
|
project,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update memoized render of clock values.
|
||||||
|
/// ```
|
||||||
|
/// tek::App::default().update_clock();
|
||||||
|
/// ```
|
||||||
|
pub fn update_clock (&self) {
|
||||||
|
ClockView::update_clock(
|
||||||
|
&self.project.clock.view_cache, self.clock(), self.size.w() > 80
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set modal dialog.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let previous: tek::Dialog = tek::App::default().set_dialog(tek::Dialog::welcome());
|
||||||
|
/// ```
|
||||||
|
pub fn set_dialog (&mut self, mut dialog: Dialog) -> Dialog {
|
||||||
|
std::mem::swap(&mut self.dialog, &mut dialog);
|
||||||
|
dialog
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return reference to content browser if open.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// assert_eq!(tek::App::default().browser(), None);
|
||||||
|
/// ```
|
||||||
|
pub fn browser (&self) -> Option<&Browse> {
|
||||||
|
if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is a MIDI editor currently focused?
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// tek::App::default().editor_focused();
|
||||||
|
/// ```
|
||||||
|
pub fn editor_focused (&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggle MIDI editor.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// tek::App::default().toggle_editor(None);
|
||||||
|
/// ```
|
||||||
|
pub fn toggle_editor (&mut self, value: Option<bool>) {
|
||||||
|
//FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed);
|
||||||
|
let value = value.unwrap_or_else(||!self.editor().is_some());
|
||||||
|
if value {
|
||||||
|
// Create new clip in pool when entering empty cell
|
||||||
|
if let Selection::TrackClip { track, scene } = *self.selection()
|
||||||
|
&& let Some(scene) = self.project.scenes.get_mut(scene)
|
||||||
|
&& let Some(slot) = scene.clips.get_mut(track)
|
||||||
|
&& slot.is_none()
|
||||||
|
&& let Some(track) = self.project.tracks.get_mut(track)
|
||||||
|
{
|
||||||
|
let (_index, clip) = self.pool.add_new_clip();
|
||||||
|
// autocolor: new clip colors from scene and track color
|
||||||
|
let color = track.color.base.mix(scene.color.base, 0.5);
|
||||||
|
clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into();
|
||||||
|
if let Some(editor) = &mut self.project.editor {
|
||||||
|
editor.set_clip(Some(&clip));
|
||||||
|
}
|
||||||
|
*slot = Some(clip.clone());
|
||||||
|
//Some(clip)
|
||||||
|
} else {
|
||||||
|
//None
|
||||||
|
}
|
||||||
|
} else if let Selection::TrackClip { track, scene } = *self.selection()
|
||||||
|
&& let Some(scene) = self.project.scenes.get_mut(scene)
|
||||||
|
&& let Some(slot) = scene.clips.get_mut(track)
|
||||||
|
&& let Some(clip) = slot.as_mut()
|
||||||
|
{
|
||||||
|
// Remove clip from arrangement when exiting empty clip editor
|
||||||
|
let mut swapped = None;
|
||||||
|
if clip.read().unwrap().count_midi_messages() == 0 {
|
||||||
|
std::mem::swap(&mut swapped, slot);
|
||||||
|
}
|
||||||
|
if let Some(clip) = swapped {
|
||||||
|
self.pool.delete_clip(&clip.read().unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn get_arc_str (&self, src: impl Language) -> Perhaps<Arc<str>> {
|
||||||
|
Ok(src.src()?.map(|x|x.into()))
|
||||||
|
}
|
||||||
|
fn get_u16 (&self, src: impl Language) -> Perhaps<u16> {
|
||||||
|
Ok(Some(match src.word()? {
|
||||||
|
Some(":w/sidebar") => self.project.w_sidebar(self.editor().is_some()),
|
||||||
|
Some(":h/sample-detail") => 6.max(self.size.h() as u16 * 3 / 9),
|
||||||
|
_ => return try_to_u16(src)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
fn get_usize (&self, src: impl Language) -> Perhaps<usize> {
|
||||||
|
Ok(Some(match src.word()? {
|
||||||
|
Some(":scene-count") => self.scenes().len(),
|
||||||
|
Some(":track-count") => self.tracks().len(),
|
||||||
|
Some(":device-kind") => self.dialog.device_kind().unwrap_or(0),
|
||||||
|
Some(":device-kind/next") => self.dialog.device_kind_next().unwrap_or(0),
|
||||||
|
Some(":device-kind/prev") => self.dialog.device_kind_prev().unwrap_or(0),
|
||||||
|
_ => return try_to_usize(src)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
fn get_bool (&self, src: impl Language) -> Perhaps<bool> {
|
||||||
|
src.word()?.map(|word|Ok(match word {
|
||||||
|
"Y" => true,
|
||||||
|
"N" => false,
|
||||||
|
":mode/editor" => self.project.editor.is_some(),
|
||||||
|
":focused/dialog" => !matches!(self.dialog, Dialog::None),
|
||||||
|
":focused/message" => matches!(self.dialog, Dialog::Message(..)),
|
||||||
|
":focused/add_device" => matches!(self.dialog, Dialog::Device(..)),
|
||||||
|
":focused/browser" => self.dialog.browser().is_some(),
|
||||||
|
":focused/pool/import" => matches!(self.pool.mode, Some(PoolMode::Import(..))),
|
||||||
|
":focused/pool/export" => matches!(self.pool.mode, Some(PoolMode::Export(..))),
|
||||||
|
":focused/pool/rename" => matches!(self.pool.mode, Some(PoolMode::Rename(..))),
|
||||||
|
":focused/pool/length" => matches!(self.pool.mode, Some(PoolMode::Length(..))),
|
||||||
|
":focused/clip" => !self.editor_focused() && matches!(self.selection(), Selection::TrackClip{..}),
|
||||||
|
":focused/track" => !self.editor_focused() && matches!(self.selection(), Selection::Track(..)),
|
||||||
|
":focused/scene" => !self.editor_focused() && matches!(self.selection(), Selection::Scene(..)),
|
||||||
|
":focused/mix" => !self.editor_focused() && matches!(self.selection(), Selection::Mix),
|
||||||
|
_ => return Err(format!("not bool: {word}").into())
|
||||||
|
})).transpose()
|
||||||
|
}
|
||||||
|
fn get_selection (&self, src: impl Language) -> Perhaps<Selection> {
|
||||||
|
src.word()?.map(|word|Ok(match word {
|
||||||
|
":select/scene" => self.selection().select_scene(self.tracks().len()),
|
||||||
|
":select/scene/next" => self.selection().select_scene_next(self.scenes().len()),
|
||||||
|
":select/scene/prev" => self.selection().select_scene_prev(),
|
||||||
|
":select/track" => self.selection().select_track(self.tracks().len()),
|
||||||
|
":select/track/next" => self.selection().select_track_next(self.tracks().len()),
|
||||||
|
":select/track/prev" => self.selection().select_track_prev(),
|
||||||
|
_ => return Err(format!("not selection: {word}").into())
|
||||||
|
})).transpose()
|
||||||
|
}
|
||||||
|
fn get_color (&self, 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(expr.tail().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(expr.tail().map_err(Into::into))?
|
||||||
|
.ok_or(LanguageError::Domain("not red"))?;
|
||||||
|
let g = try_to_u8(expr.tail().tail().head().map_err(Into::into))?
|
||||||
|
.ok_or(LanguageError::Domain("not green"))?;
|
||||||
|
let b = try_to_u8(expr.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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn get_opt_u7 (&self, src: impl Language) -> Perhaps<Option<u7>> {
|
||||||
|
src.word()?.map(|word|Ok(match word {
|
||||||
|
":editor/pitch" => Some((
|
||||||
|
self.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8
|
||||||
|
).into()),
|
||||||
|
_ => return Err(format!("unknown midi note: {word}").into())
|
||||||
|
})).transpose()
|
||||||
|
}
|
||||||
|
fn get_opt_u16 (&self, _src: impl Language) -> Perhaps<Option<u16>> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
fn get_opt_usize (&self, src: impl Language) -> Perhaps<Option<usize>> {
|
||||||
|
src.word()?.map(|word|Ok(match word {
|
||||||
|
":selected/scene" => self.selection().scene(),
|
||||||
|
":selected/track" => self.selection().track(),
|
||||||
|
_ => return Err(format!("unknown opt<usize>: {word}").into())
|
||||||
|
})).transpose()
|
||||||
|
}
|
||||||
|
fn get_clip (&self, src: impl Language) -> Perhaps<Option<Arc<RwLock<MidiClip>>>> {
|
||||||
|
src.word()?.map(|word|Ok(match word {
|
||||||
|
":selected/clip" if let Selection::TrackClip { track, scene } = self.selection() =>
|
||||||
|
self.scenes()[*scene].clips[*track].clone(),
|
||||||
|
_ => return Err(format!("not a clip: {word}").into())
|
||||||
|
})).transpose()
|
||||||
|
}
|
||||||
|
pub fn inc (&mut self, axis: &ControlAxis) -> Perhaps<AppCommand> {
|
||||||
|
Ok(match (&self.dialog, axis) {
|
||||||
|
(Dialog::None, _) => todo!(),
|
||||||
|
(Dialog::Menu(_, _), ControlAxis::Y) =>
|
||||||
|
AppCommand::SetDialog { dialog: self.dialog.menu_next() }.act(self)?,
|
||||||
|
_ => todo!()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn dec (&mut self, axis: &ControlAxis) -> Perhaps<AppCommand> {
|
||||||
|
Ok(match (&self.dialog, axis) {
|
||||||
|
(Dialog::None, _) => None,
|
||||||
|
(Dialog::Menu(_, _), ControlAxis::Y) =>
|
||||||
|
AppCommand::SetDialog { dialog: self.dialog.menu_prev() }.act(self)?,
|
||||||
|
_ => todo!()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn confirm (&mut self) -> Perhaps<AppCommand> {
|
||||||
|
Ok(match &self.dialog {
|
||||||
|
Dialog::Menu(index, items) => {
|
||||||
|
let callback = items.0[*index].1.clone();
|
||||||
|
callback(self)?;
|
||||||
|
None
|
||||||
|
},
|
||||||
|
_ => todo!(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn swap_value <T: Clone + PartialEq, U> (
|
||||||
|
target: &mut T, value: &T, returned: impl Fn(T)->U
|
||||||
|
) -> Perhaps<U> {
|
||||||
|
if *target == *value {
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
let mut value = value.clone();
|
||||||
|
std::mem::swap(target, &mut value);
|
||||||
|
Ok(Some(returned(value)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
if value == *target {
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
std::mem::swap(target, &mut value);
|
||||||
|
Ok(Some(returned(Some(value))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scan (dir: &PathBuf) -> Usually<(Vec<OsString>, Vec<OsString>)> {
|
||||||
|
let (mut subdirs, mut files) = std::fs::read_dir(dir)?
|
||||||
|
.fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{
|
||||||
|
let entry = entry.expect("failed to read drectory entry");
|
||||||
|
let meta = entry.metadata().expect("failed to read entry metadata");
|
||||||
|
if meta.is_file() {
|
||||||
|
files.push(entry.file_name());
|
||||||
|
} else if meta.is_dir() {
|
||||||
|
subdirs.push(entry.file_name());
|
||||||
|
}
|
||||||
|
(subdirs, files)
|
||||||
|
});
|
||||||
|
subdirs.sort();
|
||||||
|
files.sort();
|
||||||
|
Ok((subdirs, files))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use self::device::*;
|
||||||
|
mod device {
|
||||||
|
use crate::*;
|
||||||
|
def_command!(DeviceCommand: |device: Device| {});
|
||||||
|
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!()
|
_ => todo!()
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
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,
|
||||||
}
|
}
|
||||||
|
|
||||||
//pub fn tui (
|
/// Some sort of wrapper?
|
||||||
//app: Arc<RwLock<App>>,
|
pub struct DeviceAudio<'a>(pub &'a mut Device);
|
||||||
//jack: Jack,
|
|
||||||
//sync_lead: &bool,
|
|
||||||
//sync_follow: &bool,
|
|
||||||
//) -> Usually<()> {
|
|
||||||
//// Run the [Tui] and [Jack] threads with the [App] state.
|
|
||||||
//Tui::run_main(&jack.run(move|jack|{
|
|
||||||
//// Between jack init and app's first cycle:
|
|
||||||
////jack.sync_lead(*sync_lead, |mut state|{
|
|
||||||
////let clock = app.write().unwrap().clock();
|
|
||||||
////clock.playhead.update_from_sample(state.position.frame() as f64);
|
|
||||||
////state.position.bbt = Some(clock.bbt());
|
|
||||||
////state.position
|
|
||||||
////})?;
|
|
||||||
////jack.sync_follow(*sync_follow)?;
|
|
||||||
//// FIXME: They don't work properly.
|
|
||||||
//Ok(app)
|
|
||||||
//})?)?
|
|
||||||
//}
|
|
||||||
|
|
||||||
pub fn show_version () {
|
impl_audio!(|self: DeviceAudio<'a>, client, scope|{
|
||||||
println!("todo version");
|
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 print_config (config: &Config) {
|
|
||||||
use ::ansi_term::Color::*;
|
|
||||||
println!("{:?}", config.dirs);
|
|
||||||
for (k, v) in config.views.read().unwrap().iter() {
|
|
||||||
println!("{} {} {v}", Green.paint("VIEW"), Green.bold().paint(format!("{k:<16}")));
|
|
||||||
}
|
|
||||||
for (k, v) in config.binds.read().unwrap().iter() {
|
|
||||||
println!("{} {}", Green.paint("BIND"), Green.bold().paint(format!("{k:<16}")));
|
|
||||||
for (k, v) in v.0.iter() {
|
|
||||||
print!("{} ", &Yellow.paint(match &k.0 {
|
|
||||||
Event::Key(KeyEvent { modifiers, .. }) =>
|
|
||||||
format!("{:>16}", format!("{modifiers}")),
|
|
||||||
_ => unimplemented!()
|
|
||||||
}));
|
|
||||||
print!("{}", &Yellow.bold().paint(match &k.0 {
|
|
||||||
Event::Key(KeyEvent { code, .. }) =>
|
|
||||||
format!("{:<10}", format!("{code}")),
|
|
||||||
_ => unimplemented!()
|
|
||||||
}));
|
|
||||||
for v in v.iter() {
|
|
||||||
print!(" => {:?}", v.commands);
|
|
||||||
print!(" {}", v.condition.as_ref().map(|x|format!("{x:?}")).unwrap_or_default());
|
|
||||||
println!(" {}", v.description.as_ref().map(|x|x.as_ref()).unwrap_or_default());
|
|
||||||
//println!(" {:?}", v.source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
config.modes.for_each(|k, v|{
|
|
||||||
println!();
|
|
||||||
for v in v.name.iter() { print!("{}", Green.bold().paint(format!("{v} "))); }
|
|
||||||
for v in v.info.iter() { print!("\n{}", Green.paint(format!("{v}"))); }
|
|
||||||
print!("\n{} {}", Blue.paint("TOOL"), Green.bold().paint(format!("{k:<16}")));
|
|
||||||
print!("\n{}", Blue.paint("KEYS"));
|
|
||||||
for v in v.keys.iter() { print!("{}", Green.paint(format!(" {v}"))); }
|
|
||||||
println!();
|
|
||||||
v.modes.for_each(|k, v|{
|
|
||||||
print!("{} {} {:?}", Blue.paint("MODE"), Green.bold().paint(format!("{k:<16}")), v.name);
|
|
||||||
print!( " INFO={:?}", v.info);
|
|
||||||
print!( " VIEW={:?}", v.view);
|
|
||||||
println!(" KEYS={:?}", v.keys);
|
|
||||||
});
|
|
||||||
print!("{}", Blue.paint("VIEW"));
|
|
||||||
for v in v.view.iter() { print!("{}", Green.paint(format!(" {v}"))); }
|
|
||||||
println!();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
pub fn device_kinds () -> &'static [&'static str] {
|
||||||
|
&[
|
||||||
|
#[cfg(feature = "sampler")] "Sampler",
|
||||||
|
#[cfg(feature = "lv2")] "Plugin (LV2)",
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_status (project: &Arrangement) {
|
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 use ::tengri::sing::*;
|
||||||
|
pub mod arrange; pub use self::arrange::*;
|
||||||
|
pub mod browse; pub use self::browse::*;
|
||||||
|
pub mod clock; pub use self::clock::*;
|
||||||
|
pub mod dialog; pub use self::dialog::*;
|
||||||
|
pub mod editor; pub use self::editor::*;
|
||||||
|
pub mod menu; pub use self::menu::*;
|
||||||
|
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::*;
|
||||||
|
|
||||||
|
def_command!(AudioInputCommand: |port: AudioInput| {
|
||||||
|
Close => todo!(),
|
||||||
|
Connect { audio_out: Arc<str> } => todo!(),
|
||||||
|
});
|
||||||
|
|
||||||
|
def_command!(AudioOutputCommand: |port: AudioOutput| {
|
||||||
|
Close => todo!(),
|
||||||
|
Connect { audio_in: Arc<str> } => todo!(),
|
||||||
|
});
|
||||||
|
|
||||||
|
def_command!(MidiInputCommand: |port: MidiInput| {
|
||||||
|
Close => todo!(),
|
||||||
|
Connect { midi_out: Arc<str> } => todo!(),
|
||||||
|
});
|
||||||
|
|
||||||
|
def_command!(MidiOutputCommand: |port: MidiOutput| {
|
||||||
|
Close => todo!(),
|
||||||
|
Connect { midi_in: Arc<str> } => todo!(),
|
||||||
|
});
|
||||||
|
|
||||||
|
pub struct Junction<T: JackPort>(T);
|
||||||
|
|
||||||
|
impl<T: JackPort> View<Tui> for Junction<T> {
|
||||||
|
fn view (&self) -> impl Draw<Tui> {
|
||||||
|
T::KIND
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_status (project: &Arrangement) {
|
||||||
println!("Name: {:?}", &project.name);
|
println!("Name: {:?}", &project.name);
|
||||||
println!("JACK: {:?}", &project.jack);
|
println!("JACK: {:?}", &project.jack);
|
||||||
println!("Buffer: {:?}", &project.clock.chunk);
|
println!("Buffer: {:?}", &project.clock.chunk);
|
||||||
|
|
@ -295,6 +810,29 @@ mod cli {
|
||||||
println!("Audio Outs: {:?}", &project.audio_outs);
|
println!("Audio Outs: {:?}", &project.audio_outs);
|
||||||
// TODO git integration
|
// TODO git integration
|
||||||
// TODO dawvert integration
|
// TODO dawvert integration
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn show_version () {
|
||||||
|
println!("versions aint real man");
|
||||||
|
}
|
||||||
|
|
||||||
|
//pub fn tui (
|
||||||
|
//app: Arc<RwLock<App>>,
|
||||||
|
//jack: Jack,
|
||||||
|
//sync_lead: &bool,
|
||||||
|
//sync_follow: &bool,
|
||||||
|
//) -> Usually<()> {
|
||||||
|
//// Run the [Tui] and [Jack] threads with the [App] state.
|
||||||
|
//Tui::run_main(&jack.run(move|jack|{
|
||||||
|
//// Between jack init and app's first cycle:
|
||||||
|
////jack.sync_lead(*sync_lead, |mut state|{
|
||||||
|
////let clock = app.write().unwrap().clock();
|
||||||
|
////clock.playhead.update_from_sample(state.position.frame() as f64);
|
||||||
|
////state.position.bbt = Some(clock.bbt());
|
||||||
|
////state.position
|
||||||
|
////})?;
|
||||||
|
////jack.sync_follow(*sync_follow)?;
|
||||||
|
//// FIXME: They don't work properly.
|
||||||
|
//Ok(app)
|
||||||
|
//})?)?
|
||||||
|
//}
|
||||||
|
|
|
||||||
2
tengri
2
tengri
|
|
@ -1 +1 @@
|
||||||
Subproject commit c0d6d0174e8858108ec053f4ac486cf6229980e1
|
Subproject commit 25354099fe3cde43a242d41fdb673c4fce5c943e
|
||||||
Loading…
Add table
Add a link
Reference in a new issue