remodularize 3

This commit is contained in:
🪞👃🪞 2025-01-08 19:40:06 +01:00
parent d38dc14e84
commit 113e7b0bad
24 changed files with 262 additions and 370 deletions

109
Cargo.lock generated
View file

@ -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"

View file

@ -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

View file

@ -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

8
cli/Cargo.toml Normal file
View file

@ -0,0 +1,8 @@
[package]
name = "tek_cli"
edition = "2021"
version = "0.2.0"
[dependencies]
tek = { path = ".." }
clap = { version = "4.5.4", features = [ "derive" ] }

View file

@ -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(())
}

View file

@ -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"

View file

@ -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::*};

View file

@ -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<Arc<RwLock<MidiClip>>>)>,
pub play_phrase: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
/// Start time and next phrase
pub(crate) next_phrase: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
pub next_phrase: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
/// 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<Port<MidiIn>>,
pub midi_ins: Vec<Port<MidiIn>>,
/// Play from current sequence to MIDI ports
pub midi_outs: Vec<Port<MidiOut>>,
pub midi_outs: Vec<Port<MidiOut>>,
/// Notes currently held at input
pub(crate) notes_in: Arc<RwLock<[bool; 128]>>,
pub notes_in: Arc<RwLock<[bool; 128]>>,
/// Notes currently held at output
pub(crate) notes_out: Arc<RwLock<[bool; 128]>>,
pub notes_out: Arc<RwLock<[bool; 128]>>,
/// MIDI output buffer
pub note_buf: Vec<u8>,
pub note_buf: Vec<u8>,
}
impl MidiPlayer {
pub fn new (

View file

@ -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<T: HasPhrases> Command<T> for PhrasePoolCommand {
impl<T: HasPhrases> Command<T> for MidiPoolCommand {
fn execute (self, model: &mut T) -> Perhaps<Self> {
use PhrasePoolCommand::*;
use MidiPoolCommand::*;
Ok(match self {
Add(mut index, phrase) => {
let phrase = Arc::new(RwLock::new(phrase));

View file

@ -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

View file

@ -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<TuiOut>
}
/// Commands supported by [FileBrowser]
#[derive(Debug, Clone, PartialEq)]
pub enum FileBrowserCommand {
Begin,
Cancel,
Confirm,
Select(usize),
Chdir(PathBuf),
Filter(Arc<str>),
}
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<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,
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> {
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!()
}
});

View file

@ -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<RwLock<JackConnection>>,

View file

@ -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::*;

View file

View file

@ -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<PoolCommand>
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<PoolCommand>
),
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!()
}
});

View file

@ -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<RwLock<JackConnection>>,
@ -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<TuiOut> {
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<TuiOut> {
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<TuiOut> + use<'_> {
row!(&self.cpu, &self.size)
}

View file

@ -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<T> = Result<T, Box<dyn Error>>;

87
tui/src/tui_file.rs Normal file
View file

@ -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<TuiOut>
}
/// Commands supported by [FileBrowser]
#[derive(Debug, Clone, PartialEq)]
pub enum FileBrowserCommand {
Begin,
Cancel,
Confirm,
Select(usize),
Chdir(PathBuf),
Filter(Arc<str>),
}
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<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,
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> {
Self::new(Some(self.path()))
}
}