diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 00000000..a583e394 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,398 @@ +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: |self: App|self.project.midi_ins); +impl_has!(Vec: |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: |self: App|self.project.as_ref()); +impl_as_mut!(Vec: |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 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 App::get_opt_u7)] +#[namespace(Option App::get_opt_u16)] +#[namespace(Option App::get_opt_usize)] +#[namespace(Option>> 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>>, + /// Undo history + pub history: Vec<(AppCommand, Option)>, + /// 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>>> +} +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 + ) -> 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) { + //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> { + Ok(src.src()?.map(|x|x.into())) + } + fn get_u16 (&self, src: impl Language) -> Perhaps { + 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 { + 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 { + 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 { + 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 { + 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> { + 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> { + Ok(None) + } + fn get_opt_usize (&self, src: impl Language) -> Perhaps> { + src.word()?.map(|word|Ok(match word { + ":selected/scene" => self.selection().scene(), + ":selected/track" => self.selection().track(), + _ => return Err(format!("unknown opt: {word}").into()) + })).transpose() + } + fn get_clip (&self, src: impl Language) -> Perhaps>>> { + 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 { + 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 { + 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 { + Ok(match &self.dialog { + Dialog::Menu(index, items) => { + let callback = items.0[*index].1.clone(); + callback(self)?; + None + }, + _ => todo!(), + }) + } +} + +pub fn swap_value ( + target: &mut T, value: &T, returned: impl Fn(T)->U +) -> Perhaps { + 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 ( + target: &mut bool, value: &Option, returned: impl Fn(Option)->U +) -> Perhaps { + 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, Vec)> { + 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)) +} diff --git a/src/app/audio.rs b/src/app/audio.rs new file mode 100644 index 00000000..e224068e --- /dev/null +++ b/src/app/audio.rs @@ -0,0 +1,52 @@ +use crate::*; + +impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } } + +impl_audio!(App: tek_jack_process, tek_jack_event); + +fn tek_jack_process (state: &mut App, client: &Client, scope: &ProcessScope) -> Control { + let t0 = state.perf.get_t0(); + state.clock().update_from_scope(scope).unwrap(); + let midi_in = state.project.midi_input_collect(scope); + if let Some(editor) = &state.editor() { + let mut pitch: Option = None; + for port in midi_in.iter() { + for event in port.iter() { + if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..})) + = event + { + pitch = Some(key.clone()); + } + } + } + if let Some(pitch) = pitch { + editor.set_note_pos(pitch.as_int() as usize); + } + } + let result = state.project.process_tracks(client, scope); + state.perf.update_from_jack_scope(t0, scope); + result +} + +fn tek_jack_event (state: &mut App, event: JackEvent) { + use JackEvent::*; + match event { + SampleRate(sr) => { state.clock().timebase.sr.set(sr as f64); }, + PortRegistration(_id, true) => { + //let port = self.jack().port_by_id(id); + //println!("\rport add: {id} {port:?}"); + //println!("\rport add: {id}"); + }, + PortRegistration(_id, false) => { + /*println!("\rport del: {id}")*/ + }, + PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ }, + PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ }, + ClientRegistration(_id, true) => {}, + ClientRegistration(_id, false) => {}, + ThreadInit => {}, + XRun => {}, + GraphReorder => {}, + _ => { panic!("{event:?}"); } + } +} diff --git a/src/app/bind.rs b/src/app/bind.rs new file mode 100644 index 00000000..c1a514f8 --- /dev/null +++ b/src/app/bind.rs @@ -0,0 +1,197 @@ +use crate::*; + +tui_keys!(self: App, input { + let commands = tek_commands_collect(self, input)?; + let results = tek_commands_execute(self, commands)?; + self.history.extend(results.into_iter()); + Ok(()) +}); + +fn tek_commands_collect (app: &App, input: &TuiEvent) + -> Usually> +{ + let mut commands = vec![]; + for id in app.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 { + for command in binding.commands.iter() { + if let Some(command) = app.namespace(command)? as Option { + commands.push(command) + } + } + } + } + } + Ok(commands) +} + +fn tek_commands_execute (app: &mut App, commands: Vec) + -> Usually)>> +{ + let mut history = vec![]; + for command in commands.into_iter() { + let result = command.act(app); + match result { Err(err) => { history.push((command, None)); return Err(err) } + Ok(undo) => { history.push((command, undo)); } }; + } + Ok(history) +} + +/// Collection of input bindings. +pub type Binds = Arc, Bind>>>>; + +pub(crate) fn load_bind (binds: &Binds, name: &impl AsRef, body: &impl Language) -> Usually<()> { + binds.write().unwrap().insert(name.as_ref().into(), Bind::load(body)?); + Ok(()) +} + +/// An map of input events (e.g. [TuiEvent]) to [Binding]s. +/// +/// ``` +/// let lang = "(@x (nop)) (@y (nop) (nop))"; +/// let bind = tek::Bind::>::load(&lang).unwrap(); +/// assert_eq!(bind.query(&'x'.into()).map(|x|x.len()), Some(1)); +/// //assert_eq!(bind.query(&'y'.into()).map(|x|x.len()), Some(2)); +/// ``` +#[derive(Debug)] pub struct Bind( + /// Map of each event (e.g. key combination) to + /// all command expressions bound to it by + /// all loaded input layers. + pub BTreeMap>> +); + +/// A sequence of zero or more commands (e.g. [AppCommand]), +/// optionally filtered by [Condition] to form layers. +/// +/// ``` +/// //FIXME: Why does it overflow? +/// //let binding: Binding<()> = tek::Binding { ..Default::default() }; +/// ``` +#[derive(Debug, Clone)] pub struct Binding { + pub commands: Arc<[C]>, + pub condition: Option, + pub description: Option>, + pub source: Option>, +} + +/// Condition that must evaluate to true in order to enable an input layer. +/// +/// ``` +/// let condition = tek::Condition(std::sync::Arc::new(Box::new(||{true}))); +/// ``` +#[derive(Clone)] pub struct Condition( + pub Arcbool + Send + Sync>> +); + +impl Bind> { + pub fn load (lang: &impl Language) -> Usually { + let mut map = Self::new(); + lang.each(|item|if item.expr().head() == Ok(Some("see")) { + // TODO + Ok(()) + } else if let Ok(Some(_word)) = item.expr().head().word() { + if let Some(event) = TuiKey::from_dsl(item.expr()?.head()?)?.to_crossterm() { + map.add(TuiEvent(event), Binding { + commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(), + condition: None, + description: None, + source: None + }); + Ok(()) + } else if Some(":char") == item.expr()?.head()? { + // TODO + return Ok(()) + } else { + return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into()) + } + } else { + return Err(format!("Config::load_bind: unexpected: {item:?}").into()) + })?; + Ok(map) + } +} + +/// Default is always empty map regardless if `E` and `C` implement [Default]. +impl Default for Bind { + fn default () -> Self { Self(Default::default()) } +} + +impl Default for Binding { + fn default () -> Self { + Self { + commands: Default::default(), + condition: Default::default(), + description: Default::default(), + source: Default::default(), + } + } +} + +impl Bind { + /// Create a new event map + pub fn new () -> Self { + Default::default() + } + /// Add a binding to an owned event map. + pub fn def (mut self, event: E, binding: Binding) -> Self { + self.add(event, binding); + self + } + /// Add a binding to an event map. + pub fn add (&mut self, event: E, binding: Binding) -> &mut Self { + if !self.0.contains_key(&event) { + self.0.insert(event.clone(), Default::default()); + } + self.0.get_mut(&event).unwrap().push(binding); + self + } + /// Return the binding(s) that correspond to an event. + pub fn query (&self, event: &E) -> Option<&[Binding]> { + self.0.get(event).map(|x|x.as_slice()) + } + /// Return the first binding that corresponds to an event, considering conditions. + pub fn dispatch (&self, event: &E) -> Option<&Binding> { + self.query(event) + .map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next()) + .flatten() + } +} + +impl_debug!(Condition |self, w| { write!(w, "*") }); + +impl_default!(AppCommand: Self::Nop); + +def_command!(AppCommand: |app: App| { + Nop => Ok(None), + Cancel => todo!(), // TODO delegate: + Confirm => app.confirm(), + Inc { axis: ControlAxis } => app.inc(axis), + Dec { axis: ControlAxis } => app.dec(axis), + SetDialog { dialog: Dialog } => { + swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }) + }, +}); + +impl<'a> Namespace<'a, AppCommand> for App { + symbols!('a |app| -> AppCommand { + "x/inc" => AppCommand::Inc { axis: ControlAxis::X }, + "x/dec" => AppCommand::Dec { axis: ControlAxis::X }, + "y/inc" => AppCommand::Inc { axis: ControlAxis::Y }, + "y/dec" => AppCommand::Dec { axis: ControlAxis::Y }, + "confirm" => AppCommand::Confirm, + "cancel" => AppCommand::Cancel, + }); +} + +/// A control axis. +/// +/// ``` +/// let axis = tek::ControlAxis::X; +/// ``` +#[derive(Debug, Copy, Clone)] pub enum ControlAxis { + X, Y, Z, I +} + +//take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref() + //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); diff --git a/src/app/config.rs b/src/app/config.rs new file mode 100644 index 00000000..ff935638 --- /dev/null +++ b/src/app/config.rs @@ -0,0 +1,120 @@ +use crate::*; + +/// Configuration: mode, view, and bind definitions. +/// +/// ``` +/// let config = tek::Config::default(); +/// ``` +/// +/// ``` +/// // Some dizzle. +/// // What indentation to use here lol? +/// let source = stringify!((mode :menu (name Menu) +/// (info Mode selector.) (keys :axis/y :confirm) +/// (view (bg (g 0) (bsp/s :ports/out +/// (bsp/n :ports/in +/// (bg (g 30) (bsp/s (fixed/y 7 :logo) +/// (fill :dialog/menu))))))))); +/// // Add this definition to the config and try to load it. +/// // A "mode" is basically a state machine +/// // with associated input and output definitions. +/// tek::Config::default().add(&source).unwrap().get_mode(":menu").unwrap(); +/// ``` +#[derive(Default, Debug)] pub struct Config { + /// XDG base directories of running user. + pub dirs: BaseDirectories, + /// Active collection of interaction modes. + pub modes: Modes, + /// Active collection of event bindings. + pub binds: Binds, + /// Active collection of view definitions. + pub views: Views, +} + +impl Config { + const CONFIG_DIR: &'static str = "tek"; + const CONFIG_SUB: &'static str = "v0"; + const CONFIG: &'static str = "tek.edn"; + const DEFAULTS: &'static str = include_str!("../tek.edn"); + + pub fn watch (callback: impl FnOnce(Self)->T) -> Usually { + let config = Self::init_new(None)?; + let watcher = notify_debouncer_mini::new_debouncer(Duration::from_millis(500), |res| { + println!("{res:?}"); + })?; + let result = callback(config); + Ok(result) + } + + pub fn init_new (dirs: Option) -> Usually { + let mut config = Self::new(None); + config.init()?; + Ok(config) + } + + /// Create a new app configuration from a set of XDG base directories, + pub fn new (dirs: Option) -> Self { + let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB); + let dirs = dirs.unwrap_or_else(default); + Self { dirs, ..Default::default() } + } + + /// Write initial contents of configuration. + pub fn init (&mut self) -> Usually<()> { + self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|{ + cfgs.add(&dsl)?; + Ok(()) + })?; + Ok(()) + } + + /// Write initial contents of a configuration file. + pub fn init_one ( + &mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()> + ) -> Usually<()> { + if self.dirs.find_config_file(path).is_none() { + //println!("Creating {path:?}"); + std::fs::write(self.dirs.place_config_file(path)?, defaults)?; + } + Ok(if let Some(path) = self.dirs.find_config_file(path) { + //println!("Loading {path:?}"); + let src = std::fs::read_to_string(&path)?; + src.as_str().each(move|item|each(self, item))?; + } else { + return Err(format!("{path}: not found").into()) + }) + } + + /// Add statements to configuration from [Dsl] source. + pub fn add (&mut self, dsl: impl Language) -> Usually<&mut Self> { + dsl.each(|item|self.add_one(item))?; + Ok(self) + } + + fn add_one (&self, item: impl Language) -> Usually<()> { + if let Some(expr) = item.expr()? { + let head = expr.head()?; + let tail = expr.tail()?; + let name = tail.head()?; + let body = tail.tail()?; + //println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default()); + match head { + Some("mode") if let Some(name) = name => self.modes.add(&name, &body)?, + Some("keys") if let Some(name) = name => load_bind(&self.binds, &name, &body)?, + Some("view") if let Some(name) = name => load_view(&self.views, &name, &body)?, + _ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into()) + } + Ok(()) + } else { + return Err(format!("Config::load: expected expr, got: {item:?}").into()) + } + } + + pub fn get_mode (&self, mode: impl AsRef) -> Option>>> { + self.modes.get(mode) + } +} + +mod views; pub use self::views::*; +mod modes; pub use self::modes::*; +mod mode; pub use self::mode::*; diff --git a/src/app/config/mode.rs b/src/app/config/mode.rs new file mode 100644 index 00000000..83d25ba9 --- /dev/null +++ b/src/app/config/mode.rs @@ -0,0 +1,83 @@ +use crate::*; + +/// Group of view and keys definitions. +/// +/// ``` +/// let mode = tek::Mode::>::default(); +/// ``` +#[derive(Default, Debug)] pub struct Mode { + pub path: PathBuf, + pub name: Vec, + pub info: Vec, + pub view: Vec, + pub keys: Vec, + pub modes: Modes, +} + +impl Mode> { + /// Add a definition to the mode. + /// + /// Supported definitions: + /// + /// - (name ...) -> name + /// - (info ...) -> description + /// - (keys ...) -> key bindings + /// - (mode ...) -> submode + /// - ... -> view + /// + /// ``` + /// let mut mode: tek::Mode> = Default::default(); + /// mode.add("(name hello)").unwrap(); + /// ``` + pub fn add (&mut self, dsl: impl Language) -> Usually<()> { + Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() { + //println!("Mode::add: {head} {:?}", expr.tail()); + let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); + match head { + "name" => self.add_name(tail)?, + "info" => self.add_info(tail)?, + "keys" => self.add_keys(tail)?, + "mode" => self.add_mode(tail)?, + _ => self.add_view(tail)?, + }; + } else if let Ok(Some(word)) = dsl.word() { + self.add_view(word); + } else { + return Err(format!("Mode::add: unexpected: {dsl:?}").into()); + }) + + //DslParse(dsl, ||Err(format!("Mode::add: unexpected: {dsl:?}").into())) + //.word(|word|self.add_view(word)) + //.expr(|expr|expr.head(|head|{ + ////println!("Mode::add: {head} {:?}", expr.tail()); + //let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); + //match head { + //"name" => self.add_name(tail), + //"info" => self.add_info(tail), + //"keys" => self.add_keys(tail)?, + //"mode" => self.add_mode(tail)?, + //_ => self.add_view(tail), + //}; + //})) + } + + fn add_name (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.name.push(src.into()))) + } + fn add_info (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.info.push(src.into()))) + } + fn add_view (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(dsl.src()?.map(|src|self.view.push(src.into()))) + } + fn add_keys (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(Some(dsl.each(|expr|{ self.keys.push(expr.trim().into()); Ok(()) })?)) + } + fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> { + Ok(Some(if let Some(id) = dsl.head()? { + self.modes.add(&id, &dsl.tail())?; + } else { + return Err(format!("Mode::add: self: incomplete: {dsl:?}").into()); + })) + } +} diff --git a/src/app/config/modes.rs b/src/app/config/modes.rs new file mode 100644 index 00000000..9ff83991 --- /dev/null +++ b/src/app/config/modes.rs @@ -0,0 +1,27 @@ +use crate::*; + +/// Collection of UI modes. +#[derive(Default, Debug, Clone)] +pub struct Modes( + Arc, Arc>>>>> +); + +impl Modes { + pub fn add (&self, name: &impl AsRef, 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) -> Option>>> { + self.0.read().unwrap().get(name.as_ref()).cloned() + } + pub fn for_each (&self, mut ator: impl FnMut(&str, &Mode>)->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() + } +} diff --git a/src/app/config/views.rs b/src/app/config/views.rs new file mode 100644 index 00000000..79560813 --- /dev/null +++ b/src/app/config/views.rs @@ -0,0 +1,10 @@ +use crate::*; + +/// Collection of custom view definitions. +pub type Views = Arc, Arc>>>; + +/// Load custom view definition. +pub(crate) fn load_view (views: &Views, name: &impl AsRef, body: &impl Language) -> Usually<()> { + views.write().unwrap().insert(name.as_ref().into(), body.src()?.unwrap_or_default().into()); + Ok(()) +} diff --git a/src/app/draw.rs b/src/app/draw.rs new file mode 100644 index 00000000..3f439d12 --- /dev/null +++ b/src/app/draw.rs @@ -0,0 +1,750 @@ +use crate::*; + +/// The [Draw] implementation for [App] handles the loaded view, +/// which is defined in terms of [dizzle] DSL. +/// +/// If there is an error, the error is displayed. FIXME: overlay it +/// Then, every top-level form of the DSL description is rendered. +impl View for App { + fn view (&self) -> impl Draw { + 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()))?; + } + for (index, dsl) in self.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()); + break; + } + } + Ok(Some(xywh)) + }) + } +} + +impl Interpret>> for App { + fn interpret_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Drawn { + tek_draw_expr(self, to, lang) + } + fn interpret_word <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Drawn { + tek_draw_word(self, to, lang) + } +} + +fn tek_draw_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Drawn { + Ok(Some(if let Some(area) = eval_view(state, to, lang)? { + area + } else if let Some(area) = eval_view_tui(state, to, lang)? { + area + } else { + return Err(format!("App::interpret_expr: unexpected: {lang:?}").into()) + })) +} + +fn tek_draw_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Drawn { + let mut frags = dsl.src()?.unwrap().split("/"); + match frags.next() { + //Some(":logo") => view_logo().draw(to), + Some(":meters") => draw_meter_section(to, frags), + Some(":tracks") => draw_tracks(to, frags, state), + Some(":scenes") => draw_scenes(to, frags), + Some(":dialog") => draw_dialog(to, frags, state, dsl), + Some(":templates") => draw_templates(to, frags, state), + Some(":sessions") => view_sessions().draw(to), + Some(":browse/title") => view_browse_title(state).draw(to), + Some(":device") => view_device(state).draw(to), + Some(":status") => "TODO: Status Bar".exact_h(1).draw(to), + Some(":editor") => "TODO Editor".draw(to), + Some(":transport") => view_transport(true, "", "", "").draw(to), + Some(":debug") => format!("[{:?}]", to.area()).exact_h(1).draw(to), + Some(_) => { + let views = state.config.views.read().unwrap(); + if let Some(dsl) = views.get(dsl.src()?.unwrap()) { + let dsl = dsl.clone(); + std::mem::drop(views); + state.interpret(to, &dsl) + } else { + unimplemented!("{dsl:?}"); + } + }, + _ => unreachable!() + } +} + +pub fn draw_meter_section (to: &mut Tui, mut frags: std::str::Split<&str>) -> Drawn { + match frags.next() { + Some("input") => bg(Rgb(30, 30, 30), "Input Meters".align_s().full_h()).draw(to), + Some("output") => bg(Rgb(30, 30, 30), "Output Meters".align_s().full_h()).draw(to), + _ => panic!() + } +} + +pub fn draw_tracks (to: &mut Tui, mut frags: std::str::Split<&str>, state: &App) -> Drawn { + match frags.next() { + None => "TODO tracks".draw(to), + Some("names") => state.project.view_track_names(state.color.clone()).draw(to),//bg(Rgb(40, 40, 40), full_w(align_w("Track Names")))), + Some("inputs") => bg(Rgb(40, 40, 40), "Track Inputs".align_w().full_w()).draw(to), + Some("devices") => bg(Rgb(40, 40, 40), "Track Devices".align_w().full_w()).draw(to), + Some("outputs") => bg(Rgb(40, 40, 40), "Track Outputs".align_w().full_w()).draw(to), + _ => panic!() + } +} + +pub fn draw_scenes (to: &mut Tui, mut frags: std::str::Split<&str>) -> Drawn { + match frags.next() { + None => "TODO Scenes".draw(to), + Some(":scenes/names") => "TODO Scene Names".draw(to), + _ => panic!() + } +} + +pub fn draw_dialog ( + to: &mut Tui, mut frags: std::str::Split<&str>, state: &App, dsl: &impl Expression) -> Drawn { + match frags.next() { + Some("menu") => if let Dialog::Menu(selected, items) = &state.dialog { + let items = items.clone(); + let selected = selected; + Some(thunk(move|to: &mut Tui|{ + for (index, MenuItem(item, _)) in items.0.iter().enumerate() { + let f = if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }; + let b = if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }; + fg_bg(f, b, item.full_w().align_w().exact_h(2)) + .push_y((4 * index) as u16).draw(to)?; + } + Ok(Some(to.area().into())) + }).full_wh()) + } else { + None + }.draw(to), + _ => unimplemented!("App::interpret_word: {dsl:?} ({frags:?})"), + } +} + +pub fn draw_templates (to: &mut Tui, frags: std::str::Split<&str>, state: &App) -> Drawn { + let height = (state.config.modes.len() * 2) as u16; + thunk(move |to: &mut Tui|{ + let mut index = 0; + state.config.modes.for_each(|id, profile| { + let b = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; + let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); + let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); + let fg1 = Rgb(224, 192, 128); + let fg2 = Rgb(224, 128, 32); + let field_name = fg(fg1, name).align_w().full_w(); + let field_id = fg(fg2, id).align_e().full_w(); + let field_info = info.align_w().full_w(); + let _ = bg(b, south(above(field_name, field_id), field_info)) + .full_w().exact_h(2).push_y((2 * index) as u16).draw(to); + index += 1; + }); + Ok(Some(to.area().into())) + }).min_w(30).exact_h(height).draw(to) +} + +/// ``` +/// let x = ""; +/// let _ = tek::view_transport(true, x.as_ref(), x.as_ref(), x.as_ref()); +/// let _ = tek::view_transport(false, x.as_ref(), x.as_ref(), x.as_ref()); +/// ``` +pub fn view_transport (play: bool, bpm: &str, beat: &str, time: &str) -> impl Draw { + let theme = ItemTheme::G[96]; + bg(Black, east!(above( + button_play_pause(play, false).align_w(), + east!( + field_h(theme, "BPM", bpm), + field_h(theme, "Beat", beat), + field_h(theme, "Time", time), + ).align_e().full_wh() + ))) +} + +/// ``` +/// let x = ""; +/// let _ = tek::view_status(None, x.as_ref(), x.as_ref(), x.as_ref()); +/// let _ = tek::view_status(Some("".into()), x.as_ref(), x.as_ref(), x.as_ref()); +/// ``` +pub fn view_status (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl Draw { + let theme = ItemTheme::G[96]; + let sr = field_h(theme, "SR", sr); + let buf = field_h(theme, "Buf", buf); + let lat = field_h(theme, "Lat", lat); + bg(Black, east!(above( + sel.map(|sel|field_h(theme, "Selected", sel)).align_w().full_wh(), + east!(sr, buf, lat).align_e().full_wh(), + ))) +} + +/// ``` +/// let _ = tek::button_play_pause(true, true); +/// let _ = tek::button_play_pause(true, false); +/// let _ = tek::button_play_pause(false, true); +/// let _ = tek::button_play_pause(false, false); +/// ``` +pub fn button_play_pause (playing: bool, compact: bool) -> impl Draw { + bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, + either(compact, + thunk(move|to: &mut Tui|either(playing, + fg(Rgb(0, 255, 0), " PLAYING "), + fg(Rgb(255, 128, 0), " STOPPED "), + ).exact_w(9).draw(to)), + thunk(move|to: &mut Tui|either(playing, + fg(Rgb(0, 255, 0), south(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), + fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",)), + ).exact_w(5).draw(to)), + ) + ) +} + +#[cfg(feature = "track")] pub fn view_track_row_section ( + _theme: ItemTheme, + button: impl Draw, + button_add: impl Draw, + content: impl Draw, +) -> impl Draw { + west( + button_add.align_nw().exact_w(4).full_h(), + east( + button.align_nw().full_h().exact_w(20), + content.align_c().full_wh() + ) + ) +} + +/// ``` +/// let bg = tengri::ratatui::style::Color::Red; +/// let fg = tengri::ratatui::style::Color::Green; +/// let _ = tek::view_wrap(bg, fg, "and then blue, too!"); +/// ``` +pub fn view_wrap (bg: Color, fg: Color, content: impl Draw) -> impl Draw { + let left = fg_bg(bg, Reset, y_repeat("▐").exact_w(1)); + let right = fg_bg(bg, Reset, y_repeat("▌").exact_w(1)); + east(left, west(right, fg_bg(fg, bg, content))) +} + +/// ``` +/// let _ = tek::view_meter("", 0.0); +/// let _ = tek::view_meters(&[0.0, 0.0]); +/// ``` +pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Draw + 'a { + let f = field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value)); + let w = if value >= 0.0 { 13 } + else if value >= -1.0 { 12 } + else if value >= -2.0 { 11 } + else if value >= -3.0 { 10 } + else if value >= -4.0 { 9 } + else if value >= -6.0 { 8 } + else if value >= -9.0 { 7 } + else if value >= -12.0 { 6 } + else if value >= -15.0 { 5 } + else if value >= -20.0 { 4 } + else if value >= -25.0 { 3 } + else if value >= -30.0 { 2 } + else if value >= -40.0 { 1 } + else { 0 }; + let c = if value >= 0.0 { Red } + else if value >= -3.0 { Yellow } + else { Green }; + south!(f, bg(c, ()).exact_wh(w, 1)) +} + +pub fn view_meters (values: &[f32;2]) -> impl Draw + use<'_> { + let left = format!("L/{:>+9.3}", values[0]); + let right = format!("R/{:>+9.3}", values[1]); + south(left, right) +} + +pub fn view_sample_info (sample: Option<&Arc>>) -> impl Draw + use<'_> { + when(sample.is_some(), thunk(move|to: &mut Tui|{ + let sample = sample.unwrap().read().unwrap(); + let theme = sample.color; + east!( + field_h(theme, "Name", format!("{:<10}", sample.name.clone())), + field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())), + field_h(theme, "Start", format!("{:<8}", sample.start)), + field_h(theme, "End", format!("{:<8}", sample.end)), + field_h(theme, "Trans", "0"), + field_h(theme, "Gain", format!("{}", sample.gain)), + ).draw(to) + })) +} + +pub fn view_sample_info_v (sample: Option<&Arc>>) -> impl Draw + use<'_> { + let a = thunk(move|to: &mut Tui|{ + let sample = sample.unwrap().read().unwrap(); + let theme = sample.color; + south!( + field_h(theme, "Name ", format!("{:<10}", sample.name.clone())) .align_w().full_w(), + field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())).align_w().full_w(), + field_h(theme, "Start ", format!("{:<8}", sample.start)) .align_w().full_w(), + field_h(theme, "End ", format!("{:<8}", sample.end)) .align_w().full_w(), + field_h(theme, "Trans ", "0") .align_w().full_w(), + field_h(theme, "Gain ", format!("{}", sample.gain)) .align_w().full_w(), + ).exact_w(20).draw(to) + }); + + let b = thunk(|to: &mut Tui|fg(Red, south!( + bold(true, "× No sample."), + "[r] record", + "[Shift-F9] import", + )).draw(to)); + + either(sample.is_some(), a, b) +} + +pub fn view_sample_status (sample: Option<&Arc>>) -> impl Draw { + bold(true, fg(g(224), sample + .map(|sample|{ + let sample = sample.read().unwrap(); + format!("Sample {}-{}", sample.start, sample.end) + }) + .unwrap_or_else(||"No sample".to_string()))) +} + +pub fn view_track_header (theme: ItemTheme, content: impl Draw) -> impl Draw { + bg(theme.darker.term, content.align_e().full_w()).exact_w(12) +} + +pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T]) + -> impl Draw + use<'a, T> +{ + let ins = ports.len() as u16; + let frame = Outer(true, Style::default().fg(g(96))); + let iter = move||ports.iter(); + let names = iter_south(iter, move|port, index|format!(" {index} {}", port.port_name()).align_w().full_h()); + let field = field_v(theme, title, names); + border(true, frame, field.exact_wh(20, 1 + ins)).exact_wh(20, 1 + ins) +} + +pub fn view_io_ports <'a, T: PortsSizes<'a>> ( + fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a +) -> impl Draw + 'a { + type Item<'a> = (usize, &'a Arc, &'a [Connect], usize, usize); + iter(items, + move|(_index, name, connections, y, y2): Item<'a>, _| south( + bold(true, fg_bg(fg, bg, east(" 󰣲 ", name).align_w())).full_h(), + iter(||connections.iter(), move|connect: &'a Connect, index|{ + bold(false, fg_bg(fg, bg, &connect.info)).exact_h(1).align_w().push_y(index as u16) + }) + ).exact_h((y2 - y) as u16).push_y(y as u16)) +} + +pub fn view_scenes_clips <'a, S: ScenesSizes<'a>> ( + scenes: impl Fn()->S, + tracks: impl TracksSizes<'a>, + select: &Selection, + editor: Option<&MidiEditor>, + size: &Sizer, + editing: bool, +) -> impl Draw { + let status = fg(Green, format!("{}x{}", size.w(), size.h())).align_se().full_wh(); + let tracks = iter_once(tracks, move|(track_index, track, _, _), _| { + let scenes = iter_once(scenes(), move|(scene_index, scene, _, _), _| { + let (name, theme): (Arc, ItemTheme) = scene_name_theme(scene, track_index); + let f = theme.lightest.term; + let (b, o) = scene_bg(theme, select, track_index, scene_index); + let w = scene_w(track, select, track_index, editor); + let y = scene_y(select, scene_index, editor); + let is_selected = scene_sel(select, track_index, scene_index, editing); + below( + Outer(true, Style::default().fg(o)).full_wh(), + below( + below( + fg_bg(o, b, "".full_wh()), + fg_bg(f, b, bold(true, name)).align_nw().full_wh(), + ), + when(is_selected, editor.map(|e|e.view())).full_wh() + ).full_wh() + ).exact_wh(w, y) + }); + scenes.full_h().exact_w(track.width as u16) + }); + + return size.of(above(status, tracks).full_wh()); + + fn scene_name_theme (scene: &Scene, track_index: usize) -> (Arc, ItemTheme) { + if let Some(Some(clip)) = &scene.clips.get(track_index) { + let clip = clip.read().unwrap(); + (format!(" ⏹ {}", &clip.name).into(), clip.color) + } else { + (" ⏹ -- ".into(), ItemTheme::G[32]) + } + } + + fn scene_bg ( + theme: ItemTheme, select: &Selection, track_index: usize, scene_index: usize + ) -> (Color, Color) { + let mut outline = theme.base.term; + (if select.track() == Some(track_index) && select.scene() == Some(scene_index) { + outline = theme.lighter.term; + theme.light.term + } else if select.track() == Some(track_index) || select.scene() == Some(scene_index) { + outline = theme.darkest.term; + theme.base.term + } else { + theme.dark.term + }, outline) + } + + fn scene_w ( + track: &Track, select: &Selection, track_index: usize, editor: Option<&MidiEditor> + ) -> u16 { + if select.track() == Some(track_index) && let Some(editor) = editor { + (editor.size.w() as usize).max(24).max(track.width) as u16 + } else { + track.width as u16 + } + } + + fn scene_y ( + select: &Selection, scene_index: usize, editor: Option<&MidiEditor> + ) -> u16 { + if select.scene() == Some(scene_index) && let Some(editor) = editor { + editor.size.h().max(12) + } else { + H_SCENE as u16 + } + } + + fn scene_sel (select: &Selection, track_index: usize, scene_index: usize, editing: bool) -> bool { + editing && select.track() == Some(track_index) && select.scene() == Some(scene_index) + } +} + +pub fn view_track_names ( + theme: ItemTheme, + tracks: impl TracksSizes<'_>, + track_count: usize, + scene_count: usize, + selected: &Selection, +) -> impl Draw { + let button = south( + button_3("t", "rack ", format!("{}{track_count}", selected.track() + .map(|track|format!("{track}/")).unwrap_or_default()), false), + button_3("s", "cene ", format!("{}{scene_count}", selected.scene() + .map(|scene|format!("{scene}/")).unwrap_or_default()), false)); + let button_2 = south( + button_2("T", "+", false), + button_2("S", "+", false)); + view_track_row_section(theme, button, button_2, bg(theme.darker.term, + thunk(|to: &mut Tui|{ + for (index, track, x1, _x2) in tracks { + let b = if selected.track() == Some(index) { + track.color.light.term + } else { + track.color.base.term + }; + bg(b, south(east( + format!("·t{index:02} "), + fg(Rgb(255, 255, 255), bold(true, &track.name)) + ).align_nw().full_w(), "")) + .exact_w(track_width(index, track)) + .push_x(x1 as u16) + .draw(to)?; + } + Ok(Some(XYWH(0, 0, 0, 0))) + }).exact_h(2))) +} + +pub fn view_track_outputs ( + theme: ItemTheme, tracks: impl TracksSizes<'_>, midi_outs: impl Iterator, +) -> impl Draw { + view_track_row_section(theme, + south(button_2("o", "utput", false).align_w().full_w(), + thunk(|to: &mut Tui|{ + for port in midi_outs { + let _ = port.port_name().align_w().full_w().draw(to)?; + } + Ok(Some(XYWH(0, 0, 0, 0))) + })), + button_2("O", "+", false), + bg(theme.darker.term, thunk(|to: &mut Tui|{ + for (index, track, _x1, _x2) in tracks { + let f = Rgb(255, 255, 255); + let b = track.color.dark.term; + let iter = ||track.sequencer.midi_outs.iter(); + let draw = |port: &MidiOutput, _|fg(f, bg(b, + format!("·o{index:02} {}", port.port_name()).full_w().align_w()).exact_h(1)); + iter_south(iter, draw).full_h().align_nw() + .exact_w(track_width(index, track)) + .draw(to)?; + } + Ok(Some(XYWH(0, 0, 0, 0))) + }).align_w())) +} + +pub fn view_track_inputs ( + theme: ItemTheme, tracks: impl TracksSizes<'_>, height: u16, +) -> impl Draw { + view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false), + bg(theme.darker.term, thunk(move|to: &mut Tui|{ + for (index, track, _x1, _x2) in tracks { + south( + bg(track.color.base.term, + east!( + either(track.sequencer.monitoring, fg(Green, "●mon "), "·mon "), + either(track.sequencer.recording, fg(Red, "●rec "), "·rec "), + either(track.sequencer.overdub, fg(Yellow, "●dub "), "·dub "), + ).align_w().full_w()), + iter_south(||track.sequencer.midi_ins.iter(), + |port, _|fg_bg(Rgb(255, 255, 255), track.color.dark.term, + format!("·i{index:02} {}", port.port_name()).align_w().full_w())) + ).align_nw().exact_wh(track_width(index, track), height + 1).draw(to)?; + } + Ok(Some(XYWH(0, 0, 0, 0))) + }).align_w())) +} + +pub fn view_scenes_names ( + scenes: impl ScenesSizes<'_>, + select: &Selection, + editor: Option<&MidiEditor>, + editing: bool, +) -> impl Draw { + thunk(move |to: &mut Tui|{ + for (index, scene, ..) in scenes { + view_scene_name(select, editor, index, scene, editing).draw(to)?; + } + Ok(Some(XYWH(1, 1, 1, 1))) + }).exact_w(20) +} + +pub fn view_scene_name ( + select: &Selection, + editor: Option<&MidiEditor>, + index: usize, + scene: &Scene, + editing: bool +) -> impl Draw { + let h = if select.scene() == Some(index) && let Some(_editor) = editor { + 7 + } else { + H_SCENE as u16 + }; + let a = east(format!("·s{index:02} "), + fg(g(255), bold(true, &scene.name))).align_w().full_w(); + let b = when(select.scene() == Some(index) && editing, south( + editor.as_ref().map(|e|e.clip_status()), + editor.as_ref().map(|e|e.edit_status())).align_nw().full_wh()); + let c = if select.scene() == Some(index) { + scene.color.light.term + } else { + scene.color.base.term + }; + bg(c, south(a, b).align_nw()).exact_wh(20, h) +} + +pub fn view_midi_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { + track.map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins)) +} + +pub fn view_midi_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { + track.map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs)) +} + +pub fn view_audio_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { + track.map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins())) +} + +pub fn view_audio_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { + track.map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs())) +} + +pub fn view_track_per <'a, T: Draw + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a +) -> impl Draw { + iter_east(tracks, move|(index, track, x1, x2): (usize, &Track, usize, usize), _|{ + fg_bg( + track.color.lightest.term, + track.color.base.term, + callback(index, track) + ).exact_w((x2 - x1) as u16) + }) +} + +pub fn view_per_track () -> impl Draw {} + +pub fn view_per_track_top () -> impl Draw {} + +pub fn view_inputs (tracks: impl TracksSizes<'_>, midi_ins: &[MidiInput]) -> impl Draw { + let title_1 = button_3("i", "nput ", format!("{}", midi_ins.len()), false).align_w().exact_wh(20, 1); + let title_2 = button_2("I", "+", false).exact_wh(4, 1); + east(title_1, west(title_2, thunk(move|to: &mut Tui|{ + for (_index, track, x1, _x2) in tracks { + let _ = south( + bg(track.color.dark.term, east!( + either(track.sequencer.monitoring, fg(Green, "mon "), "mon "), + either(track.sequencer.recording, fg(Red, "rec "), "rec "), + either(track.sequencer.overdub, fg(Yellow, "dub "), "dub "), + ).exact_w(track.width as u16)).align_w().push_x(x1 as u16), + thunk(move |to: &mut Tui|{ + for (index, port) in midi_ins.iter().enumerate() { + let _ = east( + east( + " ● ", + bold(true, fg(Rgb(255,255,255), port.port_name())) + ).align_w().exact_w(20), + west( + ().exact_w(4), + bg(track.color.darker.term, east!( + either(track.sequencer.monitoring, fg(Green, " ● "), " · "), + either(track.sequencer.recording, fg(Red, " ● "), " · "), + either(track.sequencer.overdub, fg(Yellow, " ● "), " · "), + ).exact_w(track.width as u16).align_w()) + ) + ).push_x(index as u16 * 10).exact_h(1).draw(to)?; + } + todo!() + }) + ).draw(to)?; + } + todo!() + }))) +} + +pub fn view_outputs ( + theme: ItemTheme, + tracks: impl TracksSizes<'_>, + midi_outs: &[MidiOutput], + height: u16, +) -> impl Draw { + + let list = south( + button_3( + "o", "utput", format!("{}", midi_outs.len()), false + ).align_w().full_w().exact_h(1), + thunk(|to: &mut Tui|{ + for (_index, port) in midi_outs.iter().enumerate() { + east( + east(" ● ", fg(Rgb(255,255,255), bold(true, port.port_name()))).align_w(), + format!("{}/{} ", + port.port().get_connections().len(), + port.connections.len()).align_e().full_w().exact_h(1)).full_w().draw(to)?; + for (index, conn) in port.connections.iter().enumerate() { + format!(" c{index:02}{}", conn.info()).align_w().full_w().exact_h(1).draw(to)?; + } + } + todo!(); + }).align_nw().full_wh().exact_h(height - 1) + ); + + view_track_row_section(theme, list, button_2("O", "+", false), + bg(theme.darker.term, thunk(|to: &mut Tui|{ + for (index, track, _x1, _x2) in tracks { + let _ = thunk(|to: &mut Tui|{ + east( + either(true, fg(Green, "play "), "play "), + either(false, fg(Yellow, "solo "), "solo "), + ).align_w().exact_h(1).draw(to)?; + for (_index, port) in midi_outs.iter().enumerate() { + east( + either(true, fg(Green, " ● "), " · "), + either(false, fg(Yellow, " ● "), " · "), + ).align_w().exact_h(1).draw(to)?; + for (_index, _conn) in port.connections.iter().enumerate() { + "".full_w().exact_h(1).draw(to)?; + } + } + todo!() + }).exact_w(track_width(index, track)).draw(to)?; + } + todo!() + }).align_w().full_w())).exact_h(height) +} + +pub fn view_track_devices ( + theme: ItemTheme, + tracks: impl TracksSizes<'_>, + track: Option<&Track>, + h: u16, +) -> impl Draw { + view_track_row_section(theme, + button_3("d", "evice", format!("{}", track.map(|t|t.devices.len()).unwrap_or(0)), false), + button_2("D", "+", false), + iter_once(tracks, move|(_, track, _x1, _x2), index|bg( + track.color.dark.term, + iter_south(move||0..h, + |_, _index|fg_bg( + ItemTheme::G[32].lightest.term, + ItemTheme::G[32].dark.term, + format!(" · {}", "--").align_nw() + ).exact_wh(track.width as u16, 2) + ).align_nw()).exact_wh( + Some(track_width(index, track)), + Some(h + 1), + ))) +} + +pub fn per_track <'a, T: Draw + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a +) -> impl Draw + 'a { + per_track_top(tracks, move|index, track|callback(index, track).full_h().align_y()) +} + +pub fn per_track_top <'a, T: Draw + 'a, U: TracksSizes<'a>> ( + tracks: impl Fn() -> U + Send + Sync + 'a, + callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a +) -> impl Draw + '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 ( + _theme: ItemTheme, _head: impl Draw, _body: impl Draw +) -> impl Draw { +} + +pub fn field_v ( + _theme: ItemTheme, _head: impl Draw, _body: impl Draw +) -> impl Draw { +} + +pub fn view_sessions () -> impl Draw { + 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 { + 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 { + 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() + })) +} diff --git a/src/app/size.rs b/src/app/size.rs new file mode 100644 index 00000000..8c80c077 --- /dev/null +++ b/src/app/size.rs @@ -0,0 +1,25 @@ +use crate::*; + +impl_has!(Sizer: |self: App|self.size); + +/// Define a type alias for iterators of sized items (columns). +macro_rules! def_sizes_iter { + ($Type:ident => $($Item:ty),+) => { + pub trait $Type<'a> = + Iterator + Send + Sync + 'a; + } +} + +def_sizes_iter!(InputsSizes => MidiInput); +def_sizes_iter!(OutputsSizes => MidiOutput); +def_sizes_iter!(PortsSizes => Arc, [Connect]); +def_sizes_iter!(ScenesSizes => Scene); +def_sizes_iter!(TracksSizes => Track); + +pub trait HasWidth { + const MIN_WIDTH: usize; + /// Increment track width. + fn width_inc (&mut self); + /// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH]. + fn width_dec (&mut self); +} diff --git a/src/deps.rs b/src/deps.rs new file mode 100644 index 00000000..2f9683a8 --- /dev/null +++ b/src/deps.rs @@ -0,0 +1,32 @@ +#[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}; diff --git a/src/device.rs b/src/device.rs new file mode 100644 index 00000000..8e842c52 --- /dev/null +++ b/src/device.rs @@ -0,0 +1,144 @@ +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> + AsMut>> HasDevices for T { + fn devices (&self) -> &Vec { + self.as_ref() + } + fn devices_mut (&mut self) -> &mut Vec { + self.as_mut() + } +} + +pub trait HasDevices: AsRef> + AsMut> { + fn devices (&self) -> &Vec { + self.as_ref() + } + fn devices_mut (&mut self) -> &mut Vec { + 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 } => todo!(), +}); + +def_command!(AudioOutputCommand: |port: AudioOutput| { + Close => todo!(), + Connect { audio_in: Arc } => todo!(), +}); + +def_command!(MidiInputCommand: |port: MidiInput| { + Close => todo!(), + Connect { midi_out: Arc } => todo!(), +}); + +def_command!(MidiOutputCommand: |port: MidiOutput| { + Close => todo!(), + Connect { midi_in: Arc } => todo!(), +}); + +pub struct Junction(T); + +impl View for Junction { + fn view (&self) -> impl Draw { + T::KIND + } +} diff --git a/src/device/arrange.rs b/src/device/arrange.rs index 92cdbc23..d54a8b2e 100644 --- a/src/device/arrange.rs +++ b/src/device/arrange.rs @@ -7,12 +7,12 @@ use crate::*; /// ``` #[derive(Default, Debug)] pub struct Arrangement { - /// JACK client handle. - pub jack: Jack<'static>, /// Project name. pub name: Arc, /// 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>, diff --git a/src/device/browse.rs b/src/device/browse.rs index a5f61c7a..7eccc2dd 100644 --- a/src/device/browse.rs +++ b/src/device/browse.rs @@ -1,4 +1,4 @@ -use crate::*; +use crate::{*, clock::*, sequence::*, sampler::*}; def_command!(FileBrowserCommand: |sampler: Sampler|{ //("begin" [] Some(Self::Begin)) @@ -246,7 +246,7 @@ impl<'a> PoolView<'a> { move|clip: Arc>, i: usize|{ let MidiClip { ref name, color, length, .. } = *clip.read().unwrap(); let item_height = 1; - let _item_offset = i as u16 * item_height; + let item_offset = i as u16 * item_height; let selected = i == pool.clip_index(); let b = if selected { color.light.term } else { color.base.term }; let f = color.lightest.term; @@ -317,7 +317,7 @@ impl Browse { fn tui (&self) -> impl Draw { iter_south_fixed(1, ||self.tui_entries(), |entry, _index|entry.origin_w().full_w()) } - fn tui_entries (&self) -> EntriesIterator<'_, Tui> { + fn tui_entries (&self) -> EntriesIterator { EntriesIterator { offset: 0, index: 0, diff --git a/src/device/clock/memo.rs b/src/device/clock/memo.rs index 33691d80..91a3290c 100644 --- a/src/device/clock/memo.rs +++ b/src/device/clock/memo.rs @@ -1,4 +1,3 @@ -#![allow(unused)] use crate::*; #[macro_export] macro_rules! rewrite { diff --git a/src/device/clock/moment.rs b/src/device/clock/moment.rs index d0e9eac2..ed93e2e2 100644 --- a/src/device/clock/moment.rs +++ b/src/device/clock/moment.rs @@ -1,5 +1,5 @@ use crate::*; -use ::std::sync::Arc; +use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}}; use ::atomic_float::AtomicF64; /// A point in time in all time scales (microsecond, sample, MIDI pulse) diff --git a/src/device/clock/ticker.rs b/src/device/clock/ticker.rs index 29f49a27..1cb163b0 100644 --- a/src/device/clock/ticker.rs +++ b/src/device/clock/ticker.rs @@ -1,4 +1,6 @@ use crate::*; +use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}}; +use ::atomic_float::AtomicF64; /// Iterator that emits subsequent ticks within a range. /// diff --git a/src/device/clock/timebase.rs b/src/device/clock/timebase.rs index 15e3cb87..fef79202 100644 --- a/src/device/clock/timebase.rs +++ b/src/device/clock/timebase.rs @@ -1,5 +1,5 @@ use crate::*; -use ::std::sync::Arc; +use ::std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::*}}; use ::atomic_float::AtomicF64; /// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat) diff --git a/src/device/dialog.rs b/src/device/dialog.rs index 169acb48..b007ffa2 100644 --- a/src/device/dialog.rs +++ b/src/device/dialog.rs @@ -1,4 +1,4 @@ -use crate::{*, device::*}; +use crate::{*, browse::*, device::*, menu::*}; /// Various possible dialog modes. /// @@ -55,23 +55,17 @@ 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"); + app.mode = app.config.modes.get(":arranger").unwrap(); })))), - 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(); @@ -127,19 +121,3 @@ impl Dialog { /// FIXME: implement pub fn browser_target (&self) -> Option<&BrowseTarget> { todo!() } } - -/// Increment a wrapping counter. -pub const fn wrap_inc (index: usize, count: usize) -> usize { - if count > 0 { (index + 1) % count } else { 0 } -} - -/// Decrement a wrapping counter. -pub const fn wrap_dec (index: usize, count: usize) -> usize { - if count > 0 { - let a = index.overflowing_sub(1).0; - let b = count.saturating_sub(1); - if a < b { a } else { b } - } else { - 0 - } -} diff --git a/src/device/editor.rs b/src/device/editor.rs index bd02db77..0abc07b5 100644 --- a/src/device/editor.rs +++ b/src/device/editor.rs @@ -1,4 +1,3 @@ -#![allow(unused)] use crate::*; /// Contains state for viewing and editing a clip. diff --git a/src/device/meter.rs b/src/device/meter.rs index 52c17949..fc28c19a 100644 --- a/src/device/meter.rs +++ b/src/device/meter.rs @@ -1,4 +1,3 @@ -#![allow(unused)] use crate::*; #[derive(Debug, Default)] pub enum MeteringMode { diff --git a/src/device/sampler.rs b/src/device/sampler.rs index baeae448..f82c0a68 100644 --- a/src/device/sampler.rs +++ b/src/device/sampler.rs @@ -1,5 +1,5 @@ -#![allow(unused)] -use crate::*; +use crate::{*, device::*, browse::*, mix::*}; + pub(crate) use symphonia::{ default::get_codecs, core::{//errors::Error as SymphoniaError, @@ -8,6 +8,11 @@ pub(crate) use symphonia::{ }, }; +mod voice; pub use self::voice::*; +mod sample; pub use self::sample::*; +mod sample_add; pub use self::sample_add::*; +mod sample_kit; pub use self::sample_kit::*; + /// Plays [Voice]s from [Sample]s. /// /// ``` @@ -350,6 +355,10 @@ fn draw_sample ( Ok(label1.len() + label2.len() + 4) } +fn read_sample_data (_: &str) -> Usually<(usize, Vec>)> { + todo!(); +} + def_command!(SamplerCommand: |sampler: Sampler| { RecordToggle { slot: usize } => { let slot = *slot; @@ -396,324 +405,3 @@ def_command!(SamplerCommand: |sampler: Sampler| { //Ok(None) }, }); - -/// A currently playing instance of a sample. -#[derive(Default, Debug, Clone)] pub struct Voice { - pub sample: Arc>, - pub after: usize, - pub position: usize, - pub velocity: f32, -} - -impl Iterator for Voice { - type Item = [f32;2]; - fn next (&mut self) -> Option { - if self.after > 0 { - self.after -= 1; - return Some([0.0, 0.0]) - } - let sample = self.sample.read().unwrap(); - if self.position < sample.end { - let position = self.position; - self.position += 1; - return sample.channels[0].get(position).map(|_amplitude|[ - sample.channels[0][position] * self.velocity * sample.gain, - sample.channels[0][position] * self.velocity * sample.gain, - ]) - } - None - } -} - -/// Collection of samples, one per slot, fixed number of slots. -/// -/// History: Separated to cleanly implement [Default]. -/// -/// ``` -/// let samples = tek::SampleKit([None, None, None, None]); -/// ``` -#[derive(Debug)] pub struct SampleKit ( - pub [Option>>;N] -); - -impl Default for SampleKit { - fn default () -> Self { Self([const { None }; N]) } -} - -impl SampleKit { - pub fn get (&self, index: usize) -> &Option>> { - if index < self.0.len() { - &self.0[index] - } else { - &None - } - } -} - -/// A sound cut. -/// -/// ``` -/// let sample = tek::Sample::default(); -/// let sample = tek::Sample::new("test", 0, 0, vec![]); -/// ``` -#[derive(Default, Debug)] pub struct Sample { - pub name: Arc, - pub start: usize, - pub end: usize, - pub channels: Vec>, - pub rate: Option, - pub gain: f32, - pub color: ItemTheme, -} - -impl Sample { - pub fn new (name: impl AsRef, start: usize, end: usize, channels: Vec>) -> Self { - Self { - name: name.as_ref().into(), - start, - end, - channels, - rate: None, - gain: 1.0, - color: ItemTheme::random(), - } - } - pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { - Voice { - sample: sample.clone(), - after, - position: sample.read().unwrap().start, - velocity: velocity.as_int() as f32 / 127.0, - } - } - pub fn handle_cc (&mut self, controller: u7, value: u7) { - let percentage = value.as_int() as f64 / 127.; - match controller.as_int() { - 20 => { - self.start = (percentage * self.end as f64) as usize; - }, - 21 => { - let length = self.channels[0].len(); - self.end = length.min( - self.start + (percentage * (length as f64 - self.start as f64)) as usize - ); - }, - 22 => { /*attack*/ }, - 23 => { /*decay*/ }, - 24 => { - self.gain = percentage as f32 * 2.0; - }, - 26 => { /* pan */ } - 25 => { /* pitch */ } - _ => {} - } - } - /// Read WAV from file - pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { - let mut channels: Vec> = vec![]; - for channel in wavers::Wav::from_path(src)?.channels() { - channels.push(channel); - } - let mut end = 0; - let mut data: Vec> = vec![]; - for samples in channels.iter() { - let channel = Vec::from(samples.as_ref()); - end = end.max(channel.len()); - data.push(channel); - } - Ok((end, data)) - } - pub fn from_file (path: &PathBuf) -> Usually { - let name = path.file_name().unwrap().to_string_lossy().into(); - let mut sample = Self { name, ..Default::default() }; - // Use file extension if present - let mut hint = Hint::new(); - if let Some(ext) = path.extension() { - hint.with_extension(&ext.to_string_lossy()); - } - let probed = symphonia::default::get_probe().format( - &hint, - MediaSourceStream::new( - Box::new(File::open(path)?), - Default::default(), - ), - &Default::default(), - &Default::default() - )?; - let mut format = probed.format; - let params = &format.tracks().iter() - .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) - .expect("no tracks found") - .codec_params; - let mut decoder = get_codecs().make(params, &Default::default())?; - loop { - match format.next_packet() { - Ok(packet) => sample.decode_packet(&mut decoder, packet)?, - Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(), - Err(err) => return Err(err.into()), - }; - }; - sample.end = sample.channels.iter().fold(0, |l, c|l + c.len()); - Ok(sample) - } - fn decode_packet ( - &mut self, decoder: &mut Box, packet: Packet - ) -> Usually<()> { - // Decode a packet - let decoded = decoder - .decode(&packet) - .map_err(|e|Box::::from(e))?; - // Determine sample rate - let spec = *decoded.spec(); - if let Some(rate) = self.rate { - if rate != spec.rate as usize { - panic!("sample rate changed"); - } - } else { - self.rate = Some(spec.rate as usize); - } - // Determine channel count - while self.channels.len() < spec.channels.count() { - self.channels.push(vec![]); - } - // Load sample - let mut samples = SampleBuffer::new( - decoded.frames() as u64, - spec - ); - if samples.capacity() > 0 { - samples.copy_interleaved_ref(decoded); - for frame in samples.samples().chunks(spec.channels.count()) { - for (chan, frame) in frame.iter().enumerate() { - self.channels[chan].push(*frame) - } - } - } - Ok(()) - } -} - -#[derive(Default, Debug)] pub struct SampleAdd { - pub exited: bool, - pub dir: PathBuf, - pub subdirs: Vec, - pub files: Vec, - pub cursor: usize, - pub offset: usize, - pub sample: Arc>, - pub voices: Arc>>, - pub _search: Option, -} - -impl_draw!(|self: SampleAdd, to: Tui|{ todo!() }); - -impl SampleAdd { - fn exited (&self) -> bool { - self.exited - } - fn exit (&mut self) { - self.exited = true - } - pub fn new ( - sample: &Arc>, - voices: &Arc>> - ) -> Usually { - let dir = std::env::current_dir()?; - let (subdirs, files) = scan(&dir)?; - Ok(Self { - exited: false, - dir, - subdirs, - files, - cursor: 0, - offset: 0, - sample: sample.clone(), - voices: voices.clone(), - _search: None - }) - } - fn rescan (&mut self) -> Usually<()> { - scan(&self.dir).map(|(subdirs, files)|{ - self.subdirs = subdirs; - self.files = files; - }) - } - fn prev (&mut self) { - self.cursor = self.cursor.saturating_sub(1); - } - fn next (&mut self) { - self.cursor = self.cursor + 1; - } - fn try_preview (&mut self) -> Usually<()> { - if let Some(path) = self.cursor_file() { - if let Ok(sample) = Sample::from_file(&path) { - *self.sample.write().unwrap() = sample; - self.voices.write().unwrap().push( - Sample::play(&self.sample, 0, &u7::from(100u8)) - ); - } - //load_sample(&path)?; - //let src = std::fs::File::open(&path)?; - //let mss = MediaSourceStream::new(Box::new(src), Default::default()); - //let mut hint = Hint::new(); - //if let Some(ext) = path.extension() { - //hint.with_extension(&ext.to_string_lossy()); - //} - //let meta_opts: MetadataOptions = Default::default(); - //let fmt_opts: FormatOptions = Default::default(); - //if let Ok(mut probed) = symphonia::default::get_probe() - //.format(&hint, mss, &fmt_opts, &meta_opts) - //{ - //panic!("{:?}", probed.format.metadata()); - //}; - } - Ok(()) - } - fn cursor_dir (&self) -> Option { - if self.cursor < self.subdirs.len() { - Some(self.dir.join(&self.subdirs[self.cursor])) - } else { - None - } - } - fn cursor_file (&self) -> Option { - if self.cursor < self.subdirs.len() { - return None - } - let index = self.cursor.saturating_sub(self.subdirs.len()); - if index < self.files.len() { - Some(self.dir.join(&self.files[index])) - } else { - None - } - } - fn pick (&mut self) -> Usually { - if self.cursor == 0 { - if let Some(parent) = self.dir.parent() { - self.dir = parent.into(); - self.rescan()?; - self.cursor = 0; - return Ok(false) - } - } - if let Some(dir) = self.cursor_dir() { - self.dir = dir; - self.rescan()?; - self.cursor = 0; - return Ok(false) - } - if let Some(path) = self.cursor_file() { - let (end, channels) = read_sample_data(&path.to_string_lossy())?; - let mut sample = self.sample.write().unwrap(); - sample.name = path.file_name().unwrap().to_string_lossy().into(); - sample.end = end; - sample.channels = channels; - return Ok(true) - } - return Ok(false) - } -} - -fn read_sample_data (_: &str) -> Usually<(usize, Vec>)> { - todo!(); -} diff --git a/src/device/sampler/sample.rs b/src/device/sampler/sample.rs new file mode 100644 index 00000000..06caf01f --- /dev/null +++ b/src/device/sampler/sample.rs @@ -0,0 +1,144 @@ +use crate::*; + +/// A sound cut. +/// +/// ``` +/// let sample = tek::Sample::default(); +/// let sample = tek::Sample::new("test", 0, 0, vec![]); +/// ``` +#[derive(Default, Debug)] pub struct Sample { + pub name: Arc, + pub start: usize, + pub end: usize, + pub channels: Vec>, + pub rate: Option, + pub gain: f32, + pub color: ItemTheme, +} + +impl Sample { + pub fn new (name: impl AsRef, start: usize, end: usize, channels: Vec>) -> Self { + Self { + name: name.as_ref().into(), + start, + end, + channels, + rate: None, + gain: 1.0, + color: ItemTheme::random(), + } + } + pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { + Voice { + sample: sample.clone(), + after, + position: sample.read().unwrap().start, + velocity: velocity.as_int() as f32 / 127.0, + } + } + pub fn handle_cc (&mut self, controller: u7, value: u7) { + let percentage = value.as_int() as f64 / 127.; + match controller.as_int() { + 20 => { + self.start = (percentage * self.end as f64) as usize; + }, + 21 => { + let length = self.channels[0].len(); + self.end = length.min( + self.start + (percentage * (length as f64 - self.start as f64)) as usize + ); + }, + 22 => { /*attack*/ }, + 23 => { /*decay*/ }, + 24 => { + self.gain = percentage as f32 * 2.0; + }, + 26 => { /* pan */ } + 25 => { /* pitch */ } + _ => {} + } + } + /// Read WAV from file + pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { + let mut channels: Vec> = vec![]; + for channel in wavers::Wav::from_path(src)?.channels() { + channels.push(channel); + } + let mut end = 0; + let mut data: Vec> = vec![]; + for samples in channels.iter() { + let channel = Vec::from(samples.as_ref()); + end = end.max(channel.len()); + data.push(channel); + } + Ok((end, data)) + } + pub fn from_file (path: &PathBuf) -> Usually { + let name = path.file_name().unwrap().to_string_lossy().into(); + let mut sample = Self { name, ..Default::default() }; + // Use file extension if present + let mut hint = Hint::new(); + if let Some(ext) = path.extension() { + hint.with_extension(&ext.to_string_lossy()); + } + let probed = symphonia::default::get_probe().format( + &hint, + MediaSourceStream::new( + Box::new(File::open(path)?), + Default::default(), + ), + &Default::default(), + &Default::default() + )?; + let mut format = probed.format; + let params = &format.tracks().iter() + .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) + .expect("no tracks found") + .codec_params; + let mut decoder = get_codecs().make(params, &Default::default())?; + loop { + match format.next_packet() { + Ok(packet) => sample.decode_packet(&mut decoder, packet)?, + Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(), + Err(err) => return Err(err.into()), + }; + }; + sample.end = sample.channels.iter().fold(0, |l, c|l + c.len()); + Ok(sample) + } + fn decode_packet ( + &mut self, decoder: &mut Box, packet: Packet + ) -> Usually<()> { + // Decode a packet + let decoded = decoder + .decode(&packet) + .map_err(|e|Box::::from(e))?; + // Determine sample rate + let spec = *decoded.spec(); + if let Some(rate) = self.rate { + if rate != spec.rate as usize { + panic!("sample rate changed"); + } + } else { + self.rate = Some(spec.rate as usize); + } + // Determine channel count + while self.channels.len() < spec.channels.count() { + self.channels.push(vec![]); + } + // Load sample + let mut samples = SampleBuffer::new( + decoded.frames() as u64, + spec + ); + if samples.capacity() > 0 { + samples.copy_interleaved_ref(decoded); + for frame in samples.samples().chunks(spec.channels.count()) { + for (chan, frame) in frame.iter().enumerate() { + self.channels[chan].push(*frame) + } + } + } + Ok(()) + } +} diff --git a/src/device/sampler/sample_add.rs b/src/device/sampler/sample_add.rs new file mode 100644 index 00000000..4fa88b67 --- /dev/null +++ b/src/device/sampler/sample_add.rs @@ -0,0 +1,122 @@ +use crate::{*, device::sampler::*}; + +#[derive(Default, Debug)] pub struct SampleAdd { + pub exited: bool, + pub dir: PathBuf, + pub subdirs: Vec, + pub files: Vec, + pub cursor: usize, + pub offset: usize, + pub sample: Arc>, + pub voices: Arc>>, + pub _search: Option, +} + +impl_draw!(|self: SampleAdd, to: Tui|{ todo!() }); + +impl SampleAdd { + fn exited (&self) -> bool { + self.exited + } + fn exit (&mut self) { + self.exited = true + } + pub fn new ( + sample: &Arc>, + voices: &Arc>> + ) -> Usually { + let dir = std::env::current_dir()?; + let (subdirs, files) = scan(&dir)?; + Ok(Self { + exited: false, + dir, + subdirs, + files, + cursor: 0, + offset: 0, + sample: sample.clone(), + voices: voices.clone(), + _search: None + }) + } + fn rescan (&mut self) -> Usually<()> { + scan(&self.dir).map(|(subdirs, files)|{ + self.subdirs = subdirs; + self.files = files; + }) + } + fn prev (&mut self) { + self.cursor = self.cursor.saturating_sub(1); + } + fn next (&mut self) { + self.cursor = self.cursor + 1; + } + fn try_preview (&mut self) -> Usually<()> { + if let Some(path) = self.cursor_file() { + if let Ok(sample) = Sample::from_file(&path) { + *self.sample.write().unwrap() = sample; + self.voices.write().unwrap().push( + Sample::play(&self.sample, 0, &u7::from(100u8)) + ); + } + //load_sample(&path)?; + //let src = std::fs::File::open(&path)?; + //let mss = MediaSourceStream::new(Box::new(src), Default::default()); + //let mut hint = Hint::new(); + //if let Some(ext) = path.extension() { + //hint.with_extension(&ext.to_string_lossy()); + //} + //let meta_opts: MetadataOptions = Default::default(); + //let fmt_opts: FormatOptions = Default::default(); + //if let Ok(mut probed) = symphonia::default::get_probe() + //.format(&hint, mss, &fmt_opts, &meta_opts) + //{ + //panic!("{:?}", probed.format.metadata()); + //}; + } + Ok(()) + } + fn cursor_dir (&self) -> Option { + if self.cursor < self.subdirs.len() { + Some(self.dir.join(&self.subdirs[self.cursor])) + } else { + None + } + } + fn cursor_file (&self) -> Option { + if self.cursor < self.subdirs.len() { + return None + } + let index = self.cursor.saturating_sub(self.subdirs.len()); + if index < self.files.len() { + Some(self.dir.join(&self.files[index])) + } else { + None + } + } + fn pick (&mut self) -> Usually { + if self.cursor == 0 { + if let Some(parent) = self.dir.parent() { + self.dir = parent.into(); + self.rescan()?; + self.cursor = 0; + return Ok(false) + } + } + if let Some(dir) = self.cursor_dir() { + self.dir = dir; + self.rescan()?; + self.cursor = 0; + return Ok(false) + } + if let Some(path) = self.cursor_file() { + let (end, channels) = read_sample_data(&path.to_string_lossy())?; + let mut sample = self.sample.write().unwrap(); + sample.name = path.file_name().unwrap().to_string_lossy().into(); + sample.end = end; + sample.channels = channels; + return Ok(true) + } + return Ok(false) + } +} diff --git a/src/device/sampler/sample_kit.rs b/src/device/sampler/sample_kit.rs new file mode 100644 index 00000000..382ce678 --- /dev/null +++ b/src/device/sampler/sample_kit.rs @@ -0,0 +1,26 @@ +use crate::*; + +/// Collection of samples, one per slot, fixed number of slots. +/// +/// History: Separated to cleanly implement [Default]. +/// +/// ``` +/// let samples = tek::SampleKit([None, None, None, None]); +/// ``` +#[derive(Debug)] pub struct SampleKit ( + pub [Option>>;N] +); + +impl Default for SampleKit { + fn default () -> Self { Self([const { None }; N]) } +} + +impl SampleKit { + pub fn get (&self, index: usize) -> &Option>> { + if index < self.0.len() { + &self.0[index] + } else { + &None + } + } +} diff --git a/src/device/sampler/voice.rs b/src/device/sampler/voice.rs new file mode 100644 index 00000000..632e5202 --- /dev/null +++ b/src/device/sampler/voice.rs @@ -0,0 +1,29 @@ +use crate::*; + +/// A currently playing instance of a sample. +#[derive(Default, Debug, Clone)] pub struct Voice { + pub sample: Arc>, + pub after: usize, + pub position: usize, + pub velocity: f32, +} + +impl Iterator for Voice { + type Item = [f32;2]; + fn next (&mut self) -> Option { + if self.after > 0 { + self.after -= 1; + return Some([0.0, 0.0]) + } + let sample = self.sample.read().unwrap(); + if self.position < sample.end { + let position = self.position; + self.position += 1; + return sample.channels[0].get(position).map(|_amplitude|[ + sample.channels[0][position] * self.velocity * sample.gain, + sample.channels[0][position] * self.velocity * sample.gain, + ]) + } + None + } +} diff --git a/src/device/sequence.rs b/src/device/sequence.rs index 573ea4c3..8088a404 100644 --- a/src/device/sequence.rs +++ b/src/device/sequence.rs @@ -1,4 +1,5 @@ -use crate::*; +use crate::{*, clock::*, device::*}; + impl +AsMut> HasSequencer for T {} @@ -41,7 +42,7 @@ impl +AsMut> 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, ::tengri::midly::MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }); +/// clip.record_event(12, 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); /// diff --git a/src/tek.edn b/src/tek.edn index 4dda6306..d14123c0 100644 --- a/src/tek.edn +++ b/src/tek.edn @@ -2,22 +2,17 @@ (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) +(mode :transport (name Transport) (info JACK transport controller.) (keys :clock :global) :transport) -(mode :menu (name Menu) (info Mode selector.) (keys :axis/y :confirm) - (view (bg (g 64) - (bsp/s (max/xy 80 2 :transport) +(mode :menu (name Menu) (info Mode selector.) (keys :axis/y :confirm) + (view (bg (g 0) + (bsp/s (max/y 2 :transport (bsp/s (max/y 3 (bg (g 80) :ports/out)) (bsp/n (max/y 3 (bg (g 80) :ports/in)) - (bg (g 30) (bsp/s (max/y 6 :logo) :dialog/menu)))))))) + (bg (g 30) (bsp/s (max/h 6 (bg (g 70) :logo) :dialog/menu)))))))))) (mode :sequencer (name Sequencer) (info MIDI sequencer.) (keys :editor :clock :global) @@ -34,13 +29,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) @@ -89,8 +84,7 @@ (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) @@ -121,35 +115,35 @@ (@up select :select/scene/dec) (@down select :select/scene/inc)) (keys :scene (see :color :launch :axis/z :axis/z2 :delete)) -(keys :help (@f1 dialog :help)) -(keys :page (@pgup page/up) - (@pgdn page/down)) -(keys :delete (@delete delete) +(keys :help (@f1 dialog :help)) +(keys :page (@pgup page/up) + (@pgdn page/down)) +(keys :delete (@delete delete) (@backspace delete/back)) (keys :input (see :axis/x :delete) (:char input)) (keys :list (see :axis/y :confirm)) (keys :length (see :axis/x :axis/y :confirm)) (keys :browse (see :list :input :focus)) -(keys :history (@u undo 1) - (@r redo 1)) -(keys :saveload (@f6 dialog :save) - (@f9 dialog :load)) -(keys :color (@c color)) -(keys :launch (@q launch)) -(keys :clock (@space clock/toggle 0) +(keys :history (@u undo 1) + (@r redo 1)) +(keys :saveload (@f6 dialog :save) + (@f9 dialog :load)) +(keys :color (@c color)) +(keys :launch (@q launch)) +(keys :clock (@space clock/toggle 0) (@shift/space clock/toggle 0)) (keys :global (see :history :saveload) - (@f8 dialog :options) - (@f10 dialog :quit)) + (@f8 dialog :options) + (@f10 dialog :quit)) (keys :clip (see :color :launch :axis/z :axis/z2 :delete) - (@l toggle :loop)) + (@l toggle :loop)) (keys :sequencer (see :color :launch) - (@shift/I input/add) - (@shift/O output/add)) + (@shift/I input/add) + (@shift/O output/add)) (keys :pool (see :axis-y :axis-w :axis/z2 :color :delete) - (@n rename/begin) - (@t length/begin) - (@m import/begin) - (@x export/begin) - (@shift/A clip/add :after :new/clip) - (@shift/D clip/add :after :cloned/clip)) + (@n rename/begin) + (@t length/begin) + (@m import/begin) + (@x export/begin) + (@shift/A clip/add :after :new/clip) + (@shift/D clip/add :after :cloned/clip)) diff --git a/src/tek.rs b/src/tek.rs index 4d918519..1f291ac9 100644 --- a/src/tek.rs +++ b/src/tek.rs @@ -1,143 +1,80 @@ #![allow(clippy::unit_arg)] -#![feature(impl_trait_in_assoc_type)] // Used by EntriesIterator; TODO remove +#![feature( + adt_const_params, + anonymous_lifetime_in_impl_trait, + impl_trait_in_assoc_type, + trait_alias, + type_changing_struct_update +)] 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. -#[allow(unused)] fn main () -> Usually<()> { +#[cfg(feature = "cli")] pub fn main () -> Usually<()> { tengri::Tui::setup_panic(); - #[cfg(feature = "cli")] { - Config::watched(crate::cli::run_with_config).map(|_|()) - } - #[cfg(not(feature = "cli"))] { - Config::watched(run_new_plain).map(|_|()) - } -} - -#[cfg(not(feature = "cli"))] -fn run_new_plain (config: Config) -> Usually<()> { - let name = "tek"; - tengri::Tui::run_main(Jack::new_run(name, move|jack|{ - let mode = ":menu"; - let title = "untitled!"; - let bpm = 74.; - let clock = Clock::new(&jack, Some(bpm))?; - let tracks = []; - let scenes = []; - Ok(App::new(Arrangement::new( - &jack, - title.into(), - clock, + let name = "tek"; + 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, tracks.into_iter(), scenes.into_iter(), - connect_midi_ins(&jack, &"M", &[], None)?.into_iter(), - connect_midi_outs(&jack, &"M", &[], None)?.into_iter(), - [].into_iter().chain(connect_audio_ins(&jack, &"L", &[], None)?.into_iter()) - .chain(connect_audio_ins(&jack, &"R", &[], None)?.into_iter()), - [].into_iter().chain(connect_audio_outs(&jack, &"L", &[], None)?.into_iter()) - .chain(connect_audio_outs(&jack, &"R", &[], None)?.into_iter()), - ), config, mode)) - })?) + 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(|_|()) } - +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#" ~ █▀█▀█ █▀▀█ █ █ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ - █ █▀ █▀▀▄ ~ heatwave is the new darkwave ~ + █ █▀ █▀▀▄ ~ v0.4.0, 2026 heatwave edition ~ ~ ▀ █▀▀█ ▀ ▀ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#; +#[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) -> 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::Cli = Default::default(); + /// let cli: tek::Cli = Default::default(); /// /// use clap::CommandFactory; - /// tek::cli::Cli::command().debug_assert(); + /// tek::Cli::command().debug_assert(); /// ``` - #[derive(Parser, Debug, Default)] + #[derive(Parser)] #[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))] - pub struct Cli { + #[derive(Debug, Default)] 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 => config.print(), - 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::cli::Action = Default::default(); + /// let action: tek::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. @@ -149,7 +86,38 @@ pub(crate) const HEADER: &'static str = r#" /// Continue work in a copy of the current session. Fork, /// Create a new empty session. - New(ProjectInit), + New { + /// Name of JACK client + #[arg(short='n', long)] name: Option, + /// Whether to attempt to become transport master + #[arg(short='Y', long, default_value_t = false)] sync_lead: bool, + /// Whether to sync to external transport master + #[arg(short='y', long, default_value_t = true)] sync_follow: bool, + /// Initial tempo in beats per minute + #[arg(short='b', long, default_value = None)] bpm: Option, + /// Whether to include a transport toolbar (default: true) + #[arg(short='c', long, default_value_t = true)] show_clock: bool, + /// MIDI outs to connect to (multiple instances accepted) + #[arg(short='I', long)] midi_from: Vec, + /// MIDI outs to connect to (multiple instances accepted) + #[arg(short='i', long)] midi_from_re: Vec, + /// MIDI ins to connect to (multiple instances accepted) + #[arg(short='O', long)] midi_to: Vec, + /// MIDI ins to connect to (multiple instances accepted) + #[arg(short='o', long)] midi_to_re: Vec, + /// Audio outs to connect to left input + #[arg(short='l', long)] left_from: Vec, + /// Audio outs to connect to right input + #[arg(short='r', long)] right_from: Vec, + /// Audio ins to connect from left output + #[arg(short='L', long)] left_to: Vec, + /// Audio ins to connect from right output + #[arg(short='R', long)] right_to: Vec, + /// Tracks to creat + #[arg(short='t', long)] tracks: Option, + /// Scenes to create + #[arg(short='s', long)] scenes: Option, + }, /// Import media as new session. Import, /// Show configuration. @@ -157,860 +125,75 @@ pub(crate) const HEADER: &'static str = r#" /// Show version. Version, } - #[derive(Debug, Clone, Parser, Default)] - pub struct ProjectInit { - /// Name of JACK client - #[arg(short='n', long)] name: Option, - /// Whether to attempt to become transport master - #[arg(short='Y', long, default_value_t = false)] sync_lead: bool, - /// Whether to sync to external transport master - #[arg(short='y', long, default_value_t = true)] sync_follow: bool, - /// Initial tempo in beats per minute - #[arg(short='b', long, default_value = None)] bpm: Option, - /// Whether to include a transport toolbar (default: true) - #[arg(short='c', long, default_value_t = true)] show_clock: bool, - /// MIDI outs to connect to (multiple instances accepted) - #[arg(short='I', long)] midi_from: Vec, - /// MIDI outs to connect to (multiple instances accepted) - #[arg(short='i', long)] midi_from_re: Vec, - /// MIDI ins to connect to (multiple instances accepted) - #[arg(short='O', long)] midi_to: Vec, - /// MIDI ins to connect to (multiple instances accepted) - #[arg(short='o', long)] midi_to_re: Vec, - /// Audio outs to connect to left input - #[arg(short='l', long)] left_from: Vec, - /// Audio outs to connect to right input - #[arg(short='r', long)] right_from: Vec, - /// Audio ins to connect from left output - #[arg(short='L', long)] left_to: Vec, - /// Audio ins to connect from right output - #[arg(short='R', long)] right_to: Vec, - /// Tracks to creat - #[arg(short='t', long)] tracks: Option, - /// Scenes to create - #[arg(short='s', long)] scenes: Option, - } - impl ProjectInit { - pub fn init (&self) -> Usually { - let Self { - name, bpm, tracks, scenes, - 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, *bpm)?, - [].into_iter(), - [].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".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".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, <, &rf, &rt, &mf, &mt, &mfr, &mtr)?; - proj.tracks_add(tracks.unwrap_or(0), None, &[], &[])?; - proj.scenes_add(scenes.unwrap_or(0))?; - //if matches!(self, Action::Status) { - //// Show status and exit - //tek_print_status(&proj); - //return Ok(()) - //} - Ok(proj) + + /// Command-line configuration. + #[cfg(feature = "cli")] + impl Cli { + pub fn run (&self) -> Usually<()> { + self.action.run(Config::init_new(None)?) } } -} -pub use self::app::*; -mod app { - use crate::*; - primitive!(u8: try_to_u8); - primitive!(u16: try_to_u16); - primitive!(usize: try_to_usize); - primitive!(isize: try_to_isize); - impl_has!(Clock: |self: App|self.project.clock); - impl_has!(Vec: |self: App|self.project.midi_ins); - impl_has!(Vec: |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: |self: App|self.project.as_ref()); - impl_as_mut!(Vec: |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 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 App::get_opt_u7)] - #[namespace(Option App::get_opt_u16)] - #[namespace(Option App::get_opt_usize)] - #[namespace(Option>> 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>>>, - /// Undo history - pub history: Vec<(AppCommand, Option)>, - /// 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>>> - } - 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) -> 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(()) + #[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, + 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, .. + } => { + 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)?, + [].into_iter(), + [].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()), + ); + //&jack, Clock::new(&jack, *bpm)?, &lf, <, &rf, &rt, &mf, &mt, &mfr, &mtr)?; + proj.tracks_add(tracks.unwrap_or(0), None, &[], &[])?; + proj.scenes_add(scenes.unwrap_or(0))?; + //if matches!(self, Action::Status) { + //// Show status and exit + //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() + ))?; }, - 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) { - //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> { - Ok(src.src()?.map(|x|x.into())) - } - fn get_u16 (&self, src: impl Language) -> Perhaps { - 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 { - 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 { - 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 { - 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 { - if let Some(expr) = src.expr()? { - match (expr.head()?, expr.tail()?) { - (Some("g"), Some(tail)) => { - let n = try_to_u8(tail.head().map_err(Into::into))?.ok_or(LanguageError::Domain("not gray"))?; - Ok(Some(Color::Rgb(n, n, n))) - }, - (Some("rgb"), Some(tail)) => { - let r = try_to_u8(tail.head().map_err(Into::into))? - .ok_or(LanguageError::Domain("not red"))?; - let g = try_to_u8(tail.tail().head().map_err(Into::into))? - .ok_or(LanguageError::Domain("not green"))?; - let b = try_to_u8(tail.tail().tail().head().map_err(Into::into))? - .ok_or(LanguageError::Domain("not blue"))?; - Ok(Some(Color::Rgb(r, g, b))) - }, - (Some(_), _) => return Err(format!("not a color expression: {expr}").into()), - (None, _) => return Err(format!("not a color expression: {expr}").into()), - } - } else if let Ok(Some(sym)) = src.word() { - Ok(match sym { - ":color/bg" => Some(Color::Rgb(28, 32, 36)), - ":color/fg" => Some(Color::Rgb(98, 92, 96)), - _ => return Err(format!("not a color: {sym}").into()) - }) - } else { - return Err(format!("not a color: {:?}", src.src()?).into()) - } - } - fn get_opt_u7 (&self, src: impl Language) -> Perhaps> { - 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> { - Ok(None) - } - fn get_opt_usize (&self, src: impl Language) -> Perhaps> { - src.word()?.map(|word|Ok(match word { - ":selected/scene" => self.selection().scene(), - ":selected/track" => self.selection().track(), - _ => return Err(format!("unknown opt: {word}").into()) - })).transpose() - } - fn get_clip (&self, src: impl Language) -> Perhaps>>> { - 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 { - 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 { - 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 { - Ok(match &self.dialog { - Dialog::Menu(index, items) => { - let callback = items.0[*index].1.clone(); - callback(self)?; - None - }, - _ => todo!(), - }) - } - } - - pub fn swap_value ( - target: &mut T, value: &T, returned: impl Fn(T)->U - ) -> Perhaps { - 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 ( - target: &mut bool, value: &Option, returned: impl Fn(Option)->U - ) -> Perhaps { - 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, Vec)> { - 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)) - } - - tui_keys!(self: App, input { - let commands = tek_commands_collect(self, input)?; - let results = tek_commands_execute(self, commands)?; - self.history.extend(results.into_iter()); - Ok(()) - }); - - fn tek_commands_collect (app: &App, input: &TuiEvent) - -> Usually> - { - let mut commands = vec![]; - 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 { - for command in binding.commands.iter() { - if let Some(command) = app.namespace(command)? as Option { - commands.push(command) - } - } - } - } - } - } - Ok(commands) - } - - fn tek_commands_execute (app: &mut App, commands: Vec) - -> Usually)>> - { - let mut history = vec![]; - for command in commands.into_iter() { - let result = command.act(app); - match result { Err(err) => { history.push((command, None)); return Err(err) } - Ok(undo) => { history.push((command, undo)); } }; - } - Ok(history) - } - - pub(crate) fn load_bind (binds: &Binds, name: &impl AsRef, body: &impl Language) -> Usually<()> { - binds.write().unwrap().insert(name.as_ref().into(), Bind::load(body)?); - Ok(()) - } - - impl Bind> { - pub fn load (lang: &impl Language) -> Usually { - let mut map = Self::new(); - lang.each(|item|if item.expr().head() == Ok(Some("see")) { - // TODO - Ok(()) - } else if let Ok(Some(_word)) = item.expr().head().word() { - if let Some(event) = TuiKey::from_dsl(item.expr()?.head()?)?.to_crossterm() { - map.add(TuiEvent(event), Binding { - commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(), - condition: None, - description: None, - source: None - }); - Ok(()) - } else if Some(":char") == item.expr()?.head()? { - // TODO - return Ok(()) - } else { - return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into()) - } - } else { - return Err(format!("Config::load_bind: unexpected: {item:?}").into()) - })?; - Ok(map) - } - } - - /// Default is always empty map regardless if `E` and `C` implement [Default]. - impl Default for Bind { - fn default () -> Self { Self(Default::default()) } - } - - impl Default for Binding { - fn default () -> Self { - Self { - commands: Default::default(), - condition: Default::default(), - description: Default::default(), - source: Default::default(), - } - } - } - - impl Bind { - /// Create a new event map - pub fn new () -> Self { - Default::default() - } - /// Add a binding to an owned event map. - pub fn def (mut self, event: E, binding: Binding) -> Self { - self.add(event, binding); - self - } - /// Add a binding to an event map. - pub fn add (&mut self, event: E, binding: Binding) -> &mut Self { - if !self.0.contains_key(&event) { - self.0.insert(event.clone(), Default::default()); - } - self.0.get_mut(&event).unwrap().push(binding); - self - } - /// Return the binding(s) that correspond to an event. - pub fn query (&self, event: &E) -> Option<&[Binding]> { - self.0.get(event).map(|x|x.as_slice()) - } - /// Return the first binding that corresponds to an event, considering conditions. - pub fn dispatch (&self, event: &E) -> Option<&Binding> { - self.query(event) - .map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next()) - .flatten() - } - } - - impl_debug!(Condition |self, w| { write!(w, "*") }); - - impl_default!(AppCommand: Self::Nop); - - def_command!(AppCommand: |app: App| { - Nop => Ok(None), - Cancel => todo!(), // TODO delegate: - Confirm => app.confirm(), - Inc { axis: ControlAxis } => app.inc(axis), - Dec { axis: ControlAxis } => app.dec(axis), - SetDialog { dialog: Dialog } => { - swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }) - }, - }); - - impl<'a> Namespace<'a, AppCommand> for App { - symbols!('a |app| -> AppCommand { - "x/inc" => AppCommand::Inc { axis: ControlAxis::X }, - "x/dec" => AppCommand::Dec { axis: ControlAxis::X }, - "y/inc" => AppCommand::Inc { axis: ControlAxis::Y }, - "y/dec" => AppCommand::Dec { axis: ControlAxis::Y }, - "confirm" => AppCommand::Confirm, - "cancel" => AppCommand::Cancel, - }); - } - - /// A control axis. - /// - /// ``` - /// let axis = tek::ControlAxis::X; - /// ``` - #[derive(Debug, Copy, Clone)] pub enum ControlAxis { - X, Y, Z, I - } - - //take!(DeviceCommand|state: Arrangement, iter|state.selected_device().as_ref() - //.map(|t|Take::take(t, iter)).transpose().map(|x|x.flatten())); - - impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } } - - impl_audio!(App: tek_jack_process, tek_jack_event); - - fn tek_jack_process (state: &mut App, client: &Client, scope: &ProcessScope) -> Control { - let t0 = state.perf.get_t0(); - state.clock().update_from_scope(scope).unwrap(); - let midi_in = state.project.midi_input_collect(scope); - if let Some(editor) = &state.editor() { - let mut pitch: Option = None; - for port in midi_in.iter() { - for event in port.iter() { - if let (_, Ok(LiveEvent::Midi {message: MidiMessage::NoteOn {key, ..}, ..})) - = event - { - pitch = Some(key.clone()); - } - } - } - if let Some(pitch) = pitch { - editor.set_note_pos(pitch.as_int() as usize); - } - } - let result = state.project.process_tracks(client, scope); - state.perf.update_from_jack_scope(t0, scope); - result - } - - fn tek_jack_event (state: &mut App, event: JackEvent) { - use JackEvent::*; - match event { - SampleRate(sr) => { state.clock().timebase.sr.set(sr as f64); }, - PortRegistration(_id, true) => { - //let port = self.jack().port_by_id(id); - //println!("\rport add: {id} {port:?}"); - //println!("\rport add: {id}"); - }, - PortRegistration(_id, false) => { - /*println!("\rport del: {id}")*/ - }, - PortsConnected(_a, _b, true) => { /*println!("\rport conn: {a} {b}")*/ }, - PortsConnected(_a, _b, false) => { /*println!("\rport disc: {a} {b}")*/ }, - ClientRegistration(_id, true) => {}, - ClientRegistration(_id, false) => {}, - ThreadInit => {}, - XRun => {}, - GraphReorder => {}, - _ => { panic!("{event:?}"); } - } - } -} - -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!() - } - } - 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!() } + Ok(()) } } - /// 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> + AsMut>> HasDevices for T { - fn devices (&self) -> &Vec { - self.as_ref() - } - fn devices_mut (&mut self) -> &mut Vec { - self.as_mut() - } - } - - pub trait HasDevices: AsRef> + AsMut> { - fn devices (&self) -> &Vec { - self.as_ref() - } - fn devices_mut (&mut self) -> &mut Vec { - self.as_mut() - } - } - - 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 } => todo!(), - }); - - def_command!(AudioOutputCommand: |port: AudioOutput| { - Close => todo!(), - Connect { audio_in: Arc } => todo!(), - }); - - def_command!(MidiInputCommand: |port: MidiInput| { - Close => todo!(), - Connect { midi_out: Arc } => todo!(), - }); - - def_command!(MidiOutputCommand: |port: MidiOutput| { - Close => todo!(), - Connect { midi_in: Arc } => todo!(), - }); - - pub struct Junction(T); - - impl View for Junction { - fn view (&self) -> impl Draw { - T::KIND - } - } -} - -pub fn print_status (project: &Arrangement) { - println!("Name: {:?}", &project.name); - println!("JACK: {:?}", &project.jack); - println!("Buffer: {:?}", &project.clock.chunk); - println!("Sample rate: {:?}", &project.clock.timebase.sr); - println!("MIDI PPQ: {:?}", &project.clock.timebase.ppq); - println!("Tempo: {:?}", &project.clock.timebase.bpm); - println!("Quantize: {:?}", &project.clock.quant); - println!("Launch: {:?}", &project.clock.sync); - println!("Playhead: {:?}us", &project.clock.playhead.usec); - println!("Playhead: {:?}s", &project.clock.playhead.sample); - println!("Playhead: {:?}p", &project.clock.playhead.pulse); - println!("Started: {:?}", &project.clock.started); - println!("Tracks:"); - for (i, t) in project.tracks.iter().enumerate() { - println!(" Track {i}: {} {} {:?} {:?}", t.name, t.width, - &t.sequencer.play_clip, &t.sequencer.next_clip); - } - println!("Scenes:"); - for (i, t) in project.scenes.iter().enumerate() { - println!(" Scene {i}: {} {:?}", &t.name, &t.clips); - } - println!("MIDI Ins: {:?}", &project.midi_ins); - println!("MIDI Outs: {:?}", &project.midi_outs); - println!("Audio Ins: {:?}", &project.audio_ins); - 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>, @@ -1033,1165 +216,85 @@ pub fn show_version () { //})?)? //} -pub use self::config::*; -mod config { - use crate::*; - use std::path::PathBuf; - use notify_debouncer_mini::{ - new_debouncer, Debouncer, DebounceEventResult, - notify::{RecursiveMode, RecommendedWatcher} - }; - - /// Configuration: mode, view, and bind definitions. - /// - /// ``` - /// let config = tek::Config::default(); - /// ``` - /// - /// ``` - /// // Some dizzle. - /// // What indentation to use here lol? - /// let source = stringify!((mode :menu (name Menu) - /// (info Mode selector.) (keys :axis/y :confirm) - /// (view (bg (g 0) (bsp/s :ports/out - /// (bsp/n :ports/in - /// (bg (g 30) (bsp/s (fixed/y 7 :logo) - /// (fill :dialog/menu))))))))); - /// // Add this definition to the config and try to load it. - /// // A "mode" is basically a state machine - /// // with associated input and output definitions. - /// tek::Config::default().add(&source).unwrap().get_mode(":menu").unwrap(); - /// ``` - #[derive(Default, Debug)] - pub struct Config { - /// XDG base directories of running user. - pub dirs: BaseDirectories, - /// Active collection of interaction modes. - pub modes: Modes, - /// Active collection of event bindings. - pub binds: Binds, - /// Active collection of view definitions. - pub views: Views, + pub fn show_version () { + println!("todo version"); } - /// Collection of custom view definitions. - pub type Views = Arc, Arc>>>; - - /// Collection of input bindings. - pub type Binds = Arc, Bind>>>>; - - /// An map of input events (e.g. [TuiEvent]) to [Binding]s. - /// - /// ``` - /// let lang = "(@x (nop)) (@y (nop) (nop))"; - /// let bind = tek::Bind::>::load(&lang).unwrap(); - /// assert_eq!(bind.query(&'x'.into()).map(|x|x.len()), Some(1)); - /// //assert_eq!(bind.query(&'y'.into()).map(|x|x.len()), Some(2)); - /// ``` - #[derive(Debug)] - pub struct Bind( - /// Map of each event (e.g. key combination) to - /// all command expressions bound to it by - /// all loaded input layers. - pub BTreeMap>> - ); - - /// A sequence of zero or more commands (e.g. [AppCommand]), - /// optionally filtered by [Condition] to form layers. - /// - /// ``` - /// //FIXME: Why does it overflow? - /// //let binding: Binding<()> = tek::Binding { ..Default::default() }; - /// ``` - #[derive(Debug, Clone)] pub struct Binding { - pub commands: Arc<[C]>, - pub condition: Option, - pub description: Option>, - pub source: Option>, - } - - /// Condition that must evaluate to true in order to enable an input layer. - /// - /// ``` - /// let condition = tek::Condition(std::sync::Arc::new(Box::new(||{true}))); - /// ``` - #[derive(Clone)] - pub struct Condition( - pub Arcbool + Send + Sync>> - ); - - /// Collection of UI modes. - /// - /// ``` - /// let mut modes = tek::Modes::default(); - /// assert_eq!(modes.len(), 0); - /// let _ = modes.add(&":foo", &"(mode (name Foo) (info Bar))"); - /// assert_eq!(modes.len(), 1); - /// ``` - #[derive(Default, Debug, Clone)] - pub struct Modes(Arc, Arc>>>>>); - - /// Group of view and keys definitions. - /// - /// ``` - /// let mode = tek::Mode::>::default(); - /// ``` - #[derive(Default, Debug)] - pub struct Mode { - pub path: PathBuf, - pub name: Vec, - pub info: Vec, - pub view: Vec, - pub keys: Vec, - pub modes: Modes, - } - - impl Config { - const CONFIG_DIR: &'static str = "tek"; - const CONFIG_SUB: &'static str = "v0"; - const CONFIG: &'static str = "tek.edn"; - const DEFAULTS: &'static str = include_str!("tek.edn"); - - pub fn watched (callback: impl FnOnce(Self)->T) -> Usually { - let config = Self::init_new(None)?; - let _watch = config.watch(None)?; - let result = callback(config); - Ok(result) + 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}"))); } - - pub fn init_new (_dirs: Option) -> Usually { - let mut config = Self::new(None); - config.init()?; - Ok(config) - } - - /// Create a new app configuration from a set of XDG base directories, - pub fn new (dirs: Option) -> Self { - let default = ||BaseDirectories::with_profile(Self::CONFIG_DIR, Self::CONFIG_SUB); - let dirs = dirs.unwrap_or_else(default); - Self { dirs, ..Default::default() } - } - - pub fn watch (&self, bounce: Option) -> Usually> { - let mut debouncer = new_debouncer( - bounce.unwrap_or(Duration::from_millis(500)), - |events: DebounceEventResult| { - panic!("updated: {events:?}"); - } - )?; - if let Some(path) = self.get_file() { - debouncer.watcher().watch(&path, RecursiveMode::NonRecursive)?; - Ok(debouncer) - } else { - Err(format!("no config path").into()) - } - } - - fn find_file (&self) -> Option { - self.dirs.find_config_file(Self::CONFIG) - } - - fn place_file (&self) -> Result { - self.dirs.place_config_file(Self::CONFIG) - } - - fn get_file (&self) -> Option { - self.dirs.get_config_file(Self::CONFIG) - } - - /// Write initial contents of configuration. - pub fn init (&mut self) -> Usually<()> { - self.init_one(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|{ - cfgs.add(&dsl)?; - Ok(()) - })?; - Ok(()) - } - - /// Write initial contents of a configuration file. - pub fn init_one ( - &mut self, - path: &str, - defaults: &str, - mut each: impl FnMut(&mut Self, &str)->Usually<()> - ) -> Usually<()> { - if self.find_file().is_none() { - //println!("Creating {path:?}"); - std::fs::write(self.place_file()?, defaults)?; - } - Ok(if let Some(path) = self.find_file() { - //println!("Loading {path:?}"); - let src = std::fs::read_to_string(&path)?; - src.as_str().each(move|item|each(self, item))?; - } else { - return Err(format!("{path}: not found").into()) - }) - } - - /// Add statements to configuration from [Dsl] source. - pub fn add (&mut self, dsl: impl Language) -> Usually<&mut Self> { - dsl.each(|item|self.add_one(item))?; - Ok(self) - } - - fn add_one (&self, item: impl Language) -> Usually<()> { - if let Some(expr) = item.expr()? { - let head = expr.head()?; - let tail = expr.tail()?; - let name = tail.head()?; - let body = tail.tail()?; - //println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default()); - match head { - Some("mode") if let Some(name) = name => self.modes.add(&name, &body)?, - Some("keys") if let Some(name) = name => load_bind(&self.binds, &name, &body)?, - Some("view") if let Some(name) = name => load_view(&self.views, &name, &body)?, - _ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into()) - } - Ok(()) - } else { - return Err(format!("Config::load: expected expr, got: {item:?}").into()) - } - } - - pub fn get_mode (&self, mode: impl AsRef) -> Option>>> { - self.modes.get(mode) - } - - pub fn print (&self) { - use ::ansi_term::Color::*; - println!("{:?}", self.dirs); - for (k, v) in self.views.read().unwrap().iter() { - println!("{} {} {v}", Green.paint("VIEW"), Green.bold().paint(format!("{k:<16}"))); - } - for (k, v) in self.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); - } + 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); } } - self.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!(); + } + 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); }); - } - } - - impl Modes { - pub fn add (&self, name: &impl AsRef, 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) -> Option>>> { - self.0.read().unwrap().get(name.as_ref()).cloned() - } - pub fn for_each (&self, mut ator: impl FnMut(&str, &Mode>)->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() - } - } - - impl Mode> { - /// Add a definition to the mode. - /// - /// Supported definitions: - /// - /// - (name ...) -> name - /// - (info ...) -> description - /// - (keys ...) -> key bindings - /// - (mode ...) -> submode - /// - ... -> view - /// - /// ``` - /// let mut mode: tek::Mode> = Default::default(); - /// mode.add("(name hello)").unwrap(); - /// ``` - pub fn add (&mut self, dsl: impl Language) -> Usually<()> { - Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() { - //println!("Mode::add: {head} {:?}", expr.tail()); - let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); - match head { - "name" => self.add_name(tail)?, - "info" => self.add_info(tail)?, - "keys" => self.add_keys(tail)?, - "mode" => self.add_mode(tail)?, - _ => self.add_view(tail)?, - }; - } else if let Ok(Some(word)) = dsl.word() { - self.add_view(word)?; - } else { - return Err(format!("Mode::add: unexpected: {dsl:?}").into()); - }) - - //DslParse(dsl, ||Err(format!("Mode::add: unexpected: {dsl:?}").into())) - //.word(|word|self.add_view(word)) - //.expr(|expr|expr.head(|head|{ - ////println!("Mode::add: {head} {:?}", expr.tail()); - //let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); - //match head { - //"name" => self.add_name(tail), - //"info" => self.add_info(tail), - //"keys" => self.add_keys(tail)?, - //"mode" => self.add_mode(tail)?, - //_ => self.add_view(tail), - //}; - //})) - } - - fn add_name (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(dsl.src()?.map(|src|self.name.push(src.into()))) - } - fn add_info (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(dsl.src()?.map(|src|self.info.push(src.into()))) - } - fn add_view (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(dsl.src()?.map(|src|self.view.push(src.into()))) - } - fn add_keys (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(Some(dsl.each(|expr|{ self.keys.push(expr.trim().into()); Ok(()) })?)) - } - fn add_mode (&mut self, dsl: impl Language) -> Perhaps<()> { - Ok(Some(if let Some(id) = dsl.head()? { - self.modes.add(&id, &dsl.tail())?; - } else { - return Err(format!("Mode::add: self: incomplete: {dsl:?}").into()); - })) - } - } -} - -pub use self::draw::*; -mod draw { - use crate::*; - - /// Load custom view definition. - pub(crate) fn load_view ( - views: &Views, - name: &impl AsRef, - 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. - /// - /// If there is an error, the error is displayed. FIXME: overlay it - /// Then, every top-level form of the DSL description is rendered. - impl View for App { - fn view (&self) -> impl Draw { - thunk(|to: &mut Tui|{ - let xywh = to.area().into(); - - if let Some(e) = self.error.read().unwrap().as_ref() { - //to.show(area(xywh, format!("KYPbanica {xywh:?}").align_c()))?; - to.show(e.as_ref().align_c())?; - } - - if let Some(ref mode) = self.mode { - for (index, dsl) in mode.view.iter().enumerate() { - if let Err(e) = self.interpret(to, dsl) { - let src = &dsl.src().unwrap_or(Some("")).unwrap_or(""); - let message = format!("Mode: {:?}\nLayer: #{index}\nError: {e}\nSource:\n{}", &mode.name, &src); - *self.error.write().unwrap() = Some(message.into()); - break; - } - } - } - - to.show(ShowSize.align_se())?; - - Ok(Some(xywh)) - }) - } - } - - impl Interpret>> for App { - fn interpret_expr <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Drawn { - tek_draw_expr(self, to, lang) - } - fn interpret_word <'a> (&'a self, to: &mut Tui, lang: &'a impl Expression) -> Drawn { - tek_draw_word(self, to, lang) - } - } - - fn tek_draw_expr (state: &App, to: &mut Tui, lang: &impl Expression) -> Drawn { - Ok(Some(if let Some(area) = eval_view(state, to, lang)? { - area - } else if let Some(area) = eval_view_tui(state, to, lang)? { - area - } else { - return Err(format!("App::interpret_expr: unexpected: {lang:?}").into()) - })) - } - - fn tek_draw_word (state: &App, to: &mut Tui, dsl: &impl Expression) -> Drawn { - let mut frags = dsl.src()?.unwrap().split("/"); - match frags.next() { - //Some(":logo") => view_logo().draw(to), - Some(":meters") => draw_meter_section(to, frags), - Some(":tracks") => draw_tracks(to, frags, state), - Some(":scenes") => draw_scenes(to, frags), - Some(":dialog") => draw_dialog(to, frags, state, dsl), - Some(":templates") => draw_templates(to, frags, state), - Some(":sessions") => view_sessions().draw(to), - Some(":browse/title") => view_browse_title(state).draw(to), - Some(":device") => view_device(state).draw(to), - Some(":status") => "TODO: Status Bar".exact_h(1).draw(to), - Some(":editor") => "TODO Editor".draw(to), - Some(":transport") => view_transport(true, "", "", "").draw(to), - Some(":debug") => format!("[{:?}]", to.area()).exact_h(1).draw(to), - Some(_) => { - let views = state.config.views.read().unwrap(); - if let Some(dsl) = views.get(dsl.src()?.unwrap()) { - let dsl = dsl.clone(); - std::mem::drop(views); - state.interpret(to, &dsl) - } else { - unimplemented!("{dsl:?}"); - } - }, - _ => unreachable!() - } - } - - impl_has!(Sizer: |self: App|self.size); - - /// Define a type alias for iterators of sized items (columns). - macro_rules! def_sizes_iter { - ($Type:ident => $($Item:ty),+) => { - pub trait $Type<'a>: Iterator + Send + Sync + 'a {} - impl<'a, T: Iterator + Send + Sync + 'a> $Type<'a> for T {} - } - } - - def_sizes_iter!(PortsSizes => Arc, [Connect]); - def_sizes_iter!(ScenesSizes => Scene); - def_sizes_iter!(TracksSizes => Track); - - pub trait HasWidth { - const MIN_WIDTH: usize; - /// Increment track width. - fn width_inc (&mut self); - /// Decrement track width, down to a hardcoded minimum of [Self::MIN_WIDTH]. - fn width_dec (&mut self); - } - - pub fn draw_meter_section (to: &mut Tui, mut frags: std::str::Split<&str>) -> Drawn { - match frags.next() { - Some("input") => bg(Rgb(30, 30, 30), "Input Meters".align_s().full_h()).draw(to), - Some("output") => bg(Rgb(30, 30, 30), "Output Meters".align_s().full_h()).draw(to), - _ => panic!() - } - } - - pub fn draw_tracks (to: &mut Tui, mut frags: std::str::Split<&str>, state: &App) -> Drawn { - match frags.next() { - None => "TODO tracks".draw(to), - Some("names") => state.project.view_track_names(state.color.clone()).draw(to),//bg(Rgb(40, 40, 40), full_w(align_w("Track Names")))), - Some("inputs") => bg(Rgb(40, 40, 40), "Track Inputs".align_w().full_w()).draw(to), - Some("devices") => bg(Rgb(40, 40, 40), "Track Devices".align_w().full_w()).draw(to), - Some("outputs") => bg(Rgb(40, 40, 40), "Track Outputs".align_w().full_w()).draw(to), - _ => panic!() - } - } - - pub fn draw_scenes (to: &mut Tui, mut frags: std::str::Split<&str>) -> Drawn { - match frags.next() { - None => "TODO Scenes".draw(to), - Some(":scenes/names") => "TODO Scene Names".draw(to), - _ => panic!() - } - } - - pub fn draw_dialog ( - to: &mut Tui, mut frags: std::str::Split<&str>, state: &App, dsl: &impl Expression) -> Drawn { - match frags.next() { - Some("menu") => if let Dialog::Menu(selected, items) = &state.dialog { - let items = items.clone(); - let selected = selected; - Some(thunk(move|to: &mut Tui|{ - for (index, MenuItem(item, _)) in items.0.iter().enumerate() { - let f = if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }; - let b = if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }; - fg_bg(f, b, item.full_w().align_w().exact_h(2)) - .push_y((4 * index) as u16).draw(to)?; - } - Ok(Some(to.area().into())) - }).full_wh()) - } else { - None - }.draw(to), - _ => unimplemented!("App::interpret_word: {dsl:?} ({frags:?})"), - } - } - - pub fn draw_templates (to: &mut Tui, _frags: std::str::Split<&str>, state: &App) -> Drawn { - let height = (state.config.modes.len() * 2) as u16; - thunk(move |to: &mut Tui|{ - let mut index = 0; - state.config.modes.for_each(|id, profile| { - let b = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; - let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); - let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); - let fg1 = Rgb(224, 192, 128); - let fg2 = Rgb(224, 128, 32); - let field_name = fg(fg1, name).align_w().full_w(); - let field_id = fg(fg2, id).align_e().full_w(); - let field_info = info.align_w().full_w(); - let _ = bg(b, south(above(field_name, field_id), field_info)) - .full_w().exact_h(2).push_y((2 * index) as u16).draw(to); - index += 1; - }); - Ok(Some(to.area().into())) - }).min_w(30).exact_h(height).draw(to) - } - - pub fn per_track <'a, T: Draw + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a - ) -> impl Draw + 'a { - per_track_top(tracks, move|index, track|callback(index, track).full_h().align_y()) - } - - pub fn per_track_top <'a, T: Draw + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a - ) -> impl Draw + '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 ( - _theme: ItemTheme, _head: impl Draw, _body: impl Draw - ) -> impl Draw { - } - - pub fn field_v ( - _theme: ItemTheme, _head: impl Draw, _body: impl Draw - ) -> impl Draw { - } - - pub fn view_sessions () -> impl Draw { - 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 { - 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 { - 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()); - /// let _ = tek::view_transport(false, x.as_ref(), x.as_ref(), x.as_ref()); - /// ``` - pub fn view_transport (play: bool, bpm: &str, beat: &str, time: &str) -> impl Draw { - let theme = ItemTheme::G[96]; - bg(Black, east!(above( - button_play_pause(play, false).align_w(), - east!( - field_h(theme, "BPM", bpm), - field_h(theme, "Beat", beat), - field_h(theme, "Time", time), - ).align_e().full_wh() - ))) - } - - /// ``` - /// let x = ""; - /// let _ = tek::view_status(None, x.as_ref(), x.as_ref(), x.as_ref()); - /// let _ = tek::view_status(Some("".into()), x.as_ref(), x.as_ref(), x.as_ref()); - /// ``` - pub fn view_status (sel: Option<&str>, sr: &str, buf: &str, lat: &str) -> impl Draw { - let theme = ItemTheme::G[96]; - let sr = field_h(theme, "SR", sr); - let buf = field_h(theme, "Buf", buf); - let lat = field_h(theme, "Lat", lat); - bg(Black, east!(above( - sel.map(|sel|field_h(theme, "Selected", sel)).align_w().full_wh(), - east!(sr, buf, lat).align_e().full_wh(), - ))) - } - - /// ``` - /// let _ = tek::button_play_pause(true, true); - /// let _ = tek::button_play_pause(true, false); - /// let _ = tek::button_play_pause(false, true); - /// let _ = tek::button_play_pause(false, false); - /// ``` - pub fn button_play_pause (playing: bool, compact: bool) -> impl Draw { - bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, - either(compact, - thunk(move|to: &mut Tui|either(playing, - fg(Rgb(0, 255, 0), " PLAYING "), - fg(Rgb(255, 128, 0), " STOPPED "), - ).exact_w(9).draw(to)), - thunk(move|to: &mut Tui|either(playing, - fg(Rgb(0, 255, 0), south(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), - fg(Rgb(255, 128, 0), south(" ▗▄▖ ", " ▝▀▘ ",)), - ).exact_w(5).draw(to)), - ) - ) - } - - #[cfg(feature = "track")] pub fn view_track_row_section ( - _theme: ItemTheme, - button: impl Draw, - button_add: impl Draw, - content: impl Draw, - ) -> impl Draw { - west( - button_add.align_nw().exact_w(4).full_h(), - east( - button.align_nw().full_h().exact_w(20), - content.align_c().full_wh() - ) - ) - } - - /// ``` - /// let bg = tengri::ratatui::style::Color::Red; - /// let fg = tengri::ratatui::style::Color::Green; - /// let _ = tek::view_wrap(bg, fg, "and then blue, too!"); - /// ``` - pub fn view_wrap (bg: Color, fg: Color, content: impl Draw) -> impl Draw { - let left = fg_bg(bg, Reset, y_repeat("▐").exact_w(1)); - let right = fg_bg(bg, Reset, y_repeat("▌").exact_w(1)); - east(left, west(right, fg_bg(fg, bg, content))) - } - - /// ``` - /// let _ = tek::view_meter("", 0.0); - /// let _ = tek::view_meters(&[0.0, 0.0]); - /// ``` - pub fn view_meter <'a> (label: &'a str, value: f32) -> impl Draw + 'a { - let f = field_h(ItemTheme::G[128], label, format!("{:>+9.3}", value)); - let w = if value >= 0.0 { 13 } - else if value >= -1.0 { 12 } - else if value >= -2.0 { 11 } - else if value >= -3.0 { 10 } - else if value >= -4.0 { 9 } - else if value >= -6.0 { 8 } - else if value >= -9.0 { 7 } - else if value >= -12.0 { 6 } - else if value >= -15.0 { 5 } - else if value >= -20.0 { 4 } - else if value >= -25.0 { 3 } - else if value >= -30.0 { 2 } - else if value >= -40.0 { 1 } - else { 0 }; - let c = if value >= 0.0 { Red } - else if value >= -3.0 { Yellow } - else { Green }; - south!(f, bg(c, ()).exact_wh(w, 1)) - } - - pub fn view_meters (values: &[f32;2]) -> impl Draw + use<'_> { - let left = format!("L/{:>+9.3}", values[0]); - let right = format!("R/{:>+9.3}", values[1]); - south(left, right) - } - - pub fn view_sample_info (sample: Option<&Arc>>) -> impl Draw + use<'_> { - when(sample.is_some(), thunk(move|to: &mut Tui|{ - let sample = sample.unwrap().read().unwrap(); - let theme = sample.color; - east!( - field_h(theme, "Name", format!("{:<10}", sample.name.clone())), - field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())), - field_h(theme, "Start", format!("{:<8}", sample.start)), - field_h(theme, "End", format!("{:<8}", sample.end)), - field_h(theme, "Trans", "0"), - field_h(theme, "Gain", format!("{}", sample.gain)), - ).draw(to) - })) - } - - pub fn view_sample_info_v (sample: Option<&Arc>>) -> impl Draw + use<'_> { - let a = thunk(move|to: &mut Tui|{ - let sample = sample.unwrap().read().unwrap(); - let theme = sample.color; - south!( - field_h(theme, "Name ", format!("{:<10}", sample.name.clone())) .align_w().full_w(), - field_h(theme, "Length", format!("{:<8}", sample.channels[0].len())).align_w().full_w(), - field_h(theme, "Start ", format!("{:<8}", sample.start)) .align_w().full_w(), - field_h(theme, "End ", format!("{:<8}", sample.end)) .align_w().full_w(), - field_h(theme, "Trans ", "0") .align_w().full_w(), - field_h(theme, "Gain ", format!("{}", sample.gain)) .align_w().full_w(), - ).exact_w(20).draw(to) + print!("{}", Blue.paint("VIEW")); + for v in v.view.iter() { print!("{}", Green.paint(format!(" {v}"))); } + println!(); }); - - let b = thunk(|to: &mut Tui|fg(Red, south!( - bold(true, "× No sample."), - "[r] record", - "[Shift-F9] import", - )).draw(to)); - - either(sample.is_some(), a, b) } - pub fn view_sample_status (sample: Option<&Arc>>) -> impl Draw { - bold(true, fg(g(224), sample - .map(|sample|{ - let sample = sample.read().unwrap(); - format!("Sample {}-{}", sample.start, sample.end) - }) - .unwrap_or_else(||"No sample".to_string()))) - } - - pub fn view_track_header (theme: ItemTheme, content: impl Draw) -> impl Draw { - bg(theme.darker.term, content.align_e().full_w()).exact_w(12) - } - - pub fn view_ports_status <'a, T: JackPort> (theme: ItemTheme, title: &'a str, ports: &'a [T]) - -> impl Draw + use<'a, T> - { - let ins = ports.len() as u16; - let frame = Outer(true, Style::default().fg(g(96))); - let iter = move||ports.iter(); - let names = iter_south(iter, move|port, index|format!(" {index} {}", port.port_name()).align_w().full_h()); - let field = field_v(theme, title, names); - border(true, frame, field.exact_wh(20, 1 + ins)).exact_wh(20, 1 + ins) - } - - pub fn view_io_ports <'a, T: PortsSizes<'a>> ( - fg: Color, bg: Color, items: impl Fn()->T + Send + Sync + 'a - ) -> impl Draw + 'a { - type Item<'a> = (usize, &'a Arc, &'a [Connect], usize, usize); - iter(items, - move|(_index, name, connections, y, y2): Item<'a>, _| south( - bold(true, fg_bg(fg, bg, east(" 󰣲 ", name).align_w())).full_h(), - iter(||connections.iter(), move|connect: &'a Connect, index|{ - bold(false, fg_bg(fg, bg, &connect.info)).exact_h(1).align_w().push_y(index as u16) - }) - ).exact_h((y2 - y) as u16).push_y(y as u16)) - } - - pub fn view_scenes_clips <'a, S: ScenesSizes<'a>> ( - scenes: impl Fn()->S, - tracks: impl TracksSizes<'a>, - select: &Selection, - editor: Option<&MidiEditor>, - size: &Sizer, - editing: bool, - ) -> impl Draw { - let status = fg(Green, format!("{}x{}", size.w(), size.h())).align_se().full_wh(); - let tracks = iter_once(tracks, move|(track_index, track, _, _), _| { - let scenes = iter_once(scenes(), move|(scene_index, scene, _, _), _| { - let (name, theme): (Arc, ItemTheme) = scene_name_theme(scene, track_index); - let f = theme.lightest.term; - let (b, o) = scene_bg(theme, select, track_index, scene_index); - let w = scene_w(track, select, track_index, editor); - let y = scene_y(select, scene_index, editor); - let is_selected = scene_sel(select, track_index, scene_index, editing); - below( - Outer(true, Style::default().fg(o)).full_wh(), - below( - below( - fg_bg(o, b, "".full_wh()), - fg_bg(f, b, bold(true, name)).align_nw().full_wh(), - ), - when(is_selected, editor.map(|e|e.view())).full_wh() - ).full_wh() - ).exact_wh(w, y) - }); - scenes.full_h().exact_w(track.width as u16) - }); - - return size.of(above(status, tracks).full_wh()); - - fn scene_name_theme (scene: &Scene, track_index: usize) -> (Arc, ItemTheme) { - if let Some(Some(clip)) = &scene.clips.get(track_index) { - let clip = clip.read().unwrap(); - (format!(" ⏹ {}", &clip.name).into(), clip.color) - } else { - (" ⏹ -- ".into(), ItemTheme::G[32]) - } + pub fn print_status (project: &Arrangement) { + println!("Name: {:?}", &project.name); + println!("JACK: {:?}", &project.jack); + println!("Buffer: {:?}", &project.clock.chunk); + println!("Sample rate: {:?}", &project.clock.timebase.sr); + println!("MIDI PPQ: {:?}", &project.clock.timebase.ppq); + println!("Tempo: {:?}", &project.clock.timebase.bpm); + println!("Quantize: {:?}", &project.clock.quant); + println!("Launch: {:?}", &project.clock.sync); + println!("Playhead: {:?}us", &project.clock.playhead.usec); + println!("Playhead: {:?}s", &project.clock.playhead.sample); + println!("Playhead: {:?}p", &project.clock.playhead.pulse); + println!("Started: {:?}", &project.clock.started); + println!("Tracks:"); + for (i, t) in project.tracks.iter().enumerate() { + println!(" Track {i}: {} {} {:?} {:?}", t.name, t.width, + &t.sequencer.play_clip, &t.sequencer.next_clip); } - - fn scene_bg ( - theme: ItemTheme, select: &Selection, track_index: usize, scene_index: usize - ) -> (Color, Color) { - let mut outline = theme.base.term; - (if select.track() == Some(track_index) && select.scene() == Some(scene_index) { - outline = theme.lighter.term; - theme.light.term - } else if select.track() == Some(track_index) || select.scene() == Some(scene_index) { - outline = theme.darkest.term; - theme.base.term - } else { - theme.dark.term - }, outline) + println!("Scenes:"); + for (i, t) in project.scenes.iter().enumerate() { + println!(" Scene {i}: {} {:?}", &t.name, &t.clips); } - - fn scene_w ( - track: &Track, select: &Selection, track_index: usize, editor: Option<&MidiEditor> - ) -> u16 { - if select.track() == Some(track_index) && let Some(editor) = editor { - (editor.size.w() as usize).max(24).max(track.width) as u16 - } else { - track.width as u16 - } - } - - fn scene_y ( - select: &Selection, scene_index: usize, editor: Option<&MidiEditor> - ) -> u16 { - if select.scene() == Some(scene_index) && let Some(editor) = editor { - editor.size.h().max(12) - } else { - H_SCENE as u16 - } - } - - fn scene_sel (select: &Selection, track_index: usize, scene_index: usize, editing: bool) -> bool { - editing && select.track() == Some(track_index) && select.scene() == Some(scene_index) - } - } - - pub fn view_track_names <'a> ( - theme: ItemTheme, - tracks: impl TracksSizes<'a>, - track_count: usize, - scene_count: usize, - selected: &Selection, - ) -> impl Draw { - let button = south( - button_3("t", "rack ", format!("{}{track_count}", selected.track() - .map(|track|format!("{track}/")).unwrap_or_default()), false), - button_3("s", "cene ", format!("{}{scene_count}", selected.scene() - .map(|scene|format!("{scene}/")).unwrap_or_default()), false)); - let button_2 = south( - button_2("T", "+", false), - button_2("S", "+", false)); - view_track_row_section(theme, button, button_2, bg(theme.darker.term, - thunk(|to: &mut Tui|{ - for (index, track, x1, _x2) in tracks { - let b = if selected.track() == Some(index) { - track.color.light.term - } else { - track.color.base.term - }; - bg(b, south(east( - format!("·t{index:02} "), - fg(Rgb(255, 255, 255), bold(true, &track.name)) - ).align_nw().full_w(), "")) - .exact_w(track_width(index, track)) - .push_x(x1 as u16) - .draw(to)?; - } - Ok(Some(XYWH(0, 0, 0, 0))) - }).exact_h(2))) - } - - pub fn view_track_outputs <'a> ( - theme: ItemTheme, - tracks: impl TracksSizes<'a>, - midi_outs: impl Iterator, - ) -> impl Draw { - view_track_row_section(theme, - south(button_2("o", "utput", false).align_w().full_w(), - thunk(|to: &mut Tui|{ - for port in midi_outs { - let _ = port.port_name().align_w().full_w().draw(to)?; - } - Ok(Some(XYWH(0, 0, 0, 0))) - })), - button_2("O", "+", false), - bg(theme.darker.term, thunk(|to: &mut Tui|{ - for (index, track, _x1, _x2) in tracks { - let f = Rgb(255, 255, 255); - let b = track.color.dark.term; - let iter = ||track.sequencer.midi_outs.iter(); - let draw = |port: &MidiOutput, _|fg(f, bg(b, - format!("·o{index:02} {}", port.port_name()).full_w().align_w()).exact_h(1)); - iter_south(iter, draw).full_h().align_nw() - .exact_w(track_width(index, track)) - .draw(to)?; - } - Ok(Some(XYWH(0, 0, 0, 0))) - }).align_w())) - } - - pub fn view_track_inputs <'a> ( - theme: ItemTheme, tracks: impl TracksSizes<'a>, height: u16, - ) -> impl Draw { - view_track_row_section(theme, button_2("i", "nput", false), button_2("I", "+", false), - bg(theme.darker.term, thunk(move|to: &mut Tui|{ - for (index, track, _x1, _x2) in tracks { - south( - bg(track.color.base.term, - east!( - either(track.sequencer.monitoring, fg(Green, "●mon "), "·mon "), - either(track.sequencer.recording, fg(Red, "●rec "), "·rec "), - either(track.sequencer.overdub, fg(Yellow, "●dub "), "·dub "), - ).align_w().full_w()), - iter_south(||track.sequencer.midi_ins.iter(), - |port, _|fg_bg(Rgb(255, 255, 255), track.color.dark.term, - format!("·i{index:02} {}", port.port_name()).align_w().full_w())) - ).align_nw().exact_wh(track_width(index, track), height + 1).draw(to)?; - } - Ok(Some(XYWH(0, 0, 0, 0))) - }).align_w())) - } - - pub fn view_scenes_names <'a> ( - scenes: impl ScenesSizes<'a>, - select: &Selection, - editor: Option<&MidiEditor>, - editing: bool, - ) -> impl Draw { - thunk(move |to: &mut Tui|{ - for (index, scene, ..) in scenes { - view_scene_name(select, editor, index, scene, editing).draw(to)?; - } - Ok(Some(XYWH(1, 1, 1, 1))) - }).exact_w(20) - } - - pub fn view_scene_name ( - select: &Selection, - editor: Option<&MidiEditor>, - index: usize, - scene: &Scene, - editing: bool - ) -> impl Draw { - let h = if select.scene() == Some(index) && let Some(_editor) = editor { - 7 - } else { - H_SCENE as u16 - }; - let a = east(format!("·s{index:02} "), - fg(g(255), bold(true, &scene.name))).align_w().full_w(); - let b = when(select.scene() == Some(index) && editing, south( - editor.as_ref().map(|e|e.clip_status()), - editor.as_ref().map(|e|e.edit_status())).align_nw().full_wh()); - let c = if select.scene() == Some(index) { - scene.color.light.term - } else { - scene.color.base.term - }; - bg(c, south(a, b).align_nw()).exact_wh(20, h) - } - - pub fn view_midi_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { - track.map(move|track|view_ports_status(theme, "MIDI ins: ", &track.sequencer.midi_ins)) - } - - pub fn view_midi_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { - track.map(move|track|view_ports_status(theme, "MIDI outs: ", &track.sequencer.midi_outs)) - } - - pub fn view_audio_ins_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { - track.map(move|track|view_ports_status(theme, "Audio ins: ", &track.audio_ins())) - } - - pub fn view_audio_outs_status (theme: ItemTheme, track: Option<&Track>) -> impl Draw { - track.map(move|track|view_ports_status(theme, "Audio outs:", &track.audio_outs())) - } - - pub fn view_track_per <'a, T: Draw + 'a, U: TracksSizes<'a>> ( - tracks: impl Fn() -> U + Send + Sync + 'a, - callback: impl Fn(usize, &'a Track)->T + Send + Sync + 'a - ) -> impl Draw { - iter_east(tracks, move|(index, track, x1, x2): (usize, &Track, usize, usize), _|{ - fg_bg( - track.color.lightest.term, - track.color.base.term, - callback(index, track) - ).exact_w((x2 - x1) as u16) - }) - } - - pub fn view_per_track () -> impl Draw {} - - pub fn view_per_track_top () -> impl Draw {} - - pub fn view_inputs <'a> ( - tracks: impl TracksSizes<'a>, midi_ins: &[MidiInput] - ) -> impl Draw { - let title_1 = button_3("i", "nput ", format!("{}", midi_ins.len()), false).align_w().exact_wh(20, 1); - let title_2 = button_2("I", "+", false).exact_wh(4, 1); - east(title_1, west(title_2, thunk(move|to: &mut Tui|{ - for (_index, track, x1, _x2) in tracks { - let _ = south( - bg(track.color.dark.term, east!( - either(track.sequencer.monitoring, fg(Green, "mon "), "mon "), - either(track.sequencer.recording, fg(Red, "rec "), "rec "), - either(track.sequencer.overdub, fg(Yellow, "dub "), "dub "), - ).exact_w(track.width as u16)).align_w().push_x(x1 as u16), - thunk(move |to: &mut Tui|{ - for (index, port) in midi_ins.iter().enumerate() { - let _ = east( - east( - " ● ", - bold(true, fg(Rgb(255,255,255), port.port_name())) - ).align_w().exact_w(20), - west( - ().exact_w(4), - bg(track.color.darker.term, east!( - either(track.sequencer.monitoring, fg(Green, " ● "), " · "), - either(track.sequencer.recording, fg(Red, " ● "), " · "), - either(track.sequencer.overdub, fg(Yellow, " ● "), " · "), - ).exact_w(track.width as u16).align_w()) - ) - ).push_x(index as u16 * 10).exact_h(1).draw(to)?; - } - todo!() - }) - ).draw(to)?; - } - todo!() - }))) - } - - pub fn view_outputs <'a> ( - theme: ItemTheme, - tracks: impl TracksSizes<'a>, - midi_outs: &[MidiOutput], - height: u16, - ) -> impl Draw { - - let list = south( - button_3( - "o", "utput", format!("{}", midi_outs.len()), false - ).align_w().full_w().exact_h(1), - thunk(|to: &mut Tui|{ - for (_index, port) in midi_outs.iter().enumerate() { - east( - east(" ● ", fg(Rgb(255,255,255), bold(true, port.port_name()))).align_w(), - format!("{}/{} ", - port.port().get_connections().len(), - port.connections.len()).align_e().full_w().exact_h(1)).full_w().draw(to)?; - for (index, conn) in port.connections.iter().enumerate() { - format!(" c{index:02}{}", conn.info()).align_w().full_w().exact_h(1).draw(to)?; - } - } - todo!(); - }).align_nw().full_wh().exact_h(height - 1) - ); - - view_track_row_section(theme, list, button_2("O", "+", false), - bg(theme.darker.term, thunk(|to: &mut Tui|{ - for (index, track, _x1, _x2) in tracks { - let _ = thunk(|to: &mut Tui|{ - east( - either(true, fg(Green, "play "), "play "), - either(false, fg(Yellow, "solo "), "solo "), - ).align_w().exact_h(1).draw(to)?; - for (_index, port) in midi_outs.iter().enumerate() { - east( - either(true, fg(Green, " ● "), " · "), - either(false, fg(Yellow, " ● "), " · "), - ).align_w().exact_h(1).draw(to)?; - for (_index, _conn) in port.connections.iter().enumerate() { - "".full_w().exact_h(1).draw(to)?; - } - } - todo!() - }).exact_w(track_width(index, track)).draw(to)?; - } - todo!() - }).align_w().full_w())).exact_h(height) - } - - pub fn view_track_devices <'a> ( - theme: ItemTheme, - tracks: impl TracksSizes<'a>, - track: Option<&Track>, - h: u16, - ) -> impl Draw { - view_track_row_section(theme, - button_3("d", "evice", format!("{}", track.map(|t|t.devices.len()).unwrap_or(0)), false), - button_2("D", "+", false), - iter_once(tracks, move|(_, track, _x1, _x2), index|bg( - track.color.dark.term, - iter_south(move||0..h, - |_, _index|fg_bg( - ItemTheme::G[32].lightest.term, - ItemTheme::G[32].dark.term, - format!(" · {}", "--").align_nw() - ).exact_wh(track.width as u16, 2) - ).align_nw()).exact_wh( - Some(track_width(index, track)), - Some(h + 1), - ))) + println!("MIDI Ins: {:?}", &project.midi_ins); + println!("MIDI Outs: {:?}", &project.midi_outs); + println!("Audio Ins: {:?}", &project.audio_ins); + println!("Audio Outs: {:?}", &project.audio_outs); + // TODO git integration + // TODO dawvert integration } } diff --git a/tengri b/tengri index 5ca32929..c0d6d017 160000 --- a/tengri +++ b/tengri @@ -1 +1 @@ -Subproject commit 5ca329292f808c137b6e2b7e80892e2a9e855696 +Subproject commit c0d6d0174e8858108ec053f4ac486cf6229980e1