diff --git a/Cargo.lock b/Cargo.lock index 976237da..538615d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,55 +23,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "anstream" -version = "0.6.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" - -[[package]] -name = "anstyle-parse" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" -dependencies = [ - "anstyle", - "windows-sys 0.59.0", -] - [[package]] name = "approx" version = "0.5.1" @@ -181,46 +132,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "clap" -version = "4.5.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" - [[package]] name = "clojure-reader" version = "0.3.0" @@ -230,12 +141,6 @@ dependencies = [ "ordered-float", ] -[[package]] -name = "colorchoice" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" - [[package]] name = "compact_str" version = "0.8.1" @@ -509,12 +414,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "is_terminal_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" - [[package]] name = "itertools" version = "0.13.0" @@ -1409,9 +1308,7 @@ name = "tek" version = "0.2.0" dependencies = [ "backtrace", - "clap", "livi", - "once_cell", "palette", "rand", "symphonia", @@ -1590,12 +1487,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - [[package]] name = "uuid" version = "1.11.0" diff --git a/Cargo.toml b/Cargo.toml index 1ecadb51..964bb9db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,14 +10,13 @@ tek_time = { path = "./time" } tek_midi = { path = "./midi" } backtrace = "0.3.72" -clap = { version = "4.5.4", features = [ "derive" ] } livi = "0.7.4" -once_cell = "1.19.0" palette = { version = "0.7.6", features = [ "random" ] } rand = "0.8.5" symphonia = { version = "0.5.4", features = [ "all" ] } toml = "0.8.12" wavers = "1.4.3" +#once_cell = "1.19.0" #no_deadlocks = "1.3.2" #suil-rs = { path = "../suil" } #vst = "0.4.0" @@ -27,44 +26,5 @@ wavers = "1.4.3" [features] default = [] -[[bin]] -name = "tek" -path = "bin/tek.rs" - -#[[bin]] -#name = "tek_arranger" -#path = "bin/cli_arranger.rs" - -#[[bin]] -#name = "tek_sequencer" -#path = "bin/cli_sequencer.rs" - -#[[bin]] -#name = "tek_groovebox" -#path = "bin/cli_groovebox.rs" - -#[[bin]] -#name = "tek_clock" -#path = "bin/cli_clock.rs" - -#[[bin]] -#name = "tek_sampler" -#path = "bin/cli_sampler.rs" - -#[[bin]] -#name = "tek_mixer" -#path = "src/cli_mixer.rs" - -#[[bin]] -#name = "tek_track" -#path = "src/cli_track.rs" - -#[[bin]] -#name = "tek_plugin" -#path = "src/cli_plugin.rs" - -[lib] -path = "src/lib.rs" - [profile.release] lto = true diff --git a/Justfile b/Justfile index 07a23259..0a8ec6c5 100644 --- a/Justfile +++ b/Justfile @@ -132,4 +132,4 @@ test: cargo test cloc: - for src in {edn,input,jack,midi,output,time,tui,.}; do echo $src; cloc $src/src; done + for src in {cli/src,edn/src,examples,input/src,jack/src,midi/src,output/src,time/src,tui/src,src}; do echo $src; cloc $src; done diff --git a/cli/Cargo.toml b/cli/Cargo.toml new file mode 100644 index 00000000..5d198b47 --- /dev/null +++ b/cli/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "tek_cli" +edition = "2021" +version = "0.2.0" + +[dependencies] +tek = { path = ".." } +clap = { version = "4.5.4", features = [ "derive" ] } diff --git a/bin/tek.rs b/cli/src/main.rs similarity index 100% rename from bin/tek.rs rename to cli/src/main.rs diff --git a/bin/todo_cli_mixer.rs b/cli/src/todo_cli_mixer.rs similarity index 100% rename from bin/todo_cli_mixer.rs rename to cli/src/todo_cli_mixer.rs diff --git a/bin/todo_cli_plugin.rs b/cli/src/todo_cli_plugin.rs similarity index 100% rename from bin/todo_cli_plugin.rs rename to cli/src/todo_cli_plugin.rs diff --git a/bin/todo_cli_sampler.rs b/cli/src/todo_cli_sampler.rs similarity index 100% rename from bin/todo_cli_sampler.rs rename to cli/src/todo_cli_sampler.rs diff --git a/examples/midi_import.rs.fixme b/examples/midi_import.rs.fixme index 658ca4eb..92ce4cb7 100644 --- a/examples/midi_import.rs.fixme +++ b/examples/midi_import.rs.fixme @@ -13,6 +13,6 @@ impl HasPhrases for ExamplePhrases { fn main () -> Usually<()> { let mut phrases = ExamplePhrases(vec![]); - PhrasePoolCommand::Import(0, String::from("./example.mid")).execute(&mut phrases)?; + MidiPoolCommand::Import(0, String::from("./example.mid")).execute(&mut phrases)?; Ok(()) } diff --git a/midi/Cargo.toml b/midi/Cargo.toml index 7ed369a4..9bcd47c4 100644 --- a/midi/Cargo.toml +++ b/midi/Cargo.toml @@ -11,3 +11,35 @@ tek_time = { path = "../time" } jack = { path = "../rust-jack" } midly = "0.5" uuid = { version = "1.10.0", features = [ "v4" ] } + +#[[bin]] +#name = "tek_arranger" +#path = "bin/cli_arranger.rs" + +#[[bin]] +#name = "tek_sequencer" +#path = "bin/cli_sequencer.rs" + +#[[bin]] +#name = "tek_groovebox" +#path = "bin/cli_groovebox.rs" + +#[[bin]] +#name = "tek_clock" +#path = "bin/cli_clock.rs" + +#[[bin]] +#name = "tek_sampler" +#path = "bin/cli_sampler.rs" + +#[[bin]] +#name = "tek_mixer" +#path = "src/cli_mixer.rs" + +#[[bin]] +#name = "tek_track" +#path = "src/cli_track.rs" + +#[[bin]] +#name = "tek_plugin" +#path = "src/cli_plugin.rs" diff --git a/midi/src/lib.rs b/midi/src/lib.rs index ffa7a50f..106b7a21 100644 --- a/midi/src/lib.rs +++ b/midi/src/lib.rs @@ -1,22 +1,22 @@ -mod midi_pool; pub(crate) use midi_pool::*; -mod midi_clip; pub(crate) use midi_clip::*; -mod midi_launch; pub(crate) use midi_launch::*; -mod midi_player; pub(crate) use midi_player::*; -mod midi_in; pub(crate) use midi_in::*; -mod midi_out; pub(crate) use midi_out::*; +mod midi_pool; pub use midi_pool::*; +mod midi_clip; pub use midi_clip::*; +mod midi_launch; pub use midi_launch::*; +mod midi_player; pub use midi_player::*; +mod midi_in; pub use midi_in::*; +mod midi_out; pub use midi_out::*; -mod midi_pitch; pub(crate) use midi_pitch::*; -mod midi_range; pub(crate) use midi_range::*; -mod midi_point; pub(crate) use midi_point::*; -mod midi_view; pub(crate) use midi_view::*; -mod midi_editor; pub(crate) use midi_editor::*; -mod midi_select; pub(crate) use midi_select::*; +mod midi_pitch; pub use midi_pitch::*; +mod midi_range; pub use midi_range::*; +mod midi_point; pub use midi_point::*; +mod midi_view; pub use midi_view::*; +mod midi_editor; pub use midi_editor::*; +mod midi_select; pub use midi_select::*; -mod piano_h; pub(crate) use self::piano_h::*; -mod piano_h_cursor; pub(crate) use self::piano_h_cursor::*; -mod piano_h_keys; pub(crate) use self::piano_h_keys::*; -mod piano_h_notes; pub(crate) use self::piano_h_notes::*; -mod piano_h_time; pub(crate) use self::piano_h_time::*; +mod piano_h; pub use self::piano_h::*; +mod piano_h_cursor; pub use self::piano_h_cursor::*; +mod piano_h_keys; pub use self::piano_h_keys::*; +mod piano_h_notes; pub use self::piano_h_notes::*; +mod piano_h_time; pub use self::piano_h_time::*; pub(crate) use ::tek_time::*; pub(crate) use ::tek_jack::{*, jack::*}; diff --git a/midi/src/midi_player.rs b/midi/src/midi_player.rs index 10ff95ed..0260d50a 100644 --- a/midi/src/midi_player.rs +++ b/midi/src/midi_player.rs @@ -21,29 +21,29 @@ pub trait HasPlayer { /// Contains state for playing a phrase pub struct MidiPlayer { /// State of clock and playhead - pub(crate) clock: Clock, + pub clock: Clock, /// Start time and phrase being played - pub(crate) play_phrase: Option<(Moment, Option>>)>, + pub play_phrase: Option<(Moment, Option>>)>, /// Start time and next phrase - pub(crate) next_phrase: Option<(Moment, Option>>)>, + pub next_phrase: Option<(Moment, Option>>)>, /// Play input through output. - pub(crate) monitoring: bool, + pub monitoring: bool, /// Write input to sequence. - pub(crate) recording: bool, + pub recording: bool, /// Overdub input to sequence. - pub(crate) overdub: bool, + pub overdub: bool, /// Send all notes off - pub(crate) reset: bool, // TODO?: after Some(nframes) + pub reset: bool, // TODO?: after Some(nframes) /// Record from MIDI ports to current sequence. - pub midi_ins: Vec>, + pub midi_ins: Vec>, /// Play from current sequence to MIDI ports - pub midi_outs: Vec>, + pub midi_outs: Vec>, /// Notes currently held at input - pub(crate) notes_in: Arc>, + pub notes_in: Arc>, /// Notes currently held at output - pub(crate) notes_out: Arc>, + pub notes_out: Arc>, /// MIDI output buffer - pub note_buf: Vec, + pub note_buf: Vec, } impl MidiPlayer { pub fn new ( diff --git a/midi/src/midi_pool.rs b/midi/src/midi_pool.rs index 23a04833..3fdd5c23 100644 --- a/midi/src/midi_pool.rs +++ b/midi/src/midi_pool.rs @@ -15,7 +15,7 @@ pub trait HasPhrases { } #[derive(Clone, Debug, PartialEq)] -pub enum PhrasePoolCommand { +pub enum MidiPoolCommand { Add(usize, MidiClip), Delete(usize), Swap(usize, usize), @@ -26,9 +26,9 @@ pub enum PhrasePoolCommand { SetColor(usize, ItemColor), } -impl Command for PhrasePoolCommand { +impl Command for MidiPoolCommand { fn execute (self, model: &mut T) -> Perhaps { - use PhrasePoolCommand::*; + use MidiPoolCommand::*; Ok(match self { Add(mut index, phrase) => { let phrase = Arc::new(RwLock::new(phrase)); diff --git a/src/arranger/arranger_command.rs b/src/arranger/arranger_command.rs index 69b49429..06672ccb 100644 --- a/src/arranger/arranger_command.rs +++ b/src/arranger/arranger_command.rs @@ -185,7 +185,7 @@ command!(|self: ArrangerCommand, state: Arranger|match self { undo }, // reload phrase in editor to update color - PoolCommand::Phrase(PhrasePoolCommand::SetColor(index, _)) => { + PoolCommand::Phrase(MidiPoolCommand::SetColor(index, _)) => { let undo = cmd.delegate(&mut state.pool, Self::Phrases)?; state.editor.set_phrase(Some(state.pool.phrase())); undo diff --git a/src/file.rs b/src/file.rs deleted file mode 100644 index 2c1a0154..00000000 --- a/src/file.rs +++ /dev/null @@ -1,151 +0,0 @@ -use crate::*; -/// Browses for phrase to import/export -#[derive(Debug, Clone)] -pub struct FileBrowser { - pub cwd: PathBuf, - pub dirs: Vec<(OsString, String)>, - pub files: Vec<(OsString, String)>, - pub filter: String, - pub index: usize, - pub scroll: usize, - pub size: Measure -} -/// Commands supported by [FileBrowser] -#[derive(Debug, Clone, PartialEq)] -pub enum FileBrowserCommand { - Begin, - Cancel, - Confirm, - Select(usize), - Chdir(PathBuf), - Filter(Arc), -} -render!(TuiOut: (self: FileBrowser) => /*Stack::down(|add|{ - let mut i = 0; - for (_, name) in self.dirs.iter() { - if i >= self.scroll { - add(&Tui::bold(i == self.index, name.as_str()))?; - } - i += 1; - } - for (_, name) in self.files.iter() { - if i >= self.scroll { - add(&Tui::bold(i == self.index, name.as_str()))?; - } - i += 1; - } - add(&format!("{}/{i}", self.index))?; - Ok(()) -})*/"todo"); -impl FileBrowser { - 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, - filter: "".to_string(), - index: 0, - scroll: 0, - size: Measure::new(), - }) - } - 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!() - }) - } - pub fn chdir (&self) -> Usually { - Self::new(Some(self.path())) - } -} -command!(|self: FileBrowserCommand, state: PoolModel|{ - use PoolMode::*; - use FileBrowserCommand::*; - let mode = &mut state.mode; - match mode { - Some(Import(index, ref mut browser)) => match self { - Cancel => { *mode = None; }, - Chdir(cwd) => { *mode = Some(Import(*index, FileBrowser::new(Some(cwd))?)); }, - Select(index) => { browser.index = index; }, - Confirm => if browser.is_file() { - let index = *index; - let path = browser.path(); - *mode = None; - PhrasePoolCommand::Import(index, path).execute(state)?; - } else if browser.is_dir() { - *mode = Some(Import(*index, browser.chdir()?)); - }, - _ => todo!(), - }, - Some(Export(index, ref mut browser)) => match self { - Cancel => { *mode = None; }, - Chdir(cwd) => { *mode = Some(Export(*index, FileBrowser::new(Some(cwd))?)); }, - Select(index) => { browser.index = index; }, - _ => unreachable!() - }, - _ => unreachable!(), - }; - None -}); -input_to_command!(FileBrowserCommand: |state: PoolModel, input: Event|{ - use FileBrowserCommand::*; - use KeyCode::{Up, Down, Left, Right, Enter, Esc, Backspace, Char}; - if let Some(PoolMode::Import(_index, browser)) = &state.mode { - match input { - kpat!(Up) => Select(browser.index.overflowing_sub(1).0 - .min(browser.len().saturating_sub(1))), - kpat!(Down) => Select(browser.index.saturating_add(1) - % browser.len()), - kpat!(Right) => Chdir(browser.cwd.clone()), - kpat!(Left) => Chdir(browser.cwd.clone()), - kpat!(Enter) => Confirm, - kpat!(Char(_)) => { todo!() }, - kpat!(Backspace) => { todo!() }, - kpat!(Esc) => Cancel, - _ => return None - } - } else if let Some(PoolMode::Export(_index, browser)) = &state.mode { - match input { - kpat!(Up) => Select(browser.index.overflowing_sub(1).0 - .min(browser.len())), - kpat!(Down) => Select(browser.index.saturating_add(1) - % browser.len()), - kpat!(Right) => Chdir(browser.cwd.clone()), - kpat!(Left) => Chdir(browser.cwd.clone()), - kpat!(Enter) => Confirm, - kpat!(Char(_)) => { todo!() }, - kpat!(Backspace) => { todo!() }, - kpat!(Esc) => Cancel, - _ => return None - } - } else { - unreachable!() - } -}); diff --git a/src/groovebox.rs b/src/groovebox.rs index ff386907..026ea606 100644 --- a/src/groovebox.rs +++ b/src/groovebox.rs @@ -7,7 +7,7 @@ use KeyCode::{Char, Delete, Tab, Up, Down, Left, Right}; use ClockCommand::{Play, Pause}; use GrooveboxCommand as Cmd; use MidiEditCommand::*; -use PhrasePoolCommand::*; +use MidiPoolCommand::*; pub struct Groovebox { _jack: Arc>, diff --git a/src/lib.rs b/src/lib.rs index fa7ab3fb..85bd11f4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,8 +6,6 @@ #![feature(associated_type_defaults)] pub mod arranger; pub use self::arranger::*; -pub mod file; pub use self::file::*; -pub mod focus; pub use self::focus::*; pub mod groovebox; pub use self::groovebox::*; pub mod meter; pub use self::meter::*; pub mod mixer; pub use self::mixer::*; @@ -42,16 +40,16 @@ pub(crate) use ::tek_tui::{ pub(crate) use std::cmp::{Ord, Eq, PartialEq}; pub(crate) use std::collections::BTreeMap; pub(crate) use std::error::Error; -pub(crate) use std::ffi::OsString; pub(crate) use std::fmt::{Debug, Display, Formatter}; pub(crate) use std::io::{Stdout, stdout}; pub(crate) use std::marker::PhantomData; pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem}; -pub(crate) use std::path::PathBuf; pub(crate) use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::{self, *}}; pub(crate) use std::sync::{Arc, Mutex, RwLock}; pub(crate) use std::thread::{spawn, JoinHandle}; pub(crate) use std::time::Duration; +pub(crate) use std::path::PathBuf; +pub(crate) use std::ffi::OsString; //#[cfg(test)] mod test_focus { //use super::focus::*; diff --git a/src/piano.rs b/src/piano.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/src/pool.rs b/src/pool.rs index 2a885762..8a62d3e8 100644 --- a/src/pool.rs +++ b/src/pool.rs @@ -37,7 +37,7 @@ pub enum PoolMode { pub enum PoolCommand { Show(bool), /// Update the contents of the phrase pool - Phrase(PhrasePoolCommand), + Phrase(MidiPoolCommand), /// Select a phrase from the phrase pool Select(usize), /// Rename a phrase @@ -121,7 +121,7 @@ fn to_phrases_command (state: &PoolModel, input: &Event) -> Option kpat!(Char('t')) => Cmd::Length(PhraseLengthCommand::Begin), kpat!(Char('m')) => Cmd::Import(FileBrowserCommand::Begin), kpat!(Char('x')) => Cmd::Export(FileBrowserCommand::Begin), - kpat!(Char('c')) => Cmd::Phrase(PhrasePoolCommand::SetColor(index, ItemColor::random())), + kpat!(Char('c')) => Cmd::Phrase(MidiPoolCommand::SetColor(index, ItemColor::random())), kpat!(Char('[')) | kpat!(Up) => Cmd::Select( index.overflowing_sub(1).0.min(state.phrases().len() - 1) ), @@ -130,32 +130,32 @@ fn to_phrases_command (state: &PoolModel, input: &Event) -> Option ), kpat!(Char('<')) => if index > 1 { state.set_phrase_index(state.phrase_index().saturating_sub(1)); - Cmd::Phrase(PhrasePoolCommand::Swap(index - 1, index)) + Cmd::Phrase(MidiPoolCommand::Swap(index - 1, index)) } else { return None }, kpat!(Char('>')) => if index < count.saturating_sub(1) { state.set_phrase_index(state.phrase_index() + 1); - Cmd::Phrase(PhrasePoolCommand::Swap(index + 1, index)) + Cmd::Phrase(MidiPoolCommand::Swap(index + 1, index)) } else { return None }, kpat!(Delete) => if index > 0 { state.set_phrase_index(index.min(count.saturating_sub(1))); - Cmd::Phrase(PhrasePoolCommand::Delete(index)) + Cmd::Phrase(MidiPoolCommand::Delete(index)) } else { return None }, - kpat!(Char('a')) | kpat!(Shift-Char('A')) => Cmd::Phrase(PhrasePoolCommand::Add(count, MidiClip::new( + kpat!(Char('a')) | kpat!(Shift-Char('A')) => Cmd::Phrase(MidiPoolCommand::Add(count, MidiClip::new( "Clip", true, 4 * PPQ, None, Some(ItemPalette::random()) ))), - kpat!(Char('i')) => Cmd::Phrase(PhrasePoolCommand::Add(index + 1, MidiClip::new( + kpat!(Char('i')) => Cmd::Phrase(MidiPoolCommand::Add(index + 1, MidiClip::new( "Clip", true, 4 * PPQ, None, Some(ItemPalette::random()) ))), kpat!(Char('d')) | kpat!(Shift-Char('D')) => { let mut phrase = state.phrases()[index].read().unwrap().duplicate(); phrase.color = ItemPalette::random_near(phrase.color, 0.25); - Cmd::Phrase(PhrasePoolCommand::Add(index + 1, phrase)) + Cmd::Phrase(MidiPoolCommand::Add(index + 1, phrase)) }, _ => return None }) @@ -201,3 +201,67 @@ impl PoolModel { } } } +command!(|self: FileBrowserCommand, state: PoolModel|{ + use PoolMode::*; + use FileBrowserCommand::*; + let mode = &mut state.mode; + match mode { + Some(Import(index, ref mut browser)) => match self { + Cancel => { *mode = None; }, + Chdir(cwd) => { *mode = Some(Import(*index, FileBrowser::new(Some(cwd))?)); }, + Select(index) => { browser.index = index; }, + Confirm => if browser.is_file() { + let index = *index; + let path = browser.path(); + *mode = None; + MidiPoolCommand::Import(index, path).execute(state)?; + } else if browser.is_dir() { + *mode = Some(Import(*index, browser.chdir()?)); + }, + _ => todo!(), + }, + Some(Export(index, ref mut browser)) => match self { + Cancel => { *mode = None; }, + Chdir(cwd) => { *mode = Some(Export(*index, FileBrowser::new(Some(cwd))?)); }, + Select(index) => { browser.index = index; }, + _ => unreachable!() + }, + _ => unreachable!(), + }; + None +}); +input_to_command!(FileBrowserCommand: |state: PoolModel, input: Event|{ + use FileBrowserCommand::*; + use KeyCode::{Up, Down, Left, Right, Enter, Esc, Backspace, Char}; + if let Some(PoolMode::Import(_index, browser)) = &state.mode { + match input { + kpat!(Up) => Select(browser.index.overflowing_sub(1).0 + .min(browser.len().saturating_sub(1))), + kpat!(Down) => Select(browser.index.saturating_add(1) + % browser.len()), + kpat!(Right) => Chdir(browser.cwd.clone()), + kpat!(Left) => Chdir(browser.cwd.clone()), + kpat!(Enter) => Confirm, + kpat!(Char(_)) => { todo!() }, + kpat!(Backspace) => { todo!() }, + kpat!(Esc) => Cancel, + _ => return None + } + } else if let Some(PoolMode::Export(_index, browser)) = &state.mode { + match input { + kpat!(Up) => Select(browser.index.overflowing_sub(1).0 + .min(browser.len())), + kpat!(Down) => Select(browser.index.saturating_add(1) + % browser.len()), + kpat!(Right) => Chdir(browser.cwd.clone()), + kpat!(Left) => Chdir(browser.cwd.clone()), + kpat!(Enter) => Confirm, + kpat!(Char(_)) => { todo!() }, + kpat!(Backspace) => { todo!() }, + kpat!(Esc) => Cancel, + _ => return None + } + } else { + unreachable!() + } +}); diff --git a/src/sequencer.rs b/src/sequencer.rs index 87c53cca..e284ec7b 100644 --- a/src/sequencer.rs +++ b/src/sequencer.rs @@ -3,7 +3,7 @@ use ClockCommand::{Play, Pause}; use KeyCode::{Tab, Char}; use SequencerCommand as Cmd; use MidiEditCommand::*; -use PhrasePoolCommand::*; +use MidiPoolCommand::*; /// Root view for standalone `tek_sequencer`. pub struct Sequencer { pub _jack: Arc>, @@ -57,6 +57,25 @@ impl Sequencer { let pool = Pull::y(1, Fill::y(Align::e(PoolView(self.pool.visible, &self.pool)))); Fixed::x(pool_w, Align::e(Fill::y(PoolView(self.compact, &self.pool)))) } + fn help () -> impl Content { + let single = |binding, command|row!(" ", col!( + Tui::fg(TuiTheme::yellow(), binding), + command + )); + let double = |(b1, c1), (b2, c2)|col!( + row!(" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,), + row!(" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,), + ); + Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!( + single("SPACE", "play/pause"), + double(("β–²β–Όβ–Άβ—€", "cursor"), ("Ctrl", "scroll"), ), + double(("a", "append"), ("s", "set note"),), + double((",.", "length"), ("<>", "triplet"), ), + double(("[]", "phrase"), ("{}", "order"), ), + double(("q", "enqueue"), ("e", "edit"), ), + double(("c", "color"), ("", ""),), + )) + } } audio!(|self:Sequencer, client, scope|{ // Start profiling cycle @@ -178,29 +197,10 @@ from!(|state:&Sequencer|SequencerStatus = { } }); render!(TuiOut: (self: SequencerStatus) => Fixed::y(2, lay!( - Self::help(), + Sequencer::help(), Fill::xy(Align::se(Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), self.stats()))), ))); impl SequencerStatus { - fn help () -> impl Content { - let single = |binding, command|row!(" ", col!( - Tui::fg(TuiTheme::yellow(), binding), - command - )); - let double = |(b1, c1), (b2, c2)|col!( - row!(" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,), - row!(" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,), - ); - Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!( - single("SPACE", "play/pause"), - double(("β–²β–Όβ–Άβ—€", "cursor"), ("Ctrl", "scroll"), ), - double(("a", "append"), ("s", "set note"),), - double((",.", "length"), ("<>", "triplet"), ), - double(("[]", "phrase"), ("{}", "order"), ), - double(("q", "enqueue"), ("e", "edit"), ), - double(("c", "color"), ("", ""),), - )) - } fn stats (&self) -> impl Content + use<'_> { row!(&self.cpu, &self.size) } diff --git a/tui/src/lib.rs b/tui/src/lib.rs index 26e77fdc..19e77380 100644 --- a/tui/src/lib.rs +++ b/tui/src/lib.rs @@ -19,10 +19,13 @@ mod tui_theme; pub use self::tui_theme::*; mod tui_border; pub use self::tui_border::*; mod tui_field; pub use self::tui_field::*; mod tui_buffer; pub use self::tui_buffer::*; +mod tui_file; pub use self::tui_file::*; pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, AtomicBool, Ordering::*}}; pub(crate) use std::io::{stdout, Stdout}; pub(crate) use std::error::Error; +pub(crate) use std::path::PathBuf; +pub(crate) use std::ffi::OsString; /// Standard result type. pub type Usually = Result>; diff --git a/tui/src/tui_file.rs b/tui/src/tui_file.rs new file mode 100644 index 00000000..743897a8 --- /dev/null +++ b/tui/src/tui_file.rs @@ -0,0 +1,87 @@ +use crate::*; +/// Browses for phrase to import/export +#[derive(Debug, Clone)] +pub struct FileBrowser { + pub cwd: PathBuf, + pub dirs: Vec<(OsString, String)>, + pub files: Vec<(OsString, String)>, + pub filter: String, + pub index: usize, + pub scroll: usize, + pub size: Measure +} +/// Commands supported by [FileBrowser] +#[derive(Debug, Clone, PartialEq)] +pub enum FileBrowserCommand { + Begin, + Cancel, + Confirm, + Select(usize), + Chdir(PathBuf), + Filter(Arc), +} +render!(TuiOut: (self: FileBrowser) => /*Stack::down(|add|{ + let mut i = 0; + for (_, name) in self.dirs.iter() { + if i >= self.scroll { + add(&Tui::bold(i == self.index, name.as_str()))?; + } + i += 1; + } + for (_, name) in self.files.iter() { + if i >= self.scroll { + add(&Tui::bold(i == self.index, name.as_str()))?; + } + i += 1; + } + add(&format!("{}/{i}", self.index))?; + Ok(()) +})*/"todo"); +impl FileBrowser { + 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, + filter: "".to_string(), + index: 0, + scroll: 0, + size: Measure::new(), + }) + } + 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!() + }) + } + pub fn chdir (&self) -> Usually { + Self::new(Some(self.path())) + } +} diff --git a/src/focus.rs b/tui/src/tui_focus.rs similarity index 100% rename from src/focus.rs rename to tui/src/tui_focus.rs diff --git a/src/menu.rs b/tui/src/tui_menu.rs similarity index 100% rename from src/menu.rs rename to tui/src/tui_menu.rs