multiline view error

This commit is contained in:
i do not exist 2026-07-28 04:15:27 +03:00
parent 07f2290017
commit b4424fcb69
15 changed files with 979 additions and 949 deletions

View file

@ -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))
}

View file

@ -11,7 +11,8 @@ fn tek_commands_collect (app: &App, input: &TuiEvent)
-> Usually<Vec<AppCommand>>
{
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())
&& let Some(bindings) = event_map.query(input) {
for binding in bindings {
@ -23,6 +24,7 @@ fn tek_commands_collect (app: &App, input: &TuiEvent)
}
}
}
}
Ok(commands)
}

View file

@ -115,6 +115,49 @@ impl Config {
}
}
mod views; pub use self::views::*;
mod modes; pub use self::modes::*;
mod mode; pub use self::mode::*;
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!();
});
}

View file

@ -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()
}
}

View file

@ -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(())
}

View file

@ -1,5 +1,21 @@
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,
/// which is defined in terms of [dizzle] DSL.
///
@ -9,17 +25,24 @@ impl View<Tui> for App {
fn view (&self) -> impl Draw<Tui> {
thunk(|to: &mut Tui|{
let xywh = to.area().into();
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) {
*self.error.write().unwrap() = Some(format!(
"mode {:?} view #{index}: {e}", &self.mode.name,
).into());
let src = &dsl.src().unwrap_or(Some("<source error>")).unwrap_or("<no source>");
let message = format!("mode {:?} view #{index}:\n{e}\n{}", &mode.name, &src);
*self.error.write().unwrap() = Some(message.into());
break;
}
}
}
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)
}
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 _ = 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),
)))
}
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()
}))
}

View file

@ -1,5 +1,31 @@
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.
///
/// ```
@ -41,7 +67,7 @@ impl Mode<Arc<str>> {
_ => self.add_view(tail)?,
};
} else if let Ok(Some(word)) = dsl.word() {
self.add_view(word);
self.add_view(word)?;
} else {
return Err(format!("Mode::add: unexpected: {dsl:?}").into());
})

View file

@ -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};

View file

@ -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
}
}

View file

@ -7,12 +7,12 @@ use crate::*;
/// ```
#[derive(Default, Debug)]
pub struct Arrangement {
/// JACK client handle.
pub jack: Jack<'static>,
/// Project name.
pub name: Arc<str>,
/// Base color.
pub color: ItemTheme,
/// JACK client handle.
pub jack: Jack<'static>,
/// FIXME a render of the project arrangement, redrawn on update.
/// TODO rename to "render_cache" or smth
pub arranger: Arc<RwLock<Buffer>>,

View file

@ -55,17 +55,23 @@ impl Dialog {
/// ```
pub fn welcome () -> Self {
Self::Menu(1, MenuItems([
MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))),
MenuItem("New session".into(), Arc::new(Box::new(|app|Ok({
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("Exit".into(), Arc::new(Box::new(|_|Ok({
()
})))),
].into()))
}
/// FIXME: generalize
/// ```
/// let _ = tek::Dialog::welcome().menu_selected();

View file

@ -42,7 +42,7 @@ impl <T: AsRef<Sequencer>+AsMut<Sequencer>> HasSequencer for T {}
/// let mut clip = tek::MidiClip::new("clip", true, 1, None, None);
/// clip.set_length(96);
/// 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_eq!(&clip.notes, &clip.duplicate().notes);
///

View file

@ -2,7 +2,9 @@
(bsp/s (exact/y 1 (text ~~~~ ~ ~< ~~ v0.3.0 ~~))
(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)
:transport)
@ -29,13 +31,13 @@
(bsp/n (fixed/y 1 :status)
(fill :samples/grid))))
(view :ports/out (fill/x (bsp/s (fill/x (align/w (text L-AUDIO-OUT)))
(bsp/e (text MIDI-OUT)
(fill/x (align/e (text AUDIO-OUT-R)))))))
(view :ports/out
(fill/x (bsp/s (fill/x (align/w (text L-AUDIO-OUT)))
(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)))
(bsp/e (text MIDI-IN)
(fill/x (align/e (text AUDIO-IN-R)))))))
(view :ports/in
(fill/x (bsp/s (fill/x (align/w (text L-AUDIO-IN)))
(bsp/e (text MIDI-IN) (fill/x (align/e (text AUDIO-IN-R)))))))
(mode :groovebox (name Groovebox) (info Sequencer with sampler.)
(keys :clock :editor :sampler :global)
@ -84,7 +86,8 @@
(keys :axis/w (@openbracket w/dec) (@closebracket w/inc))
(keys :axis/w2 (@openbrace w2/dec) (@closebrace w2/inc))
(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)
(@z toggle :lock))
(keys :editor/add (@a editor/append :true)

View file

@ -9,72 +9,140 @@
pub extern crate atomic_float;
pub extern crate xdg;
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.
#[cfg(feature = "cli")] pub fn main () -> Usually<()> {
#[allow(unused)] fn main () -> Usually<()> {
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";
tengri::Tui::run_main(Jack::new_run(name, move|jack|{
let mode = ":menu";
let title = "untitled!";
let bpm = 74.;
Config::watch(|config|tengri::Tui::run_main(Jack::new_run(name, move|jack|{
let clock = Clock::new(&jack, Some(bpm))?;
let tracks = [];
let scenes = [];
let project = Arrangement::new(
&jack, title.into(), clock,
Ok(App::new(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()),
);
Ok(App::new(&jack, project, config, mode))
})?)).map(|_|())
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 mod deps;
pub(crate) use self::deps::*;
pub mod device;
pub use self::device::*;
pub mod app;
pub use self::app::*;
/// CLI banner.
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::*;
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.
///
/// ```
/// let cli: tek::Cli = Default::default();
/// let cli: tek::cli::Cli = Default::default();
///
/// 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))]
#[derive(Debug, Default)] pub struct Cli {
pub struct Cli {
/// Pre-defined configuration modes.
///
/// TODO: Replace these with scripted configurations.
#[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.
///
/// ```
/// 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
#[default] Resume,
/// Run headlessly in current session.
@ -86,7 +154,16 @@ mod cli {
/// Continue work in a copy of the current session.
Fork,
/// 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
#[arg(short='n', long)] name: Option<String>,
/// Whether to attempt to become transport master
@ -117,55 +194,32 @@ mod cli {
#[arg(short='t', long)] tracks: Option<usize>,
/// Scenes to create
#[arg(short='s', long)] scenes: Option<usize>,
},
/// Import media as new session.
Import,
/// Show configuration.
Config,
/// Show version.
Version,
}
/// Command-line configuration.
#[cfg(feature = "cli")]
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 {
impl ProjectInit {
pub fn init (&self) -> Usually<Arrangement> {
let Self {
name, bpm, tracks, scenes,
sync_lead, sync_follow,
midi_from: mf, midi_from_re: mfr, midi_to: mt, midi_to_re: mtr,
left_from: lf, right_from: rf, left_to: lt, right_to: rt, ..
} => {
sync_lead: _, sync_follow: _,
left_from, right_from, midi_from, midi_from_re,
left_to, right_to, midi_to, midi_to_re,
..
} = self;
let name = name.as_ref().map_or("tek", |x|x.as_str());
let jack = Jack::new(&name)?;
let mut proj = Arrangement::new(
&jack,
name.into(),
Clock::new(&jack, None)?,
Clock::new(&jack, *bpm)?,
[].into_iter(),
[].into_iter(),
Connect::midi_ins(&jack, &"M", &[], None)?.into_iter(),
Connect::midi_outs(&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".to_string(), midi_to.as_ref(), Some(midi_to_re))?.into_iter(),
[].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, &"L".to_string(), &left_from, None)?.into_iter())
.chain(connect_audio_ins(&jack, &"R".to_string(), &right_from, None)?.into_iter()),
[].into_iter()
.chain(Connect::audio_outs(&jack, &"L", &[], None)?.into_iter())
.chain(Connect::audio_outs(&jack, &"R", &[], None)?.into_iter()),
);
.chain(connect_audio_outs(&jack, &"L".to_string(), &left_to, None)?.into_iter())
.chain(connect_audio_outs(&jack, &"R".to_string(), &right_to, None)?.into_iter()));
//&jack, Clock::new(&jack, *bpm)?, &lf, &lt, &rf, &rt, &mf, &mt, &mfr, &mtr)?;
proj.tracks_add(tracks.unwrap_or(0), None, &[], &[])?;
proj.scenes_add(scenes.unwrap_or(0))?;
@ -174,100 +228,561 @@ mod cli {
//tek_print_status(&proj);
//return Ok(())
//}
// Initialize the app state
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(())
//}
let (_keyboard, _terminal) = Exit::run(|exited|Tui::io(
exited.as_ref(),
&app,
Duration::from_millis(100),
Duration::from_millis(10),
std::io::stdout()
))?;
Ok(proj)
}
}
}
pub use self::app::*;
mod app {
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 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!()
}
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 (
//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)
//})?)?
//}
/// Some sort of wrapper?
pub struct DeviceAudio<'a>(pub &'a mut Device);
pub fn show_version () {
println!("todo version");
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 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!("JACK: {:?}", &project.jack);
println!("Buffer: {:?}", &project.clock.chunk);
@ -295,6 +810,29 @@ mod cli {
println!("Audio Outs: {:?}", &project.audio_outs);
// TODO git 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

@ -1 +1 @@
Subproject commit c0d6d0174e8858108ec053f4ac486cf6229980e1
Subproject commit 25354099fe3cde43a242d41fdb673c4fce5c943e