mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-10-08 18:56:42 +02:00
1247 lines
45 KiB
Rust
1247 lines
45 KiB
Rust
#![allow(clippy::unit_arg)]
|
|
|
|
pub use self::deps::*; mod deps;
|
|
|
|
/// Command-line entrypoint.
|
|
#[allow(unused)]
|
|
#[hotpath::main]
|
|
fn main () -> Usually<()> {
|
|
tengri::Tui::setup_panic();
|
|
#[cfg(not(feature = "cli"))]
|
|
let outcome = config_watch_run(main_plain, None);
|
|
#[cfg(feature = "cli")]
|
|
let outcome = config_watch_run(main_cli, None);
|
|
if let Err(e) = outcome {
|
|
println!("{e:#?}");
|
|
std::process::exit(1);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Start with empty project.
|
|
#[cfg(not(feature = "cli"))]
|
|
fn main_plain (config: Arc<Watch>) -> 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::create(
|
|
&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::*;
|
|
/// let mut app = App::default();
|
|
/// let _ = app.scenes_add_one(None, None).unwrap();
|
|
/// 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();
|
|
/// ```
|
|
#[derive(Default, Debug)]
|
|
#[namespace(Arc<[Connect]> tek_arc_array_connect)]
|
|
#[namespace(Arc<str> tek_arc_str)]
|
|
#[namespace(Color tek_color)]
|
|
#[namespace(Option<Arc<[Connect]>> tek_opt_arc_array_connect)]
|
|
#[namespace(Option<Arc<str>> tek_opt_arc_str)]
|
|
#[namespace(Option<ItemTheme> tek_opt_itemtheme)]
|
|
#[namespace(Option<bool> tek_opt_bool)]
|
|
#[namespace(Option<u16> tek_opt_u16)]
|
|
#[namespace(Option<u32> tek_opt_u32)]
|
|
#[namespace(Option<usize> tek_opt_usize)]
|
|
#[namespace(PathBuf tek_path_buf)]
|
|
#[namespace(Vec<Arc<str>> tek_vec_arc_str)]
|
|
#[namespace(bool tek_bool)]
|
|
#[namespace(f64 tek_f64)]
|
|
#[namespace(u16 tek_u16)]
|
|
#[namespace(usize tek_usize)]
|
|
#[namespace(ControlAxis ControlAxis::eval)]
|
|
#[namespace(Dialog Dialog::eval)]
|
|
#[namespace(ClipSlot ClipSlot::eval)]
|
|
#[namespace(ClipPool ClipPool::eval)]
|
|
#[namespace(SampleSlot SampleSlot::eval)]
|
|
#[namespace(SamplePool SamplePool::eval)]
|
|
#[namespace(ItemColor)]
|
|
#[namespace(ItemTheme)]
|
|
#[namespace(MidiClip)]
|
|
#[namespace(isize)]
|
|
#[namespace(u32)]
|
|
#[namespace(u8)]
|
|
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,
|
|
/// Configuration watcher
|
|
pub watch: Arc<Watch>,
|
|
/// Currently active modes
|
|
pub stack: Arc<RwLock<Vec<Frame>>>,
|
|
/// 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
|
|
///
|
|
/// ```
|
|
/// # use ::tek::{*, tengri::Jack};
|
|
/// let proj = Project::new_jack(&"test_tek").expect("failed to connect to jack");
|
|
/// let conf = include_str!("tek.edn");
|
|
/// let mode = vec!["Arranger".into(), "Menu".into()];
|
|
/// let tek = config_reload(App::new(None, proj, None, mode), conf).unwrap();
|
|
/// ```
|
|
pub fn new (
|
|
exit: Option<Exit>,
|
|
project: Project,
|
|
watch: impl Into<Option<Arc<Watch>>>,
|
|
modes: impl Into<Option<Vec<Frame>>>,
|
|
) -> Self {
|
|
App {
|
|
exit: exit.unwrap_or_default(),
|
|
jack: project.jack.clone(),
|
|
color: ItemTheme::random(),
|
|
dialog: Dialog::welcome(),
|
|
stack: Arc::new(RwLock::new(modes.into().unwrap_or_default())),
|
|
watch: watch.into().unwrap_or_default(),
|
|
project,
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub use self::conf::*; mod conf {
|
|
use crate::*;
|
|
|
|
mod axis; pub use self::axis::*;
|
|
mod bind; pub use self::bind::*;
|
|
mod lang; pub use self::lang::*;
|
|
mod mode; pub use self::mode::*;
|
|
mod stack; pub use self::stack::*;
|
|
mod view; pub use self::view::*;
|
|
|
|
pub trait HasError<T> {
|
|
fn error (&self) -> &RwLock<Option<T>>;
|
|
fn error_set (&self, e: Option<T>) {
|
|
*self.error().write() = e;
|
|
}
|
|
}
|
|
impl<T: AsRef<Config>> HasError<Arc<str>> for T {
|
|
fn error (&self) -> &RwLock<Option<Arc<str>>> {
|
|
&self.as_ref().error
|
|
}
|
|
}
|
|
impl HasError<Arc<str>> for Arc<Watch> {
|
|
fn error (&self) -> &RwLock<Option<Arc<str>>> {
|
|
&self.as_ref().config.error
|
|
}
|
|
}
|
|
|
|
pub trait HasConfig {
|
|
fn config (&self) -> &Config;
|
|
/// Print the configuration.
|
|
fn config_print (&self) {
|
|
let config = self.config();
|
|
print_config_views(config);
|
|
print_config_binds(config);
|
|
print_config_modes(config);
|
|
}
|
|
/// Make this configuration empty.
|
|
fn config_clear (&self) {
|
|
let config = self.config();
|
|
config.modes_clear();
|
|
config.views_clear();
|
|
config.binds_clear();
|
|
config.stamp.store(quanta::Clock::new().raw(), Relaxed);
|
|
}
|
|
}
|
|
impl HasConfig for App {
|
|
fn config (&self) -> &Config {
|
|
&self.watch.config
|
|
}
|
|
}
|
|
impl HasConfig for Watch {
|
|
fn config (&self) -> &Config {
|
|
&self.config
|
|
}
|
|
}
|
|
impl HasConfig for Config {
|
|
fn config (&self) -> &Config {
|
|
self
|
|
}
|
|
}
|
|
|
|
/// Default configuration contents.
|
|
const CONFIG_DEFAULT: &'static str = include_str!("tek.edn");
|
|
|
|
/// Create, initialize, and watch a new configuration.
|
|
pub fn config_watch_run <T: 'static> (
|
|
call: impl FnOnce(Arc<Watch>)->T,
|
|
dirs: impl Into<Option<BaseDirectories>>,
|
|
) -> Usually<T> {
|
|
let config = Arc::new(Watch::new(dirs.into()));
|
|
config_init(config.as_ref())?;
|
|
let watcher = config.clone().watch_config(None)?;
|
|
*config.watcher.write() = Some(watcher);
|
|
let result = call(config);
|
|
Ok(result)
|
|
}
|
|
|
|
pub fn config_init <C: AsRef<Watch> + AsRef<Config>> (config: C) -> Usually<C> {
|
|
// Write initial contents of configuration:
|
|
let watcher: &Watch = config.as_ref();
|
|
if watcher.find_file().is_none() {
|
|
std::fs::write(watcher.place_file()?, CONFIG_DEFAULT)?;
|
|
}
|
|
// Load config from file:
|
|
Ok(if let Some(path) = watcher.find_file() {
|
|
let source = std::fs::read_to_string(&path)?;
|
|
config_reload(config, source)?
|
|
} else {
|
|
return Err(format!("config_init: not found").into())
|
|
})
|
|
}
|
|
|
|
/// Reload configuration from source.
|
|
pub fn config_reload <C: AsRef<Config>, L: Language> (state: C, src: L) -> Usually<C> {
|
|
state.as_ref().config_clear();
|
|
src.each(state, |c, s|config_add(c, s.trim()))
|
|
}
|
|
|
|
/// Load configuration item.
|
|
fn config_add <C: AsRef<Config>, L: Language> (state: C, src: L) -> Usually<C> {
|
|
if let Some(expr) = src.expr()? {
|
|
match expr.head()? {
|
|
Some("mode") => state.as_ref().modes_add(expr.tail()),
|
|
Some("view") => state.as_ref().views_add(expr.tail()),
|
|
Some("bind") => state.as_ref().binds_def_keys(expr.tail()),
|
|
Some("axis") => state.as_ref().binds_def_axis(expr.tail()),
|
|
_ => return Err(format!("config_add: unexpected: {src:?}").into())
|
|
}?;
|
|
Ok(state)
|
|
} else {
|
|
Err(format!("Config::add_one: tried to add empty item").into())
|
|
}
|
|
}
|
|
|
|
/// Contains timestamped mode, view, and bind definitions.
|
|
///
|
|
/// ```
|
|
/// # use ::{std::sync::Arc, tek::*};
|
|
///
|
|
/// // Default empty config:
|
|
/// let config: Config = Default::default();
|
|
///
|
|
/// // Define a binding:
|
|
/// config.binds_def_keys(stringify!(Confirm (@enter confirm))).unwrap();
|
|
///
|
|
/// // Define an axis:
|
|
/// config.binds_def_axis(stringify!(Y @up @down)).unwrap();
|
|
/// ```
|
|
#[derive(Default, Debug)]
|
|
pub struct Config {
|
|
/// Error caught during (re)loading
|
|
pub error: RwLock<Option<Arc<str>>>,
|
|
/// Timestamp
|
|
pub stamp: AtomicU64,
|
|
/// Active collection of interaction modes.
|
|
pub modes: Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode>>>>,
|
|
/// Active collection of view definitions.
|
|
pub views: Arc<RwLock<BTreeMap<Arc<str>, Arc<View<Tui, App>>>>>,
|
|
/// Active collection of key bindings.
|
|
pub keys: Arc<RwLock<BTreeMap<Arc<str>, Arc<Bind<TuiEvent, Arc<str>>>>>>,
|
|
/// Active collection of axis bindings.
|
|
pub axis: Arc<RwLock<BTreeMap<Arc<str>, Arc<Axis<TuiEvent>>>>>,
|
|
}
|
|
|
|
impl Config {
|
|
/// Create a new timestamped config state.
|
|
pub fn new () -> Self {
|
|
Self { stamp: quanta::Clock::new().raw().into(), ..Default::default() }
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Configuration watcher and manager.
|
|
///
|
|
/// ```
|
|
/// let config = tek::Watch::new(None);
|
|
/// ```
|
|
#[derive(Default, Debug)]
|
|
pub struct Watch {
|
|
/// XDG base directories of running user.
|
|
pub dirs: BaseDirectories,
|
|
/// File system notifier
|
|
pub watcher: RwLock<Option<PollWatcher>>,
|
|
/// App configuration
|
|
pub config: Config,
|
|
}
|
|
|
|
impl Watch {
|
|
/// Default configuration directory.
|
|
const DIR: &'static str = "tek";
|
|
/// Default configuration subdirectory.
|
|
const SUB: &'static str = "v0";
|
|
/// Default configuration file name.
|
|
const EDN: &'static str = "tek.edn";
|
|
/// Create a new watched app configuration
|
|
/// from a set of XDG base directories,
|
|
pub fn new (dirs: Option<BaseDirectories>) -> Self {
|
|
let dirs = dirs.unwrap_or_else(||BaseDirectories::with_profile(Self::DIR, Self::SUB));
|
|
Self { dirs, ..Default::default() }
|
|
}
|
|
/// Find the config file.
|
|
fn find_file (&self) -> Option<PathBuf> {
|
|
self.dirs.find_config_file(Self::EDN)
|
|
}
|
|
/// Place config file in default location.
|
|
fn place_file (&self) -> Result<PathBuf, std::io::Error> {
|
|
self.dirs.place_config_file(Self::EDN)
|
|
}
|
|
/// Get path to config file.
|
|
fn get_file (&self) -> Option<PathBuf> {
|
|
self.dirs.get_config_file(Self::EDN)
|
|
}
|
|
/// Watch a config's file for changes.
|
|
pub fn watch_config (self: Arc<Self>, poll: Option<Duration>) -> Usually<PollWatcher> {
|
|
let poll = poll.unwrap_or(Duration::from_millis(250));
|
|
let opts = NotifyConfig::default().with_poll_interval(poll);
|
|
let call = self.clone().get_watch_config_callback();
|
|
let mut watcher = PollWatcher::new(call, opts)?;
|
|
if let Some(path) = self.get_file() {
|
|
watcher.watch(&path, RecursiveMode::NonRecursive)?;
|
|
Ok(watcher)
|
|
} else {
|
|
Err(format!("no config path").into())
|
|
}
|
|
}
|
|
fn get_watch_config_callback (self: Arc<Self>) -> impl Fn(Result<
|
|
notify_debouncer_full::notify::Event,
|
|
notify_debouncer_full::notify::Error,
|
|
>) {
|
|
let config = self.clone();
|
|
move|result|{
|
|
match result {
|
|
Ok(_events) => if let Err(e) = config_init(config.as_ref()) {
|
|
config.error_set(Some(format!("{e:?}").into()));
|
|
panic!("{e:?}");
|
|
} else {
|
|
//println!("config updated");
|
|
},
|
|
Err(errors) => {
|
|
panic!("{errors:?}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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<u7> = 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.tracks_jack_process(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::gear::*; mod gear {
|
|
use crate::*;
|
|
|
|
mod device; pub use self::device::*;
|
|
mod midi; pub use self::midi::*;
|
|
mod mixer; pub use self::mixer::*;
|
|
mod sample; pub use self::sample::*;
|
|
|
|
#[cfg(feature = "vst2")] mod vst2;
|
|
#[cfg(feature = "vst2")] pub use self::vst2::*;
|
|
|
|
#[cfg(feature = "lv2")] mod lv2;
|
|
#[cfg(feature = "lv2")] pub use self::lv2::*;
|
|
|
|
#[cfg(feature = "lv2_gui")] mod lv2_gui;
|
|
#[cfg(feature = "lv2_gui")] pub use self::lv2_gui::*;
|
|
|
|
pub struct Junction<T: JackPort>(T);
|
|
|
|
impl<T: JackPort> Draw<Tui> for Junction<T> {
|
|
fn draw (&self, to: &mut Tui) -> Drawn<u16> {
|
|
T::KIND.draw(to)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub use self::proj::*; mod proj {
|
|
use crate::*;
|
|
|
|
mod pool; pub use self::pool::*;
|
|
mod scene; pub use self::scene::*;
|
|
mod select; pub use self::select::*;
|
|
mod track; pub use self::track::*;
|
|
mod time; pub use self::time::*;
|
|
|
|
pub trait HasProject: AsRef<Option<Project>> + AsMut<Option<Project>> {}
|
|
|
|
/// Has a loaded project.
|
|
impl<T> HasProject for T where T: AsRef<Option<Project>> + AsMut<Option<Project>> {}
|
|
|
|
/// Arranger.
|
|
///
|
|
/// ```
|
|
/// let arranger = tek::Project::default();
|
|
/// ```
|
|
#[namespace(ClipPool ClipPool::eval)]
|
|
#[derive(Default, Debug)]
|
|
pub struct Project {
|
|
/// JACK client handle.
|
|
pub jack: Jack<'static>,
|
|
/// Project name.
|
|
pub name: Arc<str>,
|
|
/// Base color.
|
|
pub color: ItemTheme,
|
|
/// FIXME a render of the project arrangement, redrawn on update.
|
|
/// TODO rename to "render_cache" or smth
|
|
pub arranger: Arc<RwLock<Buffer>>,
|
|
/// Display size
|
|
pub size: Sizer,
|
|
/// Display size of clips area
|
|
pub size_inner: Sizer,
|
|
/// Selected UI element
|
|
pub selection: Selection,
|
|
/// Contains all recently created clips.
|
|
#[cfg(feature = "sequencer")] pub clips: Pool<MidiClip>,
|
|
/// Contains all recently created clips.
|
|
#[cfg(feature = "sampler")] pub samples: Pool<Sample>,
|
|
/// Source of time
|
|
#[cfg(feature = "clock")] pub clock: Clock,
|
|
/// Allows one MIDI clip to be edited
|
|
#[cfg(feature = "editor")] pub editor: Option<MidiEditor>,
|
|
/// List of global midi inputs
|
|
#[cfg(feature = "port")] pub midi_ins: Vec<MidiInput>,
|
|
/// List of global midi outputs
|
|
#[cfg(feature = "port")] pub midi_outs: Vec<MidiOutput>,
|
|
/// List of global audio inputs
|
|
#[cfg(feature = "port")] pub audio_ins: Vec<AudioInput>,
|
|
/// List of global audio outputs
|
|
#[cfg(feature = "port")] pub audio_outs: Vec<AudioOutput>,
|
|
/// Last track number (to avoid duplicate port names)
|
|
#[cfg(feature = "track")] pub track_last: usize,
|
|
/// List of tracks
|
|
#[cfg(feature = "track")] pub tracks: Vec<Track>,
|
|
/// Scroll offset of tracks
|
|
#[cfg(feature = "track")] pub track_scroll: usize,
|
|
/// List of scenes
|
|
#[cfg(feature = "scene")] pub scenes: Vec<Scene>,
|
|
/// Scroll offset of scenes
|
|
#[cfg(feature = "scene")] pub scenes_scroll: usize,
|
|
}
|
|
|
|
impl Project {
|
|
pub fn new (jack: &Jack<'static>) -> Self {
|
|
Self { jack: jack.clone(), ..Default::default() }
|
|
}
|
|
pub fn new_jack (name: impl AsRef<str>) -> Usually<Self> {
|
|
let name = name.as_ref();
|
|
Ok(Self { name: name.into(), jack: Jack::new(name)?, ..Default::default() })
|
|
}
|
|
/// Create a new arrangement.
|
|
pub fn create (
|
|
jack: &Jack<'static>,
|
|
name: Arc<str>,
|
|
clock: Clock,
|
|
tracks: impl Iterator<Item = Track>,
|
|
scenes: impl Iterator<Item = Scene>,
|
|
midi_ins: impl Iterator<Item = MidiInput>,
|
|
midi_outs: impl Iterator<Item = MidiOutput>,
|
|
audio_ins: impl Iterator<Item = AudioInput>,
|
|
audio_outs: impl Iterator<Item = AudioOutput>,
|
|
) -> Self {
|
|
Self {
|
|
jack: jack.clone(),
|
|
color: ItemTheme::random(),
|
|
selection: Selection::TrackClip { track: 0, scene: 0 },
|
|
tracks: tracks.collect(),
|
|
scenes: scenes.collect(),
|
|
midi_ins: midi_ins.collect(),
|
|
midi_outs: midi_outs.collect(),
|
|
audio_ins: audio_ins.collect(),
|
|
audio_outs: audio_outs.collect(),
|
|
size_inner: Sizer(Arc::new(40.into()), Arc::new(25.into())),
|
|
clock,
|
|
name,
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl HasJack<'static> for Project {
|
|
fn jack (&self) -> &Jack<'static> {
|
|
&self.jack
|
|
}
|
|
}
|
|
|
|
impl HasClip for Project {
|
|
fn clip_slot (&self) -> Option<Slot<MidiClip>> {
|
|
match self.selection {
|
|
Selection::Clip(index) =>
|
|
Some(self.clips.slot(index)),
|
|
Selection::TrackClip { track, scene } if let Some(scene) = self.scenes.get(scene) =>
|
|
Some(scene.clips.slot(track)),
|
|
_ =>
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
impl HasClipsSize for Project {
|
|
fn clips_size (&self) -> &Sizer {
|
|
&self.size_inner
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "cli")]
|
|
/// Command to launch into new project from CLI.
|
|
#[derive(Debug, Clone, Parser, Default)]
|
|
pub struct ProjectInit {
|
|
/// Name of JACK client
|
|
#[arg(short='n', long)] name: Option<String>,
|
|
/// Whether to attempt to become transport master
|
|
#[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<f64>,
|
|
/// 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<String>,
|
|
/// MIDI outs to connect to (multiple instances accepted)
|
|
#[arg(short='i', long)] midi_from_re: Vec<String>,
|
|
/// MIDI ins to connect to (multiple instances accepted)
|
|
#[arg(short='O', long)] midi_to: Vec<String>,
|
|
/// MIDI ins to connect to (multiple instances accepted)
|
|
#[arg(short='o', long)] midi_to_re: Vec<String>,
|
|
/// Audio outs to connect to left input
|
|
#[arg(short='l', long)] left_from: Vec<String>,
|
|
/// Audio outs to connect to right input
|
|
#[arg(short='r', long)] right_from: Vec<String>,
|
|
/// Audio ins to connect from left output
|
|
#[arg(short='L', long)] left_to: Vec<String>,
|
|
/// Audio ins to connect from right output
|
|
#[arg(short='R', long)] right_to: Vec<String>,
|
|
/// Tracks to creat
|
|
#[arg(short='t', long)] tracks: Option<usize>,
|
|
/// Scenes to create
|
|
#[arg(short='s', long)] scenes: Option<usize>,
|
|
}
|
|
|
|
#[cfg(feature = "cli")]
|
|
impl ProjectInit {
|
|
pub fn init (&self) -> Usually<Project> {
|
|
let name = self.name.as_ref().map_or("tek", |x|x.as_str());
|
|
let jack = Jack::new(&name)?;
|
|
let midi_ins = connect_midi_ins(&jack, &"M".to_string(), &self.midi_from, Some(&self.midi_from_re))?;
|
|
let midi_out = connect_midi_outs(&jack, &"M".to_string(), &self.midi_to, Some(&self.midi_to_re))?;
|
|
let inputs = [].into_iter()
|
|
.chain(connect_audio_ins(&jack, &"L".to_string(), &self.left_from, None)?.into_iter())
|
|
.chain(connect_audio_ins(&jack, &"R".to_string(), &self.right_from, None)?.into_iter());
|
|
let outputs = [].into_iter()
|
|
.chain(connect_audio_outs(&jack, &"L".to_string(), &self.left_to, None)?.into_iter())
|
|
.chain(connect_audio_outs(&jack, &"R".to_string(), &self.right_to, None)?.into_iter());
|
|
let mut proj = Project::create(&jack, name.into(), Clock::new(&jack, self.bpm)?,
|
|
[].into_iter(), [].into_iter(), midi_ins.into_iter(), midi_out.into_iter(),
|
|
inputs, outputs);
|
|
proj.tracks_add_many(self.tracks.unwrap_or(0), None, [].into(), [].into())?;
|
|
proj.scenes_add_many(self.scenes.unwrap_or(0))?;
|
|
Ok(proj)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
#[cfg(feature = "cli")] pub use self::cli::*;
|
|
#[cfg(feature = "cli")] mod cli {
|
|
use super::*;
|
|
|
|
/// Banner.
|
|
pub(crate) const HEADER: &'static str = r#"
|
|
~ █▀█▀█ █▀▀█ █ █ ~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~
|
|
█ █▀ █▀▀▄ ~ heatwave is the new darkwave ~
|
|
~ ▀ █▀▀█ ▀ ▀ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ "#;
|
|
|
|
pub fn show_version () {
|
|
println!("versions aint real man");
|
|
}
|
|
|
|
pub fn main_cli (config: Arc<Watch>) -> Usually<()> {
|
|
Cli::parse().run(Some(config.into()))
|
|
}
|
|
|
|
/// The command-line interface descriptor.
|
|
///
|
|
/// ```
|
|
/// let cli: tek::Cli = Default::default();
|
|
///
|
|
/// use clap::CommandFactory;
|
|
/// tek::Cli::command().debug_assert();
|
|
/// ```
|
|
#[derive(Parser, Debug, Default)]
|
|
#[command(name = "tek", version, about = Some(HEADER), long_about = Some(HEADER))]
|
|
pub struct Cli {
|
|
/// Pre-defined configuration modes.
|
|
///
|
|
/// TODO: Replace these with scripted configurations.
|
|
#[command(subcommand)]
|
|
pub action: Action,
|
|
|
|
/// Record data for performance flamegraph.
|
|
#[arg(long)]
|
|
trace: bool,
|
|
}
|
|
|
|
/// Command-line configuration.
|
|
impl Cli {
|
|
pub fn run (&self, mut config: Option<Arc<Watch>>) -> Usually<()> {
|
|
self.action.run(
|
|
config.unwrap_or_else(||Arc::new(Watch::new(None))),
|
|
self.trace
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Application modes that can be passed to the mommand line interface.
|
|
///
|
|
/// ```
|
|
/// let action: tek::Action = Default::default();
|
|
/// ```
|
|
#[derive(Debug, Clone, Subcommand, Default)]
|
|
pub enum Action {
|
|
/// Continue where you left off
|
|
#[default] Resume,
|
|
/// Run headlessly in current session.
|
|
Headless,
|
|
/// Show status of current session.
|
|
Status,
|
|
/// List known sessions.
|
|
List,
|
|
/// Continue work in a copy of the current session.
|
|
Fork,
|
|
/// Create a new empty session.
|
|
New(ProjectInit),
|
|
/// Import media as new session.
|
|
Import,
|
|
/// Show configuration.
|
|
Config,
|
|
/// Show version.
|
|
Version,
|
|
}
|
|
|
|
impl Action {
|
|
fn run (&self, watch: Arc<Watch>, _trace: bool) -> Usually<()> {
|
|
use Action::*;
|
|
match self {
|
|
Version => show_version(),
|
|
Config => watch.config_print(),
|
|
Resume => todo!("resume session"),
|
|
List => todo!("list sessions"),
|
|
New(sesh) => Exit::run(|exit|Tui::run_main(
|
|
exit.clone(),
|
|
{
|
|
#[allow(unused_mut)]
|
|
let mut app = App::new(Some(exit), sesh.init()?, watch, Some(vec![
|
|
"Menu".into()
|
|
]));
|
|
app.dialog = Dialog::welcome();
|
|
#[cfg(feature = "prof2")] {
|
|
if trace {
|
|
app.guard = Some({
|
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
|
//let tracy = tracing_tracy::TracyLayer::default();
|
|
let (flame, _guard) = tracing_flame::FlameLayer::with_file("./tracing.folded").unwrap();
|
|
//let registry = tracing_subscriber::registry().with(flame).init();
|
|
//tracing::subscriber::set_global_default(registry)?;
|
|
tracing::subscriber::set_global_default(tracing_subscriber::registry().with(flame))?;
|
|
_guard
|
|
})
|
|
}
|
|
}
|
|
Arc::new(RwLock::new(app))
|
|
})).map(|_|())?,
|
|
_ => todo!()
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub fn print_status (project: &Project) {
|
|
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
|
|
}
|
|
}
|
|
|
|
impl HasJack<'static> for App { fn jack (&self) -> &Jack<'static> { &self.jack } }
|
|
|
|
/// 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 {}
|
|
}
|
|
}
|
|
|
|
def_sizes_iter!(ScenesSizes => Scene);
|
|
def_sizes_iter!(TracksSizes => Track);
|
|
def_sizes_iter!(PortsSizes => Arc<str>, [Connect]);
|
|
|
|
impl_default!(Timebase: Self::new(48000f64, 150f64, DEFAULT_PPQ));
|
|
|
|
primitive!(isize: try_to_isize);
|
|
primitive!(u16: try_to_u16);
|
|
primitive!(u8: try_to_u8);
|
|
primitive!(usize: try_to_usize);
|
|
|
|
impl_as_ref!(Config: |self: Config| self);
|
|
impl_as_ref!(Config: |self: Watch| &self.config);
|
|
impl_as_mut!(Config: |self: Watch| &mut self.config);
|
|
impl_as_ref!(Watch: |self: Watch| &self);
|
|
|
|
impl_debug!(MenuItem: |self, w| { write!(w, "{}", &self.0) });
|
|
impl_default!(MenuItem: Self::new("", []));
|
|
|
|
impl_as_mut!(Option<MidiEditor>: |self: Project| &mut self.editor);
|
|
impl_as_mut_opt!(Scene: |self: Project| self.selected_scene_mut());
|
|
impl_as_mut_opt!(Track: |self: Project| self.selected_track_mut());
|
|
impl_as_ref!(Option<MidiEditor>: |self: Project| &self.editor);
|
|
impl_as_ref_opt!(Scene: |self: Project| self.selected_scene());
|
|
impl_as_ref_opt!(Track: |self: Project| self.selected_track());
|
|
impl_has!(Jack<'static>: |self: Project| self.jack);
|
|
impl_has!(Selection: |self: Project| self.selection);
|
|
impl_has!(Sizer: |self: Project| self.size);
|
|
impl_has!(Vec<MidiInput>: |self: Project| self.midi_ins);
|
|
impl_has!(Vec<MidiOutput>: |self: Project| self.midi_outs);
|
|
|
|
impl_as_ref!(Config: |self: App|self.watch.config.as_ref());
|
|
impl_as_mut!(Option<MidiEditor>: |self: App|self.project.as_mut());
|
|
impl_as_mut!(Option<Project>: |self: App|todo!());
|
|
impl_as_ref!(Option<MidiEditor>: |self: App|self.project.as_ref());
|
|
impl_as_ref!(Option<Project>: |self: App|todo!());
|
|
impl_as_mut_opt!(Scene: |self: App|self.project.as_mut_opt());
|
|
impl_as_mut_opt!(Track: |self: App|self.project.as_mut_opt());
|
|
impl_as_ref_opt!(Scene: |self: App|self.project.as_ref_opt());
|
|
impl_as_ref_opt!(Track: |self: App|self.project.as_ref_opt());
|
|
impl_audio!(App: tek_jack_process, tek_jack_event);
|
|
|
|
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_has!(Vec<MidiInput>: |self: App|self.project.midi_ins);
|
|
impl_has!(Vec<MidiOutput>: |self: App|self.project.midi_outs);
|
|
|
|
impl_from!(AppCmd: |x: BrowseCmd| AppCmd::Browse { command: x });
|
|
impl_from!(AppCmd: |x: MidiClipCmd| AppCmd::MidiClip { command: x });
|
|
impl_from!(AppCmd: |x: MidiEditCmd| AppCmd::MidiEdit { command: x });
|
|
impl_from!(AppCmd: |x: ProjectCmd| AppCmd::Project { command: x });
|
|
impl_from!(AppCmd: |x: SceneCmd| AppCmd::Scene { command: x });
|
|
impl_from!(AppCmd: |x: ScenesCmd| AppCmd::Scenes { command: x });
|
|
impl_from!(AppCmd: |x: TrackCmd| AppCmd::Track { command: x });
|
|
impl_from!(AppCmd: |x: TracksCmd| AppCmd::Tracks { command: x });
|
|
|
|
tui_keys!(self: App, input {
|
|
#[cfg(feature = "prof2")] profiling::scope!("App::tui_keys!");
|
|
self.dispatch_bind(input)
|
|
});
|
|
|
|
/// 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 Draw<Tui> for App {
|
|
fn draw (&self, to: &mut Tui) -> Drawn<u16> {
|
|
#[cfg(feature = "prof2")] profiling::scope!("App::draw");
|
|
self.perf.cycle(&mut |_|{
|
|
draw_error(to, self.error.as_ref())?;
|
|
draw_stack(to, &self)?;
|
|
view_debug(&*self.stack.read(), &self.perf, &self.size).draw(to)?;
|
|
#[cfg(feature = "prof2")] profiling::finish_frame!();
|
|
Ok(Some(to.area().into()))
|
|
})
|
|
}
|
|
}
|
|
|
|
pub trait DispatchBind<C: Clone>: HasBinds + HasStack + HasModes + Namespace<C> {
|
|
fn dispatch_bind (&mut self, input: &TuiEvent) -> Usually<()> {
|
|
if let Some(bindings) = self.dispatch_bind_match(input) {
|
|
return self.dispatch_bind_exec(bindings)
|
|
}
|
|
Ok(())
|
|
}
|
|
/// Find command bindings that match the ,nput.
|
|
/// ```
|
|
/// # use ::{std::sync::Arc, tek::*};
|
|
/// // Configure modes:
|
|
/// let app = config_reload(App::default(), stringify!(
|
|
/// (axis Y @up @down)
|
|
/// (bind Confirm (@enter confirm))
|
|
/// (mode Test (axis Y Index) (bind (@escape Cancel) Confirm))
|
|
/// )).unwrap();
|
|
/// println!("Modes: {:#?}", app.watch.config.modes);
|
|
///
|
|
/// // No mode is active:
|
|
/// assert!(app.dispatch_bind_match(&"@left".into()).is_none());
|
|
/// assert!(app.dispatch_bind_match(&"@enter".into()).is_none());
|
|
/// assert!(app.dispatch_bind_match(&"@escape".into()).is_none());
|
|
/// assert!(app.dispatch_bind_match(&"@down".into()).is_none());
|
|
/// assert!(app.dispatch_bind_match(&"@up".into()).is_none());
|
|
///
|
|
/// // Push mode onto stack:
|
|
/// app.stack_push("Test");
|
|
/// assert!(app.dispatch_bind_match(&"@left".into()).is_none());
|
|
/// assert!(app.dispatch_bind_match(&"@enter".into()).is_some());
|
|
/// assert!(app.dispatch_bind_match(&"@escape".into()).is_some());
|
|
/// assert!(app.dispatch_bind_match(&"@down".into()).is_some());
|
|
/// assert!(app.dispatch_bind_match(&"@up".into()).is_some());
|
|
/// ```
|
|
fn dispatch_bind_match (&self, input: &TuiEvent) -> Option<Arc<[Binding<Arc<str>>]>> {
|
|
for Frame { name, data } in self.stack().read().iter().rev() {
|
|
if let Some(mode) = self.modes_get(name).as_ref().map(Arc::clone) {
|
|
if let Some(binds) = self.dispatch_bind_match_axis(mode.as_ref(), input) {
|
|
return Some(binds)
|
|
}
|
|
if let Some(binds) = self.dispatch_bind_match_keys(mode.as_ref(), input) {
|
|
return Some(binds)
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
/// ```
|
|
/// # use ::{std::sync::Arc, tek::*};
|
|
/// let app = config_reload(App::default(), stringify!(
|
|
/// (axis Y @up @down) (mode Test (axis Y Index))
|
|
/// )).unwrap();
|
|
/// assert!(app.dispatch_bind_match(&"@down".into()).is_none());
|
|
/// assert!(app.dispatch_bind_match(&"@up".into()).is_none());
|
|
/// app.stack_push("Test");
|
|
/// assert!(app.dispatch_bind_match(&"@down".into()).is_some());
|
|
/// assert!(app.dispatch_bind_match(&"@up".into()).is_some());
|
|
/// ```
|
|
fn dispatch_bind_match_axis (&self, mode: &Mode, input: &TuiEvent)
|
|
-> Option<Arc<[Binding<Arc<str>>]>>
|
|
{
|
|
let axis = self.binds_axis().read();
|
|
for (name, _param) in mode.axis.iter() {
|
|
if let Some(axis) = axis.get(name) {
|
|
if *input == axis.0 {
|
|
return Some([Binding::new([format!("(dec {name})").into()].into())].into())
|
|
} else if *input == axis.1 {
|
|
return Some([Binding::new([format!("(inc {name})").into()].into())].into())
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
/// ```
|
|
/// # use ::{std::sync::Arc, tek::*};
|
|
/// let app = config_reload(App::default(), stringify!(
|
|
/// (bind Confirm (@enter confirm)) (mode Test (bind Confirm (@escape Cancel)))
|
|
/// )).unwrap();
|
|
/// assert!(app.dispatch_bind_match(&"@enter".into()).is_none());
|
|
/// assert!(app.dispatch_bind_match(&"@escape".into()).is_none());
|
|
/// app.stack_push("Test");
|
|
/// assert!(app.dispatch_bind_match(&"@enter".into()).is_some());
|
|
/// assert!(app.dispatch_bind_match(&"@escape".into()).is_some());
|
|
/// ```
|
|
fn dispatch_bind_match_keys (&self, mode: &Mode, input: &TuiEvent)
|
|
-> Option<Arc<[Binding<Arc<str>>]>>
|
|
{
|
|
for id in mode.keys.iter() {
|
|
if let Some(event_map) = self.binds_find(id)
|
|
&& let Some(bindings) = event_map.query(input) {
|
|
return Some(bindings.into())
|
|
}
|
|
|
|
if let Ok(Some(expr)) = id.expr()
|
|
&& let Ok(Some(head)) = expr.head()
|
|
&& *input == head.into()
|
|
&& let Ok(Some(tail)) = expr.tail() {
|
|
return Some([Binding::new([tail.into()].into())].into())
|
|
}
|
|
}
|
|
None
|
|
}
|
|
fn dispatch_bind_exec (&mut self, bindings: Arc<[Binding<Arc<str>>]>) -> Usually<()> {
|
|
for binding in bindings.iter() {
|
|
for src in binding.commands.iter() {
|
|
if let Some(command) = Namespace::<C>::namespace(self, &src)? {
|
|
self.cmd_dispatch(command)?;
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
fn cmd_dispatch (&mut self, command: C) -> Usually<()>;
|
|
}
|
|
|
|
impl DispatchBind<AppCmd> for App {
|
|
fn cmd_dispatch (&mut self, command: AppCmd) -> Usually<()> {
|
|
match command.clone().dispatch(self) {
|
|
Ok(undo) => { self.history.push((command, undo)); Ok(()) },
|
|
Err(err) => { self.history.push((command, None)); Err(err) },
|
|
}
|
|
}
|
|
}
|
|
|
|
/// An item of a menu.
|
|
///
|
|
/// ```
|
|
/// let item: tek::MenuItem = Default::default();
|
|
/// ```
|
|
#[derive(Clone)] pub struct MenuItem(pub Arc<str>, pub Arc<[AppCmd]>);
|
|
|
|
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))
|
|
}
|
|
}
|
|
|
|
/// 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 {
|
|
|
|
/// ```
|
|
/// 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()
|
|
) }
|
|
]),
|
|
|
|
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!() }
|
|
|
|
}
|
|
|
|
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 }
|
|
}
|
|
}
|
|
|
|
/// 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,
|
|
}
|
|
|
|
#[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>>>),
|
|
}
|
|
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))
|
|
}
|