mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-10-08 18:56:42 +02:00
801 lines
27 KiB
Rust
801 lines
27 KiB
Rust
#![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<Item=(usize, $(&'a $Item,)+ usize, usize)> + Send + Sync + 'a {}
|
|
impl<'a, T: Iterator<Item=(usize, $(&'a $Item,)+ usize, usize)> + 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_has; pub use self::tek_has::*;
|
|
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<str> 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<Arc<[Connect]>> get_opt_arc_array_connect)]
|
|
#[namespace(Option<Arc<str>> get_opt_arc_str)]
|
|
#[namespace(Option<ItemTheme> get_opt_itemtheme)]
|
|
#[namespace(Option<bool> get_opt_bool)]
|
|
#[namespace(Option<u16> get_opt_u16)]
|
|
#[namespace(Option<u32> get_opt_u32)]
|
|
#[namespace(Option<usize> get_opt_usize)]
|
|
#[namespace(PathBuf get_path_buf)]
|
|
#[namespace(Vec<Arc<str>> 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<Config>,
|
|
/// Currently selected mode
|
|
pub modes: Arc<RwLock<Vec<Arc<str>>>>,
|
|
/// Undo history
|
|
pub history: Vec<(AppCmd, Option<AppCmd>)>,
|
|
/// Dialog overlay
|
|
pub dialog: Dialog,
|
|
/// Contains the currently edited musical arrangement
|
|
pub project: Project,
|
|
/// Error, if any
|
|
pub error: Arc<RwLock<Option<Arc<str>>>>,
|
|
/// Tracing guard
|
|
#[cfg(feature = "prof2")] pub guard: Option<tracing_flame::FlushGuard<std::io::BufWriter<std::fs::File>>>,
|
|
}
|
|
|
|
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<Exit>,
|
|
project: Project,
|
|
config: Arc<Config>,
|
|
mode: impl AsRef<str>
|
|
) -> 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<Arc<str>> {
|
|
Ok(src.src()?.map(|x|x.into()))
|
|
}
|
|
|
|
fn get_u16 <'a> (state: &App, src: impl Language) -> Perhaps<u16> {
|
|
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<usize> {
|
|
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<f64> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_bool <'a> (state: &App, src: impl Language) -> Perhaps<bool> {
|
|
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<Selection> {
|
|
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<Color> {
|
|
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<Option<u7>> {
|
|
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<Option<usize>> {
|
|
eval_word(src, |word|Ok(match word {
|
|
|
|
":selected/scene" => state.selection().scene(),
|
|
":selected/track" => state.selection().track(),
|
|
|
|
_ => return err!("unknown opt<usize>: {word}")
|
|
|
|
}))
|
|
}
|
|
|
|
|
|
fn get_opt_u16 <'a> (_: &App, _src: impl Language) -> Perhaps<Option<u16>> {
|
|
Ok(None)
|
|
}
|
|
|
|
fn get_opt_u32 <'a> (_: &App, _src: impl Language) -> Perhaps<Option<u32>> {
|
|
Ok(None)
|
|
}
|
|
|
|
fn get_opt_itemtheme <'a> (_: &App, _: impl Language) -> Perhaps<Option<ItemTheme>> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_opt_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps<Option<Arc<[Connect]>>> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_arc_array_connect <'a> (_: &App, _: impl Language) -> Perhaps<Arc<[Connect]>> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_opt_arc_str <'a> (_: &App, _: impl Language) -> Perhaps<Option<Arc<str>>> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_opt_bool <'a> (_: &App, _: impl Language) -> Perhaps<Option<bool>> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_path_buf <'a> (_: &App, _: impl Language) -> Perhaps<PathBuf> {
|
|
todo!()
|
|
}
|
|
|
|
fn get_vec_arc_str <'a> (_: &App, _: impl Language) -> Perhaps<Vec<Arc<str>>> {
|
|
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<MidiClip>: |self: App|self.project.clips);
|
|
impl_has!(Pool<Sample>: |self: App|self.project.samples);
|
|
impl_has!(Selection: |self: App|self.project.selection);
|
|
impl_has!(Sizer: |self: App|self.size);
|
|
impl_as_ref!(Option<Project>: |self: App|todo!());
|
|
impl_as_mut!(Option<Project>: |self: App|todo!());
|
|
impl_as_ref!(Option<MidiEditor>: |self: App|self.project.as_ref());
|
|
impl_as_mut!(Option<MidiEditor>: |self: App|self.project.as_mut());
|
|
|
|
impl HasClip for App {
|
|
fn clip_slot (&self) -> Option<Slot<MidiClip>> {
|
|
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<Arc<[Binding<Arc<str>>]>> {
|
|
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<Arc<str>>]>) -> Usually<()> {
|
|
for binding in bindings.iter() {
|
|
for src in binding.commands.iter() {
|
|
if let Some(command) = Namespace::<AppCmd>::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<ControlAxis> {
|
|
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}")
|
|
|
|
}))
|
|
}
|
|
}
|
|
|
|
/// 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<str>),
|
|
Browse(BrowseTarget, Arc<Browse>),
|
|
Options,
|
|
}
|
|
|
|
impl Dialog {
|
|
|
|
fn get_dialog <'a> (_: &App, src: impl Language) -> Perhaps<Dialog> {
|
|
|
|
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 { 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<usize> {
|
|
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<usize> {
|
|
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<usize> {
|
|
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<usize> {
|
|
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<Browse>> { 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<str>, 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 <const N: usize> (name: impl AsRef<str>, actions: [AppCmd; N]) -> Self {
|
|
Self(name.as_ref().into(), Arc::new(actions))
|
|
}
|
|
}
|
|
|
|
//impl AsRef<Arc<[MenuItem]>> 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<S>
|
|
//}
|
|
|
|
#[derive(Clone, Debug)] pub enum BrowseTarget {
|
|
SaveProject,
|
|
LoadProject,
|
|
ImportSample(Arc<RwLock<Option<Sample>>>),
|
|
ExportSample(Arc<RwLock<Option<Sample>>>),
|
|
ImportClip(Arc<RwLock<Option<MidiClip>>>),
|
|
ExportClip(Arc<RwLock<Option<MidiClip>>>),
|
|
}
|
|
|
|
// Commands supported by [Browse]
|
|
//#[derive(Debug, Clone, PartialEq)]
|
|
//pub enum BrowseCommand {
|
|
//Begin,
|
|
//Cancel,
|
|
//Confirm,
|
|
//Select(usize),
|
|
//Chdir(PathBuf),
|
|
//Filter(Arc<str>),
|
|
//}
|
|
impl Browse {
|
|
pub fn new (cwd: Option<PathBuf>) -> Usually<Self> {
|
|
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(|_|"<unreadable>".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> { 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<OsString>, Vec<OsString>)> {
|
|
let (mut subdirs, mut files) = std::fs::read_dir(dir)?
|
|
.fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{
|
|
let entry = entry.expect("failed to read drectory entry");
|
|
let meta = entry.metadata().expect("failed to read entry metadata");
|
|
if meta.is_file() {
|
|
files.push(entry.file_name());
|
|
} else if meta.is_dir() {
|
|
subdirs.push(entry.file_name());
|
|
}
|
|
(subdirs, files)
|
|
});
|
|
subdirs.sort();
|
|
files.sort();
|
|
Ok((subdirs, files))
|
|
}
|
|
|
|
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 }
|
|
}
|
|
}
|
|
|
|
impl HasTrackScroll for App {
|
|
fn track_scroll (&self) -> usize {
|
|
self.project.track_scroll()
|
|
}
|
|
fn tracks_last (&self) -> usize {
|
|
self.project.track_last
|
|
}
|
|
fn tracks_last_mut (&mut self) -> &mut usize {
|
|
&mut self.project.track_last
|
|
}
|
|
}
|
|
|
|
impl HasSceneScroll for App {
|
|
fn scene_scroll (&self) -> usize { self.project.scene_scroll() }
|
|
}
|