#![allow(clippy::unit_arg)] mod deps; pub use self::deps::*; mod midi_octave; pub use self::midi_octave::*; mod midi_piano; pub use self::midi_piano::*; mod midi_point; pub use self::midi_point::*; mod midi_range; pub use self::midi_range::*; mod smpl; pub use self::smpl::*; mod smpl_sample; pub use self::smpl_sample::*; mod smpl_add; pub use self::smpl_add::*; mod smpl_kit; pub use self::smpl_kit::*; mod smpl_voice; pub use self::smpl_voice::*; mod midi_clip; pub use self::midi_clip::*; mod midi_edit; pub use self::midi_edit::*; mod midi_mon; pub use self::midi_mon::*; mod midi_play; pub use self::midi_play::*; mod midi_rec; pub use self::midi_rec::*; mod midi_seq; pub use self::midi_seq::*; mod time_base; pub use self::time_base::*; mod time_moment; pub use self::time_moment::*; mod time_ticker; pub use self::time_ticker::*; /// 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 {} } } mod proj; pub use self::proj::*; mod proj_clock; pub use self::proj_clock::*; mod proj_device; pub use self::proj_device::*; mod proj_mixer; pub use self::proj_mixer::*; mod proj_port; pub use self::proj_port::*; mod proj_scene; pub use self::proj_scene::*; mod proj_select; pub use self::proj_select::*; mod proj_track; pub use self::proj_track::*; mod tek_bind; pub use self::tek_bind::*; mod tek_cli; pub use self::tek_cli::*; mod tek_conf; pub use self::tek_conf::*; mod tek_ctrl; pub use self::tek_ctrl::*; mod tek_mode; pub use self::tek_mode::*; mod tek_pool; pub use self::tek_pool::*; mod tek_view; pub use self::tek_view::*; #[cfg(feature = "lv2")] mod device_lv2; #[cfg(feature = "lv2")] pub use self::device_lv2::*; #[cfg(feature = "lv2_gui")] mod device_lv2_gui; #[cfg(feature = "lv2_gui")] pub use self::device_lv2_gui::*; #[cfg(feature = "vst2")] mod device_vst2; #[cfg(feature = "vst2")] pub use self::device_vst2::*; /// Command-line entrypoint. #[allow(unused)] #[hotpath::main] fn main () -> Usually<()> { tengri::Tui::setup_panic(); #[cfg(feature = "cli")] let outcome = Config::watched(run_with_config); #[cfg(not(feature = "cli"))] let outcome = Config::watched(run_new_plain); if let Err(e) = outcome { println!("{e:#?}"); std::process::exit(1); } Ok(()) } #[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 = []; let project = Project::new( &jack, title.into(), clock, tracks.into_iter(), scenes.into_iter(), connect_midi_ins(&jack, &"M", &[], None)?.into_iter(), connect_midi_outs(&jack, &"M", &[], None)?.into_iter(), [].into_iter().chain(connect_audio_ins(&jack, &"L", &[], None)?.into_iter()) .chain(connect_audio_ins(&jack, &"R", &[], None)?.into_iter()), [].into_iter().chain(connect_audio_outs(&jack, &"L", &[], None)?.into_iter()) .chain(connect_audio_outs(&jack, &"R", &[], None)?.into_iter()), ); let mut app = App::new(None, project, config, mode); app.dialog = Dialog::welcome(); Ok(app) })?) } /// Total application state. /// /// ``` /// use tek::{HasTracks, HasScenes, TracksView, ScenesView}; /// let mut app = tek::App::default(); /// let _ = app.scenes_add_one(None, None).unwrap(); /// let _ = app.update_clock(); /// app.project.editor = Some(Default::default()); /// //let _: Vec<_> = app.project.inputs_with_sizes().collect(); /// //let _: Vec<_> = app.project.outputs_with_sizes().collect(); /// let _: Vec<_> = app.project.tracks_with_sizes().collect(); /// //let _: Vec<_> = app.project.scenes_with_sizes(true, 10, 10).collect(); /// //let _: Vec<_> = app.scenes_with_colors(true, 10).collect(); /// //let _: Vec<_> = app.scenes_with_track_colors(true, 10, 10).collect(); /// let _ = app.project.w(); /// //let _ = app.project.w_sidebar(); /// //let _ = app.project.w_tracks_area(); /// let _ = app.project.h(); /// //let _ = app.project.h_tracks_area(); /// //let _ = app.project.h_inputs(); /// //let _ = app.project.h_outputs(); /// let _ = app.project.h_scenes(); /// ``` #[derive(Default, Debug)] #[namespace(Arc<[Connect]> get_arc_array_connect)] #[namespace(Arc get_arc_str)] #[namespace(Color get_color)] #[namespace(ControlAxis ControlAxis::get_axis)] #[namespace(Dialog Dialog::get_dialog)] #[namespace(ItemColor)] #[namespace(ItemTheme)] #[namespace(MidiClip)] #[namespace(ClipSlot ClipSlot::eval)] #[namespace(ClipPool ClipPool::eval)] #[namespace(SampleSlot SampleSlot::eval)] #[namespace(SamplePool SamplePool::eval)] #[namespace(Option> get_opt_arc_array_connect)] #[namespace(Option> get_opt_arc_str)] #[namespace(Option get_opt_itemtheme)] #[namespace(Option get_opt_bool)] #[namespace(Option get_opt_u16)] #[namespace(Option get_opt_u32)] #[namespace(Option get_opt_usize)] #[namespace(PathBuf get_path_buf)] #[namespace(Vec> get_vec_arc_str)] #[namespace(bool get_bool)] #[namespace(f64 get_f64)] #[namespace(isize)] #[namespace(u16 get_u16)] #[namespace(u32)] #[namespace(u8)] #[namespace(usize get_usize)] pub struct App { /// Exit flag pub exit: Exit, /// Base color. pub color: ItemTheme, /// Must not be dropped for the duration of the process pub jack: Jack<'static>, /// Display size pub size: Sizer, /// Performance counter pub perf: PerfModel, /// Available view modes and input bindings pub config: Arc, /// Currently selected mode pub modes: Arc>>>, /// Undo history pub history: Vec<(AppCmd, Option)>, /// Dialog overlay pub dialog: Dialog, /// Contains the currently edited musical arrangement pub project: Project, /// Error, if any pub error: Arc>>>, /// Tracing guard #[cfg(feature = "prof2")] pub guard: Option>>, } impl App { /// Create a new application instance from a backend, project, config, and mode /// /// ``` /// let mut proj = tek::Project::default(); /// proj.jack = tek::tengri::Jack::new(&"test_tek").expect("failed to connect to jack"); /// let config = "(mode hi (name Hello) (info World))"; /// let mut conf = tek::Config::load(tek::Config::default(), config).unwrap(); /// let tek = tek::App::new(None, proj, conf.into(), "hello"); /// ``` pub fn new ( exit: Option, project: Project, config: Arc, mode: impl AsRef ) -> Self { App { exit: exit.unwrap_or_default(), jack: project.jack.clone(), color: ItemTheme::random(), dialog: Dialog::welcome(), modes: Arc::new(RwLock::new(vec![mode.as_ref().into()])), config, project, ..Default::default() } } } fn get_arc_str <'a> (_: &App, src: impl Language) -> Perhaps> { Ok(src.src()?.map(|x|x.into())) } fn get_u16 <'a> (state: &App, src: impl Language) -> Perhaps { Ok(Some(match src.word()? { Some(":w/sidebar") => state.project.w_sidebar(state.editor().is_some()), Some(":h/sample-detail") => 6.max(state.size.h() as u16 * 3 / 9), _ => return try_to_u16(src) })) } fn get_usize <'a> (state: &App, src: impl Language) -> Perhaps { Ok(Some(match src.word()? { Some(":scene-count") => state.scenes().len(), Some(":track-count") => state.tracks().len(), Some(":device-kind") => state.dialog.device_kind().unwrap_or(0), Some(":device-kind/next") => state.dialog.device_kind_next().unwrap_or(0), Some(":device-kind/prev") => state.dialog.device_kind_prev().unwrap_or(0), _ => return try_to_usize(src) })) } fn get_f64 <'a> (_state: &App, _src: impl Language) -> Perhaps { todo!() } fn get_bool <'a> (state: &App, src: impl Language) -> Perhaps { src.word()?.map(|word|Ok(match word { "Y" => true, "N" => false, ":mode/editor" => state.project.editor.is_some(), ":focused/dialog" => !matches!(state.dialog, Dialog::None), ":focused/message" => matches!(state.dialog, Dialog::Message(..)), ":focused/add_device" => matches!(state.dialog, Dialog::Device(..)), ":focused/browser" => state.dialog.browser().is_some(), //":focused/pool/import" => matches!(state.project.pool.mode, Some(PoolMode::Import(..))), //":focused/pool/export" => matches!(state.project.pool.mode, Some(PoolMode::Export(..))), //":focused/pool/rename" => matches!(state.project.pool.mode, Some(PoolMode::Rename(..))), //":focused/pool/length" => matches!(state.project.pool.mode, Some(PoolMode::Length(..))), ":focused/clip" => matches!(state.selection(), Selection::TrackClip{..}), ":focused/track" => matches!(state.selection(), Selection::Track(..)), ":focused/scene" => matches!(state.selection(), Selection::Scene(..)), ":focused/mix" => matches!(state.selection(), Selection::Mix), _ => return err!("not bool: {word}") })).transpose() } #[allow(unused)] fn get_selection <'a> (state: &App, src: impl Language) -> Perhaps { src.word()?.map(|word|Ok(match word { ":select/scene" => state.selection().select_scene(state.tracks().len()), ":select/scene/next" => state.selection().select_scene_next(state.scenes().len()), ":select/scene/prev" => state.selection().select_scene_prev(), ":select/track" => state.selection().select_track(state.tracks().len()), ":select/track/next" => state.selection().select_track_next(state.tracks().len()), ":select/track/prev" => state.selection().select_track_prev(), _ => return err!("not selection: {word}") })).transpose() } fn get_color <'a> (_: &App, 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!("not a color expression: {expr}"), (None, _) => return err!("not a color expression: {expr}"), } } 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!("not a color: {sym}") }) } else { return err!("not a color: {:?}", src.src()?) } } #[allow(unused)] fn get_opt_u7 <'a> (state: &App, src: impl Language) -> Perhaps> { eval_word(src, |word|Ok(match word { ":editor/pitch" => Some(( state.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8 ).into()), _ => return err!("unknown midi note: {word}") })) } fn get_opt_usize <'a> (state: &App, src: impl Language) -> Perhaps> { eval_word(src, |word|Ok(match word { ":selected/scene" => state.selection().scene(), ":selected/track" => state.selection().track(), _ => return err!("unknown opt: {word}") })) } fn get_opt_u16 <'a> (_: &App, _src: impl Language) -> Perhaps> { Ok(None) } fn get_opt_u32 <'a> (_: &App, _src: impl Language) -> Perhaps> { Ok(None) } fn get_opt_itemtheme <'a> (_: &App, _: impl Language) -> Perhaps> { todo!() } fn get_opt_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps>> { todo!() } fn get_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps> { todo!() } fn get_opt_arc_str <'a> (_: &App, _: impl Language) -> Perhaps>> { todo!() } fn get_opt_bool <'a> (_: &App, _: impl Language) -> Perhaps> { todo!() } fn get_path_buf <'a> (_: &App, _: impl Language) -> Perhaps { todo!() } fn get_vec_arc_str <'a> (_: &App, _: impl Language) -> Perhaps>> { todo!() } primitive!(u8: try_to_u8); primitive!(u16: try_to_u16); primitive!(usize: try_to_usize); primitive!(isize: try_to_isize); impl_has!(Clock: |self: App|self.project.clock); impl_has!(Dialog: |self: App|self.dialog); impl_has!(Jack<'static>: |self: App|self.jack); impl_has!(Pool: |self: App|self.project.clips); impl_has!(Pool: |self: App|self.project.samples); impl_has!(Selection: |self: App|self.project.selection); impl_has!(Sizer: |self: App|self.size); impl_as_ref!(Option: |self: App|todo!()); impl_as_mut!(Option: |self: App|todo!()); impl_as_ref!(Option: |self: App|self.project.as_ref()); impl_as_mut!(Option: |self: App|self.project.as_mut()); impl HasClip for App { fn clip_slot (&self) -> Option> { self.project.clip_slot() } } tui_keys!(self: App, input { #[cfg(feature = "prof2")] profiling::scope!("App::tui_keys!"); Self::input_cmd(self, input) }); impl App { fn input_cmd (state: &mut Self, input: &TuiEvent) -> Usually<()> { if let Some(bindings) = Self::input_cmds_find(state, input) { return Self::input_cmds_exec(state, bindings) } Ok(()) } fn input_cmds_find (state: &mut Self, input: &TuiEvent) -> Option>]>> { for name in state.modes.read().iter().rev() { let mode = state.config.get_mode(name).as_ref().map(Arc::clone); if let Some(mode) = mode { let binds = state.config.binds.clone(); for id in mode.keys.iter() { if let Some(event_map) = binds.find(id) && let Some(bindings) = event_map.query(input) { return Some(bindings.into()) } } } } None } fn input_cmds_exec (state: &mut Self, bindings: Arc<[Binding>]>) -> Usually<()> { for binding in bindings.iter() { for src in binding.commands.iter() { if let Some(command) = Namespace::::namespace(state, &src)? { Self::cmd_dispatch(state, command)?; } } } Ok(()) } fn cmd_dispatch (state: &mut Self, command: AppCmd) -> Usually<()> { match command.clone().dispatch(state) { Ok(undo) => { state.history.push((command, undo)); Ok(()) }, Err(err) => { state.history.push((command, None)); Err(err) } } } } /// A control axis. /// /// ``` /// let axis = tek::ControlAxis::X; /// ``` #[derive(Debug, Copy, Clone)] pub enum ControlAxis { X, XFine, Y, YFine, Z, ZFine, I, IFine, } impl ControlAxis { fn get_axis <'a> (_: &App, src: impl Language) -> Perhaps { eval_word(src, |word|Ok(match word { "X" => ControlAxis::X, "XFine" | "X2" => ControlAxis::XFine, "Y" => ControlAxis::Y, "YFine" | "Y2" => ControlAxis::YFine, "Z" => ControlAxis::Z, "ZFine" | "Z2" => ControlAxis::ZFine, _ => return err!("unknown axis {word}") })) } } 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); } /// Various possible dialog modes. /// /// ``` /// let dialog: tek::Dialog = Default::default(); /// ``` #[derive(Debug, Clone, Default, PartialEq)] pub enum Dialog { #[default] None, Help(usize), Menu(usize, Arc<[MenuItem]>), Device(usize), Message(Arc), Browse(BrowseTarget, Arc), Options, } impl Dialog { fn get_dialog <'a> (_: &App, src: impl Language) -> Perhaps { eval_word(&src, |word|Ok(match word { ":none" => Dialog::None, ":options" => Dialog::Options, ":device" => Dialog::Device(0), ":device/prev" => Dialog::Device(0), ":device/next" => Dialog::Device(0), ":help" => Dialog::Help(0), ":save" => Dialog::Browse( BrowseTarget::SaveProject, Browse::new(None).unwrap().into()), ":load" => Dialog::Browse( BrowseTarget::LoadProject, Browse::new(None).unwrap().into()), ":import/clip" => Dialog::Browse( BrowseTarget::ImportClip(Default::default()), Browse::new(None).unwrap().into()), ":export/clip" => Dialog::Browse( BrowseTarget::ExportClip(Default::default()), Browse::new(None).unwrap().into()), ":import/sample" => Dialog::Browse( BrowseTarget::ImportSample(Default::default()), Browse::new(None).unwrap().into()), ":export/sample" => Dialog::Browse( BrowseTarget::ExportSample(Default::default()), Browse::new(None).unwrap().into()), _ => return Err(format!("not a dialog: {}", src.src()?.unwrap_or("")).into()) })) } /// ``` /// let _ = tek::Dialog::welcome(); /// ``` pub fn welcome () -> Self { Self::Menu(1, Arc::new([ MenuItem::new(" Resume session ", [ AppCmd::Project { command: ProjectCmd::LoadLast } ]), MenuItem::new(" Create session ", [ AppCmd::Dialog { dialog: Dialog::None }, AppCmd::Mode { command: ModeCmd::Reset { modes: vec!["Arranger".into()] } }, AppCmd::Tracks { command: TracksCmd::Add }, AppCmd::Scenes { command: ScenesCmd::Add }, AppCmd::MidiClip { command: MidiClipCmd::Upsert }, AppCmd::MidiEdit { command: MidiEditCmd::Toggle }, ]), MenuItem::new(" Load session ", [ AppCmd::Dialog { dialog: Dialog::Browse( BrowseTarget::LoadProject, Browse::new(None).unwrap().into() ) //dialog: Dialog::Browse { //title: "Load session".into(), //filter: "\\.tek$", //} } ]), MenuItem::new(" Exit ", [ AppCmd::Exit ]), ])) } /// FIXME: generalize /// ``` /// let _ = tek::Dialog::welcome().menu_selected(); /// ``` pub fn menu_selected (&self) -> Option { if let Self::Menu(selected, _) = self { Some(*selected) } else { None } } /// FIXME: generalize /// ``` /// let _ = tek::Dialog::welcome().menu_next(); /// ``` pub fn menu_next (&self) -> Self { match self { Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.len()), items.clone()), _ => Self::None } } /// FIXME: generalize /// ``` /// let _ = tek::Dialog::welcome().menu_prev(); /// ``` pub fn menu_prev (&self) -> Self { match self { Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.len()), items.clone()), _ => Self::None } } /// FIXME: generalize /// ``` /// let _ = tek::Dialog::welcome().device_kind(); /// ``` pub fn device_kind (&self) -> Option { if let Self::Device(index) = self { Some(*index) } else { None } } /// FIXME: generalize /// ``` /// let _ = tek::Dialog::welcome().device_kind_next(); /// ``` pub fn device_kind_next (&self) -> Option { self.device_kind().map(|index|(index + 1) % device_kinds().len()) } /// FIXME: generalize /// ``` /// let _ = tek::Dialog::welcome().device_kind_prev(); /// ``` pub fn device_kind_prev (&self) -> Option { self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1))) } /// FIXME: implement pub fn message (&self) -> Option<&str> { todo!() } /// FIXME: implement pub fn browser (&self) -> Option<&Arc> { todo!() } /// 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 } } /// An item of a menu. /// /// ``` /// let item: tek::MenuItem = Default::default(); /// ``` #[derive(Clone)] pub struct MenuItem(pub Arc, pub Arc<[AppCmd]>); impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) }); impl_default!(MenuItem: Self::new("", [])); impl PartialEq for MenuItem { fn eq (&self, other: &Self) -> bool { self.0 == other.0 } } impl MenuItem { pub fn new (name: impl AsRef, actions: [AppCmd; N]) -> Self { Self(name.as_ref().into(), Arc::new(actions)) } } //impl AsRef> for MenuItems { //fn as_ref (&self) -> &Arc<[MenuItem]> { //&self.0 //} //} /// Browses for files to load/save. /// /// ``` /// let browse = tek::Browse::default(); /// ``` #[derive(Debug, Clone, Default, PartialEq)] pub struct Browse { pub cwd: PathBuf, pub dirs: Vec<(OsString, String)>, pub files: Vec<(OsString, String)>, pub filter: String, pub index: usize, pub scroll: usize, pub size: Sizer, } //pub(crate) struct EntriesIterator<'a, S: Screen> { //pub browser: &'a Browse, //pub offset: usize, //pub length: usize, //pub index: usize, //_screen: std::marker::PhantomData //} #[derive(Clone, Debug)] pub enum BrowseTarget { SaveProject, LoadProject, ImportSample(Arc>>), ExportSample(Arc>>), ImportClip(Arc>>), ExportClip(Arc>>), } // Commands supported by [Browse] //#[derive(Debug, Clone, PartialEq)] //pub enum BrowseCommand { //Begin, //Cancel, //Confirm, //Select(usize), //Chdir(PathBuf), //Filter(Arc), //} impl Browse { pub fn new (cwd: Option) -> Usually { let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? }; let mut dirs = vec![]; let mut files = vec![]; for entry in std::fs::read_dir(&cwd)? { let entry = entry?; let name = entry.file_name(); let decoded = name.clone().into_string().unwrap_or_else(|_|"".to_string()); let meta = entry.metadata()?; if meta.is_dir() { dirs.push((name, format!("📁 {decoded}"))); } else if meta.is_file() { files.push((name, format!("📄 {decoded}"))); } } Ok(Self { cwd, dirs, files, ..Default::default() }) } pub fn to_chdir (&self) -> Usually { Self::new(Some(self.path())) } pub fn len (&self) -> usize { self.dirs.len() + self.files.len() } pub fn is_dir (&self) -> bool { self.index < self.dirs.len() } pub fn is_file (&self) -> bool { self.index >= self.dirs.len() } pub fn path (&self) -> PathBuf { self.cwd.join(if self.is_dir() { &self.dirs[self.index].0 } else if self.is_file() { &self.files[self.index - self.dirs.len()].0 } else { unreachable!() }) } //fn tui_entries (&self) -> EntriesIterator<'_, Tui> { //EntriesIterator { //offset: 0, //index: 0, //length: self.dirs.len() + self.files.len(), //browser: self, //_screen: Default::default(), //} //} } impl PartialEq for BrowseTarget { fn eq (&self, other: &Self) -> bool { match self { Self::ImportSample(_) => false, Self::ExportSample(_) => false, Self::ImportClip(_) => false, Self::ExportClip(_) => false, #[allow(unused)] t => matches!(other, t) } } } 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)) } impl App { /// 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 } } }