use crate::*; use std::path::PathBuf; #[derive(Default, Debug)] pub struct App { /// Must not be dropped for the duration of the process pub jack: Jack, /// Port handles pub ports: std::collections::BTreeMap>, /// Display size pub size: Measure, /// Performance counter pub perf: PerfModel, // View and input definition pub config: Configuration, /// Contains all recently created clips. pub pool: Pool, /// Contains the currently edited musical arrangement pub project: Arrangement, /// Undo history pub history: Vec<(AppCommand, Option)>, // Dialog overlay pub dialog: Option, // Cache of formatted strings pub view_cache: Arc>, /// Base color. pub color: ItemTheme, } has!(Jack: |self: App|self.jack); has!(Pool: |self: App|self.pool); has!(Option: |self: App|self.dialog); has!(Clock: |self: App|self.project.clock); has!(Option: |self: App|self.project.editor); has!(Selection: |self: App|self.project.selection); has!(Vec: |self: App|self.project.midi_ins); has!(Vec: |self: App|self.project.midi_outs); has!(Vec: |self: App|self.project.scenes); has!(Vec: |self: App|self.project.tracks); has!(Measure: |self: App|self.size); maybe_has!(Track: |self: App| { MaybeHas::::get(&self.project) }; { MaybeHas::::get_mut(&mut self.project) }); impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() } } maybe_has!(Scene: |self: App| { MaybeHas::::get(&self.project) }; { MaybeHas::::get_mut(&mut self.project) }); impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } } has_clips!(|self: App|self.pool.clips); impl HasClipsSize for App { fn clips_size (&self) -> &Measure { &self.project.inner_size } } //has_editor!(|self: App|{ //editor = self.editor; //editor_w = { //let size = self.size.w(); //let editor = self.editor.as_ref().expect("missing editor"); //let time_len = editor.time_len().get(); //let time_zoom = editor.time_zoom().get().max(1); //(5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16) //}; //editor_h = 15; //is_editing = self.editor.is_some(); //}); impl App { pub fn update_clock (&self) { ViewCache::update_clock(&self.view_cache, self.clock(), self.size.w() > 80) } pub fn toggle_dialog (&mut self, mut dialog: Option) -> Option { std::mem::swap(&mut self.dialog, &mut dialog); dialog } 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 { self.clip_auto_create(); } else { self.clip_auto_remove(); } } pub fn browser (&self) -> Option<&Browser> { self.dialog.as_ref().and_then(|dialog|match dialog { Dialog::Browser(_, b) => Some(b), _ => None }) } pub(crate) fn device_pick (&mut self, index: usize) { self.dialog = Some(Dialog::Device(index)); } pub(crate) fn device_add (&mut self, index: usize) -> Usually<()> { match index { 0 => self.device_add_sampler(), 1 => self.device_add_lv2(), _ => unreachable!(), } } fn device_add_lv2 (&mut self) -> Usually<()> { todo!(); Ok(()) } fn device_add_sampler (&mut self) -> Usually<()> { let name = self.jack.with_client(|c|c.name().to_string()); let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].name(); let track = self.track().expect("no active track"); let port = format!("{}/Sampler", &track.name); let connect = PortConnect::exact(format!("{name}:{midi}")); let sampler = if let Ok(sampler) = Sampler::new( &self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]] ) { self.dialog = None; Device::Sampler(sampler) } else { self.dialog = Some(Dialog::Message(Message::FailedToAddDevice)); return Err("failed to add device".into()) }; let track = self.track_mut().expect("no active track"); track.devices.push(sampler); Ok(()) } // Create new clip in pool when entering empty cell fn clip_auto_create (&mut self) -> Option>> { 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, mut 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(ref mut editor) = &mut self.project.editor { editor.set_clip(Some(&clip)); } *slot = Some(clip.clone()); Some(clip) } else { None } } // Remove clip from arrangement when exiting empty clip editor fn clip_auto_remove (&mut self) { 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() { 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()); } } } } #[tengri_proc::expose] impl App { fn _todo_isize_stub (&self) -> isize { todo!() } fn _todo_item_theme_stub (&self) -> ItemTheme { todo!() } fn w_sidebar (&self) -> u16 { self.project.w_sidebar(self.editor().is_some()) } fn h_sample_detail (&self) -> u16 { 6.max(self.height() as u16 * 3 / 9) } fn focus_editor (&self) -> bool { self.project.editor.is_some() } fn focus_dialog (&self) -> bool { self.dialog.is_some() } fn focus_message (&self) -> bool { matches!(self.dialog, Some(Dialog::Message(..))) } fn focus_device_add (&self) -> bool { matches!(self.dialog, Some(Dialog::Device(..))) } fn focus_browser (&self) -> bool { self.browser().is_some() } fn focus_clip (&self) -> bool { !self.focus_editor() && matches!(self.selection(), Selection::TrackClip{..}) } fn focus_track (&self) -> bool { !self.focus_editor() && matches!(self.selection(), Selection::Track(..)) } fn focus_scene (&self) -> bool { !self.focus_editor() && matches!(self.selection(), Selection::Scene(..)) } fn focus_mix (&self) -> bool { !self.focus_editor() && matches!(self.selection(), Selection::Mix) } fn focus_pool_import (&self) -> bool { matches!(self.pool.mode, Some(PoolMode::Import(..))) } fn focus_pool_export (&self) -> bool { matches!(self.pool.mode, Some(PoolMode::Export(..))) } fn focus_pool_rename (&self) -> bool { matches!(self.pool.mode, Some(PoolMode::Rename(..))) } fn focus_pool_length (&self) -> bool { matches!(self.pool.mode, Some(PoolMode::Length(..))) } fn dialog_none (&self) -> Option { None } fn dialog_device (&self) -> Option { Some(Dialog::Device(0)) // TODO } fn dialog_device_prev (&self) -> Option { Some(Dialog::Device(0)) // TODO } fn dialog_device_next (&self) -> Option { Some(Dialog::Device(0)) // TODO } fn dialog_help (&self) -> Option { Some(Dialog::Help(0)) } fn dialog_menu (&self) -> Option { Some(Dialog::Menu(0)) } fn dialog_save (&self) -> Option { Some(Dialog::Browser(BrowserTarget::SaveProject, Browser::new(None).unwrap())) } fn dialog_load (&self) -> Option { Some(Dialog::Browser(BrowserTarget::LoadProject, Browser::new(None).unwrap())) } fn dialog_import_clip (&self) -> Option { Some(Dialog::Browser(BrowserTarget::ImportClip(Default::default()), Browser::new(None).unwrap())) } fn dialog_export_clip (&self) -> Option { Some(Dialog::Browser(BrowserTarget::ExportClip(Default::default()), Browser::new(None).unwrap())) } fn dialog_import_sample (&self) -> Option { Some(Dialog::Browser(BrowserTarget::ImportSample(Default::default()), Browser::new(None).unwrap())) } fn dialog_export_sample (&self) -> Option { Some(Dialog::Browser(BrowserTarget::ExportSample(Default::default()), Browser::new(None).unwrap())) } fn dialog_options (&self) -> Option { Some(Dialog::Options) } fn editor_pitch (&self) -> Option { Some((self.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into()) } fn scene_count (&self) -> usize { self.scenes().len() } fn scene_selected (&self) -> Option { self.selection().scene() } fn track_count (&self) -> usize { self.tracks().len() } fn track_selected (&self) -> Option { self.selection().track() } fn select_scene (&self) -> Selection { self.selection().select_scene(self.tracks().len()) } fn select_scene_next (&self) -> Selection { self.selection().select_scene_next(self.scenes().len()) } fn select_scene_prev (&self) -> Selection { self.selection().select_scene_prev() } fn select_track (&self) -> Selection { self.selection().select_track(self.tracks().len()) } fn select_track_next (&self) -> Selection { self.selection().select_track_next(self.tracks().len()) } fn select_track_prev (&self) -> Selection { self.selection().select_track_prev() } fn clip_selected (&self) -> Option>> { match self.selection() { Selection::TrackClip { track, scene } => self.scenes()[*scene].clips[*track].clone(), _ => None } } fn device_kind (&self) -> usize { if let Some(Dialog::Device(index)) = self.dialog { index } else { 0 } } fn device_kind_prev (&self) -> usize { if let Some(Dialog::Device(index)) = self.dialog { index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1)) } else { 0 } } fn device_kind_next (&self) -> usize { if let Some(Dialog::Device(index)) = self.dialog { (index + 1) % device_kinds().len() } else { 0 } } } /// Configuration #[derive(Default, Debug)] pub struct Configuration { /// Path of configuration entrypoint pub path: PathBuf, /// Name of configuration pub name: Option>, /// Description of configuration pub info: Option>, /// View definition pub view: TokenIter<'static>, // Input keymap pub keys: InputMap<'static, App, AppCommand, TuiIn, TokenIter<'static>>, } impl Configuration { pub fn new (path: &impl AsRef, _watch: bool) -> Usually { let text = read_and_leak(path.as_ref())?; let [name, info, view, keys] = Self::parse(TokenIter::from(text))?; Ok(Self { path: path.as_ref().into(), info: info.map(Self::parse_info).flatten(), name: name.map(Self::parse_name).flatten(), view: Self::parse_view(view)?, keys: Self::parse_keys(&path, keys)?, }) } fn parse (iter: TokenIter) -> Usually<[Option;4]> { let mut name: Option = None; let mut info: Option = None; let mut view: Option = None; let mut keys: Option = None; for token in iter { match token.value { Value::Exp(_, mut exp) => { let next = exp.next(); match next { Some(Token { value: Value::Key(sym), .. }) => match sym { "name" => name = Some(exp), "info" => info = Some(exp), "keys" => keys = Some(exp), "view" => view = Some(exp), _ => return Err( format!("(e3) unexpected symbol {sym:?}").into() ) }, _ => return Err( format!("(e2) unexpected exp {:?}", next.map(|x|x.value)).into() ) } }, t => return Err( format!("(e1) unexpected token {token:?}").into() ) }; } Ok([name, info, view, keys]) } fn parse_info (mut iter: TokenIter) -> Option> { iter.next().and_then(|x|if let Value::Str(x) = x.value { Some(x.into()) } else { None }) } fn parse_name (mut iter: TokenIter) -> Option> { iter.next().and_then(|x|if let Value::Str(x) = x.value { Some(x.into()) } else { None }) } fn parse_view (iter: Option) -> Usually { if let Some(view) = iter { Ok(view) } else { Err(format!("missing view definition").into()) } } fn parse_keys (base: &impl AsRef, iter: Option>) -> Usually>> { if iter.is_none() { return Err(format!("missing keys definition").into()) } let mut keys = iter.unwrap(); let mut map = InputMap::default(); while let Some(token) = keys.next() { if let Value::Exp(_, mut exp) = token.value { let next = exp.next(); if let Some(Token { value: Value::Key(sym), .. }) = next { match sym { "layer" => { if let Some(Token { value: Value::Str(path), .. }) = exp.peek() { let path = base.as_ref().parent().unwrap().join(unquote(path)); if !std::fs::exists(&path)? { return Err(format!("(e5) not found: {path:?}").into()) } map.add_layer(read_and_leak(path)?.into()); print!("layer:\n path: {:?}...", exp.0.0.trim()); println!("ok"); } else { return Err(format!("(e4) unexpected non-string {next:?}").into()) } }, "layer-if" => { let mut cond = None; let next = exp.next(); if let Some(Token { value: Value::Sym(sym), .. }) = next { cond = Some(leak(sym)); } else { return Err(format!("(e4) unexpected non-symbol {next:?}").into()) }; let token = exp.peek(); if let Some(Token { value: Value::Str(path), .. }) = token { let path = base.as_ref().parent().unwrap().join(unquote(path)); if !std::fs::exists(&path)? { return Err(format!("(e5) not found: {path:?}").into()) } print!("layer-if:\n cond: {}\n path: {path:?}...", cond.unwrap_or_default()); let keys = read_and_leak(path)?.into(); let cond = cond.unwrap(); println!("ok"); map.add_layer_if( Box::new(move |state: &App|{ Context::get(state, &mut format!("{cond}").as_str().into()) .unwrap_or_else(||panic!( "missing input layer conditional {cond} from {exp:?}")) }), keys ); } else { return Err(format!("(e4) unexpected non-symbol {next:?}").into()) } }, _ => return Err(format!("(e3) unexpected symbol {sym:?}").into()) } } else { return Err(format!("(e2) unexpected exp {:?}", next.map(|x|x.value)).into()) } } else { return Err(format!("(e1) unexpected token {token:?}").into()) } } Ok(map) } } fn read_and_leak (path: impl AsRef) -> Usually<&'static str> { Ok(leak(String::from_utf8(std::fs::read(path.as_ref())?)?)) } fn leak (x: impl AsRef) -> &'static str { Box::leak(x.as_ref().into()) } fn unquote (x: &str) -> &str { let mut chars = x.chars(); chars.next(); //chars.next_back(); chars.as_str() } macro_rules! default_config { ($path:literal) => { ($path, include_str!($path)) }; } pub const DEFAULT_CONFIGS: &'static [(&'static str, &'static str)] = &[ default_config!("../../../config/config_arranger.edn"), default_config!("../../../config/config_groovebox.edn"), default_config!("../../../config/config_sampler.edn"), default_config!("../../../config/config_sequencer.edn"), default_config!("../../../config/config_transport.edn"), default_config!("../../../config/keys_arranger.edn"), default_config!("../../../config/keys_clip.edn"), default_config!("../../../config/keys_clock.edn"), default_config!("../../../config/keys_editor.edn"), default_config!("../../../config/keys_global.edn"), default_config!("../../../config/keys_groovebox.edn"), default_config!("../../../config/keys_length.edn"), default_config!("../../../config/keys_mix.edn"), default_config!("../../../config/keys_pool.edn"), default_config!("../../../config/keys_pool_file.edn"), default_config!("../../../config/keys_rename.edn"), default_config!("../../../config/keys_sampler.edn"), default_config!("../../../config/keys_scene.edn"), default_config!("../../../config/keys_sequencer.edn"), default_config!("../../../config/keys_track.edn"), ];