diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..72e8ffc0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +* diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..7b08ef33 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +root = true +[*] +max_line_length = 132 diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..1d953f4b --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use nix diff --git a/.forgejo/workflows/build.yaml b/.forgejo/workflows/build.yaml deleted file mode 100644 index 316ad5c0..00000000 --- a/.forgejo/workflows/build.yaml +++ /dev/null @@ -1,6 +0,0 @@ -on: [push] -jobs: - build: - runs-on: rust - stepS: - - run: cargo build diff --git a/.forgejo/workflows/release.yml.off b/.forgejo/workflows/release.yml.off new file mode 100644 index 00000000..e2371720 --- /dev/null +++ b/.forgejo/workflows/release.yml.off @@ -0,0 +1,36 @@ +on: + push: + tags: '*' +jobs: + build: + runs-on: codeberg-small-lazy + container: { image: "alpine:edge" } + steps: + + - name: install deps + run: apk add --no-cache bash nodejs tree rustup git just cloc build-base clang20-dev pipewire-jack-dev lilv-dev serd-dev + + - run: git clone --depth 1 --recursive $GITHUB_SERVER_URL/$GITHUB_REPOSITORY . + - run: whoami && pwd && tree && cloc src/ && cloc . + + - run: rustup-init -y + - run: source "$HOME/.cargo/env" && rustup install nightly && rustup default nightly && cargo version -vv + #- run: source "$HOME/.cargo/env" && RUSTFLAGS="-Ctarget-feature=-crt-static" just doc + - run: source "$HOME/.cargo/env" && RUSTFLAGS="-Ctarget-feature=-crt-static" just test + - run: source "$HOME/.cargo/env" && RUSTFLAGS="-Ctarget-feature=-crt-static" just build-release + + - run: tree && mkdir -p .release && mv -v target/release/tek .release + + - name: publish release + uses: https://data.forgejo.org/actions/forgejo-release@v2.6.0 + with: + url: "https://codeberg.org" + direction: upload + tag: "${{ github.ref_name }}" + sha: "${{ github.sha }}" + release-dir: .release + override: true + verbose: true + #hide-archive-link: true + #token: ${{ secrets.TOKEN }} + #release-notes-assistant: true diff --git a/.forgejo/workflows/test.yaml b/.forgejo/workflows/test.yaml new file mode 100644 index 00000000..16d33122 --- /dev/null +++ b/.forgejo/workflows/test.yaml @@ -0,0 +1,49 @@ +on: + push: + branches: '*' +jobs: + build: + container: { image: "alpine:edge" } + steps: + + - name: install deps + run: apk add --no-cache nodejs tree rustup git just cloc build-base clang20-dev pipewire-jack-dev lilv-dev serd-dev + + - run: git clone --depth 1 --recursive $GITHUB_SERVER_URL/$GITHUB_REPOSITORY . + - run: whoami && pwd && tree && cloc src/ && cloc . + + #- id: cache + #name: cache restore + #uses: https://data.forgejo.org/actions/cache/restore@v4 + #with: + #key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + #path: | + #~/.cargo/bin/ + #~/.cargo/registry/index/ + #~/.cargo/registry/cache/ + #~/.cargo/git/db/ + #target/ + + #- name: cache hit + #if: steps.cache.outputs.cache-hit == 'true' + #run: echo "cache hit! :)" + #- name: cache miss + #if: steps.cache.outputs.cache-miss != 'true' + #run: echo "cache miss! :(" + + - run: cloc src/ && cloc . + - run: rustup-init -y + - run: source "$HOME/.cargo/env" && rustup install nightly && rustup default nightly && cargo version -vv + - run: source "$HOME/.cargo/env" && RUSTFLAGS="-Ctarget-feature=-crt-static" just test + - run: tree + + #- name: cache save + #uses: https://data.forgejo.org/actions/cache/save@v4 + #with: + #key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + #path: | + #~/.cargo/bin/ + #~/.cargo/registry/index/ + #~/.cargo/registry/cache/ + #~/.cargo/git/db/ + #target/ diff --git a/.gitignore b/.gitignore index 1a417612..e5790860 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,15 @@ -target +target/* +!target/.gitkeep perf.data* flamegraph*.svg vgcore* example.mid +cov +*/cov +*.profraw +build/* +!build/README.md +!build/*.sh +!build/Dockerfile.* +.misc +.direnv diff --git a/.gitmodules b/.gitmodules index e69de29b..15f065ba 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,10 @@ +[submodule "rust-jack"] + path = rust-jack + url = https://codeberg.org/unspeaker/rust-jack + branch = timebase +[submodule "tengri"] + path = deps/tengri + url = ../tengri/ +[submodule "deps/rust-jack"] + path = deps/rust-jack + url = https://codeberg.org/unspeaker/rust-jack diff --git a/crates/tek.old/Cargo.toml b/.old/Cargo.toml similarity index 100% rename from crates/tek.old/Cargo.toml rename to .old/Cargo.toml diff --git a/crates/tek.old/README.md b/.old/README.md similarity index 100% rename from crates/tek.old/README.md rename to .old/README.md diff --git a/crates/tek/examples/demo.rs b/.old/demo.rs.old similarity index 56% rename from crates/tek/examples/demo.rs rename to .old/demo.rs.old index ba0e022c..6b205580 100644 --- a/crates/tek/examples/demo.rs +++ b/.old/demo.rs.old @@ -1,5 +1,4 @@ -use tek_core::*; -use tek_core::jack::*; +use tek::*; fn main () -> Usually<()> { Tui::run(Arc::new(RwLock::new(Demo::new())))?; @@ -15,20 +14,7 @@ impl Demo { fn new () -> Self { Self { index: 0, - items: vec![ - //Box::new(tek_sequencer::TransportPlayPauseButton { - //_engine: Default::default(), - //transport: None, - //value: Some(TransportState::Stopped), - //focused: true - //}), - //Box::new(tek_sequencer::TransportPlayPauseButton { - //_engine: Default::default(), - //transport: None, - //value: Some(TransportState::Rolling), - //focused: false - //}), - ] + items: vec![] } } } @@ -41,26 +27,26 @@ impl Content for Demo { add(&Background(Color::Rgb(0,128,128)))?; - add(&Outset::XY(1, 1, Stack::down(|add|{ + add(&Margin::XY(1, 1, Stack::down(|add|{ add(&Layers::new(|add|{ add(&Background(Color::Rgb(128,96,0)))?; add(&Border(Square(border_style)))?; - add(&Outset::XY(2, 1, "..."))?; + add(&Margin::XY(2, 1, "..."))?; Ok(()) }).debug())?; add(&Layers::new(|add|{ add(&Background(Color::Rgb(128,64,0)))?; add(&Border(Lozenge(border_style)))?; - add(&Outset::XY(4, 2, "---"))?; + add(&Margin::XY(4, 2, "---"))?; Ok(()) }).debug())?; add(&Layers::new(|add|{ add(&Background(Color::Rgb(96,64,0)))?; add(&Border(SquareBold(border_style)))?; - add(&Outset::XY(6, 3, "~~~"))?; + add(&Margin::XY(6, 3, "~~~"))?; Ok(()) }).debug())?; @@ -70,15 +56,15 @@ impl Content for Demo { Ok(()) })) - //Align::Center(Outset::X(1, Layers::new(|add|{ + //Align::Center(Margin::X(1, Layers::new(|add|{ //add(&Background(Color::Rgb(128,0,0)))?; //add(&Stack::down(|add|{ - //add(&Outset::Y(1, Layers::new(|add|{ + //add(&Margin::Y(1, Layers::new(|add|{ //add(&Background(Color::Rgb(0,128,0)))?; //add(&Align::Center("12345"))?; //add(&Align::Center("FOO")) //})))?; - //add(&Outset::XY(1, 1, Layers::new(|add|{ + //add(&Margin::XY(1, 1, Layers::new(|add|{ //add(&Align::Center("1234567"))?; //add(&Align::Center("BAR"))?; //add(&Background(Color::Rgb(0,0,128))) @@ -88,13 +74,13 @@ impl Content for Demo { //Align::Y(Layers::new(|add|{ //add(&Background(Color::Rgb(128,0,0)))?; - //add(&Outset::X(1, Align::Center(Stack::down(|add|{ - //add(&Align::X(Outset::Y(1, Layers::new(|add|{ + //add(&Margin::X(1, Align::Center(Stack::down(|add|{ + //add(&Align::X(Margin::Y(1, Layers::new(|add|{ //add(&Background(Color::Rgb(0,128,0)))?; //add(&Align::Center("12345"))?; //add(&Align::Center("FOO")) //})))?; - //add(&Outset::XY(1, 1, Layers::new(|add|{ + //add(&Margin::XY(1, 1, Layers::new(|add|{ //add(&Align::Center("1234567"))?; //add(&Align::Center("BAR"))?; //add(&Background(Color::Rgb(0,0,128))) @@ -105,13 +91,14 @@ impl Content for Demo { } } -impl Handle for Demo { - fn handle (&mut self, from: &TuiInput) -> Perhaps { +impl Handle for Demo { + fn handle (&mut self, from: &TuiIn) -> Perhaps { + use KeyCode::{PageUp, PageDown}; match from.event() { - key!(KeyCode::PageUp) => { + kexp!(PageUp) => { self.index = (self.index + 1) % self.items.len(); }, - key!(KeyCode::PageDown) => { + kexp!(PageDown) => { self.index = if self.index > 1 { self.index - 1 } else { @@ -123,22 +110,3 @@ impl Handle for Demo { Ok(Some(true)) } } - -//lisp!(CONTENT Demo (LET - //(BORDER-STYLE (STYLE (FG (RGB 0 0 0)))) - //(BG-COLOR-0 (RGB 0 128 128)) - //(BG-COLOR-1 (RGB 128 96 0)) - //(BG-COLOR-2 (RGB 128 64 0)) - //(BG-COLOR-3 (RGB 96 64 0)) - //(CENTER (LAYERS - //(BACKGROUND BG-COLOR-0) - //(OUTSET-XY 1 1 (SPLIT-DOWN - //(LAYERS (BACKGROUND BG-COLOR-1) - //(BORDER SQUARE BORDER-STYLE) - //(OUTSET-XY 2 1 "...")) - //(LAYERS (BACKGROUND BG-COLOR-2) - //(BORDER LOZENGE BORDER-STYLE) - //(OUTSET-XY 4 2 "---")) - //(LAYERS (BACKGROUND BG-COLOR-3) - //(BORDER SQUARE-BOLD BORDER-STYLE) - //(OUTSET-XY 2 1 "~~~")))))))) diff --git a/crates/tek.old/example.edn b/.old/example.edn similarity index 100% rename from crates/tek.old/example.edn rename to .old/example.edn diff --git a/.old/from_arranger.rs b/.old/from_arranger.rs new file mode 100644 index 00000000..07502a6b --- /dev/null +++ b/.old/from_arranger.rs @@ -0,0 +1,188 @@ + + +//pub struct ArrangerVCursor { + //cols: Vec<(usize, usize)>, + //rows: Vec<(usize, usize)>, + //color: ItemPalette, + //reticle: Reticle, + //selected: ArrangerSelection, + //scenes_w: u16, +//} + +//pub(crate) const HEADER_H: u16 = 0; // 5 +//pub(crate) const SCENES_W_OFFSET: u16 = 0; +//from!(|args:(&Arranger, usize)|ArrangerVCursor = Self { + //cols: Arranger::track_widths(&args.0.tracks), + //rows: Arranger::scene_heights(&args.0.scenes, args.1), + //selected: args.0.selected(), + //scenes_w: SCENES_W_OFFSET + ArrangerScene::longest_name(&args.0.scenes) as u16, + //color: args.0.color, + //reticle: Reticle(Style { + //fg: Some(args.0.color.lighter.rgb), + //bg: None, + //underline_color: None, + //add_modifier: Modifier::empty(), + //sub_modifier: Modifier::DIM + //}), +//}); +//impl Content for ArrangerVCursor { + //fn render (&self, to: &mut TuiOut) { + //let area = to.area(); + //let focused = true; + //let selected = self.selected; + //let get_track_area = |t: usize| [ + //self.scenes_w + area.x() + self.cols[t].1 as u16, area.y(), + //self.cols[t].0 as u16, area.h(), + //]; + //let get_scene_area = |s: usize| [ + //area.x(), HEADER_H + area.y() + (self.rows[s].1 / PPQ) as u16, + //area.w(), (self.rows[s].0 / PPQ) as u16 + //]; + //let get_clip_area = |t: usize, s: usize| [ + //(self.scenes_w + area.x() + self.cols[t].1 as u16).saturating_sub(1), + //HEADER_H + area.y() + (self.rows[s].1/PPQ) as u16, + //self.cols[t].0 as u16 + 2, + //(self.rows[s].0 / PPQ) as u16 + //]; + //let mut track_area: Option<[u16;4]> = None; + //let mut scene_area: Option<[u16;4]> = None; + //let mut clip_area: Option<[u16;4]> = None; + //let area = match selected { + //ArrangerSelection::Mix => area, + //ArrangerSelection::Track(t) => { + //track_area = Some(get_track_area(t)); + //area + //}, + //ArrangerSelection::Scene(s) => { + //scene_area = Some(get_scene_area(s)); + //area + //}, + //ArrangerSelection::Clip(t, s) => { + //track_area = Some(get_track_area(t)); + //scene_area = Some(get_scene_area(s)); + //clip_area = Some(get_clip_area(t, s)); + //area + //}, + //}; + //let bg = self.color.lighter.rgb;//Color::Rgb(0, 255, 0); + //if let Some([x, y, width, height]) = track_area { + //to.fill_fg([x, y, 1, height], bg); + //to.fill_fg([x + width, y, 1, height], bg); + //} + //if let Some([_, y, _, height]) = scene_area { + //to.fill_ul([area.x(), y - 1, area.w(), 1], bg); + //to.fill_ul([area.x(), y + height - 1, area.w(), 1], bg); + //} + //if focused { + //to.place(if let Some(clip_area) = clip_area { + //clip_area + //} else if let Some(track_area) = track_area { + //track_area.clip_h(HEADER_H) + //} else if let Some(scene_area) = scene_area { + //scene_area.clip_w(self.scenes_w) + //} else { + //area.clip_w(self.scenes_w).clip_h(HEADER_H) + //}, &self.reticle) + //}; + //} +//} +//impl Arranger { + //fn render_mode (state: &Self) -> impl Content + use<'_> { + //match state.mode { + //ArrangerMode::H => todo!("horizontal arranger"), + //ArrangerMode::V(factor) => Self::render_mode_v(state, factor), + //} + //} +//} +//render!(TuiOut: (self: Arranger) => { + //let pool_w = if self.pool.visible { self.splits[1] } else { 0 }; + //let color = self.color; + //let layout = Bsp::a(Fill::xy(ArrangerStatus::from(self)), + //Bsp::n(Fixed::x(pool_w, PoolView(self.pool.visible, &self.pool)), + //Bsp::n(TransportView::new(true, &self.clock), + //Bsp::s(Fixed::y(1, MidiEditStatus(&self.editor)), + //Bsp::n(Fill::x(Fixed::y(20, + //Bsp::a(Fill::xy(Tui::bg(color.darkest.rgb, "background")), + //Bsp::a( + //Fill::xy(Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb))), + //Self::render_mode(self))))), Fill::y(&"fixme: self.editor")))))); + //self.size.of(layout) +//}); + //Align::n(Fill::xy(lay!( + //Align::n(Fill::xy(Tui::bg(self.color.darkest.rgb, " "))), + //Align::n(Fill::xy(ArrangerVRowSep::from((self, 1)))), + //Align::n(Fill::xy(ArrangerVColSep::from(self))), + //Align::n(Fill::xy(ArrangerVClips::new(self, 1))), + //Align::n(Fill::xy(ArrangerVCursor::from((self, 1)))))))))))))))); + //Align::n(Fill::xy(":"))))))))))))); + //"todo:")))))))); + //Bsp::s( + //Align::n(Fixed::y(1, Fill::x(ArrangerVIns::from(self)))), + //Bsp::s( + //Fixed::y(20, Align::n(ArrangerVClips::new(self, 1))), + //Fill::x(Fixed::y(1, ArrangerVOuts::from(self))))))))))))); + //Bsp::s( + //Bsp::s( + //Bsp::s( + //Fill::xy(ArrangerVClips::new(self, 1)), + //Fill::x(ArrangerVOuts::from(self))))) + + //let cell = phat_sel_3( + //selected_track == Some(i) && selected_scene == Some(j), + //Tui::fg(TuiTheme::g(64), Push::x(1, name)), + //Tui::fg(TuiTheme::g(64), Push::x(1, name)), + //if selected_track == Some(i) && selected_scene.map(|s|s+1) == Some(j) { + //None + //} else { + //Some(TuiTheme::g(32).into()) + //}, + //TuiTheme::g(32).into(), + //TuiTheme::g(32).into(), + //); + // TODO: port per track: + //for connection in midi_from.iter() { + //let mut split = connection.as_ref().split("="); + //let number = split.next().unwrap().trim(); + //if let Ok(track) = number.parse::() { + //if track < 1 { + //panic!("Tracks start from 1") + //} + //if track > count { + //panic!("Tried to connect track {track} or {count}. Pass -t {track} to increase number of tracks.") + //} + //if let Some(port) = split.next() { + //if let Some(port) = jack.read().unwrap().client().port_by_name(port).as_ref() { + ////jack.read().unwrap().client().connect_ports(port, &self.tracks[track-1].player.midi_ins[0])?; + //} else { + //panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names."); + //} + //} else { + //panic!("No port specified for track {track}. Format is TRACK_NUMBER=PORT_NAME") + //} + //} else { + //panic!("Failed to parse track number: {number}") + //} + //} + //for connection in midi_to.iter() { + //let mut split = connection.as_ref().split("="); + //let number = split.next().unwrap().trim(); + //if let Ok(track) = number.parse::() { + //if track < 1 { + //panic!("Tracks start from 1") + //} + //if track > count { + //panic!("Tried to connect track {track} or {count}. Pass -t {track} to increase number of tracks.") + //} + //if let Some(port) = split.next() { + //if let Some(port) = jack.read().unwrap().client().port_by_name(port).as_ref() { + ////jack.read().unwrap().client().connect_ports(&self.tracks[track-1].player.midi_outs[0], port)?; + //} else { + //panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names."); + //} + //} else { + //panic!("No port specified for track {track}. Format is TRACK_NUMBER=PORT_NAME") + //} + //} else { + //panic!("Failed to parse track number: {number}") + //} + //} diff --git a/.old/midi.scratch.rs b/.old/midi.scratch.rs new file mode 100644 index 00000000..8602766a --- /dev/null +++ b/.old/midi.scratch.rs @@ -0,0 +1,31 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// +//keymap!(KEYS_MIDI_EDITOR = |s: MidiEditor, _input: Event| MidiEditCommand { + //key(Up) => SetNoteCursor(s.note_pos() + 1), + //key(Char('w')) => SetNoteCursor(s.note_pos() + 1), + //key(Down) => SetNoteCursor(s.note_pos().saturating_sub(1)), + //key(Char('s')) => SetNoteCursor(s.note_pos().saturating_sub(1)), + //key(Left) => SetTimeCursor(s.time_pos().saturating_sub(s.note_len())), + //key(Char('a')) => SetTimeCursor(s.time_pos().saturating_sub(s.note_len())), + //key(Right) => SetTimeCursor((s.time_pos() + s.note_len()) % s.clip_length()), + //ctrl(alt(key(Up))) => SetNoteScroll(s.note_pos() + 3), + //ctrl(alt(key(Down))) => SetNoteScroll(s.note_pos().saturating_sub(3)), + //ctrl(alt(key(Left))) => SetTimeScroll(s.time_pos().saturating_sub(s.time_zoom().get())), + //ctrl(alt(key(Right))) => SetTimeScroll((s.time_pos() + s.time_zoom().get()) % s.clip_length()), + //ctrl(key(Up)) => SetNoteScroll(s.note_lo().get() + 1), + //ctrl(key(Down)) => SetNoteScroll(s.note_lo().get().saturating_sub(1)), + //ctrl(key(Left)) => SetTimeScroll(s.time_start().get().saturating_sub(s.note_len())), + //ctrl(key(Right)) => SetTimeScroll(s.time_start().get() + s.note_len()), + //alt(key(Up)) => SetNoteCursor(s.note_pos() + 3), + //alt(key(Down)) => SetNoteCursor(s.note_pos().saturating_sub(3)), + //alt(key(Left)) => SetTimeCursor(s.time_pos().saturating_sub(s.time_zoom().get())), + //alt(key(Right)) => SetTimeCursor((s.time_pos() + s.time_zoom().get()) % s.clip_length()), + //key(Char('d')) => SetTimeCursor((s.time_pos() + s.note_len()) % s.clip_length()), + //key(Char('z')) => SetTimeLock(!s.time_lock().get()), + //key(Char('-')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::next(s.time_zoom().get()) }), + //key(Char('_')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::next(s.time_zoom().get()) }), + //key(Char('=')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::prev(s.time_zoom().get()) }), + //key(Char('+')) => SetTimeZoom(if s.time_lock().get() { s.time_zoom().get() } else { NoteDuration::prev(s.time_zoom().get()) }), + ////// TODO: kpat!(Char('/')) => // toggle 3plet + ////// TODO: kpat!(Char('?')) => // toggle dotted +//}); + diff --git a/.old/midi_import.rs b/.old/midi_import.rs new file mode 100644 index 00000000..d2cceae2 --- /dev/null +++ b/.old/midi_import.rs @@ -0,0 +1,20 @@ +use tek::*; +use tengri::input::*; +use std::sync::*; +struct ExampleClips(Arc>>>>); +impl HasClips for ExampleClips { + fn clips (&self) -> RwLockReadGuard<'_, Vec>>> { + self.0.read().unwrap() + } + fn clips_mut (&self) -> RwLockWriteGuard<'_, Vec>>> { + self.0.write().unwrap() + } +} +fn main () -> Result<(), Box> { + let mut clips = Pool::default();//ExampleClips(Arc::new(vec![].into())); + PoolClipCommand::Import { + index: 0, + path: std::path::PathBuf::from("./example.mid") + }.execute(&mut clips)?; + Ok(()) +} diff --git a/.old/sampler_scratch.rs b/.old/sampler_scratch.rs new file mode 100644 index 00000000..82009355 --- /dev/null +++ b/.old/sampler_scratch.rs @@ -0,0 +1,105 @@ +//handle!(TuiIn: |self: Sampler, input|SamplerCommand::execute_with_state(self, input.event())); +//input_to_command!(SamplerCommand: |state: Sampler, input: Event|match state.mode{ + //Some(SamplerMode::Import(..)) => Self::Import( + //FileBrowserCommand::input_to_command(state, input)? + //), + //_ => match input { + //// load sample + //kpat!(Shift-Char('L')) => Self::Import(FileBrowserCommand::Begin), + //kpat!(KeyCode::Up) => Self::Select(state.note_pos().overflowing_add(1).0.min(127)), + //kpat!(KeyCode::Down) => Self::Select(state.note_pos().overflowing_sub(1).0.min(127)), + //_ => return None + //} +//}); +//impl Handle for AddSampleModal { + //fn handle (&mut self, from: &TuiIn) -> Perhaps { + //if from.handle_keymap(self, KEYMAP_ADD_SAMPLE)? { + //return Ok(Some(true)) + //} + //Ok(Some(true)) + //} +//} +//pub const KEYMAP_ADD_SAMPLE: &'static [KeyBinding] = keymap!(AddSampleModal { + //[Esc, NONE, "sampler/add/close", "close help dialog", |modal: &mut AddSampleModal|{ + //modal.exit(); + //Ok(true) + //}], + //[Up, NONE, "sampler/add/prev", "select previous entry", |modal: &mut AddSampleModal|{ + //modal.prev(); + //Ok(true) + //}], + //[Down, NONE, "sampler/add/next", "select next entry", |modal: &mut AddSampleModal|{ + //modal.next(); + //Ok(true) + //}], + //[Enter, NONE, "sampler/add/enter", "activate selected entry", |modal: &mut AddSampleModal|{ + //if modal.pick()? { + //modal.exit(); + //} + //Ok(true) + //}], + //[Char('p'), NONE, "sampler/add/preview", "preview selected entry", |modal: &mut AddSampleModal|{ + //modal.try_preview()?; + //Ok(true) + //}] +//}); +//from_atom!("sampler" => |jack: &Jack, args| -> crate::Sampler { + //let mut name = String::new(); + //let mut dir = String::new(); + //let mut samples = BTreeMap::new(); + //atom!(atom in args { + //Atom::Map(map) => { + //if let Some(Atom::Str(n)) = map.get(&Atom::Key(":name")) { + //name = String::from(*n); + //} + //if let Some(Atom::Str(n)) = map.get(&Atom::Key(":dir")) { + //dir = String::from(*n); + //} + //}, + //Atom::List(args) => match args.first() { + //Some(Atom::Symbol("sample")) => { + //let (midi, sample) = MidiSample::from_atom((jack, &dir), &args[1..])?; + //if let Some(midi) = midi { + //samples.insert(midi, sample); + //} else { + //panic!("sample without midi binding: {}", sample.read().unwrap().name); + //} + //}, + //_ => panic!("unexpected in sampler {name}: {args:?}") + //}, + //_ => panic!("unexpected in sampler {name}: {atom:?}") + //}); + //Self::new(jack, &name) +//}); +//from_atom!("sample" => |(_jack, dir): (&Jack, &str), args| -> MidiSample { + //let mut name = String::new(); + //let mut file = String::new(); + //let mut midi = None; + //let mut start = 0usize; + //atom!(atom in args { + //Atom::Map(map) => { + //if let Some(Atom::Str(n)) = map.get(&Atom::Key(":name")) { + //name = String::from(*n); + //} + //if let Some(Atom::Str(f)) = map.get(&Atom::Key(":file")) { + //file = String::from(*f); + //} + //if let Some(Atom::Int(i)) = map.get(&Atom::Key(":start")) { + //start = *i as usize; + //} + //if let Some(Atom::Int(m)) = map.get(&Atom::Key(":midi")) { + //midi = Some(u7::from(*m as u8)); + //} + //}, + //_ => panic!("unexpected in sample {name}"), + //}); + //let (end, data) = Sample::read_data(&format!("{dir}/{file}"))?; + //Ok((midi, Arc::new(RwLock::new(crate::Sample { + //name, + //start, + //end, + //channels: data, + //rate: None, + //gain: 1.0 + //})))) +//}); diff --git a/.old/scratch.rs b/.old/scratch.rs new file mode 100644 index 00000000..9bd28f30 --- /dev/null +++ b/.old/scratch.rs @@ -0,0 +1,375 @@ +//impl Bar for ArrangerStatus { + //type State = (ArrangerFocus, ArrangerSelection, bool); + //fn hotkey_fg () -> Color where Self: Sized { + //TuiTheme::HOTKEY_FG + //} + //fn update (&mut self, (focused, selected, entered): &Self::State) { + //*self = match focused { + ////ArrangerFocus::Menu => { todo!() }, + //ArrangerFocus::Transport(_) => ArrangerStatus::Transport, + //ArrangerFocus::Arranger => match selected { + //ArrangerSelection::Mix => ArrangerStatus::ArrangerMix, + //ArrangerSelection::Track(_) => ArrangerStatus::ArrangerTrack, + //ArrangerSelection::Scene(_) => ArrangerStatus::ArrangerScene, + //ArrangerSelection::Clip(_, _) => ArrangerStatus::ArrangerClip, + //}, + //ArrangerFocus::Phrases => ArrangerStatus::PhrasePool, + //ArrangerFocus::PhraseEditor => match entered { + //true => ArrangerStatus::PhraseEdit, + //false => ArrangerStatus::PhraseView, + //}, + //} + //} +//} + +//render!(|self: ArrangerStatus|{ + + //let label = match self { + //Self::Transport => "TRANSPORT", + //Self::ArrangerMix => "PROJECT", + //Self::ArrangerTrack => "TRACK", + //Self::ArrangerScene => "SCENE", + //Self::ArrangerClip => "CLIP", + //Self::PhrasePool => "SEQ LIST", + //Self::PhraseView => "VIEW SEQ", + //Self::PhraseEdit => "EDIT SEQ", + //}; + + //let status_bar_bg = TuiTheme::status_bar_bg(); + + //let mode_bg = TuiTheme::mode_bg(); + //let mode_fg = TuiTheme::mode_fg(); + //let mode = Tui::fg(mode_fg, Tui::bg(mode_bg, Tui::bold(true, format!(" {label} ")))); + + //let commands = match self { + //Self::ArrangerMix => Self::command(&[ + //["", "c", "olor"], + //["", "<>", "resize"], + //["", "+-", "zoom"], + //["", "n", "ame/number"], + //["", "Enter", " stop all"], + //]), + //Self::ArrangerClip => Self::command(&[ + //["", "g", "et"], + //["", "s", "et"], + //["", "a", "dd"], + //["", "i", "ns"], + //["", "d", "up"], + //["", "e", "dit"], + //["", "c", "olor"], + //["re", "n", "ame"], + //["", ",.", "select"], + //["", "Enter", " launch"], + //]), + //Self::ArrangerTrack => Self::command(&[ + //["re", "n", "ame"], + //["", ",.", "resize"], + //["", "<>", "move"], + //["", "i", "nput"], + //["", "o", "utput"], + //["", "m", "ute"], + //["", "s", "olo"], + //["", "Del", "ete"], + //["", "Enter", " stop"], + //]), + //Self::ArrangerScene => Self::command(&[ + //["re", "n", "ame"], + //["", "Del", "ete"], + //["", "Enter", " launch"], + //]), + //Self::PhrasePool => Self::command(&[ + //["", "a", "ppend"], + //["", "i", "nsert"], + //["", "d", "uplicate"], + //["", "Del", "ete"], + //["", "c", "olor"], + //["re", "n", "ame"], + //["leng", "t", "h"], + //["", ",.", "move"], + //["", "+-", "resize view"], + //]), + //Self::PhraseView => Self::command(&[ + //["", "enter", " edit"], + //["", "arrows/pgup/pgdn", " scroll"], + //["", "+=", "zoom"], + //]), + //Self::PhraseEdit => Self::command(&[ + //["", "esc", " exit"], + //["", "a", "ppend"], + //["", "s", "et"], + //["", "][", "length"], + //["", "+-", "zoom"], + //]), + //_ => Self::command(&[]) + //}; + + ////let commands = commands.iter().reduce(String::new(), |s, (a, b, c)| format!("{s} {a}{b}{c}")); + //Tui::bg(status_bar_bg, Fill::w(row!([mode, commands]))) + +//}); + +///// Status bar for arranger app +//#[derive(Copy, Clone, Debug)] +//pub enum ArrangerStatus { + //Transport, + //ArrangerMix, + //ArrangerTrack, + //ArrangerScene, + //ArrangerClip, + //PhrasePool, + //PhraseView, + //PhraseEdit, +//} + + //let focused = true; + //let _tracks = view.tracks(); + //lay!( + //focused.then_some(Background(TuiTheme::border_bg())), + //row!( + //// name + //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ + //todo!() + ////let Self(tracks, selected) = self; + ////let yellow = Some(Style::default().yellow().bold().not_dim()); + ////let white = Some(Style::default().white().bold().not_dim()); + ////let area = to.area(); + ////let area = [area.x(), area.y(), 3 + 5.max(track_name_max_len(tracks)) as u16, area.h()]; + ////let offset = 0; // track scroll offset + ////for y in 0..area.h() { + ////if y == 0 { + ////to.blit(&"Mixer", area.x() + 1, area.y() + y, Some(DIM))?; + ////} else if y % 2 == 0 { + ////let index = (y as usize - 2) / 2 + offset; + ////if let Some(track) = tracks.get(index) { + ////let selected = selected.track() == Some(index); + ////let style = if selected { yellow } else { white }; + ////to.blit(&format!(" {index:>02} "), area.x(), area.y() + y, style)?; + ////to.blit(&*track.name.read().unwrap(), area.x() + 4, area.y() + y, style)?; + ////} + ////} + ////} + ////Ok(Some(area)) + //}), + //// monitor + //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ + //todo!() + ////let Self(tracks) = self; + ////let mut area = to.area(); + ////let on = Some(Style::default().not_dim().green().bold()); + ////let off = Some(DIM); + ////area.x += 1; + ////for y in 0..area.h() { + ////if y == 0 { + //////" MON ".blit(to.buffer, area.x, area.y + y, style2)?; + ////} else if y % 2 == 0 { + ////let index = (y as usize - 2) / 2; + ////if let Some(track) = tracks.get(index) { + ////let style = if track.monitoring { on } else { off }; + ////to.blit(&" MON ", area.x(), area.y() + y, style)?; + ////} else { + ////area.height = y; + ////break + ////} + ////} + ////} + ////area.width = 4; + ////Ok(Some(area)) + //}), + //// record + //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ + //todo!() + ////let Self(tracks) = self; + ////let mut area = to.area(); + ////let on = Some(Style::default().not_dim().red().bold()); + ////let off = Some(Style::default().dim()); + ////area.x += 1; + ////for y in 0..area.h() { + ////if y == 0 { + //////" REC ".blit(to.buffer, area.x, area.y + y, style2)?; + ////} else if y % 2 == 0 { + ////let index = (y as usize - 2) / 2; + ////if let Some(track) = tracks.get(index) { + ////let style = if track.recording { on } else { off }; + ////to.blit(&" REC ", area.x(), area.y() + y, style)?; + ////} else { + ////area.height = y; + ////break + ////} + ////} + ////} + ////area.width = 4; + ////Ok(Some(area)) + //}), + //// overdub + //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ + //todo!() + ////let Self(tracks) = self; + ////let mut area = to.area(); + ////let on = Some(Style::default().not_dim().yellow().bold()); + ////let off = Some(Style::default().dim()); + ////area.x = area.x + 1; + ////for y in 0..area.h() { + ////if y == 0 { + //////" OVR ".blit(to.buffer, area.x, area.y + y, style2)?; + ////} else if y % 2 == 0 { + ////let index = (y as usize - 2) / 2; + ////if let Some(track) = tracks.get(index) { + ////to.blit(&" OVR ", area.x(), area.y() + y, if track.overdub { + ////on + ////} else { + ////off + ////})?; + ////} else { + ////area.height = y; + ////break + ////} + ////} + ////} + ////area.width = 4; + ////Ok(Some(area)) + //}), + //// erase + //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ + //todo!() + ////let Self(tracks) = self; + ////let mut area = to.area(); + ////let off = Some(Style::default().dim()); + ////area.x = area.x + 1; + ////for y in 0..area.h() { + ////if y == 0 { + //////" DEL ".blit(to.buffer, area.x, area.y + y, style2)?; + ////} else if y % 2 == 0 { + ////let index = (y as usize - 2) / 2; + ////if let Some(_) = tracks.get(index) { + ////to.blit(&" DEL ", area.x(), area.y() + y, off)?; + ////} else { + ////area.height = y; + ////break + ////} + ////} + ////} + ////area.width = 4; + ////Ok(Some(area)) + //}), + //// gain + //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ + //todo!() + ////let Self(tracks) = self; + ////let mut area = to.area(); + ////let off = Some(Style::default().dim()); + ////area.x = area.x() + 1; + ////for y in 0..area.h() { + ////if y == 0 { + //////" GAIN ".blit(to.buffer, area.x, area.y + y, style2)?; + ////} else if y % 2 == 0 { + ////let index = (y as usize - 2) / 2; + ////if let Some(_) = tracks.get(index) { + ////to.blit(&" +0.0 ", area.x(), area.y() + y, off)?; + ////} else { + ////area.height = y; + ////break + ////} + ////} + ////} + ////area.width = 7; + ////Ok(Some(area)) + //}), + //// scenes + //Widget::new(|_|{todo!()}, |to: &mut TuiOutput|{ + //let [x, y, _, height] = to.area(); + //let mut x2 = 0; + //Ok(for (scene_index, scene) in view.scenes().iter().enumerate() { + //let active_scene = view.selected.scene() == Some(scene_index); + //let sep = Some(if active_scene { + //Style::default().yellow().not_dim() + //} else { + //Style::default().dim() + //}); + //for y in y+1..y+height { + //to.blit(&"│", x + x2, y, sep); + //} + //let name = scene.name.read().unwrap(); + //let mut x3 = name.len() as u16; + //to.blit(&*name, x + x2, y, sep); + //for (i, clip) in scene.clips.iter().enumerate() { + //let active_track = view.selected.track() == Some(i); + //if let Some(clip) = clip { + //let y2 = y + 2 + i as u16 * 2; + //let label = format!("{}", clip.read().unwrap().name); + //to.blit(&label, x + x2, y2, Some(if active_track && active_scene { + //Style::default().not_dim().yellow().bold() + //} else { + //Style::default().not_dim() + //})); + //x3 = x3.max(label.len() as u16) + //} + //} + //x2 = x2 + x3 + 1; + //}) + //}), + //) + //) +//} + +//impl Command for ArrangerSceneCommand { +//} + //Edit(phrase) => { state.state.phrase = phrase.clone() }, + //ToggleViewMode => { state.state.mode.to_next(); }, + //Delete => { state.state.delete(); }, + //Activate => { state.state.activate(); }, + //ZoomIn => { state.state.zoom_in(); }, + //ZoomOut => { state.state.zoom_out(); }, + //MoveBack => { state.state.move_back(); }, + //MoveForward => { state.state.move_forward(); }, + //RandomColor => { state.state.randomize_color(); }, + //Put => { state.state.phrase_put(); }, + //Get => { state.state.phrase_get(); }, + //AddScene => { state.state.scene_add(None, None)?; }, + //AddTrack => { state.state.track_add(None, None)?; }, + //ToggleLoop => { state.state.toggle_loop() }, + //pub fn zoom_in (&mut self) { + //if let ArrangerEditorMode::V(factor) = self.mode { + //self.mode = ArrangerEditorMode::V(factor + 1) + //} + //} + //pub fn zoom_out (&mut self) { + //if let ArrangerEditorMode::V(factor) = self.mode { + //self.mode = ArrangerEditorMode::V(factor.saturating_sub(1)) + //} + //} + //pub fn move_back (&mut self) { + //match self.selected { + //ArrangerEditorFocus::Scene(s) => { + //if s > 0 { + //self.scenes.swap(s, s - 1); + //self.selected = ArrangerEditorFocus::Scene(s - 1); + //} + //}, + //ArrangerEditorFocus::Track(t) => { + //if t > 0 { + //self.tracks.swap(t, t - 1); + //self.selected = ArrangerEditorFocus::Track(t - 1); + //// FIXME: also swap clip order in scenes + //} + //}, + //_ => todo!("arrangement: move forward") + //} + //} + //pub fn move_forward (&mut self) { + //match self.selected { + //ArrangerEditorFocus::Scene(s) => { + //if s < self.scenes.len().saturating_sub(1) { + //self.scenes.swap(s, s + 1); + //self.selected = ArrangerEditorFocus::Scene(s + 1); + //} + //}, + //ArrangerEditorFocus::Track(t) => { + //if t < self.tracks.len().saturating_sub(1) { + //self.tracks.swap(t, t + 1); + //self.selected = ArrangerEditorFocus::Track(t + 1); + //// FIXME: also swap clip order in scenes + //} + //}, + //_ => todo!("arrangement: move forward") + //} + //} diff --git a/crates/tek.old/src/app.rs b/.old/src/app.rs similarity index 100% rename from crates/tek.old/src/app.rs rename to .old/src/app.rs diff --git a/crates/tek.old/src/app_focus.rs b/.old/src/app_focus.rs similarity index 100% rename from crates/tek.old/src/app_focus.rs rename to .old/src/app_focus.rs diff --git a/crates/tek.old/src/app_paths.rs b/.old/src/app_paths.rs similarity index 100% rename from crates/tek.old/src/app_paths.rs rename to .old/src/app_paths.rs diff --git a/crates/tek.old/src/cli.rs b/.old/src/cli.rs similarity index 100% rename from crates/tek.old/src/cli.rs rename to .old/src/cli.rs diff --git a/crates/tek.old/src/control.rs b/.old/src/control.rs similarity index 97% rename from crates/tek.old/src/control.rs rename to .old/src/control.rs index 46bd349f..ff45ff11 100644 --- a/crates/tek.old/src/control.rs +++ b/.old/src/control.rs @@ -78,19 +78,19 @@ pub const KEYMAP_GLOBAL: &'static [KeyBinding] = keymap!(App { Ok(true) }], [Char('+'), NONE, "quant_inc", "quantize coarser", |app: &mut App| { - app.transport.quant = next_note_length(app.transport.quant); + app.transport.quant = Note::next(app.transport.quant); Ok(true) }], [Char('_'), NONE, "quant_dec", "quantize finer", |app: &mut App| { - app.transport.quant = prev_note_length(app.transport.quant); + app.transport.quant = Note::prev(app.transport.quant); Ok(true) }], [Char('='), NONE, "zoom_in", "show fewer ticks per block", |app: &mut App| { - app.arranger.sequencer_mut().map(|s|s.time_axis.scale_mut(&prev_note_length)); + app.arranger.sequencer_mut().map(|s|s.time_axis.scale_mut(&Note::prev)); Ok(true) }], [Char('-'), NONE, "zoom_out", "show more ticks per block", |app: &mut App| { - app.arranger.sequencer_mut().map(|s|s.time_axis.scale_mut(&next_note_length)); + app.arranger.sequencer_mut().map(|s|s.time_axis.scale_mut(&Note::next)); Ok(true) }], [Char('x'), NONE, "extend", "double the current clip", |app: &mut App| { diff --git a/crates/tek.old/src/edn.rs b/.old/src/edn.rs similarity index 100% rename from crates/tek.old/src/edn.rs rename to .old/src/edn.rs diff --git a/crates/tek.old/src/help.rs b/.old/src/help.rs similarity index 100% rename from crates/tek.old/src/help.rs rename to .old/src/help.rs diff --git a/crates/tek.old/src/main.rs b/.old/src/main.rs similarity index 100% rename from crates/tek.old/src/main.rs rename to .old/src/main.rs diff --git a/crates/tek.old/src/setup.rs b/.old/src/setup.rs similarity index 100% rename from crates/tek.old/src/setup.rs rename to .old/src/setup.rs diff --git a/.old/tek.rs.old b/.old/tek.rs.old new file mode 100644 index 00000000..e64fd51b --- /dev/null +++ b/.old/tek.rs.old @@ -0,0 +1,2113 @@ +/////////////////////////////////////////////////////////////////////////////////////////////////// + +//#[cfg(test)] mod test_focus { + //use super::focus::*; + //#[test] fn test_focus () { + + //struct FocusTest { + //focused: char, + //cursor: (usize, usize) + //} + + //impl HasFocus for FocusTest { + //type Item = char; + //fn focused (&self) -> Self::Item { + //self.focused + //} + //fn set_focused (&mut self, to: Self::Item) { + //self.focused = to + //} + //} + + //impl FocusGrid for FocusTest { + //fn focus_cursor (&self) -> (usize, usize) { + //self.cursor + //} + //fn focus_cursor_mut (&mut self) -> &mut (usize, usize) { + //&mut self.cursor + //} + //fn focus_layout (&self) -> &[&[Self::Item]] { + //&[ + //&['a', 'a', 'a', 'b', 'b', 'd'], + //&['a', 'a', 'a', 'b', 'b', 'd'], + //&['a', 'a', 'a', 'c', 'c', 'd'], + //&['a', 'a', 'a', 'c', 'c', 'd'], + //&['e', 'e', 'e', 'e', 'e', 'e'], + //] + //} + //} + + //let mut tester = FocusTest { focused: 'a', cursor: (0, 0) }; + + //tester.focus_right(); + //assert_eq!(tester.cursor.0, 3); + //assert_eq!(tester.focused, 'b'); + + //tester.focus_down(); + //assert_eq!(tester.cursor.1, 2); + //assert_eq!(tester.focused, 'c'); + + //} +//} +//use crate::*; + +//struct TestEngine([u16;4], Vec>); + +//impl Engine for TestEngine { + //type Unit = u16; + //type Size = [Self::Unit;2]; + //type Area = [Self::Unit;4]; + //type Input = Self; + //type Handled = bool; + //fn exited (&self) -> bool { + //true + //} +//} + +//#[derive(Copy, Clone)] +//struct TestArea(u16, u16); + +//impl Render for TestArea { + //fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> { + //Ok(Some([to[0], to[1], self.0, self.1])) + //} + //fn render (&self, to: &mut TestEngine) -> Perhaps<[u16;4]> { + //if let Some(layout) = self.layout(to.area())? { + //for y in layout.y()..layout.y()+layout.h()-1 { + //for x in layout.x()..layout.x()+layout.w()-1 { + //to.1[y as usize][x as usize] = '*'; + //} + //} + //Ok(Some(layout)) + //} else { + //Ok(None) + //} + //} +//} + +//#[test] +//fn test_plus_minus () -> Usually<()> { + //let area = [0, 0, 10, 10]; + //let engine = TestEngine(area, vec![vec![' ';10];10]); + //let test = TestArea(4, 4); + //assert_eq!(test.layout(area)?, Some([0, 0, 4, 4])); + //assert_eq!(Push::X(1, test).layout(area)?, Some([1, 0, 4, 4])); + //Ok(()) +//} + +//#[test] +//fn test_outset_align () -> Usually<()> { + //let area = [0, 0, 10, 10]; + //let engine = TestEngine(area, vec![vec![' ';10];10]); + //let test = TestArea(4, 4); + //assert_eq!(test.layout(area)?, Some([0, 0, 4, 4])); + //assert_eq!(Margin::X(1, test).layout(area)?, Some([0, 0, 6, 4])); + //assert_eq!(Align::X(test).layout(area)?, Some([3, 0, 4, 4])); + //assert_eq!(Align::X(Margin::X(1, test)).layout(area)?, Some([2, 0, 6, 4])); + //assert_eq!(Margin::X(1, Align::X(test)).layout(area)?, Some([2, 0, 6, 4])); + //Ok(()) +//} + +////#[test] +////fn test_misc () -> Usually<()> { + ////let area: [u16;4] = [0, 0, 10, 10]; + ////let test = TestArea(4, 4); + ////assert_eq!(test.layout(area)?, + ////Some([0, 0, 4, 4])); + ////assert_eq!(Align::Center(test).layout(area)?, + ////Some([3, 3, 4, 4])); + ////assert_eq!(Align::Center(Stack::down(|add|{ + ////add(&test)?; + ////add(&test) + ////})).layout(area)?, + ////Some([3, 1, 4, 8])); + ////assert_eq!(Align::Center(Stack::down(|add|{ + ////add(&Margin::XY(2, 2, test))?; + ////add(&test) + ////})).layout(area)?, + ////Some([2, 0, 6, 10])); + ////assert_eq!(Align::Center(Stack::down(|add|{ + ////add(&Margin::XY(2, 2, test))?; + ////add(&Padding::XY(2, 2, test)) + ////})).layout(area)?, + ////Some([2, 1, 6, 8])); + ////assert_eq!(Stack::down(|add|{ + ////add(&Margin::XY(2, 2, test))?; + ////add(&Padding::XY(2, 2, test)) + ////}).layout(area)?, + ////Some([0, 0, 6, 8])); + ////assert_eq!(Stack::right(|add|{ + ////add(&Stack::down(|add|{ + ////add(&Margin::XY(2, 2, test))?; + ////add(&Padding::XY(2, 2, test)) + ////}))?; + ////add(&Align::Center(TestArea(2 ,2))) + ////}).layout(area)?, + ////Some([0, 0, 8, 8])); + ////Ok(()) +////} + +////#[test] +////fn test_offset () -> Usually<()> { + ////let area: [u16;4] = [50, 50, 100, 100]; + ////let test = TestArea(3, 3); + ////assert_eq!(Push::X(1, test).layout(area)?, Some([51, 50, 3, 3])); + ////assert_eq!(Push::Y(1, test).layout(area)?, Some([50, 51, 3, 3])); + ////assert_eq!(Push::XY(1, 1, test).layout(area)?, Some([51, 51, 3, 3])); + ////Ok(()) +////} + +////#[test] +////fn test_outset () -> Usually<()> { + ////let area: [u16;4] = [50, 50, 100, 100]; + ////let test = TestArea(3, 3); + ////assert_eq!(Margin::X(1, test).layout(area)?, Some([49, 50, 5, 3])); + ////assert_eq!(Margin::Y(1, test).layout(area)?, Some([50, 49, 3, 5])); + ////assert_eq!(Margin::XY(1, 1, test).layout(area)?, Some([49, 49, 5, 5])); + ////Ok(()) +////} + +////#[test] +////fn test_padding () -> Usually<()> { + ////let area: [u16;4] = [50, 50, 100, 100]; + ////let test = TestArea(3, 3); + ////assert_eq!(Padding::X(1, test).layout(area)?, Some([51, 50, 1, 3])); + ////assert_eq!(Padding::Y(1, test).layout(area)?, Some([50, 51, 3, 1])); + ////assert_eq!(Padding::XY(1, 1, test).layout(area)?, Some([51, 51, 1, 1])); + ////Ok(()) +////} + +////#[test] +////fn test_stuff () -> Usually<()> { + ////let area: [u16;4] = [0, 0, 100, 100]; + ////assert_eq!("1".layout(area)?, + ////Some([0, 0, 1, 1])); + ////assert_eq!("333".layout(area)?, + ////Some([0, 0, 3, 1])); + ////assert_eq!(Layers::new(|add|{add(&"1")?;add(&"333")}).layout(area)?, + ////Some([0, 0, 3, 1])); + ////assert_eq!(Stack::down(|add|{add(&"1")?;add(&"333")}).layout(area)?, + ////Some([0, 0, 3, 2])); + ////assert_eq!(Stack::right(|add|{add(&"1")?;add(&"333")}).layout(area)?, + ////Some([0, 0, 4, 1])); + ////assert_eq!(Stack::down(|add|{ + ////add(&Stack::right(|add|{add(&"1")?;add(&"333")}))?; + ////add(&"55555") + ////}).layout(area)?, + ////Some([0, 0, 5, 2])); + ////let area: [u16;4] = [1, 1, 100, 100]; + ////assert_eq!(Margin::X(1, Stack::right(|add|{add(&"1")?;add(&"333")})).layout(area)?, + ////Some([0, 1, 6, 1])); + ////assert_eq!(Margin::Y(1, Stack::right(|add|{add(&"1")?;add(&"333")})).layout(area)?, + ////Some([1, 0, 4, 3])); + ////assert_eq!(Margin::XY(1, 1, Stack::right(|add|{add(&"1")?;add(&"333")})).layout(area)?, + ////Some([0, 0, 6, 3])); + ////assert_eq!(Stack::down(|add|{ + ////add(&Margin::XY(1, 1, "1"))?; + ////add(&Margin::XY(1, 1, "333")) + ////}).layout(area)?, + ////Some([1, 1, 5, 6])); + ////let area: [u16;4] = [1, 1, 95, 100]; + ////assert_eq!(Align::Center(Stack::down(|add|{ + ////add(&Margin::XY(1, 1, "1"))?; + ////add(&Margin::XY(1, 1, "333")) + ////})).layout(area)?, + ////Some([46, 48, 5, 6])); + ////assert_eq!(Align::Center(Stack::down(|add|{ + ////add(&Layers::new(|add|{ + //////add(&Margin::XY(1, 1, Background(Color::Rgb(0,128,0))))?; + ////add(&Margin::XY(1, 1, "1"))?; + ////add(&Margin::XY(1, 1, "333"))?; + //////add(&Background(Color::Rgb(0,128,0)))?; + ////Ok(()) + ////}))?; + ////add(&Layers::new(|add|{ + //////add(&Margin::XY(1, 1, Background(Color::Rgb(0,0,128))))?; + ////add(&Margin::XY(1, 1, "555"))?; + ////add(&Margin::XY(1, 1, "777777"))?; + //////add(&Background(Color::Rgb(0,0,128)))?; + ////Ok(()) + ////})) + ////})).layout(area)?, + ////Some([46, 48, 5, 6])); + ////Ok(()) +////} + +//#[derive(Default)] pub struct Sequencer { + //pub jack: Arc>, + //pub compact: bool, + //pub editor: MidiEditor, + //pub midi_buf: Vec>>, + //pub note_buf: Vec, + //pub perf: PerfModel, + //pub player: MidiPlayer, + //pub pool: MidiPool, + //pub selectors: bool, + //pub size: Measure, + //pub status: bool, + //pub transport: bool, +//} +//has_size!(|self:Sequencer|&self.size); +//has_clock!(|self:Sequencer|&self.player.clock); +//has_clips!(|self:Sequencer|self.pool.clips); +//has_editor!(|self:Sequencer|self.editor); +//has_player!(|self:Sequencer|self.player); + +//#[derive(Default)] pub struct Groovebox { + //pub jack: Arc>, + //pub compact: bool, + //pub editor: MidiEditor, + //pub midi_buf: Vec>>, + //pub note_buf: Vec, + //pub perf: PerfModel, + //pub player: MidiPlayer, + //pub pool: MidiPool, + //pub sampler: Sampler, + //pub size: Measure, + //pub status: bool, +//} +//has_clock!(|self: Groovebox|self.player.clock()); + +//#[derive(Default)] pub struct Arranger { + //pub clock: Clock, + //pub color: ItemPalette, + //pub compact: bool, + //pub editing: AtomicBool, + //pub editor: MidiEditor, + //pub jack: Arc>, + //pub midi_buf: Vec>>, + //pub midi_ins: Vec>, + //pub midi_outs: Vec>, + //pub note_buf: Vec, + //pub perf: PerfModel, + //pub pool: MidiPool, + //pub scenes: Vec, + //pub selected: ArrangerSelection, + //pub size: Measure, + //pub splits: [u16;2], + //pub tracks: Vec, +//} +//has_clock!(|self: Arranger|&self.clock); +//has_clips!(|self: Arranger|self.pool.clips); +//has_editor!(|self: Arranger|self.editor); + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +//render!(TuiOut: (self: Sequencer) => self.size.of(EdnView::from_source(self, Self::EDN))); +//impl EdnViewData for &Sequencer { + //fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> { + //use EdnItem::*; + //match item { + //Nil => Box::new(()), + //Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())), + //Sym(":editor") => (&self.editor).boxed(), + //Sym(":pool") => self.pool_view().boxed(), + //Sym(":status") => self.status_view().boxed(), + //Sym(":toolbar") => self.toolbar_view().boxed(), + //_ => panic!("no content for {item:?}") + //} + //} +//} +//impl Sequencer { + //const EDN: &'static str = include_str!("../edn/sequencer.edn"); + //fn toolbar_view (&self) -> impl Content + use<'_> { + //Fill::x(Fixed::y(2, Align::x(ClockView::new(true, &self.player.clock)))) + //} + //fn status_view (&self) -> impl Content + use<'_> { + //Bsp::e( + //When(self.selectors, Bsp::e( + //self.player.play_status(), + //self.player.next_status(), + //)), + //Bsp::e( + //self.editor.clip_status(), + //self.editor.edit_status(), + //) + //) + //} + //fn pool_view (&self) -> impl Content + use<'_> { + //let w = self.size.w(); + //let clip_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; + //let pool_w = if self.pool.visible { clip_w } else { 0 }; + //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(("[]", "clip"), ("{}", "order"), ), + //double(("q", "enqueue"), ("e", "edit"), ), + //double(("c", "color"), ("", ""),), + //)) + //} +//} +///////////////////////////////////////////////////////////////////////////////////////////////////// +//render!(TuiOut: (self: Groovebox) => self.size.of(EdnView::from_source(self, Self::EDN))); +//impl EdnViewData for &Groovebox { + //fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> { + //use EdnItem::*; + //match item { + //Nil => Box::new(()), + //Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())), + //Sym(":editor") => (&self.editor).boxed(), + //Sym(":pool") => self.pool().boxed(), + //Sym(":status") => self.status().boxed(), + //Sym(":toolbar") => self.toolbar().boxed(), + //Sym(":sampler") => self.sampler().boxed(), + //Sym(":sample") => self.sample().boxed(), + //_ => panic!("no content for {item:?}") + //} + //} + //fn get_unit (&self, item: EdnItem<&str>) -> u16 { + //use EdnItem::*; + //match item.to_str() { + //":sample-h" => if self.compact { 0 } else { 5 }, + //":samples-w" => if self.compact { 4 } else { 11 }, + //":samples-y" => if self.compact { 1 } else { 0 }, + //":pool-w" => if self.compact { 5 } else { + //let w = self.size.w(); + //if w > 60 { 20 } else if w > 40 { 15 } else { 10 } + //}, + //_ => 0 + //} + //} +//} +//impl Groovebox { + //const EDN: &'static str = include_str!("../edn/groovebox.edn"); + //fn toolbar (&self) -> impl Content + use<'_> { + //Fill::x(Fixed::y(2, lay!( + //Fill::x(Align::w(Meter("L/", self.sampler.input_meter[0]))), + //Fill::x(Align::e(Meter("R/", self.sampler.input_meter[1]))), + //Align::x(ClockView::new(true, &self.player.clock)), + //))) + //} + //fn status (&self) -> impl Content + use<'_> { + //row!( + //self.player.play_status(), + //self.player.next_status(), + //self.editor.clip_status(), + //self.editor.edit_status(), + //) + //} + //fn sample (&self) -> impl Content + use<'_> { + //let note_pt = self.editor.note_point(); + //let sample_h = if self.compact { 0 } else { 5 }; + //Max::y(sample_h, Fill::xy( + //Bsp::a( + //Fill::x(Align::w(Fixed::y(1, self.sampler.status(note_pt)))), + //self.sampler.viewer(note_pt)))) + //} + //fn pool (&self) -> impl Content + use<'_> { + //let w = self.size.w(); + //let pool_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; + //Fixed::x(if self.compact { 5 } else { pool_w }, + //PoolView(self.compact, &self.pool)) + //} + //fn sampler (&self) -> impl Content + use<'_> { + //let note_pt = self.editor.note_point(); + //let sampler_w = if self.compact { 4 } else { 40 }; + //let sampler_y = if self.compact { 1 } else { 0 }; + //Fixed::x(sampler_w, Push::y(sampler_y, Fill::y(self.sampler.list(self.compact, &self.editor)))) + //} +//} + +///// Status bar for sequencer app +//#[derive(Clone)] +//pub struct GrooveboxStatus { + //pub(crate) width: usize, + //pub(crate) cpu: Option, + //pub(crate) size: String, + //pub(crate) playing: bool, +//} +//from!(|state: &Groovebox|GrooveboxStatus = { + //let samples = state.clock().chunk.load(Relaxed); + //let rate = state.clock().timebase.sr.get(); + //let buffer = samples as f64 / rate; + //let width = state.size.w(); + //Self { + //width, + //playing: state.clock().is_rolling(), + //cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")), + //size: format!("{}x{}│", width, state.size.h()), + //} +//}); +//render!(TuiOut: (self: GrooveboxStatus) => Fixed::y(2, lay!( + //Self::help(), + //Fill::xy(Align::se(Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), self.stats()))), +//))); +//impl GrooveboxStatus { + //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) + //} +//} +//macro_rules! edn_context { + //($Struct:ident |$l:lifetime, $state:ident| { + //$($key:literal = $field:ident: $Type:ty => $expr:expr,)* + //}) => { + + //#[derive(Default)] + //pub struct EdnView<$l> { $($field: Option<$Type>),* } + + //impl<$l> EdnView<$l> { + //pub fn parse <'e> (edn: &[Edn<'e>]) -> impl Fn(&$Struct) + use<'e> { + //let imports = Self::imports_all(edn); + //move |state| { + //let mut context = EdnView::default(); + //for import in imports.iter() { + //context.import(state, import) + //} + //} + //} + //fn imports_all <'e> (edn: &[Edn<'e>]) -> Vec<&'e str> { + //let mut imports = vec![]; + //for edn in edn.iter() { + //for import in Self::imports_one(edn) { + //imports.push(import); + //} + //} + //imports + //} + //fn imports_one <'e> (edn: &Edn<'e>) -> Vec<&'e str> { + //match edn { + //Edn::Symbol(import) => vec![import], + //Edn::List(edn) => Self::imports_all(edn.as_slice()), + //_ => vec![], + //} + //} + //pub fn import (&mut self, $state: &$l$Struct, key: &str) { + //match key { + //$($key => self.$field = Some($expr),)* + //_ => {} + //} + //} + //} + //} +//} + +////impl Groovebox { + ////fn status (&self) -> impl Content + use<'_> { + ////let note_pt = self.editor.note_point(); + ////Align::w(Fixed::y(1, )) + ////} + ////fn pool (&self) -> impl Content + use<'_> { + ////let w = self.size.w(); + ////let pool_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; + ////Fixed::x(if self.compact { 5 } else { pool_w }, + ////) + ////} + ////fn sampler (&self) -> impl Content + use<'_> { + ////let sampler_w = if self.compact { 4 } else { 11 }; + ////let sampler_y = if self.compact { 1 } else { 0 }; + ////Fixed::x(sampler_w, Push::y(sampler_y, Fill::y( + ////SampleList::new(self.compact, &self.sampler, &self.editor)))) + ////} +////} +/////////////////////////////////////////////////////////////////////////////////////////////////// +//render!(TuiOut: (self: Arranger) => self.size.of(EdnView::from_source(self, Self::EDN))); +//impl EdnViewData for &Arranger { + //fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> { + //use EdnItem::*; + //let tracks_w = self.tracks_with_sizes().last().unwrap().3 as u16; + //match item { + //Nil => Box::new(()), + //Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())), + //Sym(":editor") => (&self.editor).boxed(), + //Sym(":pool") => self.pool().boxed(), + //Sym(":status") => self.status().boxed(), + //Sym(":toolbar") => self.toolbar().boxed(), + //Sym(":tracks") => self.track_row(tracks_w).boxed(), + //Sym(":scenes") => self.scene_row(tracks_w).boxed(), + //Sym(":inputs") => self.input_row(tracks_w).boxed(), + //Sym(":outputs") => self.output_row(tracks_w).boxed(), + //_ => panic!("no content for {item:?}") + //} + //} +//} +//impl Arranger { + //const EDN: &'static str = include_str!("../edn/arranger.edn"); + //pub const LEFT_SEP: char = '▎'; + + //fn toolbar (&self) -> impl Content + use<'_> { + //Fill::x(Fixed::y(2, Align::x(ClockView::new(true, &self.clock)))) + //} + //fn pool (&self) -> impl Content + use<'_> { + //Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact, &self.pool))) + //} + //fn status (&self) -> impl Content + use<'_> { + //Bsp::e(self.editor.clip_status(), self.editor.edit_status()) + //} + //fn is_editing (&self) -> bool { + // !self.pool.visible + //} + //fn sidebar_w (&self) -> u16 { + //let w = self.size.w(); + //let w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; + //let w = if self.pool.visible { w } else { 8 }; + //w + //} + //fn editor_w (&self) -> usize { + ////self.editor.note_len() / self.editor.note_zoom().get() + //(5 + (self.editor.time_len().get() / self.editor.time_zoom().get())) + //.min(self.size.w().saturating_sub(20)) + //.max(16) + ////self.editor.time_axis().get().max(16) + ////50 + //} + //pub fn scenes_with_sizes (&self, h: usize) + //-> impl Iterator + //{ + //let mut y = 0; + //let editing = self.is_editing(); + //let (selected_track, selected_scene) = match self.selected { + //ArrangerSelection::Clip(t, s) => (Some(t), Some(s)), + //_ => (None, None) + //}; + //self.scenes.iter().enumerate().map(move|(s, scene)|{ + //let active = editing && selected_track.is_some() && selected_scene == Some(s); + //let height = if active { 15 } else { h }; + //let data = (s, scene, y, y + height); + //y += height; + //data + //}) + //} + //pub fn tracks_with_sizes (&self) + //-> impl Iterator + //{ + //tracks_with_sizes(self.tracks.iter(), match self.selected { + //ArrangerSelection::Track(t) if self.is_editing() => Some(t), + //ArrangerSelection::Clip(t, _) if self.is_editing() => Some(t), + //_ => None + //}, self.editor_w()) + //} + + //fn play_row (&self, tracks_w: u16) -> impl Content + '_ { + //let h = 2; + //Fixed::y(h, Bsp::e( + //Fixed::xy(self.sidebar_w() as u16, h, self.play_header()), + //Fill::x(Align::c(Fixed::xy(tracks_w, h, self.play_cells()))) + //)) + //} + //fn play_header (&self) -> BoxThunk { + //(||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Playing")).boxed()).into() + //} + //fn play_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + //(move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| { + ////let color = track.color; + //let color: ItemPalette = track.color.dark.into(); + //let timebase = self.clock().timebase(); + //let value = Tui::fg_bg(color.lightest.rgb, color.base.rgb, + //if let Some((_, Some(clip))) = track.player.play_clip().as_ref() { + //let length = clip.read().unwrap().length; + //let elapsed = track.player.pulses_since_start().unwrap() as usize; + //format!("+{:>}", timebase.format_beats_1_short((elapsed % length) as f64)) + //} else { + //String::new() + //}); + //let cell = Bsp::s(value, phat_hi(color.dark.rgb, color.darker.rgb)); + //Tui::bg(color.base.rgb, map_east(x1 as u16, (x2 - x1) as u16, cell)) + //})).boxed()).into() + //} + + //fn next_row (&self, tracks_w: u16) -> impl Content + '_ { + //let h = 2; + //Fixed::y(h, Bsp::e( + //Fixed::xy(self.sidebar_w() as u16, h, self.next_header()), + //Fill::x(Align::c(Fixed::xy(tracks_w, h, self.next_cells()))) + //)) + //} + //fn next_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + //(||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Next")).boxed()).into() + //} + //fn next_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + //(move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| { + //let color: ItemPalette = track.color; + //let color: ItemPalette = track.color.dark.into(); + //let current = &self.clock().playhead; + //let timebase = ¤t.timebase; + //let cell = Self::cell(color, Tui::bold(true, { + //let mut result = String::new(); + //if let Some((t, _)) = track.player.next_clip().as_ref() { + //let target = t.pulse.get(); + //let current = current.pulse.get(); + //if target > current { + //result = format!("-{:>}", timebase.format_beats_0_short(target - current)) + //} + //} + //result + //})); + //let cell = Tui::fg_bg(color.lightest.rgb, color.base.rgb, cell); + //let cell = Bsp::s(cell, phat_hi(color.dark.rgb, color.darker.rgb)); + //Tui::bg(color.base.rgb, map_east(x1 as u16, (x2 - x1) as u16, cell)) + //})).boxed()).into() + //} + ///// beats until switchover + //fn cell_until_next (track: &ArrangerTrack, current: &Arc) + //-> Option> + //{ + //let timebase = ¤t.timebase; + //let mut result = String::new(); + //if let Some((t, _)) = track.player.next_clip().as_ref() { + //let target = t.pulse.get(); + //let current = current.pulse.get(); + //if target > current { + //result = format!("-{:>}", timebase.format_beats_0_short(target - current)) + //} + //} + //Some(result) + //} + + //fn track_row (&self, tracks_w: u16) -> impl Content + '_ { + //let h = 3; + //let border = |x|x;//Rugged(Style::default().fg(Color::Rgb(0,0,0)).bg(Color::Reset)).enclose2(x); + //Fixed::y(h, Bsp::e( + //Fixed::xy(self.sidebar_w() as u16, h, self.track_header()), + //Fill::x(Align::c(Fixed::xy(tracks_w, h, border(self.track_cells())))) + //)) + //} + //fn track_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + //(||Tui::bold(true, Bsp::s( + //row!( + //Tui::fg(TuiTheme::g(128), "add "), + //Tui::fg(TuiTheme::orange(), "t"), + //Tui::fg(TuiTheme::g(128), "rack"), + //), + //row!( + //Tui::fg(TuiTheme::orange(), "a"), + //Tui::fg(TuiTheme::g(128), "dd scene"), + //), + //).boxed())).into() + //} + //fn track_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + //let iter = ||self.tracks_with_sizes(); + //(move||Align::x(Map::new(iter, move|(_, track, x1, x2), i| { + //let name = Push::x(1, &track.name); + //let color = track.color; + //let fg = color.lightest.rgb; + //let bg = color.base.rgb; + //let active = self.selected.track() == Some(i); + //let bfg = if active { Color::Rgb(255,255,255) } else { Color::Rgb(0,0,0) }; + //let border = Style::default().fg(bfg).bg(bg); + //Tui::bg(bg, map_east(x1 as u16, (x2 - x1) as u16, + //Outer(border).enclose(Tui::fg_bg(fg, bg, Tui::bold(true, Fill::x(Align::x(name))))) + //)) + //})).boxed()).into() + //} + + //fn input_row (&self, tracks_w: u16) -> impl Content + '_ { + //let h = 2 + self.midi_ins[0].connect.len() as u16; + //Fixed::y(h, Bsp::e( + //Fixed::xy(self.sidebar_w() as u16, h, self.input_header()), + //Fill::x(Align::c(Fixed::xy(tracks_w, h, self.input_cells()))) + //)) + //} + //fn input_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + //(||Bsp::s( + //Tui::bold(true, row!( + //Tui::fg(TuiTheme::g(128), "midi "), + //Tui::fg(TuiTheme::orange(), "I"), + //Tui::fg(TuiTheme::g(128), "ns"), + //)), + //Bsp::s( + //Fill::x(Tui::bold(true, Tui::fg_bg(TuiTheme::g(224), TuiTheme::g(64), + //Align::w(&self.midi_ins[0].name)))), + //self.midi_ins.get(0) + //.and_then(|midi_in|midi_in.connect.get(0)) + //.map(|connect|Fill::x(Align::w( + //Tui::bold(false, Tui::fg_bg(TuiTheme::g(224), TuiTheme::g(64), connect.info()))))) + //) + //).boxed()).into() + //} + //fn input_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + //(move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| { + //let w = (x2 - x1) as u16; + //let color: ItemPalette = track.color.dark.into(); + //map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, Bsp::n( + //Self::rec_mon(color.base.rgb, false, false), + //phat_hi(color.base.rgb, color.dark.rgb) + //)))) + //})).boxed()).into() + //} + //fn rec_mon (bg: Color, rec: bool, mon: bool) -> impl Content { + //row!( + //Tui::fg_bg(if rec { Color::Red } else { bg }, bg, "▐"), + //Tui::fg_bg(if rec { Color::White } else { Color::Rgb(0,0,0) }, bg, "REC"), + //Tui::fg_bg(if rec { Color::White } else { bg }, bg, "▐"), + //Tui::fg_bg(if mon { Color::White } else { Color::Rgb(0,0,0) }, bg, "MON"), + //Tui::fg_bg(if mon { Color::White } else { bg }, bg, "▌"), + //) + //} + + //fn output_row (&self, tracks_w: u16) -> impl Content + '_ { + //let h = 2 + self.midi_outs[0].connect.len() as u16; + //Fixed::y(h, Bsp::e( + //Fixed::xy(self.sidebar_w() as u16, h, self.output_header()), + //Fill::x(Align::c(Fixed::xy(tracks_w, h, self.output_cells()))) + //)) + //} + //fn output_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + //(||Bsp::s( + //Tui::bold(true, row!( + //Tui::fg(TuiTheme::g(128), "midi "), + //Tui::fg(TuiTheme::orange(), "O"), + //Tui::fg(TuiTheme::g(128), "uts"), + //)), + //Bsp::s( + //Fill::x(Tui::bold(true, Tui::fg_bg(TuiTheme::g(224), TuiTheme::g(64), + //Align::w(&self.midi_outs[0].name)))), + //self.midi_outs.get(0) + //.and_then(|midi_out|midi_out.connect.get(0)) + //.map(|connect|Fill::x(Align::w( + //Tui::bold(false, Tui::fg_bg(TuiTheme::g(224), TuiTheme::g(64), connect.info()))))) + //), + //).boxed()).into() + //} + //fn output_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + //(move||Align::x(Map::new(||self.tracks_with_sizes(), move|(_, track, x1, x2), i| { + //let w = (x2 - x1) as u16; + //let color: ItemPalette = track.color.dark.into(); + //map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, Bsp::n( + //Self::mute_solo(color.base.rgb, false, false), + //phat_hi(color.dark.rgb, color.darker.rgb) + //)))) + //})).boxed()).into() + //} + //fn mute_solo (bg: Color, mute: bool, solo: bool) -> impl Content { + //row!( + //Tui::fg_bg(if mute { Color::White } else { Color::Rgb(0,0,0) }, bg, "MUTE"), + //Tui::fg_bg(if mute { Color::White } else { bg }, bg, "▐"), + //Tui::fg_bg(if solo { Color::White } else { Color::Rgb(0,0,0) }, bg, "SOLO"), + //) + //} + + //fn scene_row (&self, tracks_w: u16) -> impl Content + '_ { + //let h = (self.size.h() as u16).saturating_sub(8).max(8); + //let border = |x|x;//Skinny(Style::default().fg(Color::Rgb(0,0,0)).bg(Color::Reset)).enclose2(x); + //Bsp::e( + //Tui::bg(Color::Reset, Fixed::xy(self.sidebar_w() as u16, h, self.scene_headers())), + //Tui::bg(Color::Reset, Fill::x(Align::c(Fixed::xy(tracks_w, h, border(self.scene_cells()))))) + //) + //} + //fn scene_headers <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + //(||{ + //let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0)))); + //let selected = self.selected.scene(); + //Fill::y(Align::c(Map::new(||self.scenes_with_sizes(2), move|(_, scene, y1, y2), i| { + //let h = (y2 - y1) as u16; + //let name = format!("🭬{}", &scene.name); + //let color = scene.color; + //let active = selected == Some(i); + //let mid = if active { color.light } else { color.base }; + //let top = Some(last_color.read().unwrap().base.rgb); + //let cell = phat_sel_3( + //active, + //Tui::bold(true, name.clone()), + //Tui::bold(true, name), + //top, + //mid.rgb, + //Color::Rgb(0, 0, 0) + //); + //*last_color.write().unwrap() = color; + //map_south(y1 as u16, h + 1, Fixed::y(h + 1, cell)) + //}))).boxed() + //}).into() + //} + //fn scene_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { + //let editing = self.is_editing(); + //let tracks = move||self.tracks_with_sizes(); + //let scenes = ||self.scenes_with_sizes(2); + //let selected_track = self.selected.track(); + //let selected_scene = self.selected.scene(); + //(move||Fill::y(Align::c(Map::new(tracks, move|(_, track, x1, x2), t| { + //let w = (x2 - x1) as u16; + //let color: ItemPalette = track.color.dark.into(); + //let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0)))); + //let cells = Map::new(scenes, move|(_, scene, y1, y2), s| { + //let h = (y2 - y1) as u16; + //let color = scene.color; + //let (name, fg, bg) = if let Some(c) = &scene.clips[t] { + //let c = c.read().unwrap(); + //(c.name.to_string(), c.color.lightest.rgb, c.color.base.rgb) + //} else { + //("⏹ ".to_string(), TuiTheme::g(64), TuiTheme::g(32)) + //}; + //let last = last_color.read().unwrap().clone(); + //let active = editing && selected_scene == Some(s) && selected_track == Some(t); + //let editor = Thunk::new(||&self.editor); + //let cell = Thunk::new(move||phat_sel_3( + //selected_track == Some(t) && selected_scene == Some(s), + //Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))), + //Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))), + //if selected_track == Some(t) && selected_scene.map(|s|s+1) == Some(s) { + //None + //} else { + //Some(bg.into()) + //}, + //bg.into(), + //bg.into(), + //)); + //let cell = Either(active, editor, cell); + //*last_color.write().unwrap() = bg.into(); + //map_south( + //y1 as u16, + //h + 1, + //Fill::x(Fixed::y(h + 1, cell)) + //) + //}); + //let column = Fixed::x(w, Tui::bg(Color::Reset, Align::y(cells)).boxed()); + //Fixed::x(w, map_east(x1 as u16, w, column)) + //}))).boxed()).into() + //} + //fn cell_clip <'a> ( + //scene: &'a ArrangerScene, index: usize, track: &'a ArrangerTrack, w: u16, h: u16 + //) -> impl Content + use<'a> { + //scene.clips.get(index).map(|clip|clip.as_ref().map(|clip|{ + //let clip = clip.read().unwrap(); + //let mut bg = TuiTheme::border_bg(); + //let name = clip.name.to_string(); + //let max_w = name.len().min((w as usize).saturating_sub(2)); + //let color = clip.color; + //bg = color.dark.rgb; + //if let Some((_, Some(ref playing))) = track.player.play_clip() { + //if *playing.read().unwrap() == *clip { + //bg = color.light.rgb + //} + //}; + //Fixed::xy(w, h, &Tui::bg(bg, Push::x(1, Fixed::x(w, &name.as_str()[0..max_w])))); + //})) + //} + + //fn track_column_separators <'a> (&'a self) -> impl Content + 'a { + //let scenes_w = 16;//.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16); + //let fg = Color::Rgb(64,64,64); + //Map::new(move||self.tracks_with_sizes(), move|(_n, _track, x1, x2), _i|{ + //Push::x(scenes_w, map_east(x1 as u16, (x2 - x1) as u16, + //Fixed::x((x2 - x1) as u16, Tui::fg(fg, RepeatV(&"·"))))) + //}) + //} + + //pub fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> { + //let mut widths = vec![]; + //let mut total = 0; + //for track in tracks.iter() { + //let width = track.width; + //widths.push((width, total)); + //total += width; + //} + //widths.push((0, total)); + //widths + //} + + //fn scene_row_sep <'a> (&'a self) -> impl Content + 'a { + //let fg = Color::Rgb(255,255,255); + //Map::new(move||self.scenes_with_sizes(1), |_, _|"") + ////Map(||rows.iter(), |(_n, _scene, y1, _y2), _i| { + ////let y = to.area().y() + (y / PPQ) as u16 + 1; + ////if y >= to.buffer.area.height { break } + ////for x in to.area().x()..to.area().x2().saturating_sub(2) { + //////if x < to.buffer.area.x && y < to.buffer.area.y { + ////if let Some(cell) = to.buffer.cell_mut(ratatui::prelude::Position::from((x, y))) { + ////cell.modifier = Modifier::UNDERLINED; + ////cell.underline_color = fg; + ////} + //////} + ////} + ////}) + //} + + //pub fn scene_heights (scenes: &[ArrangerScene], factor: usize) -> Vec<(usize, usize)> { + //let mut total = 0; + //if factor == 0 { + //scenes.iter().map(|scene|{ + //let pulses = scene.pulses().max(PPQ); + //total += pulses; + //(pulses, total - pulses) + //}).collect() + //} else { + //(0..=scenes.len()).map(|i|{ + //(factor*PPQ, factor*PPQ*i) + //}).collect() + //} + //} + //fn cursor (&self) -> impl Content + '_ { + //let color = self.color; + //let bg = color.lighter.rgb;//Color::Rgb(0, 255, 0); + //let selected = self.selected(); + //let cols = Arranger::track_widths(&self.tracks); + //let rows = Arranger::scene_heights(&self.scenes, 1); + //let scenes_w = 16.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16); + //let focused = true; + //let reticle = Reticle(Style { + //fg: Some(self.color.lighter.rgb), + //bg: None, + //underline_color: None, + //add_modifier: Modifier::empty(), + //sub_modifier: Modifier::DIM + //}); + //RenderThunk::new(move|to: &mut TuiOut|{ + //let area = to.area(); + //let [x, y, w, h] = area.xywh(); + //let mut track_area: Option<[u16;4]> = match selected { + //ArrangerSelection::Track(t) | ArrangerSelection::Clip(t, _) => Some([ + //x + scenes_w + cols[t].1 as u16, y, + //cols[t].0 as u16, h, + //]), + //_ => None + //}; + //let mut scene_area: Option<[u16;4]> = match selected { + //ArrangerSelection::Scene(s) | ArrangerSelection::Clip(_, s) => Some([ + //x, y + HEADER_H + (rows[s].1 / PPQ) as u16, + //w, (rows[s].0 / PPQ) as u16 + //]), + //_ => None + //}; + //let mut clip_area: Option<[u16;4]> = match selected { + //ArrangerSelection::Clip(t, s) => Some([ + //(scenes_w + x + cols[t].1 as u16).saturating_sub(1), + //HEADER_H + y + (rows[s].1/PPQ) as u16, + //cols[t].0 as u16 + 2, + //(rows[s].0 / PPQ) as u16 + //]), + //_ => None + //}; + //if let Some([x, y, width, height]) = track_area { + //to.fill_fg([x, y, 1, height], bg); + //to.fill_fg([x + width, y, 1, height], bg); + //} + //if let Some([_, y, _, height]) = scene_area { + //to.fill_ul([x, y - 1, w, 1], bg); + //to.fill_ul([x, y + height - 1, w, 1], bg); + //} + //if focused { + //to.place(if let Some(clip_area) = clip_area { + //clip_area + //} else if let Some(track_area) = track_area { + //track_area.clip_h(HEADER_H) + //} else if let Some(scene_area) = scene_area { + //scene_area.clip_w(scenes_w) + //} else { + //area.clip_w(scenes_w).clip_h(HEADER_H) + //}, &reticle) + //}; + //}) + //} + + ///// A 1-row cell. + //fn cell > (color: ItemPalette, field: T) -> impl Content { + //Tui::fg_bg(color.lightest.rgb, color.base.rgb, Fixed::y(1, field)) + //} +//} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +//handle!(TuiIn: |self: Sequencer, input|SequencerCommand::execute_with_state(self, input.event())); +//handle!(TuiIn: |self: Groovebox, input|GrooveboxCommand::execute_with_state(self, input.event())); +//handle!(TuiIn: |self: Arranger, input|ArrangerCommand::execute_with_state(self, input.event())); +//use SequencerCommand as SeqCmd; +//use GrooveboxCommand as GrvCmd; +//use ArrangerCommand as ArrCmd; +//#[derive(Clone, Debug)] pub enum SequencerCommand { + //Compact(bool), + //History(isize), + //Clock(ClockCommand), + //Pool(PoolCommand), + //Editor(MidiEditCommand), + //Enqueue(Option>>), +//} +//#[derive(Clone, Debug)] pub enum GrooveboxCommand { + //Compact(bool), + //History(isize), + //Clock(ClockCommand), + //Pool(PoolCommand), + //Editor(MidiEditCommand), + //Enqueue(Option>>), + //Sampler(SamplerCommand), +//} +//#[derive(Clone, Debug)] pub enum ArrangerCommand { + //History(isize), + //Color(ItemPalette), + //Clock(ClockCommand), + //Scene(SceneCommand), + //Track(TrackCommand), + //Clip(ClipCommand), + //Select(ArrangerSelection), + //Zoom(usize), + //Pool(PoolCommand), + //Editor(MidiEditCommand), + //StopAll, + //Clear, +//} + +//command!(|self: SequencerCommand, state: Sequencer|match self { + //Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?, + //Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?, + //Self::Enqueue(clip) => { state.player.enqueue_next(clip.as_ref()); None }, + //Self::History(delta) => { todo!("undo/redo") }, + + //Self::Pool(cmd) => match cmd { + //// autoselect: automatically load selected clip in editor + //PoolCommand::Select(_) => { + //let undo = cmd.delegate(&mut state.pool, Self::Pool)?; + //state.editor.set_clip(state.pool.clip().as_ref()); + //undo + //}, + //// update color in all places simultaneously + //PoolCommand::Clip(PoolCmd::SetColor(index, _)) => { + //let undo = cmd.delegate(&mut state.pool, Self::Pool)?; + //state.editor.set_clip(state.pool.clip().as_ref()); + //undo + //}, + //_ => cmd.delegate(&mut state.pool, Self::Pool)? + //}, + //Self::Compact(compact) => if state.compact != compact { + //state.compact = compact; + //Some(Self::Compact(!compact)) + //} else { + //None + //}, +//}); +//command!(|self: GrooveboxCommand, state: Groovebox|match self { + //Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?, + //Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?, + //Self::Enqueue(clip) => { state.player.enqueue_next(clip.as_ref()); None }, + //Self::History(delta) => { todo!("undo/redo") }, + //Self::Sampler(cmd) => cmd.delegate(&mut state.sampler, Self::Sampler)?, + + //Self::Pool(cmd) => match cmd { + //// autoselect: automatically load selected clip in editor + //PoolCommand::Select(_) => { + //let undo = cmd.delegate(&mut state.pool, Self::Pool)?; + //state.editor.set_clip(state.pool.clip().as_ref()); + //undo + //}, + //// update color in all places simultaneously + //PoolCommand::Clip(PoolCmd::SetColor(index, _)) => { + //let undo = cmd.delegate(&mut state.pool, Self::Pool)?; + //state.editor.set_clip(state.pool.clip().as_ref()); + //undo + //}, + //_ => cmd.delegate(&mut state.pool, Self::Pool)? + //}, + //Self::Compact(compact) => if state.compact != compact { + //state.compact = compact; + //Some(Self::Compact(!compact)) + //} else { + //None + //}, +//}); +//command!(|self: ArrangerCommand, state: Arranger|match self { + //Self::Clear => { todo!() }, + //Self::Clip(cmd) => cmd.delegate(state, Self::Clip)?, + //Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?, + //Self::Editor(cmd) => cmd.delegate(&mut state.editor, Self::Editor)?, + //Self::History(_) => { todo!() }, + //Self::Scene(cmd) => cmd.delegate(state, Self::Scene)?, + //Self::Select(s) => { state.selected = s; None }, + //Self::Track(cmd) => cmd.delegate(state, Self::Track)?, + //Self::Zoom(_) => { todo!(); }, + + //Self::StopAll => { + //for track in 0..state.tracks.len() { state.tracks[track].player.enqueue_next(None); } + //None + //}, + //Self::Color(palette) => { + //let old = state.color; + //state.color = palette; + //Some(Self::Color(old)) + //}, + //Self::Pool(cmd) => { + //match cmd { + //// autoselect: automatically load selected clip in editor + //PoolCommand::Select(_) => { + //let undo = cmd.delegate(&mut state.pool, Self::Pool)?; + //state.editor.set_clip(state.pool.clip().as_ref()); + //undo + //}, + //// reload clip in editor to update color + //PoolCommand::Clip(PoolClipCommand::SetColor(index, _)) => { + //let undo = cmd.delegate(&mut state.pool, Self::Pool)?; + //state.editor.set_clip(state.pool.clip().as_ref()); + //undo + //}, + //_ => cmd.delegate(&mut state.pool, Self::Pool)? + //} + //}, +//}); +//command!(|self: SceneCommand, state: Arranger|match self { + //Self::Add => { state.scene_add(None, None)?; None } + //Self::Del(index) => { state.scene_del(index); None }, + //Self::SetColor(index, color) => { + //let old = state.scenes[index].color; + //state.scenes[index].color = color; + //Some(Self::SetColor(index, old)) + //}, + //Self::Enqueue(scene) => { + //for track in 0..state.tracks.len() { + //state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref()); + //} + //None + //}, + //_ => None +//}); +//command!(|self: TrackCommand, state: Arranger|match self { + //Self::Add => { state.track_add(None, None)?; None }, + //Self::Del(index) => { state.track_del(index); None }, + //Self::Stop(track) => { state.tracks[track].player.enqueue_next(None); None }, + //Self::SetColor(index, color) => { + //let old = state.tracks[index].color; + //state.tracks[index].color = color; + //Some(Self::SetColor(index, old)) + //}, + //_ => None +//}); +//command!(|self: ClipCommand, state: Arranger|match self { + //Self::Get(track, scene) => { todo!() }, + //Self::Put(track, scene, clip) => { + //let old = state.scenes[scene].clips[track].clone(); + //state.scenes[scene].clips[track] = clip; + //Some(Self::Put(track, scene, old)) + //}, + //Self::Enqueue(track, scene) => { + //state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref()); + //None + //}, + //_ => None +//}); +//keymap!(KEYS_SEQUENCER = |state: Sequencer, input: Event| SequencerCommand { + //// TODO: k: toggle on-screen keyboard + //ctrl(key(Char('k'))) => { todo!("keyboard") }, + //// Transport: Play/pause + //key(Char(' ')) => SeqCmd::Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }), + //// Transport: Play from start or rewind to start + //shift(key(Char(' '))) => SeqCmd::Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }), + //// u: undo + //key(Char('u')) => SeqCmd::History(-1), + //// Shift-U: redo + //key(Char('U')) => SeqCmd::History( 1), + //// Tab: Toggle compact mode + //key(Tab) => SeqCmd::Compact(!state.compact), + //// q: Enqueue currently edited clip + //key(Char('q')) => SeqCmd::Enqueue(state.pool.clip().clone()), + //// 0: Enqueue clip 0 (stop all) + //key(Char('0')) => SeqCmd::Enqueue(Some(state.clips()[0].clone())), + //// e: Toggle between editing currently playing or other clip + ////key(Char('e')) => if let Some((_, Some(playing))) = state.player.play_clip() { + ////let editing = state.editor.clip().as_ref().map(|p|p.read().unwrap().clone()); + ////let selected = state.pool.clip().clone(); + ////SeqCmd::Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing { + ////selected + ////} else { + ////playing.clone() + ////}))) + ////} else { + ////return None + ////} +//}, if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) { + //SeqCmd::Editor(command) +//} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) { + //SeqCmd::Pool(command) +//} else { + //return None +//}); +//keymap!(<'a> KEYS_GROOVEBOX = |state: Groovebox, input: Event| GrooveboxCommand { + //// Tab: Toggle compact mode + //key(Tab) => GrvCmd::Compact(!state.compact), + //// q: Enqueue currently edited clip + //key(Char('q')) => GrvCmd::Enqueue(state.pool.clip().clone()), + //// 0: Enqueue clip 0 (stop all) + //key(Char('0')) => GrvCmd::Enqueue(Some(state.pool.clips()[0].clone())), + //// TODO: k: toggle on-screen keyboard + //ctrl(key(Char('k'))) => todo!("keyboard"), + //// Transport: Play from start or rewind to start + //ctrl(key(Char(' '))) => GrvCmd::Clock( + //if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) } + //), + //// Shift-R: toggle recording + //shift(key(Char('R'))) => GrvCmd::Sampler(if state.sampler.recording.is_some() { + //SmplCmd::RecordFinish + //} else { + //SmplCmd::RecordBegin(u7::from(state.editor.note_point() as u8)) + //}), + //// Shift-Del: delete sample + //shift(key(Delete)) => GrvCmd::Sampler( + //SmplCmd::SetSample(u7::from(state.editor.note_point() as u8), None) + //), + //// e: Toggle between editing currently playing or other clip + ////shift(key(Char('e'))) => if let Some((_, Some(playing))) = state.player.play_clip() { + ////let editing = state.editor.clip().as_ref().map(|p|p.read().unwrap().clone()); + ////let selected = state.pool.clip().clone().map(|s|s.read().unwrap().clone()); + ////GrvCmd::Editor(Show(if selected != editing { + ////selected + ////} else { + ////Some(playing.clone()) + ////})) + ////} else { + ////return None + ////}, +//}, if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) { + //GrvCmd::Editor(command) +//} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) { + //GrvCmd::Pool(command) +//} else { + //return None +//}); +//keymap!(KEYS_ARRANGER = |state: Arranger, input: Event| ArrangerCommand { + //key(Char('u')) => ArrCmd::History(-1), + //key(Char('U')) => ArrCmd::History(1), + //// TODO: k: toggle on-screen keyboard + //ctrl(key(Char('k'))) => { todo!("keyboard") }, + //// Transport: Play/pause + //key(Char(' ')) => ArrCmd::Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }), + //// Transport: Play from start or rewind to start + //shift(key(Char(' '))) => ArrCmd::Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }), + //key(Char('e')) => ArrCmd::Editor(MidiEditCommand::Show(state.pool.clip().clone())), + //ctrl(key(Char('a'))) => ArrCmd::Scene(SceneCommand::Add), + //ctrl(key(Char('A'))) => return None,//ArrCmd::Scene(SceneCommand::Add), + //ctrl(key(Char('t'))) => ArrCmd::Track(TrackCommand::Add), + //// Tab: Toggle visibility of clip pool column + //key(Tab) => ArrCmd::Pool(PoolCommand::Show(!state.pool.visible)), +//}, { + //use ArrangerSelection as Selected; + //use SceneCommand as Scene; + //use TrackCommand as Track; + //use ClipCommand as Clip; + //let t_len = state.tracks.len(); + //let s_len = state.scenes.len(); + //match state.selected { + //Selected::Clip(t, s) => clip_keymap(state, input, t, s), + //Selected::Scene(s) => scene_keymap(state, input, s), + //Selected::Track(t) => track_keymap(state, input, t), + //Selected::Mix => match input { + + //kpat!(Delete) => Some(ArrCmd::Clear), + //kpat!(Char('0')) => Some(ArrCmd::StopAll), + //kpat!(Char('c')) => Some(ArrCmd::Color(ItemPalette::random())), + + //kpat!(Up) => return None, + //kpat!(Down) => Some(ArrCmd::Select(Selected::Scene(0))), + //kpat!(Left) => return None, + //kpat!(Right) => Some(ArrCmd::Select(Selected::Track(0))), + + //_ => None + //}, + //} +//}.or_else(||if let Some(command) = MidiEditCommand::input_to_command(&state.editor, input) { + //Some(ArrCmd::Editor(command)) +//} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) { + //Some(ArrCmd::Pool(command)) +//} else { + //None +//})?); + +//fn clip_keymap (state: &Arranger, input: &Event, t: usize, s: usize) -> Option { + //use ArrangerSelection as Selected; + //use SceneCommand as Scene; + //use TrackCommand as Track; + //use ClipCommand as Clip; + //let t_len = state.tracks.len(); + //let s_len = state.scenes.len(); + //Some(match input { + + //kpat!(Char('g')) => ArrCmd::Pool(PoolCommand::Select(0)), + //kpat!(Char('q')) => ArrCmd::Clip(Clip::Enqueue(t, s)), + //kpat!(Char('l')) => ArrCmd::Clip(Clip::SetLoop(t, s, false)), + + //kpat!(Enter) => if state.scenes[s].clips[t].is_none() { + //// FIXME: get this clip from the pool (autoregister via intmut) + //let (_, clip) = state.add_clip(); + //ArrCmd::Clip(Clip::Put(t, s, Some(clip))) + //} else { + //return None + //}, + //kpat!(Delete) => ArrCmd::Clip(Clip::Put(t, s, None)), + //kpat!(Char('p')) => ArrCmd::Clip(Clip::Put(t, s, state.pool.clip().clone())), + //kpat!(Char(',')) => ArrCmd::Clip(Clip::Put(t, s, None)), + //kpat!(Char('.')) => ArrCmd::Clip(Clip::Put(t, s, None)), + //kpat!(Char('<')) => ArrCmd::Clip(Clip::Put(t, s, None)), + //kpat!(Char('>')) => ArrCmd::Clip(Clip::Put(t, s, None)), + + //kpat!(Up) => ArrCmd::Select(if s > 0 { Selected::Clip(t, s - 1) } else { Selected::Track(t) }), + //kpat!(Down) => ArrCmd::Select(Selected::Clip(t, (s + 1).min(s_len.saturating_sub(1)))), + //kpat!(Left) => ArrCmd::Select(if t > 0 { Selected::Clip(t - 1, s) } else { Selected::Scene(s) }), + //kpat!(Right) => ArrCmd::Select(Selected::Clip((t + 1).min(t_len.saturating_sub(1)), s)), + + //_ => return None + //}) +//} +//fn scene_keymap (state: &Arranger, input: &Event, s: usize) -> Option { + //use ArrangerSelection as Selected; + //use SceneCommand as Scene; + //use TrackCommand as Track; + //use ClipCommand as Clip; + //let t_len = state.tracks.len(); + //let s_len = state.scenes.len(); + //Some(match input { + + //kpat!(Char(',')) => ArrCmd::Scene(Scene::Swap(s, s - 1)), + //kpat!(Char('.')) => ArrCmd::Scene(Scene::Swap(s, s + 1)), + //kpat!(Char('<')) => ArrCmd::Scene(Scene::Swap(s, s - 1)), + //kpat!(Char('>')) => ArrCmd::Scene(Scene::Swap(s, s + 1)), + //kpat!(Char('q')) => ArrCmd::Scene(Scene::Enqueue(s)), + //kpat!(Delete) => ArrCmd::Scene(Scene::Del(s)), + //kpat!(Char('c')) => ArrCmd::Scene(Scene::SetColor(s, ItemPalette::random())), + + //kpat!(Up) => ArrCmd::Select(if s > 0 { Selected::Scene(s - 1) } else { Selected::Mix }), + //kpat!(Down) => ArrCmd::Select(Selected::Scene((s + 1).min(s_len.saturating_sub(1)))), + //kpat!(Left) => return None, + //kpat!(Right) => ArrCmd::Select(Selected::Clip(0, s)), + + //_ => return None + //}) +//} +//fn track_keymap (state: &Arranger, input: &Event, t: usize) -> Option { + //use ArrangerSelection as Selected; + //use SceneCommand as Scene; + //use TrackCommand as Track; + //use ClipCommand as Clip; + //let t_len = state.tracks.len(); + //let s_len = state.scenes.len(); + //Some(match input { + + //kpat!(Char(',')) => ArrCmd::Track(Track::Swap(t, t - 1)), + //kpat!(Char('.')) => ArrCmd::Track(Track::Swap(t, t + 1)), + //kpat!(Char('<')) => ArrCmd::Track(Track::Swap(t, t - 1)), + //kpat!(Char('>')) => ArrCmd::Track(Track::Swap(t, t + 1)), + //kpat!(Delete) => ArrCmd::Track(Track::Del(t)), + //kpat!(Char('c')) => ArrCmd::Track(Track::SetColor(t, ItemPalette::random())), + + //kpat!(Up) => return None, + //kpat!(Down) => ArrCmd::Select(Selected::Clip(t, 0)), + //kpat!(Left) => ArrCmd::Select(if t > 0 { Selected::Track(t - 1) } else { Selected::Mix }), + //kpat!(Right) => ArrCmd::Select(Selected::Track((t + 1).min(t_len.saturating_sub(1)))), + + //_ => return None + //}) +//} +//impl HasJack for Arranger { + //fn jack (&self) -> &Arc> { &self.jack } +//} + +//audio!(|self: Sequencer, client, scope|{ + //// Start profiling cycle + //let t0 = self.perf.get_t0(); + + //// Update transport clock + //if Control::Quit == ClockAudio(self).process(client, scope) { + //return Control::Quit + //} + //// Update MIDI sequencer + //if Control::Quit == PlayerAudio( + //&mut self.player, &mut self.note_buf, &mut self.midi_buf + //).process(client, scope) { + //return Control::Quit + //} + + //// End profiling cycle + //self.perf.update(t0, scope); + + //Control::Continue +//}); + +//audio!(|self: Groovebox, client, scope|{ + //// Start profiling cycle + //let t0 = self.perf.get_t0(); + + //// Update transport clock + //if Control::Quit == ClockAudio(&mut self.player).process(client, scope) { + //return Control::Quit + //} + + //// Update MIDI sequencer + //if Control::Quit == PlayerAudio( + //&mut self.player, &mut self.note_buf, &mut self.midi_buf + //).process(client, scope) { + //return Control::Quit + //} + + //// Update sampler + //if Control::Quit == SamplerAudio(&mut self.sampler).process(client, scope) { + //return Control::Quit + //} + + //// TODO move these to editor and sampler: + //for RawMidi { time, bytes } in self.player.midi_ins[0].port.iter(scope) { + //if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { + //match message { + //MidiMessage::NoteOn { ref key, .. } => { + //self.editor.set_note_point(key.as_int() as usize); + //}, + //MidiMessage::Controller { controller, value } => { + //if let Some(sample) = &self.sampler.mapped[self.editor.note_point()] { + //sample.write().unwrap().handle_cc(controller, value) + //} + //} + //_ => {} + //} + //} + //} + + //// End profiling cycle + //self.perf.update(t0, scope); + + //Control::Continue +//}); + +//audio!(|self: Arranger, client, scope|{ + //// Start profiling cycle + //let t0 = self.perf.get_t0(); + + //// Update transport clock + //if Control::Quit == ClockAudio(self).process(client, scope) { + //return Control::Quit + //} + + ////// Update MIDI sequencers + ////let tracks = &mut self.tracks; + ////let note_buf = &mut self.note_buf; + ////let midi_buf = &mut self.midi_buf; + ////if Control::Quit == TracksAudio(tracks, note_buf, midi_buf).process(client, scope) { + ////return Control::Quit + ////} + + //// FIXME: one of these per playing track + ////self.now.set(0.); + ////if let ArrangerSelection::Clip(t, s) = self.selected { + ////let clip = self.scenes.get(s).map(|scene|scene.clips.get(t)); + ////if let Some(Some(Some(clip))) = clip { + ////if let Some(track) = self.tracks().get(t) { + ////if let Some((ref started_at, Some(ref playing))) = track.player.play_clip { + ////let clip = clip.read().unwrap(); + ////if *playing.read().unwrap() == *clip { + ////let pulse = self.current().pulse.get(); + ////let start = started_at.pulse.get(); + ////let now = (pulse - start) % clip.length as f64; + ////self.now.set(now); + ////} + ////} + ////} + ////} + ////} + + //// End profiling cycle + //self.perf.update(t0, scope); + //return Control::Continue +//}); + //fn get_device_mut (&self, i: usize) -> Option>>> { + //self.devices.get(i).map(|d|d.state.write().unwrap()) + //} + //pub fn device_mut (&self) -> Option>>> { + //self.get_device_mut(self.device) + //} + ///// Add a device to the end of the chain. + //pub fn append_device (&mut self, device: JackDevice) -> Usually<&mut JackDevice> { + //self.devices.push(device); + //let index = self.devices.len() - 1; + //Ok(&mut self.devices[index]) + //} + //pub fn add_device (&mut self, device: JackDevice) { + //self.devices.push(device); + //} + //pub fn connect_first_device (&self) -> Usually<()> { + //if let (Some(port), Some(device)) = (&self.midi_out, self.devices.get(0)) { + //device.client.as_client().connect_ports(&port, &device.midi_ins()?[0])?; + //} + //Ok(()) + //} + //pub fn connect_last_device (&self, app: &Track) -> Usually<()> { + //Ok(match self.devices.get(self.devices.len().saturating_sub(1)) { + //Some(device) => { + //app.audio_out(0).map(|left|device.connect_audio_out(0, &left)).transpose()?; + //app.audio_out(1).map(|right|device.connect_audio_out(1, &right)).transpose()?; + //() + //}, + //None => () + //}) + //} +use crate::*; +impl MixerTrack { + pub fn new (name: &str) -> Usually { + Ok(Self { + name: name.to_string().into(), + audio_ins: vec![], + audio_outs: vec![], + devices: vec![], + //ports: JackPorts::default(), + //devices: vec![], + //device: 0, + }) + } +} + +pub struct TrackView<'a> { + pub chain: Option<&'a MixerTrack>, + pub direction: Direction, + pub focused: bool, + pub entered: bool, +} + +impl<'a> Content for TrackView<'a> { + fn render (&self, to: &mut TuiOut) { + todo!(); + //let mut area = to.area(); + //if let Some(chain) = self.chain { + //match self.direction { + //Direction::Down => area.width = area.width.min(40), + //Direction::Right => area.width = area.width.min(10), + //_ => { unimplemented!() }, + //} + //to.fill_bg(to.area(), Nord::bg_lo(self.focused, self.entered)); + //let mut split = Stack::new(self.direction); + //for device in chain.devices.as_slice().iter() { + //split = split.add_ref(device); + //} + //let (area, areas) = split.render_areas(to)?; + //if self.focused && self.entered && areas.len() > 0 { + //Corners(Style::default().green().not_dim()).draw(to.with_rect(areas[0]))?; + //} + //Ok(Some(area)) + //} else { + //let [x, y, width, height] = area; + //let label = "No chain selected"; + //let x = x + (width - label.len() as u16) / 2; + //let y = y + height / 2; + //to.blit(&label, x, y, Some(Style::default().dim().bold()))?; + //Ok(Some(area)) + //} + } +} + +//impl Content for Mixer { + //fn content (&self) -> impl Content { + //Stack::right(|add| { + //for channel in self.tracks.iter() { + //add(channel)?; + //} + //Ok(()) + //}) + //} +//} + +//impl Content for Track { + //fn content (&self) -> impl Content { + //TrackView { + //chain: Some(&self), + //direction: tek_core::Direction::Right, + //focused: true, + //entered: true, + ////pub channels: u8, + ////pub input_ports: Vec>, + ////pub pre_gain_meter: f64, + ////pub gain: f64, + ////pub insert_ports: Vec>, + ////pub return_ports: Vec>, + ////pub post_gain_meter: f64, + ////pub post_insert_meter: f64, + ////pub level: f64, + ////pub pan: f64, + ////pub output_ports: Vec>, + ////pub post_fader_meter: f64, + ////pub route: String, + //} + //} +//} + +handle!(TuiIn: |self: Mixer, engine|{ + if let crossterm::event::Event::Key(event) = engine.event() { + + match event.code { + //KeyCode::Char('c') => { + //if event.modifiers == KeyModifiers::CONTROL { + //self.exit(); + //} + //}, + KeyCode::Down => { + self.selected_track = (self.selected_track + 1) % self.tracks.len(); + println!("{}", self.selected_track); + return Ok(Some(true)) + }, + KeyCode::Up => { + if self.selected_track == 0 { + self.selected_track = self.tracks.len() - 1; + } else { + self.selected_track -= 1; + } + println!("{}", self.selected_track); + return Ok(Some(true)) + }, + KeyCode::Left => { + if self.selected_column == 0 { + self.selected_column = 6 + } else { + self.selected_column -= 1; + } + return Ok(Some(true)) + }, + KeyCode::Right => { + if self.selected_column == 6 { + self.selected_column = 0 + } else { + self.selected_column += 1; + } + return Ok(Some(true)) + }, + _ => { + println!("\n{event:?}"); + } + } + + } + Ok(None) +}); + +handle!(TuiIn: |self:MixerTrack,from|{ + match from.event() { + //, NONE, "chain_cursor_up", "move cursor up", || { + kpat!(KeyCode::Up) => { + Ok(Some(true)) + }, + // , NONE, "chain_cursor_down", "move cursor down", || { + kpat!(KeyCode::Down) => { + Ok(Some(true)) + }, + // Left, NONE, "chain_cursor_left", "move cursor left", || { + kpat!(KeyCode::Left) => { + //if let Some(track) = app.arranger.track_mut() { + //track.device = track.device.saturating_sub(1); + //return Ok(true) + //} + Ok(Some(true)) + }, + // , NONE, "chain_cursor_right", "move cursor right", || { + kpat!(KeyCode::Right) => { + //if let Some(track) = app.arranger.track_mut() { + //track.device = (track.device + 1).min(track.devices.len().saturating_sub(1)); + //return Ok(true) + //} + Ok(Some(true)) + }, + // , NONE, "chain_mode_switch", "switch the display mode", || { + kpat!(KeyCode::Char('`')) => { + //app.chain_mode = !app.chain_mode; + Ok(Some(true)) + }, + _ => Ok(None) + } +}); + +pub enum MixerTrackCommand {} + +//impl MixerTrackDevice for LV2Plugin {} + +pub trait MixerTrackDevice: Debug + Send + Sync { + fn boxed (self) -> Box where Self: Sized + 'static { + Box::new(self) + } +} + +impl MixerTrackDevice for Sampler {} + +impl MixerTrackDevice for Plugin {} + +const SYM_NAME: &str = ":name"; +const SYM_GAIN: &str = ":gain"; +const SYM_SAMPLER: &str = "sampler"; +const SYM_LV2: &str = "lv2"; + +from_edn!("mixer/track" => |jack: &Arc>, args| -> MixerTrack { + let mut _gain = 0.0f64; + let mut track = MixerTrack { + name: "".into(), + audio_ins: vec![], + audio_outs: vec![], + devices: vec![], + }; + edn!(edn in args { + Edn::Map(map) => { + if let Some(Edn::Str(n)) = map.get(&Edn::Key(SYM_NAME)) { + track.name = n.to_string(); + } + if let Some(Edn::Double(g)) = map.get(&Edn::Key(SYM_GAIN)) { + _gain = f64::from(*g); + } + }, + Edn::List(args) => match args.first() { + // Add a sampler device to the track + Some(Edn::Symbol(SYM_SAMPLER)) => { + track.devices.push( + Box::new(Sampler::from_edn(jack, &args[1..])?) as Box + ); + panic!( + "unsupported in track {}: {:?}; tek_mixer not compiled with feature \"sampler\"", + &track.name, + args.first().unwrap() + ) + }, + // Add a LV2 plugin to the track. + Some(Edn::Symbol(SYM_LV2)) => { + track.devices.push( + Box::new(Plugin::from_edn(jack, &args[1..])?) as Box + ); + panic!( + "unsupported in track {}: {:?}; tek_mixer not compiled with feature \"plugin\"", + &track.name, + args.first().unwrap() + ) + }, + None => + panic!("empty list track {}", &track.name), + _ => + panic!("unexpected in track {}: {:?}", &track.name, args.first().unwrap()) + }, + _ => {} + }); + Ok(track) +}); + +//impl ArrangerScene { + + ////TODO + ////pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually { + ////let mut name = None; + ////let mut clips = vec![]; + ////edn!(edn in args { + ////Edn::Map(map) => { + ////let key = map.get(&Edn::Key(":name")); + ////if let Some(Edn::Str(n)) = key { + ////name = Some(*n); + ////} else { + ////panic!("unexpected key in scene '{name:?}': {key:?}") + ////} + ////}, + ////Edn::Symbol("_") => { + ////clips.push(None); + ////}, + ////Edn::Int(i) => { + ////clips.push(Some(*i as usize)); + ////}, + ////_ => panic!("unexpected in scene '{name:?}': {edn:?}") + ////}); + ////Ok(ArrangerScene { + ////name: Arc::new(name.unwrap_or("").to_string().into()), + ////color: ItemColor::random(), + ////clips, + ////}) + ////} +//} + +// TODO: +//keymap!(TRANSPORT_BPM_KEYS = |state: Clock, input: Event| ClockCommand { + //key(Char(',')) => SetBpm(state.bpm().get() - 1.0), + //key(Char('.')) => SetBpm(state.bpm().get() + 1.0), + //key(Char('<')) => SetBpm(state.bpm().get() - 0.001), + //key(Char('>')) => SetBpm(state.bpm().get() + 0.001), +//}); +//keymap!(TRANSPORT_QUANT_KEYS = |state: Clock, input: Event| ClockCommand { + //key(Char(',')) => SetQuant(state.quant.prev()), + //key(Char('.')) => SetQuant(state.quant.next()), + //key(Char('<')) => SetQuant(state.quant.prev()), + //key(Char('>')) => SetQuant(state.quant.next()), +//}); +//keymap!(TRANSPORT_SYNC_KEYS = |sync: Clock, input: Event | ClockCommand { + //key(Char(',')) => SetSync(state.sync.prev()), + //key(Char('.')) => SetSync(state.sync.next()), + //key(Char('<')) => SetSync(state.sync.prev()), + //key(Char('>')) => SetSync(state.sync.next()), +//}); +//keymap!(TRANSPORT_SEEK_KEYS = |state: Clock, input: Event| ClockCommand { + //key(Char(',')) => todo!("transport seek bar"), + //key(Char('.')) => todo!("transport seek bar"), + //key(Char('<')) => todo!("transport seek beat"), + //key(Char('>')) => todo!("transport seek beat"), +//}); + +//#[derive(Debug)] +//pub struct MIDIPlayer { + ///// Global timebase + //pub clock: Arc, + ///// Start time and clip being played + //pub play_clip: Option<(Moment, Option>>)>, + ///// Start time and next clip + //pub next_clip: Option<(Moment, Option>>)>, + ///// Play input through output. + //pub monitoring: bool, + ///// Write input to sequence. + //pub recording: bool, + ///// Overdub input to sequence. + //pub overdub: bool, + ///// Send all notes off + //pub reset: bool, // TODO?: after Some(nframes) + ///// Record from MIDI ports to current sequence. + //pub midi_inputs: Vec>, + ///// Play from current sequence to MIDI ports + //pub midi_outputs: Vec>, + ///// MIDI output buffer + //pub midi_note: Vec, + ///// MIDI output buffer + //pub midi_chunk: Vec>>, + ///// Notes currently held at input + //pub notes_in: Arc>, + ///// Notes currently held at output + //pub notes_out: Arc>, +//} + +///// Methods used primarily by the process callback +//impl MIDIPlayer { + //pub fn new ( + //jack: &Arc>, + //clock: &Arc, + //name: &str + //) -> Usually { + //let jack = jack.read().unwrap(); + //Ok(Self { + //clock: clock.clone(), + //clip: None, + //next_clip: None, + //notes_in: Arc::new(RwLock::new([false;128])), + //notes_out: Arc::new(RwLock::new([false;128])), + //monitoring: false, + //recording: false, + //overdub: true, + //reset: true, + //midi_note: Vec::with_capacity(8), + //midi_chunk: vec![Vec::with_capacity(16);16384], + //midi_outputs: vec![ + //jack.client().register_port(format!("{name}_out0").as_str(), MidiOut::default())? + //], + //midi_inputs: vec![ + //jack.client().register_port(format!("{name}_in0").as_str(), MidiIn::default())? + //], + //}) + //} +//} +use std::sync::{Arc, RwLock}; +use std::collections::BTreeMap; +pub use clojure_reader::edn::Edn; + +//#[derive(Debug, Copy, Clone, Default, PartialEq)] +//pub struct Items<'a>(&'a [Item<'a>]); +//impl<'a> Items<'a> { + //fn iter (&'a self) -> ItemsIterator<'a> { + //ItemsIterator(0, self.0) + //} +//} + +//pub struct ItemsIterator<'a>(usize, &'a [Item<'a>]); +//impl<'a> Iterator for ItemsIterator<'a> { + //type Item = &'a Item<'a>; + //fn next (&mut self) -> Option { + //let item = self.1.get(self.0); + //self.0 += 1; + //item + //} +//} + +/* + +nice but doesn't work without compile time slice concat +(which i guess could be implemeted using an unsafe linked list?) +never done that one before im ny life, might try + +use konst::slice_concat; + +const fn read <'a> ( + chars: impl Iterator +) -> Result, ParseError> { + use Range::*; + let mut state = Range::Nil; + let mut tokens: &[Range<'a>] = &[]; + while let Some(c) = chars.next() { + state = match state { + // must begin expression + Nil => match c { + ' ' => Nil, + '(' => Exp(&[]), + ':' => Sym(&[]), + '1'..'9' => Num(digit(c)), + 'a'..'z' => Key(&[&[c]]), + _ => return Err(ParseError::Unexpected(c)) + }, + Num(b) => match c { + ' ' => return Ok(Num(digit(c))), + '1'..'9' => Num(b*10+digit(c)), + _ => return Err(ParseError::Unexpected(c)) + } + Sym([]) => match c { + 'a'..'z' => Sym(&[c]), + _ => return Err(ParseError::Unexpected(c)) + }, + Sym([b @ ..]) => match c { + ' ' => return Ok(Sym(&b)), + 'a'..'z' | '0'..'9' | '-' => Sym(&[..b, c]), + _ => return Err(ParseError::Unexpected(c)) + } + Key([[b @ ..]]) => match c { + ' ' => return Ok(Key(&[&b])), + '/' => Key(&[&b, &[]]), + 'a'..'z' | '0'..'9' | '-' => Key(&[&[..b, c], &[]]), + _ => return Err(ParseError::Unexpected(c)) + } + Key([s @ .., []]) => match c { + 'a'..'z' => Key(&[..s, &[c]]), + _ => return Err(ParseError::Unexpected(c)) + } + Key([s @ .., [b @ ..]]) => match c { + '/' => Key([..s, &b, &[]]), + 'a'..'z' | '0'..'9' | '-' => Key(&[..s, &[..b, c]]), + _ => return Err(ParseError::Unexpected(c)) + } + // expression must begin with key or symbol + Exp([]) => match c { + ' ' => Exp(&[]), + ')' => return Err(ParseError::Empty), + ':' => Exp(&[Sym(&[':'])]), + c => Exp(&[Key(&[&[c]])]), + }, + + // expression can't begin with number + Exp([Num(num)]) => return Err(ParseError::Unexpected(c)), + + // symbol begins with : and lowercase a-z + Exp([Sym([':'])]) => match c { + 'a'..'z' => Exp(&[Sym(&[':', c])]), + _ => return Err(ParseError::Unexpected(c)), + }, + + // any other char is part of symbol until space or ) + Exp([Sym([':', b @ ..])]) => match c { + ')' => { tokens = &[..tokens, Exp(&[Sym(&[":", ..b])])]; Nil }, + ' ' => Exp(&[Sym(&[':', ..b]), Nil]), + c => Exp(&[Sym(&[':', ..b, c])]), + }, + + // key begins with lowercase a-z + Exp([Key([])]) => match c { + 'a'..'z' => Exp([Key([[c]])]), + _ => return Err(ParseError::Unexpected(c)), + }, + + // any other char is part of key until slash space or ) + Exp([Key([[b @ ..]])]) => match c { + '/' => Exp(&[Key(&[[..b], []])]), + ' ' => Exp(&[Key(&[[..b]]), Nil]), + ')' => { tokens = &[..tokens, Exp(&[Sym(&[":", ..b])])]; Nil }, + c => Exp(&[Key(&[[..b, c]])]) + } + + // slash adds new section to key + Exp([Key([b @ .., []])]) => match c { + '/' => Exp(&[Key(&[[..b], []])]), + ' ' => Exp(&[Key(&[[..b]]), Nil]), + ')' => { tokens = &[..tokens, Exp(&[Sym(&[":", ..b])])]; Nil }, + c => Exp(&[Key(&[[..b, c]])]) + } + + } + } + Ok(state) +} +*/ + + +/// EDN parsing helper. +#[macro_export] macro_rules! edn { + ($edn:ident { $($pat:pat => $expr:expr),* $(,)? }) => { + match $edn { $($pat => $expr),* } + }; + ($edn:ident in $args:ident { $($pat:pat => $expr:expr),* $(,)? }) => { + for $edn in $args { + edn!($edn { $($pat => $expr),* }) + } + }; +} + +pub trait FromEdn: Sized { + const ID: &'static str; + fn from_edn (context: C, expr: &[Edn<'_>]) -> + std::result::Result>; +} + +/// Implements the [FromEdn] trait. +#[macro_export] macro_rules! from_edn { + ($id:expr => |$context:tt:$Context:ty, $args:ident| -> $T:ty $body:block) => { + impl FromEdn<$Context> for $T { + const ID: &'static str = $id; + fn from_edn <'e> ($context: $Context, $args: &[Edn<'e>]) -> Usually { + $body + } + } + } +} +use crate::*; +use std::sync::Arc; +pub enum EdnIterator { + Nil, + Sym(Arc), + Exp(Vec) +} +impl EdnIterator { + pub fn new (item: &EdnItem) -> Self { + use EdnItem::*; + match item { + Sym(t) => Self::Sym(t.clone()), + Exp(i) => Self::Exp(i.iter().map(EdnIterator::new).collect()), + _ => Self::Nil, + } + } +} +impl Iterator for EdnIterator { + type Item = EdnItem; + fn next (&mut self) -> Option { + use EdnIterator::*; + match self { + Sym(t) => { + let t = *t; + *self = Nil; + Some(Sym(t)) + }, + Exp(v) => match v.as_mut_slice() { + [a] => if let Some(next) = a.next() { + Some(next) + } else { + *self = Exp(v.split_off(1)); + self.next() + }, + _ => { + *self = Nil; + None + } + }, + _ => { + *self = Nil; + None + } + } + } +} diff --git a/.old/todo_arranger.edn b/.old/todo_arranger.edn new file mode 100644 index 00000000..81fc7dc7 --- /dev/null +++ b/.old/todo_arranger.edn @@ -0,0 +1,83 @@ + This is the unified Tek Arranger. + + Its appearance is defined by the following view definition: + +{def :view (bsp/s (fixed/y 2 :toolbar) + (fill/x (align/c (bsp/w (fixed/x :pool-w :pool) + (bsp/n (fixed/y 3 :outputs) + (bsp/n (fixed/y 3 :inputs) + (bsp/n (fixed/y 3 :tracks) :scenes)))))))} + + The arranger's behavior is controlled by the + following keymaps: + +{def :keys + (@u undo 1) + (@shift-u redo 1) + (@space clock toggle) + (@shift-space clock toggle 0) + (@ctrl-a scene add) + (@ctrl-t track add) + (@tab edit :clip) + (@c color)} + +{def :keys-mix + (@down select 0 1) + (@s select 0 1) + + (@right select 1 0) + (@d select 1 0)} + +{def :keys-track + (@left select :track-prev :scene) + (@a select :track-prev :scene) + (@right select :track-next :scene) + (@d select :track-next :scene) + (@down select :track :scene-next) + (@s select :track :scene-next) + + (@q track launch) + (@c track color :track) + (@comma track swap-prev) + (@period track swap-next) + (@lt track size-dec) + (@gt track size-inc) + (@delete track delete)} + +{def :keys-scene + (@up select :track :scene-prev) + (@w select :track :scene-prev) + (@down select :track :scene-next) + (@s select :track :scene-next) + (@right select :track-next :scene) + (@d select :track-next :scene) + + (@q scene launch) + (@c scene color :scene) + (@comma scene swap-prev) + (@period scene swap-next) + (@lt scene size-dec) + (@gt scene size-inc) + (@delete scene delete)} + +{def :keys-clip + (@up select :track :scene-prev) + (@w select :track :scene-prev) + (@down select :track :scene-next) + (@s select :track :scene-next) + (@left select :track-prev :scene) + (@a select :track-prev :scene) + (@right select :track-next :scene) + (@d select :track-next :scene) + + (@q enqueue :clip) + (@c clip color :track :scene) + (@g clip get) + (@p clip put) + (@delete clip del) + (@comma clip prev) + (@period clip next) + (@lt clip swap-prev) + (@gt clip swap-next) + (@l clip loop-toggle)} + diff --git a/crates/tek/src/cli/todo_cli_mixer.rs b/.old/todo_cli_mixer.rs similarity index 87% rename from crates/tek/src/cli/todo_cli_mixer.rs rename to .old/todo_cli_mixer.rs index d14f9807..e419ffed 100644 --- a/crates/tek/src/cli/todo_cli_mixer.rs +++ b/.old/todo_cli_mixer.rs @@ -1,4 +1,4 @@ -use crate::*; +include!("./lib.rs"); pub fn main () -> Usually<()> { MixerCli::parse().run() @@ -13,7 +13,7 @@ pub fn main () -> Usually<()> { impl MixerCli { fn run (&self) -> Usually<()> { - Tui::run(JackClient::new("tek_mixer")?.activate_with(|jack|{ + Tui::run(JackConnection::new("tek_mixer")?.activate_with(|jack|{ let mut mixer = Mixer::new(jack, self.name.as_ref().map(|x|x.as_str()).unwrap_or("mixer"))?; for channel in 0..self.channels.unwrap_or(8) { mixer.track_add(&format!("Track {}", channel + 1), 1)?; diff --git a/crates/tek/src/cli/todo_cli_plugin.rs b/.old/todo_cli_plugin.rs similarity index 87% rename from crates/tek/src/cli/todo_cli_plugin.rs rename to .old/todo_cli_plugin.rs index 06acc9bf..cfb81fe2 100644 --- a/crates/tek/src/cli/todo_cli_plugin.rs +++ b/.old/todo_cli_plugin.rs @@ -1,4 +1,4 @@ -use crate::*; +include!("./lib.rs"); pub fn main () -> Usually<()> { PluginCli::parse().run() @@ -13,7 +13,7 @@ pub fn main () -> Usually<()> { impl PluginCli { fn run (&self) -> Usually<()> { - Tui::run(JackClient::new("tek_plugin")?.activate_with(|jack|{ + Tui::run(JackConnection::new("tek_plugin")?.activate_with(|jack|{ let mut plugin = Plugin::new_lv2( jack, self.name.as_ref().map(|x|x.as_str()).unwrap_or("mixer"), diff --git a/crates/tek/src/cli/todo_cli_sampler.rs b/.old/todo_cli_sampler.rs similarity index 86% rename from crates/tek/src/cli/todo_cli_sampler.rs rename to .old/todo_cli_sampler.rs index 0fcc8a7d..75e4c62a 100644 --- a/crates/tek/src/cli/todo_cli_sampler.rs +++ b/.old/todo_cli_sampler.rs @@ -1,4 +1,4 @@ -use crate::*; +include!("./lib.rs"); pub fn main () -> Usually<()> { SamplerCli::parse().run() @@ -13,7 +13,7 @@ pub fn main () -> Usually<()> { impl SamplerCli { fn run (&self) -> Usually<()> { - Tui::run(JackClient::new("tek_sampler")?.activate_with(|jack|{ + Tui::run(JackConnection::new("tek_sampler")?.activate_with(|jack|{ let mut plugin = Sampler::new( jack, self.name.as_ref().map(|x|x.as_str()).unwrap_or("mixer"), diff --git a/crates/tek/examples/sequencer.edn b/.old/todo_project0.edn similarity index 100% rename from crates/tek/examples/sequencer.edn rename to .old/todo_project0.edn diff --git a/crates/tek/examples/mixer.edn b/.old/todo_project1.edn similarity index 100% rename from crates/tek/examples/mixer.edn rename to .old/todo_project1.edn diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..f40cf8e8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,18 @@ +## development + +you'll need a Rust toolchain and various system libraries. + +you can obtain the former using `rustup` and the latter using `nix-shell`. +there's a `shell.nix` provided with the project. + +from there, use the commands in the `Justfile`, e.g.: + +```sh +just arranger +``` + +note that `tek > 0.2.0-rc.7` will require rust nightly +for the unstable features `type_alias_impl_trait` and +`impl_trait_in_assoc_type`. make some noise for lucky +[**rust rfc2515**](https://github.com/rust-lang/rust/issues/63063) +if you want to see this buildable with stable/beta. diff --git a/Cargo.lock b/Cargo.lock index bfd59c5e..34c84187 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,22 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 + +[[package]] +name = "ab_glyph" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e074464580a518d16a7126262fffaaa47af89d4099d4cb403f8ed938ba12ee7d" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "addr2line" @@ -13,9 +29,22 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.3", + "once_cell", + "version_check", + "zerocopy", +] [[package]] name = "allocator-api2" @@ -24,10 +53,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] -name = "anstream" -version = "0.6.18" +name = "android-activity" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" +dependencies = [ + "android-properties", + "bitflags 2.9.3", + "cc", + "cesu8", + "jni", + "jni-sys", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 1.0.69", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "anstream" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", @@ -40,36 +96,37 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.6" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys 0.60.2", ] [[package]] @@ -81,12 +138,30 @@ dependencies = [ "num-traits", ] +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + [[package]] name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "atomic_float" version = "1.1.0" @@ -95,15 +170,15 @@ checksum = "628d228f918ac3b82fe590352cc719d30664a0c13ca3a60266fe02c7132d480a" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -124,6 +199,21 @@ dependencies = [ "console", ] +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "1.3.2" @@ -132,15 +222,24 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "by_address" @@ -150,15 +249,41 @@ checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" [[package]] name = "bytemuck" -version = "1.20.0" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" +checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" [[package]] -name = "byteorder" -version = "1.5.0" +name = "bytes" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.9.3", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] [[package]] name = "cassowary" @@ -168,24 +293,47 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "castaway" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" dependencies = [ "rustversion", ] [[package]] -name = "cfg-if" -version = "1.0.0" +name = "cc" +version = "1.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "clap" -version = "4.5.23" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" dependencies = [ "clap_builder", "clap_derive", @@ -193,9 +341,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.23" +version = "4.5.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" dependencies = [ "anstream", "anstyle", @@ -205,9 +353,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" dependencies = [ "heck", "proc-macro2", @@ -217,55 +365,146 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" - -[[package]] -name = "clojure-reader" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8755ac891a1a9a21101250e9e8ab55d742ec7957928cbc1b728dc1cbc234e5ce" -dependencies = [ - "ordered-float", -] +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] [[package]] name = "compact_str" -version = "0.7.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" dependencies = [ "castaway", "cfg-if", "itoa", + "rustversion", "ryu", "static_assertions", ] [[package]] -name = "console" -version = "0.15.8" +name = "concurrent-queue" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" dependencies = [ "encode_unicode", - "lazy_static", "libc", - "windows-sys 0.52.0", + "once_cell", + "windows-sys 0.59.0", +] + +[[package]] +name = "const_panic" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb8a602185c3c95b52f86dc78e55a6df9a287a7a93ddbcf012509930880cf879" +dependencies = [ + "const_panic_proc_macros", + "typewit", +] + +[[package]] +name = "const_panic_proc_macros" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c5b80a80fb52c1a6ca02e3cd829a76b472ff0a15588196fd8da95221f0c1e4b" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "convert_case" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -282,21 +521,39 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crossterm" -version = "0.27.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.3", "crossterm_winapi", - "libc", "mio", - "parking_lot 0.12.3", + "parking_lot 0.12.4", + "rustix 0.38.44", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.9.3", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot 0.12.4", + "rustix 1.0.8", "signal-hook", "signal-hook-mio", "winapi", @@ -312,16 +569,124 @@ dependencies = [ ] [[package]] -name = "either" -version = "1.13.0" +name = "ctor" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "encode_unicode" -version = "0.3.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "encoding_rs" @@ -334,9 +699,19 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] [[package]] name = "extended" @@ -351,20 +726,81 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" [[package]] -name = "foldhash" -version = "0.1.3" +name = "fastrand" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -375,9 +811,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", @@ -391,15 +827,56 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "indexmap" -version = "2.7.0" +name = "hermit-abi" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "i24" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bafc62750bce7c2603750d1c19532e226bb85bd21f5385ab952cc29b8c31e2af" +dependencies = [ + "bytemuck", + "num-traits", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" dependencies = [ "equivalent", "hashbrown", ] +[[package]] +name = "indoc" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" + +[[package]] +name = "instability" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "instant" version = "0.1.13" @@ -415,15 +892,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -434,18 +902,28 @@ dependencies = [ ] [[package]] -name = "itoa" -version = "1.0.14" +name = "itertools" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jack" version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78a4ae24f4ee29676aef8330fed1104e72f314cab16643dbeb61bfd99b4a8273" dependencies = [ - "bitflags 2.6.0", + "approx", + "bitflags 2.9.3", + "crossbeam-channel", + "ctor", "jack-sys", "lazy_static", "libc", @@ -455,10 +933,8 @@ dependencies = [ [[package]] name = "jack-sys" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6013b7619b95a22b576dfb43296faa4ecbe40abbdb97dfd22ead520775fc86ab" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.9.3", "lazy_static", "libc", "libloading", @@ -467,15 +943,74 @@ dependencies = [ ] [[package]] -name = "js-sys" -version = "0.3.76" +name = "jni" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", ] +[[package]] +name = "konst" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4381b9b00c55f251f2ebe9473aef7c117e96828def1a7cb3bd3f0f903c6894e9" +dependencies = [ + "const_panic", + "konst_kernel", + "konst_proc_macros", + "typewit", +] + +[[package]] +name = "konst_kernel" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4b1eb7788f3824c629b1116a7a9060d6e898c358ebff59070093d51103dcc3c" +dependencies = [ + "typewit", +] + +[[package]] +name = "konst_proc_macros" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00af7901ba50898c9e545c24d5c580c96a982298134e8037d8978b6594782c07" + [[package]] name = "lazy_static" version = "1.5.0" @@ -484,18 +1019,29 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.168" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libloading" -version = "0.7.4" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "winapi", + "windows-targets 0.53.3", +] + +[[package]] +name = "libredox" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" +dependencies = [ + "bitflags 2.9.3", + "libc", + "redox_syscall 0.5.17", ] [[package]] @@ -519,6 +1065,24 @@ dependencies = [ "lv2_raw", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litrs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" + [[package]] name = "livi" version = "0.7.5" @@ -534,9 +1098,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -544,9 +1108,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "lru" @@ -574,9 +1138,18 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "memmap2" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" +dependencies = [ + "libc", +] [[package]] name = "midly" @@ -589,23 +1162,53 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "0.8.11" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "log", - "wasi", - "windows-sys 0.48.0", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.9.3", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", ] [[package]] @@ -618,27 +1221,267 @@ dependencies = [ ] [[package]] -name = "object" -version = "0.36.5" +name = "num_enum" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.9.3", + "block2", + "libc", + "objc2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.9.3", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.9.3", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2", + "objc2", + "objc2-contacts", + "objc2-foundation", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.9.3", + "block2", + "dispatch", + "libc", + "objc2", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.9.3", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.9.3", + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.9.3", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.9.3", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.20.2" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] -name = "ordered-float" -version = "4.5.0" +name = "once_cell_polyfill" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c65ee1f9701bf938026630b455d5315f490640234259037edb259798b3bcf85e" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "orbclient" +version = "0.3.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43" dependencies = [ - "num-traits", + "libredox", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", ] [[package]] @@ -651,7 +1494,7 @@ dependencies = [ "fast-srgb8", "palette_derive", "phf", - "rand", + "rand 0.8.5", ] [[package]] @@ -679,12 +1522,12 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", - "parking_lot_core 0.9.10", + "parking_lot_core 0.9.11", ] [[package]] @@ -703,13 +1546,13 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.7", + "redox_syscall 0.5.17", "smallvec", "windows-targets 0.52.6", ] @@ -721,10 +1564,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] -name = "phf" -version = "0.11.2" +name = "percent-encoding" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ "phf_macros", "phf_shared", @@ -732,19 +1581,19 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand", + "rand 0.8.5", ] [[package]] name = "phf_macros" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ "phf_generator", "phf_shared", @@ -755,61 +1604,162 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ "siphasher", ] [[package]] -name = "pkg-config" -version = "0.3.31" +name = "pin-project" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "polling" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5bd19146350fe804f7cb2669c851c03d69da628803dab0d98018142aaa5d829" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.0.8", + "windows-sys 0.60.2", +] [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] -name = "proc-macro2" -version = "1.0.92" +name = "proc-macro-crate" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] -name = "quanta" -version = "0.12.3" +name = "proptest" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5" +checksum = "6fcdab19deb5195a31cf7726a210015ff1496ba1464fd42cb4f537b8b01b471f" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.9.3", + "lazy_static", + "num-traits", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "proptest-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee1c9ac207483d5e7db4940700de86a9aae46ef90c48b57f99fe7edb8345e49" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" dependencies = [ "crossbeam-utils", "libc", "once_cell", "raw-cpuid", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "web-sys", "winapi", ] [[package]] -name = "quote" -version = "1.0.37" +name = "quick-error" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.8.5" @@ -817,8 +1767,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -828,7 +1788,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -837,43 +1807,68 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.3", ] [[package]] name = "ratatui" -version = "0.26.3" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.3", "cassowary", "compact_str", - "crossterm", - "itertools 0.12.1", + "crossterm 0.28.1", + "indoc", + "instability", + "itertools 0.13.0", "lru", "paste", - "stability", "strum", "unicode-segmentation", "unicode-truncate", - "unicode-width", + "unicode-width 0.2.0", ] [[package]] name = "raw-cpuid" -version = "11.2.0" +version = "11.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0" +checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.3", ] [[package]] -name = "rayon" -version = "1.10.0" +name = "raw-window-handle" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -881,9 +1876,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -900,13 +1895,28 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ - "bitflags 2.6.0", + "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags 2.9.3", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "ringbuf" version = "0.3.3" @@ -918,21 +1928,74 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.3", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags 2.9.3", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.60.2", +] [[package]] name = "rustversion" -version = "1.0.18" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] name = "scopeguard" @@ -941,19 +2004,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] -name = "serde" -version = "1.0.215" +name = "sctk-adwaita" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit", + "tiny-skia", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.215" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -962,18 +2038,24 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.8" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" dependencies = [ "serde", ] [[package]] -name = "signal-hook" -version = "0.3.17" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" dependencies = [ "libc", "signal-hook-registry", @@ -992,33 +2074,63 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] [[package]] name = "siphasher" -version = "0.3.11" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] -name = "stability" -version = "0.2.1" +name = "smithay-client-toolkit" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "quote", - "syn", + "bitflags 2.9.3", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", ] [[package]] @@ -1027,6 +2139,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + [[package]] name = "strsim" version = "0.11.1" @@ -1252,9 +2370,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.90" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -1263,26 +2381,137 @@ dependencies = [ [[package]] name = "tek" -version = "0.2.0" +version = "0.3.0" dependencies = [ "atomic_float", "backtrace", - "better-panic", "clap", - "clojure-reader", - "crossterm", "jack", + "konst", "livi", "midly", - "once_cell", "palette", - "quanta", - "rand", - "ratatui", + "proptest", + "proptest-derive", + "rand 0.8.5", "symphonia", + "tek_device", + "tengri", "toml", "uuid", "wavers", + "winit", + "xdg", +] + +[[package]] +name = "tek_device" +version = "0.3.0" +dependencies = [ + "atomic_float", + "backtrace", + "clap", + "jack", + "konst", + "livi", + "midly", + "palette", + "proptest", + "proptest-derive", + "rand 0.8.5", + "symphonia", + "tek_engine", + "tengri", + "toml", + "uuid", + "wavers", + "winit", + "xdg", +] + +[[package]] +name = "tek_engine" +version = "0.3.0" +dependencies = [ + "atomic_float", + "jack", + "midly", + "proptest", + "proptest-derive", + "tengri", +] + +[[package]] +name = "tempfile" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix 1.0.8", + "windows-sys 0.60.2", +] + +[[package]] +name = "tengri" +version = "0.14.0" +dependencies = [ + "tengri_core", + "tengri_dsl", + "tengri_input", + "tengri_output", + "tengri_tui", +] + +[[package]] +name = "tengri_core" +version = "0.14.0" + +[[package]] +name = "tengri_dsl" +version = "0.14.0" +dependencies = [ + "const_panic", + "itertools 0.14.0", + "konst", + "tengri_core", + "thiserror 2.0.16", +] + +[[package]] +name = "tengri_input" +version = "0.14.0" +dependencies = [ + "tengri_core", +] + +[[package]] +name = "tengri_output" +version = "0.14.0" +dependencies = [ + "tengri_core", + "tengri_dsl", +] + +[[package]] +name = "tengri_tui" +version = "0.14.0" +dependencies = [ + "atomic_float", + "better-panic", + "crossterm 0.29.0", + "konst", + "palette", + "quanta", + "rand 0.8.5", + "ratatui", + "tengri_core", + "tengri_dsl", + "tengri_input", + "tengri_output", + "unicode-width 0.2.0", ] [[package]] @@ -1291,7 +2520,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +dependencies = [ + "thiserror-impl 2.0.16", ] [[package]] @@ -1306,44 +2544,145 @@ dependencies = [ ] [[package]] -name = "toml" -version = "0.8.19" +name = "thiserror-impl" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "toml" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" +dependencies = [ + "indexmap", "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.7.0", + "toml_parser", + "toml_writer", + "winnow", ] [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_datetime" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", - "serde", - "serde_spanned", - "toml_datetime", + "toml_datetime 0.6.11", "winnow", ] [[package]] -name = "unicode-ident" -version = "1.0.14" +name = "toml_parser" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + +[[package]] +name = "typewit" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97e72ba082eeb9da9dc68ff5a2bf727ef6ce362556e8d29ec1aed3bd05e7d86a" +dependencies = [ + "typewit_proc_macros", +] + +[[package]] +name = "typewit_proc_macros" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-segmentation" @@ -1359,7 +2698,7 @@ checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" dependencies = [ "itertools 0.13.0", "unicode-segmentation", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -1368,6 +2707,18 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "utf8parse" version = "0.2.2" @@ -1376,35 +2727,72 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.11.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" dependencies = [ - "getrandom", + "getrandom 0.3.3", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", ] [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] [[package]] name = "wasm-bindgen" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", @@ -1415,10 +2803,23 @@ dependencies = [ ] [[package]] -name = "wasm-bindgen-macro" -version = "0.2.99" +name = "wasm-bindgen-futures" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1426,9 +2827,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", @@ -1439,27 +2840,150 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "wavers" -version = "1.4.3" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab501e9e5b13446d3a6e846de0c190c96b85ca3eced3bd00460edc67654500c8" +checksum = "d599ddd98c95ad3d3fc898cfb5c4023430f6ea62f96083f46d13cc8b82589bd3" dependencies = [ "bytemuck", + "i24", "num-traits", "paste", - "thiserror", + "thiserror 1.0.69", +] + +[[package]] +name = "wayland-backend" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.0.8", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +dependencies = [ + "bitflags 2.9.3", + "rustix 1.0.8", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.9.3", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447ccc440a881271b19e9989f75726d60faa09b95b0200a9b7eb5cc47c3eeb29" +dependencies = [ + "rustix 1.0.8", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +dependencies = [ + "bitflags 2.9.3", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032" +dependencies = [ + "bitflags 2.9.3", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" +dependencies = [ + "bitflags 2.9.3", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", ] [[package]] name = "web-sys" -version = "0.3.76" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", @@ -1481,6 +3005,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +dependencies = [ + "windows-sys 0.60.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -1488,12 +3021,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows-sys" -version = "0.48.0" +name = "windows-link" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets 0.48.5", + "windows-targets 0.42.2", ] [[package]] @@ -1514,6 +3053,30 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -1538,13 +3101,36 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -1557,6 +3143,18 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -1569,6 +3167,18 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -1581,12 +3191,30 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -1599,6 +3227,18 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -1611,6 +3251,18 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -1623,6 +3275,18 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -1636,29 +3300,158 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "winnow" -version = "0.6.20" +name = "windows_x86_64_msvc" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winit" +version = "0.30.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.9.3", + "block2", + "bytemuck", + "calloop", + "cfg_aliases", + "concurrent-queue", + "core-foundation", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "sctk-adwaita", + "smithay-client-toolkit", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] [[package]] -name = "zerocopy" -version = "0.7.35" +name = "wit-bindgen-rt" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.3", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 0.38.44", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + +[[package]] +name = "xdg" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.9.3", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ - "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 301685bc..9f379dc6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,53 @@ [workspace] resolver = "2" -members = [ - "crates/tek", - #"crates/tek_core", - #"crates/tek_api", - #"crates/tek_tui", - #"crates/tek_cli", - #"crates/tek_layout" -] +members = [ "./app", "./engine", "./device" ] +exclude = [ "./deps/tengri" ] + +[workspace.package] +edition = "2024" +version = "0.3.0" + +[profile.release] +lto = true + +[profile.coverage] +inherits = "test" +lto = false + +[workspace.dependencies.tengri] +path = "./deps/tengri/tengri" +features = [ "tui", "dsl" ] + +[workspace.dependencies.tengri_proc] +path = "./deps/tengri/proc" + +[workspace.dependencies.jack] +path = "./deps/rust-jack" + +[workspace.dependencies] +tek = { path = "./tek" } + +atomic_float = { version = "1.0.0" } +backtrace = { version = "0.3.72" } +bumpalo = { version = "3.19.0" } +clap = { version = "4.5.4", features = [ "derive" ] } +gtk = { version = "0.18.1" } +konst = { version = "0.3.16", features = [ "rust_1_83" ] } +livi = { version = "0.7.4" } +midly = { version = "0.5" } +palette = { version = "0.7.6", features = [ "random" ] } +quanta = { version = "0.12.3" } +rand = { version = "0.8.5" } +symphonia = { version = "0.5.4", features = [ "all" ] } +toml = { version = "0.9.2" } +uuid = { version = "1.10.0", features = [ "v4" ] } +wavers = { version = "1.4.3" } +winit = { version = "0.30.4", features = [ "x11" ] } +xdg = { version = "3.0.0" } +#once_cell = "1.19.0" +#no_deadlocks = "1.3.2" +#suil-rs = { path = "../suil" } +#vst = "0.4.0" +#vst3 = "0.1.0" +proptest = { version = "^1" } +proptest-derive = { version = "^0.5.1" } diff --git a/Justfile b/Justfile index 07b49870..94551690 100644 --- a/Justfile +++ b/Justfile @@ -1,34 +1,117 @@ +export RUSTFLAGS := "--cfg procmacro2_semver_exempt -Zmacro-backtrace -Clink-arg=-fuse-ld=mold" +export RUST_BACKTRACE := "1" + default: - just -l -status: - cargo c - cloc --by-file src/ - git status + @just -l + +cloc: + for src in {cli,edn/src,input/src,jack/src,midi/src,output/src,plugin/src,sampler/src,tek/src,time/src,tui/src}; do echo; echo $src; cloc --quiet $src; done + +bacon: + bacon -s + +check: + reset && cargo check + +test: + cargo test --workspace --exclude jack + +covfig := "CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' RUSTDOCFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='cov/cargo-test-%p-%m.profraw'" +grcov-binary := "--binary-path ./target/coverage/deps/" +grcov-ignore := "--ignore-not-existing --ignore '../*' --ignore \"/*\" --ignore 'target/*'" +cov: + {{covfig}} time cargo test -j4 --workspace --exclude jack --profile coverage + rm -rf target/coverage/html || true + {{covfig}} time grcov . -s . {{grcov-binary}} {{grcov-ignore}} -t html -o target/coverage/html +cov-md: + {{covfig}} time cargo test -j4 --workspace --exclude jack --profile coverage + {{covfig}} time grcov . -s . {{grcov-binary}} {{grcov-ignore}} -t markdown | sort +llcov: + time cargo llvm-cov --workspace --exclude jack --profile coverage --no-report + time cargo llvm-cov --workspace --exclude jack --profile coverage --no-report --doc + time cargo llvm-cov report --doctests --html #--output-path target/coverage/html + +build: + reset && cargo build + +debug := "reset && cargo run --" +run: + {{debug}} +run-init: + rm -rf ~/.config/tek && {{debug}} + +prof: + CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph -- + +doc: + cargo doc -j4 --workspace --document-private-items + +release := "reset && cargo run --release --" +release: + {{release}} +build-release: + time cargo build -j4 --release + +amend: + git commit --amend push: - git push -u codeberg main - git push -u origin main + git push -u codeberg main && git push -u origin main tpush: - git push --tags -u codeberg - git push --tags -u origin + git push --tags -u codeberg && git push --tags -u origin fpush: - git push -fu codeberg main - git push -fu origin main + git push -fu codeberg main && git push -fu origin main ftpush: - git push --tags -fu codeberg - git push --tags -fu origin -transport: - cargo run --bin tek_transport + git push --tags -fu codeberg && git push --tags -fu origin + +name := "-n tek" +bpm := "-b 174" +clock: + {{debug}} {{name}} {{bpm}} clock +clock-release: + {{release}} {{name}} {{bpm}} clock + +midi-in := "-i 'Midi-Bridge:.*nanoKEY.*:.*capture.*'" +midi-out := "-o 'Midi-Bridge:.*playback.*'" +audio-in := "-l 'Komplete Audio 6 Pro:capture_AUX1' -r 'Komplete Audio 6 Pro:capture_AUX1'" +audio-out := "-L 'Komplete Audio 6 Pro:playback_AUX1' -R 'Komplete Audio 6 Pro:playback_AUX1'" +firefox-in := "-l 'Firefox:output_FL*' -r 'Firefox:output_FR*'" arranger: - cargo run --bin tek_arranger + {{debug}} {{name}} {{bpm}} arranger +arranger-ext: + {{debug}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} arranger +arranger-release: + {{release}} {{name}} {{bpm}} arranger +arranger-release-ext: + {{release}} {{name}} {{bpm}} {{midi-in}} {{firefox-in}} {{midi-out}} arranger + +groovebox: + {{debug}} {{name}} {{bpm}} groovebox +groovebox-ext: + reset + {{debug}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} {{audio-in}} {{audio-out}} groovebox +groovebox-browser: + {{debug}} {{name}} {{bpm}} {{audio-in}} groovebox +groovebox-release: + {{release}} {{name}} {{bpm}} groovebox +groovebox-release-ext: + {{release}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} {{audio-in}} {{audio-out}} groovebox +groovebox-release-ext-browser: + {{release}} {{name}} {{bpm}} {{midi-in}} {{firefox-in}} {{audio-out}} groovebox + sequencer: - cargo run --bin tek_sequencer + {{debug}} {{name}} {{bpm}} sequencer +sequencer-ext: + {{debug}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} sequencer sequencer-release: - cargo run --release --bin tek_sequencer + {{release}} {{name}} {{bpm}} sequencer +sequencer-release-ext: + {{release}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} sequencer + mixer: - cargo run --bin tek_mixer + {{debug}} mixer track: - cargo run --bin tek_track + {{debug}} track sampler: - cargo run --bin tek_sampler + {{debug}} sampler plugin: - cargo run --bin tek_plugin + {{debug}} plugin diff --git a/LICENSE b/LICENSE index 4b61f9fc..be3f7b28 100644 --- a/LICENSE +++ b/LICENSE @@ -1,8 +1,661 @@ -0. The attached collection of letters, numbers, punctuation and other characters will be - collectively referred to as "the work". -1. The work exists as-is. It is composed as an extended meditation on the futility of computing. - No implication is made that the work compiles, executes, or that it is good for anything - whatsoever. -2. You may not copy, modify, or distribute the work for any purpose. -3. You may not affirm to third parties that the work exists, that you are its "author", - or that the "author" of the work exists. + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md index c927ac6f..64655591 100644 --- a/README.md +++ b/README.md @@ -1,116 +1,90 @@ -# tek +# tek [![Please don't upload to GitHub](https://nogithub.codeberg.page/badge.svg)](https://nogithub.codeberg.page) -[![Please don't upload to GitHub](https://nogithub.codeberg.page/badge.svg)](https://nogithub.codeberg.page) +a music making program for [24-bit unicode terminals](https://sw.kovidgoyal.net/kitty/). -a music making program for your terminal +written in [rust](https://www.rust-lang.org/) +with [ratatui](https://ratatui.rs/) on [crossterm](https://docs.rs/crossterm/latest/crossterm/) +for [jack](https://jackaudio.org/) and [pipewire](https://www.pipewire.org/). -## project status +**tek** is available as [source](https://codeberg.org/unspeaker/tek#building-from-source), +[statically linked binaries](https://codeberg.org/unspeaker/tek/releases), and on the +[aur](https://codeberg.org/unspeaker/tek#arch-linux). -for roadmap, see https://codeberg.org/unspeaker/tek/milestones +author is reachable via [**mastodon** `@unspeaker@mastodon.social`](https://mastodon.social/@unspeaker) +or [**matrix** `@unspeaker:matrix.org`](https://matrix.to/#/@unspeaker:matrix.org) -> [!WARNING] -> -> As of 2024-10-25, I'm on track to release `tek 0.2.0` sometime in December 2024. -> I plan to tag the previous working prototype (as seen in the demos published in the -> [tek channel at basspistol's peertube](https://v.basspistol.org/c/tek/videos)) as `0.1.0`— -> once I've identified the appropriate commit! -> -> I've been dreaming of this project for a decade, and finally had the experience and peace of mind -> to start building it in late May 2024. I quickly reached the limit of how much of the UI I can -> write imperatively, so I started refactoring it in a more declarative style. The new interface -> logic is holding out pretty well, though it's not presently without its warts. -> -> Your moral support means a lot to me. Feel free to [contact me on Mastodon](https://mastodon.social/@unspeaker)! -> (Especially if you know how to host LV2 plugin UIs in `winit`; or how to relink abandoned Win32 -> VST2s into LV2 or CLAP monoliths 😁) -> -> Love, -> -> (a rogue knowledge worker in a cyberpunk dystopia) - -## what it does - -Tek is a [MIDI](https://en.wikipedia.org/wiki/MIDI) sequencer, sampler, and plugin host -for the Linux terminal. It's written in [Rust](https://www.rust-lang.org/), and targets -[JACK](https://jackaudio.org/) (or [Pipewire](https://www.pipewire.org/)'s JACK implementation). - -## design goals - -### lightweight - -My goal is to have a pop-up scratchpad for musical ideas that doesn't get in the way -of building upon them. Kind of like [Ableton](https://www.ableton.com/) — but for free systems, -and without all the bloat! - -### flexible - -Besides Ableton, I'm also inspired by the workflow of trackers and various old-school hardware -sequencers (of which I've broken several). I've found that every existing music-making tool -takes me about 80% of the way to the music I want to make. And so, after a decade of fucking -around, I've decided it's finally time to make good on my old dream to build the instrument -that will take me 100% there. - -### programmable - -A secondary goal is to make my music making environment extensible, programmable, and -interoperable; the intended project format is an -[S-expression](https://en.wikipedia.org/wiki/S-expression)-based notation -([EDN](https://en.wikipedia.org/wiki/Clojure#Extensible_Data_Notation), -[Steel](https://github.com/mattwparas/steel), or similar... though I've also been -looking for an excuse to embed a -[Forth](https://en.wikipedia.org/wiki/Forth_(programming_language)) 😏) - -## getting started - -### requirements - -* Linux -* JACK or Pipewire -* a terminal supporting 24-bit colors (I use `kitty`) - -### recommended - -* MIDI controller -* Samples -* LV2 plugins - -### downloads - -> [!WARNING] -> -> Binaries are currently unavailable. Right now your only option is to build from source. -> In the future I plan to integrate Forgejo Actions / Codeberg CI. - -### building from source - -You need a Rust toolchain and various system libraries. You can obtain the former -using `rustup` and the latter using `nix-shell`. From there, use the commands in the -`Justfile`, e.g.: - -```sh -just arranger -``` +| | | +|-|-| +|![Screenshot of Arranger Mode](https://codeberg.org/unspeaker/tek/attachments/5014ff4d-9ece-4862-90de-3bc6573eacf6)|![Screenshot of Groovebox Mode](https://codeberg.org/unspeaker/tek/attachments/bbc12eda-1d5e-4e0f-9474-f585128255a5)
![Screenshot of Help in Groovebox Mode](https://codeberg.org/unspeaker/tek/attachments/d8963b84-8183-4c05-b77b-349a4c4c6161)| ## usage -> [!WARNING] -> -> The following applies to `tek 0.1.0`. I will update it as part of the `0.2.0` release. +* **requirements:** linux; jack or pipewire; 24-bit terminal (i use `kitty`) +* **recommended:** midi controller; samples in wav format; lv2 plugins. -### Overview +## keymaps -Tek is inspired by "clip launching" workflows as exemplified by Ableton Live, Bitwig Studio, -Ardour, and probably others. The main view consists of three sections: +* Arranger: + * [x] arrows: navigate + * [x] tab: enter editor + * [x] `q`: enqueue clip + * [x] space: play/pause +* Editor: + * [x] arrows: navigate + * [x] `,` / `.`: change note length + * [x] enter: write note + * [x] `-` / `=`: zoom midi editor + * [ ] `z`: zoom lock/unlock + * [ ] del: delete +* Global: + * [x] esc: options menu + * [x] f1: help/command list + * [ ] f2: rename + * [ ] f6: save + * [ ] f9: load -* The **arranger view** corresponds to Ableton's Session and Arrangement views. - It allows you to put together a musical composition as a sequence of **phrases**, - playing simultaneously across multiple **tracks**. -* The **sequencer view** allows you to edit phrases, which consist of MIDI events. -* The **chain view** allows you to add **devices** to each track. Devices determine - how a given phrase will sound. Currently, there are two devices implemented: - **sampler** and **plugin**. +## installation -> [!NOTE] -> Use `Tab` to switch focus between views. Use `Enter` to exclusively focus the highlighted view, -> and `Esc` to unfocus it. When a view is focused, use the `Arrow Keys` and `Enter` to navigate. -> Use `;` (semicolon) to open the command palette, which will list the remaining keybindings. +### binary download + +you can download [tek 0.2.0 "almost static"](https://codeberg.org/unspeaker/tek/releases/tag/0.2.0) +from codeberg releases. this standalone binary release, should work on any glibc-based system. + +### from distro repositories + +[![Packaging status](https://repology.org/badge/vertical-allrepos/tek.svg)](https://repology.org/project/tek/versions) + +#### arch linux + +[tek 0.2.0-rc7](https://aur.archlinux.org/packages/tek) is available as a package in the AUR. +you can install it using your preferred AUR helper (e.g. `paru`): + +```sh +paru -S tek +``` + +### building from source + +requires docker. + +``` +git clone --recursive -b 0.2 https://codeberg.org/unspeaker/tek +cd tek # enter directory +cat bin/release-glibc.sh # preview build script +sudo bin/release-glibc.sh # run build script +sudo cp bin/tek /usr/local/bin/tek # install +``` + +## design goals + +* inspired by trackers and hardware sequencers, + but with the critical feature that 90s samplers lack: + able to **resample, i.e. record while playing!** + +* **pop-up scratchpad for musical ideas.** + low resource consumption, can stay open in background. + but flexible enough to allow expanding on compositions + +* **human- and machine- readable project format** + simple representation for project data + enable scripting and remapping. diff --git a/app/Cargo.toml b/app/Cargo.toml new file mode 100644 index 00000000..d6c3afa9 --- /dev/null +++ b/app/Cargo.toml @@ -0,0 +1,58 @@ +[package] +name = "tek" +edition = { workspace = true } +version = { workspace = true } + +[lib] +path = "tek.rs" + +[[bin]] +name = "tek" +path = "tek_cli.rs" + +[target.'cfg(target_os = "linux")'] +rustflags = ["-C", "link-arg=-fuse-ld=mold"] + +[dependencies] +tek_device = { path = "../device" } + +atomic_float = { workspace = true } +backtrace = { workspace = true } +clap = { workspace = true, optional = true } +jack = { workspace = true } +konst = { workspace = true } +livi = { workspace = true, optional = true } +midly = { workspace = true } +palette = { workspace = true } +rand = { workspace = true } +symphonia = { workspace = true, optional = true } +tengri = { workspace = true } +toml = { workspace = true } +uuid = { workspace = true, optional = true } +wavers = { workspace = true, optional = true } +winit = { workspace = true, optional = true } +xdg = { workspace = true } + +[dev-dependencies] +proptest = { workspace = true } +proptest-derive = { workspace = true } + +[features] +arranger = ["port", "editor", "sequencer", "editor"] +browse = [] +clap = [] +cli = ["dep:clap"] +clock = [] +default = ["cli", "arranger", "sampler", "lv2"] +editor = [] +host = ["lv2"] +lv2 = ["port", "livi", "winit"] +meter = [] +mixer = [] +pool = [] +port = [] +sampler = ["port", "meter", "mixer", "browse", "symphonia", "wavers"] +sequencer = ["port", "clock", "uuid", "pool"] +sf2 = [] +vst2 = [] +vst3 = [] diff --git a/app/tek.edn b/app/tek.edn new file mode 100644 index 00000000..17b02a6c --- /dev/null +++ b/app/tek.edn @@ -0,0 +1,224 @@ +(keys :axis/x + (@left x/dec) + (@right x/inc)) +(keys :axis/x2 + (@shift/left x2/dec) + (@shift/right x2/inc)) +(keys :axis/y + (@up y/dec) + (@down y/inc)) +(keys :axis/y2 + (@shift/up y2/dec) + (@shift/down y2/inc)) +(keys :axis/z + (@minus z/dec) + (@equal z/inc)) +(keys :axis/z2 + (@underscore z2/dec) + (@plus z2/inc)) +(keys :axis/i + (@comma i/dec) + (@period z/inc)) +(keys :axis/i2 + (@lt i2/dec) + (@gt z2/inc)) +(keys :axis/w + (@openbracket w/dec) + (@closebracket w/inc)) +(keys :axis/w2 + (@openbrace w2/dec) + (@closebrace w2/inc)) + +(mode :menu (keys :axis/y :confirm) :menu) + +(keys :confirm + (@enter confirm)) + +(view :menu (bg (g 0) (bsp/s + :ports/out + (bsp/n :ports/in (bg (g 30) (bsp/s (fixed/y 7 :logo) (fill :dialog/menu))))))) + +(view :menu (bsp/s + (push/y 4 (fixed/xy 20 2 (bg (g 0) :debug))) + (fixed 20 2 (bg (g 20) (push/x 2 :debug))))) + +(view :menu (bsp/s (fixed/y 4 :debug) :debug)) + +(view :ports/out (fill/x (fixed/y 3 (bsp/a + (fill/x (align/w (text L-AUDIO-OUT))) + (bsp/a (text MIDI-OUT) (fill/x (align/e (text AUDIO-OUT-R)))))))) + +(view :ports/in (fill/x (fixed/y 3 (bsp/a + (fill/x (align/w (text L-AUDIO-IN))) + (bsp/a (text MIDI-IN) (fill/x (align/e (text AUDIO-IN-R)))))))) + +(view :browse (bsp/s + (padding 3 1 :browse-title) + (enclose (fg (g 96)) browser))) + +(keys :help + (@f1 dialog :help)) + +(keys :back + (@escape back)) + +(keys :page + (@pgup page/up) + (@pgdn page/down)) + +(keys :delete + (@delete delete) + (@backspace delete/back)) + +(keys :input (see :axis/x :delete) + (:char input)) + +(keys :list (see :axis/y :confirm)) + +(keys :length (see :axis/x :axis/y :confirm)) + +(keys :browse (see :list :input :focus)) + +(keys :history + (@u undo 1) + (@r redo 1)) + +(keys :clock + (@space clock/toggle 0) + (@shift/space clock/toggle 0)) + +(keys :color + (@c color)) + +(keys :launch + (@q launch)) + +(keys :saveload + (@f6 dialog :save) + (@f9 dialog :load)) + +(keys :global (see :history :saveload) + (@f8 dialog :options) + (@f10 dialog :quit)) + +(keys :focus) + +(mode :transport (name Transport) (info A JACK transport controller.) (keys :clock :global) + (view :transport)) + +(mode :arranger (name Arranger) (info A grid of launchable clips arranged by track and scene.) + (mode :editor (keys :editor)) (mode :dialog (keys :dialog)) (mode :message (keys :message)) + (mode :add-device (keys :add-device)) (mode :browse (keys :browse)) (mode :rename (keys :input)) + (mode :length (keys :rename)) (mode :clip (keys :clip)) (mode :track (keys :track)) + (mode :scene (keys :scene)) (mode :mix (keys :mix)) + (keys :clock :arranger :global) :arranger) + +(view :arranger (bsp/n + :status + (bsp/w :meters/output (bsp/e :meters/input :arrangement)))) + +(view :arrangement (bsp/n + :tracks/inputs + (bsp/s :tracks/outputs (bsp/s :tracks/names (bsp/s :tracks/devices + (fill (either :mode/editor (bsp/e :scenes/names :editor) :scenes))))))) + +(keys :arranger (see :color :launch :scenes :tracks) + (@tab project/edit) (@enter project/edit) + (@shift/I project/input/add) (@shift/O project/output/add) + (@shift/S project/scene/add) (@shift/T project/track/add) + (@shift/D dialog/show :dialog/device)) + +(keys :tracks + (@t select :select/track) + (@left select :select/track/dec) + (@right select :select/track/inc)) + +(keys :scenes + (@s select :select/scene) + (@up select :select/scene/dec) + (@down select :select/scene/inc)) + +(keys :track (see :color :launch :axis/z :axis/z2 :delete) + (@r toggle :rec) + (@m toggle :mon) + (@p toggle :play) + (@P toggle :solo)) +(keys :scene (see :color :launch :axis/z :axis/z2 :delete)) + +(keys :clip (see :color :launch :axis/z :axis/z2 :delete) + (@l toggle :loop)) + +(mode :groovebox (name Groovebox) (info A sequencer with built-in sampler.) + (mode browse (keys :browse)) + (mode rename (keys :pool-rename)) + (mode length (keys :pool-length)) + (keys :clock :editor :sampler :global) (view :groovebox)) + +(view :groovebox (bsp/w + :meters/output + (bsp/e :meters/input (bsp/w :groove/meta :groove/editor)))) + +(view :groove/meta (fill/y (align/n (stack/s + :midi-ins/status :midi-outs/status :audio-ins/status :audio-outs/status :pool)))) + +(view :groove/editor (bsp/n + :groove/sample + :groove/sequence)) + +(view :groove/sample (fixed/y :h-sample-detail (bsp/e + (fill/y (fixed/x 20 (align/nw :sample-status))) + :sample-viewer))) + +(view :groove/sequence (bsp/e + (fill/y (align/n (bsp/s :status/v :editor-status))) + (bsp/e :samples/keys :editor))) + +(mode :sampler (name Sampler) (info A sampling soundboard.) + (keys :sampler :global) (view :sampler)) + +(view :sampler (bsp/s + (fixed/y 1 :transport) + (bsp/n (fixed/y 1 :status) (fill :samples/grid)))) + +(keys :sampler (see :sampler/directions :sampler/record :sampler/play)) + +(keys :sampler/record + (@r sampler/record/toggle :sample/selected) (@shift/R sampler/record/back)) + +(keys :sampler/play + (@p sampler/play/sample :sample/selected) (@P sampler/stop/sample :sample/selected)) + +(keys :sampler/import-export + (@shift/f6 dialog :dialog/export/sample) (@shift/f9 dialog :dialog/import/sample)) + +(keys :sampler/directions + (@up sampler/select :sample/above) + (@down sampler/select :sample/below) + (@left sampler/select :sample/to/left) + (@right sampler/select :sample/to/right)) + +(mode :sequencer (name Sequencer) (info A MIDI sequencer.) + (mode browse (keys :browse)) (mode rename (keys :pool/rename)) (mode length (keys :pool/length)) + (keys :editor :clock :global) (view :sequencer)) + +(view :sequencer (bsp/s + (fixed/y 1 :transport) + (bsp/n (fixed/y 1 :status) (fill (bsp/a (fill/xy (align/e :pool)) :editor))))) + +(keys :editor (see :editor/view :editor/note)) + +(keys :editor/view (see :axis/x :axis/x2 :axis/z :axis/z2) + (@z toggle :lock)) + +(keys :editor/note (see :axis/i :axis/i2 :axis/y :page) + (@a editor/append :true) + (@enter editor/append :false) + (@del editor/delete/note) + (@shift/del editor/delete/note)) + +(keys :pool (see :axis-y :axis-w :axis/z2 :color :delete) + (@n rename/begin) (@t length/begin) (@m import/begin) (@x export/begin) + (@shift/A clip/add :after :new/clip) (@shift/D clip/add :after :cloned/clip)) + +(keys :sequencer (see :color :launch) + (@shift/I input/add) (@shift/O output/add)) diff --git a/app/tek.rs b/app/tek.rs new file mode 100644 index 00000000..1ce0c252 --- /dev/null +++ b/app/tek.rs @@ -0,0 +1,468 @@ +#![feature( + adt_const_params, + associated_type_defaults, + closure_lifetime_binder, + if_let_guard, + impl_trait_in_assoc_type, + trait_alias, + type_alias_impl_trait, + type_changing_struct_update, +)] + +#![allow( + clippy::unit_arg +)] + +#[cfg(test)] mod tek_test; + +mod tek_bind; pub use self::tek_bind::*; +mod tek_cfg; pub use self::tek_cfg::*; +mod tek_deps; pub use self::tek_deps::*; +mod tek_mode; pub use self::tek_mode::*; +mod tek_view; pub use self::tek_view::*; + +/// Total state +#[derive(Default, Debug)] +pub struct App { + /// Base color. + pub color: ItemTheme, + /// Must not be dropped for the duration of the process + pub jack: Jack<'static>, + /// Display size + pub size: Measure, + /// Performance counter + pub perf: PerfModel, + /// Available view modes and input bindings + pub config: Config, + /// Currently selected mode + pub mode: Arc>>, + /// Undo history + pub history: Vec<(AppCommand, Option)>, + /// Dialog overlay + pub dialog: Dialog, + /// Contains all recently created clips. + pub pool: Pool, + /// Contains the currently edited musical arrangement + pub project: Arrangement, +} + +audio!( + |self: App, client, scope|{ + let t0 = self.perf.get_t0(); + self.clock().update_from_scope(scope).unwrap(); + let midi_in = self.project.midi_input_collect(scope); + if let Some(editor) = &self.editor() { + let mut pitch: Option = 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 = self.project.process_tracks(client, scope); + self.perf.update_from_jack_scope(t0, scope); + result + }; + |self, event|{ + use JackEvent::*; + match event { + SampleRate(sr) => { self.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:?}"); } + } + } +); + +// Allow source to be read as Literal string +dsl_ns!(App: Arc { + literal = |dsl|Ok(dsl.src()?.map(|x|x.into())); +}); + +// Provide boolean values. +dsl_ns!(App: bool { + // TODO literal = ... + word = |app| { + ":mode/editor" => app.project.editor.is_some(), + ":focused/dialog" => !matches!(app.dialog, Dialog::None), + ":focused/message" => matches!(app.dialog, Dialog::Message(..)), + ":focused/add_device" => matches!(app.dialog, Dialog::Device(..)), + ":focused/browser" => app.dialog.browser().is_some(), + ":focused/pool/import" => matches!(app.pool.mode, Some(PoolMode::Import(..))), + ":focused/pool/export" => matches!(app.pool.mode, Some(PoolMode::Export(..))), + ":focused/pool/rename" => matches!(app.pool.mode, Some(PoolMode::Rename(..))), + ":focused/pool/length" => matches!(app.pool.mode, Some(PoolMode::Length(..))), + ":focused/clip" => !app.editor_focused() && matches!(app.selection(), + Selection::TrackClip{..}), + ":focused/track" => !app.editor_focused() && matches!(app.selection(), + Selection::Track(..)), + ":focused/scene" => !app.editor_focused() && matches!(app.selection(), + Selection::Scene(..)), + ":focused/mix" => !app.editor_focused() && matches!(app.selection(), + Selection::Mix), + }; +}); + +// TODO: provide colors here +dsl_ns!(App: ItemTheme {}); + +dsl_ns!(App: Dialog { + word = |app| { + ":dialog/none" => Dialog::None, + ":dialog/options" => Dialog::Options, + ":dialog/device" => Dialog::Device(0), + ":dialog/device/prev" => Dialog::Device(0), + ":dialog/device/next" => Dialog::Device(0), + ":dialog/help" => Dialog::Help(0), + ":dialog/save" => Dialog::Browse(BrowseTarget::SaveProject, + Browse::new(None).unwrap().into()), + ":dialog/load" => Dialog::Browse(BrowseTarget::LoadProject, + Browse::new(None).unwrap().into()), + ":dialog/import/clip" => Dialog::Browse(BrowseTarget::ImportClip(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/export/clip" => Dialog::Browse(BrowseTarget::ExportClip(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/import/sample" => Dialog::Browse(BrowseTarget::ImportSample(Default::default()), + Browse::new(None).unwrap().into()), + ":dialog/export/sample" => Dialog::Browse(BrowseTarget::ExportSample(Default::default()), + Browse::new(None).unwrap().into()), + }; +}); + +dsl_ns!(App: Selection { + word = |app| { + ":select/scene" => app.selection().select_scene(app.tracks().len()), + ":select/scene/next" => app.selection().select_scene_next(app.scenes().len()), + ":select/scene/prev" => app.selection().select_scene_prev(), + ":select/track" => app.selection().select_track(app.tracks().len()), + ":select/track/next" => app.selection().select_track_next(app.tracks().len()), + ":select/track/prev" => app.selection().select_track_prev(), + }; +}); + +dsl_ns!(App: Color { + word = |app| { + ":color/bg" => Color::Rgb(28, 32, 36), + }; + expr = |app| { + "g" (n: u8) => Color::Rgb(n, n, n), + "rgb" (r: u8, g: u8, b: u8) => Color::Rgb(r, g, b), + }; +}); + +dsl_ns!(App: Option { + word = |app| { + ":editor/pitch" => Some( + (app.editor().as_ref().map(|e|e.get_note_pos()).unwrap() as u8).into() + ) + }; +}); + +dsl_ns!(App: Option { + word = |app| { + ":selected/scene" => app.selection().scene(), + ":selected/track" => app.selection().track(), + }; +}); + +dsl_ns!(App: Option>> { + word = |app| { + ":selected/clip" => if let Selection::TrackClip { track, scene } = app.selection() { + app.scenes()[*scene].clips[*track].clone() + } else { + None + } + }; +}); + +dsl_ns!(App: u8 { + literal = |dsl|Ok(if let Some(src) = dsl.src()? { + Some(to_number(src)? as u8) + } else { + None + }); +}); + +dsl_ns!(App: u16 { + literal = |dsl|Ok(if let Some(src) = dsl.src()? { + Some(to_number(src)? as u16) + } else { + None + }); + word = |app| { + ":w/sidebar" => app.project.w_sidebar(app.editor().is_some()), + ":h/sample-detail" => 6.max(app.height() as u16 * 3 / 9), + }; +}); + +dsl_ns!(App: usize { + literal = |dsl|Ok(if let Some(src) = dsl.src()? { + Some(to_number(src)? as usize) + } else { + None + }); + word = |app| { + ":scene-count" => app.scenes().len(), + ":track-count" => app.tracks().len(), + ":device-kind" => app.dialog.device_kind().unwrap_or(0), + ":device-kind/next" => app.dialog.device_kind_next().unwrap_or(0), + ":device-kind/prev" => app.dialog.device_kind_prev().unwrap_or(0), + }; +}); + +dsl_ns!(App: isize { + literal = |dsl|Ok(if let Some(src) = dsl.src()? { + Some(to_number(src)? as isize) + } else { + None + }); +}); + +has!(Jack<'static>: |self: App|self.jack); +has!(Pool: |self: App|self.pool); +has!(Dialog: |self: App|self.dialog); +has!(Clock: |self: App|self.project.clock); +has!(Option: |self: App|self.project.editor); +has!(Selection: |self: App|self.project.selection); +has!(Vec: |self: App|self.project.midi_ins); +has!(Vec: |self: App|self.project.midi_outs); +has!(Vec: |self: App|self.project.scenes); +has!(Vec: |self: App|self.project.tracks); +has!(Measure: |self: App|self.size); +has_clips!( |self: App|self.pool.clips); +maybe_has!(Track: |self: App| { MaybeHas::::get(&self.project) }; + { MaybeHas::::get_mut(&mut self.project) }); +maybe_has!(Scene: |self: App| { MaybeHas::::get(&self.project) }; + { MaybeHas::::get_mut(&mut self.project) }); + +impl HasClipsSize for App { + fn clips_size (&self) -> &Measure { &self.project.size_inner } +} +impl HasTrackScroll for App { + fn track_scroll (&self) -> usize { self.project.track_scroll() } +} +impl HasSceneScroll for App { + fn scene_scroll (&self) -> usize { self.project.scene_scroll() } +} +impl HasJack<'static> for App { + fn jack (&self) -> &Jack<'static> { &self.jack } +} +impl ScenesView for App { + fn w_side (&self) -> u16 { 20 } + fn w_mid (&self) -> u16 { (self.width() as u16).saturating_sub(self.w_side()) } + fn h_scenes (&self) -> u16 { (self.height() as u16).saturating_sub(20) } +} + + +impl App { + + pub fn editor_focused (&self) -> bool { + false + } + + pub fn toggle_dialog (&mut self, mut dialog: Dialog) -> Dialog { + std::mem::swap(&mut self.dialog, &mut dialog); + dialog + } + + pub fn toggle_editor (&mut self, value: Option) { + //FIXME: self.editing.store(value.unwrap_or_else(||!self.is_editing()), Relaxed); + let value = value.unwrap_or_else(||!self.editor().is_some()); + if value { + // Create new clip in pool when entering empty cell + if let Selection::TrackClip { track, scene } = *self.selection() + && let Some(scene) = self.project.scenes.get_mut(scene) + && let Some(slot) = scene.clips.get_mut(track) + && slot.is_none() + && let Some(track) = self.project.tracks.get_mut(track) + { + let (index, mut clip) = self.pool.add_new_clip(); + // autocolor: new clip colors from scene and track color + let color = track.color.base.mix(scene.color.base, 0.5); + clip.write().unwrap().color = ItemColor::random_near(color, 0.2).into(); + if let Some(editor) = &mut self.project.editor { + editor.set_clip(Some(&clip)); + } + *slot = Some(clip.clone()); + //Some(clip) + } else { + //None + } + } else if let Selection::TrackClip { track, scene } = *self.selection() + && let Some(scene) = self.project.scenes.get_mut(scene) + && let Some(slot) = scene.clips.get_mut(track) + && let Some(clip) = slot.as_mut() + { + // Remove clip from arrangement when exiting empty clip editor + let mut swapped = None; + if clip.read().unwrap().count_midi_messages() == 0 { + std::mem::swap(&mut swapped, slot); + } + if let Some(clip) = swapped { + self.pool.delete_clip(&clip.read().unwrap()); + } + } + } + + pub fn browser (&self) -> Option<&Browse> { + if let Dialog::Browse(_, ref b) = self.dialog { Some(b) } else { None } + } + + pub fn device_pick (&mut self, index: usize) { + self.dialog = Dialog::Device(index); + } + + pub fn add_device (&mut self, index: usize) -> Usually<()> { + match index { + 0 => { + let name = self.jack.with_client(|c|c.name().to_string()); + let midi = self.project.track().expect("no active track").sequencer.midi_outs[0].port_name(); + let track = self.track().expect("no active track"); + let port = format!("{}/Sampler", &track.name); + let connect = Connect::exact(format!("{name}:{midi}")); + let sampler = if let Ok(sampler) = Sampler::new( + &self.jack, &port, &[connect], &[&[], &[]], &[&[], &[]] + ) { + self.dialog = Dialog::None; + Device::Sampler(sampler) + } else { + self.dialog = Dialog::Message("Failed to add device.".into()); + return Err("failed to add device".into()) + }; + let track = self.track_mut().expect("no active track"); + track.devices.push(sampler); + Ok(()) + }, + 1 => { + todo!(); + Ok(()) + }, + _ => unreachable!(), + } + } + + pub fn update_clock (&self) { + ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80) + } +} + +/// Various possible dialog modes. +#[derive(Debug, Clone, Default, PartialEq)] +pub enum Dialog { + #[default] None, + Help(usize), + Menu(usize, MenuItems), + Device(usize), + Message(Arc), + Browse(BrowseTarget, Arc), + Options, +} + +#[derive(Debug, Clone, Default, PartialEq)] +pub struct MenuItems(pub Arc<[MenuItem]>); + +impl AsRef> for MenuItems { + fn as_ref (&self) -> &Arc<[MenuItem]> { + &self.0 + } +} + +#[derive(Clone)] +pub struct MenuItem( + /// Label + pub Arc, + /// Callback + pub ArcUsually<()> + Send + Sync>> +); + +impl Default for MenuItem { + fn default () -> Self { Self("".into(), Arc::new(Box::new(|_|Ok(())))) } +} + +impl_debug!(MenuItem |self, w| { write!(w, "{}", &self.0) }); + +impl PartialEq for MenuItem { + fn eq (&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Dialog { + pub fn welcome () -> Self { + Self::Menu(1, MenuItems([ + MenuItem("Resume session".into(), Arc::new(Box::new(|_|Ok(())))), + MenuItem("Create new session".into(), Arc::new(Box::new(|app|Ok({ + app.dialog = Dialog::None; + app.mode = app.config.modes.clone().read().unwrap().get(":arranger").cloned().unwrap(); + })))), + MenuItem("Load old session".into(), Arc::new(Box::new(|_|Ok(())))), + ].into())) + } + pub fn menu_next (&self) -> Self { + match self { + Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.0.len()), items.clone()), + _ => Self::None + } + } + pub fn menu_prev (&self) -> Self { + match self { + Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.0.len()), items.clone()), + _ => Self::None + } + } + pub fn menu_selected (&self) -> Option { + if let Self::Menu(selected, _) = self { Some(*selected) } else { None } + } + pub fn device_kind (&self) -> Option { + if let Self::Device(index) = self { Some(*index) } else { None } + } + pub fn device_kind_next (&self) -> Option { + self.device_kind().map(|index|(index + 1) % device_kinds().len()) + } + pub fn device_kind_prev (&self) -> Option { + self.device_kind().map(|index|index.overflowing_sub(1).0.min(device_kinds().len().saturating_sub(1))) + } + pub fn message (&self) -> Option<&str> { + todo!() + } + pub fn browser (&self) -> Option<&Arc> { + todo!() + } + pub fn browser_target (&self) -> Option<&BrowseTarget> { + todo!() + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +//has_editor!(|self: App|{ + //editor = self.editor; + //editor_w = { + //let size = self.size.w(); + //let editor = self.editor.as_ref().expect("missing editor"); + //let time_len = editor.time_len().get(); + //let time_zoom = editor.time_zoom().get().max(1); + //(5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16) + //}; + //editor_h = 15; + //is_editing = self.editor.is_some(); +//}); diff --git a/app/tek_bind.rs b/app/tek_bind.rs new file mode 100644 index 00000000..84763b70 --- /dev/null +++ b/app/tek_bind.rs @@ -0,0 +1,325 @@ +use crate::*; + +pub type Binds = Arc, EventMap>>>>; + +/// A collection of input bindings. +#[derive(Debug)] +pub struct EventMap( + /// Map of each event (e.g. key combination) to + /// all command expressions bound to it by + /// all loaded input layers. + pub BTreeMap>> +); + +/// An input binding. +#[derive(Debug, Clone)] +pub struct Binding { + pub commands: Arc<[C]>, + pub condition: Option, + pub description: Option>, + pub source: Option>, +} + +/// Input bindings are only returned if this evaluates to true +#[derive(Clone)] +pub struct Condition(Arcbool + Send + Sync>>); + +/// Default is always empty map regardless if `E` and `C` implement [Default]. +impl Default for EventMap { + fn default () -> Self { Self(Default::default()) } +} + +impl EventMap { + /// Create a new event map + pub fn new () -> Self { + Default::default() + } + /// Add a binding to an owned event map. + pub fn def (mut self, event: E, binding: Binding) -> Self { + self.add(event, binding); + self + } + /// Add a binding to an event map. + pub fn add (&mut self, event: E, binding: Binding) -> &mut Self { + if !self.0.contains_key(&event) { + self.0.insert(event.clone(), Default::default()); + } + self.0.get_mut(&event).unwrap().push(binding); + self + } + /// Return the binding(s) that correspond to an event. + pub fn query (&self, event: &E) -> Option<&[Binding]> { + self.0.get(event).map(|x|x.as_slice()) + } + /// Return the first binding that corresponds to an event, considering conditions. + pub fn dispatch (&self, event: &E) -> Option<&Binding> { + self.query(event) + .map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next()) + .flatten() + } +} + +impl EventMap> { + pub fn load_into (binds: &Binds, name: &impl AsRef, body: &impl Dsl) -> Usually<()> { + println!("EventMap::load_into: {}: {body:?}", name.as_ref()); + let mut map = Self::new(); + body.each(|item|if item.expr().head() == Ok(Some("see")) { + // TODO + Ok(()) + } else if let Ok(Some(_word)) = item.expr().head().word() { + if let Some(key) = TuiEvent::from_dsl(item.expr()?.head()?)? { + map.add(key, Binding { + commands: [item.expr()?.tail()?.unwrap_or_default().into()].into(), + condition: None, + description: None, + source: None + }); + Ok(()) + } else if Some(":char") == item.expr()?.head()? { + // TODO + return Ok(()) + } else { + return Err(format!("Config::load_bind: invalid key: {:?}", item.expr()?.head()?).into()) + } + } else { + return Err(format!("Config::load_bind: unexpected: {item:?}").into()) + })?; + binds.write().unwrap().insert(name.as_ref().into(), map); + Ok(()) + } +} + +impl Binding { + pub fn from_dsl (dsl: impl Dsl) -> Usually { + let command: Option = None; + let condition: Option = None; + let description: Option> = None; + let source: Option> = None; + if let Some(command) = command { + Ok(Self { commands: [command].into(), condition, description, source }) + } else { + Err(format!("no command in {dsl:?}").into()) + } + } +} + +impl_debug!(Condition |self, w| { write!(w, "*") }); + +handle!(TuiIn:|self: App, input|{ + let mut commands = vec![]; + for id in self.mode.keys.iter() { + if let Some(event_map) = self.config.binds.clone().read().unwrap().get(id.as_ref()) { + if let Some(bindings) = event_map.query(input.event()) { + for binding in bindings { + for command in binding.commands.iter() { + if let Some(command) = self.from(command)? as Option { + commands.push(command) + } + } + } + } + } + } + for command in commands.into_iter() { + let result = command.execute(self); + match result { + Ok(undo) => { + self.history.push((command, undo)); + }, + Err(e) => { + self.history.push((command, None)); + return Err(e) + } + } + } + Ok(None) +}); + +#[derive(Debug, Copy, Clone)] +pub enum Axis { X, Y, Z, I } + +impl<'a> DslNs<'a, AppCommand> for App {} +impl<'a> DslNsExprs<'a, AppCommand> for App {} +impl<'a> DslNsWords<'a, AppCommand> for App { + dsl_words!('a |app| -> AppCommand { + "x/inc" => AppCommand::Inc { axis: Axis::X }, + "x/dec" => AppCommand::Dec { axis: Axis::X }, + "y/inc" => AppCommand::Inc { axis: Axis::Y }, + "y/dec" => AppCommand::Dec { axis: Axis::Y }, + "confirm" => AppCommand::Confirm, + "cancel" => AppCommand::Cancel, + }); +} + +impl Default for AppCommand { fn default () -> Self { Self::Nop } } + +def_command!(AppCommand: |app: App| { + Nop => Ok(None), + Confirm => Ok(match &app.dialog { + Dialog::Menu(index, items) => { + let callback = items.0[*index].1.clone(); + callback(app)?; + None + }, + _ => todo!(), + }), + Cancel => todo!(), // TODO delegate: + Inc { axis: Axis } => Ok(match (&app.dialog, axis) { + (Dialog::None, _) => todo!(), + (Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_next() } + .execute(app)?, + _ => todo!() + }), + Dec { axis: Axis } => Ok(match (&app.dialog, axis) { + (Dialog::None, _) => None, + (Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_prev() } + .execute(app)?, + _ => todo!() + }), + SetDialog { dialog: Dialog } => { + swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }) + }, +}); + + //AppCommand => { + //("x/inc" / + //("stop-all") => todo!(),//app.project.stop_all(), + //("enqueue", clip: Option>>) => todo!(), + //("history", delta: isize) => todo!(), + //("zoom", zoom: usize) => todo!(), + //("select", selection: Selection) => todo!(), + //("dialog" / command: DialogCommand) => todo!(), + //("project" / command: ArrangementCommand) => todo!(), + //("clock" / command: ClockCommand) => todo!(), + //("sampler" / command: SamplerCommand) => todo!(), + //("pool" / command: PoolCommand) => todo!(), + //("edit" / editor: MidiEditCommand) => todo!(), + //}; + + //DialogCommand; + + //ArrangementCommand; + + //ClockCommand; + + //SamplerCommand; + + //PoolCommand; + + //MidiEditCommand; + + +//take!(DialogCommand |state: App, iter|Take::take(&state.dialog, iter)); +//#[derive(Clone, Debug)] +//pub enum DialogCommand { + //Open { dialog: Dialog }, + //Close +//} + +//impl Command> for DialogCommand { + //fn execute (self, state: &mut Option) -> Perhaps { + //match self { + //Self::Open { dialog } => { + //*state = Some(dialog); + //}, + //Self::Close => { + //*state = None; + //} + //}; + //Ok(None) + //} +//} + +//dsl!(DialogCommand: |self: Dialog, iter|todo!()); +//Dsl::take(&mut self.dialog, iter)); + +//#[tengri_proc::command(Option)]//Nope. +//impl DialogCommand { + //fn open (dialog: &mut Option, new: Dialog) -> Perhaps { + //*dialog = Some(new); + //Ok(None) + //} + //fn close (dialog: &mut Option) -> Perhaps { + //*dialog = None; + //Ok(None) + //} +//} +// +//dsl_bind!(AppCommand: App { + //enqueue = |app, clip: Option>>| { todo!() }; + //history = |app, delta: isize| { todo!() }; + //zoom = |app, zoom: usize| { todo!() }; + //stop_all = |app| { app.tracks_stop_all(); Ok(None) }; + ////dialog = |app, command: DialogCommand| + ////Ok(command.delegate(&mut app.dialog, |c|Self::Dialog{command: c})?); + //project = |app, command: ArrangementCommand| + //Ok(command.delegate(&mut app.project, |c|Self::Project{command: c})?); + //clock = |app, command: ClockCommand| + //Ok(command.execute(app.clock_mut())?.map(|c|Self::Clock{command: c})); + //sampler = |app, command: SamplerCommand| + //Ok(app.project.sampler_mut().map(|s|command.delegate(s, |command|Self::Sampler{command})) + //.transpose()?.flatten()); + //pool = |app, command: PoolCommand| { + //let undo = command.clone().delegate(&mut app.pool, |command|AppCommand::Pool{command})?; + //// update linked editor after pool action + //match command { + //// autoselect: automatically load selected clip in editor + //PoolCommand::Select { .. } | + //// autocolor: update color in all places simultaneously + //PoolCommand::Clip { command: PoolClipCommand::SetColor { .. } } => { + //let clip = app.pool.clip().clone(); + //app.editor_mut().map(|editor|editor.set_clip(clip.as_ref())) + //}, + //_ => None + //}; + //Ok(undo) + //}; + //select = |app, selection: Selection| { + //*app.project.selection_mut() = selection; + ////todo! + ////if let Some(ref mut editor) = app.editor_mut() { + ////editor.set_clip(match selection { + ////Selection::TrackClip { track, scene } if let Some(Some(Some(clip))) = app + ////.project + ////.scenes.get(scene) + ////.map(|s|s.clips.get(track)) + ////=> + ////Some(clip), + ////_ => + ////None + ////}); + ////} + //Ok(None) + ////("select" [t: usize, s: usize] Some(match (t.expect("no track"), s.expect("no scene")) { + ////(0, 0) => Self::Select(Selection::Mix), + ////(t, 0) => Self::Select(Selection::Track(t)), + ////(0, s) => Self::Select(Selection::Scene(s)), + ////(t, s) => Self::Select(Selection::TrackClip { track: t, scene: s }) }))) + //// autoedit: load focused clip in editor. + //}; + ////fn color (app: &mut App, theme: ItemTheme) -> Perhaps { + ////Ok(app.set_color(Some(theme)).map(|theme|Self::Color{theme})) + ////} + ////fn launch (app: &mut App) -> Perhaps { + ////app.project.launch(); + ////Ok(None) + ////} + //toggle_editor = |app, value: bool|{ app.toggle_editor(Some(value)); Ok(None) }; + //editor = |app, command: MidiEditCommand| Ok(if let Some(editor) = app.editor_mut() { + //let undo = command.clone().delegate(editor, |command|AppCommand::Editor{command})?; + //// update linked sampler after editor action + //app.project.sampler_mut().map(|sampler|match command { + //// autoselect: automatically select sample in sampler + //MidiEditCommand::SetNotePos { pos } => { sampler.set_note_pos(pos); }, + //_ => {} + //}); + //undo + //} else { + //None + //}); +//}); +//take!(ClockCommand |state: App, iter|Take::take(state.clock(), iter)); +//take!(MidiEditCommand |state: App, iter|Ok(state.editor().map(|x|Take::take(x, iter)).transpose()?.flatten())); +//take!(PoolCommand |state: App, iter|Take::take(&state.pool, iter)); +//take!(SamplerCommand |state: App, iter|Ok(state.project.sampler().map(|x|Take::take(x, iter)).transpose()?.flatten())); +//take!(ArrangementCommand |state: App, iter|Take::take(&state.project, iter)); diff --git a/app/tek_cfg.rs b/app/tek_cfg.rs new file mode 100644 index 00000000..ee323bb6 --- /dev/null +++ b/app/tek_cfg.rs @@ -0,0 +1,65 @@ +use crate::*; + +/// Configuration. +/// +/// Contains mode, view, and bind definitions. +#[derive(Default, Debug)] +pub struct Config { + pub dirs: BaseDirectories, + pub modes: Modes, + pub views: Views, + pub binds: Binds, +} + +impl Config { + const CONFIG: &'static str = "tek.edn"; + const DEFAULTS: &'static str = include_str!("./tek.edn"); + + pub fn new (dirs: Option) -> Self { + Self { + dirs: dirs.unwrap_or_else(||BaseDirectories::with_profile("tek", "v0")), + ..Default::default() + } + } + pub fn init (&mut self) -> Usually<()> { + self.init_file(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|cfgs.load(&dsl))?; + Ok(()) + } + pub fn init_file ( + &mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()> + ) -> Usually<()> { + if self.dirs.find_config_file(path).is_none() { + println!("Creating {path:?}"); + std::fs::write(self.dirs.place_config_file(path)?, defaults)?; + } + Ok(if let Some(path) = self.dirs.find_config_file(path) { + println!("Loading {path:?}"); + let src = std::fs::read_to_string(&path)?; + src.as_str().each(move|item|each(self, item))?; + } else { + return Err(format!("{path}: not found").into()) + }) + } + pub fn load (&mut self, dsl: impl Dsl) -> Usually<()> { + dsl.each(|item|if let Some(expr) = item.expr()? { + let head = expr.head()?; + let tail = expr.tail()?; + let name = tail.head()?; + let body = tail.tail()?; + println!("Config::load: {} {} {}", head.unwrap_or_default(), name.unwrap_or_default(), body.unwrap_or_default()); + match head { + Some("mode") if let Some(name) = name => + Mode::>::load_into(&self.modes, &name, &body)?, + Some("keys") if let Some(name) = name => + EventMap::>::load_into(&self.binds, &name, &body)?, + Some("view") if let Some(name) = name => { + self.views.write().unwrap().insert(name.into(), body.src()?.unwrap_or_default().into()); + }, + _ => return Err(format!("Config::load: expected view/keys/mode, got: {item:?}").into()) + } + Ok(()) + } else { + return Err(format!("Config::load: expected expr, got: {item:?}").into()) + }) + } +} diff --git a/app/tek_cli.rs b/app/tek_cli.rs new file mode 100644 index 00000000..3562ecab --- /dev/null +++ b/app/tek_cli.rs @@ -0,0 +1,132 @@ +pub(crate) use tek::*; +pub(crate) use clap::{self, Parser, Subcommand}; + +/// Application entrypoint. +pub fn main () -> Usually<()> { + Cli::parse().run() +} + +#[derive(Debug, Parser)] +#[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)] mode: Option, + /// Name of JACK client + #[arg(short='n', long)] name: Option, + /// Whether to attempt to become transport master + #[arg(short='S', long, default_value_t = false)] sync_lead: bool, + /// Whether to sync to external transport master + #[arg(short='s', long, default_value_t = true)] sync_follow: bool, + /// Initial tempo in beats per minute + #[arg(short='b', long, default_value = None)] bpm: Option, + /// Whether to include a transport toolbar (default: true) + #[arg(short='t', long, default_value_t = true)] show_clock: bool, + /// MIDI outs to connect to (multiple instances accepted) + #[arg(short='I', long)] midi_from: Vec, + /// MIDI outs to connect to (multiple instances accepted) + #[arg(short='i', long)] midi_from_re: Vec, + /// MIDI ins to connect to (multiple instances accepted) + #[arg(short='O', long)] midi_to: Vec, + /// MIDI ins to connect to (multiple instances accepted) + #[arg(short='o', long)] midi_to_re: Vec, + /// Audio outs to connect to left input + #[arg(short='l', long)] left_from: Vec, + /// Audio outs to connect to right input + #[arg(short='r', long)] right_from: Vec, + /// Audio ins to connect from left output + #[arg(short='L', long)] left_to: Vec, + /// Audio ins to connect from right output + #[arg(short='R', long)] right_to: Vec, +} + +/// Application modes +#[derive(Debug, Clone, Subcommand)] +pub enum LaunchMode { + /// Create a new session instead of loading the previous one. + New, +} + +impl Cli { + pub fn run (&self) -> Usually<()> { + let name = self.name.as_ref().map_or("tek", |x|x.as_str()); + let tracks = vec![]; + let scenes = vec![]; + let empty = &[] as &[&str]; + let left_froms = Connect::collect(&self.left_from, empty, empty); + let left_tos = Connect::collect(&self.left_to, empty, empty); + let right_froms = Connect::collect(&self.right_from, empty, empty); + let right_tos = Connect::collect(&self.right_to, empty, empty); + let _audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; + let _audio_tos = &[left_tos.as_slice(), right_tos.as_slice()]; + let mut config = Config::new(None); + config.init()?; + Tui::new()?.run(&Jack::new_run(&name, move|jack|{ + let app = App { + jack: jack.clone(), + color: ItemTheme::random(), + dialog: Dialog::welcome(), + mode: config.modes.clone().read().unwrap().get(":menu").cloned().unwrap(), + config, + project: Arrangement { + name: Default::default(), + color: ItemTheme::random(), + jack: jack.clone(), + clock: Clock::new(&jack, self.bpm)?, + tracks, + scenes, + selection: Selection::TrackClip { track: 0, scene: 0 }, + midi_ins: { + let mut midi_ins = vec![]; + for (index, connect) in self.midi_froms().iter().enumerate() { + midi_ins.push(jack.midi_in(&format!("M/{index}"), &[connect.clone()])?); + } + midi_ins + }, + midi_outs: { + let mut midi_outs = vec![]; + for (index, connect) in self.midi_tos().iter().enumerate() { + midi_outs.push(jack.midi_out(&format!("{index}/M"), &[connect.clone()])?); + }; + midi_outs + }, + ..Default::default() + }, + ..Default::default() + }; + jack.sync_lead(self.sync_lead, |mut state|{ + let clock = app.clock(); + clock.playhead.update_from_sample(state.position.frame() as f64); + state.position.bbt = Some(clock.bbt()); + state.position + })?; + jack.sync_follow(self.sync_follow)?; + Ok(app) + })?) + } + fn midi_froms (&self) -> Vec { + Connect::collect(&self.midi_from, &[] as &[&str], &self.midi_from_re) + } + fn midi_tos (&self) -> Vec { + Connect::collect(&self.midi_to, &[] as &[&str], &self.midi_to_re) + } +} + +/// CLI header +const HEADER: &'static str = r#" +~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~ ~ ~ ~~ ~ ~ ~ ~~ ~ ~ ~ ~ ~~~~~~ ~ ~~~ + ~~ ║ ~ ╟─╌ ~╟─< ~ v0.3.0, 2025 sum(m)er @ the nose of the cat. ~ +~~~ ╨ ~ ╙──╜ ╨ ╜ ~ ~~~ ~ ~ ~ ~ ~~~ ~~~ ~ ~~ ~~ ~~ ~ ~~ + On first run, Tek will create configuration and state dirs: + * [x] ~/.config/tek - config + * [ ] ~/.local/share/tek - projects + * [ ] ~/.local/lib/tek - plugins + * [ ] ~/.cache/tek - cache +~"#; + +#[cfg(test)] #[test] fn test_cli () { + use clap::CommandFactory; + Cli::command().debug_assert(); + //let jack = Jack::default(); +} diff --git a/app/tek_deps.rs b/app/tek_deps.rs new file mode 100644 index 00000000..b4af1e3f --- /dev/null +++ b/app/tek_deps.rs @@ -0,0 +1,38 @@ +pub(crate) use ::{ + tek_device::{*, tek_engine::*}, + tengri::{ + Usually, Perhaps, Has, MaybeHas, has, maybe_has, impl_debug, from, + wrap_inc, wrap_dec, + dsl::*, + input::*, + output::*, + tui::{ + *, + ratatui::{ + self, + prelude::{Rect, Style, Stylize, Buffer, Modifier, buffer::Cell, Color::{self, *}}, + widgets::{Widget, canvas::{Canvas, Line}}, + }, + crossterm::{ + self, + event::{Event, KeyCode::{self, *}}, + }, + } + }, + std::{ + path::{Path, PathBuf}, + sync::{Arc, RwLock}, + sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}, + error::Error, + collections::BTreeMap, + fmt::Write, + cmp::Ord, + ffi::OsString, + fmt::{Debug, Formatter}, + fs::File, + ops::{Add, Sub, Mul, Div, Rem}, + thread::JoinHandle + }, + xdg::BaseDirectories, + atomic_float::* +}; diff --git a/app/tek_mode.rs b/app/tek_mode.rs new file mode 100644 index 00000000..f304b517 --- /dev/null +++ b/app/tek_mode.rs @@ -0,0 +1,58 @@ +use super::*; + +pub type Modes = Arc, Arc>>>>>; + +/// A set of currently active view and keys definitions, +/// with optional name and description. +#[derive(Default, Debug)] +pub struct Mode { + pub path: PathBuf, + pub name: Vec, + pub info: Vec, + pub view: Vec, + pub keys: Vec, + pub modes: Modes, +} + +impl Draw for Mode { + fn draw (&self, to: &mut TuiOut) { + self.content().draw(to) + } +} + +impl Mode> { + + pub fn load_into (modes: &Modes, name: &impl AsRef, body: &impl Dsl) -> Usually<()> { + let mut mode = Self::default(); + println!("Mode::load_into: {}: {body:?}", name.as_ref()); + body.each(|item|mode.load_one(item))?; + modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode)); + Ok(()) + } + + fn load_one (&mut self, dsl: impl Dsl) -> Usually<()> { + Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(head)) = expr.head() { + println!("Mode::load_one: {head} {:?}", expr.tail()); + let tail = expr.tail()?.map(|x|x.trim()).unwrap_or(""); + match head { + "name" => self.name.push(tail.into()), + "info" => self.info.push(tail.into()), + "view" => self.view.push(tail.into()), + "keys" => tail.each(|expr|{self.keys.push(expr.trim().into()); Ok(())})?, + "mode" => if let Some(id) = tail.head()? { + Self::load_into(&self.modes, &id, &tail.tail())?; + } else { + return Err(format!("Mode::load_one: self: incomplete: {expr:?}").into()); + }, + _ => { + return Err(format!("Mode::load_one: unexpected expr: {head:?} {tail:?}").into()) + }, + }; + } else if let Ok(Some(word)) = dsl.word() { + self.view.push(word.into()); + } else { + return Err(format!("Mode::load_one: unexpected: {dsl:?}").into()); + }) + } + +} diff --git a/app/tek_test.rs b/app/tek_test.rs new file mode 100644 index 00000000..71f604be --- /dev/null +++ b/app/tek_test.rs @@ -0,0 +1,103 @@ +use crate::*; + +#[cfg(test)] #[test] fn test_app () -> Usually<()> { + let mut app = App::default(); + let _ = app.scene_add(None, None)?; + let _ = app.update_clock(); + Ok(()) +} + +#[cfg(test)] #[test] fn test_track () -> Usually<()> { + let track = Track::default(); + Ok(()) +} + +#[cfg(test)] #[test] fn test_scene () -> Usually<()> { + let scene = Scene::default(); + let _ = scene.pulses(); + let _ = scene.is_playing(&[]); + Ok(()) +} + +#[cfg(test)] #[test] fn test_view_layout () { + let _ = button_play_pause(true); + let _ = button_2("", "", true); + let _ = button_2("", "", false); + let _ = button_3("", "", "", true); + let _ = button_3("", "", "", false); + //let _ = heading("", "", 0, "", true); + //let _ = heading("", "", 0, "", false); + let _ = wrap(Reset, Reset, ""); +} + +#[cfg(test)] mod test_view_meter { + use super::*; + use proptest::prelude::*; + #[test] fn test_view_meter () { + let _ = view_meter("", 0.0); + let _ = view_meters(&[0.0, 0.0]); + } + proptest! { + #[test] fn proptest_view_meter ( + label in "\\PC*", value in f32::MIN..f32::MAX + ) { + let _ = view_meter(&label, value); + } + #[test] fn proptest_view_meters ( + value1 in f32::MIN..f32::MAX, + value2 in f32::MIN..f32::MAX + ) { + let _ = view_meters(&[value1, value2]); + } + } +} + +#[cfg(test)] #[test] fn test_view_iter () { + let mut app = App::default(); + 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(); +} + +#[cfg(test)] #[test] fn test_view_sizes () { + let app = App::default(); + let _ = app.project.w(); + //let _ = app.project.w_sidebar(); + //let _ = app.project.w_tracks_area(); + let _ = app.project.h(); + //let _ = app.project.h_tracks_area(); + //let _ = app.project.h_inputs(); + //let _ = app.project.h_outputs(); + let _ = app.project.h_scenes(); +} + +#[cfg(test)] #[test] fn test_midi_edit () { + let _editor = MidiEditor::default(); + let mut editor = MidiEditor { + mode: PianoHorizontal::new(Some(&Arc::new(RwLock::new(MidiClip::stop_all())))), + size: Default::default(), + //keys: Default::default(), + }; + let _ = editor.put_note(true); + let _ = editor.put_note(false); + let _ = editor.clip_status(); + let _ = editor.edit_status(); + struct TestEditorHost(Option); + has!(Option: |self: TestEditorHost|self.0); + //has_editor!(|self: TestEditorHost|{ + //editor = self.0; + //editor_w = 0; + //editor_h = 0; + //is_editing = false; + //}); + let mut host = TestEditorHost(Some(editor)); + let _ = host.editor(); + let _ = host.editor_mut(); + let _ = host.is_editing(); + let _ = host.editor_w(); + let _ = host.editor_h(); +} diff --git a/app/tek_view.rs b/app/tek_view.rs new file mode 100644 index 00000000..9198ca0c --- /dev/null +++ b/app/tek_view.rs @@ -0,0 +1,353 @@ +use crate::*; + +pub type Views = Arc, Arc>>>; + +impl Draw for App { + fn draw (&self, to: &mut TuiOut) { + for (index, dsl) in self.mode.view.iter().enumerate() { + if let Err(e) = self.view(to, dsl) { + panic!("render #{index} failed ({e}): {dsl}"); + } + } + } +} + +impl View for App { + fn view_expr <'a> (&'a self, to: &mut TuiOut, expr: &'a impl DslExpr) -> Usually<()> { + if evaluate_output_expression(self, to, expr)? + || evaluate_output_expression_tui(self, to, expr)? { + Ok(()) + } else { + Err(format!("App::view_expr: unexpected: {expr:?}").into()) + } + } + fn view_word <'a> (&'a self, to: &mut TuiOut, dsl: &'a impl DslExpr) -> Usually<()> { + let mut frags = dsl.src()?.unwrap().split("/"); + match frags.next() { + Some(":logo") => to.place(&view_logo()), + Some(":status") => to.place(&Fixed::Y(1, "TODO: Status Bar")), + Some(":meters") => match frags.next() { + Some("input") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::Y(Align::s("Input Meters")))), + Some("output") => to.place(&Tui::bg(Rgb(30, 30, 30), Fill::Y(Align::s("Output Meters")))), + _ => panic!() + }, + Some(":tracks") => match frags.next() { + None => to.place(&"TODO tracks"), + Some("names") => to.place(&self.project.view_track_names(self.color.clone())),//Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Names")))), + Some("inputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Inputs")))), + Some("devices") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Devices")))), + Some("outputs") => to.place(&Tui::bg(Rgb(40, 40, 40), Fill::X(Align::w("Track Outputs")))), + _ => panic!() + }, + Some(":scenes") => match frags.next() { + None => to.place(&"TODO scenes"), + Some(":scenes/names") => to.place(&"TODO Scene Names"), + _ => panic!() + }, + Some(":editor") => to.place(&"TODO Editor"), + Some(":dialog") => match frags.next() { + Some("menu") => to.place(&if let Dialog::Menu(selected, items) = &self.dialog { + let items = items.clone(); + let selected = selected; + Some(Fill::XY(Thunk::new(move|to: &mut TuiOut|{ + for (index, MenuItem(item, _)) in items.0.iter().enumerate() { + to.place(&Push::Y((2 * index) as u16, + Tui::fg_bg( + if *selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) }, + if *selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) }, + Fixed::Y(2, Align::n(Fill::X(item))) + ))); + } + }))) + } else { + None + }), + _ => unimplemented!("App::view_word: {dsl:?} ({frags:?})"), + }, + Some(":templates") => to.place(&{ + let modes = self.config.modes.clone(); + let height = (modes.read().unwrap().len() * 2) as u16; + Fixed::Y(height, Min::X(30, Thunk::new(move |to: &mut TuiOut|{ + for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() { + let bg = if index == 0 { Rgb(70,70,70) } else { Rgb(50,50,50) }; + let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or(""); + let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or(""); + let fg1 = Rgb(224, 192, 128); + let fg2 = Rgb(224, 128, 32); + let field_name = Fill::X(Align::w(Tui::fg(fg1, name))); + let field_id = Fill::X(Align::e(Tui::fg(fg2, id))); + let field_info = Fill::X(Align::w(info)); + to.place(&Push::Y((2 * index) as u16, + Fixed::Y(2, Fill::X(Tui::bg(bg, Bsp::s( + Bsp::a(field_name, field_id), field_info)))))); + } + }))) + }), + Some(":sessions") => to.place(&Fixed::Y(6, Min::X(30, Thunk::new(|to: &mut TuiOut|{ + let fg = Rgb(224, 192, 128); + for (index, name) in ["session1", "session2", "session3"].iter().enumerate() { + let bg = if index == 0 { Rgb(50,50,50) } else { Rgb(40,40,40) }; + to.place(&Push::Y((2 * index) as u16, + &Fixed::Y(2, Fill::X(Tui::bg(bg, Align::w(Tui::fg(fg, name))))))); + } + })))), + Some(":browse/title") => to.place(&Fill::X(Align::w(FieldV(ItemColor::default(), + match self.dialog.browser_target().unwrap() { + BrowseTarget::SaveProject => "Save project:", + BrowseTarget::LoadProject => "Load project:", + BrowseTarget::ImportSample(_) => "Import sample:", + BrowseTarget::ExportSample(_) => "Export sample:", + BrowseTarget::ImportClip(_) => "Import clip:", + BrowseTarget::ExportClip(_) => "Export clip:", + }, Shrink::X(3, Fixed::Y(1, Tui::fg(Tui::g(96), RepeatH("🭻")))))))), + Some(":device") => { + let selected = self.dialog.device_kind().unwrap(); + to.place(&Bsp::s(Tui::bold(true, "Add device"), Map::south(1, + move||device_kinds().iter(), + move|_label: &&'static str, i|{ + let bg = if i == selected { Rgb(64,128,32) } else { Rgb(0,0,0) }; + let lb = if i == selected { "[ " } else { " " }; + let rb = if i == selected { " ]" } else { " " }; + Fill::X(Tui::bg(bg, Bsp::e(lb, Bsp::w(rb, "FIXME device name")))) }))) + }, + Some(":debug") => to.place(&Fixed::Y(1, format!("[{:?}]", to.area()))), + Some(_) => { + let views = self.config.views.read().unwrap(); + if let Some(dsl) = views.get(dsl.src()?.unwrap()) { + let dsl = dsl.clone(); + std::mem::drop(views); + self.view(to, &dsl)? + } else { + unimplemented!("{dsl:?}"); + } + }, + _ => unreachable!() + } + Ok(()) + } +} + +fn view_logo () -> impl Content { + Fixed::XY(32, 7, Tui::bold(true, Tui::fg(Rgb(240,200,180), col!{ + Fixed::Y(1, ""), + Fixed::Y(1, ""), + Fixed::Y(1, "~~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~~~~~~~~~"), + Fixed::Y(1, Bsp::e("~~~~ ║ ~ ╟─╌ ~╟─< ~~ ", Bsp::e(Tui::fg(Rgb(230,100,40), "v0.3.0"), " ~~"))), + Fixed::Y(1, "~~~~ ╨ ~ ╙──╜ ╨ ╜ ~~~~~~~~~~~~"), + }))) +} + +//pub fn view_nil (_: &App) -> TuiCb { + //|to|to.place(&Fill::XY("·")) +//} + + //Bsp::s("", + //Map::south(1, + //move||app.config.binds.layers.iter() + //.filter_map(|a|(a.0)(app).then_some(a.1)) + //.flat_map(|a|a) + //.filter_map(|x|if let Value::Exp(_, iter)=x.value{ Some(iter) } else { None }) + //.skip(offset) + //.take(20), + //|mut b,i|Fixed::X(60, Align::w(Bsp::e("(", Bsp::e( + //b.next().map(|t|Fixed::X(16, Align::w(Tui::fg(Rgb(64,224,0), format!("{}", t.value))))), + //Bsp::e(" ", Align::w(format!("{}", b.0.0.trim()))))))))))), + + //Dialog::Browse(BrowseTarget::Load, browser) => { + //"bobcat".boxed() + ////Bsp::s( + ////Fill::X(Align::w(Margin::XY(1, 1, Bsp::e( + ////Tui::bold(true, " Load project: "), + ////Shrink::X(3, Fixed::Y(1, RepeatH("🭻"))))))), + ////Outer(true, Style::default().fg(Tui::g(96))) + ////.enclose(Fill::XY(browser))) + //}, + //Dialog::Browse(BrowseTarget::Export, browser) => { + //"bobcat".boxed() + ////Bsp::s( + ////Fill::X(Align::w(Margin::XY(1, 1, Bsp::e( + ////Tui::bold(true, " Export: "), + ////Shrink::X(3, Fixed::Y(1, RepeatH("🭻"))))))), + ////Outer(true, Style::default().fg(Tui::g(96))) + ////.enclose(Fill::XY(browser))) + //}, + //Dialog::Browse(BrowseTarget::Import, browser) => { + //"bobcat".boxed() + ////Bsp::s( + ////Fill::X(Align::w(Margin::XY(1, 1, Bsp::e( + ////Tui::bold(true, " Import: "), + ////Shrink::X(3, Fixed::Y(1, RepeatH("🭻"))))))), + ////Outer(true, Style::default().fg(Tui::g(96))) + ////.enclose(Fill::XY(browser))) + //}, +// + //pub fn view_history (&self) -> impl Content { + //Fixed::Y(1, Fill::X(Align::w(FieldH(self.color, + //format!("History ({})", self.history.len()), + //self.history.last().map(|last|Fill::X(Align::w(format!("{:?}", last.0)))))))) + //} + //pub fn view_status_h2 (&self) -> impl Content { + //self.update_clock(); + //let theme = self.color; + //let clock = self.clock(); + //let playing = clock.is_rolling(); + //let cache = clock.view_cache.clone(); + ////let selection = self.selection().describe(self.tracks(), self.scenes()); + //let hist_len = self.history.len(); + //let hist_last = self.history.last(); + //Fixed::Y(2, Stack::east(move|add: &mut dyn FnMut(&dyn Draw)|{ + //add(&Fixed::X(5, Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, + //Either::new(false, // TODO + //Thunk::new(move||Fixed::X(9, Either::new(playing, + //Tui::fg(Rgb(0, 255, 0), " PLAYING "), + //Tui::fg(Rgb(255, 128, 0), " STOPPED "))) + //), + //Thunk::new(move||Fixed::X(5, Either::new(playing, + //Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), + //Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",)))) + //) + //) + //))); + //add(&" "); + //{ + //let cache = cache.read().unwrap(); + //add(&Fixed::X(15, Align::w(Bsp::s( + //FieldH(theme, "Beat", cache.beat.view.clone()), + //FieldH(theme, "Time", cache.time.view.clone()), + //)))); + //add(&Fixed::X(13, Align::w(Bsp::s( + //Fill::X(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), + //Fill::X(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), + //)))); + //add(&Fixed::X(12, Align::w(Bsp::s( + //Fill::X(Align::w(FieldH(theme, "Buf", cache.buf.view.clone()))), + //Fill::X(Align::w(FieldH(theme, "Lat", cache.lat.view.clone()))), + //)))); + ////add(&Bsp::s( + //////Fill::X(Align::w(FieldH(theme, "Selected", Align::w(selection)))), + ////Fill::X(Align::w(FieldH(theme, format!("History ({})", hist_len), + ////hist_last.map(|last|Fill::X(Align::w(format!("{:?}", last.0))))))), + ////"" + ////)); + //////if let Some(last) = self.history.last() { + //////add(&FieldV(theme, format!("History ({})", self.history.len()), + //////Fill::X(Align::w(format!("{:?}", last.0))))); + //////} + //} + //})) + //} + //pub fn view_status_v (&self) -> impl Content + use<'_> { + //self.update_clock(); + //let cache = self.project.clock.view_cache.read().unwrap(); + //let theme = self.color; + //let playing = self.clock().is_rolling(); + //Tui::bg(theme.darker.rgb, Fixed::XY(20, 5, Outer(true, Style::default().fg(Tui::g(96))).enclose( + //col!( + //Fill::X(Align::w(Bsp::e( + //Align::w(Tui::bg(if playing { Rgb(0, 128, 0) } else { Rgb(128, 64, 0) }, + //Either::new(false, // TODO + //Thunk::new(move||Fixed::X(9, Either::new(playing, + //Tui::fg(Rgb(0, 255, 0), " PLAYING "), + //Tui::fg(Rgb(255, 128, 0), " STOPPED "))) + //), + //Thunk::new(move||Fixed::X(5, Either::new(playing, + //Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), + //Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",)))) + //) + //) + //)), + //Bsp::s( + //FieldH(theme, "Beat", cache.beat.view.clone()), + //FieldH(theme, "Time", cache.time.view.clone()), + //), + //))), + //Fill::X(Align::w(FieldH(theme, "BPM", cache.bpm.view.clone()))), + //Fill::X(Align::w(FieldH(theme, "SR ", cache.sr.view.clone()))), + //Fill::X(Align::w(FieldH(theme, "Buf", Bsp::e(cache.buf.view.clone(), Bsp::e(" = ", cache.lat.view.clone()))))), + //)))) + //} + //pub fn view_status (&self) -> impl Content + use<'_> { + //self.update_clock(); + //let cache = self.project.clock.view_cache.read().unwrap(); + //view_status(Some(self.project.selection.describe(self.tracks(), self.scenes())), + //cache.sr.view.clone(), cache.buf.view.clone(), cache.lat.view.clone()) + //} + //pub fn view_transport (&self) -> impl Content + use<'_> { + //self.update_clock(); + //let cache = self.project.clock.view_cache.read().unwrap(); + //view_transport(self.project.clock.is_rolling(), + //cache.bpm.view.clone(), cache.beat.view.clone(), cache.time.view.clone()) + //} + //pub fn view_editor (&self) -> impl Content + use<'_> { + //let bg = self.editor() + //.and_then(|editor|editor.clip().clone()) + //.map(|clip|clip.read().unwrap().color.darker) + //.unwrap_or(self.color.darker); + //Fill::XY(Tui::bg(bg.rgb, self.editor())) + //} + //pub fn view_editor_status (&self) -> impl Content + use<'_> { + //self.editor().map(|e|Fixed::X(20, Outer(true, Style::default().fg(Tui::g(96))).enclose( + //Fill::Y(Align::n(Bsp::s(e.clip_status(), e.edit_status())))))) + //} + //pub fn view_midi_ins_status (&self) -> impl Content + use<'_> { + //self.project.view_midi_ins_status(self.color) + //} + //pub fn view_midi_outs_status (&self) -> impl Content + use<'_> { + //self.project.view_midi_outs_status(self.color) + //} + //pub fn view_audio_ins_status (&self) -> impl Content + use<'_> { + //self.project.view_audio_ins_status(self.color) + //} + //pub fn view_audio_outs_status (&self) -> impl Content + use<'_> { + //self.project.view_audio_outs_status(self.color) + //} + //pub fn view_scenes (&self) -> impl Content + use<'_> { + //Bsp::e( + //Fixed::X(20, Align::nw(self.project.view_scenes_names())), + //self.project.view_scenes_clips(), + //) + //} + //pub fn view_scenes_names (&self) -> impl Content + use<'_> { + //self.project.view_scenes_names() + //} + //pub fn view_scenes_clips (&self) -> impl Content + use<'_> { + //self.project.view_scenes_clips() + //} + //pub fn view_tracks_inputs <'a> (&'a self) -> impl Content + use<'a> { + //Fixed::Y(1 + self.project.midi_ins.len() as u16, + //self.project.view_inputs(self.color)) + //} + //pub fn view_tracks_outputs <'a> (&'a self) -> impl Content + use<'a> { + //self.project.view_outputs(self.color) + //} + //pub fn view_tracks_devices <'a> (&'a self) -> impl Content + use<'a> { + //Fixed::Y(4, self.project.view_track_devices(self.color)) + //} + //pub fn view_tracks_names <'a> (&'a self) -> impl Content + use<'a> { + //Fixed::Y(2, self.project.view_track_names(self.color)) + //} + //pub fn view_pool (&self) -> impl Content + use<'_> { + //Fixed::X(20, Bsp::s( + //Fill::X(Align::w(FieldH(self.color, "Clip pool:", ""))), + //Fill::Y(Align::n(Tui::bg(Rgb(0, 0, 0), Outer(true, Style::default().fg(Tui::g(96))) + //.enclose(PoolView(&self.pool))))))) + //} + //pub fn view_samples_keys (&self) -> impl Content + use<'_> { + //self.project.sampler().map(|s|s.view_list(true, self.editor().unwrap())) + //} + //pub fn view_samples_grid (&self) -> impl Content + use<'_> { + //self.project.sampler().map(|s|s.view_grid()) + //} + //pub fn view_sample_viewer (&self) -> impl Content + use<'_> { + //self.project.sampler().map(|s|s.view_sample(self.editor().unwrap().get_note_pos())) + //} + //pub fn view_sample_info (&self) -> impl Content + use<'_> { + //self.project.sampler().map(|s|s.view_sample_info(self.editor().unwrap().get_note_pos())) + //} + //pub fn view_sample_status (&self) -> impl Content + use<'_> { + //self.project.sampler().map(|s|Outer(true, Style::default().fg(Tui::g(96))).enclose( + //Fill::Y(Align::n(s.view_sample_status(self.editor().unwrap().get_note_pos()))))) + //} + ////let options = ||["Projects", "Settings", "Help", "Quit"].iter(); + ////let option = |a,i|Tui::fg(Rgb(255,255,255), format!("{}", a)); + ////Bsp::s(Tui::bold(true, "tek!"), Bsp::s("", Map::south(1, options, option))) diff --git a/crates/tek/architecture.svg b/architecture.svg similarity index 100% rename from crates/tek/architecture.svg rename to architecture.svg diff --git a/bacon.toml b/bacon.toml new file mode 100644 index 00000000..4af91cd0 --- /dev/null +++ b/bacon.toml @@ -0,0 +1,64 @@ +# https://dystroy.org/bacon/config/ +default_job = "test" +env.CARGO_TERM_COLOR = "always" +[keybindings] +c = "job:check" +t = "job:test" +n = "job:nextest" +l = "job:clippy" +[jobs] +[jobs.check] +command = ["cargo", "check"] +need_stdout = false +watch = ["deps", "engine", "device", "app"] +[jobs.check-all] +command = ["cargo", "check", "--all-targets"] +need_stdout = false +watch = ["deps", "engine", "device", "app"] +[jobs.clippy] +command = ["cargo", "clippy"] +need_stdout = false +watch = ["deps", "engine", "device", "app"] +[jobs.clippy-all] +command = ["cargo", "clippy", "--all-targets"] +need_stdout = false +watch = ["deps", "engine", "device", "app"] +[jobs.test] +command = ["cargo", "test", "--workspace", "--exclude", "jack"] +need_stdout = true +watch = ["deps", "engine", "device", "app"] +[jobs.nextest] +watch = ["deps", "engine", "device", "app"] +command = [ + "cargo", "nextest", "run", + "--hide-progress-bar", "--failure-output", "final" +] +need_stdout = true +analyzer = "nextest" +[jobs.doc] +command = ["cargo", "doc", "--no-deps"] +need_stdout = false +[jobs.doc-open] +command = ["cargo", "doc", "--no-deps", "--open"] +need_stdout = false +on_success = "back" # so that we don't open the browser at each change +[jobs.run] +command = [ + "cargo", "run", + # put launch parameters for your program behind a `--` separator +] +need_stdout = true +allow_warnings = true +background = true +[jobs.run-long] +watch = ["deps", "engine", "device", "app"] +command = [ "cargo", "run", ] +need_stdout = true +allow_warnings = true +background = false +on_change_strategy = "kill_then_restart" +[jobs.ex] +watch = ["deps", "engine", "device", "app"] +command = ["cargo", "run", "--example"] +need_stdout = true +allow_warnings = true diff --git a/build/Dockerfile.glibc b/build/Dockerfile.glibc new file mode 100644 index 00000000..ec33045e --- /dev/null +++ b/build/Dockerfile.glibc @@ -0,0 +1,14 @@ +FROM docker.io/library/debian:bookworm +RUN apt update \ + && apt install -y build-essential bash tree git wget \ + pkg-config libjack-dev liblilv-dev libserd-dev libsord-dev +RUN adduser --quiet --uid 1000 --disabled-password build +RUN wget https://static.rust-lang.org/rustup/dist/x86_64-unknown-linux-gnu/rustup-init \ + && chmod +x ./rustup-init \ + && mv rustup-init /usr/bin/rustup-init +USER build +WORKDIR /home/build +RUN rustup-init -yv --profile minimal --default-toolchain nightly \ + && rm -rvf "$HOME/.rustup/roolchains/*/share" +RUN ls -alh "$HOME" && bash -c '. "$HOME/.cargo/env" \ + && cargo version -vv' diff --git a/build/Dockerfile.musl b/build/Dockerfile.musl new file mode 100644 index 00000000..ed350cdc --- /dev/null +++ b/build/Dockerfile.musl @@ -0,0 +1,13 @@ +FROM docker.io/library/alpine:edge + +RUN apk add --no-cache build-base bash tree rustup git just cloc clang20-dev pipewire-jack-dev + +RUN adduser -Du1000 build + +USER 1000 + +RUN rustup-init -y --profile minimal --default-toolchain nightly \ + && rm -rvf "$HOME/.rustup/roolchains/*/share" + +RUN source "$HOME/.cargo/env" \ + && cargo version -vv diff --git a/build/README.md b/build/README.md new file mode 100644 index 00000000..2f70feaa --- /dev/null +++ b/build/README.md @@ -0,0 +1,11 @@ +This directory contains Dockerfiles and shell scripts +for building Tek in a container. For now, only the +GLIBC build works, as the Musl static build is unable +to `dlopen` the system's `libjack.so`. + +Invoke from repo root, like this: `build/release-glibc.sh`. +This will first build a Docker image, `tek:glibc`, which +will contain all build-time dependencies; then, it +will invoke a `cargo build --release` in a container +spawned from that image, ultimately placing the +release build in this directory, as `build/tek`. diff --git a/build/release-glibc-shell.sh b/build/release-glibc-shell.sh new file mode 100755 index 00000000..3a22285a --- /dev/null +++ b/build/release-glibc-shell.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env sh +set -exo pipefail +docker inspect tek:glibc || time docker build --cache-from=internal \ + -f build/Dockerfile.glibc -t tek:glibc . +time docker run \ + --rm -itu0 \ + -v .:/build -w /build \ + -vtek-build-cargo:/home/build/.cargo \ + -vtek-build-target:/build/target \ + -eRUST_JACK_DLOPEN=true \ + tek:glibc $@ diff --git a/build/release-glibc.sh b/build/release-glibc.sh new file mode 100755 index 00000000..b7c01fc1 --- /dev/null +++ b/build/release-glibc.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env sh +set -exo pipefail +docker inspect tek:glibc || time docker build --cache-from=internal \ + -f build/Dockerfile.glibc -t tek:glibc . +time docker run \ + --rm -itu0 \ + -v .:/build -w /build \ + -vtek-build-cargo:/home/build/.cargo \ + -vtek-build-target:/build/target \ + -eRUST_JACK_DLOPEN=true \ + tek:glibc sh -c "chown -R 1000:1000 /build/target \ + && su build -c '. ~/.cargo/env \ + && time cargo build -j4 --release \ + && cp target/release/tek build/'" diff --git a/build/release-musl-shell.sh b/build/release-musl-shell.sh new file mode 100755 index 00000000..8e5c047c --- /dev/null +++ b/build/release-musl-shell.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env sh +set -exo pipefail +docker inspect tek:musl || time docker build --cache-from=internal \ + -f build/Dockerfile.musl -t tek:musl . +time docker run \ + --rm -itu0 \ + -v .:/build -w /build \ + -vtek-build-cargo:/home/build/.cargo \ + -vtek-build-target:/build/target \ + -eRUST_JACK_DLOPEN=true \ + tek:musl $@ diff --git a/build/release-musl.sh b/build/release-musl.sh new file mode 100755 index 00000000..b8526fb5 --- /dev/null +++ b/build/release-musl.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env sh +set -exo pipefail +docker inspect tek:musl || time docker build --cache-from=internal \ + -f build/Dockerfile.musl -t tek:musl . +time docker run \ + --rm -itu0 \ + -v .:/build -w /build \ + -vtek-build-cargo:/home/build/.cargo \ + -vtek-build-target:/build/target \ + -eRUST_JACK_DLOPEN=true \ + tek:musl sh -c "chown -R 1000:1000 /build/target \ + && su build -c 'source ~/.cargo/env \ + && just build-release \ + && cp target/release/tek build/'" diff --git a/crates/tek/Cargo.toml b/crates/tek/Cargo.toml deleted file mode 100644 index 2a39075e..00000000 --- a/crates/tek/Cargo.toml +++ /dev/null @@ -1,60 +0,0 @@ -[package] -name = "tek" -edition = "2021" -version = "0.2.0" - -[dependencies] -#no_deadlocks = "1.3.2" -#vst3 = "0.1.0" -atomic_float = "1.0.0" -backtrace = "0.3.72" -better-panic = "0.3.0" -clap = { version = "4.5.4", features = [ "derive" ] } -clojure-reader = "0.1.0" -crossterm = "0.27" -jack = "0.13" -livi = "0.7.4" -midly = "0.5" -once_cell = "1.19.0" -palette = { version = "0.7.6", features = [ "random" ] } -quanta = "0.12.3" -rand = "0.8.5" -ratatui = { version = "0.26.3", features = [ "unstable-widget-ref", "underline-color" ] } -#suil-rs = { path = "../suil" } -symphonia = { version = "0.5.4", features = [ "all" ] } -toml = "0.8.12" -uuid = { version = "1.10.0", features = [ "v4" ] } -#vst = "0.4.0" -wavers = "1.4.3" -#winit = { version = "0.30.4", features = [ "x11" ] } - -[dev-dependencies] -#tek_app = { version = "0.1.0", path = "../tek_app" } - -[[bin]] -name = "tek_arranger" -path = "src/cli/cli_arranger.rs" - -[[bin]] -name = "tek_sequencer" -path = "src/cli/cli_sequencer.rs" - -[[bin]] -name = "tek_transport" -path = "src/cli/cli_transport.rs" - -#[[bin]] -#name = "tek_mixer" -#path = "src/cli_mixer.rs" - -#[[bin]] -#name = "tek_track" -#path = "src/cli_track.rs" - -#[[bin]] -#name = "tek_sampler" -#path = "src/cli_sampler.rs" - -#[[bin]] -#name = "tek_plugin" -#path = "src/cli_plugin.rs" diff --git a/crates/tek/README.md b/crates/tek/README.md deleted file mode 100644 index 0d8195be..00000000 --- a/crates/tek/README.md +++ /dev/null @@ -1,49 +0,0 @@ - - -# `tek_sequencer` - -This crate implements a MIDI sequencer and arranger with clip launching. - ---- - -# `tek_arranger` - ---- - -# `tek_timer` - -This crate implements time sync and JACK transport control. - -* Warning: If transport is set rolling by qjackctl, this program can't pause it -* Todo: bpm: shift +/- 0.001 -* Todo: quant/sync: shift = next/prev value of same type (normal, triplet, dotted) - * Or: use shift to switch between inc/dec top/bottom value? -* Todo: focus play button -* Todo: focus time position -* Todo: edit numeric values -* Todo: jump to time/bbt markers -* Todo: count xruns - ---- - -# `tek_mixer` - -// TODO: -// - Meters: propagate clipping: -// - If one stage clips, all stages after it are marked red -// - If one track clips, all tracks that feed from it are marked red? - -# `tek_track` - ---- - -# `tek_sampler` - -This crate implements a sampler device which plays audio files -in response to MIDI notes. - ---- - -# `tek_plugin` - - diff --git a/crates/tek/examples/midi_import.rs b/crates/tek/examples/midi_import.rs deleted file mode 100644 index b67d9e03..00000000 --- a/crates/tek/examples/midi_import.rs +++ /dev/null @@ -1,18 +0,0 @@ -use tek_api::*; - -struct ExamplePhrases(Vec>>); - -impl HasPhrases for ExamplePhrases { - fn phrases (&self) -> &Vec>> { - &self.0 - } - fn phrases_mut (&mut self) -> &mut Vec>> { - &mut self.0 - } -} - -fn main () -> Usually<()> { - let mut phrases = ExamplePhrases(vec![]); - PhrasePoolCommand::Import(0, String::from("./example.mid")).execute(&mut phrases)?; - Ok(()) -} diff --git a/crates/tek/src/api.rs b/crates/tek/src/api.rs deleted file mode 100644 index a3c2e9ca..00000000 --- a/crates/tek/src/api.rs +++ /dev/null @@ -1,10 +0,0 @@ -use crate::*; - -mod phrase; pub(crate) use phrase::*; -mod jack; pub(crate) use self::jack::*; -mod clip; pub(crate) use clip::*; -mod color; pub(crate) use color::*; -mod clock; pub(crate) use clock::*; -mod player; pub(crate) use player::*; -mod scene; pub(crate) use scene::*; -mod track; pub(crate) use track::*; diff --git a/crates/tek/src/api/_todo_api_channel.rs b/crates/tek/src/api/_todo_api_channel.rs deleted file mode 100644 index 513a6934..00000000 --- a/crates/tek/src/api/_todo_api_channel.rs +++ /dev/null @@ -1,83 +0,0 @@ -use crate::*; - -pub enum MixerTrackCommand {} - -/// A mixer track. -#[derive(Debug)] -pub struct MixerTrack { - pub name: String, - /// Inputs of 1st device - pub audio_ins: Vec>, - /// Outputs of last device - pub audio_outs: Vec>, - /// Device chain - pub devices: Vec>, -} - -//impl MixerTrackDevice for LV2Plugin {} - -impl MixerTrack { - const SYM_NAME: &'static str = ":name"; - const SYM_GAIN: &'static str = ":gain"; - const SYM_SAMPLER: &'static str = "sampler"; - const SYM_LV2: &'static str = "lv2"; - pub fn from_edn <'a, 'e> (jack: &Arc>, args: &[Edn<'e>]) -> Usually { - let mut _gain = 0.0f64; - let mut track = MixerTrack { - name: String::new(), - audio_ins: vec![], - audio_outs: vec![], - devices: vec![], - }; - edn!(edn in args { - Edn::Map(map) => { - if let Some(Edn::Str(n)) = map.get(&Edn::Key(Self::SYM_NAME)) { - track.name = n.to_string(); - } - if let Some(Edn::Double(g)) = map.get(&Edn::Key(Self::SYM_GAIN)) { - _gain = f64::from(*g); - } - }, - Edn::List(args) => match args.get(0) { - // Add a sampler device to the track - Some(Edn::Symbol(Self::SYM_SAMPLER)) => { - track.devices.push( - Box::new(Sampler::from_edn(jack, &args[1..])?) as Box - ); - panic!( - "unsupported in track {}: {:?}; tek_mixer not compiled with feature \"sampler\"", - &track.name, - args.get(0).unwrap() - ) - }, - // Add a LV2 plugin to the track. - Some(Edn::Symbol(Self::SYM_LV2)) => { - track.devices.push( - Box::new(LV2Plugin::from_edn(jack, &args[1..])?) as Box - ); - panic!( - "unsupported in track {}: {:?}; tek_mixer not compiled with feature \"plugin\"", - &track.name, - args.get(0).unwrap() - ) - }, - None => - panic!("empty list track {}", &track.name), - _ => - panic!("unexpected in track {}: {:?}", &track.name, args.get(0).unwrap()) - }, - _ => {} - }); - Ok(track) - } -} - -pub trait MixerTrackDevice: Debug + Send + Sync { - fn boxed (self) -> Box where Self: Sized + 'static { - Box::new(self) - } -} - -impl MixerTrackDevice for Sampler {} - -impl MixerTrackDevice for Plugin {} diff --git a/crates/tek/src/api/_todo_api_mixer.rs b/crates/tek/src/api/_todo_api_mixer.rs deleted file mode 100644 index cd8df774..00000000 --- a/crates/tek/src/api/_todo_api_mixer.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::*; - -#[derive(Debug)] -pub struct Mixer { - /// JACK client handle (needs to not be dropped for standalone mode to work). - pub jack: Arc>, - pub name: String, - pub tracks: Vec, - pub selected_track: usize, - pub selected_column: usize, -} - -pub struct MixerAudio { - model: Arc> -} - -impl From<&Arc>> for MixerAudio { - fn from (model: &Arc>) -> Self { - Self { model: model.clone() } - } -} - -impl Audio for MixerAudio { - fn process (&mut self, _: &Client, _: &ProcessScope) -> Control { - Control::Continue - } -} diff --git a/crates/tek/src/api/_todo_api_plugin.rs b/crates/tek/src/api/_todo_api_plugin.rs deleted file mode 100644 index 3edbac42..00000000 --- a/crates/tek/src/api/_todo_api_plugin.rs +++ /dev/null @@ -1,114 +0,0 @@ -use crate::*; - -/// A plugin device. -#[derive(Debug)] -pub struct Plugin { - /// JACK client handle (needs to not be dropped for standalone mode to work). - pub jack: Arc>, - pub name: String, - pub path: Option, - pub plugin: Option, - pub selected: usize, - pub mapping: bool, - pub midi_ins: Vec>, - pub midi_outs: Vec>, - pub audio_ins: Vec>, - pub audio_outs: Vec>, -} -impl Plugin { - pub fn new_lv2 ( - jack: &Arc>, - name: &str, - path: &str, - ) -> Usually { - Ok(Self { - jack: jack.clone(), - name: name.into(), - path: Some(String::from(path)), - plugin: Some(PluginKind::LV2(LV2Plugin::new(path)?)), - selected: 0, - mapping: false, - midi_ins: vec![], - midi_outs: vec![], - audio_ins: vec![], - audio_outs: vec![], - }) - } - - //fn jack_from_lv2 (name: &str, plugin: &::livi::Plugin) -> Usually { - //let counts = plugin.port_counts(); - //let mut jack = Jack::new(name)?; - //for i in 0..counts.atom_sequence_inputs { - //jack = jack.midi_in(&format!("midi-in-{i}")) - //} - //for i in 0..counts.atom_sequence_outputs { - //jack = jack.midi_out(&format!("midi-out-{i}")); - //} - //for i in 0..counts.audio_inputs { - //jack = jack.audio_in(&format!("audio-in-{i}")); - //} - //for i in 0..counts.audio_outputs { - //jack = jack.audio_out(&format!("audio-out-{i}")); - //} - //Ok(jack) - //} -} - -pub struct PluginAudio(Arc>); - -impl From<&Arc>> for PluginAudio { - fn from (model: &Arc>) -> Self { - Self(model.clone()) - } -} - -impl Audio for PluginAudio { - fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { - let state = &mut*self.0.write().unwrap(); - match state.plugin.as_mut() { - Some(PluginKind::LV2(LV2Plugin { - features, - ref mut instance, - ref mut input_buffer, - .. - })) => { - let urid = features.midi_urid(); - input_buffer.clear(); - for port in state.midi_ins.iter() { - let mut atom = ::livi::event::LV2AtomSequence::new( - &features, - scope.n_frames() as usize - ); - for event in port.iter(scope) { - match event.bytes.len() { - 3 => atom.push_midi_event::<3>( - event.time as i64, - urid, - &event.bytes[0..3] - ).unwrap(), - _ => {} - } - } - input_buffer.push(atom); - } - let mut outputs = vec![]; - for _ in state.midi_outs.iter() { - outputs.push(::livi::event::LV2AtomSequence::new( - &features, - scope.n_frames() as usize - )); - } - let ports = ::livi::EmptyPortConnections::new() - .with_atom_sequence_inputs(input_buffer.iter()) - .with_atom_sequence_outputs(outputs.iter_mut()) - .with_audio_inputs(state.audio_ins.iter().map(|o|o.as_slice(scope))) - .with_audio_outputs(state.audio_outs.iter_mut().map(|o|o.as_mut_slice(scope))); - unsafe { - instance.run(scope.n_frames() as usize, ports).unwrap() - }; - }, - _ => {} - } - Control::Continue - } -} diff --git a/crates/tek/src/api/_todo_api_plugin_kind.rs b/crates/tek/src/api/_todo_api_plugin_kind.rs deleted file mode 100644 index 0f35ca3a..00000000 --- a/crates/tek/src/api/_todo_api_plugin_kind.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::*; - -/// Supported plugin formats. -#[derive(Default)] -pub enum PluginKind { - #[default] None, - LV2(LV2Plugin), - VST2 { instance: ::vst::host::PluginInstance }, - VST3, -} - -impl Debug for PluginKind { - fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), Error> { - write!(f, "{}", match self { - Self::None => "(none)", - Self::LV2(_) => "LV2", - Self::VST2{..} => "VST2", - Self::VST3 => "VST3", - }) - } -} diff --git a/crates/tek/src/api/_todo_api_plugin_lv2.rs b/crates/tek/src/api/_todo_api_plugin_lv2.rs deleted file mode 100644 index b47c159b..00000000 --- a/crates/tek/src/api/_todo_api_plugin_lv2.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::*; - -/// A LV2 plugin. -#[derive(Debug)] -pub struct LV2Plugin { - pub world: livi::World, - pub instance: livi::Instance, - pub plugin: livi::Plugin, - pub features: Arc, - pub port_list: Vec, - pub input_buffer: Vec, - pub ui_thread: Option>, -} - -impl LV2Plugin { - const INPUT_BUFFER: usize = 1024; - pub fn new (uri: &str) -> Usually { - let world = livi::World::with_load_bundle(&uri); - let features = world - .build_features(livi::FeaturesBuilder { - min_block_length: 1, - max_block_length: 65536, - }); - let plugin = world - .iter_plugins() - .nth(0) - .expect(&format!("plugin not found: {uri}")); - Ok(Self { - instance: unsafe { - plugin - .instantiate(features.clone(), 48000.0) - .expect(&format!("instantiate failed: {uri}")) - }, - port_list: plugin.ports().collect::>(), - input_buffer: Vec::with_capacity(Self::INPUT_BUFFER), - ui_thread: None, - world, - features, - plugin, - }) - } -} - -impl LV2Plugin { - pub fn from_edn <'e> (jack: &Arc>, args: &[Edn<'e>]) -> Usually { - let mut name = String::new(); - let mut path = String::new(); - edn!(edn in args { - Edn::Map(map) => { - if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { - name = String::from(*n); - } - if let Some(Edn::Str(p)) = map.get(&Edn::Key(":path")) { - path = String::from(*p); - } - }, - _ => panic!("unexpected in lv2 '{name}'"), - }); - Plugin::new_lv2(jack, &name, &path) - } -} diff --git a/crates/tek/src/api/_todo_api_sampler.rs b/crates/tek/src/api/_todo_api_sampler.rs deleted file mode 100644 index 2976c08a..00000000 --- a/crates/tek/src/api/_todo_api_sampler.rs +++ /dev/null @@ -1,135 +0,0 @@ -use crate::*; - -/// The sampler plugin plays sounds. -#[derive(Debug)] -pub struct Sampler { - pub jack: Arc>, - pub name: String, - pub mapped: BTreeMap>>, - pub unmapped: Vec>>, - pub voices: Arc>>, - pub midi_in: Port, - pub audio_outs: Vec>, - pub buffer: Vec>, - pub output_gain: f32 -} - -impl Sampler { - pub fn from_edn <'e> (jack: &Arc>, args: &[Edn<'e>]) -> Usually { - let mut name = String::new(); - let mut dir = String::new(); - let mut samples = BTreeMap::new(); - edn!(edn in args { - Edn::Map(map) => { - if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { - name = String::from(*n); - } - if let Some(Edn::Str(n)) = map.get(&Edn::Key(":dir")) { - dir = String::from(*n); - } - }, - Edn::List(args) => match args.get(0) { - Some(Edn::Symbol("sample")) => { - let (midi, sample) = Sample::from_edn(jack, &dir, &args[1..])?; - if let Some(midi) = midi { - samples.insert(midi, sample); - } else { - panic!("sample without midi binding: {}", sample.read().unwrap().name); - } - }, - _ => panic!("unexpected in sampler {name}: {args:?}") - }, - _ => panic!("unexpected in sampler {name}: {edn:?}") - }); - let midi_in = jack.read().unwrap().client().register_port("in", MidiIn::default())?; - Ok(Sampler { - jack: jack.clone(), - name: name.into(), - mapped: samples, - unmapped: Default::default(), - voices: Default::default(), - buffer: Default::default(), - midi_in: midi_in, - audio_outs: vec![], - output_gain: 0. - }) - } -} - -pub struct SamplerAudio { - model: Arc> -} - -impl From<&Arc>> for SamplerAudio { - fn from (model: &Arc>) -> Self { - Self { model: model.clone() } - } -} - -impl Audio for SamplerAudio { - #[inline] fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { - self.process_midi_in(scope); - self.clear_output_buffer(); - self.process_audio_out(scope); - self.write_output_buffer(scope); - Control::Continue - } -} - -impl SamplerAudio { - - /// Create [Voice]s from [Sample]s in response to MIDI input. - pub fn process_midi_in (&mut self, scope: &ProcessScope) { - let Sampler { midi_in, mapped, voices, .. } = &*self.model.read().unwrap(); - for RawMidi { time, bytes } in midi_in.iter(scope) { - if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { - if let MidiMessage::NoteOn { ref key, ref vel } = message { - if let Some(sample) = mapped.get(key) { - voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); - } - } - } - } - } - - /// Zero the output buffer. - pub fn clear_output_buffer (&mut self) { - for buffer in self.model.write().unwrap().buffer.iter_mut() { - buffer.fill(0.0); - } - } - - /// Mix all currently playing samples into the output. - pub fn process_audio_out (&mut self, scope: &ProcessScope) { - let Sampler { ref mut buffer, voices, output_gain, .. } = &mut*self.model.write().unwrap(); - let channel_count = buffer.len(); - voices.write().unwrap().retain_mut(|voice|{ - for index in 0..scope.n_frames() as usize { - if let Some(frame) = voice.next() { - for (channel, sample) in frame.iter().enumerate() { - // Averaging mixer: - //self.buffer[channel % channel_count][index] = ( - //(self.buffer[channel % channel_count][index] + sample * self.output_gain) / 2.0 - //); - buffer[channel % channel_count][index] += sample * *output_gain; - } - } else { - return false - } - } - return true - }); - } - - /// Write output buffer to output ports. - pub fn write_output_buffer (&mut self, scope: &ProcessScope) { - let Sampler { ref mut audio_outs, buffer, .. } = &mut*self.model.write().unwrap(); - for (i, port) in audio_outs.iter_mut().enumerate() { - let buffer = &buffer[i]; - for (i, value) in port.as_mut_slice(scope).iter_mut().enumerate() { - *value = *buffer.get(i).unwrap_or(&0.0); - } - } - } - -} diff --git a/crates/tek/src/api/_todo_api_sampler_sample.rs b/crates/tek/src/api/_todo_api_sampler_sample.rs deleted file mode 100644 index aa85676e..00000000 --- a/crates/tek/src/api/_todo_api_sampler_sample.rs +++ /dev/null @@ -1,72 +0,0 @@ -use crate::*; - -/// A sound sample. -#[derive(Default, Debug)] -pub struct Sample { - pub name: String, - pub start: usize, - pub end: usize, - pub channels: Vec>, - pub rate: Option, -} - -impl Sample { - pub fn new (name: &str, start: usize, end: usize, channels: Vec>) -> Self { - Self { name: name.to_string(), start, end, channels, rate: None } - } - pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { - Voice { - sample: sample.clone(), - after, - position: sample.read().unwrap().start, - velocity: velocity.as_int() as f32 / 127.0, - } - } - pub fn from_edn <'e> (jack: &Arc>, dir: &str, args: &[Edn<'e>]) -> Usually<(Option, Arc>)> { - let mut name = String::new(); - let mut file = String::new(); - let mut midi = None; - let mut start = 0usize; - edn!(edn in args { - Edn::Map(map) => { - if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { - name = String::from(*n); - } - if let Some(Edn::Str(f)) = map.get(&Edn::Key(":file")) { - file = String::from(*f); - } - if let Some(Edn::Int(i)) = map.get(&Edn::Key(":start")) { - start = *i as usize; - } - if let Some(Edn::Int(m)) = map.get(&Edn::Key(":midi")) { - midi = Some(u7::from(*m as u8)); - } - }, - _ => panic!("unexpected in sample {name}"), - }); - let (end, data) = Sample::read_data(&format!("{dir}/{file}"))?; - Ok((midi, Arc::new(RwLock::new(Self { - name: name.into(), - start, - end, - channels: data, - rate: None - })))) - } - - /// Read WAV from file - pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { - let mut channels: Vec> = vec![]; - for channel in wavers::Wav::from_path(src)?.channels() { - channels.push(channel); - } - let mut end = 0; - let mut data: Vec> = vec![]; - for samples in channels.iter() { - let channel = Vec::from(samples.as_ref()); - end = end.max(channel.len()); - data.push(channel); - } - Ok((end, data)) - } -} diff --git a/crates/tek/src/api/_todo_api_sampler_voice.rs b/crates/tek/src/api/_todo_api_sampler_voice.rs deleted file mode 100644 index 1dd3ba4a..00000000 --- a/crates/tek/src/api/_todo_api_sampler_voice.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::*; - -/// A currently playing instance of a sample. -#[derive(Default, Debug, Clone)] -pub struct Voice { - pub sample: Arc>, - pub after: usize, - pub position: usize, - pub velocity: f32, -} - -impl Iterator for Voice { - type Item = [f32;2]; - fn next (&mut self) -> Option { - if self.after > 0 { - self.after = self.after - 1; - return Some([0.0, 0.0]) - } - let sample = self.sample.read().unwrap(); - if self.position < sample.end { - let position = self.position; - self.position = self.position + 1; - return sample.channels[0].get(position).map(|_amplitude|[ - sample.channels[0][position] * self.velocity, - sample.channels[0][position] * self.velocity, - ]) - } - None - } -} diff --git a/crates/tek/src/api/clip.rs b/crates/tek/src/api/clip.rs deleted file mode 100644 index fe8781ce..00000000 --- a/crates/tek/src/api/clip.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::*; - -#[derive(Clone, Debug)] -pub enum ArrangerClipCommand { - Play, - Get(usize, usize), - Set(usize, usize, Option>>), - Edit(Option>>), - SetLoop(bool), - RandomColor, -} - -//impl Command for ArrangerClipCommand { - //fn execute (self, state: &mut T) -> Perhaps { - //match self { - //_ => todo!() - //} - //Ok(None) - //} -//} diff --git a/crates/tek/src/api/clock.rs b/crates/tek/src/api/clock.rs deleted file mode 100644 index 8e6e8574..00000000 --- a/crates/tek/src/api/clock.rs +++ /dev/null @@ -1,198 +0,0 @@ -use crate::*; - -pub trait HasClock: Send + Sync { - fn clock (&self) -> &ClockModel; -} - -#[derive(Clone, Debug, PartialEq)] -pub enum ClockCommand { - Play(Option), - Pause(Option), - SeekUsec(f64), - SeekSample(f64), - SeekPulse(f64), - SetBpm(f64), - SetQuant(f64), - SetSync(f64), -} - -impl Command for ClockCommand { - fn execute (self, state: &mut T) -> Perhaps { - use ClockCommand::*; - match self { - Play(start) => state.clock().play_from(start)?, - Pause(pause) => state.clock().pause_at(pause)?, - SeekUsec(usec) => state.clock().playhead.update_from_usec(usec), - SeekSample(sample) => state.clock().playhead.update_from_sample(sample), - SeekPulse(pulse) => state.clock().playhead.update_from_pulse(pulse), - SetBpm(bpm) => return Ok(Some(SetBpm(state.clock().timebase().bpm.set(bpm)))), - SetQuant(quant) => return Ok(Some(SetQuant(state.clock().quant.set(quant)))), - SetSync(sync) => return Ok(Some(SetSync(state.clock().sync.set(sync)))), - }; - Ok(None) - } -} - -#[derive(Clone)] -pub struct Timeline { - pub timebase: Arc, - pub started: Arc>>, - pub loopback: Arc>>, -} - -impl Default for Timeline { - fn default () -> Self { - Self { - timebase: Arc::new(Timebase::default()), - started: RwLock::new(None).into(), - loopback: RwLock::new(None).into(), - } - } -} - -#[derive(Clone)] -pub struct ClockModel { - /// JACK transport handle. - pub transport: Arc, - /// Global temporal resolution (shared by [Moment] fields) - pub timebase: Arc, - /// Current global sample and usec (monotonic from JACK clock) - pub global: Arc, - /// Global sample and usec at which playback started - pub started: Arc>>, - /// Current playhead position - pub playhead: Arc, - /// Note quantization factor - pub quant: Arc, - /// Launch quantization factor - pub sync: Arc, - /// Size of buffer in samples - pub chunk: Arc, -} - -impl From<&Arc>> for ClockModel { - fn from (jack: &Arc>) -> Self { - let jack = jack.read().unwrap(); - let chunk = jack.client().buffer_size(); - let transport = jack.client().transport(); - let timebase = Arc::new(Timebase::default()); - Self { - quant: Arc::new(24.into()), - sync: Arc::new(384.into()), - transport: Arc::new(transport), - chunk: Arc::new((chunk as usize).into()), - global: Arc::new(Moment::zero(&timebase)), - playhead: Arc::new(Moment::zero(&timebase)), - started: RwLock::new(None).into(), - timebase, - } - } -} - -impl std::fmt::Debug for ClockModel { - fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("ClockModel") - .field("timebase", &self.timebase) - .field("chunk", &self.chunk) - .field("quant", &self.quant) - .field("sync", &self.sync) - .field("global", &self.global) - .field("playhead", &self.playhead) - .field("started", &self.started) - .finish() - } -} - -impl ClockModel { - pub fn timebase (&self) -> &Arc { - &self.timebase - } - /// Current sample rate - pub fn sr (&self) -> &SampleRate { - &self.timebase.sr - } - /// Current tempo - pub fn bpm (&self) -> &BeatsPerMinute { - &self.timebase.bpm - } - /// Current MIDI resolution - pub fn ppq (&self) -> &PulsesPerQuaver { - &self.timebase.ppq - } - /// Next pulse that matches launch sync (for phrase switchover) - pub fn next_launch_pulse (&self) -> usize { - let sync = self.sync.get() as usize; - let pulse = self.playhead.pulse.get() as usize; - if pulse % sync == 0 { - pulse - } else { - (pulse / sync + 1) * sync - } - } - /// Start playing, optionally seeking to a given location beforehand - pub fn play_from (&self, start: Option) -> Usually<()> { - if let Some(start) = start { - self.transport.locate(start)?; - } - self.transport.start()?; - Ok(()) - } - /// Pause, optionally seeking to a given location afterwards - pub fn pause_at (&self, pause: Option) -> Usually<()> { - self.transport.stop()?; - if let Some(pause) = pause { - self.transport.locate(pause)?; - } - Ok(()) - } - /// Is currently paused? - pub fn is_stopped (&self) -> bool { - self.started.read().unwrap().is_none() - } - /// Is currently playing? - pub fn is_rolling (&self) -> bool { - self.started.read().unwrap().is_some() - } - /// Update chunk size - pub fn set_chunk (&self, n_frames: usize) { - self.chunk.store(n_frames, Ordering::Relaxed); - } - pub fn update_from_scope (&self, scope: &ProcessScope) -> Usually<()> { - self.set_chunk(scope.n_frames() as usize); - let CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?; - self.global.sample.set(current_frames as f64); - self.global.usec.set(current_usecs as f64); - let mut started = self.started.write().unwrap(); - match self.transport.query_state()? { - TransportState::Rolling => { - if started.is_none() { - let moment = Moment::zero(&self.timebase); - moment.sample.set(current_frames as f64); - moment.usec.set(current_usecs as f64); - *started = Some(moment); - } - }, - TransportState::Stopped => { - if started.is_some() { - *started = None; - } - }, - _ => {} - }; - self.playhead.update_from_sample(match *started { - Some(ref instant) => current_frames as f64 - instant.sample.get(), - None => 0. - }); - Ok(()) - } -} - -/// Hosts the JACK callback for updating the temporal pointer and playback status. -pub struct ClockAudio<'a, T: HasClock>(pub &'a mut T); - -impl<'a, T: HasClock> Audio for ClockAudio<'a, T> { - #[inline] fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { - self.0.clock().update_from_scope(scope).unwrap(); - Control::Continue - } -} diff --git a/crates/tek/src/api/color.rs b/crates/tek/src/api/color.rs deleted file mode 100644 index c23e6ba7..00000000 --- a/crates/tek/src/api/color.rs +++ /dev/null @@ -1,6 +0,0 @@ -use crate::*; - -pub trait HasColor { - fn color (&self) -> ItemColor; - fn color_mut (&self) -> &mut ItemColor; -} diff --git a/crates/tek/src/api/jack.rs b/crates/tek/src/api/jack.rs deleted file mode 100644 index 97ba47d8..00000000 --- a/crates/tek/src/api/jack.rs +++ /dev/null @@ -1,463 +0,0 @@ -use crate::*; - -pub trait JackApi { - fn jack (&self) -> &Arc>; -} - -pub trait HasMidiIns { - fn midi_ins (&self) -> &Vec>; - fn midi_ins_mut (&mut self) -> &mut Vec>; - fn has_midi_ins (&self) -> bool { - self.midi_ins().len() > 0 - } -} - -pub trait HasMidiOuts { - fn midi_outs (&self) -> &Vec>; - fn midi_outs_mut (&mut self) -> &mut Vec>; - fn midi_note (&mut self) -> &mut Vec; - fn has_midi_outs (&self) -> bool { - self.midi_outs().len() > 0 - } -} - -//////////////////////////////////////////////////////////////////////////////////// - -pub trait JackActivate: Sized { - fn activate_with ( - self, - init: impl FnOnce(&Arc>)->Usually - ) - -> Usually>>; -} - -impl JackActivate for JackClient { - fn activate_with ( - self, - init: impl FnOnce(&Arc>)->Usually - ) - -> Usually>> - { - let client = Arc::new(RwLock::new(self)); - let target = Arc::new(RwLock::new(init(&client)?)); - let event = Box::new(move|_|{/*TODO*/}) as Box; - let events = Notifications(event); - let frame = Box::new({ - let target = target.clone(); - move|c: &_, s: &_|if let Ok(mut target) = target.write() { - target.process(c, s) - } else { - Control::Quit - } - }); - let frames = ClosureProcessHandler::new(frame as BoxedAudioHandler); - let mut buffer = Self::Activating; - std::mem::swap(&mut*client.write().unwrap(), &mut buffer); - *client.write().unwrap() = Self::Active(Client::from(buffer).activate_async(events, frames)?); - Ok(target) - } -} - -/// Trait for things that have a JACK process callback. -pub trait Audio: Send + Sync { - fn process(&mut self, _: &Client, _: &ProcessScope) -> Control { - Control::Continue - } - fn callback( - state: &Arc>, client: &Client, scope: &ProcessScope - ) -> Control where Self: Sized { - if let Ok(mut state) = state.write() { - state.process(client, scope) - } else { - Control::Quit - } - } -} - -/// A UI component that may be associated with a JACK client by the `Jack` factory. -pub trait AudioComponent: Component + Audio { - /// Perform type erasure for collecting heterogeneous devices. - fn boxed(self) -> Box> - where - Self: Sized + 'static, - { - Box::new(self) - } -} - -/// All things that implement the required traits can be treated as `AudioComponent`. -impl + Audio> AudioComponent for W {} - -/// Trait for things that may expose JACK ports. -pub trait Ports { - fn audio_ins(&self) -> Usually>> { - Ok(vec![]) - } - fn audio_outs(&self) -> Usually>> { - Ok(vec![]) - } - fn midi_ins(&self) -> Usually>> { - Ok(vec![]) - } - fn midi_outs(&self) -> Usually>> { - Ok(vec![]) - } -} - -fn register_ports( - client: &Client, - names: Vec, - spec: T, -) -> Usually>> { - names - .into_iter() - .try_fold(BTreeMap::new(), |mut ports, name| { - let port = client.register_port(&name, spec)?; - ports.insert(name, port); - Ok(ports) - }) -} - -fn query_ports(client: &Client, names: Vec) -> BTreeMap> { - names.into_iter().fold(BTreeMap::new(), |mut ports, name| { - let port = client.port_by_name(&name).unwrap(); - ports.insert(name, port); - ports - }) -} - -///// A [AudioComponent] bound to a JACK client and a set of ports. -//pub struct JackDevice { - ///// The active JACK client of this device. - //pub client: DynamicAsyncClient, - ///// The device state, encapsulated for sharing between threads. - //pub state: Arc>>>, - ///// Unowned copies of the device's JACK ports, for connecting to the device. - ///// The "real" readable/writable `Port`s are owned by the `state`. - //pub ports: UnownedJackPorts, -//} - -//impl std::fmt::Debug for JackDevice { - //fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - //f.debug_struct("JackDevice") - //.field("ports", &self.ports) - //.finish() - //} -//} - -//impl Render for JackDevice { - //type Engine = E; - //fn min_size(&self, to: E::Size) -> Perhaps { - //self.state.read().unwrap().layout(to) - //} - //fn render(&self, to: &mut E::Output) -> Usually<()> { - //self.state.read().unwrap().render(to) - //} -//} - -//impl Handle for JackDevice { - //fn handle(&mut self, from: &E::Input) -> Perhaps { - //self.state.write().unwrap().handle(from) - //} -//} - -//impl Ports for JackDevice { - //fn audio_ins(&self) -> Usually>> { - //Ok(self.ports.audio_ins.values().collect()) - //} - //fn audio_outs(&self) -> Usually>> { - //Ok(self.ports.audio_outs.values().collect()) - //} - //fn midi_ins(&self) -> Usually>> { - //Ok(self.ports.midi_ins.values().collect()) - //} - //fn midi_outs(&self) -> Usually>> { - //Ok(self.ports.midi_outs.values().collect()) - //} -//} - -//impl JackDevice { - ///// Returns a locked mutex of the state's contents. - //pub fn state(&self) -> LockResult>>> { - //self.state.read() - //} - ///// Returns a locked mutex of the state's contents. - //pub fn state_mut(&self) -> LockResult>>> { - //self.state.write() - //} - //pub fn connect_midi_in(&self, index: usize, port: &Port) -> Usually<()> { - //Ok(self - //.client - //.as_client() - //.connect_ports(port, self.midi_ins()?[index])?) - //} - //pub fn connect_midi_out(&self, index: usize, port: &Port) -> Usually<()> { - //Ok(self - //.client - //.as_client() - //.connect_ports(self.midi_outs()?[index], port)?) - //} - //pub fn connect_audio_in(&self, index: usize, port: &Port) -> Usually<()> { - //Ok(self - //.client - //.as_client() - //.connect_ports(port, self.audio_ins()?[index])?) - //} - //pub fn connect_audio_out(&self, index: usize, port: &Port) -> Usually<()> { - //Ok(self - //.client - //.as_client() - //.connect_ports(self.audio_outs()?[index], port)?) - //} -//} - -///// Collection of JACK ports as [AudioIn]/[AudioOut]/[MidiIn]/[MidiOut]. -//#[derive(Default, Debug)] -//pub struct JackPorts { - //pub audio_ins: BTreeMap>, - //pub midi_ins: BTreeMap>, - //pub audio_outs: BTreeMap>, - //pub midi_outs: BTreeMap>, -//} - -///// Collection of JACK ports as [Unowned]. -//#[derive(Default, Debug)] -//pub struct UnownedJackPorts { - //pub audio_ins: BTreeMap>, - //pub midi_ins: BTreeMap>, - //pub audio_outs: BTreeMap>, - //pub midi_outs: BTreeMap>, -//} - -//impl JackPorts { - //pub fn clone_unowned(&self) -> UnownedJackPorts { - //let mut unowned = UnownedJackPorts::default(); - //for (name, port) in self.midi_ins.iter() { - //unowned.midi_ins.insert(name.clone(), port.clone_unowned()); - //} - //for (name, port) in self.midi_outs.iter() { - //unowned.midi_outs.insert(name.clone(), port.clone_unowned()); - //} - //for (name, port) in self.audio_ins.iter() { - //unowned.audio_ins.insert(name.clone(), port.clone_unowned()); - //} - //for (name, port) in self.audio_outs.iter() { - //unowned - //.audio_outs - //.insert(name.clone(), port.clone_unowned()); - //} - //unowned - //} -//} - -///// Implement the `Ports` trait. -//#[macro_export] -//macro_rules! ports { - //($T:ty $({ $(audio: { - //$(ins: |$ai_arg:ident|$ai_impl:expr,)? - //$(outs: |$ao_arg:ident|$ao_impl:expr,)? - //})? $(midi: { - //$(ins: |$mi_arg:ident|$mi_impl:expr,)? - //$(outs: |$mo_arg:ident|$mo_impl:expr,)? - //})?})?) => { - //impl Ports for $T {$( - //$( - //$(fn audio_ins <'a> (&'a self) -> Usually>> { - //let cb = |$ai_arg:&'a Self|$ai_impl; - //cb(self) - //})? - //)? - //$( - //$(fn audio_outs <'a> (&'a self) -> Usually>> { - //let cb = (|$ao_arg:&'a Self|$ao_impl); - //cb(self) - //})? - //)? - //)? $( - //$( - //$(fn midi_ins <'a> (&'a self) -> Usually>> { - //let cb = (|$mi_arg:&'a Self|$mi_impl); - //cb(self) - //})? - //)? - //$( - //$(fn midi_outs <'a> (&'a self) -> Usually>> { - //let cb = (|$mo_arg:&'a Self|$mo_impl); - //cb(self) - //})? - //)? - //)?} - //}; -//} - -///// `JackDevice` factory. Creates JACK `Client`s, performs port registration -///// and activation, and encapsulates a `AudioComponent` into a `JackDevice`. -//pub struct Jack { - //pub client: Client, - //pub midi_ins: Vec, - //pub audio_ins: Vec, - //pub midi_outs: Vec, - //pub audio_outs: Vec, -//} - -//impl Jack { - //pub fn new(name: &str) -> Usually { - //Ok(Self { - //midi_ins: vec![], - //audio_ins: vec![], - //midi_outs: vec![], - //audio_outs: vec![], - //client: Client::new(name, ClientOptions::NO_START_SERVER)?.0, - //}) - //} - //pub fn run<'a: 'static, D, E>( - //self, - //state: impl FnOnce(JackPorts) -> Box, - //) -> Usually> - //where - //D: AudioComponent + Sized + 'static, - //E: Engine + 'static, - //{ - //let owned_ports = JackPorts { - //audio_ins: register_ports(&self.client, self.audio_ins, AudioIn::default())?, - //audio_outs: register_ports(&self.client, self.audio_outs, AudioOut::default())?, - //midi_ins: register_ports(&self.client, self.midi_ins, MidiIn::default())?, - //midi_outs: register_ports(&self.client, self.midi_outs, MidiOut::default())?, - //}; - //let midi_outs = owned_ports - //.midi_outs - //.values() - //.map(|p| Ok(p.name()?)) - //.collect::>>()?; - //let midi_ins = owned_ports - //.midi_ins - //.values() - //.map(|p| Ok(p.name()?)) - //.collect::>>()?; - //let audio_outs = owned_ports - //.audio_outs - //.values() - //.map(|p| Ok(p.name()?)) - //.collect::>>()?; - //let audio_ins = owned_ports - //.audio_ins - //.values() - //.map(|p| Ok(p.name()?)) - //.collect::>>()?; - //let state = Arc::new(RwLock::new(state(owned_ports) as Box>)); - //let client = self.client.activate_async( - //Notifications(Box::new({ - //let _state = state.clone(); - //move |_event| { - //// FIXME: this deadlocks - ////state.lock().unwrap().handle(&event).unwrap(); - //} - //}) as Box), - //contrib::ClosureProcessHandler::new(Box::new({ - //let state = state.clone(); - //move |c: &Client, s: &ProcessScope| state.write().unwrap().process(c, s) - //}) as BoxedAudioHandler), - //)?; - //Ok(JackDevice { - //ports: UnownedJackPorts { - //audio_ins: query_ports(&client.as_client(), audio_ins), - //audio_outs: query_ports(&client.as_client(), audio_outs), - //midi_ins: query_ports(&client.as_client(), midi_ins), - //midi_outs: query_ports(&client.as_client(), midi_outs), - //}, - //client, - //state, - //}) - //} - //pub fn audio_in(mut self, name: &str) -> Self { - //self.audio_ins.push(name.to_string()); - //self - //} - //pub fn audio_out(mut self, name: &str) -> Self { - //self.audio_outs.push(name.to_string()); - //self - //} - //pub fn midi_in(mut self, name: &str) -> Self { - //self.midi_ins.push(name.to_string()); - //self - //} - //pub fn midi_out(mut self, name: &str) -> Self { - //self.midi_outs.push(name.to_string()); - //self - //} -//} - -//impl Command for ArrangerSceneCommand { -//} - //Edit(phrase) => { state.state.phrase = phrase.clone() }, - //ToggleViewMode => { state.state.mode.to_next(); }, - //Delete => { state.state.delete(); }, - //Activate => { state.state.activate(); }, - //ZoomIn => { state.state.zoom_in(); }, - //ZoomOut => { state.state.zoom_out(); }, - //MoveBack => { state.state.move_back(); }, - //MoveForward => { state.state.move_forward(); }, - //RandomColor => { state.state.randomize_color(); }, - //Put => { state.state.phrase_put(); }, - //Get => { state.state.phrase_get(); }, - //AddScene => { state.state.scene_add(None, None)?; }, - //AddTrack => { state.state.track_add(None, None)?; }, - //ToggleLoop => { state.state.toggle_loop() }, - //pub fn zoom_in (&mut self) { - //if let ArrangerEditorMode::Vertical(factor) = self.mode { - //self.mode = ArrangerEditorMode::Vertical(factor + 1) - //} - //} - //pub fn zoom_out (&mut self) { - //if let ArrangerEditorMode::Vertical(factor) = self.mode { - //self.mode = ArrangerEditorMode::Vertical(factor.saturating_sub(1)) - //} - //} - //pub fn move_back (&mut self) { - //match self.selected { - //ArrangerEditorFocus::Scene(s) => { - //if s > 0 { - //self.scenes.swap(s, s - 1); - //self.selected = ArrangerEditorFocus::Scene(s - 1); - //} - //}, - //ArrangerEditorFocus::Track(t) => { - //if t > 0 { - //self.tracks.swap(t, t - 1); - //self.selected = ArrangerEditorFocus::Track(t - 1); - //// FIXME: also swap clip order in scenes - //} - //}, - //_ => todo!("arrangement: move forward") - //} - //} - //pub fn move_forward (&mut self) { - //match self.selected { - //ArrangerEditorFocus::Scene(s) => { - //if s < self.scenes.len().saturating_sub(1) { - //self.scenes.swap(s, s + 1); - //self.selected = ArrangerEditorFocus::Scene(s + 1); - //} - //}, - //ArrangerEditorFocus::Track(t) => { - //if t < self.tracks.len().saturating_sub(1) { - //self.tracks.swap(t, t + 1); - //self.selected = ArrangerEditorFocus::Track(t + 1); - //// FIXME: also swap clip order in scenes - //} - //}, - //_ => todo!("arrangement: move forward") - //} - //} - -//impl From for Clock { - //fn from (current: Moment) -> Self { - //Self { - //playing: Some(TransportState::Stopped).into(), - //started: None.into(), - //quant: 24.into(), - //sync: (current.timebase.ppq.get() * 4.).into(), - //current, - //} - //} -//} diff --git a/crates/tek/src/api/phrase.rs b/crates/tek/src/api/phrase.rs deleted file mode 100644 index 7125856a..00000000 --- a/crates/tek/src/api/phrase.rs +++ /dev/null @@ -1,172 +0,0 @@ -use crate::*; - -pub trait HasPhrases { - fn phrases (&self) -> &Vec>>; - fn phrases_mut (&mut self) -> &mut Vec>>; -} - -#[derive(Clone, Debug, PartialEq)] -pub enum PhrasePoolCommand { - Add(usize, Phrase), - Delete(usize), - Swap(usize, usize), - Import(usize, PathBuf), - Export(usize, PathBuf), - SetName(usize, String), - SetLength(usize, usize), - SetColor(usize, ItemColor), -} - -impl Command for PhrasePoolCommand { - fn execute (self, model: &mut T) -> Perhaps { - use PhrasePoolCommand::*; - Ok(match self { - Add(mut index, phrase) => { - let phrase = Arc::new(RwLock::new(phrase)); - let phrases = model.phrases_mut(); - if index >= phrases.len() { - index = phrases.len(); - phrases.push(phrase) - } else { - phrases.insert(index, phrase); - } - Some(Self::Delete(index)) - }, - Delete(index) => { - let phrase = model.phrases_mut().remove(index).read().unwrap().clone(); - Some(Self::Add(index, phrase)) - }, - Swap(index, other) => { - model.phrases_mut().swap(index, other); - Some(Self::Swap(index, other)) - }, - Import(index, path) => { - let bytes = std::fs::read(&path)?; - let smf = Smf::parse(bytes.as_slice())?; - let mut t = 0u32; - let mut events = vec![]; - for track in smf.tracks.iter() { - for event in track.iter() { - t += event.delta.as_int(); - if let TrackEventKind::Midi { channel, message } = event.kind { - events.push((t, channel.as_int(), message)); - } - } - } - let mut phrase = Phrase::new("imported", true, t as usize + 1, None, None); - for event in events.iter() { - phrase.notes[event.0 as usize].push(event.2); - } - Self::Add(index, phrase).execute(model)? - }, - Export(_index, _path) => { - todo!("export phrase to midi file"); - }, - SetName(index, name) => { - let mut phrase = model.phrases()[index].write().unwrap(); - let old_name = phrase.name.clone(); - phrase.name = name; - Some(Self::SetName(index, old_name)) - }, - SetLength(index, length) => { - let mut phrase = model.phrases()[index].write().unwrap(); - let old_len = phrase.length; - phrase.length = length; - Some(Self::SetLength(index, old_len)) - }, - SetColor(index, color) => { - let mut color = ItemColorTriplet::from(color); - std::mem::swap(&mut color, &mut model.phrases()[index].write().unwrap().color); - Some(Self::SetColor(index, color.base)) - }, - }) - } -} - -/// A MIDI sequence. -#[derive(Debug, Clone)] -pub struct Phrase { - pub uuid: uuid::Uuid, - /// Name of phrase - pub name: String, - /// Temporal resolution in pulses per quarter note - pub ppq: usize, - /// Length of phrase in pulses - pub length: usize, - /// Notes in phrase - pub notes: PhraseData, - /// Whether to loop the phrase or play it once - pub loop_on: bool, - /// Start of loop - pub loop_start: usize, - /// Length of loop - pub loop_length: usize, - /// All notes are displayed with minimum length - pub percussive: bool, - /// Identifying color of phrase - pub color: ItemColorTriplet, -} - -/// MIDI message structural -pub type PhraseData = Vec>; - -impl Phrase { - pub fn new ( - name: impl AsRef, - loop_on: bool, - length: usize, - notes: Option, - color: Option, - ) -> Self { - Self { - uuid: uuid::Uuid::new_v4(), - name: name.as_ref().to_string(), - ppq: PPQ, - length, - notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]), - loop_on, - loop_start: 0, - loop_length: length, - percussive: true, - color: color.unwrap_or_else(ItemColorTriplet::random) - } - } - pub fn set_length (&mut self, length: usize) { - self.length = length; - self.notes = vec![Vec::with_capacity(16);length]; - } - pub fn duplicate (&self) -> Self { - let mut clone = self.clone(); - clone.uuid = uuid::Uuid::new_v4(); - clone - } - pub fn toggle_loop (&mut self) { self.loop_on = !self.loop_on; } - pub fn record_event (&mut self, pulse: usize, message: MidiMessage) { - if pulse >= self.length { panic!("extend phrase first") } - self.notes[pulse].push(message); - } - /// Check if a range `start..end` contains MIDI Note On `k` - pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool { - //panic!("{:?} {start} {end}", &self); - for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() { - for event in events.iter() { - if let MidiMessage::NoteOn {key,..} = event { if *key == k { return true } } - } - } - return false - } -} - -impl Default for Phrase { - fn default () -> Self { - Self::new("(empty)", false, 0, None, Some(ItemColor::from(Color::Rgb(0, 0, 0)).into())) - } -} - -impl PartialEq for Phrase { - fn eq (&self, other: &Self) -> bool { - self.uuid == other.uuid - } -} - -impl Eq for Phrase {} diff --git a/crates/tek/src/api/player.rs b/crates/tek/src/api/player.rs deleted file mode 100644 index f728e3b8..00000000 --- a/crates/tek/src/api/player.rs +++ /dev/null @@ -1,362 +0,0 @@ -use crate::*; - -pub trait HasPlayer { - fn player (&self) -> &impl MidiPlayerApi; - fn player_mut (&mut self) -> &mut impl MidiPlayerApi; -} - -pub trait MidiPlayerApi: MidiRecordApi + MidiPlaybackApi + Send + Sync {} - -pub trait HasPlayPhrase: HasClock { - fn reset (&self) -> bool; - fn reset_mut (&mut self) -> &mut bool; - fn play_phrase (&self) -> &Option<(Moment, Option>>)>; - fn play_phrase_mut (&mut self) -> &mut Option<(Moment, Option>>)>; - fn next_phrase (&self) -> &Option<(Moment, Option>>)>; - fn next_phrase_mut (&mut self) -> &mut Option<(Moment, Option>>)>; - fn pulses_since_start (&self) -> Option { - if let Some((started, Some(_))) = self.play_phrase().as_ref() { - Some(self.clock().playhead.pulse.get() - started.pulse.get()) - } else { - None - } - } - fn enqueue_next (&mut self, phrase: Option<&Arc>>) { - let start = self.clock().next_launch_pulse() as f64; - let instant = Moment::from_pulse(&self.clock().timebase(), start); - let phrase = phrase.map(|p|p.clone()); - *self.next_phrase_mut() = Some((instant, phrase)); - *self.reset_mut() = true; - } -} - -pub trait MidiRecordApi: HasClock + HasPlayPhrase + HasMidiIns { - fn notes_in (&self) -> &Arc>; - - fn recording (&self) -> bool; - fn recording_mut (&mut self) -> &mut bool; - fn toggle_record (&mut self) { - *self.recording_mut() = !self.recording(); - } - fn record (&mut self, scope: &ProcessScope, midi_buf: &mut Vec>>) { - let sample0 = scope.last_frame_time() as usize; - // For highlighting keys and note repeat - let notes_in = self.notes_in().clone(); - if self.clock().is_rolling() { - if let Some((started, ref phrase)) = self.play_phrase().clone() { - let start = started.sample.get() as usize; - let quant = self.clock().quant.get(); - let timebase = self.clock().timebase().clone(); - let monitoring = self.monitoring(); - let recording = self.recording(); - for input in self.midi_ins_mut().iter() { - for (sample, event, bytes) in parse_midi_input(input.iter(scope)) { - if let LiveEvent::Midi { message, .. } = event { - if monitoring { - midi_buf[sample].push(bytes.to_vec()) - } - if recording { - if let Some(phrase) = phrase { - let mut phrase = phrase.write().unwrap(); - let length = phrase.length; - phrase.record_event({ - let sample = (sample0 + sample - start) as f64; - let pulse = timebase.samples_to_pulse(sample); - let quantized = (pulse / quant).round() * quant; - let looped = quantized as usize % length; - looped - }, message); - } - } - update_keys(&mut*notes_in.write().unwrap(), &message); - } - } - } - } - if let Some((start_at, phrase)) = &self.next_phrase() { - // TODO switch to next phrase and record into it - } - } - } - - fn monitoring (&self) -> bool; - fn monitoring_mut (&mut self) -> &mut bool; - fn toggle_monitor (&mut self) { - *self.monitoring_mut() = !self.monitoring(); - } - fn monitor (&mut self, scope: &ProcessScope, midi_buf: &mut Vec>>) { - // For highlighting keys and note repeat - let notes_in = self.notes_in().clone(); - for input in self.midi_ins_mut().iter() { - for (sample, event, bytes) in parse_midi_input(input.iter(scope)) { - if let LiveEvent::Midi { message, .. } = event { - midi_buf[sample].push(bytes.to_vec()); - update_keys(&mut*notes_in.write().unwrap(), &message); - } - } - } - } - - fn overdub (&self) -> bool; - fn overdub_mut (&mut self) -> &mut bool; - fn toggle_overdub (&mut self) { - *self.overdub_mut() = !self.overdub(); - } -} - -pub trait MidiPlaybackApi: HasPlayPhrase + HasClock + HasMidiOuts { - - fn notes_out (&self) -> &Arc>; - - /// Clear the section of the output buffer that we will be using, - /// emitting "all notes off" at start of buffer if requested. - fn clear ( - &mut self, scope: &ProcessScope, out_buf: &mut Vec>>, reset: bool - ) { - for frame in &mut out_buf[0..scope.n_frames() as usize] { - frame.clear(); - } - if reset { - all_notes_off(out_buf); - } - } - - /// Output notes from phrase to MIDI output ports. - fn play ( - &mut self, scope: &ProcessScope, note_buf: &mut Vec, out_buf: &mut Vec>> - ) -> bool { - let mut next = false; - // Write MIDI events from currently playing phrase (if any) to MIDI output buffer - if self.clock().is_rolling() { - let sample0 = scope.last_frame_time() as usize; - let samples = scope.n_frames() as usize; - // If no phrase is playing, prepare for switchover immediately - next = self.play_phrase().is_none(); - let phrase = self.play_phrase(); - let started0 = &self.clock().started; - let timebase = self.clock().timebase(); - let notes_out = self.notes_out(); - let next_phrase = self.next_phrase(); - if let Some((started, phrase)) = phrase { - // First sample to populate. Greater than 0 means that the first - // pulse of the phrase falls somewhere in the middle of the chunk. - let sample = started.sample.get() as usize; - let sample = sample + started0.read().unwrap().as_ref().unwrap().sample.get() as usize; - let sample = sample0.saturating_sub(sample); - // Iterator that emits sample (index into output buffer at which to write MIDI event) - // paired with pulse (index into phrase from which to take the MIDI event) for each - // sample of the output buffer that corresponds to a MIDI pulse. - let pulses = timebase.pulses_between_samples(sample, sample + samples); - // Notes active during current chunk. - let notes = &mut notes_out.write().unwrap(); - for (sample, pulse) in pulses { - // If a next phrase is enqueued, and we're past the end of the current one, - // break the loop here (FIXME count pulse correctly) - next = next_phrase.is_some() && if let Some(ref phrase) = phrase { - pulse >= phrase.read().unwrap().length - } else { - true - }; - if next { - break - } - // If there's a currently playing phrase, output notes from it to buffer: - if let Some(ref phrase) = phrase { - // Source phrase from which the MIDI events will be taken. - let phrase = phrase.read().unwrap(); - // Phrase with zero length is not processed - if phrase.length > 0 { - // Current pulse index in source phrase - let pulse = pulse % phrase.length; - // Output each MIDI event from phrase at appropriate frames of output buffer: - for message in phrase.notes[pulse].iter() { - // Clear output buffer for this MIDI event. - note_buf.clear(); - // TODO: support MIDI channels other than CH1. - let channel = 0.into(); - // Serialize MIDI event into message buffer. - LiveEvent::Midi { channel, message: *message } - .write(note_buf) - .unwrap(); - // Append serialized message to output buffer. - out_buf[sample].push(note_buf.clone()); - // Update the list of currently held notes. - update_keys(&mut*notes, &message); - } - } - } - } - } - } - next - } - - /// Handle switchover from current to next playing phrase. - fn switchover ( - &mut self, scope: &ProcessScope, note_buf: &mut Vec, out_buf: &mut Vec>> - ) { - if self.clock().is_rolling() { - let sample0 = scope.last_frame_time() as usize; - //let samples = scope.n_frames() as usize; - if let Some((start_at, phrase)) = &self.next_phrase() { - let start = start_at.sample.get() as usize; - let sample = self.clock().started.read().unwrap().as_ref().unwrap().sample.get() as usize; - // If it's time to switch to the next phrase: - if start <= sample0.saturating_sub(sample) { - // Samples elapsed since phrase was supposed to start - let skipped = sample0 - start; - // Switch over to enqueued phrase - let started = Moment::from_sample(&self.clock().timebase(), start as f64); - *self.play_phrase_mut() = Some((started, phrase.clone())); - // Unset enqueuement (TODO: where to implement looping?) - *self.next_phrase_mut() = None - } - // TODO fill in remaining ticks of chunk from next phrase. - // ?? just call self.play(scope) again, since enqueuement is off ??? - self.play(scope, note_buf, out_buf); - // ?? or must it be with modified scope ?? - // likely not because start time etc - } - } - } - - /// Write a chunk of MIDI notes to the output buffer. - fn write ( - &mut self, scope: &ProcessScope, out_buf: &Vec>> - ) { - let samples = scope.n_frames() as usize; - for port in self.midi_outs_mut().iter_mut() { - let writer = &mut port.writer(scope); - for time in 0..samples { - for event in out_buf[time].iter() { - writer.write(&RawMidi { time: time as u32, bytes: &event }) - .expect(&format!("{event:?}")); - } - } - } - } -} - -/// Add "all notes off" to the start of a buffer. -pub fn all_notes_off (output: &mut [Vec>]) { - let mut buf = vec![]; - let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() }; - let evt = LiveEvent::Midi { channel: 0.into(), message: msg }; - evt.write(&mut buf).unwrap(); - output[0].push(buf); -} - -/// Return boxed iterator of MIDI events -pub fn parse_midi_input (input: MidiIter) -> Box + '_> { - Box::new(input.map(|RawMidi { time, bytes }|( - time as usize, - LiveEvent::parse(bytes).unwrap(), - bytes - ))) -} - -/// Update notes_in array -pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) { - match message { - MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; } - MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; }, - _ => {} - } -} - -/// Hosts the JACK callback for a single MIDI player -pub struct PlayerAudio<'a, T: MidiPlayerApi>( - /// Player - pub &'a mut T, - /// Note buffer - pub &'a mut Vec, - /// Note chunk buffer - pub &'a mut Vec>>, -); - -/// JACK process callback for a sequencer's phrase player/recorder. -impl<'a, T: MidiPlayerApi> Audio for PlayerAudio<'a, T> { - fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { - let model = &mut self.0; - let note_buf = &mut self.1; - let midi_buf = &mut self.2; - // Clear output buffer(s) - model.clear(scope, midi_buf, false); - // Write chunk of phrase to output, handle switchover - if model.play(scope, note_buf, midi_buf) { - model.switchover(scope, note_buf, midi_buf); - } - if model.has_midi_ins() { - if model.recording() || model.monitoring() { - // Record and/or monitor input - model.record(scope, midi_buf) - } else if model.has_midi_outs() && model.monitoring() { - // Monitor input to output - model.monitor(scope, midi_buf) - } - } - // Write to output port(s) - model.write(scope, midi_buf); - Control::Continue - } -} - -//#[derive(Debug)] -//pub struct MIDIPlayer { - ///// Global timebase - //pub clock: Arc, - ///// Start time and phrase being played - //pub play_phrase: Option<(Moment, Option>>)>, - ///// Start time and next phrase - //pub next_phrase: Option<(Moment, Option>>)>, - ///// Play input through output. - //pub monitoring: bool, - ///// Write input to sequence. - //pub recording: bool, - ///// Overdub input to sequence. - //pub overdub: bool, - ///// Send all notes off - //pub reset: bool, // TODO?: after Some(nframes) - ///// Record from MIDI ports to current sequence. - //pub midi_inputs: Vec>, - ///// Play from current sequence to MIDI ports - //pub midi_outputs: Vec>, - ///// MIDI output buffer - //pub midi_note: Vec, - ///// MIDI output buffer - //pub midi_chunk: Vec>>, - ///// Notes currently held at input - //pub notes_in: Arc>, - ///// Notes currently held at output - //pub notes_out: Arc>, -//} - -///// Methods used primarily by the process callback -//impl MIDIPlayer { - //pub fn new ( - //jack: &Arc>, - //clock: &Arc, - //name: &str - //) -> Usually { - //let jack = jack.read().unwrap(); - //Ok(Self { - //clock: clock.clone(), - //phrase: None, - //next_phrase: None, - //notes_in: Arc::new(RwLock::new([false;128])), - //notes_out: Arc::new(RwLock::new([false;128])), - //monitoring: false, - //recording: false, - //overdub: true, - //reset: true, - //midi_note: Vec::with_capacity(8), - //midi_chunk: vec![Vec::with_capacity(16);16384], - //midi_outputs: vec![ - //jack.client().register_port(format!("{name}_out0").as_str(), MidiOut::default())? - //], - //midi_inputs: vec![ - //jack.client().register_port(format!("{name}_in0").as_str(), MidiIn::default())? - //], - //}) - //} -//} diff --git a/crates/tek/src/api/scene.rs b/crates/tek/src/api/scene.rs deleted file mode 100644 index 662569bc..00000000 --- a/crates/tek/src/api/scene.rs +++ /dev/null @@ -1,127 +0,0 @@ -use crate::*; - -pub trait HasScenes { - fn scenes (&self) -> &Vec; - fn scenes_mut (&mut self) -> &mut Vec; - fn scene_add (&mut self, name: Option<&str>, color: Option) -> Usually<&mut S>; - fn scene_del (&mut self, index: usize) { - self.scenes_mut().remove(index); - } - fn scene_default_name (&self) -> String { - format!("Scene {}", self.scenes().len() + 1) - } - fn selected_scene (&self) -> Option<&S> { - None - } - fn selected_scene_mut (&mut self) -> Option<&mut S> { - None - } -} - -#[derive(Clone, Debug)] -pub enum ArrangerSceneCommand { - Add, - Delete(usize), - RandomColor, - Play(usize), - Swap(usize, usize), - SetSize(usize), - SetZoom(usize), -} - -//impl Command for ArrangerSceneCommand { - //fn execute (self, state: &mut T) -> Perhaps { - //match self { - //Self::Delete(index) => { state.scene_del(index); }, - //_ => todo!() - //} - //Ok(None) - //} -//} - -pub trait ArrangerSceneApi: Sized { - fn name (&self) -> &Arc>; - fn clips (&self) -> &Vec>>>; - fn color (&self) -> ItemColor; - - fn ppqs (scenes: &[Self], factor: usize) -> Vec<(usize, usize)> { - let mut total = 0; - if factor == 0 { - scenes.iter().map(|scene|{ - let pulses = scene.pulses().max(PPQ); - total = total + pulses; - (pulses, total - pulses) - }).collect() - } else { - (0..=scenes.len()).map(|i|{ - (factor*PPQ, factor*PPQ*i) - }).collect() - } - } - - fn longest_name (scenes: &[Self]) -> usize { - scenes.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max) - } - - /// Returns the pulse length of the longest phrase in the scene - fn pulses (&self) -> usize { - self.clips().iter().fold(0, |a, p|{ - a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0)) - }) - } - - /// Returns true if all phrases in the scene are - /// currently playing on the given collection of tracks. - fn is_playing (&self, tracks: &[T]) -> bool { - self.clips().iter().any(|clip|clip.is_some()) && self.clips().iter().enumerate() - .all(|(track_index, clip)|match clip { - Some(clip) => tracks - .get(track_index) - .map(|track|{ - if let Some((_, Some(phrase))) = track.player().play_phrase() { - *phrase.read().unwrap() == *clip.read().unwrap() - } else { - false - } - }) - .unwrap_or(false), - None => true - }) - } - - fn clip (&self, index: usize) -> Option<&Arc>> { - match self.clips().get(index) { Some(Some(clip)) => Some(clip), _ => None } - } - -} - -//impl ArrangerScene { - - ////TODO - ////pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually { - ////let mut name = None; - ////let mut clips = vec![]; - ////edn!(edn in args { - ////Edn::Map(map) => { - ////let key = map.get(&Edn::Key(":name")); - ////if let Some(Edn::Str(n)) = key { - ////name = Some(*n); - ////} else { - ////panic!("unexpected key in scene '{name:?}': {key:?}") - ////} - ////}, - ////Edn::Symbol("_") => { - ////clips.push(None); - ////}, - ////Edn::Int(i) => { - ////clips.push(Some(*i as usize)); - ////}, - ////_ => panic!("unexpected in scene '{name:?}': {edn:?}") - ////}); - ////Ok(ArrangerScene { - ////name: Arc::new(name.unwrap_or("").to_string().into()), - ////color: ItemColor::random(), - ////clips, - ////}) - ////} -//} diff --git a/crates/tek/src/api/track.rs b/crates/tek/src/api/track.rs deleted file mode 100644 index 43229a71..00000000 --- a/crates/tek/src/api/track.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::*; - -pub trait HasTracks: Send + Sync { - fn tracks (&self) -> &Vec; - fn tracks_mut (&mut self) -> &mut Vec; -} - -impl HasTracks for Vec { - fn tracks (&self) -> &Vec { - self - } - fn tracks_mut (&mut self) -> &mut Vec { - self - } -} - -pub trait ArrangerTracksApi: HasTracks { - fn track_add (&mut self, name: Option<&str>, color: Option)-> Usually<&mut T>; - fn track_del (&mut self, index: usize); - fn track_default_name (&self) -> String { - format!("Track {}", self.tracks().len() + 1) - } -} - -#[derive(Clone, Debug)] -pub enum ArrangerTrackCommand { - Add, - Delete(usize), - RandomColor, - Stop, - Swap(usize, usize), - SetSize(usize), - SetZoom(usize), -} - -pub trait ArrangerTrackApi: HasPlayer + Send + Sync + Sized { - /// Name of track - fn name (&self) -> &Arc>; - /// Preferred width of track column - fn width (&self) -> usize; - /// Preferred width of track column - fn width_mut (&mut self) -> &mut usize; - /// Identifying color of track - fn color (&self) -> ItemColor; - - fn longest_name (tracks: &[Self]) -> usize { - tracks.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max) - } - - const MIN_WIDTH: usize = 3; - - fn width_inc (&mut self) { - *self.width_mut() += 1; - } - - fn width_dec (&mut self) { - if self.width() > Self::MIN_WIDTH { - *self.width_mut() -= 1; - } - } -} - -/// Hosts the JACK callback for a collection of tracks -pub struct TracksAudio<'a, T: ArrangerTrackApi, H: HasTracks>( - // Track collection - pub &'a mut H, - /// Note buffer - pub &'a mut Vec, - /// Note chunk buffer - pub &'a mut Vec>>, - /// Marker - pub PhantomData, -); - -impl<'a, T: ArrangerTrackApi, H: HasTracks> Audio for TracksAudio<'a, T, H> { - #[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { - let model = &mut self.0; - let note_buffer = &mut self.1; - let output_buffer = &mut self.2; - for track in model.tracks_mut().iter_mut() { - if PlayerAudio(track.player_mut(), note_buffer, output_buffer).process(client, scope) == Control::Quit { - return Control::Quit - } - } - Control::Continue - } -} diff --git a/crates/tek/src/cli/cli_arranger.rs b/crates/tek/src/cli/cli_arranger.rs deleted file mode 100644 index 4c11ea1f..00000000 --- a/crates/tek/src/cli/cli_arranger.rs +++ /dev/null @@ -1,49 +0,0 @@ -include!("../lib.rs"); - -pub fn main () -> Usually<()> { - ArrangerCli::parse().run() -} - -/// Parses CLI arguments to the `tek_arranger` invocation. -#[derive(Debug, Parser)] -#[command(version, about, long_about = None)] -pub struct ArrangerCli { - /// Name of JACK client - #[arg(short, long)] name: Option, - /// Whether to include a transport toolbar (default: true) - #[arg(short, long, default_value_t = true)] transport: bool, - /// Number of tracks - #[arg(short = 'x', long, default_value_t = 8)] tracks: usize, - /// Number of scenes - #[arg(short, long, default_value_t = 8)] scenes: usize, -} - -impl ArrangerCli { - /// Run the arranger TUI from CLI arguments. - fn run (&self) -> Usually<()> { - Tui::run(JackClient::new("tek_arranger")?.activate_with(|jack|{ - let mut app = ArrangerTui::try_from(jack)?; - if let Some(name) = self.name.as_ref() { - *app.name.write().unwrap() = name.clone(); - } - let track_color_1 = ItemColor::random(); - let track_color_2 = ItemColor::random(); - for i in 0..self.tracks { - let _track = app.track_add( - None, - Some(track_color_1.mix(track_color_2, i as f32 / self.tracks as f32)) - )?; - } - let scene_color_1 = ItemColor::random(); - let scene_color_2 = ItemColor::random(); - for i in 0..self.scenes { - let _scene = app.scene_add( - None, - Some(scene_color_1.mix(scene_color_2, i as f32 / self.scenes as f32)) - )?; - } - Ok(app) - })?)?; - Ok(()) - } -} diff --git a/crates/tek/src/cli/cli_sequencer.rs b/crates/tek/src/cli/cli_sequencer.rs deleted file mode 100644 index ec18266e..00000000 --- a/crates/tek/src/cli/cli_sequencer.rs +++ /dev/null @@ -1,45 +0,0 @@ -include!("../lib.rs"); - -pub fn main () -> Usually<()> { - SequencerCli::parse().run() -} - -#[derive(Debug, Parser)] -#[command(version, about, long_about = None)] -pub struct SequencerCli { - /// Name of JACK client - #[arg(short, long)] name: Option, - /// Pulses per quarter note (sequencer resolution; default: 96) - #[arg(short, long)] ppq: Option, - /// Default phrase duration (in pulses; default: 4 * PPQ = 1 bar) - #[arg(short, long)] length: Option, - /// Whether to include a transport toolbar (default: true) - #[arg(short, long, default_value_t = true)] transport: bool -} - -impl SequencerCli { - fn run (&self) -> Usually<()> { - Tui::run(JackClient::new("tek_sequencer")?.activate_with(|jack|{ - let mut app = SequencerTui::try_from(jack)?; - //app.editor.view_mode.set_time_zoom(1); - // TODO: create from arguments - let midi_in = app.jack.read().unwrap().register_port("in", MidiIn::default())?; - app.player.midi_ins.push(midi_in); - let midi_out = app.jack.read().unwrap().register_port("out", MidiOut::default())?; - app.player.midi_outs.push(midi_out); - if let Some(_) = self.name.as_ref() { - // TODO: sequencer.name = Arc::new(RwLock::new(name.clone())); - } - if let Some(_) = self.ppq { - // TODO: sequencer.ppq = ppq; - } - if let Some(_) = self.length { - // TODO: if let Some(phrase) = sequencer.phrase.as_mut() { - //phrase.write().unwrap().length = length; - //} - } - Ok(app) - })?)?; - Ok(()) - } -} diff --git a/crates/tek/src/cli/cli_transport.rs b/crates/tek/src/cli/cli_transport.rs deleted file mode 100644 index 68f47c94..00000000 --- a/crates/tek/src/cli/cli_transport.rs +++ /dev/null @@ -1,9 +0,0 @@ -include!("../lib.rs"); - -/// Application entrypoint. -pub fn main () -> Usually<()> { - Tui::run(JackClient::new("tek_transport")?.activate_with(|jack|{ - TransportTui::try_from(jack) - })?)?; - Ok(()) -} diff --git a/crates/tek/src/core.rs b/crates/tek/src/core.rs deleted file mode 100644 index 8a3cd53f..00000000 --- a/crates/tek/src/core.rs +++ /dev/null @@ -1,13 +0,0 @@ -use crate::*; - -mod audio; pub(crate) use audio::*; -mod color; pub(crate) use color::*; -mod command; pub(crate) use command::*; -mod edn; pub(crate) use edn::*; -mod engine; pub(crate) use engine::*; -mod focus; pub(crate) use focus::*; -mod input; pub(crate) use input::*; -mod output; pub(crate) use output::*; -mod pitch; pub(crate) use pitch::*; -mod space; pub(crate) use space::*; -mod time; pub(crate) use time::*; diff --git a/crates/tek/src/core/audio.rs b/crates/tek/src/core/audio.rs deleted file mode 100644 index 176b62a6..00000000 --- a/crates/tek/src/core/audio.rs +++ /dev/null @@ -1,181 +0,0 @@ -use crate::*; -use jack::*; - -#[derive(Debug)] -/// Event enum for JACK events. -pub enum JackEvent { - ThreadInit, - Shutdown(ClientStatus, String), - Freewheel(bool), - SampleRate(Frames), - ClientRegistration(String, bool), - PortRegistration(PortId, bool), - PortRename(PortId, String, String), - PortsConnected(PortId, PortId, bool), - GraphReorder, - XRun, -} - -/// Wraps [Client] or [DynamicAsyncClient] in place. -#[derive(Debug)] -pub enum JackClient { - /// Before activation. - Inactive(Client), - /// During activation. - Activating, - /// After activation. Must not be dropped for JACK thread to persist. - Active(DynamicAsyncClient), -} - -/// Trait for things that wrap a JACK client. -pub trait AudioEngine { - - fn transport (&self) -> Transport { - self.client().transport() - } - - fn port_by_name (&self, name: &str) -> Option> { - self.client().port_by_name(name) - } - - fn register_port (&self, name: &str, spec: PS) -> Usually> { - Ok(self.client().register_port(name, spec)?) - } - - fn client (&self) -> &Client; - - fn activate ( - self, - process: impl FnMut(&Arc>, &Client, &ProcessScope) -> Control + Send + 'static - ) -> Usually>> where Self: Send + Sync + 'static; - - fn thread_init (&self, _: &Client) {} - - unsafe fn shutdown (&mut self, status: ClientStatus, reason: &str) {} - - fn freewheel (&mut self, _: &Client, enabled: bool) {} - - fn client_registration (&mut self, _: &Client, name: &str, reg: bool) {} - - fn port_registration (&mut self, _: &Client, id: PortId, reg: bool) {} - - fn ports_connected (&mut self, _: &Client, a: PortId, b: PortId, are: bool) {} - - fn sample_rate (&mut self, _: &Client, frames: Frames) -> Control { - Control::Continue - } - - fn port_rename (&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { - Control::Continue - } - - fn graph_reorder (&mut self, _: &Client) -> Control { - Control::Continue - } - - fn xrun (&mut self, _: &Client) -> Control { - Control::Continue - } -} - -impl AudioEngine for JackClient { - fn client(&self) -> &Client { - match self { - Self::Inactive(ref client) => client, - Self::Activating => panic!("jack client has not finished activation"), - Self::Active(ref client) => client.as_client(), - } - } - fn activate( - self, - mut cb: impl FnMut(&Arc>, &Client, &ProcessScope) -> Control + Send + 'static, - ) -> Usually>> - where - Self: Send + Sync + 'static - { - let client = Client::from(self); - let state = Arc::new(RwLock::new(Self::Activating)); - let event = Box::new(move|_|{/*TODO*/}) as Box; - let events = Notifications(event); - let frame = Box::new({let state = state.clone(); move|c: &_, s: &_|cb(&state, c, s)}); - let frames = contrib::ClosureProcessHandler::new(frame as BoxedAudioHandler); - *state.write().unwrap() = Self::Active(client.activate_async(events, frames)?); - Ok(state) - } -} - -pub type DynamicAsyncClient = AsyncClient; - -pub type DynamicAudioHandler = contrib::ClosureProcessHandler<(), BoxedAudioHandler>; - -pub type BoxedAudioHandler = Box Control + Send>; - -impl JackClient { - pub fn new (name: &str) -> Usually { - let (client, _) = Client::new(name, ClientOptions::NO_START_SERVER)?; - Ok(Self::Inactive(client)) - } -} - -impl From for Client { - fn from (jack: JackClient) -> Client { - match jack { - JackClient::Inactive(client) => client, - JackClient::Activating => panic!("jack client still activating"), - JackClient::Active(_) => panic!("jack client already activated"), - } - } -} - -/// Notification handler used by the [Jack] factory -/// when constructing [JackDevice]s. -pub type DynamicNotifications = Notifications>; - -/// Generic notification handler that emits [JackEvent] -pub struct Notifications(pub T); - -impl NotificationHandler for Notifications { - fn thread_init(&self, _: &Client) { - self.0(JackEvent::ThreadInit); - } - - unsafe fn shutdown(&mut self, status: ClientStatus, reason: &str) { - self.0(JackEvent::Shutdown(status, reason.into())); - } - - fn freewheel(&mut self, _: &Client, enabled: bool) { - self.0(JackEvent::Freewheel(enabled)); - } - - fn sample_rate(&mut self, _: &Client, frames: Frames) -> Control { - self.0(JackEvent::SampleRate(frames)); - Control::Quit - } - - fn client_registration(&mut self, _: &Client, name: &str, reg: bool) { - self.0(JackEvent::ClientRegistration(name.into(), reg)); - } - - fn port_registration(&mut self, _: &Client, id: PortId, reg: bool) { - self.0(JackEvent::PortRegistration(id, reg)); - } - - fn port_rename(&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { - self.0(JackEvent::PortRename(id, old.into(), new.into())); - Control::Continue - } - - fn ports_connected(&mut self, _: &Client, a: PortId, b: PortId, are: bool) { - self.0(JackEvent::PortsConnected(a, b, are)); - } - - fn graph_reorder(&mut self, _: &Client) -> Control { - self.0(JackEvent::GraphReorder); - Control::Continue - } - - fn xrun(&mut self, _: &Client) -> Control { - self.0(JackEvent::XRun); - Control::Continue - } -} diff --git a/crates/tek/src/core/color.rs b/crates/tek/src/core/color.rs deleted file mode 100644 index 9464e1f2..00000000 --- a/crates/tek/src/core/color.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::*; -use rand::{thread_rng, distributions::uniform::UniformSampler}; -pub use ratatui::prelude::Color; - -/// A color in OKHSL and RGB representations. -#[derive(Debug, Default, Copy, Clone, PartialEq)] -pub struct ItemColor { - pub okhsl: Okhsl, - pub rgb: Color, -} -/// A color in OKHSL and RGB with lighter and darker variants. -#[derive(Debug, Default, Copy, Clone, PartialEq)] -pub struct ItemColorTriplet { - pub base: ItemColor, - pub light: ItemColor, - pub dark: ItemColor, -} -/// Adds TUI RGB representation to an OKHSL value. -impl From> for ItemColor { - fn from (okhsl: Okhsl) -> Self { Self { okhsl, rgb: okhsl_to_rgb(okhsl) } } -} -/// Adds OKHSL representation to a TUI RGB value. -impl From for ItemColor { - fn from (rgb: Color) -> Self { Self { rgb, okhsl: rgb_to_okhsl(rgb) } } -} -impl ItemColor { - pub fn random () -> Self { - let mut rng = thread_rng(); - let lo = Okhsl::new(-180.0, 0.01, 0.25); - let hi = Okhsl::new( 180.0, 0.9, 0.5); - UniformOkhsl::new(lo, hi).sample(&mut rng).into() - } - pub fn random_dark () -> Self { - let mut rng = thread_rng(); - let lo = Okhsl::new(-180.0, 0.025, 0.075); - let hi = Okhsl::new( 180.0, 0.5, 0.150); - UniformOkhsl::new(lo, hi).sample(&mut rng).into() - } - pub fn random_near (color: Self, distance: f32) -> Self { - color.mix(Self::random(), distance) - } - pub fn mix (&self, other: Self, distance: f32) -> Self { - if distance > 1.0 { panic!("color mixing takes distance between 0.0 and 1.0"); } - self.okhsl.mix(other.okhsl, distance).into() - } -} -impl From for ItemColorTriplet { - fn from (base: ItemColor) -> Self { - let mut light = base.okhsl.clone(); - light.lightness = (light.lightness * 1.15).min(Okhsl::::max_lightness()); - let mut dark = base.okhsl.clone(); - dark.lightness = (dark.lightness * 0.85).max(Okhsl::::min_lightness()); - dark.saturation = (dark.saturation * 0.85).max(Okhsl::::min_saturation()); - Self { base, light: light.into(), dark: dark.into() } - } -} -impl ItemColorTriplet { - pub fn random () -> Self { - ItemColor::random().into() - } - pub fn random_near (color: Self, distance: f32) -> Self { - color.base.mix(ItemColor::random(), distance).into() - } -} -pub fn okhsl_to_rgb (color: Okhsl) -> Color { - let Srgb { red, green, blue, .. }: Srgb = Srgb::from_color_unclamped(color); - Color::Rgb((red * 255.0) as u8, (green * 255.0) as u8, (blue * 255.0) as u8,) -} -pub fn rgb_to_okhsl (color: Color) -> Okhsl { - if let Color::Rgb(r, g, b) = color { - Okhsl::from_color(Srgb::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0)) - } else { - unreachable!("only Color::Rgb is supported") - } -} diff --git a/crates/tek/src/core/command.rs b/crates/tek/src/core/command.rs deleted file mode 100644 index 480a42e8..00000000 --- a/crates/tek/src/core/command.rs +++ /dev/null @@ -1,96 +0,0 @@ -use crate::*; - -#[derive(Clone)] -pub enum NextPrev { - Next, - Prev, -} - -pub trait Execute { - fn command (&mut self, command: T) -> Perhaps; -} - -pub trait Command: Send + Sync + Sized { - fn execute (self, state: &mut S) -> Perhaps; -} -pub fn delegate , S> ( - cmd: C, - wrap: impl Fn(C)->B, - state: &mut S, -) -> Perhaps { - Ok(cmd.execute(state)?.map(|x|wrap(x))) -} - -pub trait InputToCommand: Command + Sized { - fn input_to_command (state: &S, input: &E::Input) -> Option; - fn execute_with_state (state: &mut S, input: &E::Input) -> Perhaps { - Ok(if let Some(command) = Self::input_to_command(state, input) { - let _undo = command.execute(state)?; - Some(true) - } else { - None - }) - } -} -pub struct MenuBar> { - pub menus: Vec>, - pub index: usize, -} -impl> MenuBar { - pub fn new () -> Self { Self { menus: vec![], index: 0 } } - pub fn add (mut self, menu: Menu) -> Self { - self.menus.push(menu); - self - } -} -pub struct Menu> { - pub title: String, - pub items: Vec>, - pub index: Option, -} -impl> Menu { - pub fn new (title: impl AsRef) -> Self { - Self { - title: title.as_ref().to_string(), - items: vec![], - index: None, - } - } - pub fn add (mut self, item: MenuItem) -> Self { - self.items.push(item); - self - } - pub fn sep (mut self) -> Self { - self.items.push(MenuItem::sep()); - self - } - pub fn cmd (mut self, hotkey: &'static str, text: &'static str, command: C) -> Self { - self.items.push(MenuItem::cmd(hotkey, text, command)); - self - } - pub fn off (mut self, hotkey: &'static str, text: &'static str) -> Self { - self.items.push(MenuItem::off(hotkey, text)); - self - } -} -pub enum MenuItem> { - /// Unused. - __(PhantomData, PhantomData), - /// A separator. Skip it. - Separator, - /// A menu item with command, description and hotkey. - Command(&'static str, &'static str, C), - /// A menu item that can't be activated but has description and hotkey - Disabled(&'static str, &'static str) -} -impl> MenuItem { - pub fn sep () -> Self { - Self::Separator - } - pub fn cmd (hotkey: &'static str, text: &'static str, command: C) -> Self { - Self::Command(hotkey, text, command) - } - pub fn off (hotkey: &'static str, text: &'static str) -> Self { - Self::Disabled(hotkey, text) - } -} diff --git a/crates/tek/src/core/edn.rs b/crates/tek/src/core/edn.rs deleted file mode 100644 index 2709043e..00000000 --- a/crates/tek/src/core/edn.rs +++ /dev/null @@ -1,14 +0,0 @@ -pub use clojure_reader::{edn::{read, Edn}, error::Error as EdnError}; - -/// EDN parsing helper. -#[macro_export] macro_rules! edn { - ($edn:ident { $($pat:pat => $expr:expr),* $(,)? }) => { - match $edn { $($pat => $expr),* } - }; - ($edn:ident in $args:ident { $($pat:pat => $expr:expr),* $(,)? }) => { - for $edn in $args { - edn!($edn { $($pat => $expr),* }) - } - }; -} - diff --git a/crates/tek/src/core/engine.rs b/crates/tek/src/core/engine.rs deleted file mode 100644 index 10effc1b..00000000 --- a/crates/tek/src/core/engine.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::*; - -/// Entry point for main loop -pub trait App { - fn run (self, context: T) -> Usually; -} - -/// Platform backend. -pub trait Engine: Send + Sync + Sized { - /// Input event type - type Input: Input; - /// Result of handling input - type Handled; - /// Render target - type Output: Output; - /// Unit of length - type Unit: Coordinate; - /// Rectangle without offset - type Size: Size + From<[Self::Unit;2]> + Debug + Copy; - /// Rectangle with offset - type Area: Area + From<[Self::Unit;4]> + Debug + Copy; - /// Prepare before run - fn setup (&mut self) -> Usually<()> { Ok(()) } - /// True if done - fn exited (&self) -> bool; - /// Clean up after run - fn teardown (&mut self) -> Usually<()> { Ok(()) } -} - -/// A UI component that can render itself as a [Render], and [Handle] input. -pub trait Component: Render + Handle {} - -/// Everything that implements [Render] and [Handle] is a [Component]. -impl + Handle> Component for C {} - -/// A component that can exit. -pub trait Exit: Send { - fn exited (&self) -> bool; - fn exit (&mut self); - fn boxed (self) -> Box where Self: Sized + 'static { - Box::new(self) - } -} - -/// Marker trait for [Component]s that can [Exit]. -pub trait ExitableComponent: Exit + Component where E: Engine { - /// Perform type erasure for collecting heterogeneous components. - fn boxed (self) -> Box> where Self: Sized + 'static { - Box::new(self) - } -} - -/// All [Components]s that implement [Exit] implement [ExitableComponent]. -impl + Exit> ExitableComponent for C {} diff --git a/crates/tek/src/core/focus.rs b/crates/tek/src/core/focus.rs deleted file mode 100644 index b153cfde..00000000 --- a/crates/tek/src/core/focus.rs +++ /dev/null @@ -1,303 +0,0 @@ -use crate::*; - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum FocusState { - Focused(T), - Entered(T), -} - -impl FocusState { - pub fn inner (&self) -> T { - match self { - Self::Focused(inner) => *inner, - Self::Entered(inner) => *inner, - } - } - pub fn set_inner (&mut self, inner: T) { - *self = match self { - Self::Focused(_) => Self::Focused(inner), - Self::Entered(_) => Self::Entered(inner), - } - } - pub fn is_focused (&self) -> bool { - if let Self::Focused(_) = self { true } else { false } - } - pub fn is_entered (&self) -> bool { - if let Self::Entered(_) = self { true } else { false } - } - pub fn to_focused (&mut self) { - *self = Self::Focused(self.inner()) - } - pub fn to_entered (&mut self) { - *self = Self::Entered(self.inner()) - } -} - -#[derive(Copy, Clone, PartialEq, Debug)] -pub enum FocusCommand { - Up, - Down, - Left, - Right, - Next, - Prev, - Enter, - Exit, -} - -impl Command for FocusCommand { - fn execute (self, state: &mut F) -> Perhaps { - use FocusCommand::*; - match self { - Next => { state.focus_next(); }, - Prev => { state.focus_prev(); }, - Up => { state.focus_up(); }, - Down => { state.focus_down(); }, - Left => { state.focus_left(); }, - Right => { state.focus_right(); }, - Enter => { state.focus_enter(); }, - Exit => { state.focus_exit(); }, - } - Ok(None) - } -} - -/// Trait for things that have focusable subparts. -pub trait HasFocus { - type Item: Copy + PartialEq + Debug; - /// Get the currently focused item. - fn focused (&self) -> Self::Item; - /// Get the currently focused item. - fn set_focused (&mut self, to: Self::Item); - /// Loop forward until a specific item is focused. - fn focus_to (&mut self, to: Self::Item) { - self.set_focused(to); - self.focus_updated(); - } - /// Run this on focus update - fn focus_updated (&mut self) {} -} - -/// Trait for things that have enterable subparts. -pub trait HasEnter: HasFocus { - /// Get the currently focused item. - fn entered (&self) -> bool; - /// Get the currently focused item. - fn set_entered (&mut self, entered: bool); - /// Enter into the currently focused component - fn focus_enter (&mut self) { - self.set_entered(true); - self.focus_updated(); - } - /// Exit the currently entered component - fn focus_exit (&mut self) { - self.set_entered(false); - self.focus_updated(); - } -} - -/// Trait for things that implement directional navigation between focusable elements. -pub trait FocusGrid: HasFocus { - fn focus_layout (&self) -> &[&[Self::Item]]; - fn focus_cursor (&self) -> (usize, usize); - fn focus_cursor_mut (&mut self) -> &mut (usize, usize); - fn focus_current (&self) -> Self::Item { - let (x, y) = self.focus_cursor(); - self.focus_layout()[y][x] - } - fn focus_update (&mut self) { - self.focus_to(self.focus_current()); - self.focus_updated() - } - fn focus_up (&mut self) { - let original_focused = self.focused(); - let (_, original_y) = self.focus_cursor(); - loop { - let (x, y) = self.focus_cursor(); - let next_y = if y == 0 { - self.focus_layout().len().saturating_sub(1) - } else { - y - 1 - }; - if next_y == original_y { - break - } - let next_x = if self.focus_layout()[y].len() == self.focus_layout()[next_y].len() { - x - } else { - ((x as f32 / self.focus_layout()[original_y].len() as f32) - * self.focus_layout()[next_y].len() as f32) as usize - }; - *self.focus_cursor_mut() = (next_x, next_y); - if self.focus_current() != original_focused { - break - } - } - self.focus_update(); - } - fn focus_down (&mut self) { - let original_focused = self.focused(); - let (_, original_y) = self.focus_cursor(); - loop { - let (x, y) = self.focus_cursor(); - let next_y = if y >= self.focus_layout().len().saturating_sub(1) { - 0 - } else { - y + 1 - }; - if next_y == original_y { - break - } - let next_x = if self.focus_layout()[y].len() == self.focus_layout()[next_y].len() { - x - } else { - ((x as f32 / self.focus_layout()[original_y].len() as f32) - * self.focus_layout()[next_y].len() as f32) as usize - }; - *self.focus_cursor_mut() = (next_x, next_y); - if self.focus_current() != original_focused { - break - } - } - self.focus_update(); - } - fn focus_left (&mut self) { - let original_focused = self.focused(); - let (original_x, y) = self.focus_cursor(); - loop { - let x = self.focus_cursor().0; - let next_x = if x == 0 { - self.focus_layout()[y].len().saturating_sub(1) - } else { - x - 1 - }; - if next_x == original_x { - break - } - *self.focus_cursor_mut() = (next_x, y); - if self.focus_current() != original_focused { - break - } - } - self.focus_update(); - } - fn focus_right (&mut self) { - let original_focused = self.focused(); - let (original_x, y) = self.focus_cursor(); - loop { - let x = self.focus_cursor().0; - let next_x = if x >= self.focus_layout()[y].len().saturating_sub(1) { - 0 - } else { - x + 1 - }; - if next_x == original_x { - break - } - self.focus_cursor_mut().0 = next_x; - if self.focus_current() != original_focused { - break - } - } - self.focus_update(); - } -} - -/// Trait for things that implement next/prev navigation between focusable elements. -pub trait FocusOrder { - /// Focus the next item. - fn focus_next (&mut self); - /// Focus the previous item. - fn focus_prev (&mut self); -} - -/// Next/prev navigation for directional focusables works in the given way. -impl FocusOrder for T { - /// Focus the next item. - fn focus_next (&mut self) { - let current = self.focused(); - let (x, y) = self.focus_cursor(); - if x < self.focus_layout()[y].len().saturating_sub(1) { - self.focus_right(); - } else { - self.focus_down(); - self.focus_cursor_mut().0 = 0; - } - if self.focused() == current { // FIXME: prevent infinite loop - self.focus_next() - } - self.focus_exit(); - self.focus_update(); - } - /// Focus the previous item. - fn focus_prev (&mut self) { - let current = self.focused(); - let (x, _) = self.focus_cursor(); - if x > 0 { - self.focus_left(); - } else { - self.focus_up(); - let (_, y) = self.focus_cursor(); - let next_x = self.focus_layout()[y].len().saturating_sub(1); - self.focus_cursor_mut().0 = next_x; - } - if self.focused() == current { // FIXME: prevent infinite loop - self.focus_prev() - } - self.focus_exit(); - self.focus_update(); - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_focus () { - - struct FocusTest { - focused: char, - cursor: (usize, usize) - } - - impl HasFocus for FocusTest { - type Item = char; - fn focused (&self) -> Self::Item { - self.focused - } - fn set_focused (&mut self, to: Self::Item) { - self.focused = to - } - } - - impl FocusGrid for FocusTest { - fn focus_cursor (&self) -> (usize, usize) { - self.cursor - } - fn focus_cursor_mut (&mut self) -> &mut (usize, usize) { - &mut self.cursor - } - fn focus_layout (&self) -> &[&[Self::Item]] { - &[ - &['a', 'a', 'a', 'b', 'b', 'd'], - &['a', 'a', 'a', 'b', 'b', 'd'], - &['a', 'a', 'a', 'c', 'c', 'd'], - &['a', 'a', 'a', 'c', 'c', 'd'], - &['e', 'e', 'e', 'e', 'e', 'e'], - ] - } - } - - let mut tester = FocusTest { focused: 'a', cursor: (0, 0) }; - - tester.focus_right(); - assert_eq!(tester.cursor.0, 3); - assert_eq!(tester.focused, 'b'); - - tester.focus_down(); - assert_eq!(tester.cursor.1, 2); - assert_eq!(tester.focused, 'c'); - - } -} diff --git a/crates/tek/src/core/input.rs b/crates/tek/src/core/input.rs deleted file mode 100644 index e9388793..00000000 --- a/crates/tek/src/core/input.rs +++ /dev/null @@ -1,58 +0,0 @@ -use crate::*; - -/// Current input state -pub trait Input { - /// Type of input event - type Event; - /// Currently handled event - fn event (&self) -> &Self::Event; - /// Whether component should exit - fn is_done (&self) -> bool; - /// Mark component as done - fn done (&self); -} - -/// Handle input -pub trait Handle: Send + Sync { - fn handle (&mut self, context: &E::Input) -> Perhaps; -} - -impl> Handle for &mut H { - fn handle (&mut self, context: &E::Input) -> Perhaps { - (*self).handle(context) - } -} - -impl> Handle for Option { - fn handle (&mut self, context: &E::Input) -> Perhaps { - if let Some(ref mut handle) = self { - handle.handle(context) - } else { - Ok(None) - } - } -} - -impl Handle for Mutex where H: Handle { - fn handle (&mut self, context: &E::Input) -> Perhaps { - self.lock().unwrap().handle(context) - } -} - -impl Handle for Arc> where H: Handle { - fn handle (&mut self, context: &E::Input) -> Perhaps { - self.lock().unwrap().handle(context) - } -} - -impl Handle for RwLock where H: Handle { - fn handle (&mut self, context: &E::Input) -> Perhaps { - self.write().unwrap().handle(context) - } -} - -impl Handle for Arc> where H: Handle { - fn handle (&mut self, context: &E::Input) -> Perhaps { - self.write().unwrap().handle(context) - } -} diff --git a/crates/tek/src/core/output.rs b/crates/tek/src/core/output.rs deleted file mode 100644 index 11871962..00000000 --- a/crates/tek/src/core/output.rs +++ /dev/null @@ -1,161 +0,0 @@ -use crate::*; - -/// Rendering target -pub trait Output { - /// Current output area - fn area (&self) -> E::Area; - /// Mutable pointer to area - fn area_mut (&mut self) -> &mut E::Area; - /// Render widget in area - fn render_in (&mut self, area: E::Area, widget: &dyn Render) -> Usually<()>; -} - -/// Cast to dynamic pointer -pub fn widget > (w: &T) -> &dyn Render { - w as &dyn Render -} - -/// A [Render] that contains other [Render]s -pub trait Content: Send + Sync { - fn content (&self) -> impl Render; -} - -//impl> Render for &C { - //fn min_size (&self, to: E::Size) -> Perhaps { - //self.content().min_size(to) - //} - //fn render (&self, to: &mut E::Output) -> Usually<()> { - //match self.min_size(to.area().wh().into())? { - //Some(wh) => to.render_in(to.area().clip(wh).into(), &self.content()), - //None => Ok(()) - //} - //} -//} - -/* - -/// Every struct that has [Content] is a renderable [Render]. -impl> Render for C { - fn min_size (&self, to: E::Size) -> Perhaps { - self.content().min_size(to) - } - fn render (&self, to: &mut E::Output) -> Usually<()> { - match self.min_size(to.area().wh().into())? { - Some(wh) => to.render_in(to.area().clip(wh).into(), &self.content()), - None => Ok(()) - } - } -} -*/ - -/// A renderable component -pub trait Render: Send + Sync { - /// Minimum size to use - fn min_size (&self, to: E::Size) -> Perhaps { - Ok(Some(to)) - } - /// Draw to output render target - fn render (&self, to: &mut E::Output) -> Usually<()>; -} - -impl> Render for &R { - fn min_size (&self, to: E::Size) -> Perhaps { - (*self).min_size(to) - } - fn render (&self, to: &mut E::Output) -> Usually<()> { - (*self).render(to) - } -} - -impl Render for &dyn Render { - fn min_size (&self, to: E::Size) -> Perhaps { - (*self).min_size(to) - } - fn render (&self, to: &mut E::Output) -> Usually<()> { - (*self).render(to) - } -} - -//impl Render for &mut dyn Render { - //fn min_size (&self, to: E::Size) -> Perhaps { - //(*self).min_size(to) - //} - //fn render (&self, to: &mut E::Output) -> Usually<()> { - //(*self).render(to) - //} -//} - -impl<'a, E: Engine> Render for Box + 'a> { - fn min_size (&self, to: E::Size) -> Perhaps { - (**self).min_size(to) - } - fn render (&self, to: &mut E::Output) -> Usually<()> { - (**self).render(to) - } -} - -impl> Render for Arc { - fn min_size (&self, to: E::Size) -> Perhaps { - self.as_ref().min_size(to) - } - fn render (&self, to: &mut E::Output) -> Usually<()> { - self.as_ref().render(to) - } -} - -impl> Render for Mutex { - fn min_size (&self, to: E::Size) -> Perhaps { - self.lock().unwrap().min_size(to) - } - fn render (&self, to: &mut E::Output) -> Usually<()> { - self.lock().unwrap().render(to) - } -} - -impl> Render for RwLock { - fn min_size (&self, to: E::Size) -> Perhaps { - self.read().unwrap().min_size(to) - } - fn render (&self, to: &mut E::Output) -> Usually<()> { - self.read().unwrap().render(to) - } -} - -impl> Render for Option { - fn min_size (&self, to: E::Size) -> Perhaps { - Ok(self.as_ref().map(|widget|widget.min_size(to)).transpose()?.flatten()) - } - fn render (&self, to: &mut E::Output) -> Usually<()> { - self.as_ref().map(|widget|widget.render(to)).unwrap_or(Ok(())) - } -} - -/// A custom [Render] defined by passing layout and render closures in place. -pub struct Widget< - E: Engine, - L: Send + Sync + Fn(E::Size)->Perhaps, - R: Send + Sync + Fn(&mut E::Output)->Usually<()> ->(L, R, PhantomData); - -impl< - E: Engine, - L: Send + Sync + Fn(E::Size)->Perhaps, - R: Send + Sync + Fn(&mut E::Output)->Usually<()> -> Widget { - pub fn new (layout: L, render: R) -> Self { - Self(layout, render, Default::default()) - } -} - -impl< - E: Engine, - L: Send + Sync + Fn(E::Size)->Perhaps, - R: Send + Sync + Fn(&mut E::Output)->Usually<()> -> Render for Widget { - fn min_size (&self, to: E::Size) -> Perhaps { - self.0(to) - } - fn render (&self, to: &mut E::Output) -> Usually<()> { - self.1(to) - } -} diff --git a/crates/tek/src/core/pitch.rs b/crates/tek/src/core/pitch.rs deleted file mode 100644 index 6a2ac714..00000000 --- a/crates/tek/src/core/pitch.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::*; -use midly::num::u7; - -pub fn to_note_name (n: usize) -> &'static str { - if n > 127 { - panic!("to_note_name({n}): must be 0-127"); - } - MIDI_NOTE_NAMES[n] -} - -pub const MIDI_NOTE_NAMES: [&'static str;128] = [ - "C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0", - "C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1", - "C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2", - "C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3", - "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4", - "C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5", - "C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6", - "C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7", - "C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8", - "C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9", - "C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10", -]; diff --git a/crates/tek/src/core/space.rs b/crates/tek/src/core/space.rs deleted file mode 100644 index a83f78c7..00000000 --- a/crates/tek/src/core/space.rs +++ /dev/null @@ -1,157 +0,0 @@ -use crate::*; - -/// Standard numeric type. -pub trait Coordinate: Send + Sync + Copy - + Add - + Sub - + Mul - + Div - + Ord + PartialEq + Eq - + Debug + Display + Default - + From + Into - + Into - + Into -{ - fn minus (self, other: Self) -> Self { - if self >= other { - self - other - } else { - 0.into() - } - } - fn ZERO () -> Self { - 0.into() - } -} - -impl Coordinate for T where T: Send + Sync + Copy - + Add - + Sub - + Mul - + Div - + Ord + PartialEq + Eq - + Debug + Display + Default - + From + Into - + Into - + Into -{} - -// TODO: return impl Point and impl Size instead of [N;x] -// to disambiguate between usage of 2-"tuple"s - -pub trait Size { - fn x (&self) -> N; - fn y (&self) -> N; - #[inline] fn w (&self) -> N { self.x() } - #[inline] fn h (&self) -> N { self.y() } - #[inline] fn wh (&self) -> [N;2] { [self.x(), self.y()] } - #[inline] fn clip_w (&self, w: N) -> [N;2] { [self.w().min(w.into()), self.h()] } - #[inline] fn clip_h (&self, h: N) -> [N;2] { [self.w(), self.h().min(h.into())] } - #[inline] fn expect_min (&self, w: N, h: N) -> Usually<&Self> { - if self.w() < w || self.h() < h { - Err(format!("min {w}x{h}").into()) - } else { - Ok(self) - } - } -} -impl Size for (N, N) { - fn x (&self) -> N { self.0 } - fn y (&self) -> N { self.1 } -} -impl Size for [N;2] { - fn x (&self) -> N { self[0] } - fn y (&self) -> N { self[1] } -} - -pub trait Area: Copy { - fn x (&self) -> N; - fn y (&self) -> N; - fn w (&self) -> N; - fn h (&self) -> N; - fn x2 (&self) -> N { self.x() + self.w() } - fn y2 (&self) -> N { self.y() + self.h() } - #[inline] fn wh (&self) -> [N;2] { [self.w(), self.h()] } - #[inline] fn xywh (&self) -> [N;4] { [self.x(), self.y(), self.w(), self.h()] } - #[inline] fn lrtb (&self) -> [N;4] { [self.x(), self.x2(), self.y(), self.y2()] } - #[inline] fn push_x (&self, x: N) -> [N;4] { [self.x() + x, self.y(), self.w(), self.h()] } - #[inline] fn push_y (&self, y: N) -> [N;4] { [self.x(), self.y() + y, self.w(), self.h()] } - #[inline] fn shrink_x (&self, x: N) -> [N;4] { [self.x(), self.y(), self.w() - x, self.h()] } - #[inline] fn shrink_y (&self, y: N) -> [N;4] { [self.x(), self.y(), self.w(), self.h() - y] } - #[inline] fn set_w (&self, w: N) -> [N;4] { [self.x(), self.y(), w, self.h()] } - #[inline] fn set_h (&self, h: N) -> [N;4] { [self.x(), self.y(), self.w(), h] } - #[inline] fn clip_h (&self, h: N) -> [N;4] { - [self.x(), self.y(), self.w(), self.h().min(h.into())] - } - #[inline] fn clip_w (&self, w: N) -> [N;4] { - [self.x(), self.y(), self.w().min(w.into()), self.h()] - } - #[inline] fn clip (&self, wh: impl Size) -> [N;4] { - [self.x(), self.y(), wh.w(), wh.h()] - } - #[inline] fn expect_min (&self, w: N, h: N) -> Usually<&Self> { - if self.w() < w || self.h() < h { - Err(format!("min {w}x{h}").into()) - } else { - Ok(self) - } - } - #[inline] fn split_fixed (&self, direction: Direction, a: N) -> ([N;4],[N;4]) { - match direction { - Direction::Up => ( - [self.x(), (self.y()+self.h()).minus(a), self.w(), a], - [self.x(), self.y(), self.w(), self.h().minus(a)], - ), - Direction::Down => ( - [self.x(), self.y(), self.w(), a], - [self.x(), self.y() + a, self.w(), self.h().minus(a)], - ), - Direction::Right => ( - [self.x(), self.y(), a, self.h()], - [self.x() + a, self.y(), self.w().minus(a), self.h()], - ), - _ => todo!(), - } - } -} - -impl Area for (N, N, N, N) { - #[inline] fn x (&self) -> N { self.0 } - #[inline] fn y (&self) -> N { self.1 } - #[inline] fn w (&self) -> N { self.2 } - #[inline] fn h (&self) -> N { self.3 } -} - -impl Area for [N;4] { - #[inline] fn x (&self) -> N { self[0] } - #[inline] fn y (&self) -> N { self[1] } - #[inline] fn w (&self) -> N { self[2] } - #[inline] fn h (&self) -> N { self[3] } -} - -#[derive(Copy, Clone, PartialEq)] -pub enum Direction { Up, Down, Left, Right, } -impl Direction { - pub fn is_up (&self) -> bool { match self { Self::Up => true, _ => false } } - pub fn is_down (&self) -> bool { match self { Self::Down => true, _ => false } } - pub fn is_left (&self) -> bool { match self { Self::Left => true, _ => false } } - pub fn is_right (&self) -> bool { match self { Self::Right => true, _ => false } } - /// Return next direction clockwise - pub fn cw (&self) -> Self { - match self { - Self::Up => Self::Right, - Self::Down => Self::Left, - Self::Left => Self::Up, - Self::Right => Self::Down, - } - } - /// Return next direction counterclockwise - pub fn ccw (&self) -> Self { - match self { - Self::Up => Self::Left, - Self::Down => Self::Right, - Self::Left => Self::Down, - Self::Right => Self::Up, - } - } -} diff --git a/crates/tek/src/core/time.rs b/crates/tek/src/core/time.rs deleted file mode 100644 index 536e403b..00000000 --- a/crates/tek/src/core/time.rs +++ /dev/null @@ -1,450 +0,0 @@ -use crate::*; -use std::iter::Iterator; - -pub const DEFAULT_PPQ: f64 = 96.0; -/// FIXME: remove this and use PPQ from timebase everywhere: -pub const PPQ: usize = 96; - -/// A unit of time, represented as an atomic 64-bit float. -/// -/// According to https://stackoverflow.com/a/873367, as per IEEE754, -/// every integer between 1 and 2^53 can be represented exactly. -/// This should mean that, even at 192kHz sampling rate, over 1 year of audio -/// can be clocked in microseconds with f64 without losing precision. -pub trait TimeUnit { - /// Returns current value - fn get (&self) -> f64; - /// Sets new value, returns old - fn set (&self, value: f64) -> f64; -} -/// Implement arithmetic for a unit of time -macro_rules! impl_op { - ($T:ident, $Op:ident, $method:ident, |$a:ident,$b:ident|{$impl:expr}) => { - impl $Op for $T { - type Output = Self; #[inline] fn $method (self, other: Self) -> Self::Output { - let $a = self.get(); let $b = other.get(); Self($impl.into()) - } - } - impl $Op for $T { - type Output = Self; #[inline] fn $method (self, other: usize) -> Self::Output { - let $a = self.get(); let $b = other as f64; Self($impl.into()) - } - } - impl $Op for $T { - type Output = Self; #[inline] fn $method (self, other: f64) -> Self::Output { - let $a = self.get(); let $b = other; Self($impl.into()) - } - } - } -} -/// Define and implement a unit of time -macro_rules! impl_time_unit { - ($T:ident) => { - impl TimeUnit for $T { - fn get (&self) -> f64 { self.0.load(Ordering::Relaxed) } - fn set (&self, value: f64) -> f64 { - let old = self.get(); - self.0.store(value, Ordering::Relaxed); - old - } - } - impl_op!($T, Add, add, |a, b|{a + b}); - impl_op!($T, Sub, sub, |a, b|{a - b}); - impl_op!($T, Mul, mul, |a, b|{a * b}); - impl_op!($T, Div, div, |a, b|{a / b}); - impl_op!($T, Rem, rem, |a, b|{a % b}); - impl From for $T { fn from (value: f64) -> Self { Self(value.into()) } } - impl From for $T { fn from (value: usize) -> Self { Self((value as f64).into()) } } - impl Into for $T { fn into (self) -> f64 { self.get() } } - impl Into for $T { fn into (self) -> usize { self.get() as usize } } - impl Into for &$T { fn into (self) -> f64 { self.get() } } - impl Into for &$T { fn into (self) -> usize { self.get() as usize } } - impl Clone for $T { fn clone (&self) -> Self { Self(self.get().into()) } } - } -} - -/// Audio sample rate in Hz (samples per second) -#[derive(Debug, Default)] pub struct SampleRate(AtomicF64); -impl_time_unit!(SampleRate); -impl SampleRate { - /// Return the duration of a sample in microseconds (floating) - #[inline] pub fn usec_per_sample (&self) -> f64 { - 1_000_000f64 / self.get() - } - /// Return the duration of a sample in microseconds (floating) - #[inline] pub fn sample_per_usec (&self) -> f64 { - self.get() / 1_000_000f64 - } - /// Convert a number of samples to microseconds (floating) - #[inline] pub fn samples_to_usec (&self, samples: f64) -> f64 { - self.usec_per_sample() * samples - } - /// Convert a number of microseconds to samples (floating) - #[inline] pub fn usecs_to_sample (&self, usecs: f64) -> f64 { - self.sample_per_usec() * usecs - } -} - -/// Tempo in beats per minute -#[derive(Debug, Default)] pub struct BeatsPerMinute(AtomicF64); -impl_time_unit!(BeatsPerMinute); - -/// MIDI resolution in PPQ (pulses per quarter note) -#[derive(Debug, Default)] pub struct PulsesPerQuaver(AtomicF64); -impl_time_unit!(PulsesPerQuaver); - -/// Timestamp in microseconds -#[derive(Debug, Default)] pub struct Microsecond(AtomicF64); -impl_time_unit!(Microsecond); -impl Microsecond { - #[inline] pub fn format_msu (&self) -> String { - let usecs = self.get() as usize; - let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000); - let (minutes, seconds) = (seconds / 60, seconds % 60); - format!("{minutes}:{seconds:02}:{msecs:03}") - } -} - -/// Timestamp in audio samples -#[derive(Debug, Default)] pub struct SampleCount(AtomicF64); -impl_time_unit!(SampleCount); - -/// Timestamp in MIDI pulses -#[derive(Debug, Default)] pub struct Pulse(AtomicF64); -impl_time_unit!(Pulse); - -/// Quantization setting for launching clips -#[derive(Debug, Default)] pub struct LaunchSync(AtomicF64); -impl_time_unit!(LaunchSync); -impl LaunchSync { - pub fn next (&self) -> f64 { - next_note_length(self.get() as usize) as f64 - } - pub fn prev (&self) -> f64 { - prev_note_length(self.get() as usize) as f64 - } -} - -/// Quantization setting for notes -#[derive(Debug, Default)] pub struct Quantize(AtomicF64); -impl_time_unit!(Quantize); -impl Quantize { - pub fn next (&self) -> f64 { - next_note_length(self.get() as usize) as f64 - } - pub fn prev (&self) -> f64 { - prev_note_length(self.get() as usize) as f64 - } -} - -/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat) -#[derive(Debug, Clone)] -pub struct Timebase { - /// Audio samples per second - pub sr: SampleRate, - /// MIDI beats per minute - pub bpm: BeatsPerMinute, - /// MIDI ticks per beat - pub ppq: PulsesPerQuaver, -} -impl Timebase { - /// Specify sample rate, BPM and PPQ - pub fn new ( - s: impl Into, - b: impl Into, - p: impl Into - ) -> Self { - Self { sr: s.into(), bpm: b.into(), ppq: p.into() } - } - /// Iterate over ticks between start and end. - #[inline] pub fn pulses_between_samples (&self, start: usize, end: usize) -> TicksIterator { - TicksIterator { spp: self.samples_per_pulse(), sample: start, start, end } - } - /// Return the duration fo a beat in microseconds - #[inline] pub fn usec_per_beat (&self) -> f64 { 60_000_000f64 / self.bpm.get() } - /// Return the number of beats in a second - #[inline] pub fn beat_per_second (&self) -> f64 { self.bpm.get() / 60f64 } - /// Return the number of microseconds corresponding to a note of the given duration - #[inline] pub fn note_to_usec (&self, (num, den): (f64, f64)) -> f64 { - 4.0 * self.usec_per_beat() * num / den - } - /// Return duration of a pulse in microseconds (BPM-dependent) - #[inline] pub fn pulse_per_usec (&self) -> f64 { self.ppq.get() / self.usec_per_beat() } - /// Return duration of a pulse in microseconds (BPM-dependent) - #[inline] pub fn usec_per_pulse (&self) -> f64 { self.usec_per_beat() / self.ppq.get() } - /// Return number of pulses to which a number of microseconds corresponds (BPM-dependent) - #[inline] pub fn usecs_to_pulse (&self, usec: f64) -> f64 { usec * self.pulse_per_usec() } - /// Convert a number of pulses to a sample number (SR- and BPM-dependent) - #[inline] pub fn pulses_to_usec (&self, pulse: f64) -> f64 { pulse / self.usec_per_pulse() } - /// Return number of pulses in a second (BPM-dependent) - #[inline] pub fn pulses_per_second (&self) -> f64 { self.beat_per_second() * self.ppq.get() } - /// Return fraction of a pulse to which a sample corresponds (SR- and BPM-dependent) - #[inline] pub fn pulses_per_sample (&self) -> f64 { - self.usec_per_pulse() / self.sr.usec_per_sample() - } - /// Return number of samples in a pulse (SR- and BPM-dependent) - #[inline] pub fn samples_per_pulse (&self) -> f64 { - self.sr.get() / self.pulses_per_second() - } - /// Convert a number of pulses to a sample number (SR- and BPM-dependent) - #[inline] pub fn pulses_to_sample (&self, p: f64) -> f64 { - self.pulses_per_sample() * p - } - /// Convert a number of samples to a pulse number (SR- and BPM-dependent) - #[inline] pub fn samples_to_pulse (&self, s: f64) -> f64 { - s / self.pulses_per_sample() - } - /// Return the number of samples corresponding to a note of the given duration - #[inline] pub fn note_to_samples (&self, note: (f64, f64)) -> f64 { - self.usec_to_sample(self.note_to_usec(note)) - } - /// Return the number of samples corresponding to the given number of microseconds - #[inline] pub fn usec_to_sample (&self, usec: f64) -> f64 { - usec * self.sr.get() / 1000f64 - } - /// Return the quantized position of a moment in time given a step - #[inline] pub fn quantize (&self, step: (f64, f64), time: f64) -> (f64, f64) { - let step = self.note_to_usec(step); - (time / step, time % step) - } - /// Quantize a collection of events - #[inline] pub fn quantize_into + Sized, T> ( - &self, step: (f64, f64), events: E - ) -> Vec<(f64, f64)> { - events.map(|(time, event)|(self.quantize(step, time).0, event)).collect() - } - /// Format a number of pulses into Beat.Bar.Pulse starting from 0 - #[inline] pub fn format_beats_0 (&self, pulse: f64) -> String { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; - format!("{}.{}.{pulses:02}", beats / 4, beats % 4) - } - /// Format a number of pulses into Beat.Bar starting from 0 - #[inline] pub fn format_beats_0_short (&self, pulse: f64) -> String { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let beats = if ppq > 0 { pulse / ppq } else { 0 }; - format!("{}.{}", beats / 4, beats % 4) - } - /// Format a number of pulses into Beat.Bar.Pulse starting from 1 - #[inline] pub fn format_beats_1 (&self, pulse: f64) -> String { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; - format!("{}.{}.{pulses:02}", beats / 4 + 1, beats % 4 + 1) - } - /// Format a number of pulses into Beat.Bar.Pulse starting from 1 - #[inline] pub fn format_beats_1_short (&self, pulse: f64) -> String { - let pulse = pulse as usize; - let ppq = self.ppq.get() as usize; - let beats = if ppq > 0 { pulse / ppq } else { 0 }; - format!("{}.{}", beats / 4 + 1, beats % 4 + 1) - } -} -impl Default for Timebase { - fn default () -> Self { Self::new(48000f64, 150f64, DEFAULT_PPQ) } -} - -#[derive(Debug, Clone)] -pub enum Moment2 { - None, - Zero, - Usec(Microsecond), - Sample(SampleCount), - Pulse(Pulse), -} - -/// A point in time in all time scales (microsecond, sample, MIDI pulse) -#[derive(Debug, Default, Clone)] -pub struct Moment { - pub timebase: Arc, - /// Current time in microseconds - pub usec: Microsecond, - /// Current time in audio samples - pub sample: SampleCount, - /// Current time in MIDI pulses - pub pulse: Pulse, -} -impl Moment { - pub fn zero (timebase: &Arc) -> Self { - Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() } - } - pub fn from_usec (timebase: &Arc, usec: f64) -> Self { - Self { - usec: usec.into(), - sample: timebase.sr.usecs_to_sample(usec).into(), - pulse: timebase.usecs_to_pulse(usec).into(), - timebase: timebase.clone(), - } - } - pub fn from_sample (timebase: &Arc, sample: f64) -> Self { - Self { - sample: sample.into(), - usec: timebase.sr.samples_to_usec(sample).into(), - pulse: timebase.samples_to_pulse(sample).into(), - timebase: timebase.clone(), - } - } - pub fn from_pulse (timebase: &Arc, pulse: f64) -> Self { - Self { - pulse: pulse.into(), - sample: timebase.pulses_to_sample(pulse).into(), - usec: timebase.pulses_to_usec(pulse).into(), - timebase: timebase.clone(), - } - } - #[inline] pub fn update_from_usec (&self, usec: f64) { - self.usec.set(usec); - self.pulse.set(self.timebase.usecs_to_pulse(usec)); - self.sample.set(self.timebase.sr.usecs_to_sample(usec)); - } - #[inline] pub fn update_from_sample (&self, sample: f64) { - self.usec.set(self.timebase.sr.samples_to_usec(sample)); - self.pulse.set(self.timebase.samples_to_pulse(sample)); - self.sample.set(sample); - } - #[inline] pub fn update_from_pulse (&self, pulse: f64) { - self.usec.set(self.timebase.pulses_to_usec(pulse)); - self.pulse.set(pulse); - self.sample.set(self.timebase.pulses_to_sample(pulse)); - } - #[inline] pub fn format_beat (&self) -> String { - self.timebase.format_beats_1(self.pulse.get()) - } -} -/// Iterator that emits subsequent ticks within a range. -pub struct TicksIterator { - spp: f64, - sample: usize, - start: usize, - end: usize, -} -impl Iterator for TicksIterator { - type Item = (usize, usize); - fn next (&mut self) -> Option { - loop { - if self.sample > self.end { return None } - let spp = self.spp; - let sample = self.sample as f64; - let start = self.start; - let end = self.end; - self.sample += 1; - //println!("{spp} {sample} {start} {end}"); - let jitter = sample.rem_euclid(spp); // ramps - let next_jitter = (sample + 1.0).rem_euclid(spp); - if jitter > next_jitter { // at crossing: - let time = (sample as usize) % (end as usize-start as usize); - let tick = (sample / spp) as usize; - return Some((time, tick)) - } - } - } -} - -/// (pulses, name), assuming 96 PPQ -pub const NOTE_DURATIONS: [(usize, &str);26] = [ - (1, "1/384"), - (2, "1/192"), - (3, "1/128"), - (4, "1/96"), - (6, "1/64"), - (8, "1/48"), - (12, "1/32"), - (16, "1/24"), - (24, "1/16"), - (32, "1/12"), - (48, "1/8"), - (64, "1/6"), - (96, "1/4"), - (128, "1/3"), - (192, "1/2"), - (256, "2/3"), - (384, "1/1"), - (512, "4/3"), - (576, "3/2"), - (768, "2/1"), - (1152, "3/1"), - (1536, "4/1"), - (2304, "6/1"), - (3072, "8/1"), - (3456, "9/1"), - (6144, "16/1"), -]; -/// Returns the next shorter length -pub fn prev_note_length (pulses: usize) -> usize { - for i in 1..=16 { let length = NOTE_DURATIONS[16-i].0; if length < pulses { return length } } - pulses -} -/// Returns the next longer length -pub fn next_note_length (pulses: usize) -> usize { - for (length, _) in &NOTE_DURATIONS { if *length > pulses { return *length } } - pulses -} -pub fn pulses_to_name (pulses: usize) -> &'static str { - for (length, name) in &NOTE_DURATIONS { if *length == pulses { return name } } - "" -} - -/// Performance counter -pub struct PerfModel { - pub enabled: bool, - clock: quanta::Clock, - // In nanoseconds - used: AtomicF64, - // In microseconds - period: AtomicF64, -} - -impl Default for PerfModel { - fn default () -> Self { - Self { - enabled: true, - clock: quanta::Clock::new(), - used: Default::default(), - period: Default::default(), - } - } -} - -impl PerfModel { - pub fn get_t0 (&self) -> Option { - if self.enabled { - Some(self.clock.raw()) - } else { - None - } - } - pub fn update (&self, t0: Option, scope: &jack::ProcessScope) { - if let Some(t0) = t0 { - let t1 = self.clock.raw(); - self.used.store( - self.clock.delta_as_nanos(t0, t1) as f64, - Ordering::Relaxed, - ); - self.period.store( - scope.cycle_times().unwrap().period_usecs as f64, - Ordering::Relaxed, - ); - } - } - pub fn percentage (&self) -> Option { - let period = self.period.load(Ordering::Relaxed) * 1000.0; - if period > 0.0 { - let used = self.used.load(Ordering::Relaxed); - Some(100.0 * used / period) - } else { - None - } - } -} - -//#[cfg(test)] -//mod test { - //use super::*; - //#[test] - //fn test_samples_to_ticks () { - //let ticks = Ticks(12.3).between_samples(0, 100).collect::>(); - //println!("{ticks:?}"); - //} -//} diff --git a/crates/tek/src/core/tui.rs b/crates/tek/src/core/tui.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/crates/tek/src/layout.rs b/crates/tek/src/layout.rs deleted file mode 100644 index 168e2022..00000000 --- a/crates/tek/src/layout.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::*; - -mod align; pub(crate) use align::*; -mod bsp; pub(crate) use bsp::*; -mod cond; pub(crate) use cond::*; -mod debug; pub(crate) use debug::*; -mod fill; pub(crate) use fill::*; -mod fixed; pub(crate) use fixed::*; -mod inset_outset; pub(crate) use inset_outset::*; -mod layers; pub(crate) use layers::*; -mod measure; pub(crate) use measure::*; -mod min_max; pub(crate) use min_max::*; -mod push_pull; pub(crate) use push_pull::*; -mod scroll; pub(crate) use scroll::*; -mod shrink_grow; pub(crate) use shrink_grow::*; -mod split; pub(crate) use split::*; -mod stack; pub(crate) use stack::*; diff --git a/crates/tek/src/layout/align.rs b/crates/tek/src/layout/align.rs deleted file mode 100644 index 48c64874..00000000 --- a/crates/tek/src/layout/align.rs +++ /dev/null @@ -1,102 +0,0 @@ -use crate::*; - -impl LayoutAlign for E {} - -pub trait LayoutAlign { - fn center_x > (w: W) -> Align { Align::X(w) } - fn center_y > (w: W) -> Align { Align::Y(w) } - fn center > (w: W) -> Align { Align::Center(w) } - fn at_n > (w: W) -> Align { Align::N(w) } - fn at_s > (w: W) -> Align { Align::S(w) } - fn at_e > (w: W) -> Align { Align::E(w) } - fn at_w > (w: W) -> Align { Align::W(w) } - fn at_nw > (w: W) -> Align { Align::NW(w) } - fn at_sw > (w: W) -> Align { Align::SW(w) } - fn at_ne > (w: W) -> Align { Align::NE(w) } - fn at_se > (w: W) -> Align { Align::SE(w) } -} - -/// Override X and Y coordinates, aligning to corner, side, or center of area -pub enum Align { - /// Draw at center of container - Center(L), - /// Draw at center of X axis - X(L), - /// Draw at center of Y axis - Y(L), - /// Draw at upper left corner of contaier - NW(L), - /// Draw at center of upper edge of container - N(L), - /// Draw at right left corner of contaier - NE(L), - /// Draw at center of left edge of container - W(L), - /// Draw at center of right edge of container - E(L), - /// Draw at lower left corner of container - SW(L), - /// Draw at center of lower edge of container - S(L), - /// Draw at lower right edge of container - SE(L) -} - -impl Align { - pub fn inner (&self) -> &T { - match self { - Self::Center(inner) => inner, - Self::X(inner) => inner, - Self::Y(inner) => inner, - Self::NW(inner) => inner, - Self::N(inner) => inner, - Self::NE(inner) => inner, - Self::W(inner) => inner, - Self::E(inner) => inner, - Self::SW(inner) => inner, - Self::S(inner) => inner, - Self::SE(inner) => inner, - } - } -} - -fn align + From<[N;4]>> (align: &Align, outer: R, inner: R) -> Option { - if outer.w() < inner.w() || outer.h() < inner.h() { - None - } else { - let [ox, oy, ow, oh] = outer.xywh(); - let [ix, iy, iw, ih] = inner.xywh(); - Some(match align { - Align::Center(_) => [ox + (ow - iw) / 2.into(), oy + (oh - ih) / 2.into(), iw, ih,].into(), - Align::X(_) => [ox + (ow - iw) / 2.into(), iy, iw, ih,].into(), - Align::Y(_) => [ix, oy + (oh - ih) / 2.into(), iw, ih,].into(), - Align::NW(_) => [ox, oy, iw, ih,].into(), - Align::N(_) => [ox + (ow - iw) / 2.into(), oy, iw, ih,].into(), - Align::NE(_) => [ox + ow - iw, oy, iw, ih,].into(), - Align::W(_) => [ox, oy + (oh - ih) / 2.into(), iw, ih,].into(), - Align::E(_) => [ox + ow - iw, oy + (oh - ih) / 2.into(), iw, ih,].into(), - Align::SW(_) => [ox, oy + oh - ih, iw, ih,].into(), - Align::S(_) => [ox + (ow - iw) / 2.into(), oy + oh - ih, iw, ih,].into(), - Align::SE(_) => [ox + ow - iw, oy + oh - ih, iw, ih,].into(), - }) - } -} - -impl> Render for Align { - fn min_size (&self, outer_area: E::Size) -> Perhaps { - self.inner().min_size(outer_area) - } - fn render (&self, to: &mut E::Output) -> Usually<()> { - let outer_area = to.area(); - Ok(if let Some(inner_size) = self.min_size(outer_area.wh().into())? { - let inner_area = outer_area.clip(inner_size); - if let Some(aligned) = align(&self, outer_area.into(), inner_area.into()) { - to.render_in(aligned, self.inner())? - } else { - () - } - } else { - () - }) - } -} diff --git a/crates/tek/src/layout/bsp.rs b/crates/tek/src/layout/bsp.rs deleted file mode 100644 index a0c3c07c..00000000 --- a/crates/tek/src/layout/bsp.rs +++ /dev/null @@ -1,105 +0,0 @@ -use crate::*; - -impl LayoutBspStatic for E {} - -pub trait LayoutBspStatic: { - fn over , B: Render> (a: A, b: B) -> Over { - Over(Default::default(), a, b) - } - fn under , B: Render> (a: A, b: B) -> Under { - Under(Default::default(), a, b) - } - fn to_north , B: Render> (a: A, b: B) -> ToNorth { - ToNorth(None, a, b) - } - fn to_south , B: Render> (a: A, b: B) -> ToSouth { - ToSouth(None, a, b) - } - fn to_east , B: Render> (a: A, b: B) -> ToEast { - ToEast(None, a, b) - } - fn to_west , B: Render> (a: A, b: B) -> ToWest { - ToWest(None, a, b) - } -} - -pub trait LayoutBspFixedStatic: { - fn to_north , B: Render> (n: E::Unit, a: A, b: B) -> ToNorth { - ToNorth(Some(n), a, b) - } - fn to_south , B: Render> (n: E::Unit, a: A, b: B) -> ToSouth { - ToSouth(Some(n), a, b) - } - fn to_east , B: Render> (n: E::Unit, a: A, b: B) -> ToEast { - ToEast(Some(n), a, b) - } - fn to_west , B: Render> (n: E::Unit, a: A, b: B) -> ToWest { - ToWest(Some(n), a, b) - } -} - -pub struct Over, B: Render>(PhantomData, A, B); - -pub struct Under, B: Render>(PhantomData, A, B); - -pub struct ToNorth, B: Render>(Option, A, B); - -pub struct ToSouth, B: Render>(Option, A, B); - -pub struct ToEast(Option, A, B); - -pub struct ToWest, B: Render>(Option, A, B); - -impl, B: Render> Render for Over { - fn min_size (&self, _: E::Size) -> Perhaps { - todo!(); - } - fn render (&self, _: &mut E::Output) -> Usually<()> { - Ok(()) - } -} - -impl, B: Render> Render for Under { - fn min_size (&self, _: E::Size) -> Perhaps { - todo!(); - } - fn render (&self, _: &mut E::Output) -> Usually<()> { - Ok(()) - } -} - -impl, B: Render> Render for ToNorth { - fn min_size (&self, _: E::Size) -> Perhaps { - todo!(); - } - fn render (&self, _: &mut E::Output) -> Usually<()> { - Ok(()) - } -} - -impl, B: Render> Render for ToSouth { - fn min_size (&self, _: E::Size) -> Perhaps { - todo!(); - } - fn render (&self, _: &mut E::Output) -> Usually<()> { - Ok(()) - } -} - -impl, B: Render> Render for ToWest { - fn min_size (&self, _: E::Size) -> Perhaps { - todo!(); - } - fn render (&self, _: &mut E::Output) -> Usually<()> { - Ok(()) - } -} - -impl, B: Render> Render for ToEast { - fn min_size (&self, _: E::Size) -> Perhaps { - todo!(); - } - fn render (&self, _: &mut E::Output) -> Usually<()> { - Ok(()) - } -} diff --git a/crates/tek/src/layout/collect.rs b/crates/tek/src/layout/collect.rs deleted file mode 100644 index 141992f1..00000000 --- a/crates/tek/src/layout/collect.rs +++ /dev/null @@ -1,79 +0,0 @@ -use crate::*; - -pub enum Collect<'a, E: Engine, const N: usize> { - Callback(CallbackCollection<'a, E>), - //Iterator(IteratorCollection<'a, E>), - Array(ArrayCollection<'a, E, N>), - Slice(SliceCollection<'a, E>), -} - -impl<'a, E: Engine, const N: usize> Collect<'a, E, N> { - pub fn iter (&'a self) -> CollectIterator<'a, E, N> { - CollectIterator(0, &self) - } -} - -impl<'a, E: Engine, const N: usize> From> for Collect<'a, E, N> { - fn from (callback: CallbackCollection<'a, E>) -> Self { - Self::Callback(callback) - } -} - -impl<'a, E: Engine, const N: usize> From> for Collect<'a, E, N> { - fn from (slice: SliceCollection<'a, E>) -> Self { - Self::Slice(slice) - } -} - -impl<'a, E: Engine, const N: usize> From> for Collect<'a, E, N>{ - fn from (array: ArrayCollection<'a, E, N>) -> Self { - Self::Array(array) - } -} - -type CallbackCollection<'a, E> = - &'a dyn Fn(&'a mut dyn FnMut(&dyn Render)->Usually<()>); - -//type IteratorCollection<'a, E> = - //&'a mut dyn Iterator>; - -type SliceCollection<'a, E> = - &'a [&'a dyn Render]; - -type ArrayCollection<'a, E, const N: usize> = - [&'a dyn Render; N]; - -pub struct CollectIterator<'a, E: Engine, const N: usize>(usize, &'a Collect<'a, E, N>); - -impl<'a, E: Engine, const N: usize> Iterator for CollectIterator<'a, E, N> { - type Item = &'a dyn Render; - fn next (&mut self) -> Option { - match self.1 { - Collect::Callback(callback) => { - todo!() - }, - //Collection::Iterator(iterator) => { - //iterator.next() - //}, - Collect::Array(array) => { - if let Some(item) = array.get(self.0) { - self.0 += 1; - //Some(item) - None - } else { - None - } - } - Collect::Slice(slice) => { - if let Some(item) = slice.get(self.0) { - self.0 += 1; - //Some(item) - None - } else { - None - } - } - } - } -} - diff --git a/crates/tek/src/layout/cond.rs b/crates/tek/src/layout/cond.rs deleted file mode 100644 index c4a6314d..00000000 --- a/crates/tek/src/layout/cond.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::*; - -impl> LayoutCond for R {} - -pub trait LayoutCond: Render + Sized { - fn when (self, cond: bool) -> If { - If(Default::default(), cond, self) - } - fn or > (self, cond: bool, other: B) -> Either { - Either(Default::default(), cond, self, other) - } -} - -impl LayoutCondStatic for E {} - -pub trait LayoutCondStatic { - fn either , B: Render> ( - condition: bool, - a: A, - b: B, - ) -> Either { - Either(Default::default(), condition, a, b) - } -} - -/// Render widget if predicate is true -pub struct If>(PhantomData, bool, A); - -impl> Render for If { - fn min_size (&self, to: E::Size) -> Perhaps { - if self.1 { - return self.2.min_size(to) - } - Ok(None) - } - fn render (&self, to: &mut E::Output) -> Usually<()> { - if self.1 { - return self.2.render(to) - } - Ok(()) - } -} - -/// Render widget A if predicate is true, otherwise widget B -pub struct Either, B: Render>( - PhantomData, - bool, - A, - B, -); - -impl, B: Render> Render for Either { - fn min_size (&self, to: E::Size) -> Perhaps { - if self.1 { - return self.2.min_size(to) - } else { - return self.3.min_size(to) - } - } - fn render (&self, to: &mut E::Output) -> Usually<()> { - if self.1 { - return self.2.render(to) - } else { - return self.3.render(to) - } - } -} diff --git a/crates/tek/src/layout/debug.rs b/crates/tek/src/layout/debug.rs deleted file mode 100644 index 6de7cbce..00000000 --- a/crates/tek/src/layout/debug.rs +++ /dev/null @@ -1,11 +0,0 @@ -use crate::*; - -impl> LayoutDebug for W {} - -pub trait LayoutDebug: Render + Sized { - fn debug (self) -> DebugOverlay { - DebugOverlay(Default::default(), self) - } -} - -pub struct DebugOverlay>(PhantomData, pub W); diff --git a/crates/tek/src/layout/fill.rs b/crates/tek/src/layout/fill.rs deleted file mode 100644 index 28ba7066..00000000 --- a/crates/tek/src/layout/fill.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::*; - -impl LayoutFill for E {} - -pub trait LayoutFill { - fn fill_x > (fill: W) -> Fill { - Fill::X(fill) - } - fn fill_y > (fill: W) -> Fill { - Fill::Y(fill) - } - fn fill_xy > (fill: W) -> Fill { - Fill::XY(fill) - } -} - -pub enum Fill> { - X(W), - Y(W), - XY(W), - _Unused(PhantomData) -} - -impl> Fill { - fn inner (&self) -> &W { - match self { - Self::X(inner) => &inner, - Self::Y(inner) => &inner, - Self::XY(inner) => &inner, - _ => unreachable!(), - } - } -} - -impl> Render for Fill { - fn min_size (&self, to: E::Size) -> Perhaps { - let area = self.inner().min_size(to.into())?; - if let Some(area) = area { - Ok(Some(match self { - Self::X(_) => [to.w().into(), area.h()], - Self::Y(_) => [area.w(), to.h().into()], - Self::XY(_) => [to.w().into(), to.h().into()], - _ => unreachable!(), - }.into())) - } else { - Ok(None) - } - } - fn render (&self, to: &mut E::Output) -> Usually<()> { - self.inner().render(to) - } -} diff --git a/crates/tek/src/layout/fixed.rs b/crates/tek/src/layout/fixed.rs deleted file mode 100644 index a73c54ba..00000000 --- a/crates/tek/src/layout/fixed.rs +++ /dev/null @@ -1,58 +0,0 @@ -use crate::*; - -impl LayoutFixed for E {} - -pub trait LayoutFixed { - fn fixed_x > (x: E::Unit, w: W) -> Fixed { - Fixed::X(x, w) - } - fn fixed_y > (y: E::Unit, w: W) -> Fixed { - Fixed::Y(y, w) - } - fn fixed_xy > (x: E::Unit, y: E::Unit, w: W) -> Fixed { - Fixed::XY(x, y, w) - } -} - -/// Enforce fixed size of drawing area -pub enum Fixed { - _Unused(PhantomData), - /// Enforce fixed width - X(E::Unit, T), - /// Enforce fixed height - Y(E::Unit, T), - /// Enforce fixed width and height - XY(E::Unit, E::Unit, T), -} - -impl Fixed { - pub fn inner (&self) -> &T { - match self { - Self::X(_, i) => i, - Self::Y(_, i) => i, - Self::XY(_, _, i) => i, - _ => unreachable!(), - } - } -} -impl> Render for Fixed { - fn min_size (&self, to: E::Size) -> Perhaps { - Ok(match self { - Self::X(w, _) => - if to.w() >= *w { Some([*w, to.h()].into()) } else { None }, - Self::Y(h, _) => - if to.h() >= *h { Some([to.w(), *h].into()) } else { None }, - Self::XY(w, h, _) - => if to.w() >= *w && to.h() >= *h { Some([*w, *h].into()) } else { None }, - _ => unreachable!(), - }) - } - fn render (&self, to: &mut E::Output) -> Usually<()> { - // 🡘 🡙 ←🡙→ - if let Some(size) = self.min_size(to.area().wh().into())? { - to.render_in(to.area().clip(size).into(), self.inner()) - } else { - Ok(()) - } - } -} diff --git a/crates/tek/src/layout/inset_outset.rs b/crates/tek/src/layout/inset_outset.rs deleted file mode 100644 index 2985aecf..00000000 --- a/crates/tek/src/layout/inset_outset.rs +++ /dev/null @@ -1,92 +0,0 @@ -use crate::*; - -impl + LayoutShrinkGrow> LayoutInsetOutset for E {} - -pub trait LayoutInsetOutset: LayoutPushPull + LayoutShrinkGrow { - fn inset_x > (x: E::Unit, w: W) -> Inset { - Inset::X(x, w) - } - fn inset_y > (y: E::Unit, w: W) -> Inset { - Inset::Y(y, w) - } - fn inset_xy > (x: E::Unit, y: E::Unit, w: W) -> Inset { - Inset::XY(x, y, w) - } - fn outset_x > (x: E::Unit, w: W) -> Outset { - Outset::X(x, w) - } - fn outset_y > (y: E::Unit, w: W) -> Outset { - Outset::Y(y, w) - } - fn outset_xy > (x: E::Unit, y: E::Unit, w: W) -> Outset { - Outset::XY(x, y, w) - } -} - -/// Shrink from each side -pub enum Inset { - /// Decrease width - X(E::Unit, T), - /// Decrease height - Y(E::Unit, T), - /// Decrease width and height - XY(E::Unit, E::Unit, T), -} - -impl> Inset { - pub fn inner (&self) -> &T { - match self { - Self::X(_, i) => i, - Self::Y(_, i) => i, - Self::XY(_, _, i) => i, - } - } -} - -impl> Render for Inset { - fn render (&self, to: &mut E::Output) -> Usually<()> { - match self { - Self::X(x, inner) => E::push_x(*x, E::shrink_x(*x, inner)), - Self::Y(y, inner) => E::push_y(*y, E::shrink_y(*y, inner)), - Self::XY(x, y, inner) => E::push_xy(*x, *y, E::shrink_xy(*x, *y, inner)), - }.render(to) - } -} - -/// Grow on each side -pub enum Outset> { - /// Increase width - X(E::Unit, T), - /// Increase height - Y(E::Unit, T), - /// Increase width and height - XY(E::Unit, E::Unit, T), -} - - -impl> Outset { - pub fn inner (&self) -> &T { - match self { - Self::X(_, i) => i, - Self::Y(_, i) => i, - Self::XY(_, _, i) => i, - } - } -} - -impl> Render for Outset { - fn min_size (&self, to: E::Size) -> Perhaps { - match *self { - Self::X(x, ref inner) => E::grow_x(x + x, inner), - Self::Y(y, ref inner) => E::grow_y(y + y, inner), - Self::XY(x, y, ref inner) => E::grow_xy(x + x, y + y, inner), - }.min_size(to) - } - fn render (&self, to: &mut E::Output) -> Usually<()> { - match *self { - Self::X(x, ref inner) => E::push_x(x, inner), - Self::Y(y, ref inner) => E::push_y(y, inner), - Self::XY(x, y, ref inner) => E::push_xy(x, y, inner), - }.render(to) - } -} diff --git a/crates/tek/src/layout/layers.rs b/crates/tek/src/layout/layers.rs deleted file mode 100644 index 353198ab..00000000 --- a/crates/tek/src/layout/layers.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::*; - -#[macro_export] macro_rules! lay { - ([$($expr:expr),* $(,)?]) => { - Layers::new(move|add|{ $(add(&$expr)?;)* Ok(()) }) - }; - (![$($expr:expr),* $(,)?]) => { - Layers::new(|add|{ $(add(&$expr)?;)* Ok(()) }) - }; - ($expr:expr) => { - Layers::new($expr) - }; -} - -pub struct Layers< - E: Engine, - F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render)->Usually<()>)->Usually<()> ->(pub F, PhantomData); - -impl< - E: Engine, - F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render)->Usually<()>)->Usually<()> -> Layers { - #[inline] - pub fn new (build: F) -> Self { - Self(build, Default::default()) - } -} - -impl Render for Layers -where - F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render)->Usually<()>)->Usually<()> -{ - fn min_size (&self, area: E::Size) -> Perhaps { - let mut w: E::Unit = 0.into(); - let mut h: E::Unit = 0.into(); - (self.0)(&mut |layer| { - if let Some(layer_area) = layer.min_size(area)? { - w = w.max(layer_area.w()); - h = h.max(layer_area.h()); - } - Ok(()) - })?; - Ok(Some([w, h].into())) - } - fn render (&self, to: &mut E::Output) -> Usually<()> { - if let Some(size) = self.min_size(to.area().wh().into())? { - (self.0)(&mut |layer|to.render_in(to.area().clip(size).into(), layer)) - } else { - Ok(()) - } - } -} diff --git a/crates/tek/src/layout/map_reduce.rs b/crates/tek/src/layout/map_reduce.rs deleted file mode 100644 index 833973b2..00000000 --- a/crates/tek/src/layout/map_reduce.rs +++ /dev/null @@ -1,33 +0,0 @@ -use crate::*; - -impl+Send+Sync, T, R: Render> LayoutMapReduce for E {} - -pub trait LayoutMapReduce+Send+Sync, T, R: Render> { - fn map R> (iterator: I, callback: F) -> Map { - Map(Default::default(), iterator, callback) - } - fn reduce , T)->R+Send+Sync> (iterator: I, callback: F) -> Reduce { - Reduce(Default::default(), iterator, callback) - } -} - -pub struct Map, R: Render, F: Fn(T)->R>( - PhantomData, - I, - F -); - -pub struct Reduce, R: Render, F: Fn(&dyn Render, T)->R>( - PhantomData<(E, R)>, - I, - F -); - -impl+Send+Sync, R: Render, F: Fn(&dyn Render, T)->R+Send+Sync> Render for Reduce { - fn min_size (&self, to: E::Size) -> Perhaps { - todo!() - } - fn render (&self, to: &mut E::Output) -> Usually<()> { - todo!() - } -} diff --git a/crates/tek/src/layout/measure.rs b/crates/tek/src/layout/measure.rs deleted file mode 100644 index aaffbc18..00000000 --- a/crates/tek/src/layout/measure.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::*; - -/// A widget that tracks its render width and height -pub struct Measure(PhantomData, AtomicUsize, AtomicUsize); - -impl Clone for Measure { - fn clone (&self) -> Self { - Self( - Default::default(), - AtomicUsize::from(self.1.load(Ordering::Relaxed)), - AtomicUsize::from(self.2.load(Ordering::Relaxed)), - ) - } -} - -impl std::fmt::Debug for Measure { - fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("Measure") - .field("width", &self.0) - .field("height", &self.1) - .finish() - } -} - -impl Measure { - pub fn w (&self) -> usize { self.1.load(Ordering::Relaxed) } - pub fn h (&self) -> usize { self.2.load(Ordering::Relaxed) } - pub fn wh (&self) -> [usize;2] { [self.w(), self.h()] } - pub fn set_w (&self, w: impl Into) { self.1.store(w.into(), Ordering::Relaxed) } - pub fn set_h (&self, h: impl Into) { self.2.store(h.into(), Ordering::Relaxed) } - pub fn set_wh (&self, w: impl Into, h: impl Into) { self.set_w(w); self.set_h(h); } - pub fn new () -> Self { Self(PhantomData::default(), 0.into(), 0.into()) } - pub fn format (&self) -> String { format!("{}x{}", self.w(), self.h()) } -} - -impl Render for Measure { - fn min_size (&self, _: E::Size) -> Perhaps { - Ok(Some([0u16.into(), 0u16.into()].into())) - } - fn render (&self, to: &mut E::Output) -> Usually<()> { - self.set_w(to.area().w()); - self.set_h(to.area().h()); - Ok(()) - } -} - diff --git a/crates/tek/src/layout/min_max.rs b/crates/tek/src/layout/min_max.rs deleted file mode 100644 index 42312bab..00000000 --- a/crates/tek/src/layout/min_max.rs +++ /dev/null @@ -1,95 +0,0 @@ -use crate::*; - -impl LayoutMinMax for E {} - -pub trait LayoutMinMax { - fn min_x > (x: E::Unit, w: W) -> Min { - Min::X(x, w) - } - fn min_y > (y: E::Unit, w: W) -> Min { - Min::Y(y, w) - } - fn min_xy > (x: E::Unit, y: E::Unit, w: W) -> Min { - Min::XY(x, y, w) - } - fn max_x > (x: E::Unit, w: W) -> Max { - Max::X(x, w) - } - fn max_y > (y: E::Unit, w: W) -> Max { - Max::Y(y, w) - } - fn max_xy > (x: E::Unit, y: E::Unit, w: W) -> Max { - Max::XY(x, y, w) - } -} - -/// Enforce minimum size of drawing area -pub enum Min> { - /// Enforce minimum width - X(E::Unit, T), - /// Enforce minimum height - Y(E::Unit, T), - /// Enforce minimum width and height - XY(E::Unit, E::Unit, T), -} - -/// Enforce maximum size of drawing area -pub enum Max> { - /// Enforce maximum width - X(E::Unit, T), - /// Enforce maximum height - Y(E::Unit, T), - /// Enforce maximum width and height - XY(E::Unit, E::Unit, T), -} - -impl> Min { - pub fn inner (&self) -> &T { - match self { - Self::X(_, i) => i, - Self::Y(_, i) => i, - Self::XY(_, _, i) => i, - } - } -} - -impl> Render for Min { - fn min_size (&self, to: E::Size) -> Perhaps { - Ok(self.inner().min_size(to)?.map(|to|match *self { - Self::X(w, _) => [to.w().max(w), to.h()], - Self::Y(h, _) => [to.w(), to.h().max(h)], - Self::XY(w, h, _) => [to.w().max(w), to.h().max(h)], - }.into())) - } - // TODO: 🡘 🡙 ←🡙→ - fn render (&self, to: &mut E::Output) -> Usually<()> { - Ok(self.min_size(to.area().wh().into())? - .map(|size|to.render_in(to.area().clip(size).into(), self.inner())) - .transpose()?.unwrap_or(())) - } -} - -impl> Max { - fn inner (&self) -> &T { - match self { - Self::X(_, i) => i, - Self::Y(_, i) => i, - Self::XY(_, _, i) => i, - } - } -} - -impl> Render for Max { - fn min_size (&self, to: E::Size) -> Perhaps { - Ok(self.inner().min_size(to)?.map(|to|match *self { - Self::X(w, _) => [to.w().min(w), to.h()], - Self::Y(h, _) => [to.w(), to.h().min(h)], - Self::XY(w, h, _) => [to.w().min(w), to.h().min(h)], - }.into())) - } - fn render (&self, to: &mut E::Output) -> Usually<()> { - Ok(self.min_size(to.area().wh().into())? - .map(|size|to.render_in(to.area().clip(size).into(), self.inner())) - .transpose()?.unwrap_or(())) - } -} diff --git a/crates/tek/src/layout/push_pull.rs b/crates/tek/src/layout/push_pull.rs deleted file mode 100644 index 2b83bd0e..00000000 --- a/crates/tek/src/layout/push_pull.rs +++ /dev/null @@ -1,129 +0,0 @@ -use crate::*; - -impl LayoutPushPull for E {} - -pub trait LayoutPushPull { - fn push_x > (x: E::Unit, w: W) -> Push { - Push::X(x, w) - } - fn push_y > (y: E::Unit, w: W) -> Push { - Push::Y(y, w) - } - fn push_xy > (x: E::Unit, y: E::Unit, w: W) -> Push { - Push::XY(x, y, w) - } - fn pull_x > (x: E::Unit, w: W) -> Pull { - Pull::X(x, w) - } - fn pull_y > (y: E::Unit, w: W) -> Pull { - Pull::Y(y, w) - } - fn pull_xy > (x: E::Unit, y: E::Unit, w: W) -> Pull { - Pull::XY(x, y, w) - } -} - -/// Increment origin point of drawing area -pub enum Push> { - /// Move origin to the right - X(E::Unit, T), - /// Move origin downwards - Y(E::Unit, T), - /// Move origin to the right and downwards - XY(E::Unit, E::Unit, T), -} - -impl> Push { - pub fn inner (&self) -> &T { - match self { - Self::X(_, i) => i, - Self::Y(_, i) => i, - Self::XY(_, _, i) => i, - } - } - pub fn x (&self) -> E::Unit { - match self { - Self::X(x, _) => *x, - Self::Y(_, _) => E::Unit::default(), - Self::XY(x, _, _) => *x, - } - } - pub fn y (&self) -> E::Unit { - match self { - Self::X(_, _) => E::Unit::default(), - Self::Y(y, _) => *y, - Self::XY(_, y, _) => *y, - } - } -} - -impl> Render for Push { - fn min_size (&self, to: E::Size) -> Perhaps { - self.inner().min_size(to) - } - fn render (&self, to: &mut E::Output) -> Usually<()> { - let area = to.area(); - Ok(self.min_size(area.wh().into())? - .map(|size|to.render_in(match *self { - Self::X(x, _) => [area.x() + x, area.y(), size.w(), size.h()], - Self::Y(y, _) => [area.x(), area.y() + y, size.w(), size.h()], - Self::XY(x, y, _) => [area.x() + x, area.y() + y, size.w(), size.h()], - _ => unreachable!(), - }.into(), self.inner())).transpose()?.unwrap_or(())) - } -} - -/// Decrement origin point of drawing area -pub enum Pull> { - _Unused(PhantomData), - /// Move origin to the right - X(E::Unit, T), - /// Move origin downwards - Y(E::Unit, T), - /// Move origin to the right and downwards - XY(E::Unit, E::Unit, T), -} - -impl> Pull { - pub fn inner (&self) -> &T { - match self { - Self::X(_, i) => i, - Self::Y(_, i) => i, - Self::XY(_, _, i) => i, - _ => unreachable!(), - } - } - pub fn x (&self) -> E::Unit { - match self { - Self::X(x, _) => *x, - Self::Y(_, _) => E::Unit::default(), - Self::XY(x, _, _) => *x, - _ => unreachable!(), - } - } - pub fn y (&self) -> E::Unit { - match self { - Self::X(_, _) => E::Unit::default(), - Self::Y(y, _) => *y, - Self::XY(_, y, _) => *y, - _ => unreachable!(), - } - } -} - -impl> Render for Pull { - fn min_size (&self, to: E::Size) -> Perhaps { - self.inner().min_size(to) - } - fn render (&self, to: &mut E::Output) -> Usually<()> { - let area = to.area(); - Ok(self.min_size(area.wh().into())? - .map(|size|to.render_in(match *self { - Self::X(x, _) => [area.x().minus(x), area.y(), size.w(), size.h()], - Self::Y(y, _) => [area.x(), area.y().minus(y), size.w(), size.h()], - Self::XY(x, y, _) => [area.x().minus(x), area.y().minus(y), size.w(), size.h()], - _ => unreachable!(), - }.into(), self.inner())).transpose()?.unwrap_or(())) - } -} - diff --git a/crates/tek/src/layout/scroll.rs b/crates/tek/src/layout/scroll.rs deleted file mode 100644 index 326f6ab6..00000000 --- a/crates/tek/src/layout/scroll.rs +++ /dev/null @@ -1,8 +0,0 @@ -use crate::*; - -/// A scrollable area. -pub struct Scroll< - E: Engine, - F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render)->Usually<()>)->Usually<()> ->(pub F, pub Direction, pub u64, PhantomData); - diff --git a/crates/tek/src/layout/shrink_grow.rs b/crates/tek/src/layout/shrink_grow.rs deleted file mode 100644 index 9c725ef4..00000000 --- a/crates/tek/src/layout/shrink_grow.rs +++ /dev/null @@ -1,104 +0,0 @@ -use crate::*; - -impl LayoutShrinkGrow for E {} - -pub trait LayoutShrinkGrow { - fn shrink_x > (x: E::Unit, w: W) -> Shrink { - Shrink::X(x, w) - } - fn shrink_y > (y: E::Unit, w: W) -> Shrink { - Shrink::Y(y, w) - } - fn shrink_xy > (x: E::Unit, y: E::Unit, w: W) -> Shrink { - Shrink::XY(x, y, w) - } - fn grow_x > (x: E::Unit, w: W) -> Grow { - Grow::X(x, w) - } - fn grow_y > (y: E::Unit, w: W) -> Grow { - Grow::Y(y, w) - } - fn grow_xy > (x: E::Unit, y: E::Unit, w: W) -> Grow { - Grow::XY(x, y, w) - } -} - -/// Shrink drawing area -pub enum Shrink> { - /// Decrease width - X(E::Unit, T), - /// Decrease height - Y(E::Unit, T), - /// Decrease width and height - XY(E::Unit, E::Unit, T), -} - -/// Expand drawing area -pub enum Grow> { - /// Increase width - X(E::Unit, T), - /// Increase height - Y(E::Unit, T), - /// Increase width and height - XY(E::Unit, E::Unit, T) -} - -impl> Shrink { - fn inner (&self) -> &T { - match self { - Self::X(_, i) => i, - Self::Y(_, i) => i, - Self::XY(_, _, i) => i, - _ => unreachable!(), - } - } -} - -impl> Grow { - fn inner (&self) -> &T { - match self { - Self::X(_, i) => i, - Self::Y(_, i) => i, - Self::XY(_, _, i) => i, - } - } -} - -impl> Render for Shrink { - fn min_size (&self, to: E::Size) -> Perhaps { - Ok(self.inner().min_size(to)?.map(|to|match *self { - Self::X(w, _) => [ - if to.w() > w { to.w() - w } else { 0.into() }, - to.h() - ], - Self::Y(h, _) => [ - to.w(), - if to.h() > h { to.h() - h } else { 0.into() } - ], - Self::XY(w, h, _) => [ - if to.w() > w { to.w() - w } else { 0.into() }, - if to.h() > h { to.h() - h } else { 0.into() } - ], - }.into())) - } - fn render (&self, to: &mut E::Output) -> Usually<()> { - Ok(self.min_size(to.area().wh().into())? - .map(|size|to.render_in(to.area().clip(size).into(), self.inner())) - .transpose()?.unwrap_or(())) - } -} - -impl> Render for Grow { - fn min_size (&self, to: E::Size) -> Perhaps { - Ok(self.inner().min_size(to)?.map(|to|match *self { - Self::X(w, _) => [to.w() + w, to.h()], - Self::Y(h, _) => [to.w(), to.h() + h], - Self::XY(w, h, _) => [to.w() + w, to.h() + h], - }.into())) - } - fn render (&self, to: &mut E::Output) -> Usually<()> { - Ok(self.min_size(to.area().wh().into())? - .map(|size|to.render_in(to.area().clip(size).into(), self.inner())) - .transpose()?.unwrap_or(())) - } -} diff --git a/crates/tek/src/layout/split.rs b/crates/tek/src/layout/split.rs deleted file mode 100644 index 5b78d405..00000000 --- a/crates/tek/src/layout/split.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::*; - -impl LayoutSplit for E {} - -pub trait LayoutSplit { - fn split , B: Render> ( - direction: Direction, amount: E::Unit, a: A, b: B - ) -> Split { - Split::new(direction, amount, a, b) - } - fn split_up , B: Render> ( - amount: E::Unit, a: A, b: B - ) -> Split { - Split::new(Direction::Up, amount, a, b) - } - - //fn split_flip > ( - //self, direction: Direction, amount: E::Unit, other: W - //) -> Split { Split::new(direction, amount, other, self) } -} - -/// A binary split with fixed proportion -pub struct Split, B: Render>( - pub Direction, pub E::Unit, A, B, PhantomData -); - -impl, B: Render> Split { - pub fn new (direction: Direction, proportion: E::Unit, a: A, b: B) -> Self { - Self(direction, proportion, a, b, Default::default()) - } - pub fn up (proportion: E::Unit, a: A, b: B) -> Self { - Self(Direction::Up, proportion, a, b, Default::default()) - } - pub fn down (proportion: E::Unit, a: A, b: B) -> Self { - Self(Direction::Down, proportion, a, b, Default::default()) - } - pub fn left (proportion: E::Unit, a: A, b: B) -> Self { - Self(Direction::Left, proportion, a, b, Default::default()) - } - pub fn right (proportion: E::Unit, a: A, b: B) -> Self { - Self(Direction::Right, proportion, a, b, Default::default()) - } -} - -impl, B: Render> Render for Split { - fn min_size (&self, to: E::Size) -> Perhaps { - Ok(Some(to)) - } - fn render (&self, to: &mut E::Output) -> Usually<()> { - let (a, b) = to.area().split_fixed(self.0, self.1); - to.render_in(a.into(), &self.2)?; - to.render_in(b.into(), &self.3)?; - Ok(()) - } -} diff --git a/crates/tek/src/layout/stack.rs b/crates/tek/src/layout/stack.rs deleted file mode 100644 index 24daf4be..00000000 --- a/crates/tek/src/layout/stack.rs +++ /dev/null @@ -1,197 +0,0 @@ -use crate::*; - -#[macro_export] macro_rules! col { - ([$($expr:expr),* $(,)?]) => { - Stack::down(move|add|{ $(add(&$expr)?;)* Ok(()) }) - }; - (![$($expr:expr),* $(,)?]) => { - Stack::down(|add|{ $(add(&$expr)?;)* Ok(()) }) - }; - ($expr:expr) => { - Stack::down($expr) - }; - ($pat:pat in $collection:expr => $item:expr) => { - Stack::down(move|add|{ for $pat in $collection { add(&$item)?; } Ok(()) }) - }; -} - -#[macro_export] macro_rules! col_up { - ([$($expr:expr),* $(,)?]) => { - Stack::up(move|add|{ $(add(&$expr)?;)* Ok(()) }) - }; - (![$($expr:expr),* $(,)?]) => { - Stack::up(|add|{ $(add(&$expr)?;)* Ok(()) }) - }; - ($expr:expr) => { - Stack::up(expr) - }; - ($pat:pat in $collection:expr => $item:expr) => { - Stack::up(move |add|{ for $pat in $collection { add(&$item)?; } Ok(()) }) - }; -} - -#[macro_export] macro_rules! row { - ([$($expr:expr),* $(,)?]) => { - Stack::right(move|add|{ $(add(&$expr)?;)* Ok(()) }) - }; - (![$($expr:expr),* $(,)?]) => { - Stack::right(|add|{ $(add(&$expr)?;)* Ok(()) }) - }; - ($expr:expr) => { - Stack::right($expr) - }; - ($pat:pat in $collection:expr => $item:expr) => { - Stack::right(move|add|{ for $pat in $collection { add(&$item)?; } Ok(()) }) - }; -} - -pub struct Stack< - E: Engine, - F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render)->Usually<()>)->Usually<()> ->(pub F, pub Direction, PhantomData); - -impl< - E: Engine, - F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render)->Usually<()>)->Usually<()> -> Stack { - #[inline] pub fn new (direction: Direction, build: F) -> Self { - Self(build, direction, Default::default()) - } - #[inline] pub fn right (build: F) -> Self { - Self::new(Direction::Right, build) - } - #[inline] pub fn down (build: F) -> Self { - Self::new(Direction::Down, build) - } - #[inline] pub fn up (build: F) -> Self { - Self::new(Direction::Up, build) - } -} - -impl Render for Stack -where - F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render)->Usually<()>)->Usually<()> -{ - fn min_size (&self, to: E::Size) -> Perhaps { - match self.1 { - - Direction::Down => { - let mut w: E::Unit = 0.into(); - let mut h: E::Unit = 0.into(); - (self.0)(&mut |component: &dyn Render| { - let max = to.h().minus(h); - if max > E::Unit::ZERO() { - let item = E::max_y(max, E::push_y(h, component)); - let size = item.min_size(to)?.map(|size|size.wh()); - if let Some([width, height]) = size { - h = h + height.into(); - w = w.max(width); - } - } - Ok(()) - })?; - Ok(Some([w, h].into())) - }, - - Direction::Right => { - let mut w: E::Unit = 0.into(); - let mut h: E::Unit = 0.into(); - (self.0)(&mut |component: &dyn Render| { - let max = to.w().minus(w); - if max > E::Unit::ZERO() { - let item = E::max_x(max, E::push_x(h, component)); - let size = item.min_size(to)?.map(|size|size.wh()); - if let Some([width, height]) = size { - w = w + width.into(); - h = h.max(height); - } - } - Ok(()) - })?; - Ok(Some([w, h].into())) - }, - - Direction::Up => { - let mut w: E::Unit = 0.into(); - let mut h: E::Unit = 0.into(); - (self.0)(&mut |component: &dyn Render| { - let max = to.h().minus(h); - if max > E::Unit::ZERO() { - let item = E::max_y(to.h() - h, component); - let size = item.min_size(to)?.map(|size|size.wh()); - if let Some([width, height]) = size { - h = h + height.into(); - w = w.max(width); - } - } - Ok(()) - })?; - Ok(Some([w, h].into())) - }, - - Direction::Left => { - let mut w: E::Unit = 0.into(); - let mut h: E::Unit = 0.into(); - (self.0)(&mut |component: &dyn Render| { - if w < to.w() { - todo!(); - } - Ok(()) - })?; - Ok(Some([w, h].into())) - }, - } - } - - fn render (&self, to: &mut E::Output) -> Usually<()> { - let area = to.area(); - let mut w = 0.into(); - let mut h = 0.into(); - match self.1 { - Direction::Down => { - (self.0)(&mut |item| { - if h < area.h() { - let item = E::max_y(area.h() - h, E::push_y(h, item)); - let show = item.min_size(area.wh().into())?.map(|s|s.wh()); - if let Some([width, height]) = show { - item.render(to)?; - h = h + height; - if width > w { w = width } - }; - } - Ok(()) - })?; - }, - Direction::Right => { - (self.0)(&mut |item| { - if w < area.w() { - let item = E::max_x(area.w() - w, E::push_x(w, item)); - let show = item.min_size(area.wh().into())?.map(|s|s.wh()); - if let Some([width, height]) = show { - item.render(to)?; - w = width + w; - if height > h { h = height } - }; - } - Ok(()) - })?; - }, - Direction::Up => { - (self.0)(&mut |item| { - if h < area.h() { - let show = item.min_size([area.w(), area.h().minus(h)].into())?.map(|s|s.wh()); - if let Some([width, height]) = show { - E::shrink_y(height, E::push_y(area.h() - height, item)) - .render(to)?; - h = h + height; - if width > w { w = width } - }; - } - Ok(()) - })?; - }, - _ => todo!() - }; - Ok(()) - } -} diff --git a/crates/tek/src/lib.rs b/crates/tek/src/lib.rs deleted file mode 100644 index 6faafea0..00000000 --- a/crates/tek/src/lib.rs +++ /dev/null @@ -1,83 +0,0 @@ -pub(crate) use std::sync::{Arc, Mutex, RwLock}; -pub(crate) use std::sync::atomic::{Ordering, AtomicBool, AtomicUsize}; -pub(crate) use std::collections::BTreeMap; -pub(crate) use std::marker::PhantomData; -pub(crate) use std::thread::{spawn, JoinHandle}; -pub(crate) use std::path::PathBuf; -pub(crate) use std::ffi::OsString; -pub(crate) use std::time::Duration; -pub(crate) use std::io::{Stdout, stdout}; -pub(crate) use std::error::Error; - -pub(crate) use ratatui; -pub(crate) use ratatui::{ - prelude::{Style, Color, Buffer}, - style::{Stylize, Modifier}, - backend::{Backend, CrosstermBackend, ClearType} -}; - -pub(crate) use jack; -pub(crate) use jack::{ - Client, ProcessScope, Control, CycleTimes, - Port, PortSpec, MidiIn, MidiOut, AudioIn, AudioOut, Unowned, - Transport, TransportState, MidiIter, RawMidi, - contrib::ClosureProcessHandler, -}; - -pub(crate) use midly; -pub(crate) use midly::{ - Smf, - MidiMessage, - TrackEventKind, - live::LiveEvent, - num::u7 -}; - -pub(crate) use palette::{ - *, - convert::*, - okhsl::* -}; - -pub(crate) use clap::{self, Parser}; - -pub(crate) use crossterm::{ExecutableCommand}; -pub(crate) use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}; -pub(crate) use crossterm::event::{KeyCode, KeyModifiers, KeyEvent, KeyEventKind, KeyEventState}; -pub(crate) use better_panic::{Settings, Verbosity}; - -pub(crate) use atomic_float::*; - -use std::ops::{Add, Sub, Mul, Div, Rem}; -use std::cmp::{Ord, Eq, PartialEq}; -use std::fmt::{Debug, Display}; - -/// Standard result type. -pub type Usually = Result>; - -/// Standard optional result type. -pub type Perhaps = Result, Box>; - -/// Define and reexport submodules. -#[macro_export] macro_rules! submod { - ($($name:ident)*) => { $(mod $name; pub use self::$name::*;)* }; -} - -/// Define public modules. -#[macro_export] macro_rules! pubmod { - ($($name:ident)*) => { $(pub mod $name;)* }; -} - -/// Define test modules. -#[macro_export] macro_rules! testmod { - ($($name:ident)*) => { $(#[cfg(test)] mod $name;)* }; -} - -mod core; pub(crate) use core::*; -mod layout; pub(crate) use layout::*; -mod api; pub(crate) use api::*; -mod tui; pub(crate) use tui::*; - -testmod! { - test -} diff --git a/crates/tek/src/test.rs b/crates/tek/src/test.rs deleted file mode 100644 index fe6a77b2..00000000 --- a/crates/tek/src/test.rs +++ /dev/null @@ -1,190 +0,0 @@ -use crate::*; - -struct TestEngine([u16;4], Vec>); - -impl Engine for TestEngine { - type Unit = u16; - type Size = [Self::Unit;2]; - type Area = [Self::Unit;4]; - type Input = Self; - type Handled = bool; - fn exited (&self) -> bool { - true - } - fn area (&self) -> Self::Area { - self.0 - } - fn area_mut (&mut self) -> &mut Self::Area { - &mut self.0 - } -} - -#[derive(Copy, Clone)] -struct TestArea(u16, u16); - -impl Render for TestArea { - type Engine = TestEngine; - fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> { - Ok(Some([to[0], to[1], self.0, self.1])) - } - fn render (&self, to: &mut Self::Engine) -> Perhaps<[u16;4]> { - if let Some(layout) = self.layout(to.area())? { - for y in layout.y()..layout.y()+layout.h()-1 { - for x in layout.x()..layout.x()+layout.w()-1 { - to.1[y as usize][x as usize] = '*'; - } - } - Ok(Some(layout)) - } else { - Ok(None) - } - } -} - -#[test] -fn test_plus_minus () -> Usually<()> { - let area = [0, 0, 10, 10]; - let engine = TestEngine(area, vec![vec![' ';10];10]); - let test = TestArea(4, 4); - assert_eq!(test.layout(area)?, Some([0, 0, 4, 4])); - assert_eq!(Push::X(1, test).layout(area)?, Some([1, 0, 4, 4])); - Ok(()) -} - -#[test] -fn test_outset_align () -> Usually<()> { - let area = [0, 0, 10, 10]; - let engine = TestEngine(area, vec![vec![' ';10];10]); - let test = TestArea(4, 4); - assert_eq!(test.layout(area)?, Some([0, 0, 4, 4])); - assert_eq!(Outset::X(1, test).layout(area)?, Some([0, 0, 6, 4])); - assert_eq!(Align::X(test).layout(area)?, Some([3, 0, 4, 4])); - assert_eq!(Align::X(Outset::X(1, test)).layout(area)?, Some([2, 0, 6, 4])); - assert_eq!(Outset::X(1, Align::X(test)).layout(area)?, Some([2, 0, 6, 4])); - Ok(()) -} - -//#[test] -//fn test_misc () -> Usually<()> { - //let area: [u16;4] = [0, 0, 10, 10]; - //let test = TestArea(4, 4); - //assert_eq!(test.layout(area)?, - //Some([0, 0, 4, 4])); - //assert_eq!(Align::Center(test).layout(area)?, - //Some([3, 3, 4, 4])); - //assert_eq!(Align::Center(Stack::down(|add|{ - //add(&test)?; - //add(&test) - //})).layout(area)?, - //Some([3, 1, 4, 8])); - //assert_eq!(Align::Center(Stack::down(|add|{ - //add(&Outset::XY(2, 2, test))?; - //add(&test) - //})).layout(area)?, - //Some([2, 0, 6, 10])); - //assert_eq!(Align::Center(Stack::down(|add|{ - //add(&Outset::XY(2, 2, test))?; - //add(&Inset::XY(2, 2, test)) - //})).layout(area)?, - //Some([2, 1, 6, 8])); - //assert_eq!(Stack::down(|add|{ - //add(&Outset::XY(2, 2, test))?; - //add(&Inset::XY(2, 2, test)) - //}).layout(area)?, - //Some([0, 0, 6, 8])); - //assert_eq!(Stack::right(|add|{ - //add(&Stack::down(|add|{ - //add(&Outset::XY(2, 2, test))?; - //add(&Inset::XY(2, 2, test)) - //}))?; - //add(&Align::Center(TestArea(2 ,2))) - //}).layout(area)?, - //Some([0, 0, 8, 8])); - //Ok(()) -//} - -//#[test] -//fn test_offset () -> Usually<()> { - //let area: [u16;4] = [50, 50, 100, 100]; - //let test = TestArea(3, 3); - //assert_eq!(Push::X(1, test).layout(area)?, Some([51, 50, 3, 3])); - //assert_eq!(Push::Y(1, test).layout(area)?, Some([50, 51, 3, 3])); - //assert_eq!(Push::XY(1, 1, test).layout(area)?, Some([51, 51, 3, 3])); - //Ok(()) -//} - -//#[test] -//fn test_outset () -> Usually<()> { - //let area: [u16;4] = [50, 50, 100, 100]; - //let test = TestArea(3, 3); - //assert_eq!(Outset::X(1, test).layout(area)?, Some([49, 50, 5, 3])); - //assert_eq!(Outset::Y(1, test).layout(area)?, Some([50, 49, 3, 5])); - //assert_eq!(Outset::XY(1, 1, test).layout(area)?, Some([49, 49, 5, 5])); - //Ok(()) -//} - -//#[test] -//fn test_inset () -> Usually<()> { - //let area: [u16;4] = [50, 50, 100, 100]; - //let test = TestArea(3, 3); - //assert_eq!(Inset::X(1, test).layout(area)?, Some([51, 50, 1, 3])); - //assert_eq!(Inset::Y(1, test).layout(area)?, Some([50, 51, 3, 1])); - //assert_eq!(Inset::XY(1, 1, test).layout(area)?, Some([51, 51, 1, 1])); - //Ok(()) -//} - -//#[test] -//fn test_stuff () -> Usually<()> { - //let area: [u16;4] = [0, 0, 100, 100]; - //assert_eq!("1".layout(area)?, - //Some([0, 0, 1, 1])); - //assert_eq!("333".layout(area)?, - //Some([0, 0, 3, 1])); - //assert_eq!(Layers::new(|add|{add(&"1")?;add(&"333")}).layout(area)?, - //Some([0, 0, 3, 1])); - //assert_eq!(Stack::down(|add|{add(&"1")?;add(&"333")}).layout(area)?, - //Some([0, 0, 3, 2])); - //assert_eq!(Stack::right(|add|{add(&"1")?;add(&"333")}).layout(area)?, - //Some([0, 0, 4, 1])); - //assert_eq!(Stack::down(|add|{ - //add(&Stack::right(|add|{add(&"1")?;add(&"333")}))?; - //add(&"55555") - //}).layout(area)?, - //Some([0, 0, 5, 2])); - //let area: [u16;4] = [1, 1, 100, 100]; - //assert_eq!(Outset::X(1, Stack::right(|add|{add(&"1")?;add(&"333")})).layout(area)?, - //Some([0, 1, 6, 1])); - //assert_eq!(Outset::Y(1, Stack::right(|add|{add(&"1")?;add(&"333")})).layout(area)?, - //Some([1, 0, 4, 3])); - //assert_eq!(Outset::XY(1, 1, Stack::right(|add|{add(&"1")?;add(&"333")})).layout(area)?, - //Some([0, 0, 6, 3])); - //assert_eq!(Stack::down(|add|{ - //add(&Outset::XY(1, 1, "1"))?; - //add(&Outset::XY(1, 1, "333")) - //}).layout(area)?, - //Some([1, 1, 5, 6])); - //let area: [u16;4] = [1, 1, 95, 100]; - //assert_eq!(Align::Center(Stack::down(|add|{ - //add(&Outset::XY(1, 1, "1"))?; - //add(&Outset::XY(1, 1, "333")) - //})).layout(area)?, - //Some([46, 48, 5, 6])); - //assert_eq!(Align::Center(Stack::down(|add|{ - //add(&Layers::new(|add|{ - ////add(&Outset::XY(1, 1, Background(Color::Rgb(0,128,0))))?; - //add(&Outset::XY(1, 1, "1"))?; - //add(&Outset::XY(1, 1, "333"))?; - ////add(&Background(Color::Rgb(0,128,0)))?; - //Ok(()) - //}))?; - //add(&Layers::new(|add|{ - ////add(&Outset::XY(1, 1, Background(Color::Rgb(0,0,128))))?; - //add(&Outset::XY(1, 1, "555"))?; - //add(&Outset::XY(1, 1, "777777"))?; - ////add(&Background(Color::Rgb(0,0,128)))?; - //Ok(()) - //})) - //})).layout(area)?, - //Some([46, 48, 5, 6])); - //Ok(()) -//} diff --git a/crates/tek/src/tui.rs b/crates/tek/src/tui.rs deleted file mode 100644 index 645073db..00000000 --- a/crates/tek/src/tui.rs +++ /dev/null @@ -1,270 +0,0 @@ -use crate::*; - -mod engine_focus; pub(crate) use engine_focus::*; -mod engine_input; pub(crate) use engine_input::*; -mod engine_style; pub(crate) use engine_style::*; -mod engine_theme; pub(crate) use engine_theme::*; -mod engine_output; pub(crate) use engine_output::*; - -//////////////////////////////////////////////////////// - -mod app_transport; pub(crate) use app_transport::*; -mod app_sequencer; pub(crate) use app_sequencer::*; -mod app_arranger; pub(crate) use app_arranger::*; - -//////////////////////////////////////////////////////// - -mod status_bar; pub(crate) use status_bar::*; -mod file_browser; pub(crate) use file_browser::*; -mod phrase_editor; pub(crate) use phrase_editor::*; -mod phrase_length; pub(crate) use phrase_length::*; -mod phrase_rename; pub(crate) use phrase_rename::*; -mod phrase_list; pub(crate) use phrase_list::*; -mod phrase_player; pub(crate) use phrase_player::*; -mod phrase_select; pub(crate) use phrase_select::*; - -//////////////////////////////////////////////////////// - -#[macro_export] macro_rules! render { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? Render for $Struct $(<$($L),*$($T),*>)? { - fn min_size (&$self, to: [u16;2]) -> Perhaps<[u16;2]> { - $cb.min_size(to) - } - fn render (&$self, to: &mut TuiOutput) -> Usually<()> { - $cb.render(to) - } - } - } -} - -pub fn render Usually<()>+Send+Sync> (render: F) -> impl Render { - Widget::new(|_|Ok(Some([0u16,0u16].into())), render) -} - -//////////////////////////////////////////////////////// - -pub struct Tui { - pub exited: Arc, - pub buffer: Buffer, - pub backend: CrosstermBackend, - pub area: [u16;4], // FIXME auto resize -} - -impl crate::core::Engine for Tui { - type Unit = u16; - type Size = [Self::Unit;2]; - type Area = [Self::Unit;4]; - type Input = TuiInput; - type Handled = bool; - type Output = TuiOutput; - fn exited (&self) -> bool { - self.exited.fetch_and(true, Ordering::Relaxed) - } - fn setup (&mut self) -> Usually<()> { - let better_panic_handler = Settings::auto().verbosity(Verbosity::Full).create_panic_handler(); - std::panic::set_hook(Box::new(move |info: &std::panic::PanicHookInfo|{ - stdout().execute(LeaveAlternateScreen).unwrap(); - CrosstermBackend::new(stdout()).show_cursor().unwrap(); - disable_raw_mode().unwrap(); - better_panic_handler(info); - })); - stdout().execute(EnterAlternateScreen)?; - self.backend.hide_cursor()?; - enable_raw_mode().map_err(Into::into) - } - fn teardown (&mut self) -> Usually<()> { - stdout().execute(LeaveAlternateScreen)?; - self.backend.show_cursor()?; - disable_raw_mode().map_err(Into::into) - } -} - -impl Tui { - /// Run the main loop. - pub fn run + Sized + 'static> ( - state: Arc> - ) -> Usually>> { - let backend = CrosstermBackend::new(stdout()); - let area = backend.size()?; - let engine = Self { - exited: Arc::new(AtomicBool::new(false)), - buffer: Buffer::empty(area), - area: [area.x, area.y, area.width, area.height], - backend, - }; - let engine = Arc::new(RwLock::new(engine)); - let _input_thread = Self::spawn_input_thread(&engine, &state, Duration::from_millis(100)); - engine.write().unwrap().setup()?; - let render_thread = Self::spawn_render_thread(&engine, &state, Duration::from_millis(10)); - render_thread.join().expect("main thread failed"); - engine.write().unwrap().teardown()?; - Ok(state) - } - fn spawn_input_thread + Sized + 'static> ( - engine: &Arc>, state: &Arc>, poll: Duration - ) -> JoinHandle<()> { - let exited = engine.read().unwrap().exited.clone(); - let state = state.clone(); - spawn(move || loop { - if exited.fetch_and(true, Ordering::Relaxed) { - break - } - if ::crossterm::event::poll(poll).is_ok() { - let event = TuiEvent::Input(::crossterm::event::read().unwrap()); - match event { - key!(Ctrl-KeyCode::Char('c')) => { - exited.store(true, Ordering::Relaxed); - }, - _ => { - let exited = exited.clone(); - if let Err(e) = state.write().unwrap().handle(&TuiInput { event, exited }) { - panic!("{e}") - } - } - } - } - }) - } - fn spawn_render_thread + Sized + 'static> ( - engine: &Arc>, state: &Arc>, sleep: Duration - ) -> JoinHandle<()> { - let exited = engine.read().unwrap().exited.clone(); - let engine = engine.clone(); - let state = state.clone(); - let size = engine.read().unwrap().backend.size().expect("get size failed"); - let mut buffer = Buffer::empty(size); - spawn(move || loop { - if exited.fetch_and(true, Ordering::Relaxed) { - break - } - let size = engine.read().unwrap().backend.size() - .expect("get size failed"); - if let Ok(state) = state.try_read() { - if buffer.area != size { - engine.write().unwrap().backend.clear_region(ClearType::All) - .expect("clear failed"); - buffer.resize(size); - buffer.reset(); - } - let mut output = TuiOutput { - buffer, - area: [size.x, size.y, size.width, size.height] - }; - state.render(&mut output).expect("render failed"); - buffer = engine.write().unwrap().flip(output.buffer, size); - } - std::thread::sleep(sleep); - }) - } - fn flip (&mut self, mut buffer: Buffer, size: ratatui::prelude::Rect) -> Buffer { - if self.buffer.area != size { - self.backend.clear_region(ClearType::All).unwrap(); - self.buffer.resize(size); - self.buffer.reset(); - } - let updates = self.buffer.diff(&buffer); - self.backend.draw(updates.into_iter()).expect("failed to render"); - self.backend.flush().expect("failed to flush output buffer"); - std::mem::swap(&mut self.buffer, &mut buffer); - buffer.reset(); - buffer - } -} - -struct Field(&'static str, String); - -render!(|self: Field|{ - Tui::to_east("│", Tui::to_east( - Tui::bold(true, self.0), - Tui::bg(Color::Rgb(0, 0, 0), self.1.as_str()), - )) -}); - -//pub struct TransportView { - //pub(crate) state: Option, - //pub(crate) selected: Option, - //pub(crate) focused: bool, - //pub(crate) bpm: f64, - //pub(crate) sync: f64, - //pub(crate) quant: f64, - //pub(crate) beat: String, - //pub(crate) msu: String, -//} - ////)?; - ////match *state { - ////Some(TransportState::Rolling) => { - ////add(&row!( - ////"│", - ////TuiStyle::fg("▶ PLAYING", Color::Rgb(0, 255, 0)), - ////format!("│0 (0)"), - ////format!("│00m00s000u"), - ////format!("│00B 0b 00/00") - ////))?; - ////add(&row!("│Now ", row!( - ////format!("│0 (0)"), //sample(chunk) - ////format!("│00m00s000u"), //msu - ////format!("│00B 0b 00/00"), //bbt - ////)))?; - ////}, - ////_ => { - ////add(&row!("│", TuiStyle::fg("⏹ STOPPED", Color::Rgb(255, 128, 0))))?; - ////add(&"")?; - ////} - ////} - ////Ok(()) - ////}).fill_x().bg(Color::Rgb(40, 50, 30)) -////}); - -//impl<'a, T: HasClock> From<&'a T> for TransportView where Option: From<&'a T> { - //fn from (state: &'a T) -> Self { - //let selected = state.into(); - //Self { - //selected, - //focused: selected.is_some(), - //state: Some(state.clock().transport.query_state().unwrap()), - //bpm: state.clock().bpm().get(), - //sync: state.clock().sync.get(), - //quant: state.clock().quant.get(), - //beat: state.clock().playhead.format_beat(), - //msu: state.clock().playhead.usec.format_msu(), - //} - //} -//} - - //row!( - ////selected.wrap(TransportFocus::PlayPause, &play_pause.fixed_xy(10, 3)), - //row!( - //col!( - //Field("SR ", format!("192000")), - //Field("BUF ", format!("1024")), - //Field("LEN ", format!("21300")), - //Field("CPU ", format!("00.0%")) - //), - //col!( - //Field("PUL ", format!("000000000")), - //Field("PPQ ", format!("96")), - //Field("BBT ", format!("00B0b00p")) - //), - //col!( - //Field("SEC ", format!("000000.000")), - //Field("BPM ", format!("000.000")), - //Field("MSU ", format!("00m00s00u")) - //), - //), - //selected.wrap(TransportFocus::Bpm, &Outset::X(1u16, { - //row! { - //"BPM ", - //format!("{}.{:03}", *bpm as usize, (bpm * 1000.0) % 1000.0) - //} - //})), - //selected.wrap(TransportFocus::Sync, &Outset::X(1u16, row! { - //"SYNC ", pulses_to_name(*sync as usize) - //})), - //selected.wrap(TransportFocus::Quant, &Outset::X(1u16, row! { - //"QUANT ", pulses_to_name(*quant as usize) - //})), - //selected.wrap(TransportFocus::Clock, &{ - //row!("B" , beat.as_str(), " T", msu.as_str()).outset_x(1) - //}).align_e().fill_x(), - //).fill_x().bg(Color::Rgb(40, 50, 30)) diff --git a/crates/tek/src/tui/_todo_tui_mixer.rs b/crates/tek/src/tui/_todo_tui_mixer.rs deleted file mode 100644 index c8203770..00000000 --- a/crates/tek/src/tui/_todo_tui_mixer.rs +++ /dev/null @@ -1,251 +0,0 @@ -use crate::*; - -pub struct Mixer { - /// JACK client handle (needs to not be dropped for standalone mode to work). - pub jack: Arc>, - pub name: String, - pub tracks: Vec>, - pub selected_track: usize, - pub selected_column: usize, -} -impl Mixer { - pub fn new (jack: &Arc>, name: &str) -> Usually { - Ok(Self { - jack: jack.clone(), - name: name.into(), - selected_column: 0, - selected_track: 1, - tracks: vec![], - }) - } - pub fn track_add (&mut self, name: &str, channels: usize) -> Usually<&mut Self> { - let track = Track::new(name)?; - self.tracks.push(track); - Ok(self) - } - pub fn track (&self) -> Option<&Track> { - self.tracks.get(self.selected_track) - } -} - -//pub const ACTIONS: [(&'static str, &'static str);2] = [ - //("+/-", "Adjust"), - //("Ins/Del", "Add/remove track"), -//]; - - -/// A sequencer track. -#[derive(Debug)] -pub struct Track { - pub name: String, - /// Inputs and outputs of 1st and last device - pub ports: JackPorts, - /// Device chain - pub devices: Vec>, - /// Device selector - pub device: usize, -} - -impl Track { - pub fn new (name: &str) -> Usually { - Ok(Self { - name: name.to_string(), - ports: JackPorts::default(), - devices: vec![], - device: 0, - }) - } - fn get_device_mut (&self, i: usize) -> Option>>> { - self.devices.get(i).map(|d|d.state.write().unwrap()) - } - pub fn device_mut (&self) -> Option>>> { - self.get_device_mut(self.device) - } - /// Add a device to the end of the chain. - pub fn append_device (&mut self, device: JackDevice) -> Usually<&mut JackDevice> { - self.devices.push(device); - let index = self.devices.len() - 1; - Ok(&mut self.devices[index]) - } - pub fn add_device (&mut self, device: JackDevice) { - self.devices.push(device); - } - //pub fn connect_first_device (&self) -> Usually<()> { - //if let (Some(port), Some(device)) = (&self.midi_out, self.devices.get(0)) { - //device.client.as_client().connect_ports(&port, &device.midi_ins()?[0])?; - //} - //Ok(()) - //} - //pub fn connect_last_device (&self, app: &Track) -> Usually<()> { - //Ok(match self.devices.get(self.devices.len().saturating_sub(1)) { - //Some(device) => { - //app.audio_out(0).map(|left|device.connect_audio_out(0, &left)).transpose()?; - //app.audio_out(1).map(|right|device.connect_audio_out(1, &right)).transpose()?; - //() - //}, - //None => () - //}) - //} -} - -pub struct TrackView<'a, E: Engine> { - pub chain: Option<&'a Track>, - pub direction: Direction, - pub focused: bool, - pub entered: bool, -} - -impl<'a> Render for TrackView<'a, Tui> { - fn min_size (&self, area: [u16;2]) -> Perhaps<[u16;2]> { - todo!() - } - fn render (&self, to: &mut TuiOutput) -> Usually<()> { - todo!(); - //let mut area = to.area(); - //if let Some(chain) = self.chain { - //match self.direction { - //Direction::Down => area.width = area.width.min(40), - //Direction::Right => area.width = area.width.min(10), - //_ => { unimplemented!() }, - //} - //to.fill_bg(to.area(), Nord::bg_lo(self.focused, self.entered)); - //let mut split = Stack::new(self.direction); - //for device in chain.devices.as_slice().iter() { - //split = split.add_ref(device); - //} - //let (area, areas) = split.render_areas(to)?; - //if self.focused && self.entered && areas.len() > 0 { - //Corners(Style::default().green().not_dim()).draw(to.with_rect(areas[0]))?; - //} - //Ok(Some(area)) - //} else { - //let [x, y, width, height] = area; - //let label = "No chain selected"; - //let x = x + (width - label.len() as u16) / 2; - //let y = y + height / 2; - //to.blit(&label, x, y, Some(Style::default().dim().bold()))?; - //Ok(Some(area)) - //} - } -} - -impl Content for Mixer { - fn content (&self) -> impl Render { - Stack::right(|add| { - for channel in self.tracks.iter() { - add(channel)?; - } - Ok(()) - }) - } -} - -impl Content for Track { - fn content (&self) -> impl Render { - TrackView { - chain: Some(&self), - direction: tek_core::Direction::Right, - focused: true, - entered: true, - //pub channels: u8, - //pub input_ports: Vec>, - //pub pre_gain_meter: f64, - //pub gain: f64, - //pub insert_ports: Vec>, - //pub return_ports: Vec>, - //pub post_gain_meter: f64, - //pub post_insert_meter: f64, - //pub level: f64, - //pub pan: f64, - //pub output_ports: Vec>, - //pub post_fader_meter: f64, - //pub route: String, - } - } -} - -impl Handle for Mixer { - fn handle (&mut self, engine: &TuiInput) -> Perhaps { - if let TuiEvent::Input(crossterm::event::Event::Key(event)) = engine.event() { - - match event.code { - //KeyCode::Char('c') => { - //if event.modifiers == KeyModifiers::CONTROL { - //self.exit(); - //} - //}, - KeyCode::Down => { - self.selected_track = (self.selected_track + 1) % self.tracks.len(); - println!("{}", self.selected_track); - return Ok(Some(true)) - }, - KeyCode::Up => { - if self.selected_track == 0 { - self.selected_track = self.tracks.len() - 1; - } else { - self.selected_track -= 1; - } - println!("{}", self.selected_track); - return Ok(Some(true)) - }, - KeyCode::Left => { - if self.selected_column == 0 { - self.selected_column = 6 - } else { - self.selected_column -= 1; - } - return Ok(Some(true)) - }, - KeyCode::Right => { - if self.selected_column == 6 { - self.selected_column = 0 - } else { - self.selected_column += 1; - } - return Ok(Some(true)) - }, - _ => { - println!("\n{event:?}"); - } - } - - } - Ok(None) - } -} -impl Handle for Track { - fn handle (&mut self, from: &TuiInput) -> Perhaps { - match from.event() { - //, NONE, "chain_cursor_up", "move cursor up", || { - key!(KeyCode::Up) => { - Ok(Some(true)) - }, - // , NONE, "chain_cursor_down", "move cursor down", || { - key!(KeyCode::Down) => { - Ok(Some(true)) - }, - // Left, NONE, "chain_cursor_left", "move cursor left", || { - key!(KeyCode::Left) => { - //if let Some(track) = app.arranger.track_mut() { - //track.device = track.device.saturating_sub(1); - //return Ok(true) - //} - Ok(Some(true)) - }, - // , NONE, "chain_cursor_right", "move cursor right", || { - key!(KeyCode::Right) => { - //if let Some(track) = app.arranger.track_mut() { - //track.device = (track.device + 1).min(track.devices.len().saturating_sub(1)); - //return Ok(true) - //} - Ok(Some(true)) - }, - // , NONE, "chain_mode_switch", "switch the display mode", || { - key!(KeyCode::Char('`')) => { - //app.chain_mode = !app.chain_mode; - Ok(Some(true)) - }, - _ => Ok(None) - } - } -} diff --git a/crates/tek/src/tui/_todo_tui_plugin.rs b/crates/tek/src/tui/_todo_tui_plugin.rs deleted file mode 100644 index 54cf0704..00000000 --- a/crates/tek/src/tui/_todo_tui_plugin.rs +++ /dev/null @@ -1,148 +0,0 @@ -use crate::*; - -/// A plugin device. -pub struct Plugin { - _engine: PhantomData, - /// JACK client handle (needs to not be dropped for standalone mode to work). - pub jack: Arc>, - pub name: String, - pub path: Option, - pub plugin: Option, - pub selected: usize, - pub mapping: bool, - pub ports: JackPorts, -} - -impl Plugin { - /// Create a plugin host device. - pub fn new ( - jack: &Arc>, - name: &str, - ) -> Usually { - Ok(Self { - _engine: Default::default(), - jack: jack.clone(), - name: name.into(), - path: None, - plugin: None, - selected: 0, - mapping: false, - ports: JackPorts::default() - }) - } -} -impl Render for Plugin { - fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> { - Ok(Some(to)) - } - fn render (&self, to: &mut TuiOutput) -> Usually<()> { - let area = to.area(); - let [x, y, _, height] = area; - let mut width = 20u16; - match &self.plugin { - Some(PluginKind::LV2(LV2Plugin { port_list, instance, .. })) => { - let start = self.selected.saturating_sub((height as usize / 2).saturating_sub(1)); - let end = start + height as usize - 2; - //draw_box(buf, Rect { x, y, width, height }); - for i in start..end { - if let Some(port) = port_list.get(i) { - let value = if let Some(value) = instance.control_input(port.index) { - value - } else { - port.default_value - }; - //let label = &format!("C·· M·· {:25} = {value:.03}", port.name); - let label = &format!("{:25} = {value:.03}", port.name); - width = width.max(label.len() as u16 + 4); - let style = if i == self.selected { - Some(Style::default().green()) - } else { - None - } ; - to.blit(&label, x + 2, y + 1 + i as u16 - start as u16, style); - } else { - break - } - } - }, - _ => {} - }; - draw_header(self, to, x, y, width)?; - Ok(()) - } -} - -fn draw_header (state: &Plugin, to: &mut TuiOutput, x: u16, y: u16, w: u16) -> Usually { - let style = Style::default().gray(); - let label1 = format!(" {}", state.name); - to.blit(&label1, x + 1, y, Some(style.white().bold())); - if let Some(ref path) = state.path { - let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]); - to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim())); - } - Ok(Rect { x, y, width: w, height: 1 }) -} - -impl Handle for Plugin { - fn handle (&mut self, from: &TuiInput) -> Perhaps { - match from.event() { - key!(KeyCode::Up) => { - self.selected = self.selected.saturating_sub(1); - Ok(Some(true)) - }, - key!(KeyCode::Down) => { - self.selected = (self.selected + 1).min(match &self.plugin { - Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1, - _ => unimplemented!() - }); - Ok(Some(true)) - }, - key!(KeyCode::PageUp) => { - self.selected = self.selected.saturating_sub(8); - Ok(Some(true)) - }, - key!(KeyCode::PageDown) => { - self.selected = (self.selected + 10).min(match &self.plugin { - Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1, - _ => unimplemented!() - }); - Ok(Some(true)) - }, - key!(KeyCode::Char(',')) => { - match self.plugin.as_mut() { - Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => { - let index = port_list[self.selected].index; - if let Some(value) = instance.control_input(index) { - instance.set_control_input(index, value - 0.01); - } - }, - _ => {} - } - Ok(Some(true)) - }, - key!(KeyCode::Char('.')) => { - match self.plugin.as_mut() { - Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => { - let index = port_list[self.selected].index; - if let Some(value) = instance.control_input(index) { - instance.set_control_input(index, value + 0.01); - } - }, - _ => {} - } - Ok(Some(true)) - }, - key!(KeyCode::Char('g')) => { - match self.plugin { - Some(PluginKind::LV2(ref mut plugin)) => { - plugin.ui_thread = Some(run_lv2_ui(LV2PluginUI::new()?)?); - }, - Some(_) => unreachable!(), - None => {} - } - Ok(Some(true)) - }, - _ => Ok(None) - } - } -} diff --git a/crates/tek/src/tui/_todo_tui_plugin_lv2.rs b/crates/tek/src/tui/_todo_tui_plugin_lv2.rs deleted file mode 100644 index a71fea12..00000000 --- a/crates/tek/src/tui/_todo_tui_plugin_lv2.rs +++ /dev/null @@ -1,46 +0,0 @@ -use super::*; -use ::livi::{ - World, - Instance, - Plugin as LiviPlugin, - Features, - FeaturesBuilder, - Port, - event::LV2AtomSequence, -}; -use std::thread::JoinHandle; - -/// A LV2 plugin. -pub struct LV2Plugin { - pub world: World, - pub instance: Instance, - pub plugin: LiviPlugin, - pub features: Arc, - pub port_list: Vec, - pub input_buffer: Vec, - pub ui_thread: Option>, -} - -impl LV2Plugin { - const INPUT_BUFFER: usize = 1024; - pub fn new (uri: &str) -> Usually { - // Get 1st plugin at URI - let world = World::with_load_bundle(&uri); - let features = FeaturesBuilder { min_block_length: 1, max_block_length: 65536 }; - let features = world.build_features(features); - let mut plugin = None; - if let Some(p) = world.iter_plugins().next() { plugin = Some(p); } - let plugin = plugin.expect("plugin not found"); - let err = &format!("init {uri}"); - let instance = unsafe { plugin.instantiate(features.clone(), 48000.0).expect(&err) }; - let mut port_list = vec![]; - for port in plugin.ports() { - port_list.push(port); - } - let input_buffer = Vec::with_capacity(Self::INPUT_BUFFER); - // Instantiate - Ok(Self { - world, instance, port_list, plugin, features, input_buffer, ui_thread: None - }) - } -} diff --git a/crates/tek/src/tui/_todo_tui_plugin_lv2_gui.rs b/crates/tek/src/tui/_todo_tui_plugin_lv2_gui.rs deleted file mode 100644 index a296eee5..00000000 --- a/crates/tek/src/tui/_todo_tui_plugin_lv2_gui.rs +++ /dev/null @@ -1,58 +0,0 @@ -use crate::*; -use std::thread::{spawn, JoinHandle}; -use ::winit::{ - application::ApplicationHandler, - event::WindowEvent, - event_loop::{ActiveEventLoop, ControlFlow, EventLoop}, - window::{Window, WindowId}, - platform::x11::EventLoopBuilderExtX11 -}; - -//pub struct LV2PluginUI { - //write: (), - //controller: (), - //widget: (), - //features: (), - //transfer: (), -//} - -pub fn run_lv2_ui (mut ui: LV2PluginUI) -> Usually> { - Ok(spawn(move||{ - let event_loop = EventLoop::builder().with_x11().with_any_thread(true).build().unwrap(); - event_loop.set_control_flow(ControlFlow::Wait); - event_loop.run_app(&mut ui).unwrap() - })) -} - -/// A LV2 plugin's X11 UI. -pub struct LV2PluginUI { - pub window: Option -} - -impl LV2PluginUI { - pub fn new () -> Usually { - Ok(Self { window: None }) - } -} - -impl ApplicationHandler for LV2PluginUI { - fn resumed (&mut self, event_loop: &ActiveEventLoop) { - self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap()); - } - fn window_event (&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) { - match event { - WindowEvent::CloseRequested => { - self.window.as_ref().unwrap().set_visible(false); - event_loop.exit(); - }, - WindowEvent::RedrawRequested => { - self.window.as_ref().unwrap().request_redraw(); - } - _ => (), - } - } -} - -fn lv2_ui_instantiate (kind: &str) { - //let host = Suil -} diff --git a/crates/tek/src/tui/_todo_tui_plugin_vst3.rs b/crates/tek/src/tui/_todo_tui_plugin_vst3.rs deleted file mode 100644 index 46330df3..00000000 --- a/crates/tek/src/tui/_todo_tui_plugin_vst3.rs +++ /dev/null @@ -1 +0,0 @@ -//! TODO diff --git a/crates/tek/src/tui/_todo_tui_sampler.rs b/crates/tek/src/tui/_todo_tui_sampler.rs deleted file mode 100644 index 791144dc..00000000 --- a/crates/tek/src/tui/_todo_tui_sampler.rs +++ /dev/null @@ -1,412 +0,0 @@ -use crate::*; - -/// The sampler plugin plays sounds. -pub struct SamplerView { - _engine: PhantomData, - pub state: Sampler, - pub cursor: (usize, usize), - pub editing: Option>>, - pub buffer: Vec>, - pub modal: Arc>>>, -} - -impl SamplerView { - pub fn new ( - jack: &Arc>, - name: &str, - mapped: Option>>> - ) -> Usually> { - Jack::new(name)? - .midi_in("midi") - .audio_in("recL") - .audio_in("recR") - .audio_out("outL") - .audio_out("outR") - .run(|ports|Box::new(Self { - _engine: Default::default(), - jack: jack.clone(), - name: name.into(), - cursor: (0, 0), - editing: None, - mapped: mapped.unwrap_or_else(||BTreeMap::new()), - unmapped: vec![], - voices: Arc::new(RwLock::new(vec![])), - ports, - buffer: vec![vec![0.0;16384];2], - output_gain: 0.5, - modal: Default::default() - })) - } - /// Immutable reference to sample at cursor. - pub fn sample (&self) -> Option<&Arc>> { - for (i, sample) in self.mapped.values().enumerate() { - if i == self.cursor.0 { - return Some(sample) - } - } - for (i, sample) in self.unmapped.iter().enumerate() { - if i + self.mapped.len() == self.cursor.0 { - return Some(sample) - } - } - None - } -} - -/// A sound sample. -#[derive(Default, Debug)] -pub struct Sample { - pub name: String, - pub start: usize, - pub end: usize, - pub channels: Vec>, - pub rate: Option, -} - -/// Load sample from WAV and assign to MIDI note. -#[macro_export] macro_rules! sample { - ($note:expr, $name:expr, $src:expr) => {{ - let (end, data) = read_sample_data($src)?; - ( - u7::from_int_lossy($note).into(), - Sample::new($name, 0, end, data).into() - ) - }}; -} - -use std::fs::File; -use symphonia::core::codecs::CODEC_TYPE_NULL; -use symphonia::core::errors::Error; -use symphonia::core::io::MediaSourceStream; -use symphonia::core::probe::Hint; -use symphonia::core::audio::SampleBuffer; -use symphonia::default::get_codecs; - -pub struct AddSampleModal { - exited: bool, - dir: PathBuf, - subdirs: Vec, - files: Vec, - cursor: usize, - offset: usize, - sample: Arc>, - voices: Arc>>, - _search: Option, -} - -impl Exit for AddSampleModal { - fn exited (&self) -> bool { - self.exited - } - fn exit (&mut self) { - self.exited = true - } -} - -impl AddSampleModal { - pub fn new ( - sample: &Arc>, - voices: &Arc>> - ) -> Usually { - let dir = std::env::current_dir()?; - let (subdirs, files) = scan(&dir)?; - Ok(Self { - exited: false, - dir, - subdirs, - files, - cursor: 0, - offset: 0, - sample: sample.clone(), - voices: voices.clone(), - _search: None - }) - } - fn rescan (&mut self) -> Usually<()> { - scan(&self.dir).map(|(subdirs, files)|{ - self.subdirs = subdirs; - self.files = files; - }) - } - fn prev (&mut self) { - self.cursor = self.cursor.saturating_sub(1); - } - fn next (&mut self) { - self.cursor = self.cursor + 1; - } - fn try_preview (&mut self) -> Usually<()> { - if let Some(path) = self.cursor_file() { - if let Ok(sample) = Sample::from_file(&path) { - *self.sample.write().unwrap() = sample; - self.voices.write().unwrap().push( - Sample::play(&self.sample, 0, &u7::from(100u8)) - ); - } - //load_sample(&path)?; - //let src = std::fs::File::open(&path)?; - //let mss = MediaSourceStream::new(Box::new(src), Default::default()); - //let mut hint = Hint::new(); - //if let Some(ext) = path.extension() { - //hint.with_extension(&ext.to_string_lossy()); - //} - //let meta_opts: MetadataOptions = Default::default(); - //let fmt_opts: FormatOptions = Default::default(); - //if let Ok(mut probed) = symphonia::default::get_probe() - //.format(&hint, mss, &fmt_opts, &meta_opts) - //{ - //panic!("{:?}", probed.format.metadata()); - //}; - } - Ok(()) - } - fn cursor_dir (&self) -> Option { - if self.cursor < self.subdirs.len() { - Some(self.dir.join(&self.subdirs[self.cursor])) - } else { - None - } - } - fn cursor_file (&self) -> Option { - if self.cursor < self.subdirs.len() { - return None - } - let index = self.cursor.saturating_sub(self.subdirs.len()); - if index < self.files.len() { - Some(self.dir.join(&self.files[index])) - } else { - None - } - } - fn pick (&mut self) -> Usually { - if self.cursor == 0 { - if let Some(parent) = self.dir.parent() { - self.dir = parent.into(); - self.rescan()?; - self.cursor = 0; - return Ok(false) - } - } - if let Some(dir) = self.cursor_dir() { - self.dir = dir; - self.rescan()?; - self.cursor = 0; - return Ok(false) - } - if let Some(path) = self.cursor_file() { - let (end, channels) = read_sample_data(&path.to_string_lossy())?; - let mut sample = self.sample.write().unwrap(); - sample.name = path.file_name().unwrap().to_string_lossy().into(); - sample.end = end; - sample.channels = channels; - return Ok(true) - } - return Ok(false) - } -} - -pub const KEYMAP_ADD_SAMPLE: &'static [KeyBinding] = keymap!(AddSampleModal { - [Esc, NONE, "sampler/add/close", "close help dialog", |modal: &mut AddSampleModal|{ - modal.exit(); - Ok(true) - }], - [Up, NONE, "sampler/add/prev", "select previous entry", |modal: &mut AddSampleModal|{ - modal.prev(); - Ok(true) - }], - [Down, NONE, "sampler/add/next", "select next entry", |modal: &mut AddSampleModal|{ - modal.next(); - Ok(true) - }], - [Enter, NONE, "sampler/add/enter", "activate selected entry", |modal: &mut AddSampleModal|{ - if modal.pick()? { - modal.exit(); - } - Ok(true) - }], - [Char('p'), NONE, "sampler/add/preview", "preview selected entry", |modal: &mut AddSampleModal|{ - modal.try_preview()?; - Ok(true) - }] -}); - -fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { - let (mut subdirs, mut files) = read_dir(dir)? - .fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{ - let entry = entry.expect("failed to read drectory entry"); - let meta = entry.metadata().expect("failed to read entry metadata"); - if meta.is_file() { - files.push(entry.file_name()); - } else if meta.is_dir() { - subdirs.push(entry.file_name()); - } - (subdirs, files) - }); - subdirs.sort(); - files.sort(); - Ok((subdirs, files)) -} - -impl Sample { - fn from_file (path: &PathBuf) -> Usually { - let mut sample = Self::default(); - sample.name = path.file_name().unwrap().to_string_lossy().into(); - // Use file extension if present - let mut hint = Hint::new(); - if let Some(ext) = path.extension() { - hint.with_extension(&ext.to_string_lossy()); - } - let probed = symphonia::default::get_probe().format( - &hint, - MediaSourceStream::new( - Box::new(File::open(path)?), - Default::default(), - ), - &Default::default(), - &Default::default() - )?; - let mut format = probed.format; - let mut decoder = get_codecs().make( - &format.tracks().iter() - .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) - .expect("no tracks found") - .codec_params, - &Default::default() - )?; - loop { - match format.next_packet() { - Ok(packet) => { - // Decode a packet - let decoded = match decoder.decode(&packet) { - Ok(decoded) => decoded, - Err(err) => { return Err(err.into()); } - }; - // Determine sample rate - let spec = *decoded.spec(); - if let Some(rate) = sample.rate { - if rate != spec.rate as usize { - panic!("sample rate changed"); - } - } else { - sample.rate = Some(spec.rate as usize); - } - // Determine channel count - while sample.channels.len() < spec.channels.count() { - sample.channels.push(vec![]); - } - // Load sample - let mut samples = SampleBuffer::new( - decoded.frames() as u64, - spec - ); - if samples.capacity() > 0 { - samples.copy_interleaved_ref(decoded); - for frame in samples.samples().chunks(spec.channels.count()) { - for (chan, frame) in frame.iter().enumerate() { - sample.channels[chan].push(*frame) - } - } - } - }, - Err(Error::IoError(_)) => break decoder.last_decoded(), - Err(err) => return Err(err.into()), - }; - }; - sample.end = sample.channels.iter().fold(0, |l, c|l + c.len()); - Ok(sample) - } -} - -impl Render for SamplerView { - fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> { - todo!() - } - fn render (&self, to: &mut TuiOutput) -> Usually<()> { - tui_render_sampler(self, to) - } -} - -pub fn tui_render_sampler (sampler: &SamplerView, to: &mut TuiOutput) -> Usually<()> { - let [x, y, _, height] = to.area(); - let style = Style::default().gray(); - let title = format!(" {} ({})", sampler.name, sampler.voices.read().unwrap().len()); - to.blit(&title, x+1, y, Some(style.white().bold().not_dim())); - let mut width = title.len() + 2; - let mut y1 = 1; - let mut j = 0; - for (note, sample) in sampler.mapped.iter() - .map(|(note, sample)|(Some(note), sample)) - .chain(sampler.unmapped.iter().map(|sample|(None, sample))) - { - if y1 >= height { - break - } - let active = j == sampler.cursor.0; - width = width.max( - draw_sample(to, x, y + y1, note, &*sample.read().unwrap(), active)? - ); - y1 = y1 + 1; - j = j + 1; - } - let height = ((2 + y1) as u16).min(height); - //Ok(Some([x, y, (width as u16).min(to.area().w()), height])) - Ok(()) -} - -fn draw_sample ( - to: &mut TuiOutput, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool -) -> Usually { - let style = if focus { Style::default().green() } else { Style::default() }; - if focus { - to.blit(&"🬴", x+1, y, Some(style.bold())); - } - let label1 = format!("{:3} {:12}", - note.map(|n|n.to_string()).unwrap_or(String::default()), - sample.name); - let label2 = format!("{:>6} {:>6} +0.0", - sample.start, - sample.end); - to.blit(&label1, x+2, y, Some(style.bold())); - to.blit(&label2, x+3+label1.len()as u16, y, Some(style)); - Ok(label1.len() + label2.len() + 4) -} - -impl Render for AddSampleModal { - fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> { - todo!() - //Align::Center(()).layout(to) - } - fn render (&self, to: &mut TuiOutput) -> Usually<()> { - todo!() - //let area = to.area(); - //to.make_dim(); - //let area = center_box( - //area, - //64.max(area.w().saturating_sub(8)), - //20.max(area.w().saturating_sub(8)), - //); - //to.fill_fg(area, Color::Reset); - //to.fill_bg(area, Nord::bg_lo(true, true)); - //to.fill_char(area, ' '); - //to.blit(&format!("{}", &self.dir.to_string_lossy()), area.x()+2, area.y()+1, Some(Style::default().bold()))?; - //to.blit(&"Select sample:", area.x()+2, area.y()+2, Some(Style::default().bold()))?; - //for (i, (is_dir, name)) in self.subdirs.iter() - //.map(|path|(true, path)) - //.chain(self.files.iter().map(|path|(false, path))) - //.enumerate() - //.skip(self.offset) - //{ - //if i >= area.h() as usize - 4 { - //break - //} - //let t = if is_dir { "" } else { "" }; - //let line = format!("{t} {}", name.to_string_lossy()); - //let line = &line[..line.len().min(area.w() as usize - 4)]; - //to.blit(&line, area.x() + 2, area.y() + 3 + i as u16, Some(if i == self.cursor { - //Style::default().green() - //} else { - //Style::default().white() - //}))?; - //} - //Lozenge(Style::default()).draw(to) - } -} diff --git a/crates/tek/src/tui/_todo_tui_sampler_cmd.rs b/crates/tek/src/tui/_todo_tui_sampler_cmd.rs deleted file mode 100644 index a1b9e16a..00000000 --- a/crates/tek/src/tui/_todo_tui_sampler_cmd.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::*; -impl Handle for Sampler { - fn handle (&mut self, from: &TuiInput) -> Perhaps { - match from.event() { - key!(KeyCode::Up) => { - self.cursor.0 = if self.cursor.0 == 0 { - self.mapped.len() + self.unmapped.len() - 1 - } else { - self.cursor.0 - 1 - }; - Ok(Some(true)) - }, - key!(KeyCode::Down) => { - self.cursor.0 = (self.cursor.0 + 1) % (self.mapped.len() + self.unmapped.len()); - Ok(Some(true)) - }, - key!(KeyCode::Char('p')) => { - if let Some(sample) = self.sample() { - self.voices.write().unwrap().push(Sample::play(sample, 0, &100.into())); - } - Ok(Some(true)) - }, - key!(KeyCode::Char('a')) => { - let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); - *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &self.voices)?)); - self.unmapped.push(sample); - Ok(Some(true)) - }, - key!(KeyCode::Char('r')) => { - if let Some(sample) = self.sample() { - *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &self.voices)?)); - } - Ok(Some(true)) - }, - key!(KeyCode::Enter) => { - if let Some(sample) = self.sample() { - self.editing = Some(sample.clone()); - } - Ok(Some(true)) - } - _ => Ok(None) - } - } -} -impl Handle for AddSampleModal { - fn handle (&mut self, from: &TuiInput) -> Perhaps { - if from.handle_keymap(self, KEYMAP_ADD_SAMPLE)? { - return Ok(Some(true)) - } - Ok(Some(true)) - } -} diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs deleted file mode 100644 index f5cc1afe..00000000 --- a/crates/tek/src/tui/app_arranger.rs +++ /dev/null @@ -1,1311 +0,0 @@ -use crate::{ - *, - api::{ - ArrangerTrackCommand, - ArrangerSceneCommand, - ArrangerClipCommand - } -}; - - -impl TryFrom<&Arc>> for ArrangerTui { - type Error = Box; - fn try_from (jack: &Arc>) -> Usually { - Ok(Self { - jack: jack.clone(), - clock: ClockModel::from(jack), - phrases: PhraseListModel::default(), - editor: PhraseEditorModel::default(), - selected: ArrangerSelection::Clip(0, 0), - scenes: vec![], - tracks: vec![], - color: Color::Rgb(28, 35, 25).into(), - history: vec![], - mode: ArrangerMode::Vertical(2), - name: Arc::new(RwLock::new(String::new())), - size: Measure::new(), - cursor: (0, 0), - splits: [20, 20], - entered: false, - menu_bar: None, - status_bar: None, - midi_buf: vec![vec![];65536], - note_buf: vec![], - perf: PerfModel::default(), - focus: FocusState::Entered(ArrangerFocus::Transport(TransportFocus::PlayPause)), - }) - } -} - -/// Root view for standalone `tek_arranger` -pub struct ArrangerTui { - pub jack: Arc>, - pub clock: ClockModel, - pub phrases: PhraseListModel, - pub tracks: Vec, - pub scenes: Vec, - pub name: Arc>, - pub splits: [u16;2], - pub selected: ArrangerSelection, - pub mode: ArrangerMode, - pub color: ItemColor, - pub entered: bool, - pub size: Measure, - pub cursor: (usize, usize), - pub menu_bar: Option>, - pub status_bar: Option, - pub history: Vec, - pub note_buf: Vec, - pub midi_buf: Vec>>, - pub editor: PhraseEditorModel, - pub focus: FocusState, - pub perf: PerfModel, -} - -impl JackApi for ArrangerTui { - fn jack (&self) -> &Arc> { - &self.jack - } -} - -impl Audio for ArrangerTui { - #[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { - // Start profiling cycle - let t0 = self.perf.get_t0(); - - // Update transport clock - if ClockAudio(self).process(client, scope) == Control::Quit { - return Control::Quit - } - - // Update MIDI sequencers - if TracksAudio( - &mut self.tracks, - &mut self.note_buf, - &mut self.midi_buf, - Default::default(), - ).process(client, scope) == Control::Quit { - return Control::Quit - } - - // FIXME: one of these per playing track - //self.now.set(0.); - //if let ArrangerSelection::Clip(t, s) = self.selected { - //let phrase = self.scenes().get(s).map(|scene|scene.clips.get(t)); - //if let Some(Some(Some(phrase))) = phrase { - //if let Some(track) = self.tracks().get(t) { - //if let Some((ref started_at, Some(ref playing))) = track.player.play_phrase { - //let phrase = phrase.read().unwrap(); - //if *playing.read().unwrap() == *phrase { - //let pulse = self.current().pulse.get(); - //let start = started_at.pulse.get(); - //let now = (pulse - start) % phrase.length as f64; - //self.now.set(now); - //} - //} - //} - //} - //} - - // End profiling cycle - self.perf.update(t0, scope); - - return Control::Continue - } -} - -// Layout for standalone arranger app. -render!(|self: ArrangerTui|{ - let arranger_focused = self.arranger_focused(); - let border = Lozenge(Style::default().bg(TuiTheme::border_bg()).fg(TuiTheme::border_fg(arranger_focused))); - col!([ - TransportView::from((self, if let ArrangerFocus::Transport(_) = self.focus.inner() { - true - } else { - false - })), - col!([ - Tui::fixed_y(self.splits[0], lay!([ - border.wrap(Tui::grow_y(1, Layers::new(move |add|{ - match self.mode { - ArrangerMode::Horizontal => - add(&arranger_content_horizontal(self))?, - ArrangerMode::Vertical(factor) => - add(&arranger_content_vertical(self, factor))? - }; - add(&self.size) - }))), - self.size, - Tui::push_x(1, Tui::fg( - TuiTheme::title_fg(arranger_focused), - format!("[{}] Arranger", if self.entered { - "■" - } else { - " " - }) - )) - ])), - Split::right( - self.splits[1], - PhraseListView::from(self), - PhraseView::from(self), - ) - ]) - ]) -}); - -impl HasClock for ArrangerTui { - fn clock (&self) -> &ClockModel { - &self.clock - } -} -impl HasClock for ArrangerTrack { - fn clock (&self) -> &ClockModel { - &self.player.clock() - } -} -impl HasPhrases for ArrangerTui { - fn phrases (&self) -> &Vec>> { - &self.phrases.phrases - } - fn phrases_mut (&mut self) -> &mut Vec>> { - &mut self.phrases.phrases - } -} - -/// Sections in the arranger app that may be focused -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum ArrangerFocus { - /// The transport (toolbar) is focused - Transport(TransportFocus), - /// The arrangement (grid) is focused - Arranger, - /// The phrase list (pool) is focused - Phrases, - /// The phrase editor (sequencer) is focused - PhraseEditor, -} - -impl From<&ArrangerTui> for Option { - fn from (state: &ArrangerTui) -> Self { - match state.focus.inner() { - ArrangerFocus::Transport(focus) => Some(focus), - _ => None - } - } -} - -impl_focus!(ArrangerTui ArrangerFocus [ - //&[ - //Menu, - //Menu, - //Menu, - //Menu, - //Menu, - //], - &[ - Transport(TransportFocus::PlayPause), - Transport(TransportFocus::Bpm), - Transport(TransportFocus::Sync), - Transport(TransportFocus::Quant), - Transport(TransportFocus::Clock), - ], &[ - Arranger, - Arranger, - Arranger, - Arranger, - Arranger, - ], &[ - Phrases, - Phrases, - PhraseEditor, - PhraseEditor, - PhraseEditor, - ], -]); - -/// Status bar for arranger app -#[derive(Copy, Clone, Debug)] -pub enum ArrangerStatus { - Transport, - ArrangerMix, - ArrangerTrack, - ArrangerScene, - ArrangerClip, - PhrasePool, - PhraseView, - PhraseEdit, -} - -impl StatusBar for ArrangerStatus { - type State = (ArrangerFocus, ArrangerSelection, bool); - fn hotkey_fg () -> Color where Self: Sized { - TuiTheme::HOTKEY_FG - } - fn update (&mut self, (focused, selected, entered): &Self::State) { - *self = match focused { - //ArrangerFocus::Menu => { todo!() }, - ArrangerFocus::Transport(_) => ArrangerStatus::Transport, - ArrangerFocus::Arranger => match selected { - ArrangerSelection::Mix => ArrangerStatus::ArrangerMix, - ArrangerSelection::Track(_) => ArrangerStatus::ArrangerTrack, - ArrangerSelection::Scene(_) => ArrangerStatus::ArrangerScene, - ArrangerSelection::Clip(_, _) => ArrangerStatus::ArrangerClip, - }, - ArrangerFocus::Phrases => ArrangerStatus::PhrasePool, - ArrangerFocus::PhraseEditor => match entered { - true => ArrangerStatus::PhraseEdit, - false => ArrangerStatus::PhraseView, - }, - } - } -} - -render!(|self: ArrangerStatus|{ - - let label = match self { - Self::Transport => "TRANSPORT", - Self::ArrangerMix => "PROJECT", - Self::ArrangerTrack => "TRACK", - Self::ArrangerScene => "SCENE", - Self::ArrangerClip => "CLIP", - Self::PhrasePool => "SEQ LIST", - Self::PhraseView => "VIEW SEQ", - Self::PhraseEdit => "EDIT SEQ", - }; - - let status_bar_bg = TuiTheme::status_bar_bg(); - - let mode_bg = TuiTheme::mode_bg(); - let mode_fg = TuiTheme::mode_fg(); - let mode = Tui::fg(mode_fg, Tui::bg(mode_bg, Tui::bold(true, format!(" {label} ")))); - - let commands = match self { - Self::ArrangerMix => Self::command(&[ - ["", "c", "olor"], - ["", "<>", "resize"], - ["", "+-", "zoom"], - ["", "n", "ame/number"], - ["", "Enter", " stop all"], - ]), - Self::ArrangerClip => Self::command(&[ - ["", "g", "et"], - ["", "s", "et"], - ["", "a", "dd"], - ["", "i", "ns"], - ["", "d", "up"], - ["", "e", "dit"], - ["", "c", "olor"], - ["re", "n", "ame"], - ["", ",.", "select"], - ["", "Enter", " launch"], - ]), - Self::ArrangerTrack => Self::command(&[ - ["re", "n", "ame"], - ["", ",.", "resize"], - ["", "<>", "move"], - ["", "i", "nput"], - ["", "o", "utput"], - ["", "m", "ute"], - ["", "s", "olo"], - ["", "Del", "ete"], - ["", "Enter", " stop"], - ]), - Self::ArrangerScene => Self::command(&[ - ["re", "n", "ame"], - ["", "Del", "ete"], - ["", "Enter", " launch"], - ]), - Self::PhrasePool => Self::command(&[ - ["", "a", "ppend"], - ["", "i", "nsert"], - ["", "d", "uplicate"], - ["", "Del", "ete"], - ["", "c", "olor"], - ["re", "n", "ame"], - ["leng", "t", "h"], - ["", ",.", "move"], - ["", "+-", "resize view"], - ]), - Self::PhraseView => Self::command(&[ - ["", "enter", " edit"], - ["", "arrows/pgup/pgdn", " scroll"], - ["", "+=", "zoom"], - ]), - Self::PhraseEdit => Self::command(&[ - ["", "esc", " exit"], - ["", "a", "ppend"], - ["", "s", "et"], - ["", "][", "length"], - ["", "+-", "zoom"], - ]), - _ => Self::command(&[]) - }; - - //let commands = commands.iter().reduce(String::new(), |s, (a, b, c)| format!("{s} {a}{b}{c}")); - Tui::bg(status_bar_bg, Tui::fill_x(row!([mode, commands]))) - -}); - -/// Display mode of arranger -#[derive(Clone, PartialEq)] -pub enum ArrangerMode { - /// Tracks are rows - Horizontal, - /// Tracks are columns - Vertical(usize), -} - -/// Arranger display mode can be cycled -impl ArrangerMode { - /// Cycle arranger display mode - pub fn to_next (&mut self) { - *self = match self { - Self::Horizontal => Self::Vertical(1), - Self::Vertical(1) => Self::Vertical(2), - Self::Vertical(2) => Self::Vertical(2), - Self::Vertical(0) => Self::Horizontal, - Self::Vertical(_) => Self::Vertical(0), - } - } -} - -pub trait ArrangerViewState { - fn arranger_focused (&self) -> bool; -} -impl ArrangerViewState for ArrangerTui { - fn arranger_focused (&self) -> bool { - self.focused() == ArrangerFocus::Arranger - } -} - -fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> { - let mut widths = vec![]; - let mut total = 0; - for track in tracks.iter() { - let width = track.width; - widths.push((width, total)); - total += width; - } - widths.push((0, total)); - widths -} - -fn any_size (_: E::Size) -> Perhaps{ - Ok(Some([0.into(),0.into()].into())) -} - -pub fn arranger_content_vertical ( - view: &ArrangerTui, - factor: usize -) -> impl Render + use<'_> { - lay!([ - Tui::at_se(Tui::fill_xy(Tui::pull_x(1, Tui::fg(TuiTheme::title_fg(view.arranger_focused()), - format!("{}x{}", view.size.w(), view.size.h())) - ))), - Tui::bg(view.color.rgb, lay!(![ - ArrangerVerticalColumnSeparator::from(view), - ArrangerVerticalRowSeparator::from((view, factor)), - col!(![ - ArrangerVerticalHeader::from(view), - ArrangerVerticalContent::from((view, factor)), - ]), - ArrangerVerticalCursor::from((view, factor)), - ])), - ]) -} - -struct ArrangerVerticalColumnSeparator { - cols: Vec<(usize, usize)>, - scenes_w: u16, - sep_fg: Color, -} -impl From<&ArrangerTui> for ArrangerVerticalColumnSeparator { - fn from (state: &ArrangerTui) -> Self { - Self { - cols: track_widths(state.tracks()), - scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, - sep_fg: TuiTheme::separator_fg(false), - } - } -} -render!(|self: ArrangerVerticalColumnSeparator|render(move|to: &mut TuiOutput|{ - let style = Some(Style::default().fg(self.sep_fg)); - Ok(for x in self.cols.iter().map(|col|col.1) { - let x = self.scenes_w + to.area().x() + x as u16; - for y in to.area().y()..to.area().y2() { - to.blit(&"▎", x, y, style); - } - }) -})); - -struct ArrangerVerticalRowSeparator { - rows: Vec<(usize, usize)>, - sep_fg: Color, -} -impl From<(&ArrangerTui, usize)> for ArrangerVerticalRowSeparator { - fn from ((state, factor): (&ArrangerTui, usize)) -> Self { - Self { - rows: ArrangerScene::ppqs(state.scenes(), factor), - sep_fg: TuiTheme::separator_fg(false), - } - } -} - -render!(|self: ArrangerVerticalRowSeparator|render(move|to: &mut TuiOutput|{ - Ok(for y in self.rows.iter().map(|row|row.1) { - let y = to.area().y() + (y / PPQ) as u16 + 1; - if y >= to.buffer.area.height { break } - for x in to.area().x()..to.area().x2().saturating_sub(2) { - if x < to.buffer.area.x && y < to.buffer.area.y { - let cell = to.buffer.get_mut(x, y); - cell.modifier = Modifier::UNDERLINED; - cell.underline_color = self.sep_fg; - } - } - }) -})); - -struct ArrangerVerticalCursor { - cols: Vec<(usize, usize)>, - rows: Vec<(usize, usize)>, - focused: bool, - selected: ArrangerSelection, - scenes_w: u16, - header_h: u16, -} -impl From<(&ArrangerTui, usize)> for ArrangerVerticalCursor { - fn from ((state, factor): (&ArrangerTui, usize)) -> Self { - Self { - cols: track_widths(state.tracks()), - rows: ArrangerScene::ppqs(state.scenes(), factor), - focused: state.arranger_focused(), - selected: state.selected, - scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, - header_h: 3, - } - } -} -render!(|self: ArrangerVerticalCursor|render(move|to: &mut TuiOutput|{ - let area = to.area(); - let focused = self.focused; - let selected = self.selected; - let get_track_area = |t: usize| [ - self.scenes_w + area.x() + self.cols[t].1 as u16, area.y(), - self.cols[t].0 as u16, area.h(), - ]; - let get_scene_area = |s: usize| [ - area.x(), self.header_h + area.y() + (self.rows[s].1 / PPQ) as u16, - area.w(), (self.rows[s].0 / PPQ) as u16 - ]; - let get_clip_area = |t: usize, s: usize| [ - self.scenes_w + area.x() + self.cols[t].1 as u16, - self.header_h + area.y() + (self.rows[s].1/PPQ) as u16, - self.cols[t].0 as u16, - (self.rows[s].0 / PPQ) as u16 - ]; - let mut track_area: Option<[u16;4]> = None; - let mut scene_area: Option<[u16;4]> = None; - let mut clip_area: Option<[u16;4]> = None; - let area = match selected { - ArrangerSelection::Mix => area, - ArrangerSelection::Track(t) => { - track_area = Some(get_track_area(t)); - area - }, - ArrangerSelection::Scene(s) => { - scene_area = Some(get_scene_area(s)); - area - }, - ArrangerSelection::Clip(t, s) => { - track_area = Some(get_track_area(t)); - scene_area = Some(get_scene_area(s)); - clip_area = Some(get_clip_area(t, s)); - area - }, - }; - let bg = TuiTheme::border_bg(); - if let Some([x, y, width, height]) = track_area { - to.fill_fg([x, y, 1, height], bg); - to.fill_fg([x + width, y, 1, height], bg); - } - if let Some([_, y, _, height]) = scene_area { - to.fill_ul([area.x(), y - 1, area.w(), 1], bg); - to.fill_ul([area.x(), y + height - 1, area.w(), 1], bg); - } - Ok(if focused { - to.render_in(if let Some(clip_area) = clip_area { clip_area } - else if let Some(track_area) = track_area { track_area.clip_h(self.header_h) } - else if let Some(scene_area) = scene_area { scene_area.clip_w(self.scenes_w) } - else { area.clip_w(self.scenes_w).clip_h(self.header_h) }, &CORNERS)? - }) -})); - -struct ArrangerVerticalHeader<'a> { - tracks: &'a Vec, - cols: Vec<(usize, usize)>, - focused: bool, - selected: ArrangerSelection, - scenes_w: u16, - header_h: u16, - timebase: &'a Arc, - current: &'a Arc, -} -impl<'a> From<&'a ArrangerTui> for ArrangerVerticalHeader<'a> { - fn from (state: &'a ArrangerTui) -> Self { - Self { - tracks: &state.tracks, - cols: track_widths(state.tracks()), - focused: state.arranger_focused(), - selected: state.selected, - scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, - header_h: 3, - timebase: state.clock().timebase(), - current: &state.clock().playhead, - } - } -} -render!(|self: ArrangerVerticalHeader<'a>|row!( - (track, w) in self.tracks.iter().zip(self.cols.iter().map(|col|col.0)) => { - // name and width of track - let name = track.name().read().unwrap(); - let max_w = w.saturating_sub(1).min(name.len()).max(2); - let name = format!("▎{}", &name[0..max_w]); - let name = Tui::bold(true, name); - // beats elapsed - let elapsed = if let Some((_, Some(phrase))) = track.player.play_phrase().as_ref() { - let length = phrase.read().unwrap().length; - let elapsed = track.player.pulses_since_start().unwrap(); - let elapsed = self.timebase.format_beats_1_short( - (elapsed as usize % length) as f64 - ); - format!("▎+{elapsed:>}") - } else { - String::from("▎") - }; - // beats until switchover - let until_next = track.player.next_phrase().as_ref().map(|(t, _)|{ - let target = t.pulse.get(); - let current = self.current.pulse.get(); - if target > current { - let remaining = target - current; - format!("▎-{:>}", self.timebase.format_beats_0_short(remaining)) - } else { - String::new() - } - }).unwrap_or(String::from("▎")); - let timer = col!([until_next, elapsed]); - // name of active MIDI input - let _input = format!("▎>{}", track.player.midi_ins().get(0) - .map(|port|port.short_name()) - .transpose()? - .unwrap_or("(none)".into())); - // name of active MIDI output - let _output = format!("▎<{}", track.player.midi_outs().get(0) - .map(|port|port.short_name()) - .transpose()? - .unwrap_or("(none)".into())); - Tui::push_x(self.scenes_w, - Tui::bg(track.color().rgb, - Tui::min_xy(w as u16, self.header_h, - col!([name, timer])))) - } -)); - -struct ArrangerVerticalContent<'a> { - size: &'a Measure, - scenes: &'a Vec, - tracks: &'a Vec, - rows: Vec<(usize, usize)>, - cols: Vec<(usize, usize)>, - header_h: u16, -} -impl<'a> From<(&'a ArrangerTui, usize)> for ArrangerVerticalContent<'a> { - fn from ((state, factor): (&'a ArrangerTui, usize)) -> Self { - Self { - size: &state.size, - scenes: &state.scenes, - tracks: &state.tracks, - rows: ArrangerScene::ppqs(state.scenes(), factor), - cols: track_widths(state.tracks()), - header_h: 3, - } - } -} -render!(|self: ArrangerVerticalContent<'a>|Tui::fixed_y( - (self.size.h() as u16).saturating_sub(self.header_h), - col!((scene, pulses) in self.scenes.iter().zip(self.rows.iter().map(|row|row.0)) => { - let height = 1.max((pulses / PPQ) as u16); - let playing = scene.is_playing(self.tracks); - Tui::fixed_y(height, row!([ - if playing { "▶ " } else { " " }, - Tui::bold(true, scene.name.read().unwrap().as_str()), - row!((track, w) in self.cols.iter().map(|col|col.0).enumerate() => { - Tui::fixed_xy(w as u16, height, Layers::new(move |add|{ - let mut bg = TuiTheme::border_bg(); - match (self.tracks.get(track), scene.clips.get(track)) { - (Some(track), Some(Some(phrase))) => { - let name = &(phrase as &Arc>).read().unwrap().name; - let name = format!("{}", name); - let max_w = name.len().min((w as usize).saturating_sub(2)); - let color = phrase.read().unwrap().color; - bg = color.dark.rgb; - if let Some((_, Some(ref playing))) = track.player.play_phrase() { - if *playing.read().unwrap() == *phrase.read().unwrap() { - bg = color.light.rgb - } - }; - add(&Tui::fixed_x(w as u16, Tui::push_x(1, &name.as_str()[0..max_w])))?; - }, - _ => {} - }; - //add(&Background(bg)) - Ok(()) - })) - })]) - ) - }) -)); - -pub fn arranger_content_horizontal ( - view: &ArrangerTui, -) -> impl Render + use<'_> { - todo!() -} - //let focused = view.arranger_focused(); - //let _tracks = view.tracks(); - //lay!( - //focused.then_some(Background(TuiTheme::border_bg())), - //row!( - //// name - //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ - //todo!() - ////let Self(tracks, selected) = self; - ////let yellow = Some(Style::default().yellow().bold().not_dim()); - ////let white = Some(Style::default().white().bold().not_dim()); - ////let area = to.area(); - ////let area = [area.x(), area.y(), 3 + 5.max(track_name_max_len(tracks)) as u16, area.h()]; - ////let offset = 0; // track scroll offset - ////for y in 0..area.h() { - ////if y == 0 { - ////to.blit(&"Mixer", area.x() + 1, area.y() + y, Some(DIM))?; - ////} else if y % 2 == 0 { - ////let index = (y as usize - 2) / 2 + offset; - ////if let Some(track) = tracks.get(index) { - ////let selected = selected.track() == Some(index); - ////let style = if selected { yellow } else { white }; - ////to.blit(&format!(" {index:>02} "), area.x(), area.y() + y, style)?; - ////to.blit(&*track.name.read().unwrap(), area.x() + 4, area.y() + y, style)?; - ////} - ////} - ////} - ////Ok(Some(area)) - //}), - //// monitor - //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ - //todo!() - ////let Self(tracks) = self; - ////let mut area = to.area(); - ////let on = Some(Style::default().not_dim().green().bold()); - ////let off = Some(DIM); - ////area.x += 1; - ////for y in 0..area.h() { - ////if y == 0 { - //////" MON ".blit(to.buffer, area.x, area.y + y, style2)?; - ////} else if y % 2 == 0 { - ////let index = (y as usize - 2) / 2; - ////if let Some(track) = tracks.get(index) { - ////let style = if track.monitoring { on } else { off }; - ////to.blit(&" MON ", area.x(), area.y() + y, style)?; - ////} else { - ////area.height = y; - ////break - ////} - ////} - ////} - ////area.width = 4; - ////Ok(Some(area)) - //}), - //// record - //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ - //todo!() - ////let Self(tracks) = self; - ////let mut area = to.area(); - ////let on = Some(Style::default().not_dim().red().bold()); - ////let off = Some(Style::default().dim()); - ////area.x += 1; - ////for y in 0..area.h() { - ////if y == 0 { - //////" REC ".blit(to.buffer, area.x, area.y + y, style2)?; - ////} else if y % 2 == 0 { - ////let index = (y as usize - 2) / 2; - ////if let Some(track) = tracks.get(index) { - ////let style = if track.recording { on } else { off }; - ////to.blit(&" REC ", area.x(), area.y() + y, style)?; - ////} else { - ////area.height = y; - ////break - ////} - ////} - ////} - ////area.width = 4; - ////Ok(Some(area)) - //}), - //// overdub - //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ - //todo!() - ////let Self(tracks) = self; - ////let mut area = to.area(); - ////let on = Some(Style::default().not_dim().yellow().bold()); - ////let off = Some(Style::default().dim()); - ////area.x = area.x + 1; - ////for y in 0..area.h() { - ////if y == 0 { - //////" OVR ".blit(to.buffer, area.x, area.y + y, style2)?; - ////} else if y % 2 == 0 { - ////let index = (y as usize - 2) / 2; - ////if let Some(track) = tracks.get(index) { - ////to.blit(&" OVR ", area.x(), area.y() + y, if track.overdub { - ////on - ////} else { - ////off - ////})?; - ////} else { - ////area.height = y; - ////break - ////} - ////} - ////} - ////area.width = 4; - ////Ok(Some(area)) - //}), - //// erase - //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ - //todo!() - ////let Self(tracks) = self; - ////let mut area = to.area(); - ////let off = Some(Style::default().dim()); - ////area.x = area.x + 1; - ////for y in 0..area.h() { - ////if y == 0 { - //////" DEL ".blit(to.buffer, area.x, area.y + y, style2)?; - ////} else if y % 2 == 0 { - ////let index = (y as usize - 2) / 2; - ////if let Some(_) = tracks.get(index) { - ////to.blit(&" DEL ", area.x(), area.y() + y, off)?; - ////} else { - ////area.height = y; - ////break - ////} - ////} - ////} - ////area.width = 4; - ////Ok(Some(area)) - //}), - //// gain - //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ - //todo!() - ////let Self(tracks) = self; - ////let mut area = to.area(); - ////let off = Some(Style::default().dim()); - ////area.x = area.x() + 1; - ////for y in 0..area.h() { - ////if y == 0 { - //////" GAIN ".blit(to.buffer, area.x, area.y + y, style2)?; - ////} else if y % 2 == 0 { - ////let index = (y as usize - 2) / 2; - ////if let Some(_) = tracks.get(index) { - ////to.blit(&" +0.0 ", area.x(), area.y() + y, off)?; - ////} else { - ////area.height = y; - ////break - ////} - ////} - ////} - ////area.width = 7; - ////Ok(Some(area)) - //}), - //// scenes - //Widget::new(|_|{todo!()}, |to: &mut TuiOutput|{ - //let [x, y, _, height] = to.area(); - //let mut x2 = 0; - //Ok(for (scene_index, scene) in view.scenes().iter().enumerate() { - //let active_scene = view.selected.scene() == Some(scene_index); - //let sep = Some(if active_scene { - //Style::default().yellow().not_dim() - //} else { - //Style::default().dim() - //}); - //for y in y+1..y+height { - //to.blit(&"│", x + x2, y, sep); - //} - //let name = scene.name.read().unwrap(); - //let mut x3 = name.len() as u16; - //to.blit(&*name, x + x2, y, sep); - //for (i, clip) in scene.clips.iter().enumerate() { - //let active_track = view.selected.track() == Some(i); - //if let Some(clip) = clip { - //let y2 = y + 2 + i as u16 * 2; - //let label = format!("{}", clip.read().unwrap().name); - //to.blit(&label, x + x2, y2, Some(if active_track && active_scene { - //Style::default().not_dim().yellow().bold() - //} else { - //Style::default().not_dim() - //})); - //x3 = x3.max(label.len() as u16) - //} - //} - //x2 = x2 + x3 + 1; - //}) - //}), - //) - //) -//} - -impl HasScenes for ArrangerTui { - fn scenes (&self) -> &Vec { - &self.scenes - } - fn scenes_mut (&mut self) -> &mut Vec { - &mut self.scenes - } - fn scene_add (&mut self, name: Option<&str>, color: Option) - -> Usually<&mut ArrangerScene> - { - let name = name.map_or_else(||self.scene_default_name(), |x|x.to_string()); - let scene = ArrangerScene { - name: Arc::new(name.into()), - clips: vec![None;self.tracks().len()], - color: color.unwrap_or_else(||ItemColor::random()), - }; - self.scenes_mut().push(scene); - let index = self.scenes().len() - 1; - Ok(&mut self.scenes_mut()[index]) - } - fn selected_scene (&self) -> Option<&ArrangerScene> { - self.selected.scene().map(|s|self.scenes().get(s)).flatten() - } - fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> { - self.selected.scene().map(|s|self.scenes_mut().get_mut(s)).flatten() - } -} - -#[derive(Default, Debug, Clone)] -pub struct ArrangerScene { - /// Name of scene - pub(crate) name: Arc>, - /// Clips in scene, one per track - pub(crate) clips: Vec>>>, - /// Identifying color of scene - pub(crate) color: ItemColor, -} - -impl ArrangerSceneApi for ArrangerScene { - fn name (&self) -> &Arc> { - &self.name - } - fn clips (&self) -> &Vec>>> { - &self.clips - } - fn color (&self) -> ItemColor { - self.color - } -} - -impl HasTracks for ArrangerTui { - fn tracks (&self) -> &Vec { - &self.tracks - } - fn tracks_mut (&mut self) -> &mut Vec { - &mut self.tracks - } -} - -impl ArrangerTracksApi for ArrangerTui { - fn track_add (&mut self, name: Option<&str>, color: Option) - -> Usually<&mut ArrangerTrack> - { - let name = name.map_or_else(||self.track_default_name(), |x|x.to_string()); - let track = ArrangerTrack { - width: name.len() + 2, - name: Arc::new(name.into()), - color: color.unwrap_or_else(||ItemColor::random()), - player: PhrasePlayerModel::from(&self.clock), - }; - self.tracks_mut().push(track); - let index = self.tracks().len() - 1; - Ok(&mut self.tracks_mut()[index]) - } - fn track_del (&mut self, index: usize) { - self.tracks_mut().remove(index); - for scene in self.scenes_mut().iter_mut() { - scene.clips.remove(index); - } - } -} - -#[derive(Debug)] -pub struct ArrangerTrack { - /// Name of track - pub(crate) name: Arc>, - /// Preferred width of track column - pub(crate) width: usize, - /// Identifying color of track - pub(crate) color: ItemColor, - /// MIDI player state - pub(crate) player: PhrasePlayerModel, -} - -impl HasPlayer for ArrangerTrack { - fn player (&self) -> &impl MidiPlayerApi { - &self.player - } - fn player_mut (&mut self) -> &mut impl MidiPlayerApi { - &mut self.player - } -} - -impl ArrangerTrackApi for ArrangerTrack { - /// Name of track - fn name (&self) -> &Arc> { - &self.name - } - /// Preferred width of track column - fn width (&self) -> usize { - self.width - } - /// Preferred width of track column - fn width_mut (&mut self) -> &mut usize { - &mut self.width - } - /// Identifying color of track - fn color (&self) -> ItemColor { - self.color - } -} - -#[derive(PartialEq, Clone, Copy, Debug)] -/// Represents the current user selection in the arranger -pub enum ArrangerSelection { - /// The whole mix is selected - Mix, - /// A track is selected. - Track(usize), - /// A scene is selected. - Scene(usize), - /// A clip (track × scene) is selected. - Clip(usize, usize), -} - -/// Focus identification methods -impl ArrangerSelection { - pub fn description ( - &self, - tracks: &Vec, - scenes: &Vec, - ) -> String { - format!("Selected: {}", match self { - Self::Mix => format!("Everything"), - Self::Track(t) => match tracks.get(*t) { - Some(track) => format!("T{t}: {}", &track.name.read().unwrap()), - None => format!("T??"), - }, - Self::Scene(s) => match scenes.get(*s) { - Some(scene) => format!("S{s}: {}", &scene.name.read().unwrap()), - None => format!("S??"), - }, - Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) { - (Some(_), Some(scene)) => match scene.clip(*t) { - Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name), - None => format!("T{t} S{s}: Empty") - }, - _ => format!("T{t} S{s}: Empty"), - } - }) - } - pub fn is_mix (&self) -> bool { - match self { Self::Mix => true, _ => false } - } - pub fn is_track (&self) -> bool { - match self { Self::Track(_) => true, _ => false } - } - pub fn is_scene (&self) -> bool { - match self { Self::Scene(_) => true, _ => false } - } - pub fn is_clip (&self) -> bool { - match self { Self::Clip(_, _) => true, _ => false } - } - pub fn track (&self) -> Option { - use ArrangerSelection::*; - match self { - Clip(t, _) => Some(*t), - Track(t) => Some(*t), - _ => None - } - } - pub fn scene (&self) -> Option { - use ArrangerSelection::*; - match self { - Clip(_, s) => Some(*s), - Scene(s) => Some(*s), - _ => None - } - } -} - -impl Handle for ArrangerTui { - fn handle (&mut self, i: &TuiInput) -> Perhaps { - ArrangerCommand::execute_with_state(self, i) - } -} - -#[derive(Clone, Debug)] -pub enum ArrangerCommand { - Focus(FocusCommand), - Undo, - Redo, - Clear, - Color(ItemColor), - Clock(ClockCommand), - Scene(ArrangerSceneCommand), - Track(ArrangerTrackCommand), - Clip(ArrangerClipCommand), - Select(ArrangerSelection), - Zoom(usize), - Phrases(PhrasesCommand), - Editor(PhraseCommand), -} - -impl Command for ArrangerCommand { - fn execute (self, state: &mut ArrangerTui) -> Perhaps { - use ArrangerCommand::*; - Ok(match self { - Focus(cmd) => cmd.execute(state)?.map(Focus), - Scene(cmd) => cmd.execute(state)?.map(Scene), - Track(cmd) => cmd.execute(state)?.map(Track), - Clip(cmd) => cmd.execute(state)?.map(Clip), - Phrases(cmd) => cmd.execute(&mut state.phrases)?.map(Phrases), - Editor(cmd) => cmd.execute(&mut state.editor)?.map(Editor), - Clock(cmd) => cmd.execute(state)?.map(Clock), - Zoom(_) => { todo!(); }, - Select(selected) => { - *state.selected_mut() = selected; - None - }, - _ => { todo!() } - }) - } -} - -impl Command for ArrangerSceneCommand { - fn execute (self, _state: &mut ArrangerTui) -> Perhaps { - //todo!(); - Ok(None) - } -} - -impl Command for ArrangerTrackCommand { - fn execute (self, _state: &mut ArrangerTui) -> Perhaps { - //todo!(); - Ok(None) - } -} - -impl Command for ArrangerClipCommand { - fn execute (self, _state: &mut ArrangerTui) -> Perhaps { - //todo!(); - Ok(None) - } -} - -pub trait ArrangerControl: TransportControl { - fn selected (&self) -> ArrangerSelection; - fn selected_mut (&mut self) -> &mut ArrangerSelection; - fn activate (&mut self) -> Usually<()>; - fn selected_phrase (&self) -> Option>>; - fn toggle_loop (&mut self); - fn randomize_color (&mut self); -} - -impl ArrangerControl for ArrangerTui { - fn selected (&self) -> ArrangerSelection { - self.selected - } - fn selected_mut (&mut self) -> &mut ArrangerSelection { - &mut self.selected - } - fn activate (&mut self) -> Usually<()> { - if let ArrangerSelection::Scene(s) = self.selected { - for (t, track) in self.tracks.iter_mut().enumerate() { - let phrase = self.scenes[s].clips[t].clone(); - if track.player.play_phrase.is_some() || phrase.is_some() { - track.player.enqueue_next(phrase.as_ref()); - } - } - if self.clock().is_stopped() { - self.clock().play_from(Some(0))?; - } - } else if let ArrangerSelection::Clip(t, s) = self.selected { - let phrase = self.scenes()[s].clips[t].clone(); - self.tracks_mut()[t].player.enqueue_next(phrase.as_ref()); - }; - Ok(()) - } - fn selected_phrase (&self) -> Option>> { - self.selected_scene()?.clips.get(self.selected.track()?)?.clone() - } - fn toggle_loop (&mut self) { - if let Some(phrase) = self.selected_phrase() { - phrase.write().unwrap().toggle_loop() - } - } - fn randomize_color (&mut self) { - match self.selected { - ArrangerSelection::Mix => { - self.color = ItemColor::random_dark() - }, - ArrangerSelection::Track(t) => { - self.tracks_mut()[t].color = ItemColor::random() - }, - ArrangerSelection::Scene(s) => { - self.scenes_mut()[s].color = ItemColor::random() - }, - ArrangerSelection::Clip(t, s) => { - if let Some(phrase) = &self.scenes_mut()[s].clips[t] { - phrase.write().unwrap().color = ItemColorTriplet::random(); - } - } - } - } -} -impl InputToCommand for ArrangerCommand { - fn input_to_command (state: &ArrangerTui, input: &TuiInput) -> Option { - to_arranger_command(state, input) - .or_else(||to_focus_command(input).map(ArrangerCommand::Focus)) - } -} - - -fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option { - use ArrangerCommand as Cmd; - use KeyCode::Char; - if !state.entered() { - return None - } - Some(match input.event() { - key!(Char('e')) => Cmd::Editor(PhraseCommand::Show(Some( - state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone() - ))), - _ => match state.focused() { - ArrangerFocus::Transport(_) => { - match TransportCommand::input_to_command(state, input)? { - TransportCommand::Clock(command) => Cmd::Clock(command), - _ => return None, - } - }, - ArrangerFocus::PhraseEditor => { - Cmd::Editor(PhraseCommand::input_to_command(&state.editor, input)?) - }, - ArrangerFocus::Phrases => { - Cmd::Phrases(PhrasesCommand::input_to_command(&state.phrases, input)?) - }, - ArrangerFocus::Arranger => { - use ArrangerSelection::*; - match input.event() { - key!(Char('l')) => Cmd::Clip(ArrangerClipCommand::SetLoop(false)), - key!(Char('+')) => Cmd::Zoom(0), // TODO - key!(Char('=')) => Cmd::Zoom(0), // TODO - key!(Char('_')) => Cmd::Zoom(0), // TODO - key!(Char('-')) => Cmd::Zoom(0), // TODO - key!(Char('`')) => { todo!("toggle state mode") }, - key!(Ctrl-Char('a')) => Cmd::Scene(ArrangerSceneCommand::Add), - key!(Ctrl-Char('t')) => Cmd::Track(ArrangerTrackCommand::Add), - _ => match state.selected() { - Mix => to_arranger_mix_command(input)?, - Track(t) => to_arranger_track_command(input, t)?, - Scene(s) => to_arranger_scene_command(input, s)?, - Clip(t, s) => to_arranger_clip_command(input, t, s)?, - } - } - } - } - }) -} - -fn to_arranger_mix_command (input: &TuiInput) -> Option { - use KeyCode::{Char, Down, Right, Delete}; - use ArrangerCommand as Cmd; - use ArrangerSelection as Select; - Some(match input.event() { - key!(Down) => Cmd::Select(Select::Scene(0)), - key!(Right) => Cmd::Select(Select::Track(0)), - key!(Char(',')) => Cmd::Zoom(0), - key!(Char('.')) => Cmd::Zoom(0), - key!(Char('<')) => Cmd::Zoom(0), - key!(Char('>')) => Cmd::Zoom(0), - key!(Delete) => Cmd::Clear, - key!(Char('c')) => Cmd::Color(ItemColor::random()), - _ => return None - }) -} - -fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option { - use KeyCode::{Char, Down, Left, Right, Delete}; - use ArrangerCommand as Cmd; - use ArrangerSelection as Select; - use ArrangerTrackCommand as Track; - Some(match input.event() { - key!(Down) => Cmd::Select(Select::Clip(t, 0)), - key!(Left) => Cmd::Select(if t > 0 { Select::Track(t - 1) } else { Select::Mix }), - key!(Right) => Cmd::Select(Select::Track(t + 1)), - key!(Char(',')) => Cmd::Track(Track::Swap(t, t - 1)), - key!(Char('.')) => Cmd::Track(Track::Swap(t, t + 1)), - key!(Char('<')) => Cmd::Track(Track::Swap(t, t - 1)), - key!(Char('>')) => Cmd::Track(Track::Swap(t, t + 1)), - key!(Delete) => Cmd::Track(Track::Delete(t)), - //key!(Char('c')) => Cmd::Track(Track::Color(t, ItemColor::random())), - _ => return None - }) -} - -fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option { - use KeyCode::{Char, Up, Down, Right, Enter, Delete}; - use ArrangerCommand as Cmd; - use ArrangerSelection as Select; - use ArrangerSceneCommand as Scene; - Some(match input.event() { - key!(Up) => Cmd::Select(if s > 0 { Select::Scene(s - 1) } else { Select::Mix }), - key!(Down) => Cmd::Select(Select::Scene(s + 1)), - key!(Right) => Cmd::Select(Select::Clip(0, s)), - key!(Char(',')) => Cmd::Scene(Scene::Swap(s, s - 1)), - key!(Char('.')) => Cmd::Scene(Scene::Swap(s, s + 1)), - key!(Char('<')) => Cmd::Scene(Scene::Swap(s, s - 1)), - key!(Char('>')) => Cmd::Scene(Scene::Swap(s, s + 1)), - key!(Enter) => Cmd::Scene(Scene::Play(s)), - key!(Delete) => Cmd::Scene(Scene::Delete(s)), - //key!(Char('c')) => Cmd::Track(Scene::Color(s, ItemColor::random())), - _ => return None - }) -} - -fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option { - use KeyCode::{Char, Up, Down, Left, Right, Delete}; - use ArrangerCommand as Cmd; - use ArrangerSelection as Select; - use ArrangerClipCommand as Clip; - Some(match input.event() { - key!(Up) => Cmd::Select(if s > 0 { Select::Clip(t, s - 1) } else { Select::Track(t) }), - key!(Down) => Cmd::Select(Select::Clip(t, s + 1)), - key!(Left) => Cmd::Select(if t > 0 { Select::Clip(t - 1, s) } else { Select::Scene(s) }), - key!(Right) => Cmd::Select(Select::Clip(t + 1, s)), - key!(Char(',')) => Cmd::Clip(Clip::Set(t, s, None)), - key!(Char('.')) => Cmd::Clip(Clip::Set(t, s, None)), - key!(Char('<')) => Cmd::Clip(Clip::Set(t, s, None)), - key!(Char('>')) => Cmd::Clip(Clip::Set(t, s, None)), - key!(Delete) => Cmd::Clip(Clip::Set(t, s, None)), - //key!(Char('c')) => Cmd::Clip(Clip::Color(t, s, ItemColor::random())), - //key!(Char('g')) => Cmd::Clip(Clip(Clip::Get(t, s))), - //key!(Char('s')) => Cmd::Clip(Clip(Clip::Set(t, s))), - _ => return None - }) -} diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs deleted file mode 100644 index 75d0b9b5..00000000 --- a/crates/tek/src/tui/app_sequencer.rs +++ /dev/null @@ -1,490 +0,0 @@ -use crate::{*, api::ClockCommand::{Play, Pause}}; -use super::phrase_editor::PhraseCommand::Show; -use super::app_transport::TransportCommand; -use KeyCode::{Char, Enter}; -use SequencerCommand::*; -use SequencerFocus::*; - -/// Create app state from JACK handle. -impl TryFrom<&Arc>> for SequencerTui { - type Error = Box; - fn try_from (jack: &Arc>) -> Usually { - - let clock = ClockModel::from(jack); - - let mut phrase = Phrase::default(); - phrase.name = "New".into(); - phrase.color = ItemColor::random().into(); - phrase.set_length(384); - - let mut phrases = PhraseListModel::default(); - let phrase = Arc::new(RwLock::new(phrase)); - phrases.phrases.push(phrase.clone()); - phrases.phrase.store(1, Ordering::Relaxed); - - let mut editor = PhraseEditorModel::default(); - editor.show_phrase(Some(phrase.clone())); - - let mut player = PhrasePlayerModel::from(&clock); - player.play_phrase = Some((Moment::zero(&clock.timebase), Some(phrase))); - - Ok(Self { - clock, - phrases, - player, - editor, - jack: jack.clone(), - size: Measure::new(), - cursor: (0, 0), - entered: false, - split: 20, - midi_buf: vec![vec![];65536], - note_buf: vec![], - perf: PerfModel::default(), - focus: FocusState::Focused(SequencerFocus::PhraseEditor) - }) - - } -} - -/// Root view for standalone `tek_sequencer`. -pub struct SequencerTui { - pub jack: Arc>, - pub clock: ClockModel, - pub phrases: PhraseListModel, - pub player: PhrasePlayerModel, - pub editor: PhraseEditorModel, - pub size: Measure, - pub cursor: (usize, usize), - pub split: u16, - pub entered: bool, - pub note_buf: Vec, - pub midi_buf: Vec>>, - pub focus: FocusState, - pub perf: PerfModel, -} - -impl JackApi for SequencerTui { - fn jack (&self) -> &Arc> { - &self.jack - } -} - -impl Audio for SequencerTui { - fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { - // Start profiling cycle - let t0 = self.perf.get_t0(); - - // Update transport clock - if ClockAudio(self).process(client, scope) == Control::Quit { - return Control::Quit - } - - // Update MIDI sequencer - if PlayerAudio( - &mut self.player, - &mut self.note_buf, - &mut self.midi_buf, - ).process(client, scope) == Control::Quit { - return Control::Quit - } - - // Update sequencer playhead indicator - //self.now().set(0.); - //if let Some((ref started_at, Some(ref playing))) = self.player.play_phrase { - //let phrase = phrase.read().unwrap(); - //if *playing.read().unwrap() == *phrase { - //let pulse = self.current().pulse.get(); - //let start = started_at.pulse.get(); - //let now = (pulse - start) % phrase.length as f64; - //self.now().set(now); - //} - //} - // End profiling cycle - self.perf.update(t0, scope); - - Control::Continue - } -} - -render!(|self: SequencerTui|lay!([ - self.size, - Tui::shrink_y(1, col!([ - TransportView::from((self, if let SequencerFocus::Transport(_) = self.focus.inner() { - true - } else { - false - })), - row!([ - Tui::fixed_x(20, Tui::split_up(2, PhraseSelector::edit_phrase( - &self.editor.phrase, - self.focused() == SequencerFocus::PhraseEditor, - self.entered() - ), col!([ - PhraseSelector::play_phrase( - &self.player, - self.focused() == SequencerFocus::PhrasePlay, - self.entered() - ), - PhraseSelector::next_phrase( - &self.player, - self.focused() == SequencerFocus::PhraseNext, - self.entered() - ), - PhraseListView::from(self), - - ]))), - PhraseView::from(self) - ]) - ])), - Tui::fill_xy(Tui::at_s(SequencerStatusBar::from(self))), -])); - -impl HasClock for SequencerTui { - fn clock (&self) -> &ClockModel { - &self.clock - } -} - -impl HasPhrases for SequencerTui { - fn phrases (&self) -> &Vec>> { - &self.phrases.phrases - } - fn phrases_mut (&mut self) -> &mut Vec>> { - &mut self.phrases.phrases - } -} - -/// Sections in the sequencer app that may be focused -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum SequencerFocus { - /// The transport (toolbar) is focused - Transport(TransportFocus), - /// The phrase list (pool) is focused - PhraseList, - /// The phrase editor (sequencer) is focused - PhraseEditor, - - PhrasePlay, - PhraseNext, -} - -impl From<&SequencerTui> for Option { - fn from (state: &SequencerTui) -> Self { - match state.focus.inner() { - SequencerFocus::Transport(focus) => Some(focus), - _ => None - } - } -} - -impl_focus!(SequencerTui SequencerFocus [ - //&[ - //Menu, - //Menu, - //Menu, - //Menu, - //Menu, - //], - &[ - Transport(TransportFocus::PlayPause), - Transport(TransportFocus::Bpm), - Transport(TransportFocus::Sync), - Transport(TransportFocus::Quant), - Transport(TransportFocus::Clock), - ], - &[ - PhrasePlay, - PhrasePlay, - PhraseEditor, - PhraseEditor, - PhraseEditor, - ], - &[ - PhraseNext, - PhraseNext, - PhraseEditor, - PhraseEditor, - PhraseEditor, - ], - &[ - PhraseList, - PhraseList, - PhraseEditor, - PhraseEditor, - PhraseEditor, - ], -] => [self: { - if self.focus.is_entered() && self.focus.inner() == SequencerFocus::PhraseEditor { - self.editor.edit_mode = PhraseEditMode::Note - } else { - self.editor.edit_mode = PhraseEditMode::Scroll - } -}]); - -/// Status bar for sequencer app -#[derive(Clone)] -pub struct SequencerStatusBar { - pub(crate) width: usize, - pub(crate) cpu: Option, - pub(crate) size: String, - pub(crate) res: String, - pub(crate) mode: &'static str, - pub(crate) help: &'static [(&'static str, &'static str, &'static str)] -} - -impl StatusBar for SequencerStatusBar { - type State = SequencerTui; - fn hotkey_fg () -> Color { - TuiTheme::HOTKEY_FG - } - fn update (&mut self, _: &SequencerTui) { - todo!() - } -} - -impl From<&SequencerTui> for SequencerStatusBar { - fn from (state: &SequencerTui) -> Self { - use super::app_transport::TransportFocus::*; - let samples = state.clock.chunk.load(Ordering::Relaxed); - let rate = state.clock.timebase.sr.get() as f64; - let buffer = samples as f64 / rate; - let width = state.size.w(); - let default_help = &[("", "⏎", " enter"), ("", "✣", " navigate")]; - Self { - width, - cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")), - size: format!("{}x{}│", width, state.size.h()), - res: format!("│{}s│{:.1}kHz│{:.1}ms│", samples, rate / 1000., buffer * 1000.), - mode: match state.focused() { - Transport(PlayPause) => " PLAY/PAUSE ", - Transport(Bpm) => " TEMPO ", - Transport(Sync) => " LAUNCH SYNC ", - Transport(Quant) => " REC QUANT ", - Transport(Clock) => " SEEK ", - PhrasePlay => " TO PLAY ", - PhraseNext => " UP NEXT ", - PhraseList => " PHRASES ", - PhraseEditor => match state.editor.edit_mode { - PhraseEditMode::Note => " EDIT MIDI ", - PhraseEditMode::Scroll => " VIEW MIDI ", - }, - }, - help: match state.focused() { - Transport(PlayPause) => &[ - ("", "⏎", " play/pause"), - ("", "✣", " navigate"), - ], - Transport(Bpm) => &[ - ("", ".,", " inc/dec"), - ("", "><", " fine"), - ], - Transport(Sync) => &[ - ("", ".,", " inc/dec"), - ], - Transport(Quant) => &[ - ("", ".,", " inc/dec"), - ], - Transport(Clock) => &[ - ("", ".,", " by beat"), - ("", "<>", " by time"), - ], - PhraseList => if state.entered() { - &[ - ("", "↕", " pick"), - ("", ".,", " move"), - ("", "⏎", " play"), - ("", "e", " edit"), - ] - } else { - default_help - }, - PhraseEditor => match state.editor.edit_mode { - PhraseEditMode::Note => &[ - ("", "✣", " cursor"), - ], - PhraseEditMode::Scroll => &[ - ("", "✣", " scroll"), - ], - } - _ => if state.entered() { - &[ - ("", "Esc", " exit") - ] - } else { - default_help - } - } - } - } -} - -render!(|self: SequencerStatusBar|{ - lay!(|add|if self.width > 60 { - add(&row!(![ - SequencerMode::from(self), - SequencerStats::from(self), - ])) - } else { - add(&col!(![ - SequencerMode::from(self), - SequencerStats::from(self), - ])) - }) -}); - -struct SequencerMode { - mode: &'static str, - help: &'static [(&'static str, &'static str, &'static str)] -} -impl From<&SequencerStatusBar> for SequencerMode { - fn from (state: &SequencerStatusBar) -> Self { - Self { - mode: state.mode, - help: state.help, - } - } -} -render!(|self:SequencerMode|{ - let orange = Color::Rgb(255,128,0); - let light = Color::Rgb(50,50,50); - let white = Color::Rgb(255,255,255); - let yellow = Color::Rgb(255,255,0); - let black = Color::Rgb(0,0,0); - row!([ - Tui::bg(orange, Tui::fg(black, Tui::bold(true, self.mode))), - Tui::bg(light, Tui::fg(white, row!((prefix, hotkey, suffix) in self.help.iter() => { - row!([" ", prefix, Tui::fg(yellow, *hotkey), suffix]) - }))) - ]) -}); - -struct SequencerStats<'a> { - cpu: &'a Option, - size: &'a String, - res: &'a String, -} -impl<'a> From<&'a SequencerStatusBar> for SequencerStats<'a> { - fn from (state: &'a SequencerStatusBar) -> Self { - Self { - cpu: &state.cpu, - size: &state.size, - res: &state.res, - } - } -} -render!(|self:SequencerStats<'a>|{ - let orange = Color::Rgb(255,128,0); - let dark = Color::Rgb(25,25,25); - let cpu = &self.cpu; - let res = &self.res; - let size = &self.size; - Tui::bg(dark, row!([ - Tui::fg(orange, cpu), - Tui::fg(orange, res), - Tui::fg(orange, size), - ])) -}); - -impl Handle for SequencerTui { - fn handle (&mut self, i: &TuiInput) -> Perhaps { - SequencerCommand::execute_with_state(self, i) - } -} - -#[derive(Clone, Debug)] -pub enum SequencerCommand { - Focus(FocusCommand), - Clock(ClockCommand), - Phrases(PhrasesCommand), - Editor(PhraseCommand), - Enqueue(Option>>), - Clear, - Undo, - Redo, -} - -impl Command for SequencerCommand { - fn execute (self, state: &mut SequencerTui) -> Perhaps { - Ok(match self { - Self::Focus(cmd) => cmd.execute(state)?.map(Focus), - Self::Phrases(cmd) => cmd.execute(&mut state.phrases)?.map(Phrases), - Self::Editor(cmd) => cmd.execute(&mut state.editor)?.map(Editor), - Self::Clock(cmd) => cmd.execute(state)?.map(Clock), - Self::Enqueue(phrase) => { - state.player.enqueue_next(phrase.as_ref()); - None - }, - Self::Undo => { todo!() }, - Self::Redo => { todo!() }, - Self::Clear => { todo!() }, - }) - } -} - -impl InputToCommand for SequencerCommand { - fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option { - if state.entered() { - to_sequencer_command(state, input) - .or_else(||to_focus_command(input).map(SequencerCommand::Focus)) - } else { - to_focus_command(input).map(SequencerCommand::Focus) - .or_else(||to_sequencer_command(state, input)) - }.or_else(||Some({ - let time_zoom = state.editor.view_mode.time_zoom(); - let next_zoom = next_note_length(time_zoom); - let prev_zoom = prev_note_length(time_zoom); - match input.event() { - key!(Char('-')) => SequencerCommand::Editor(PhraseCommand::SetTimeZoom(next_zoom)), - key!(Char('_')) => SequencerCommand::Editor(PhraseCommand::SetTimeZoom(next_zoom)), - key!(Char('=')) => SequencerCommand::Editor(PhraseCommand::SetTimeZoom(prev_zoom)), - key!(Char('+')) => SequencerCommand::Editor(PhraseCommand::SetTimeZoom(prev_zoom)), - _ => return None - } - })) - } -} - -pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option { - Some(match input.event() { - // Play/pause - key!(Char(' ')) => Clock( - if state.clock().is_stopped() { Play(None) } else { Pause(None) } - ), - // Play from start/rewind to start - key!(Shift-Char(' ')) => Clock( - if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) } - ), - // Edit phrase - key!(Char('e')) => match state.focused() { - SequencerFocus::PhrasePlay => Editor(Show( - state.player.play_phrase().as_ref().map(|x|x.1.as_ref()).flatten().map(|x|x.clone()) - )), - SequencerFocus::PhraseNext => Editor(Show( - state.player.next_phrase().as_ref().map(|x|x.1.as_ref()).flatten().map(|x|x.clone()) - )), - SequencerFocus::PhraseList => Editor(Show( - Some(state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone()) - )), - _ => return None, - }, - _ => match state.focused() { - SequencerFocus::Transport(_) => match TransportCommand::input_to_command(state, input)? { - TransportCommand::Clock(command) => Clock(command), - _ => return None, - }, - SequencerFocus::PhraseEditor => Editor( - PhraseCommand::input_to_command(&state.editor, input)? - ), - SequencerFocus::PhraseList => match input.event() { - key!(Enter) => Enqueue(Some( - state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone() - )), - _ => Phrases( - PhrasesCommand::input_to_command(&state.phrases, input)? - ), - } - _ => return None - } - }) -} diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs deleted file mode 100644 index 1f05267a..00000000 --- a/crates/tek/src/tui/app_transport.rs +++ /dev/null @@ -1,344 +0,0 @@ -use crate::*; -use crate::api::ClockCommand::{Play, Pause, SetBpm, SetQuant, SetSync}; -use TransportCommand::{Focus, Clock}; -use FocusCommand::{Next, Prev}; -use KeyCode::{Enter, Left, Right, Char}; - -/// Transport clock app. -pub struct TransportTui { - pub jack: Arc>, - pub clock: ClockModel, - pub size: Measure, - pub cursor: (usize, usize), - pub focus: FocusState, -} - -/// Create app state from JACK handle. -impl TryFrom<&Arc>> for TransportTui { - type Error = Box; - fn try_from (jack: &Arc>) -> Usually { - Ok(Self { - jack: jack.clone(), - clock: ClockModel::from(jack), - size: Measure::new(), - cursor: (0, 0), - focus: FocusState::Entered(TransportFocus::PlayPause) - }) - } -} - -impl std::fmt::Debug for TransportTui { - fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("TransportTui") - .field("jack", &self.jack) - .field("size", &self.size) - .field("cursor", &self.cursor) - .finish() - } -} - -impl HasClock for TransportTui { - fn clock (&self) -> &ClockModel { - &self.clock - } -} - -impl JackApi for TransportTui { - fn jack (&self) -> &Arc> { - &self.jack - } -} - -impl Audio for TransportTui { - fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { - ClockAudio(self).process(client, scope) - } -} - -render!(|self: TransportTui|TransportView::from((self, true))); - -pub struct TransportView { - focused: bool, - - sr: String, - bpm: String, - ppq: String, - beat: String, - - global_sample: String, - global_second: String, - - started: bool, - - current_sample: f64, - current_second: f64, -} - -impl From<(&T, bool)> for TransportView { - fn from ((state, focused): (&T, bool)) -> Self { - let clock = state.clock(); - let sr = format!("{:.1}k", clock.timebase.sr.get() / 1000.0); - let bpm = format!("{:.3}", clock.timebase.bpm.get()); - let ppq = format!("{:.0}", clock.timebase.ppq.get()); - if let Some(started) = clock.started.read().unwrap().as_ref() { - let current_sample = (clock.global.sample.get() - started.sample.get())/1000.; - let current_usec = clock.global.usec.get() - started.usec.get(); - let current_second = current_usec/1000000.; - Self { - focused, - sr, - bpm, - ppq, - started: true, - global_sample: format!("{:.0}k", started.sample.get()/1000.), - global_second: format!("{:.1}s", started.usec.get()/1000.), - current_sample, - current_second, - beat: clock.timebase.format_beats_0( - clock.timebase.usecs_to_pulse(current_usec) - ), - } - } else { - Self { - focused, - sr, - bpm, - ppq, - started: false, - global_sample: format!("{:.0}k", clock.global.sample.get()/1000.), - global_second: format!("{:.1}s", clock.global.usec.get()/1000000.), - current_sample: 0.0, - current_second: 0.0, - beat: format!("0.0.00") - } - } - } -} - -struct TransportField<'a>(&'a str, &'a str); -render!(|self: TransportField<'a>|{ - col!([ - Tui::fg(Color::Rgb(150, 150, 150), self.0), - Tui::bold(true, Tui::fg(Color::Rgb(200, 200, 200), self.1)), - ]) -}); - -render!(|self: TransportView|{ - let bg = if self.focused { TuiTheme::border_bg() } else { TuiTheme::bg() }; - let border_style = Style::default() - .bg(bg) - .fg(TuiTheme::border_fg(self.focused)); - Tui::bg(bg, lay!(move|add|{ - add(&Tui::fill_x(Tui::at_w(lay!(move|add|{ - add(&Lozenge(border_style))?; - add(&Tui::outset_x(1, row!([ - TransportField("Beat", self.beat.as_str()), " ", - TransportField("BPM ", self.bpm.as_str()), " ", - ]))) - }))))?; - add(&Tui::fill_x(Tui::center_x(Tui::pull_x(2, row!([ - col!(|add|{ - if self.started { - add(&col!([Tui::fg(Color::Rgb(0, 255, 0), "▶ PLAYING"), ""])) - } else { - add(&col!(["", Tui::fg(Color::Rgb(255, 128, 0), "⏹ STOPPED")])) - } - }), - ])))))?; - add(&Tui::fill_x(Tui::at_e(lay!(move|add|{ - add(&Lozenge(border_style))?; - add(&Tui::outset_x(1, row!([ - TransportField("Second", format!("{:.1}s", self.current_second).as_str()), " ", - TransportField("Rate ", self.sr.as_str()), " ", - TransportField("Sample", format!("{:.0}k", self.current_sample).as_str()), - ]))) - })))) - })) -}); - -/// Which item of the transport toolbar is focused -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum TransportFocus { - Bpm, - Sync, - PlayPause, - Clock, - Quant, -} - -impl From<&TransportTui> for Option { - fn from (state: &TransportTui) -> Self { - Some(state.focus.inner()) - } -} - -impl FocusWrap for TransportFocus { - fn wrap <'a, W: Render> (self, focus: TransportFocus, content: &'a W) - -> impl Render + 'a - { - let focused = focus == self; - let corners = focused.then_some(CORNERS); - //let highlight = focused.then_some(Tui::bg(Color::Rgb(60, 70, 50))); - lay!([corners, /*highlight,*/ *content]) - } -} - -impl FocusWrap for Option { - fn wrap <'a, W: Render> (self, focus: TransportFocus, content: &'a W) - -> impl Render + 'a - { - let focused = Some(focus) == self; - let corners = focused.then_some(CORNERS); - //let highlight = focused.then_some(Background(Color::Rgb(60, 70, 50))); - lay!([corners, /*highlight,*/ *content]) - } -} - -impl_focus!(TransportTui TransportFocus [ - //&[Menu], - &[ - PlayPause, - Bpm, - Sync, - Quant, - Clock, - ], -]); - -#[derive(Copy, Clone)] -pub struct TransportStatusBar; - -impl StatusBar for TransportStatusBar { - type State = (); - fn hotkey_fg () -> Color { - TuiTheme::HOTKEY_FG - } - fn update (&mut self, _: &()) { - todo!() - } -} - -render!(|self: TransportStatusBar|"todo"); - -impl Handle for TransportTui { - fn handle (&mut self, from: &TuiInput) -> Perhaps { - TransportCommand::execute_with_state(self, from) - } -} - -#[derive(Clone, Debug, PartialEq)] -pub enum TransportCommand { - Focus(FocusCommand), - Clock(ClockCommand), -} - -impl Command for TransportCommand { - fn execute (self, state: &mut T) -> Perhaps { - Ok(match self { - Self::Focus(cmd) => cmd.execute(state)?.map(Self::Focus), - Self::Clock(cmd) => cmd.execute(state)?.map(Self::Clock), - }) - } -} - -pub trait TransportControl: HasClock + FocusGrid + HasEnter { - fn transport_focused (&self) -> Option; -} - -impl TransportControl for TransportTui { - fn transport_focused (&self) -> Option { - Some(self.focus.inner()) - } -} - -impl TransportControl for SequencerTui { - fn transport_focused (&self) -> Option { - match self.focus.inner() { - SequencerFocus::Transport(focus) => Some(focus), - _ => None - } - } -} - -impl TransportControl for ArrangerTui { - fn transport_focused (&self) -> Option { - match self.focus.inner() { - ArrangerFocus::Transport(focus) => Some(focus), - _ => None - } - } -} - -impl InputToCommand for TransportCommand { - fn input_to_command (state: &T, input: &TuiInput) -> Option { - to_transport_command(state, input) - .or_else(||to_focus_command(input).map(TransportCommand::Focus)) - } -} - -pub fn to_transport_command (state: &T, input: &TuiInput) -> Option -where - T: TransportControl -{ - Some(match input.event() { - key!(Left) => Focus(Prev), - key!(Right) => Focus(Next), - key!(Char(' ')) => Clock(if state.clock().is_stopped() { - Play(None) - } else { - Pause(None) - }), - key!(Shift-Char(' ')) => Clock(if state.clock().is_stopped() { - Play(Some(0)) - } else { - Pause(Some(0)) - }), - _ => match state.transport_focused().unwrap() { - TransportFocus::Bpm => match input.event() { - key!(Char(',')) => Clock(SetBpm(state.clock().bpm().get() - 1.0)), - key!(Char('.')) => Clock(SetBpm(state.clock().bpm().get() + 1.0)), - key!(Char('<')) => Clock(SetBpm(state.clock().bpm().get() - 0.001)), - key!(Char('>')) => Clock(SetBpm(state.clock().bpm().get() + 0.001)), - _ => return None, - }, - TransportFocus::Quant => match input.event() { - key!(Char(',')) => Clock(SetQuant(state.clock().quant.prev())), - key!(Char('.')) => Clock(SetQuant(state.clock().quant.next())), - key!(Char('<')) => Clock(SetQuant(state.clock().quant.prev())), - key!(Char('>')) => Clock(SetQuant(state.clock().quant.next())), - _ => return None, - }, - TransportFocus::Sync => match input.event() { - key!(Char(',')) => Clock(SetSync(state.clock().sync.prev())), - key!(Char('.')) => Clock(SetSync(state.clock().sync.next())), - key!(Char('<')) => Clock(SetSync(state.clock().sync.prev())), - key!(Char('>')) => Clock(SetSync(state.clock().sync.next())), - _ => return None, - }, - TransportFocus::Clock => match input.event() { - key!(Char(',')) => todo!("transport seek bar"), - key!(Char('.')) => todo!("transport seek bar"), - key!(Char('<')) => todo!("transport seek beat"), - key!(Char('>')) => todo!("transport seek beat"), - _ => return None, - }, - TransportFocus::PlayPause => match input.event() { - key!(Enter) => Clock( - if state.clock().is_stopped() { - Play(None) - } else { - Pause(None) - } - ), - key!(Shift-Enter) => Clock( - if state.clock().is_stopped() { - Play(Some(0)) - } else { - Pause(Some(0)) - } - ), - _ => return None, - }, - } - }) -} diff --git a/crates/tek/src/tui/engine_focus.rs b/crates/tek/src/tui/engine_focus.rs deleted file mode 100644 index 4b0c417a..00000000 --- a/crates/tek/src/tui/engine_focus.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::*; - -pub trait FocusWrap { - fn wrap <'a, W: Render> (self, focus: T, content: &'a W) - -> impl Render + 'a; -} - -pub fn to_focus_command (input: &TuiInput) -> Option { - use KeyCode::{Tab, BackTab, Up, Down, Left, Right, Enter, Esc}; - Some(match input.event() { - key!(Tab) => FocusCommand::Next, - key!(Shift-Tab) => FocusCommand::Prev, - key!(BackTab) => FocusCommand::Prev, - key!(Shift-BackTab) => FocusCommand::Prev, - key!(Up) => FocusCommand::Up, - key!(Down) => FocusCommand::Down, - key!(Left) => FocusCommand::Left, - key!(Right) => FocusCommand::Right, - key!(Enter) => FocusCommand::Enter, - key!(Esc) => FocusCommand::Exit, - _ => return None - }) -} - -#[macro_export] macro_rules! impl_focus { - ($Struct:ident $Focus:ident $Grid:expr $(=> [$self:ident : $update_focus:expr])?) => { - impl HasFocus for $Struct { - type Item = $Focus; - /// Get the currently focused item. - fn focused (&self) -> Self::Item { - self.focus.inner() - } - /// Get the currently focused item. - fn set_focused (&mut self, to: Self::Item) { - self.focus.set_inner(to) - } - $(fn focus_updated (&mut $self) { $update_focus })? - } - impl HasEnter for $Struct { - /// Get the currently focused item. - fn entered (&self) -> bool { - self.focus.is_entered() - } - /// Get the currently focused item. - fn set_entered (&mut self, entered: bool) { - if entered { - self.focus.to_entered() - } else { - self.focus.to_focused() - } - } - } - impl FocusGrid for $Struct { - fn focus_cursor (&self) -> (usize, usize) { - self.cursor - } - fn focus_cursor_mut (&mut self) -> &mut (usize, usize) { - &mut self.cursor - } - fn focus_layout (&self) -> &[&[$Focus]] { - use $Focus::*; - &$Grid - } - } - } -} diff --git a/crates/tek/src/tui/engine_input.rs b/crates/tek/src/tui/engine_input.rs deleted file mode 100644 index 7508bf22..00000000 --- a/crates/tek/src/tui/engine_input.rs +++ /dev/null @@ -1,181 +0,0 @@ -use crate::*; - -pub struct TuiInput { - pub(crate) exited: Arc, - pub(crate) event: TuiEvent, -} - -#[derive(Debug, Clone)] -pub enum TuiEvent { - /// Terminal input - Input(::crossterm::event::Event), - /// Update values but not the whole form. - Update, - /// Update the whole form. - Redraw, - /// Device gains focus - Focus, - /// Device loses focus - Blur, - // /// JACK notification - // Jack(JackEvent) -} - -impl Input for TuiInput { - type Event = TuiEvent; - fn event (&self) -> &TuiEvent { &self.event } - fn is_done (&self) -> bool { self.exited.fetch_and(true, Ordering::Relaxed) } - fn done (&self) { self.exited.store(true, Ordering::Relaxed); } -} - -impl TuiInput { - // TODO remove - pub fn handle_keymap (&self, state: &mut T, keymap: &KeyMap) -> Usually { - match self.event() { - TuiEvent::Input(crossterm::event::Event::Key(event)) => { - for (code, modifiers, _, _, command) in keymap.iter() { - if *code == event.code && modifiers.bits() == event.modifiers.bits() { - return command(state) - } - } - }, - _ => {} - }; - Ok(false) - } -} - -pub type KeyHandler = &'static dyn Fn(&mut T)->Usually; - -pub type KeyBinding = (KeyCode, KeyModifiers, &'static str, &'static str, KeyHandler); - -pub type KeyMap = [KeyBinding]; - -/// Define a key -pub const fn key (code: KeyCode) -> KeyEvent { - let modifiers = KeyModifiers::NONE; - let kind = KeyEventKind::Press; - let state = KeyEventState::NONE; - KeyEvent { code, modifiers, kind, state } -} - -/// Add Ctrl modifier to key -pub const fn ctrl (key: KeyEvent) -> KeyEvent { - KeyEvent { modifiers: key.modifiers.union(KeyModifiers::CONTROL), ..key } -} - -/// Add Alt modifier to key -pub const fn alt (key: KeyEvent) -> KeyEvent { - KeyEvent { modifiers: key.modifiers.union(KeyModifiers::ALT), ..key } -} - -/// Add Shift modifier to key -pub const fn shift (key: KeyEvent) -> KeyEvent { - KeyEvent { modifiers: key.modifiers.union(KeyModifiers::SHIFT), ..key } -} - -/// Define a keymap -#[macro_export] macro_rules! keymap { - ($T:ty { $([$k:ident $(($char:literal))?, $m:ident, $n: literal, $d: literal, $f: expr]),* $(,)? }) => { - &[ - $((KeyCode::$k $(($char))?, KeyModifiers::$m, $n, $d, &$f as KeyHandler<$T>)),* - ] as &'static [KeyBinding<$T>] - } -} - -/// Define a key in a keymap -#[macro_export] macro_rules! map_key { - ($k:ident $(($char:literal))?, $m:ident, $n: literal, $d: literal, $f: expr) => { - (KeyCode::$k $(($char))?, KeyModifiers::$m, $n, $d, &$f as &dyn Fn()->Usually) - } -} - -/// Shorthand for key match statement -#[macro_export] macro_rules! match_key { - ($event:expr, { - $($key:pat=>$block:expr),* $(,)? - }) => { - match $event { - $(crossterm::event::Event::Key(crossterm::event::KeyEvent { - code: $key, - modifiers: crossterm::event::KeyModifiers::NONE, - kind: crossterm::event::KeyEventKind::Press, - state: crossterm::event::KeyEventState::NONE - }) => { - $block - })* - _ => Ok(None) - } - } -} - -/// Define key pattern in key match statement -#[macro_export] macro_rules! key { - ($code:pat) => { - TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { - code: $code, - modifiers: crossterm::event::KeyModifiers::NONE, - kind: crossterm::event::KeyEventKind::Press, - state: crossterm::event::KeyEventState::NONE - })) - }; - (Ctrl-$code:pat) => { - TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { - code: $code, - modifiers: crossterm::event::KeyModifiers::CONTROL, - kind: crossterm::event::KeyEventKind::Press, - state: crossterm::event::KeyEventState::NONE - })) - }; - (Alt-$code:pat) => { - TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { - code: $code, - modifiers: crossterm::event::KeyModifiers::ALT, - kind: crossterm::event::KeyEventKind::Press, - state: crossterm::event::KeyEventState::NONE - })) - }; - (Shift-$code:pat) => { - TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { - code: $code, - modifiers: crossterm::event::KeyModifiers::SHIFT, - kind: crossterm::event::KeyEventKind::Press, - state: crossterm::event::KeyEventState::NONE - })) - } -} - -#[macro_export] macro_rules! key_lit { - ($code:expr) => { - TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { - code: $code, - modifiers: crossterm::event::KeyModifiers::NONE, - kind: crossterm::event::KeyEventKind::Press, - state: crossterm::event::KeyEventState::NONE - })) - }; - (Ctrl-$code:expr) => { - TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { - code: $code, - modifiers: crossterm::event::KeyModifiers::CONTROL, - kind: crossterm::event::KeyEventKind::Press, - state: crossterm::event::KeyEventState::NONE - })) - }; - (Alt-$code:expr) => { - TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { - code: $code, - modifiers: crossterm::event::KeyModifiers::ALT, - kind: crossterm::event::KeyEventKind::Press, - state: crossterm::event::KeyEventState::NONE - })) - }; - (Shift-$code:expr) => { - TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { - code: $code, - modifiers: crossterm::event::KeyModifiers::SHIFT, - kind: crossterm::event::KeyEventKind::Press, - state: crossterm::event::KeyEventState::NONE - })) - } -} diff --git a/crates/tek/src/tui/engine_output.rs b/crates/tek/src/tui/engine_output.rs deleted file mode 100644 index 5fac2e11..00000000 --- a/crates/tek/src/tui/engine_output.rs +++ /dev/null @@ -1,184 +0,0 @@ -use crate::*; -use ratatui::buffer::Cell; - -/// Every struct that has [Content]<[Tui]> is a renderable [Render]<[Tui]>. -//impl> Render for C { - //fn min_size (&self, to: [u16;2]) -> Perhaps { - //self.content().min_size(to) - //} - //fn render (&self, to: &mut TuiOutput) -> Usually<()> { - //match self.min_size(to.area().wh().into())? { - //Some(wh) => to.render_in(to.area().clip(wh).into(), &self.content()), - //None => Ok(()) - //} - //} -//} - -pub struct TuiOutput { - pub buffer: Buffer, - pub area: [u16;4] -} - -impl Output for TuiOutput { - #[inline] fn area (&self) -> [u16;4] { self.area } - #[inline] fn area_mut (&mut self) -> &mut [u16;4] { &mut self.area } - #[inline] fn render_in (&mut self, - area: [u16;4], - widget: &dyn Render - ) -> Usually<()> { - let last = self.area(); - *self.area_mut() = area; - widget.render(self)?; - *self.area_mut() = last; - Ok(()) - } -} - -impl TuiOutput { - pub fn buffer_update (&mut self, - area: [u16;4], - callback: &impl Fn(&mut Cell, u16, u16) - ) { - buffer_update(&mut self.buffer, area, callback); - } - pub fn fill_bold (&mut self, area: [u16;4], on: bool) { - if on { - self.buffer_update(area, &|cell,_,_|cell.modifier.insert(Modifier::BOLD)) - } else { - self.buffer_update(area, &|cell,_,_|cell.modifier.remove(Modifier::BOLD)) - } - } - pub fn fill_bg (&mut self, area: [u16;4], color: Color) { - self.buffer_update(area, &|cell,_,_|{cell.set_bg(color);}) - } - pub fn fill_fg (&mut self, area: [u16;4], color: Color) { - self.buffer_update(area, &|cell,_,_|{cell.set_fg(color);}) - } - pub fn fill_ul (&mut self, area: [u16;4], color: Color) { - self.buffer_update(area, &|cell,_,_|{ - cell.modifier = ratatui::prelude::Modifier::UNDERLINED; - cell.underline_color = color; - }) - } - pub fn fill_char (&mut self, area: [u16;4], c: char) { - self.buffer_update(area, &|cell,_,_|{cell.set_char(c);}) - } - pub fn make_dim (&mut self) { - for cell in self.buffer.content.iter_mut() { - cell.bg = ratatui::style::Color::Rgb(30,30,30); - cell.fg = ratatui::style::Color::Rgb(100,100,100); - cell.modifier = ratatui::style::Modifier::DIM; - } - } - pub fn blit ( - &mut self, text: &impl AsRef, x: u16, y: u16, style: Option