diff --git a/.dockerignore b/.dockerignore index e69de29b..72e8ffc0 100644 --- a/.dockerignore +++ 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.nix b/.forgejo/workflows/build.nix deleted file mode 100644 index cb702884..00000000 --- a/.forgejo/workflows/build.nix +++ /dev/null @@ -1,38 +0,0 @@ -{pkgs?import{}}: pkgs.mkShell (with pkgs; { - nativeBuildInputs = [ - cargo - pkg-config - freetype - libclang - cloc - #bear - ]; - buildInputs = [ - jack2 - lilv - serd - libclang - #suil - glib - gtk3 - ]; - LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ - pipewire.jack - # for ChowKick.lv2: - freetype - libgcc.lib - # for Panagement - xorg.libX11 - xorg.libXcursor - xorg.libXi - libxkbcommon - #suil - # for Helm: - alsa-lib - curl - libglvnd - #xorg_sys_opengl - ]; - VST3_SDK_DIR = "/home/user/Lab/Music/tek/vst3sdk/"; - LIBCLANG_PATH = "${pkgs.libclang.lib.outPath}/lib"; -}) diff --git a/.forgejo/workflows/build.yaml b/.forgejo/workflows/build.yaml deleted file mode 100644 index 45ccc8a1..00000000 --- a/.forgejo/workflows/build.yaml +++ /dev/null @@ -1,10 +0,0 @@ -on: [push] -jobs: - build: - container: - image: nixos/nix:latest - steps: - - run: nix-channel --list && nix-channel --update - - run: nix-shell -p git --command 'git clone $GITHUB_SERVER_URL/$GITHUB_REPOSITORY .' - - run: whoami && pwd && ls -al - - run: nix-shell --command 'cargo version -vv && cargo test && cloc crates/tek/src' .forgejo/workflows/build.nix 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.fixme b/.old/demo.rs.old similarity index 57% rename from crates/tek/examples/demo.rs.fixme rename to .old/demo.rs.old index f3e9e3f2..6b205580 100644 --- a/crates/tek/examples/demo.rs.fixme +++ b/.old/demo.rs.old @@ -14,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![] } } } @@ -40,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())?; @@ -69,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))) @@ -87,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))) @@ -104,14 +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_expr!(PageUp) => { + kexp!(PageUp) => { self.index = (self.index + 1) % self.items.len(); }, - key_expr!(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/.scratch.rs b/.old/scratch.rs similarity index 100% rename from .scratch.rs rename to .old/scratch.rs 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 100% rename from crates/tek.old/src/control.rs rename to .old/src/control.rs 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 a6ff44fe..94551690 100644 --- a/Justfile +++ b/Justfile @@ -1,91 +1,117 @@ -default: - just -l +export RUSTFLAGS := "--cfg procmacro2_semver_exempt -Zmacro-backtrace -Clink-arg=-fuse-ld=mold" +export RUST_BACKTRACE := "1" -status: - cargo c - cloc --by-file src/ - git status +default: + @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 + git push --tags -fu codeberg && git push --tags -fu origin -transport: - reset - cargo run --bin tek_transport -transport-release: - reset - cargo run --release --bin tek_transport +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: - reset - cargo run --bin tek_arranger + {{debug}} {{name}} {{bpm}} arranger arranger-ext: - reset - cargo run --bin tek_arranger -- -n tek \ - -i "1=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \ - -o "1=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" \ - -i "2=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \ - -o "2=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" \ - -i "3=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \ - -o "3=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" \ - -i "4=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \ - -o "4=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" + {{debug}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} arranger arranger-release: - reset - cargo run --release --bin tek_arranger + {{release}} {{name}} {{bpm}} arranger arranger-release-ext: - reset - cargo run --release --bin tek_arranger -- -n tek \ - -i "1=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \ - -o "1=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" \ - -i "2=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \ - -o "2=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" \ - -i "3=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \ - -o "3=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" \ - -i "4=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" \ - -o "4=Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" + {{release}} {{name}} {{bpm}} {{midi-in}} {{firefox-in}} {{midi-out}} arranger groovebox: + {{debug}} {{name}} {{bpm}} groovebox +groovebox-ext: reset - cargo run --bin tek_groovebox + {{debug}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} {{audio-in}} {{audio-out}} groovebox +groovebox-browser: + {{debug}} {{name}} {{bpm}} {{audio-in}} groovebox groovebox-release: - reset - cargo run --release --bin tek_groovebox + {{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: - reset - cargo run --bin tek_sequencer + {{debug}} {{name}} {{bpm}} sequencer sequencer-ext: - reset - cargo run --bin tek_sequencer -- -i "Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" -o "Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" + {{debug}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} sequencer sequencer-release: - reset - cargo run --release --bin tek_sequencer + {{release}} {{name}} {{bpm}} sequencer sequencer-release-ext: - reset - cargo run --release --bin tek_sequencer -- -i "Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" -o "Midi-Bridge:Komplete Audio 6 1:(playback_0) Komplete Audio 6 MIDI 1" + {{release}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} sequencer mixer: - reset - cargo run --bin tek_mixer + {{debug}} mixer track: - reset - cargo run --bin tek_track + {{debug}} track sampler: - reset - cargo run --bin tek_sampler + {{debug}} sampler plugin: - reset - 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 349ca090..00000000 --- a/crates/tek/Cargo.toml +++ /dev/null @@ -1,64 +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_groovebox" -path = "src/cli/cli_groovebox.rs" - -[[bin]] -name = "tek_transport" -path = "src/cli/cli_transport.rs" - -[[bin]] -name = "tek_sampler" -path = "src/cli/cli_sampler.rs" - -#[[bin]] -#name = "tek_mixer" -#path = "src/cli_mixer.rs" - -#[[bin]] -#name = "tek_track" -#path = "src/cli_track.rs" - -#[[bin]] -#name = "tek_plugin" -#path = "src/cli_plugin.rs" diff --git a/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/demo_bsp.rs.fixme b/crates/tek/examples/demo_bsp.rs.fixme deleted file mode 100644 index 5301d7d9..00000000 --- a/crates/tek/examples/demo_bsp.rs.fixme +++ /dev/null @@ -1,19 +0,0 @@ -use tek::*; -use std::sync::{Arc, RwLock}; - -fn main () -> Usually<()> { - Tui::run(Arc::new(RwLock::new(BspDemo(Default::default()))))?; - Ok(()) -} - -pub struct BspDemo(std::marker::PhantomData); - -render!(|self:BspDemo|Fill::wh(Align::c( - Bsp::n(Bsp::s(Bsp::e(Bsp::w("00", "11"), "22"), "33"), "44") -))); - -impl Handle for BspDemo { - fn handle (&mut self, from: &TuiInput) -> Perhaps { - Ok(None) - } -} diff --git a/crates/tek/examples/midi_import.rs.fixme b/crates/tek/examples/midi_import.rs.fixme deleted file mode 100644 index 658ca4eb..00000000 --- a/crates/tek/examples/midi_import.rs.fixme +++ /dev/null @@ -1,18 +0,0 @@ -use tek::*; - -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 a4b2c74b..00000000 --- a/crates/tek/src/api.rs +++ /dev/null @@ -1 +0,0 @@ -mod sampler; pub(crate) use sampler::*; diff --git a/crates/tek/src/audio.rs b/crates/tek/src/audio.rs deleted file mode 100644 index a44f6276..00000000 --- a/crates/tek/src/audio.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub(crate) mod audio_in; -pub(crate) mod audio_out; -pub(crate) mod sampler; pub(crate) use sampler::*; diff --git a/crates/tek/src/audio/channel.rs b/crates/tek/src/audio/channel.rs deleted file mode 100644 index 513a6934..00000000 --- a/crates/tek/src/audio/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/audio/mixer.rs b/crates/tek/src/audio/mixer.rs deleted file mode 100644 index a0ac9366..00000000 --- a/crates/tek/src/audio/mixer.rs +++ /dev/null @@ -1,15 +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> -} -from!(|mode: &Arc>| MixerAudio = Self { model: model.clone() }); -audio!(|self: MixerAudio, _, _|Control::Continue); diff --git a/crates/tek/src/audio/sampler.rs b/crates/tek/src/audio/sampler.rs deleted file mode 100644 index c81e28cc..00000000 --- a/crates/tek/src/audio/sampler.rs +++ /dev/null @@ -1,152 +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 -} - -/// A sound sample. -#[derive(Default, Debug)] -pub struct Sample { - pub name: String, - pub start: usize, - pub end: usize, - pub channels: Vec>, - pub rate: Option, -} - -/// 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, -} - -/// 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() - ) - }}; -} - -impl Sampler { - - /// 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; - 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.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, .. } = self; - 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, .. } = self; - 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); - } - } - } - -} - -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, - } - } - /// 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)) - } -} - -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/cli/cli_arranger.rs b/crates/tek/src/cli/cli_arranger.rs deleted file mode 100644 index a5dd7d86..00000000 --- a/crates/tek/src/cli/cli_arranger.rs +++ /dev/null @@ -1,136 +0,0 @@ -include!("../lib.rs"); - -pub fn main () -> Usually<()> { - ArrangerCli::parse().run() -} - -/// Launches an interactive MIDI arranger. -#[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 = 4)] - tracks: usize, - - /// Number of scenes - #[arg(short, long, default_value_t = 8)] - scenes: usize, - - /// MIDI outs to connect each track to. - #[arg(short='i', long)] - midi_from: Vec, - - /// MIDI ins to connect each track to. - #[arg(short='o', long)] - midi_to: Vec, -} - -impl ArrangerCli { - /// Run the arranger TUI from CLI arguments. - fn run (&self) -> Usually<()> { - let mut client_name = String::from("tek_arranger"); - if let Some(name) = self.name.as_ref() { - client_name = name.clone(); - } - Tui::run(JackClient::new(client_name.as_str())?.activate_with(|jack|{ - let mut app = ArrangerTui::try_from(jack)?; - let jack = jack.read().unwrap(); - app.color = ItemPalette::random(); - add_tracks(&jack, &mut app, &self)?; - add_scenes(&mut app, self.scenes)?; - Ok(app) - })?)?; - Ok(()) - } -} - -fn add_tracks (jack: &JackClient, app: &mut ArrangerTui, cli: &ArrangerCli) -> Usually<()> { - let n = cli.tracks; - let track_color_1 = ItemColor::random(); - let track_color_2 = ItemColor::random(); - for i in 0..n { - let track = app.track_add(None, Some( - track_color_1.mix(track_color_2, i as f32 / n as f32).into() - ))?; - track.width = 8; - let name = track.name.read().unwrap(); - track.player.midi_ins.push( - jack.register_port(&format!("{}I", &name), MidiIn::default())? - ); - track.player.midi_outs.push( - jack.register_port(&format!("{}O", &name), MidiOut::default())? - ); - } - for connection in cli.midi_from.iter() { - let mut split = connection.split("="); - let number = split.next().unwrap().trim(); - if let Ok(track) = number.parse::() { - if track < 1 { - panic!("Tracks are zero-indexed") - } - if track > n { - panic!("Tried to connect track {track} or {n}. Pass -t {track} to increase number of tracks.") - } - if let Some(port) = split.next() { - if let Some(port) = jack.port_by_name(&port) { - jack.client().connect_ports(&port, &app.tracks[track-1].player.midi_ins[0])?; - //jack.client().connect_ports(&port, &app.tracks[track].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 cli.midi_to.iter() { - let mut split = connection.split("="); - let number = split.next().unwrap().trim(); - if let Ok(track) = number.parse::() { - if track < 1 { - panic!("Tracks are zero-indexed") - } - if track > n { - panic!("Tried to connect track {track} or {n}. Pass -t {track} to increase number of tracks.") - } - if let Some(port) = split.next() { - if let Some(port) = jack.port_by_name(&port) { - jack.client().connect_ports(&app.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}") - } - } - Ok(()) -} - -fn add_scenes (app: &mut ArrangerTui, n: usize) -> Usually<()> { - let scene_color_1 = ItemColor::random(); - let scene_color_2 = ItemColor::random(); - for i in 0..n { - let _scene = app.scene_add(None, Some( - scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() - ))?; - } - Ok(()) -} - -#[test] fn verify_arranger_cli () { - use clap::CommandFactory; - ArrangerCli::command().debug_assert(); -} diff --git a/crates/tek/src/cli/cli_groovebox.rs b/crates/tek/src/cli/cli_groovebox.rs deleted file mode 100644 index 77bac1bc..00000000 --- a/crates/tek/src/cli/cli_groovebox.rs +++ /dev/null @@ -1,25 +0,0 @@ -include!("../lib.rs"); -pub fn main () -> Usually<()> { - GrooveboxCli::parse().run() -} - -#[derive(Debug, Parser)] -#[command(version, about, long_about = None)] -pub struct GrooveboxCli; - -impl GrooveboxCli { - fn run (&self) -> Usually<()> { - Tui::run(JackClient::new("tek_groovebox")?.activate_with(|jack|{ - let midi_in_1 = jack.read().unwrap().register_port("in1", MidiIn::default())?; - let midi_out = jack.read().unwrap().register_port("out", MidiOut::default())?; - let midi_in_2 = jack.read().unwrap().register_port("in2", MidiIn::default())?; - let audio_in_1 = jack.read().unwrap().register_port("inL", AudioIn::default())?; - let audio_in_2 = jack.read().unwrap().register_port("inR", AudioIn::default())?; - let audio_out_1 = jack.read().unwrap().register_port("out1", AudioOut::default())?; - let audio_out_2 = jack.read().unwrap().register_port("out2", AudioOut::default())?; - let mut app = GrooveboxTui::try_from(jack)?; - Ok(app) - })?)?; - Ok(()) - } -} diff --git a/crates/tek/src/cli/cli_sampler.rs b/crates/tek/src/cli/cli_sampler.rs deleted file mode 100644 index 4b882fb9..00000000 --- a/crates/tek/src/cli/cli_sampler.rs +++ /dev/null @@ -1,22 +0,0 @@ -include!("../lib.rs"); - -pub fn main () -> Usually<()> { - SamplerCli::parse().run() -} - -#[derive(Debug, Parser)] #[command(version, about, long_about = None)] pub struct SamplerCli { - /// Name of JACK client - #[arg(short, long)] name: Option, - /// Path to plugin - #[arg(short, long)] path: Option, -} - -impl SamplerCli { - fn run (&self) -> Usually<()> { - Tui::run(JackClient::new("tek_sampler")?.activate_with(|x|{ - let sampler = SamplerTui::try_from(x)?; - Ok(sampler) - })?)?; - Ok(()) - } -} diff --git a/crates/tek/src/cli/cli_sequencer.rs b/crates/tek/src/cli/cli_sequencer.rs deleted file mode 100644 index f5cbb3b3..00000000 --- a/crates/tek/src/cli/cli_sequencer.rs +++ /dev/null @@ -1,80 +0,0 @@ -include!("../lib.rs"); - -pub fn main () -> Usually<()> { - SequencerCli::parse().run() -} - -/// Launches a single interactive MIDI sequencer. -#[derive(Debug, Parser)] -#[command(version, about, long_about = None)] -pub struct SequencerCli { - /// 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, - - /// MIDI outs to connect to (multiple instances accepted) - #[arg(short='i', long)] - midi_from: Vec, - - /// MIDI ins to connect to (multiple instances accepted) - #[arg(short='o', long)] - midi_to: Vec, - - /// Default phrase duration (in pulses; default: 4 * PPQ = 1 bar) - #[arg(short, long)] - length: Option, -} - -impl SequencerCli { - fn run (&self) -> Usually<()> { - let name = self.name.as_ref().map(|n|n.as_str()).unwrap_or("tek_sequencer"); - Tui::run(JackClient::new(name)?.activate_with(|jack|{ - let mut app = SequencerTui::try_from(jack)?; - let jack = jack.read().unwrap(); - let midi_in = jack.register_port("i", MidiIn::default())?; - let midi_out = jack.register_port("o", MidiOut::default())?; - connect_from(&jack, &midi_in, &self.midi_from)?; - connect_to(&jack, &midi_out, &self.midi_to)?; - app.player.midi_ins.push(midi_in); - app.player.midi_outs.push(midi_out); - if let Some(_) = self.length { - // TODO: if let Some(phrase) = sequencer.phrase.as_mut() { - //phrase.write().unwrap().length = length; - //} - } - Ok(app) - })?)?; - Ok(()) - } -} - -fn connect_from (jack: &JackClient, input: &Port, ports: &[String]) -> Usually<()> { - for port in ports.iter() { - if let Some(port) = jack.port_by_name(&port) { - jack.client().connect_ports(&port, &input)?; - } else { - panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names."); - } - } - Ok(()) -} - -fn connect_to (jack: &JackClient, output: &Port, ports: &[String]) -> Usually<()> { - for port in ports.iter() { - if let Some(port) = jack.port_by_name(&port) { - jack.client().connect_ports(&output, &port)?; - } else { - panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names."); - } - } - Ok(()) -} - -#[test] fn verify_sequencer_cli () { - use clap::CommandFactory; - SequencerCli::command().debug_assert(); -} 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 180d527c..00000000 --- a/crates/tek/src/core.rs +++ /dev/null @@ -1,66 +0,0 @@ -pub(crate) use std::error::Error; - -pub(crate) mod color; pub(crate) use color::*; -pub(crate) mod command; pub(crate) use command::*; -pub(crate) mod engine; pub(crate) use engine::*; -pub(crate) mod focus; pub(crate) use focus::*; -pub(crate) mod input; pub(crate) use input::*; -pub(crate) mod output; pub(crate) use output::*; - -pub(crate) use std::sync::atomic::{Ordering, AtomicBool, AtomicUsize}; -pub(crate) use Ordering::Relaxed; - -pub use self::{ - engine::Engine, - input::Handle, - output::Render -}; - -/// Standard result type. -pub type Usually = Result>; - -/// Standard optional result type. -pub type Perhaps = Result, Box>; - -/// Define test modules. -#[macro_export] macro_rules! testmod { - ($($name:ident)*) => { $(#[cfg(test)] mod $name;)* }; -} - -/// Prototypal case of implementor macro. -/// Saves 4loc per data pats. -#[macro_export] macro_rules! from { - ($(<$($lt:lifetime),+>)?|$state:ident:$Source:ty|$Target:ty=$cb:expr) => { - impl $(<$($lt),+>)? From<$Source> for $Target { - fn from ($state:$Source) -> Self { $cb } - } - }; -} - -pub trait Gettable { - /// Returns current value - fn get (&self) -> T; -} - -pub trait Mutable: Gettable { - /// Sets new value, returns old - fn set (&mut self, value: T) -> T; -} - -pub trait InteriorMutable: Gettable { - /// Sets new value, returns old - fn set (&self, value: T) -> T; -} - -impl Gettable for AtomicBool { - fn get (&self) -> bool { self.load(Relaxed) } -} -impl InteriorMutable for AtomicBool { - fn set (&self, value: bool) -> bool { self.swap(value, Relaxed) } -} -impl Gettable for AtomicUsize { - fn get (&self) -> usize { self.load(Relaxed) } -} -impl InteriorMutable for AtomicUsize { - fn set (&self, value: usize) -> usize { self.swap(value, Relaxed) } -} diff --git a/crates/tek/src/core/color.rs b/crates/tek/src/core/color.rs deleted file mode 100644 index 519cb20a..00000000 --- a/crates/tek/src/core/color.rs +++ /dev/null @@ -1,98 +0,0 @@ -use crate::*; -use rand::{thread_rng, distributions::uniform::UniformSampler}; -pub use ratatui::prelude::Color; - -pub trait HasColor { - fn color (&self) -> ItemColor; - fn color_mut (&self) -> &mut ItemColor; -} - -/// 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 ItemPalette { - pub base: ItemColor, - pub light: ItemColor, - pub lighter: ItemColor, - pub lightest: ItemColor, - pub dark: ItemColor, - pub darker: ItemColor, - pub darkest: ItemColor, -} -from!(|okhsl: Okhsl|ItemColor = Self { okhsl, rgb: okhsl_to_rgb(okhsl) }); -from!(|rgb: Color|ItemColor = Self { rgb, okhsl: rgb_to_okhsl(rgb) }); -// A single color within item theme parameters, in OKHSL and RGB representations. -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() - } -} -from!(|base: Color|ItemPalette = Self::from(ItemColor::from(base))); -from!(|base: ItemColor|ItemPalette = { - let mut light = base.okhsl.clone(); - light.lightness = (light.lightness * 1.3).min(Okhsl::::max_lightness()); - let mut lighter = light.clone(); - lighter.lightness = (lighter.lightness * 1.3).min(Okhsl::::max_lightness()); - let mut lightest = lighter.clone(); - lightest.lightness = (lightest.lightness * 1.3).min(Okhsl::::max_lightness()); - - let mut dark = base.okhsl.clone(); - dark.lightness = (dark.lightness * 0.75).max(Okhsl::::min_lightness()); - dark.saturation = (dark.saturation * 0.75).max(Okhsl::::min_saturation()); - let mut darker = dark.clone(); - darker.lightness = (darker.lightness * 0.66).max(Okhsl::::min_lightness()); - darker.saturation = (darker.saturation * 0.66).max(Okhsl::::min_saturation()); - let mut darkest = darker.clone(); - darkest.lightness = (darkest.lightness * 0.50).max(Okhsl::::min_lightness()); - darkest.saturation = (darkest.saturation * 0.50).max(Okhsl::::min_saturation()); - - Self { - base, - light: light.into(), - lighter: lighter.into(), - lightest: lightest.into(), - dark: dark.into(), - darker: darker.into(), - darkest: darkest.into(), - } -}); -impl ItemPalette { - 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 1f59e548..00000000 --- a/crates/tek/src/core/command.rs +++ /dev/null @@ -1,116 +0,0 @@ -use crate::*; - -#[macro_export] macro_rules! command { - (|$self:ident:$Command:ty,$state:ident:$State:ty|$handler:expr) => { - impl Command<$State> for $Command { - fn execute ($self, $state: &mut $State) -> Perhaps { - Ok($handler) - } - } - } -} - -#[macro_export] macro_rules! input_to_command { - ($Command:ty: <$Engine:ty>|$state:ident:$State:ty,$input:ident|$handler:expr) => { - impl InputToCommand<$Engine, $State> for $Command { - fn input_to_command ($state: &$State, $input: &<$Engine as Engine>::Input) -> Option { - Some($handler) - } - } - } -} - -#[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/engine.rs b/crates/tek/src/core/engine.rs deleted file mode 100644 index fb9ef473..00000000 --- a/crates/tek/src/core/engine.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::*; - -/// 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 0bae0cb6..00000000 --- a/crates/tek/src/core/focus.rs +++ /dev/null @@ -1,317 +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, - Set(T) -} - -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(); }, - Set(to) => { state.set_focused(to); }, - } - Ok(None) - } -} - -/// Trait for things that have focusable subparts. -pub trait HasFocus { - type Item: Copy + PartialEq + Debug + Send + Sync; - /// 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(); - } -} - -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_pat!(Tab) => FocusCommand::Next, - key_pat!(Shift-Tab) => FocusCommand::Prev, - key_pat!(BackTab) => FocusCommand::Prev, - key_pat!(Shift-BackTab) => FocusCommand::Prev, - key_pat!(Up) => FocusCommand::Up, - key_pat!(Down) => FocusCommand::Down, - key_pat!(Left) => FocusCommand::Left, - key_pat!(Right) => FocusCommand::Right, - key_pat!(Enter) => FocusCommand::Enter, - key_pat!(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/core/input.rs b/crates/tek/src/core/input.rs deleted file mode 100644 index 19c42b3b..00000000 --- a/crates/tek/src/core/input.rs +++ /dev/null @@ -1,68 +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; -} - -#[macro_export] macro_rules! handle { - (<$E:ty>|$self:ident:$Struct:ty,$input:ident|$handler:expr) => { - impl Handle<$E> for $Struct { - fn handle (&mut $self, $input: &<$E as Engine>::Input) -> Perhaps<<$E as Engine>::Handled> { - $handler - } - } - } -} - -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 8798251f..00000000 --- a/crates/tek/src/core/output.rs +++ /dev/null @@ -1,151 +0,0 @@ -use crate::*; - -#[macro_export] macro_rules! render { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*E$(,$T:ident$(:$U:path)?)*$(,)?>)?|$cb:expr) => { - impl Render for $Struct $(<$($L,)* E, $($T),*>)? { - fn min_size (&$self, to: ::Size) -> Perhaps<::Size> { - $cb.min_size(to) - } - fn render (&$self, to: &mut ::Output) -> Usually<()> { - $cb.render(to) - } - } - }; - (<$E:ty>|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? Render<$E> for $Struct $(<$($L),*$($T),*>)? { - fn min_size (&$self, to: <$E as Engine>::Size) -> Perhaps<<$E as Engine>::Size> { - $cb.min_size(to) - } - fn render (&$self, to: &mut <$E as Engine>::Output) -> Usually<()> { - $cb.render(to) - } - } - } -} - -/// 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 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/test.rs b/crates/tek/src/core/test.rs deleted file mode 100644 index 88699914..00000000 --- a/crates/tek/src/core/test.rs +++ /dev/null @@ -1,49 +0,0 @@ -#[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'); - - } -} diff --git a/crates/tek/src/edn.rs b/crates/tek/src/edn.rs deleted file mode 100644 index f0c4d6ad..00000000 --- a/crates/tek/src/edn.rs +++ /dev/null @@ -1,123 +0,0 @@ -use crate::*; -pub use clojure_reader::edn::Edn; -//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),* }) - } - }; -} - -impl crate::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. - }) - } -} - -impl crate::Sample { - 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 - })))) - } -} - - -//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/jack.rs b/crates/tek/src/jack.rs deleted file mode 100644 index 0dd51dc1..00000000 --- a/crates/tek/src/jack.rs +++ /dev/null @@ -1,291 +0,0 @@ -pub use ::jack as libjack; -pub(crate) mod activate; pub(crate) use self::activate::*; -pub(crate) mod audio; pub(crate) use self::audio::*; -pub(crate) mod client; pub(crate) use self::client::*; -pub(crate) mod jack_event; pub(crate) use self::jack_event::*; -pub(crate) mod ports; pub(crate) use self::ports::*; -pub(crate) use ::jack::{ - contrib::ClosureProcessHandler, - Client, AsyncClient, ClientOptions, ClientStatus, - ProcessScope, Control, CycleTimes, - Port, PortId, - PortSpec, MidiIn, MidiOut, AudioIn, AudioOut, Unowned, - Transport, TransportState, MidiIter, RawMidi, - Frames, - NotificationHandler, -}; - -/// Implement [TryFrom<&Arc>>]: create app state from wrapped JACK handle. -#[macro_export] macro_rules! from_jack { - (|$jack:ident|$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)? $cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? TryFrom<&Arc>> for $Struct $(<$($L),*$($T),*>)? { - type Error = Box; - fn try_from ($jack: &Arc>) -> Usually { - Ok($cb) - } - } - }; -} - -//////////////////////////////////////////////////////////////////////////////////// - -///// 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), - //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 - //} -//} diff --git a/crates/tek/src/jack/activate.rs b/crates/tek/src/jack/activate.rs deleted file mode 100644 index 37829f3c..00000000 --- a/crates/tek/src/jack/activate.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::*; - -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) - } -} - -/// 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 {} diff --git a/crates/tek/src/jack/audio.rs b/crates/tek/src/jack/audio.rs deleted file mode 100644 index b78c7e65..00000000 --- a/crates/tek/src/jack/audio.rs +++ /dev/null @@ -1,78 +0,0 @@ -use crate::*; - -/// Implement [Audio]: provide JACK callbacks. -#[macro_export] macro_rules! audio { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? { - #[inline] fn process (&mut $self, $c: &Client, $s: &ProcessScope) -> Control { $cb } - } - } -} - -/// Trait for thing that has 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 - } - } -} - - -/// 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 - } -} diff --git a/crates/tek/src/jack/client.rs b/crates/tek/src/jack/client.rs deleted file mode 100644 index be18cb0f..00000000 --- a/crates/tek/src/jack/client.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::*; -pub type DynamicAsyncClient = AsyncClient; -pub type DynamicAudioHandler = ClosureProcessHandler<(), BoxedAudioHandler>; -pub type BoxedAudioHandler = Box Control + Send>; -/// 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), -} -from!(|jack: JackClient|Client = match jack { - JackClient::Inactive(client) => client, - JackClient::Activating => panic!("jack client still activating"), - JackClient::Active(_) => panic!("jack client already activated"), -}); -impl JackClient { - pub fn new (name: &str) -> Usually { - let (client, _) = Client::new(name, ClientOptions::NO_START_SERVER)?; - Ok(Self::Inactive(client)) - } -} -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 = ClosureProcessHandler::new(frame as BoxedAudioHandler); - *state.write().unwrap() = Self::Active(client.activate_async(events, frames)?); - Ok(state) - } -} diff --git a/crates/tek/src/jack/jack_event.rs b/crates/tek/src/jack/jack_event.rs deleted file mode 100644 index e62945d7..00000000 --- a/crates/tek/src/jack/jack_event.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::*; - -#[derive(Debug, Clone, PartialEq)] -/// 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, -} - -/// 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/jack/ports.rs b/crates/tek/src/jack/ports.rs deleted file mode 100644 index 299f123e..00000000 --- a/crates/tek/src/jack/ports.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::*; - -/// 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 - }) -} diff --git a/crates/tek/src/lib.rs b/crates/tek/src/lib.rs deleted file mode 100644 index 34971df0..00000000 --- a/crates/tek/src/lib.rs +++ /dev/null @@ -1,60 +0,0 @@ -pub mod core; pub use self::core::*; -pub mod time; pub(crate) use self::time::*; -pub mod space; pub(crate) use self::space::*; -pub mod tui; pub(crate) use self::tui::*; -pub mod edn; -pub mod jack; pub(crate) use self::jack::*; -pub mod midi; pub(crate) use self::midi::*; -pub mod audio; pub(crate) use self::audio::*; -//pub mod plugin; pub(crate) use self::plugin::*; - -pub(crate) use clap::{self, Parser}; - -pub use ::better_panic; -pub(crate) use better_panic::{Settings, Verbosity}; - -pub use ::atomic_float; -pub(crate) use atomic_float::*; - -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::ops::{Add, Sub, Mul, Div, Rem}; -pub(crate) use std::cmp::{Ord, Eq, PartialEq}; -pub(crate) use std::fmt::{Debug, Display}; - -pub use ::crossterm; -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 use ::ratatui; -pub(crate) use ratatui::{ - prelude::{Style, Color, Buffer}, - style::{Stylize, Modifier}, - backend::{Backend, CrosstermBackend, ClearType} -}; - -pub use ::midly; -pub(crate) use ::midly::{ - Smf, - MidiMessage, - TrackEventKind, - live::LiveEvent, - num::u7 -}; - -pub use ::palette; -pub(crate) use ::palette::{ - *, - convert::*, - okhsl::* -}; - -testmod! { test } diff --git a/crates/tek/src/midi.rs b/crates/tek/src/midi.rs deleted file mode 100644 index 4e54d012..00000000 --- a/crates/tek/src/midi.rs +++ /dev/null @@ -1,303 +0,0 @@ -use crate::*; - -pub(crate) mod midi_in; pub(crate) use midi_in::*; -pub(crate) mod midi_launch; pub(crate) use midi_launch::*; -pub(crate) mod midi_note; pub(crate) use midi_note::*; -pub(crate) mod midi_out; pub(crate) use midi_out::*; -pub(crate) mod midi_phrase; pub(crate) use midi_phrase::*; -pub(crate) mod midi_play; pub(crate) use midi_play::*; -pub(crate) mod midi_pool; pub(crate) use midi_pool::*; -pub(crate) mod midi_rec; pub(crate) use midi_rec::*; - -/// 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; }, - _ => {} - } -} - -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", -]; - -pub trait MidiPlayerApi: MidiRecordApi + MidiPlaybackApi + Send + Sync {} - -impl MidiPlayerApi for PhrasePlayerModel {} - -pub trait HasPlayer { - fn player (&self) -> &impl MidiPlayerApi; - fn player_mut (&mut self) -> &mut impl MidiPlayerApi; -} - -#[macro_export] macro_rules! has_player { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasPlayer for $Struct $(<$($L),*$($T),*>)? { - fn player (&$self) -> &impl MidiPlayerApi { &$cb } - fn player_mut (&mut $self) -> &mut impl MidiPlayerApi { &mut$cb } - } - } -} - -/// Contains state for playing a phrase -pub struct PhrasePlayerModel { - /// State of clock and playhead - pub(crate) clock: ClockModel, - /// Start time and phrase being played - pub(crate) play_phrase: Option<(Moment, Option>>)>, - /// Start time and next phrase - pub(crate) next_phrase: Option<(Moment, Option>>)>, - /// Play input through output. - pub(crate) monitoring: bool, - /// Write input to sequence. - pub(crate) recording: bool, - /// Overdub input to sequence. - pub(crate) overdub: bool, - /// Send all notes off - pub(crate) reset: bool, // TODO?: after Some(nframes) - /// Record from MIDI ports to current sequence. - pub midi_ins: Vec>, - /// Play from current sequence to MIDI ports - pub midi_outs: Vec>, - /// Notes currently held at input - pub(crate) notes_in: Arc>, - /// Notes currently held at output - pub(crate) notes_out: Arc>, - /// MIDI output buffer - pub note_buf: Vec, -} -impl std::fmt::Debug for PhrasePlayerModel { - fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("PhrasePlayerModel") - .field("clock", &self.clock) - .field("play_phrase", &self.play_phrase) - .field("next_phrase", &self.next_phrase) - .finish() - } -} -from!(|clock: &ClockModel| PhrasePlayerModel = Self { - clock: clock.clone(), - midi_ins: vec![], - midi_outs: vec![], - note_buf: vec![0;8], - reset: true, - recording: false, - monitoring: false, - overdub: false, - play_phrase: None, - next_phrase: None, - notes_in: RwLock::new([false;128]).into(), - notes_out: RwLock::new([false;128]).into(), -}); -from!(|state: (&ClockModel, &Arc>)|PhrasePlayerModel = { - let (clock, phrase) = state; - let mut model = Self::from(clock); - model.play_phrase = Some((Moment::zero(&clock.timebase), Some(phrase.clone()))); - model -}); -has_clock!(|self:PhrasePlayerModel|&self.clock); - -impl HasMidiIns for PhrasePlayerModel { - fn midi_ins (&self) -> &Vec> { - &self.midi_ins - } - fn midi_ins_mut (&mut self) -> &mut Vec> { - &mut self.midi_ins - } -} - -impl HasMidiOuts for PhrasePlayerModel { - fn midi_outs (&self) -> &Vec> { - &self.midi_outs - } - fn midi_outs_mut (&mut self) -> &mut Vec> { - &mut self.midi_outs - } - fn midi_note (&mut self) -> &mut Vec { - &mut self.note_buf - } -} - -/// 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 - } -} - -impl MidiRecordApi for PhrasePlayerModel { - fn recording (&self) -> bool { - self.recording - } - fn recording_mut (&mut self) -> &mut bool { - &mut self.recording - } - fn monitoring (&self) -> bool { - self.monitoring - } - fn monitoring_mut (&mut self) -> &mut bool { - &mut self.monitoring - } - fn overdub (&self) -> bool { - self.overdub - } - fn overdub_mut (&mut self) -> &mut bool { - &mut self.overdub - } - fn notes_in (&self) -> &Arc> { - &self.notes_in - } -} - -impl MidiPlaybackApi for PhrasePlayerModel { - fn notes_out (&self) -> &Arc> { - &self.notes_in - } -} - -impl HasPlayPhrase for PhrasePlayerModel { - fn reset (&self) -> bool { - self.reset - } - fn reset_mut (&mut self) -> &mut bool { - &mut self.reset - } - fn play_phrase (&self) -> &Option<(Moment, Option>>)> { - &self.play_phrase - } - fn play_phrase_mut (&mut self) -> &mut Option<(Moment, Option>>)> { - &mut self.play_phrase - } - fn next_phrase (&self) -> &Option<(Moment, Option>>)> { - &self.next_phrase - } - fn next_phrase_mut (&mut self) -> &mut Option<(Moment, Option>>)> { - &mut self.next_phrase - } -} - -//#[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/midi/midi_in.rs b/crates/tek/src/midi/midi_in.rs deleted file mode 100644 index 4750ca9e..00000000 --- a/crates/tek/src/midi/midi_in.rs +++ /dev/null @@ -1,10 +0,0 @@ -use crate::*; - -/// Trait for thing that may receive MIDI. -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 - } -} diff --git a/crates/tek/src/midi/midi_launch.rs b/crates/tek/src/midi/midi_launch.rs deleted file mode 100644 index e680601a..00000000 --- a/crates/tek/src/midi/midi_launch.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::*; - -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() { - let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); - Some(elapsed) - } else { - None - } - } - fn pulses_since_start_looped (&self) -> Option { - if let Some((started, Some(phrase))) = self.play_phrase().as_ref() { - let elapsed = self.clock().playhead.pulse.get() - started.pulse.get(); - let length = phrase.read().unwrap().length.max(1); // prevent div0 on empty phrase - let elapsed = (elapsed as usize % length) as f64; - Some(elapsed) - } 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; - } -} diff --git a/crates/tek/src/midi/midi_note.rs b/crates/tek/src/midi/midi_note.rs deleted file mode 100644 index d34226c6..00000000 --- a/crates/tek/src/midi/midi_note.rs +++ /dev/null @@ -1,143 +0,0 @@ -use crate::*; -pub struct Note; -impl Note { - /// (pulses, name), assuming 96 PPQ - pub const 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 (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 (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 } } - "" - } -} -pub trait MidiView: MidiRange + MidiPoint + HasSize { - /// Make sure cursor is within range - fn autoscroll (&self) { - let note_point = self.note_point().min(127); - let note_lo = self.note_lo().get(); - let note_hi = self.note_hi(); - if note_point < note_lo { - self.note_lo().set(note_point); - } else if note_point > note_hi { - self.note_lo().set((note_lo + note_point).saturating_sub(note_hi)); - } - } - /// Make sure range is within display - fn autozoom (&self) { - let time_len = self.time_len().get(); - let time_axis = self.time_axis().get(); - let mut time_zoom = self.time_zoom().get(); - //while time_len.div_ceil(time_zoom) > time_axis { - //println!("\r{time_len} {time_zoom} {time_axis}"); - //time_zoom = Note::next(time_zoom); - //} - //self.time_zoom().set(time_zoom); - } -} -#[derive(Debug, Clone)] -pub struct MidiRangeModel { - pub time_len: Arc, - /// Length of visible time axis - pub time_axis: Arc, - /// Earliest time displayed - pub time_start: Arc, - /// Time step - pub time_zoom: Arc, - /// Auto rezoom to fit in time axis - pub time_lock: Arc, - /// Length of visible note axis - pub note_axis: Arc, - // Lowest note displayed - pub note_lo: Arc, -} -from!(|data:(usize, bool)|MidiRangeModel = Self { - time_len: Arc::new(0.into()), - note_axis: Arc::new(0.into()), - note_lo: Arc::new(0.into()), - time_axis: Arc::new(0.into()), - time_start: Arc::new(0.into()), - time_zoom: Arc::new(data.0.into()), - time_lock: Arc::new(data.1.into()), -}); -pub trait MidiRange { - fn time_len (&self) -> &AtomicUsize; - fn time_zoom (&self) -> &AtomicUsize; - fn time_lock (&self) -> &AtomicBool; - fn time_start (&self) -> &AtomicUsize; - fn note_lo (&self) -> &AtomicUsize; - fn note_axis (&self) -> &AtomicUsize; - fn time_axis (&self) -> &AtomicUsize; - fn note_hi (&self) -> usize { - (self.note_lo().get() + self.note_axis().get().saturating_sub(1)).min(127) - } - fn time_end (&self) -> usize { - self.time_start().get() + self.time_axis().get() * self.time_zoom().get() - } -} -impl MidiRange for MidiRangeModel { - fn time_len (&self) -> &AtomicUsize { &self.time_len } - fn time_zoom (&self) -> &AtomicUsize { &self.time_zoom } - fn time_lock (&self) -> &AtomicBool { &self.time_lock } - fn time_start (&self) -> &AtomicUsize { &self.time_start } - fn note_lo (&self) -> &AtomicUsize { &self.note_lo } - fn note_axis (&self) -> &AtomicUsize { &self.note_axis } - fn time_axis (&self) -> &AtomicUsize { &self.time_axis } -} - -#[derive(Debug, Clone)] -pub struct MidiPointModel { - /// Time coordinate of cursor - pub time_point: Arc, - /// Note coordinate of cursor - pub note_point: Arc, - /// Length of note that will be inserted, in pulses - pub note_len: Arc, -} -impl Default for MidiPointModel { - fn default () -> Self { - Self { - time_point: Arc::new(0.into()), - note_point: Arc::new(36.into()), - note_len: Arc::new(24.into()), - } - } -} -pub trait MidiPoint { - fn note_len (&self) -> usize; - fn set_note_len (&self, x: usize); - fn note_point (&self) -> usize; - fn set_note_point (&self, x: usize); - fn time_point (&self) -> usize; - fn set_time_point (&self, x: usize); - fn note_end (&self) -> usize { self.note_point() + self.note_len() } -} -impl MidiPoint for MidiPointModel { - fn note_len (&self) -> usize { self.note_len.load(Relaxed)} - fn set_note_len (&self, x: usize) { self.note_len.store(x, Relaxed) } - fn note_point (&self) -> usize { self.note_point.load(Relaxed).min(127) } - fn set_note_point (&self, x: usize) { self.note_point.store(x.min(127), Relaxed) } - fn time_point (&self) -> usize { self.time_point.load(Relaxed) } - fn set_time_point (&self, x: usize) { self.time_point.store(x, Relaxed) } -} diff --git a/crates/tek/src/midi/midi_out.rs b/crates/tek/src/midi/midi_out.rs deleted file mode 100644 index 0010ef4d..00000000 --- a/crates/tek/src/midi/midi_out.rs +++ /dev/null @@ -1,12 +0,0 @@ -use crate::*; - -/// Trait for thing that may output MIDI. -pub trait HasMidiOuts { - fn midi_outs (&self) -> &Vec>; - fn midi_outs_mut (&mut self) -> &mut Vec>; - fn has_midi_outs (&self) -> bool { - self.midi_outs().len() > 0 - } - /// Buffer for serializing a MIDI event. FIXME rename - fn midi_note (&mut self) -> &mut Vec; -} diff --git a/crates/tek/src/midi/midi_phrase.rs b/crates/tek/src/midi/midi_phrase.rs deleted file mode 100644 index 1c1f66dd..00000000 --- a/crates/tek/src/midi/midi_phrase.rs +++ /dev/null @@ -1,110 +0,0 @@ -use crate::*; - -pub trait HasPhrase { - fn phrase (&self) -> &Arc>; -} - -#[macro_export] macro_rules! has_phrase { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasPhrase for $Struct $(<$($L),*$($T),*>)? { - fn phrase (&$self) -> &Arc> { &$cb } - } - } -} - -/// 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 looped: 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: ItemPalette, -} - -/// MIDI message structural -pub type PhraseData = Vec>; - -impl Phrase { - pub fn new ( - name: impl AsRef, - looped: 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]), - looped, - loop_start: 0, - loop_length: length, - percussive: true, - color: color.unwrap_or_else(ItemPalette::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.looped = !self.looped; } - 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( - "Stop", - false, - 1, - Some(vec![vec![MidiMessage::Controller { - controller: 123.into(), - value: 0.into() - }]]), - Some(ItemColor::from(Color::Rgb(32, 32, 32)).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/midi/midi_play.rs b/crates/tek/src/midi/midi_play.rs deleted file mode 100644 index e32af28b..00000000 --- a/crates/tek/src/midi/midi_play.rs +++ /dev/null @@ -1,134 +0,0 @@ -use crate::*; - -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:?}")); - } - } - } - } -} diff --git a/crates/tek/src/midi/midi_pool.rs b/crates/tek/src/midi/midi_pool.rs deleted file mode 100644 index 555059be..00000000 --- a/crates/tek/src/midi/midi_pool.rs +++ /dev/null @@ -1,93 +0,0 @@ -use crate::*; - -pub trait HasPhrases { - fn phrases (&self) -> &Vec>>; - fn phrases_mut (&mut self) -> &mut Vec>>; -} - -#[macro_export] macro_rules! has_phrases { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasPhrases for $Struct $(<$($L),*$($T),*>)? { - fn phrases (&$self) -> &Vec>> { &$cb } - fn phrases_mut (&mut $self) -> &mut Vec>> { &mut$cb } - } - } -} - -#[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 = ItemPalette::from(color); - std::mem::swap(&mut color, &mut model.phrases()[index].write().unwrap().color); - Some(Self::SetColor(index, color.base)) - }, - }) - } -} diff --git a/crates/tek/src/midi/midi_rec.rs b/crates/tek/src/midi/midi_rec.rs deleted file mode 100644 index 22a58f74..00000000 --- a/crates/tek/src/midi/midi_rec.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::*; - -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(); - } -} diff --git a/crates/tek/src/plugin.rs b/crates/tek/src/plugin.rs deleted file mode 100644 index efbe4cd0..00000000 --- a/crates/tek/src/plugin.rs +++ /dev/null @@ -1,127 +0,0 @@ -pub(crate) mod lv2; pub(crate) use lv2::*; -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>, -} - -/// 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", - }) - } -} -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>); -from!(|model: &Arc>| PluginAudio = Self(model.clone())); -audio!(|self: PluginAudio, client_, _scope|{ - 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/plugin/lv2.rs b/crates/tek/src/plugin/lv2.rs deleted file mode 100644 index b47c159b..00000000 --- a/crates/tek/src/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/plugin/lv2_gui.rs b/crates/tek/src/plugin/lv2_gui.rs deleted file mode 100644 index 8b94a94c..00000000 --- a/crates/tek/src/plugin/lv2_gui.rs +++ /dev/null @@ -1,59 +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/plugin/lv2_tui.rs b/crates/tek/src/plugin/lv2_tui.rs deleted file mode 100644 index 4ec242b0..00000000 --- a/crates/tek/src/plugin/lv2_tui.rs +++ /dev/null @@ -1,47 +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/plugin/vst3_tui.rs b/crates/tek/src/plugin/vst3_tui.rs deleted file mode 100644 index 0f3ed08a..00000000 --- a/crates/tek/src/plugin/vst3_tui.rs +++ /dev/null @@ -1,2 +0,0 @@ -//! TODO - diff --git a/crates/tek/src/space.rs b/crates/tek/src/space.rs deleted file mode 100644 index 1934f727..00000000 --- a/crates/tek/src/space.rs +++ /dev/null @@ -1,30 +0,0 @@ -pub(crate) mod coord; pub(crate) use coord::*; -pub(crate) mod size; pub(crate) use size::*; -pub(crate) mod area; pub(crate) use area::*; -pub(crate) mod direction; pub(crate) use direction::*; - -// TODO: return impl Point and impl Size instead of [N;x] -// to disambiguate between usage of 2-"tuple"s - -////////////////////////////////////////////////////// - -pub(crate) mod align; -pub(crate) mod bsp; -pub(crate) mod cond; -pub(crate) mod fill; -pub(crate) mod fixed; pub(crate) use fixed::*; -pub(crate) mod inset_outset; pub(crate) use inset_outset::*; -pub(crate) mod layers; pub(crate) use layers::*; -pub(crate) mod measure; pub(crate) use measure::*; -pub(crate) mod min_max; pub(crate) use min_max::*; -pub(crate) mod push_pull; pub(crate) use push_pull::*; -pub(crate) mod scroll; -pub(crate) mod shrink_grow; pub(crate) use shrink_grow::*; -pub(crate) mod split; pub(crate) use split::*; -pub(crate) mod stack; pub(crate) use stack::*; - -pub use self::{ - align::*, - bsp::*, - fill::*, -}; diff --git a/crates/tek/src/space/align.rs b/crates/tek/src/space/align.rs deleted file mode 100644 index 9cfa1ec5..00000000 --- a/crates/tek/src/space/align.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::*; - -/// Override X and Y coordinates, aligning to corner, side, or center of area -pub enum Align> { - _Unused(PhantomData), - /// Draw at center of container - Center(T), - /// Draw at center of X axis - X(T), - /// Draw at center of Y axis - Y(T), - /// Draw at upper left corner of contaier - NW(T), - /// Draw at center of upper edge of container - N(T), - /// Draw at right left corner of contaier - NE(T), - /// Draw at center of left edge of container - W(T), - /// Draw at center of right edge of container - E(T), - /// Draw at lower left corner of container - SW(T), - /// Draw at center of lower edge of container - S(T), - /// Draw at lower right edge of container - SE(T) -} - -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, - _ => unreachable!(), - } - } - pub fn c (w: T) -> Self { Self::Center(w) } - pub fn x (w: T) -> Self { Self::X(w) } - pub fn y (w: T) -> Self { Self::Y(w) } - pub fn n (w: T) -> Self { Self::N(w) } - pub fn s (w: T) -> Self { Self::S(w) } - pub fn e (w: T) -> Self { Self::E(w) } - pub fn w (w: T) -> Self { Self::W(w) } - pub fn nw (w: T) -> Self { Self::NW(w) } - pub fn sw (w: T) -> Self { Self::SW(w) } - pub fn ne (w: T) -> Self { Self::NE(w) } - pub fn se (w: T) -> Self { Self::SE(w) } -} - -fn align, N: Coordinate, R: Area + 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(), - _ => unreachable!() - }) - } -} - -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/space/area.rs b/crates/tek/src/space/area.rs deleted file mode 100644 index c1c8d350..00000000 --- a/crates/tek/src/space/area.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::*; - -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().minus(x), self.h()] - } - #[inline] fn shrink_y (&self, y: N) -> [N;4] { - [self.x(), self.y(), self.w(), self.h().minus(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()], - ), - Direction::Left => ( - [self.x() + self.w() - a, self.y(), a, self.h()], - [self.x(), self.y(), self.w() - a, self.h()], - ), - } - } -} - -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] } -} diff --git a/crates/tek/src/space/bsp.rs b/crates/tek/src/space/bsp.rs deleted file mode 100644 index 47474f1c..00000000 --- a/crates/tek/src/space/bsp.rs +++ /dev/null @@ -1,104 +0,0 @@ -use crate::*; - -pub enum Bsp, Y: Render> { - /// X is north of Y - N(Option, Option), - /// X is south of Y - S(Option, Option), - /// X is east of Y - E(Option, Option), - /// X is west of Y - W(Option, Option), - /// X is above Y - A(Option, Option), - /// X is below Y - B(Option, Option), - /// Should be avoided. - Null(PhantomData), -} - -impl, Y: Render> Bsp { - pub fn new (x: X) -> Self { Self::A(Some(x), None) } - pub fn n (x: X, y: Y) -> Self { Self::N(Some(x), Some(y)) } - pub fn s (x: X, y: Y) -> Self { Self::S(Some(x), Some(y)) } - pub fn e (x: X, y: Y) -> Self { Self::E(Some(x), Some(y)) } - pub fn w (x: X, y: Y) -> Self { Self::W(Some(x), Some(y)) } - pub fn a (x: X, y: Y) -> Self { Self::A(Some(x), Some(y)) } - pub fn b (x: X, y: Y) -> Self { Self::B(Some(x), Some(y)) } -} - -impl, Y: Render> Default for Bsp { - fn default () -> Self { - Self::Null(Default::default()) - } -} - -impl, Y: Render> Render for Bsp { - fn min_size (&self, to: E::Size) -> Perhaps { - Ok(Some(match self { - Self::Null(_) => [0.into(), 0.into()].into(), - Self::S(a, b) => { - let a = a.min_size(to)?.unwrap_or([0.into(), 0.into()].into()); - let b = b.min_size(to)?.unwrap_or([0.into(), 0.into()].into()); - [a.w().max(b.w()), a.h() + b.h()].into() - }, - Self::E(a, b) => { - let a = a.min_size(to)?.unwrap_or([0.into(), 0.into()].into()); - let b = b.min_size(to)?.unwrap_or([0.into(), 0.into()].into()); - [a.w() + b.w(), a.h().max(b.h())].into() - }, - Self::W(a, b) => { - let a = a.min_size(to)?.unwrap_or([0.into(), 0.into()].into()); - let b = b.min_size(to)?.unwrap_or([0.into(), 0.into()].into()); - [a.w() + b.w(), a.h().max(b.h())].into() - }, - Self::N(a, b) => { - let a = a.min_size(to)?.unwrap_or([0.into(), 0.into()].into()); - let b = b.min_size(to)?.unwrap_or([0.into(), 0.into()].into()); - [a.w().max(b.w()), a.h() + b.h()].into() - }, - _ => todo!() - })) - } - fn render (&self, to: &mut E::Output) -> Usually<()> { - let n = [0.into(), 0.into()].into(); - let s = to.area().wh().into(); - Ok(match self { - Self::Null(_) => {}, - Self::S(a, b) => { - let s_a = a.min_size(s)?.unwrap_or(n); - let _ = b.min_size(s)?.unwrap_or(n); - let h = s_a.h().into(); - to.render_in(to.area().clip_h(h).into(), a)?; - to.render_in(to.area().shrink_y(h).push_y(h).into(), b)?; - }, - Self::E(a, b) => { - let s_a = a.min_size(s)?.unwrap_or(n); - let _ = b.min_size(s)?.unwrap_or(n); - let w = s_a.w().into(); - to.render_in(to.area().clip_w(w).into(), a)?; - to.render_in(to.area().push_x(w).shrink_x(w).into(), b)?; - }, - Self::W(a, b) => { - let s_a = a.min_size(s)?.unwrap_or(n); - let _ = b.min_size(s)?.unwrap_or(n); - let w = (to.area().w() - s_a.w()).into(); - to.render_in(to.area().push_x(w).into(), a)?; - to.render_in(to.area().shrink_x(w).into(), b)?; - }, - Self::N(a, b) => { - let s_a = a.min_size(s)?.unwrap_or(n); - let _ = b.min_size(s)?.unwrap_or(n); - let h = to.area().h() - s_a.h(); - to.render_in(to.area().push_y(h).into(), a)?; - to.render_in(to.area().shrink_y(h).into(), b)?; - }, - _ => todo!() - }) - } -} - -#[cfg(test)] -mod test { - use super::*; -} diff --git a/crates/tek/src/space/collect.rs b/crates/tek/src/space/collect.rs deleted file mode 100644 index 141992f1..00000000 --- a/crates/tek/src/space/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/space/cond.rs b/crates/tek/src/space/cond.rs deleted file mode 100644 index 7966f536..00000000 --- a/crates/tek/src/space/cond.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::*; - -pub enum Cond, B: Render> { - _Unused(E), - When(bool, A), - Either(bool, A, B) -} - -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/space/coord.rs b/crates/tek/src/space/coord.rs deleted file mode 100644 index 7feac7dc..00000000 --- a/crates/tek/src/space/coord.rs +++ /dev/null @@ -1,37 +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 -{} diff --git a/crates/tek/src/space/direction.rs b/crates/tek/src/space/direction.rs deleted file mode 100644 index 7ad3eb8d..00000000 --- a/crates/tek/src/space/direction.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::*; - -#[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/space/fill.rs b/crates/tek/src/space/fill.rs deleted file mode 100644 index 044d751a..00000000 --- a/crates/tek/src/space/fill.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::*; - -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!(), - } - } - pub fn w (fill: W) -> Self { - Self::X(fill) - } - pub fn h (fill: W) -> Self { - Self::Y(fill) - } - pub fn wh (fill: W) -> Self { - Self::XY(fill) - } -} - -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/space/fixed.rs b/crates/tek/src/space/fixed.rs deleted file mode 100644 index 63db372b..00000000 --- a/crates/tek/src/space/fixed.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::*; - -/// 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!(), - } - } - pub fn w (x: E::Unit, w: T) -> Self { - Self::X(x, w) - } - pub fn h (y: E::Unit, w: T) -> Self { - Self::Y(y, w) - } - pub fn wh (x: E::Unit, y: E::Unit, w: T) -> Self { - Self::XY(x, y, w) - } -} - -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/space/inset_outset.rs b/crates/tek/src/space/inset_outset.rs deleted file mode 100644 index 2985aecf..00000000 --- a/crates/tek/src/space/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/space/layers.rs b/crates/tek/src/space/layers.rs deleted file mode 100644 index 353198ab..00000000 --- a/crates/tek/src/space/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/space/map_reduce.rs b/crates/tek/src/space/map_reduce.rs deleted file mode 100644 index 833973b2..00000000 --- a/crates/tek/src/space/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/space/measure.rs b/crates/tek/src/space/measure.rs deleted file mode 100644 index c92264cb..00000000 --- a/crates/tek/src/space/measure.rs +++ /dev/null @@ -1,93 +0,0 @@ -use crate::*; - -pub trait HasSize { - fn size (&self) -> &Measure; -} - -#[macro_export] macro_rules! has_size { - (<$E:ty>|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasSize<$E> for $Struct $(<$($L),*$($T),*>)? { - fn size (&$self) -> &Measure<$E> { $cb } - } - } -} - -impl LayoutDebug for E {} - -pub trait LayoutDebug { - fn debug > (other: W) -> DebugOverlay { - DebugOverlay(Default::default(), other) - } -} - -pub struct DebugOverlay>(PhantomData, pub W); - -/// A widget that tracks its render width and height -#[derive(Default)] -pub struct Measure { - _engine: PhantomData, - pub x: Arc, - pub y: Arc, -} - -impl Clone for Measure { - fn clone (&self) -> Self { - Self { - _engine: Default::default(), - x: self.x.clone(), - y: self.y.clone(), - } - } -} - -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.x) - .field("height", &self.y) - .finish() - } -} - -impl Measure { - pub fn w (&self) -> usize { self.x.load(Relaxed) } - pub fn h (&self) -> usize { self.y.load(Relaxed) } - pub fn wh (&self) -> [usize;2] { [self.w(), self.h()] } - pub fn set_w (&self, w: impl Into) { self.x.store(w.into(), Relaxed) } - pub fn set_h (&self, h: impl Into) { self.y.store(h.into(), Relaxed) } - pub fn set_wh (&self, w: impl Into, h: impl Into) { self.set_w(w); self.set_h(h); } - pub fn format (&self) -> String { format!("{}x{}", self.w(), self.h()) } - pub fn new () -> Self { - Self { - _engine: PhantomData::default(), - x: Arc::new(0.into()), - y: Arc::new(0.into()), - } - } -} - -impl Render for Measure { - fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> { - Ok(Some([0u16.into(), 0u16.into()].into())) - } - fn render (&self, to: &mut TuiOutput) -> Usually<()> { - self.set_w(to.area().w()); - self.set_h(to.area().h()); - Ok(()) - } -} - -impl Measure { - pub fn debug (&self) -> ShowMeasure { - ShowMeasure(&self) - } -} - -pub struct ShowMeasure<'a>(&'a Measure); -render!(|self: ShowMeasure<'a>|render(|to|Ok({ - let w = self.0.w(); - let h = self.0.h(); - to.blit(&format!(" {w} x {h} "), to.area.x(), to.area.y(), Some( - Style::default().bold().italic().bg(Color::Rgb(255, 0, 255)).fg(Color::Rgb(0,0,0)) - )) -}))); diff --git a/crates/tek/src/space/min_max.rs b/crates/tek/src/space/min_max.rs deleted file mode 100644 index f0b5f26f..00000000 --- a/crates/tek/src/space/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/space/push_pull.rs b/crates/tek/src/space/push_pull.rs deleted file mode 100644 index 2b83bd0e..00000000 --- a/crates/tek/src/space/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/space/scroll.rs b/crates/tek/src/space/scroll.rs deleted file mode 100644 index 326f6ab6..00000000 --- a/crates/tek/src/space/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/space/shrink_grow.rs b/crates/tek/src/space/shrink_grow.rs deleted file mode 100644 index 9c725ef4..00000000 --- a/crates/tek/src/space/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/space/size.rs b/crates/tek/src/space/size.rs deleted file mode 100644 index 5fe0ed40..00000000 --- a/crates/tek/src/space/size.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::*; - -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] } -} diff --git a/crates/tek/src/space/split.rs b/crates/tek/src/space/split.rs deleted file mode 100644 index e4c98785..00000000 --- a/crates/tek/src/space/split.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::*; - -impl LayoutSplit for E {} - -pub trait LayoutSplit { - fn split , B: Render> ( - flip: bool, direction: Direction, amount: E::Unit, a: A, b: B - ) -> Split { - Split::new(flip, direction, amount, a, b) - } - fn split_n , B: Render> ( - flip: bool, amount: E::Unit, a: A, b: B - ) -> Split { - Self::split(flip, Direction::Up, amount, a, b) - } - fn split_s , B: Render> ( - flip: bool, amount: E::Unit, a: A, b: B - ) -> Split { - Self::split(flip, Direction::Down, amount, a, b) - } - fn split_w , B: Render> ( - flip: bool, amount: E::Unit, a: A, b: B - ) -> Split { - Self::split(flip, Direction::Left, amount, a, b) - } - fn split_e , B: Render> ( - flip: bool, amount: E::Unit, a: A, b: B - ) -> Split { - Self::split(flip, Direction::Right, amount, a, b) - } -} - -/// A binary split with fixed proportion -pub struct Split, B: Render>( - pub bool, pub Direction, pub E::Unit, A, B, PhantomData -); - -impl, B: Render> Split { - pub fn new (flip: bool, direction: Direction, proportion: E::Unit, a: A, b: B) -> Self { - Self(flip, direction, proportion, a, b, Default::default()) - } - pub fn up (flip: bool, proportion: E::Unit, a: A, b: B) -> Self { - Self(flip, Direction::Up, proportion, a, b, Default::default()) - } - pub fn down (flip: bool, proportion: E::Unit, a: A, b: B) -> Self { - Self(flip, Direction::Down, proportion, a, b, Default::default()) - } - pub fn left (flip: bool, proportion: E::Unit, a: A, b: B) -> Self { - Self(flip, Direction::Left, proportion, a, b, Default::default()) - } - pub fn right (flip: bool, proportion: E::Unit, a: A, b: B) -> Self { - Self(flip, 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.1, self.2); - Ok(if self.0 { - to.render_in(a.into(), &self.4)?; - to.render_in(b.into(), &self.3)?; - } else { - to.render_in(a.into(), &self.3)?; - to.render_in(b.into(), &self.4)?; - }) - } -} diff --git a/crates/tek/src/space/stack.rs b/crates/tek/src/space/stack.rs deleted file mode 100644 index 914217d9..00000000 --- a/crates/tek/src/space/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 w: E::Unit = 0.into(); - let 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/test.rs b/crates/tek/src/test.rs deleted file mode 100644 index fcb75eeb..00000000 --- a/crates/tek/src/test.rs +++ /dev/null @@ -1,183 +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 - //} -//} - -//#[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!(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/time.rs b/crates/tek/src/time.rs deleted file mode 100644 index 4cc89706..00000000 --- a/crates/tek/src/time.rs +++ /dev/null @@ -1,16 +0,0 @@ -pub(crate) mod clock; pub(crate) use clock::*; -pub(crate) mod moment; pub(crate) use moment::*; -pub(crate) mod perf; pub(crate) use perf::*; -pub(crate) mod pulse; pub(crate) use pulse::*; -pub(crate) mod sr; pub(crate) use sr::*; -pub(crate) mod unit; pub(crate) use unit::*; - -//#[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/time/clock.rs b/crates/tek/src/time/clock.rs deleted file mode 100644 index 8798029a..00000000 --- a/crates/tek/src/time/clock.rs +++ /dev/null @@ -1,193 +0,0 @@ -use crate::*; - -pub trait HasClock: Send + Sync { - fn clock (&self) -> &ClockModel; -} - -#[macro_export] macro_rules! has_clock { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasClock for $Struct $(<$($L),*$($T),*>)? { - fn clock (&$self) -> &ClockModel { $cb } - } - } -} - -#[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 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>>, - /// Playback offset (when playing not from start) - pub offset: 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, -} - -from!(|jack: &Arc>| ClockModel = { - 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)), - offset: 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, Relaxed); - } - pub fn update_from_scope (&self, scope: &ProcessScope) -> Usually<()> { - // Store buffer length - self.set_chunk(scope.n_frames() as usize); - - // Store reported global frame and usec - 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); - - // If transport has just started or just stopped, - // update starting point: - let mut started = self.started.write().unwrap(); - match (self.transport.query_state()?, started.as_ref()) { - (TransportState::Rolling, 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, Some(_)) => { - *started = None; - }, - _ => {} - }; - - self.playhead.update_from_sample(started.as_ref() - .map(|started|current_frames as f64 - started.sample.get()) - .unwrap_or(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/time/perf.rs b/crates/tek/src/time/perf.rs deleted file mode 100644 index abe1e593..00000000 --- a/crates/tek/src/time/perf.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::*; - -/// 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: &ProcessScope) { - if let Some(t0) = t0 { - let t1 = self.clock.raw(); - self.used.store( - self.clock.delta_as_nanos(t0, t1) as f64, - Relaxed, - ); - self.period.store( - scope.cycle_times().unwrap().period_usecs as f64, - Relaxed, - ); - } - } - pub fn percentage (&self) -> Option { - let period = self.period.load(Relaxed) * 1000.0; - if period > 0.0 { - let used = self.used.load(Relaxed); - Some(100.0 * used / period) - } else { - None - } - } -} diff --git a/crates/tek/src/tui.rs b/crates/tek/src/tui.rs deleted file mode 100644 index 7d108d84..00000000 --- a/crates/tek/src/tui.rs +++ /dev/null @@ -1,280 +0,0 @@ -use crate::*; - -mod tui_input; -pub(crate) use tui_input::*; -pub use tui_input::TuiInput; - -mod tui_output; -pub(crate) use tui_output::*; -pub use tui_output::TuiOutput; - -//////////////////////////////////////////////////////// - -mod tui_style; -mod tui_theme; -pub(crate) use tui_theme::*; -mod tui_border; -pub(crate) use tui_border::*; - -//////////////////////////////////////////////////////// - -mod app_transport; pub(crate) use app_transport::*; -mod app_sequencer; pub(crate) use app_sequencer::*; -mod app_sampler; pub(crate) use app_sampler::*; -mod app_groovebox; pub(crate) use app_groovebox::*; -mod app_arranger; pub(crate) use app_arranger::*; - -/////////////////////////////////////////////////////// - -mod arranger_command; pub(crate) use arranger_command::*; -mod arranger_scene; pub(crate) use arranger_scene::*; -mod arranger_select; pub(crate) use arranger_select::*; -mod arranger_track; pub(crate) use arranger_track::*; -mod arranger_mode_h; -mod arranger_mode_v; pub(crate) use arranger_mode_v::*; - -//////////////////////////////////////////////////////// - -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 piano_horizontal; pub(crate) use piano_horizontal::*; -mod phrase_length; pub(crate) use phrase_length::*; -mod phrase_rename; pub(crate) use phrase_rename::*; -mod pool; pub(crate) use pool::*; -mod port_select; - -//////////////////////////////////////////////////////// - -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, 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, Relaxed) { - break - } - if ::crossterm::event::poll(poll).is_ok() { - let event = TuiEvent::Input(::crossterm::event::read().unwrap()); - match event { - key_pat!(Ctrl-KeyCode::Char('c')) => { - exited.store(true, 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, 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/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs deleted file mode 100644 index 6a554b17..00000000 --- a/crates/tek/src/tui/app_arranger.rs +++ /dev/null @@ -1,181 +0,0 @@ -use crate::*; -/// Root view for standalone `tek_arranger` -pub struct ArrangerTui { - jack: Arc>, - pub clock: ClockModel, - pub phrases: PoolModel, - pub tracks: Vec, - pub scenes: Vec, - pub splits: [u16;2], - pub selected: ArrangerSelection, - pub mode: ArrangerMode, - pub color: ItemPalette, - pub size: Measure, - pub note_buf: Vec, - pub midi_buf: Vec>>, - pub editor: PhraseEditorModel, - pub perf: PerfModel, -} -impl ArrangerTui { - pub fn selected (&self) -> ArrangerSelection { - self.selected - } - pub fn selected_mut (&mut self) -> &mut ArrangerSelection { - &mut self.selected - } - pub 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[t].player.enqueue_next(phrase.as_ref()); - }; - Ok(()) - } - pub fn selected_phrase (&self) -> Option>> { - self.selected_scene()?.clips.get(self.selected.track()?)?.clone() - } - pub fn toggle_loop (&mut self) { - if let Some(phrase) = self.selected_phrase() { - phrase.write().unwrap().toggle_loop() - } - } - pub fn randomize_color (&mut self) { - match self.selected { - ArrangerSelection::Mix => { self.color = ItemPalette::random() }, - ArrangerSelection::Track(t) => { self.tracks[t].color = ItemPalette::random() }, - ArrangerSelection::Scene(s) => { self.scenes[s].color = ItemPalette::random() }, - ArrangerSelection::Clip(t, s) => if let Some(phrase) = &self.scenes[s].clips[t] { - phrase.write().unwrap().color = ItemPalette::random(); - } - } - } -} -from_jack!(|jack| ArrangerTui { - let clock = ClockModel::from(jack); - let phrase = Arc::new(RwLock::new(Phrase::new( - "New", true, 4 * clock.timebase.ppq.get() as usize, - None, Some(ItemColor::random().into()) - ))); - Self { - clock, - phrases: (&phrase).into(), - editor: (&phrase).into(), - selected: ArrangerSelection::Clip(0, 0), - scenes: vec![], - tracks: vec![], - color: TuiTheme::bg().into(), - mode: ArrangerMode::V(1), - size: Measure::new(), - splits: [12, 20], - midi_buf: vec![vec![];65536], - note_buf: vec![], - perf: PerfModel::default(), - jack: jack.clone(), - } -}); -impl ArrangerTui { - fn render_mode (state: &Self) -> impl Render + use<'_> { - match state.mode { - ArrangerMode::H => todo!("horizontal arranger"), - ArrangerMode::V(factor) => Self::render_mode_v(state, factor), - } - } -} -render!(|self: ArrangerTui|{ - let play = Fixed::wh(5, 2, PlayPause(self.clock.is_rolling())); - let transport = TransportView::from((self, Some(ItemPalette::from(TuiTheme::g(96))), true)); - let with_transport = |x|col!([row!(![&play, &transport]), &x]); - let pool_size = if self.phrases.visible { self.splits[1] } else { 0 }; - let with_pool = |x|Split::left(false, pool_size, PoolView(&self.phrases), x); - let status = ArrangerStatus::from(self); - let with_editbar = |x|Tui::split_n(false, 3, PhraseEditStatus(&self.editor), x); - let with_status = |x|Tui::split_n(false, 2, status, x); - let with_size = |x|lay!([&self.size, x]); - let arranger = ||lay!(|add|{ - let color = self.color; - add(&Fill::wh(Tui::bg(color.darkest.rgb, ())))?; - add(&Fill::wh(Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb))))?; - add(&Self::render_mode(self)) - }); - with_size(with_status(with_editbar(with_pool(with_transport(col!([ - Fill::w(Fixed::h(20, arranger())), - Fill::wh(&self.editor), - ])))))) -}); -audio!(|self: ArrangerTui, 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 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 -}); -has_clock!(|self: ArrangerTui|&self.clock); -has_phrases!(|self: ArrangerTui|self.phrases.phrases); -has_editor!(|self: ArrangerTui|self.editor); -handle!(|self: ArrangerTui, input|ArrangerCommand::execute_with_state(self, input)); - -/// Display mode of arranger -#[derive(Clone, PartialEq)] -pub enum ArrangerMode { - /// Tracks are columns - V(usize), - /// Tracks are rows - H, -} -render!(|self: ArrangerMode|{}); - -/// Arranger display mode can be cycled -impl ArrangerMode { - /// Cycle arranger display mode - pub fn to_next (&mut self) { - *self = match self { - Self::H => Self::V(1), - Self::V(1) => Self::V(2), - Self::V(2) => Self::V(2), - Self::V(0) => Self::H, - Self::V(_) => Self::V(0), - } - } -} - -fn any_size (_: E::Size) -> Perhaps{ - Ok(Some([0.into(),0.into()].into())) -} diff --git a/crates/tek/src/tui/app_groovebox.rs b/crates/tek/src/tui/app_groovebox.rs deleted file mode 100644 index 92241815..00000000 --- a/crates/tek/src/tui/app_groovebox.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::*; -use super::*; -pub struct GrooveboxTui { - pub sequencer: SequencerTui, - pub sampler: SamplerTui, - pub split: u16, - pub focus: GrooveboxFocus -} -from_jack!(|jack|GrooveboxTui { - let mut sequencer = SequencerTui::try_from(jack)?; - sequencer.status = false; - let midi_in_1 = jack.read().unwrap().register_port("in1", MidiIn::default())?; - let midi_out = jack.read().unwrap().register_port("out", MidiOut::default())?; - let midi_in_2 = jack.read().unwrap().register_port("in2", MidiIn::default())?; - let audio_in_1 = jack.read().unwrap().register_port("inL", AudioIn::default())?; - let audio_in_2 = jack.read().unwrap().register_port("inR", AudioIn::default())?; - let audio_out_1 = jack.read().unwrap().register_port("out1", AudioOut::default())?; - let audio_out_2 = jack.read().unwrap().register_port("out2", AudioOut::default())?; - Self { - sequencer, - sampler: SamplerTui::try_from(jack)?, - split: 16, - focus: GrooveboxFocus::Sampler, - } -}); -pub enum GrooveboxFocus { - Sequencer, - Sampler -} -audio!(|self:GrooveboxTui,_client,_process|Control::Continue); -render!(|self:GrooveboxTui|Bsp::n( - Fixed::h(2, SequencerStatus::from(&self.sequencer)), - Fill::h(Bsp::s(&self.sequencer, &self.sampler)), -)); -pub enum GrooveboxCommand { - Sequencer(SequencerCommand), - Sampler(SamplerCommand), -} -handle!(|self:GrooveboxTui,input|GrooveboxCommand::execute_with_state(self, input)); -input_to_command!(GrooveboxCommand: |state:GrooveboxTui,input|match input.event() { - _ => match state.focus { - GrooveboxFocus::Sequencer => GrooveboxCommand::Sequencer( - SequencerCommand::input_to_command(&state.sequencer, input)?), - GrooveboxFocus::Sampler => GrooveboxCommand::Sampler( - SamplerCommand::input_to_command(&state.sampler, input)?), - } -}); -command!(|self:GrooveboxCommand,state:GrooveboxTui|match self { - GrooveboxCommand::Sequencer(command) => - command.execute(&mut state.sequencer)?.map(GrooveboxCommand::Sequencer), - GrooveboxCommand::Sampler(command) => - command.execute(&mut state.sampler)?.map(GrooveboxCommand::Sampler), -}); diff --git a/crates/tek/src/tui/app_mixer.rs b/crates/tek/src/tui/app_mixer.rs deleted file mode 100644 index 111d5cb2..00000000 --- a/crates/tek/src/tui/app_mixer.rs +++ /dev/null @@ -1,248 +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, - } - } -} - -handle!(|self:Mixer,engine|{ - 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) -}); - -handle!(|self:Track,from|{ - 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/app_plugin.rs b/crates/tek/src/tui/app_plugin.rs deleted file mode 100644 index 8f4eddfd..00000000 --- a/crates/tek/src/tui/app_plugin.rs +++ /dev/null @@ -1,147 +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 }) -} - -handle!(|self:Plugin,from|{ - 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/app_sampler.rs b/crates/tek/src/tui/app_sampler.rs deleted file mode 100644 index 8753739e..00000000 --- a/crates/tek/src/tui/app_sampler.rs +++ /dev/null @@ -1,467 +0,0 @@ -use crate::*; -use super::*; -use KeyCode::Char; -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 SamplerTui { - pub state: Sampler, - pub cursor: (usize, usize), - pub editing: Option>>, - pub mode: Option, -} -pub enum SamplerMode { - // Load sample from path - Import(usize, FileBrowser), -} -from_jack!(|jack|SamplerTui{ - let midi_in = jack.read().unwrap().client().register_port("in", MidiIn::default())?; - let audio_outs = vec![ - jack.read().unwrap().client().register_port("outL", AudioOut::default())?, - jack.read().unwrap().client().register_port("outR", AudioOut::default())?, - ]; - Self { - cursor: (0, 0), - editing: None, - mode: None, - state: Sampler { - jack: jack.clone(), - name: "Sampler".into(), - mapped: BTreeMap::new(), - unmapped: vec![], - voices: Arc::new(RwLock::new(vec![])), - buffer: vec![vec![0.0;16384];2], - output_gain: 0.5, - midi_in, - audio_outs, - }, - } -}); -handle!(|self:SamplerTui,input|SamplerCommand::execute_with_state(self, input)); -pub enum SamplerCommand { - Import(FileBrowserCommand), - SelectNote(usize), - SelectField(usize), - SetName(String), - SetNote(u7, Arc>), - SetGain(f32), - NoteOn(u7, u7), - NoteOff(u7) -} -input_to_command!(SamplerCommand:|state:SamplerTui,input|match state.mode { - Some(SamplerMode::Import(..)) => Self::Import( - FileBrowserCommand::input_to_command(state, input)? - ), - _ => match input.event() { - // load sample - key_pat!(Char('l')) => Self::Import(FileBrowserCommand::Begin), - _ => return None - } - //key_pat!(KeyCode::Up) => state.cursor.0 = if state.cursor.0 == 0 { - //mapped.len() + unmapped.len() - 1 - //} else { - //state.cursor.0 - 1 - //}, - //key_pat!(KeyCode::Down) => { - //state.cursor.0 = (state.cursor.0 + 1) % (mapped.len() + unmapped.len()); - //}, - //key_pat!(KeyCode::Char('p')) => if let Some(sample) = self.sample() { - //voices.write().unwrap().push(Sample::play(sample, 0, &100.into())); - //}, - //key_pat!(KeyCode::Char('a')) => { - //let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); - //self.mode = None;//Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); - //unmapped.push(sample); - //}, - //key_pat!(KeyCode::Char('r')) => if let Some(sample) = self.sample() { - //self.mode = None;//Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?)); - //}, - //key_pat!(KeyCode::Enter) => if let Some(sample) = self.sample() { - //self.editing = Some(sample.clone()); - //}, - //_ => { - //return Ok(None) - //} - //} -}); -input_to_command!(FileBrowserCommand:|state:SamplerTui,input|match input { - _ => return None -}); -command!(|self:SamplerCommand,state:SamplerTui|match self { - SamplerCommand::Import(FileBrowserCommand::Begin) => { - let voices = &state.state.voices; - let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); - state.mode = Some(SamplerMode::Import(0, FileBrowser::new(None)?)); - None - }, - _ => todo!() -}); -command!(|self:FileBrowserCommand,state:SamplerTui|match self { - _ => todo!() -}); -impl SamplerTui { - /// Immutable reference to sample at cursor. - pub fn sample (&self) -> Option<&Arc>> { - for (i, sample) in self.state.mapped.values().enumerate() { - if i == self.cursor.0 { - return Some(sample) - } - } - for (i, sample) in self.state.unmapped.iter().enumerate() { - if i + self.state.mapped.len() == self.cursor.0 { - return Some(sample) - } - } - None - } -} - -audio!(|self: SamplerTui, _client, scope|{ - self.state.process_midi_in(scope); - self.state.clear_output_buffer(); - self.state.process_audio_out(scope); - self.state.write_output_buffer(scope); - Control::Continue -}); - -render!(|self: SamplerTui|Tui::min_y(10, Fill::wh(lay!([ - - Fill::wh(render(|to|{ // border - let [x, y, w, h] = to.area(); - let green = Some(Style::default().fg(Color::Green)); - to.blit(&"🭚", x, y, green); - to.blit(&"🭥", x + w.saturating_sub(1), y, green); - to.blit(&"🬿", x, y + h.saturating_sub(1), green); - to.blit(&"🭊", x + w.saturating_sub(1), y + h.saturating_sub(1), green); - Ok(()) - })), - - col!(|add|{ - add(&Tui::push_x(2, row!([ - Tui::bold(true, "Sampler"), "|Voices: ", - &format!("{}", self.state.voices.read().unwrap().len()), - ])))?; - if let Some(SamplerMode::Import(_, browser)) = self.mode.as_ref() { - add(&browser) - } else { - add(&row!([ - " ", - col!(note in 35..=42 => { - &format!("{note:>3} ---------------- +0.0dB") - }), - ])) - } - }), - -])))); - -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) - } -} - -fn read_sample_data (_: &str) -> Usually<(usize, Vec>)> { - todo!(); -} - -fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { - let (mut subdirs, mut files) = std::fs::read_dir(dir)? - .fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{ - let entry = entry.expect("failed to read drectory entry"); - let meta = entry.metadata().expect("failed to read entry metadata"); - if meta.is_file() { - files.push(entry.file_name()); - } else if meta.is_dir() { - subdirs.push(entry.file_name()); - } - (subdirs, files) - }); - subdirs.sort(); - files.sort(); - Ok((subdirs, files)) -} - -impl 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) - } -} - -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) - } -} - -//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)) - //} -//} - -//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) - //}] -//}); diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs deleted file mode 100644 index 01e6db86..00000000 --- a/crates/tek/src/tui/app_sequencer.rs +++ /dev/null @@ -1,171 +0,0 @@ -use crate::*; -use ClockCommand::{Play, Pause}; -use KeyCode::{Tab, Char}; -use SequencerCommand::*; -use PhraseCommand::*; -use PhrasePoolCommand::*; -/// Root view for standalone `tek_sequencer`. -pub struct SequencerTui { - _jack: Arc>, - pub(crate) clock: ClockModel, - pub(crate) phrases: PoolModel, - pub(crate) player: PhrasePlayerModel, - pub(crate) editor: PhraseEditorModel, - pub(crate) size: Measure, - pub(crate) show_pool: bool, - pub(crate) status: bool, - pub(crate) note_buf: Vec, - pub(crate) midi_buf: Vec>>, - pub(crate) perf: PerfModel, -} -from_jack!(|jack|SequencerTui { - let clock = ClockModel::from(jack); - let phrase = Arc::new(RwLock::new(Phrase::new( - "New", true, 4 * clock.timebase.ppq.get() as usize, - None, Some(ItemColor::random().into()) - ))); - Self { - _jack: jack.clone(), - phrases: PoolModel::from(&phrase), - editor: PhraseEditorModel::from(&phrase), - player: PhrasePlayerModel::from((&clock, &phrase)), - size: Measure::new(), - midi_buf: vec![vec![];65536], - note_buf: vec![], - perf: PerfModel::default(), - show_pool: true, - status: true, - clock, - } -}); -render!(|self: SequencerTui|{ - let w = self.size.w(); - let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; - let pool_w = if self.show_pool { phrase_w } else { 0 }; - let pool = Fill::h(Align::e(PoolView(&self.phrases))); - let with_pool = move|x|Tui::split_w(false, pool_w, pool, x); - let status = SequencerStatus::from(self); - let with_status = |x|Tui::split_n(false, if self.status { 2 } else { 0 }, status, x); - let with_editbar = |x|Tui::split_n(false, 3, PhraseEditStatus(&self.editor), x); - let with_size = |x|lay!([self.size, x]); - let editor = with_editbar(with_pool(Fill::wh(&self.editor))); - let color = self.player.play_phrase().as_ref().map(|(_,p)| - p.as_ref().map(|p|p.read().unwrap().color) - ).flatten().clone(); - let play = Fixed::wh(5, 2, PlayPause(self.clock.is_rolling())); - let transport = Fixed::h(2, TransportView::from((self, color, true))); - let toolbar = row!([play, col!([ - PhraseSelector::play_phrase(&self.player), - PhraseSelector::next_phrase(&self.player), - ]), transport]); - Tui::min_y(15, with_size(with_status(col!([ toolbar, editor, ])))) -}); -audio!(|self:SequencerTui, 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 -}); -has_size!(|self:SequencerTui|&self.size); -has_clock!(|self:SequencerTui|&self.clock); -has_phrases!(|self:SequencerTui|self.phrases.phrases); -has_editor!(|self:SequencerTui|self.editor); -handle!(|self:SequencerTui,input|SequencerCommand::execute_with_state(self, input)); -#[derive(Clone, Debug)] pub enum SequencerCommand { - Clock(ClockCommand), - Phrases(PhrasesCommand), - Editor(PhraseCommand), - Enqueue(Option>>), - ShowPool(bool), -} -input_to_command!(SequencerCommand: |state:SequencerTui,input|match input.event() { - // Transport: Play/pause - key_pat!(Char(' ')) => Clock( - if state.clock().is_stopped() { Play(None) } else { Pause(None) } - ), - // Transport: Play from start or rewind to start - key_pat!(Shift-Char(' ')) => Clock( - if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) } - ), - // TODO: u: undo - key_pat!(Char('u')) => { todo!("undo") }, - // TODO: Shift-U: redo - key_pat!(Char('U')) => { todo!("redo") }, - // TODO: k: toggle on-screen keyboard - key_pat!(Ctrl-Char('k')) => { todo!("keyboard") }, - // Tab: Toggle visibility of phrase pool column - key_pat!(Tab) => ShowPool(!state.show_pool), - // q: Enqueue currently edited phrase - key_pat!(Char('q')) => Enqueue(Some(state.phrases.phrase().clone())), - // 0: Enqueue phrase 0 (stop all) - key_pat!(Char('0')) => Enqueue(Some(state.phrases.phrases()[0].clone())), - // e: Toggle between editing currently playing or other phrase - key_pat!(Char('e')) => if let Some((_, Some(playing))) = state.player.play_phrase() { - let editing = state.editor.phrase().as_ref().map(|p|p.read().unwrap().clone()); - let selected = state.phrases.phrase().clone(); - Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing { - selected - } else { - playing.clone() - }))) - } else { - return None - }, - // For the rest, use the default keybindings of the components. - // The ones defined above supersede them. - _ => if let Some(command) = PhraseCommand::input_to_command(&state.editor, input) { - Editor(command) - } else if let Some(command) = PhrasesCommand::input_to_command(&state.phrases, input) { - Phrases(command) - } else { - return None - } -}); -command!(|self: SequencerCommand, state: SequencerTui|match self { - Self::Phrases(cmd) => { - let mut default = |cmd: PhrasesCommand|cmd - .execute(&mut state.phrases) - .map(|x|x.map(Phrases)); - match cmd { - // autoselect: automatically load selected phrase in editor - PhrasesCommand::Select(_) => { - let undo = default(cmd)?; - state.editor.set_phrase(Some(state.phrases.phrase())); - undo - }, - // update color in all places simultaneously - PhrasesCommand::Phrase(SetColor(index, _)) => { - let undo = default(cmd)?; - state.editor.set_phrase(Some(state.phrases.phrase())); - undo - }, - _ => default(cmd)? - } - }, - Self::Editor(cmd) => { - let default = ||cmd.execute(&mut state.editor).map(|x|x.map(Editor)); - match cmd { - _ => default()? - } - }, - Self::Clock(cmd) => cmd.execute(state)?.map(Clock), - Self::Enqueue(phrase) => { - state.player.enqueue_next(phrase.as_ref()); - None - }, - Self::ShowPool(value) => { - state.show_pool = value; - None - } -}); diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs deleted file mode 100644 index 92e8eb80..00000000 --- a/crates/tek/src/tui/app_transport.rs +++ /dev/null @@ -1,263 +0,0 @@ -use crate::*; -use 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: TransportFocus, -} -from_jack!(|jack|TransportTui Self { - jack: jack.clone(), - clock: ClockModel::from(jack), - size: Measure::new(), - cursor: (0, 0), - focus: TransportFocus::PlayPause -}); -has_clock!(|self:TransportTui|&self.clock); -audio!(|self:TransportTui,client,scope|ClockAudio(self).process(client, scope)); -handle!(|self:TransportTui,from|TransportCommand::execute_with_state(self, from)); -render!(|self: TransportTui|TransportView::from((self, None, true))); -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() - } -} -pub struct TransportView { - color: ItemPalette, - 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, Option, bool)> for TransportView { - fn from ((state, color, focused): (&T, Option, 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()); - let color = color.unwrap_or(ItemPalette::from(TuiTheme::g(32))); - 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 { - color, 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 { - color, 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!("000.0.00") - } - } - } -} -render!(|self: TransportView|{ - let color = self.color; - struct Field<'a>(&'a str, &'a str, &'a ItemPalette); - render!(|self: Field<'a>|row!([ - Tui::fg_bg(self.2.lightest.rgb, self.2.base.rgb, Tui::bold(true, self.0)), - Tui::fg_bg(self.2.base.rgb, self.2.darkest.rgb, "▌"), - Tui::fg_bg(self.2.lightest.rgb, self.2.darkest.rgb, format!("{:>10}", self.1)), - Tui::fg_bg(self.2.darkest.rgb, self.2.base.rgb, "▌"), - ])); - Tui::bg(color.base.rgb, Fill::w(row!([ - //PlayPause(self.started), " ", - col!([ - Field(" Beat", self.beat.as_str(), &color), - Field(" BPM", self.bpm.as_str(), &color), - ]), - " ", - col!([ - Field("Time", format!("{:.1}s", self.current_second).as_str(), &color), - Field("Smpl", format!("{:.1}k", self.current_sample).as_str(), &color), - ]), - ]))) -}); -pub struct PlayPause(pub bool); -render!(|self: PlayPause|Tui::bg( - if self.0{Color::Rgb(0,128,0)}else{Color::Rgb(128,64,0)}, - Fixed::w(5, col!(|add|if self.0 { - add(&Tui::fg(Color::Rgb(0, 255, 0), col!([ - " 🭍🭑🬽 ", - " 🭞🭜🭘 ", - ]))) - } else { - add(&Tui::fg(Color::Rgb(255, 128, 0), col!([ - " ▗▄▖ ", - " ▝▀▘ ", - ]))) - })) -)); -impl HasFocus for TransportTui { - type Item = TransportFocus; - fn focused (&self) -> Self::Item { - self.focus - } - fn set_focused (&mut self, to: Self::Item) { - self.focus = to - } -} -/// Which item of the transport toolbar is focused -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum TransportFocus { - Bpm, - Sync, - PlayPause, - Clock, - Quant, -} -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]) - } -} -pub trait TransportControl: HasClock + { - fn transport_focused (&self) -> Option; -} -impl TransportControl for TransportTui { - fn transport_focused (&self) -> Option { - Some(self.focus) - } -} -#[derive(Clone, Debug, PartialEq)] -pub enum TransportCommand { - Focus(FocusCommand), - Clock(ClockCommand), -} -command!(|self:TransportCommand,state:TransportTui|match self { - //Self::Focus(cmd) => cmd.execute(state)?.map(Self::Focus), - Self::Clock(cmd) => cmd.execute(state)?.map(Self::Clock), - _ => unreachable!(), -}); -//command!(|self:TransportFocus,state:TransportTui|{ - //if let FocusCommand::Set(to) = self { state.set_focused(to); } - //Ok(None) -//}); -impl InputToCommand for TransportCommand { - fn input_to_command (state: &TransportTui, 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, - U: Into>, -{ - Some(match input.event() { - key_pat!(Left) => Focus(Prev), - key_pat!(Right) => Focus(Next), - key_pat!(Char(' ')) => Clock(if state.clock().is_stopped() { - Play(None) - } else { - Pause(None) - }), - key_pat!(Shift-Char(' ')) => Clock(if state.clock().is_stopped() { - Play(Some(0)) - } else { - Pause(Some(0)) - }), - _ => match state.transport_focused().unwrap() { - TransportFocus::Bpm => to_bpm_command(input, state.clock().bpm().get())?, - TransportFocus::Quant => to_quant_command(input, &state.clock().quant)?, - TransportFocus::Sync => to_sync_command(input, &state.clock().sync)?, - TransportFocus::Clock => to_seek_command(input)?, - TransportFocus::PlayPause => match input.event() { - key_pat!(Enter) => Clock( - if state.clock().is_stopped() { - Play(None) - } else { - Pause(None) - } - ), - key_pat!(Shift-Enter) => Clock( - if state.clock().is_stopped() { - Play(Some(0)) - } else { - Pause(Some(0)) - } - ), - _ => return None, - }, - } - }) -} -fn to_bpm_command (input: &TuiInput, bpm: f64) -> Option { - Some(match input.event() { - key_pat!(Char(',')) => Clock(SetBpm(bpm - 1.0)), - key_pat!(Char('.')) => Clock(SetBpm(bpm + 1.0)), - key_pat!(Char('<')) => Clock(SetBpm(bpm - 0.001)), - key_pat!(Char('>')) => Clock(SetBpm(bpm + 0.001)), - _ => return None, - }) -} -fn to_quant_command (input: &TuiInput, quant: &Quantize) -> Option { - Some(match input.event() { - key_pat!(Char(',')) => Clock(SetQuant(quant.prev())), - key_pat!(Char('.')) => Clock(SetQuant(quant.next())), - key_pat!(Char('<')) => Clock(SetQuant(quant.prev())), - key_pat!(Char('>')) => Clock(SetQuant(quant.next())), - _ => return None, - }) -} -fn to_sync_command (input: &TuiInput, sync: &LaunchSync) -> Option { - Some(match input.event() { - key_pat!(Char(',')) => Clock(SetSync(sync.prev())), - key_pat!(Char('.')) => Clock(SetSync(sync.next())), - key_pat!(Char('<')) => Clock(SetSync(sync.prev())), - key_pat!(Char('>')) => Clock(SetSync(sync.next())), - _ => return None, - }) -} -fn to_seek_command (input: &TuiInput) -> Option { - Some(match input.event() { - key_pat!(Char(',')) => todo!("transport seek bar"), - key_pat!(Char('.')) => todo!("transport seek bar"), - key_pat!(Char('<')) => todo!("transport seek beat"), - key_pat!(Char('>')) => todo!("transport seek beat"), - _ => return None, - }) -} diff --git a/crates/tek/src/tui/arranger_command.rs b/crates/tek/src/tui/arranger_command.rs deleted file mode 100644 index ddba4ce5..00000000 --- a/crates/tek/src/tui/arranger_command.rs +++ /dev/null @@ -1,254 +0,0 @@ -use crate::*; -use ClockCommand::{Play, Pause}; -use KeyCode::{Char, Delete, Tab, Up, Down, Left, Right}; - -#[derive(Clone, Debug)] pub enum ArrangerCommand { - History(isize), - Color(ItemPalette), - Clock(ClockCommand), - Scene(ArrangerSceneCommand), - Track(ArrangerTrackCommand), - Clip(ArrangerClipCommand), - Select(ArrangerSelection), - Zoom(usize), - Phrases(PhrasesCommand), - Editor(PhraseCommand), - StopAll, - Clear, -} -#[derive(Clone, Debug)] -pub enum ArrangerTrackCommand { - Add, - Delete(usize), - Stop(usize), - Swap(usize, usize), - SetSize(usize), - SetZoom(usize), - SetColor(usize, ItemPalette), -} -#[derive(Clone, Debug)] -pub enum ArrangerSceneCommand { - Enqueue(usize), - Add, - Delete(usize), - Swap(usize, usize), - SetSize(usize), - SetZoom(usize), - SetColor(usize, ItemPalette), -} -#[derive(Clone, Debug)] -pub enum ArrangerClipCommand { - Get(usize, usize), - Put(usize, usize, Option>>), - Enqueue(usize, usize), - Edit(Option>>), - SetLoop(usize, usize, bool), - SetColor(usize, usize, ItemPalette), -} - -input_to_command!(ArrangerCommand: |state: ArrangerTui, input|match input.event() { - key_pat!(Char('u')) => Self::History(-1), - key_pat!(Char('U')) => Self::History(1), - // TODO: k: toggle on-screen keyboard - key_pat!(Ctrl-Char('k')) => { todo!("keyboard") }, - // Transport: Play/pause - key_pat!(Char(' ')) => - Self::Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }), - // Transport: Play from start or rewind to start - key_pat!(Shift-Char(' ')) => - Self::Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }), - key_pat!(Char('e')) => - Self::Editor(PhraseCommand::Show(Some(state.phrases.phrase().clone()))), - key_pat!(Ctrl-Left) => - Self::Scene(ArrangerSceneCommand::Add), - key_pat!(Ctrl-Char('t')) => - Self::Track(ArrangerTrackCommand::Add), - // Tab: Toggle visibility of phrase pool column - key_pat!(Tab) => - Self::Phrases(PhrasesCommand::Show(!state.phrases.visible)), - _ => to_arrangement_command(state, input).or_else(||{ - if let Some(command) = PhraseCommand::input_to_command(&state.editor, input) { - Some(Self::Editor(command)) - } else if let Some(command) = PhrasesCommand::input_to_command(&state.phrases, input) { - Some(Self::Phrases(command)) - } else { - None - } - })? -}); -fn to_arrangement_command (state: &ArrangerTui, input: &TuiInput) -> Option { - use ArrangerCommand as Cmd; - use ArrangerSelection as Selected; - use ArrangerSceneCommand as Scene; - use ArrangerTrackCommand as Track; - use ArrangerClipCommand as Clip; - let t_len = state.tracks.len(); - let s_len = state.scenes.len(); - match state.selected() { - Selected::Clip(t, s) => match input.event() { - key_pat!(Char('g')) => Some(Cmd::Phrases(PhrasesCommand::Select(0))), - key_pat!(Char('q')) => Some(Cmd::Clip(Clip::Enqueue(t, s))), - key_pat!(Char(',')) => Some(Cmd::Clip(Clip::Put(t, s, None))), - key_pat!(Char('.')) => Some(Cmd::Clip(Clip::Put(t, s, None))), - key_pat!(Char('<')) => Some(Cmd::Clip(Clip::Put(t, s, None))), - key_pat!(Char('>')) => Some(Cmd::Clip(Clip::Put(t, s, None))), - key_pat!(Char('p')) => Some(Cmd::Clip(Clip::Put(t, s, Some(state.phrases.phrase().clone())))), - key_pat!(Char('l')) => Some(Cmd::Clip(ArrangerClipCommand::SetLoop(t, s, false))), - key_pat!(Delete) => Some(Cmd::Clip(Clip::Put(t, s, None))), - - key_pat!(Up) => Some(Cmd::Select( - if s > 0 { Selected::Clip(t, s - 1) } else { Selected::Track(t) })), - key_pat!(Down) => Some(Cmd::Select( - Selected::Clip(t, (s + 1).min(s_len.saturating_sub(1))))), - key_pat!(Left) => Some(Cmd::Select( - if t > 0 { Selected::Clip(t - 1, s) } else { Selected::Scene(s) })), - key_pat!(Right) => Some(Cmd::Select( - Selected::Clip((t + 1).min(t_len.saturating_sub(1)), s))), - - _ => None - }, - Selected::Scene(s) => match input.event() { - key_pat!(Char(',')) => Some(Cmd::Scene(Scene::Swap(s, s - 1))), - key_pat!(Char('.')) => Some(Cmd::Scene(Scene::Swap(s, s + 1))), - key_pat!(Char('<')) => Some(Cmd::Scene(Scene::Swap(s, s - 1))), - key_pat!(Char('>')) => Some(Cmd::Scene(Scene::Swap(s, s + 1))), - key_pat!(Char('q')) => Some(Cmd::Scene(Scene::Enqueue(s))), - key_pat!(Delete) => Some(Cmd::Scene(Scene::Delete(s))), - key_pat!(Char('c')) => Some(Cmd::Scene(Scene::SetColor(s, ItemPalette::random()))), - - key_pat!(Up) => Some( - Cmd::Select(if s > 0 { Selected::Scene(s - 1) } else { Selected::Mix })), - key_pat!(Down) => Some( - Cmd::Select(Selected::Scene((s + 1).min(s_len.saturating_sub(1))))), - key_pat!(Left) => - return None, - key_pat!(Right) => Some( - Cmd::Select(Selected::Clip(0, s))), - - _ => None - }, - Selected::Track(t) => match input.event() { - key_pat!(Char(',')) => Some(Cmd::Track(Track::Swap(t, t - 1))), - key_pat!(Char('.')) => Some(Cmd::Track(Track::Swap(t, t + 1))), - key_pat!(Char('<')) => Some(Cmd::Track(Track::Swap(t, t - 1))), - key_pat!(Char('>')) => Some(Cmd::Track(Track::Swap(t, t + 1))), - key_pat!(Delete) => Some(Cmd::Track(Track::Delete(t))), - key_pat!(Char('c')) => Some(Cmd::Track(Track::SetColor(t, ItemPalette::random()))), - - key_pat!(Up) => - return None, - key_pat!(Down) => Some( - Cmd::Select(Selected::Clip(t, 0))), - key_pat!(Left) => Some( - Cmd::Select(if t > 0 { Selected::Track(t - 1) } else { Selected::Mix })), - key_pat!(Right) => Some( - Cmd::Select(Selected::Track((t + 1).min(t_len.saturating_sub(1))))), - - _ => None - }, - Selected::Mix => match input.event() { - key_pat!(Delete) => Some(Cmd::Clear), - key_pat!(Char('0')) => Some(Cmd::StopAll), - key_pat!(Char('c')) => Some(Cmd::Color(ItemPalette::random())), - - key_pat!(Up) => - return None, - key_pat!(Down) => Some( - Cmd::Select(Selected::Scene(0))), - key_pat!(Left) => - return None, - key_pat!(Right) => Some( - Cmd::Select(Selected::Track(0))), - - _ => None - }, - } -} -command!(|self: ArrangerCommand, state: ArrangerTui|match self { - Self::Scene(cmd) => cmd.execute(state)?.map(Self::Scene), - Self::Track(cmd) => cmd.execute(state)?.map(Self::Track), - Self::Clip(cmd) => cmd.execute(state)?.map(Self::Clip), - Self::Editor(cmd) => cmd.execute(&mut state.editor)?.map(Self::Editor), - Self::Clock(cmd) => cmd.execute(state)?.map(Self::Clock), - Self::Zoom(_) => { todo!(); }, - Self::Select(selected) => { - *state.selected_mut() = selected; - None - }, - Self::Color(palette) => { - let old = state.color; - state.color = palette; - Some(Self::Color(old)) - }, - Self::Phrases(cmd) => { - let mut default = |cmd: PhrasesCommand|{ - cmd.execute(&mut state.phrases).map(|x|x.map(Self::Phrases)) - }; - match cmd { - // autoselect: automatically load selected phrase in editor - PhrasesCommand::Select(_) => { - let undo = default(cmd)?; - state.editor.set_phrase(Some(state.phrases.phrase())); - undo - }, - // reload phrase in editor to update color - PhrasesCommand::Phrase(PhrasePoolCommand::SetColor(index, _)) => { - let undo = default(cmd)?; - state.editor.set_phrase(Some(state.phrases.phrase())); - undo - }, - _ => default(cmd)? - } - }, - Self::History(_) => { todo!() }, - Self::StopAll => { - for track in 0..state.tracks.len() { - state.tracks[track].player.enqueue_next(None); - } - None - }, - Self::Clear => { todo!() }, -}); -command!(|self: ArrangerTrackCommand, state: ArrangerTui|match self { - Self::SetColor(index, color) => { - let old = state.tracks[index].color; - state.tracks[index].color = color; - Some(Self::SetColor(index, old)) - }, - Self::Stop(track) => { - state.tracks[track].player.enqueue_next(None); - None - }, - _ => None -}); -command!(|self: ArrangerSceneCommand, state: ArrangerTui|match self { - Self::Delete(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: ArrangerClipCommand, state: ArrangerTui|match self { - Self::Get(track, scene) => { todo!() }, - Self::Put(track, scene, phrase) => { - let old = state.scenes[scene].clips[track].clone(); - state.scenes[scene].clips[track] = phrase; - 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 -}); diff --git a/crates/tek/src/tui/arranger_mode_v.rs b/crates/tek/src/tui/arranger_mode_v.rs deleted file mode 100644 index 9f35c3ee..00000000 --- a/crates/tek/src/tui/arranger_mode_v.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::*; - -mod v_clips; pub(crate) use self::v_clips::*; -mod v_cursor; pub(crate) use self::v_cursor::*; -mod v_head; pub(crate) use self::v_head::*; -mod v_io; pub(crate) use self::v_io::*; -mod v_sep; pub(crate) use self::v_sep::*; - -const HEADER_H: u16 = 5; -const SCENES_W_OFFSET: u16 = 3; - -impl ArrangerTui { - pub fn render_mode_v (state: &ArrangerTui, factor: usize) -> impl Render + use<'_> { - lay!([ - ArrangerVColSep::from(state), - ArrangerVRowSep::from((state, factor)), - col!([ - ArrangerVHead::from(state), - ArrangerVIns::from(state), - ArrangerVClips::from((state, factor)), - ArrangerVOuts::from(state), - ]), - ArrangerVCursor::from((state, factor)), - ]) - } -} - diff --git a/crates/tek/src/tui/arranger_mode_v/v_clips.rs b/crates/tek/src/tui/arranger_mode_v/v_clips.rs deleted file mode 100644 index 41cd38e2..00000000 --- a/crates/tek/src/tui/arranger_mode_v/v_clips.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::*; -use super::*; - -pub struct ArrangerVClips<'a> { - size: &'a Measure, - scenes: &'a Vec, - tracks: &'a Vec, - rows: Vec<(usize, usize)>, -} - -from!(<'a>|args:(&'a ArrangerTui, usize)|ArrangerVClips<'a> = Self { - size: &args.0.size, - scenes: &args.0.scenes, - tracks: &args.0.tracks, - rows: ArrangerScene::ppqs(&args.0.scenes, args.1), -}); - -render!(|self: ArrangerVClips<'a>|Fill::wh( - col!((scene, pulses) in self.scenes.iter().zip(self.rows.iter().map(|row|row.0)) => { - Self::format_scene(&self.tracks, scene, pulses) - }) -)); - -impl<'a> ArrangerVClips<'a> { - fn format_scene ( - tracks: &'a [ArrangerTrack], scene: &'a ArrangerScene, pulses: usize - ) -> impl Render + use<'a> { - let height = 1.max((pulses / PPQ) as u16); - let playing = scene.is_playing(tracks); - Fixed::h(height, row!([ - Tui::bg(scene.color.base.rgb, if playing { "▶ " } else { " " }), - Tui::fg_bg(scene.color.lightest.rgb, scene.color.base.rgb, Tui::grow_x(1, Tui::bold(true, scene.name.read().unwrap().as_str()))), - row!((index, track, x1, x2) in ArrangerTrack::with_widths(tracks) => { - Self::format_clip(scene, index, track, (x2 - x1) as u16, height) - })]) - ) - } - fn format_clip ( - scene: &'a ArrangerScene, index: usize, track: &'a ArrangerTrack, w: u16, h: u16 - ) -> impl Render + use<'a> { - Fixed::wh(w, h, Layers::new(move |add|{ - let mut bg = TuiTheme::border_bg(); - if let Some(Some(phrase)) = scene.clips.get(index) { - 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::bg(bg, - Tui::push_x(1, Fixed::w(w as u16, &name.as_str()[0..max_w]))) - )?; - } - //add(&Background(bg)) - Ok(()) - })) - } -} diff --git a/crates/tek/src/tui/arranger_mode_v/v_cursor.rs b/crates/tek/src/tui/arranger_mode_v/v_cursor.rs deleted file mode 100644 index 0dcd85e2..00000000 --- a/crates/tek/src/tui/arranger_mode_v/v_cursor.rs +++ /dev/null @@ -1,81 +0,0 @@ -use crate::*; -use super::*; - -pub struct ArrangerVCursor { - cols: Vec<(usize, usize)>, - rows: Vec<(usize, usize)>, - color: ItemPalette, - reticle: Reticle, - selected: ArrangerSelection, - scenes_w: u16, -} - -from!(|args:(&ArrangerTui, usize)|ArrangerVCursor = Self { - cols: ArrangerTrack::widths(&args.0.tracks), - rows: ArrangerScene::ppqs(&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 - }), -}); - -render!(|self: ArrangerVCursor|render(move|to: &mut TuiOutput|{ - 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); - } - 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(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)? - }) -})); diff --git a/crates/tek/src/tui/arranger_mode_v/v_head.rs b/crates/tek/src/tui/arranger_mode_v/v_head.rs deleted file mode 100644 index d2e7e812..00000000 --- a/crates/tek/src/tui/arranger_mode_v/v_head.rs +++ /dev/null @@ -1,83 +0,0 @@ -use crate::*; -use super::*; - -pub struct ArrangerVHead<'a> { - scenes_w: u16, - timebase: &'a Arc, - current: &'a Arc, - tracks: &'a [ArrangerTrack], -} - -from!(<'a>|state: &'a ArrangerTui|ArrangerVHead<'a> = Self { // A - tracks: &state.tracks, - timebase: state.clock().timebase(), - current: &state.clock().playhead, - scenes_w: SCENES_W_OFFSET + ArrangerScene::longest_name(&state.scenes) as u16, -}); - -render!(|self: ArrangerVHead<'a>|Tui::push_x(self.scenes_w, row!( - (_, track, x1, x2) in ArrangerTrack::with_widths(self.tracks) => { - let (w, h) = (ArrangerTrack::MIN_WIDTH.max(x2 - x1), HEADER_H); - let color = track.color(); - fn row > (color: ItemPalette, field: &T) -> impl Render + use<'_, T> { - row!([ - Tui::fg(color.light.rgb, "▎"), - Tui::fg(color.lightest.rgb, field) - ]) - } - Tui::bg(color.base.rgb, Tui::min_xy(w as u16, h, Fixed::wh(w as u16, 5, col!([ - row(color, &Self::format_name(track, w)), - row(color, &Self::format_input(track)?), - row(color, &Self::format_output(track)?), - row(color, &Self::format_elapsed(track, &self.timebase)), - row(color, &Self::format_until_next(track, &self.current)), - ])))) - } -))); - -impl<'a> ArrangerVHead<'a> { - /// name and width of track - fn format_name (track: &ArrangerTrack, w: usize) -> impl Render { - let name = track.name().read().unwrap().clone(); - Tui::bold(true, Tui::fg(track.color.lightest.rgb, name)) - } - /// input port - fn format_input (track: &ArrangerTrack) -> Usually> { - Ok(format!(">{}", track.player.midi_ins().get(0).map(|port|port.short_name()) - .transpose()?.unwrap_or("?".into()))) - } - /// output port - fn format_output (track: &ArrangerTrack) -> Usually> { - Ok(format!("<{}", track.player.midi_outs().get(0).map(|port|port.short_name()) - .transpose()?.unwrap_or("?".into()))) - } - /// beats elapsed - fn format_elapsed (track: &ArrangerTrack, timebase: &Arc) -> impl Render { - let mut result = String::new(); - 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 = timebase.format_beats_1_short( - (elapsed as usize % length) as f64 - ); - result = format!("+{elapsed:>}") - } - result - } - /// beats until switchover - fn format_until_next (track: &ArrangerTrack, current: &Arc) - -> Option> - { - let timebase = ¤t.timebase; - let mut result = String::new(); - if let Some((t, _)) = track.player.next_phrase().as_ref() { - let target = t.pulse.get(); - let current = current.pulse.get(); - if target > current { - let remaining = target - current; - result = format!("-{:>}", timebase.format_beats_0_short(remaining)) - } - } - Some(result) - } -} diff --git a/crates/tek/src/tui/arranger_mode_v/v_io.rs b/crates/tek/src/tui/arranger_mode_v/v_io.rs deleted file mode 100644 index 1577f97b..00000000 --- a/crates/tek/src/tui/arranger_mode_v/v_io.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::*; -use super::*; - -pub struct ArrangerVIns<'a> { - size: &'a Measure, - tracks: &'a Vec, -} - -from!(<'a>|args: &'a ArrangerTui|ArrangerVIns<'a> = Self { - size: &args.size, - tracks: &args.tracks, -}); - -render!(|self: ArrangerVIns<'a>|()); - -pub struct ArrangerVOuts<'a> { - size: &'a Measure, - tracks: &'a Vec, -} - -from!(<'a>|args: &'a ArrangerTui|ArrangerVOuts<'a> = Self { - size: &args.size, - tracks: &args.tracks, -}); - -render!(|self: ArrangerVOuts<'a>|()); diff --git a/crates/tek/src/tui/arranger_mode_v/v_sep.rs b/crates/tek/src/tui/arranger_mode_v/v_sep.rs deleted file mode 100644 index f895e27e..00000000 --- a/crates/tek/src/tui/arranger_mode_v/v_sep.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::*; -use super::*; - -pub struct ArrangerVColSep { - fg: Color, - cols: Vec<(usize, usize)>, - scenes_w: u16 -} - -from!(|state:&ArrangerTui|ArrangerVColSep = Self { - fg: TuiTheme::separator_fg(false), - cols: ArrangerTrack::widths(&state.tracks), - scenes_w: SCENES_W_OFFSET + ArrangerScene::longest_name(&state.scenes) as u16, -}); - -render!(|self: ArrangerVColSep|render(move|to: &mut TuiOutput|{ - let style = Some(Style::default().fg(self.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); - } - }) -})); - -pub struct ArrangerVRowSep { - fg: Color, - rows: Vec<(usize, usize)>, -} - -from!(|args:(&ArrangerTui, usize)|ArrangerVRowSep = Self { - fg: TuiTheme::separator_fg(false), - rows: ArrangerScene::ppqs(&args.0.scenes, args.1), -}); - -render!(|self: ArrangerVRowSep|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.fg; - } - } - }) -})); diff --git a/crates/tek/src/tui/arranger_scene.rs b/crates/tek/src/tui/arranger_scene.rs deleted file mode 100644 index ddbf3114..00000000 --- a/crates/tek/src/tui/arranger_scene.rs +++ /dev/null @@ -1,94 +0,0 @@ -use crate::*; - -impl ArrangerTui { - pub 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(||ItemPalette::random()), - }; - self.scenes.push(scene); - let index = self.scenes.len() - 1; - Ok(&mut self.scenes[index]) - } - pub fn scene_del (&mut self, index: usize) { - todo!("delete scene"); - } - fn scene_default_name (&self) -> String { - format!("S{:3>}", self.scenes.len() + 1) - } - pub fn selected_scene (&self) -> Option<&ArrangerScene> { - self.selected.scene().map(|s|self.scenes.get(s)).flatten() - } - pub fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> { - self.selected.scene().map(|s|self.scenes.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: ItemPalette, -} - -impl ArrangerScene { - pub fn name (&self) -> &Arc> { - &self.name - } - pub fn clips (&self) -> &Vec>>> { - &self.clips - } - pub fn color (&self) -> ItemPalette { - self.color - } - pub 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() - } - } - pub 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 - pub 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. - pub fn is_playing (&self, tracks: &[ArrangerTrack]) -> 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 - }) - } - pub fn clip (&self, index: usize) -> Option<&Arc>> { - match self.clips().get(index) { Some(Some(clip)) => Some(clip), _ => None } - } -} diff --git a/crates/tek/src/tui/arranger_select.rs b/crates/tek/src/tui/arranger_select.rs deleted file mode 100644 index 0718e7e9..00000000 --- a/crates/tek/src/tui/arranger_select.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::*; - -#[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 - } - } -} diff --git a/crates/tek/src/tui/arranger_track.rs b/crates/tek/src/tui/arranger_track.rs deleted file mode 100644 index 20c2f69f..00000000 --- a/crates/tek/src/tui/arranger_track.rs +++ /dev/null @@ -1,118 +0,0 @@ -use crate::*; -use KeyCode::{Char, Delete}; - -impl ArrangerTui { - pub fn track_next_name (&self) -> String { - format!("T{}", self.tracks.len() + 1) - } - pub fn track_add (&mut self, name: Option<&str>, color: Option) - -> Usually<&mut ArrangerTrack> - { - let name = name.map_or_else(||self.track_next_name(), |x|x.to_string()); - let track = ArrangerTrack { - width: name.len() + 2, - name: Arc::new(name.into()), - color: color.unwrap_or_else(||ItemPalette::random()), - player: PhrasePlayerModel::from(&self.clock), - }; - self.tracks.push(track); - let index = self.tracks.len() - 1; - Ok(&mut self.tracks[index]) - } - pub fn track_del (&mut self, index: usize) { - self.tracks.remove(index); - for scene in self.scenes.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: ItemPalette, - /// MIDI player state - pub(crate) player: PhrasePlayerModel, -} - -has_clock!(|self:ArrangerTrack|self.player.clock()); -has_player!(|self:ArrangerTrack|self.player); - -impl ArrangerTrack { - pub fn widths (tracks: &[Self]) -> 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 - } - pub fn with_widths (tracks: &[ArrangerTrack]) - -> impl Iterator - { - let mut x = 0; - tracks.iter().enumerate().map(move |(index, track)|{ - let data = (index, track, x, x + track.width); - x += track.width; - data - }) - } - /// Name of track - pub 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 - pub fn color (&self) -> ItemPalette { - self.color - } - fn longest_name (tracks: &[Self]) -> usize { - tracks.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max) - } - pub const MIN_WIDTH: usize = 6; - 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>( - // Track collection - pub &'a mut [ArrangerTrack], - /// Note buffer - pub &'a mut Vec, - /// Note chunk buffer - pub &'a mut Vec>>, -); - -impl<'a> Audio for TracksAudio<'a> { - #[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.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/tui/file_browser.rs b/crates/tek/src/tui/file_browser.rs deleted file mode 100644 index 9911058d..00000000 --- a/crates/tek/src/tui/file_browser.rs +++ /dev/null @@ -1,89 +0,0 @@ -use crate::*; -/// Browses for phrase to import/export -#[derive(Debug, Clone)] -pub struct FileBrowser { - pub cwd: PathBuf, - pub dirs: Vec<(OsString, String)>, - pub files: Vec<(OsString, String)>, - pub filter: String, - pub index: usize, - pub scroll: usize, - pub size: Measure -} -/// Commands supported by [FileBrowser] -#[derive(Debug, Clone, PartialEq)] -pub enum FileBrowserCommand { - Begin, - Cancel, - Confirm, - Select(usize), - Chdir(PathBuf), - Filter(String), -} -render!(|self: FileBrowser|{ - Stack::down(|add|{ - let mut i = 0; - for (_, name) in self.dirs.iter() { - if i >= self.scroll { - add(&Tui::bold(i == self.index, name.as_str()))?; - } - i += 1; - } - for (_, name) in self.files.iter() { - if i >= self.scroll { - add(&Tui::bold(i == self.index, name.as_str()))?; - } - i += 1; - } - add(&format!("{}/{i}", self.index))?; - Ok(()) - }) -}); -impl FileBrowser { - pub fn new (cwd: Option) -> Usually { - let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? }; - let mut dirs = vec![]; - let mut files = vec![]; - for entry in std::fs::read_dir(&cwd)? { - let entry = entry?; - let name = entry.file_name(); - let decoded = name.clone().into_string().unwrap_or_else(|_|"".to_string()); - let meta = entry.metadata()?; - if meta.is_dir() { - dirs.push((name, format!("📁 {decoded}"))); - } else if meta.is_file() { - files.push((name, format!("📄 {decoded}"))); - } - } - Ok(Self { - cwd, - dirs, - files, - filter: "".to_string(), - index: 0, - scroll: 0, - size: Measure::new(), - }) - } - pub fn len (&self) -> usize { - self.dirs.len() + self.files.len() - } - pub fn is_dir (&self) -> bool { - self.index < self.dirs.len() - } - pub fn is_file (&self) -> bool { - self.index >= self.dirs.len() - } - pub fn path (&self) -> PathBuf { - self.cwd.join(if self.is_dir() { - &self.dirs[self.index].0 - } else if self.is_file() { - &self.files[self.index - self.dirs.len()].0 - } else { - unreachable!() - }) - } - pub fn chdir (&self) -> Usually { - Self::new(Some(self.path())) - } -} diff --git a/crates/tek/src/tui/phrase_editor.rs b/crates/tek/src/tui/phrase_editor.rs deleted file mode 100644 index 2667236d..00000000 --- a/crates/tek/src/tui/phrase_editor.rs +++ /dev/null @@ -1,229 +0,0 @@ -use crate::*; -use KeyCode::{Char, Up, Down, Left, Right, Enter}; -use PhraseCommand::*; - -pub trait HasEditor { - fn editor (&self) -> &PhraseEditorModel; -} - -#[macro_export] macro_rules! has_editor { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasEditor for $Struct $(<$($L),*$($T),*>)? { - fn editor (&$self) -> &PhraseEditorModel { &$cb } - } - } -} - -#[derive(Clone, Debug)] -pub enum PhraseCommand { - // TODO: 1-9 seek markers that by default start every 8th of the phrase - AppendNote, - PutNote, - SetNoteCursor(usize), - SetNoteLength(usize), - SetNoteScroll(usize), - SetTimeCursor(usize), - SetTimeScroll(usize), - SetTimeZoom(usize), - SetTimeLock(bool), - Show(Option>>), - ToggleDirection, -} - -impl InputToCommand for PhraseCommand { - fn input_to_command (state: &PhraseEditorModel, from: &TuiInput) -> Option { - return state.to_editor_command(from) - } -} - -impl PhraseEditorModel { - fn phrase_length (&self) -> usize { - self.phrase().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1) - } - const KEYS: [(TuiEvent, &'static dyn Fn(&Self)->PhraseCommand);31] = [ - (kexp!(Ctrl-Alt-Up), &|s: &Self|SetNoteScroll(s.note_point() + 3)), - (kexp!(Ctrl-Alt-Down), &|s: &Self|SetNoteScroll(s.note_point().saturating_sub(3))), - (kexp!(Ctrl-Alt-Left), &|s: &Self|SetTimeScroll(s.time_point().saturating_sub(s.time_zoom().get()))), - (kexp!(Ctrl-Alt-Right), &|s: &Self|SetTimeScroll((s.time_point() + s.time_zoom().get()) % s.phrase_length())), - (kexp!(Ctrl-Up), &|s: &Self|SetNoteScroll(s.note_lo().get() + 1)), - (kexp!(Ctrl-Down), &|s: &Self|SetNoteScroll(s.note_lo().get().saturating_sub(1))), - (kexp!(Ctrl-Left), &|s: &Self|SetTimeScroll(s.time_start().get().saturating_sub(s.note_len()))), - (kexp!(Ctrl-Right), &|s: &Self|SetTimeScroll(s.time_start().get() + s.note_len())), - (kexp!(Alt-Up), &|s: &Self|SetNoteCursor(s.note_point() + 3)), - (kexp!(Alt-Down), &|s: &Self|SetNoteCursor(s.note_point().saturating_sub(3))), - (kexp!(Alt-Left), &|s: &Self|SetTimeCursor(s.time_point().saturating_sub(s.time_zoom().get()))), - (kexp!(Alt-Right), &|s: &Self|SetTimeCursor((s.time_point() + s.time_zoom().get()) % s.phrase_length())), - (kexp!(Up), &|s: &Self|SetNoteCursor(s.note_point() + 1)), - (kexp!(Char('w')), &|s: &Self|SetNoteCursor(s.note_point() + 1)), - (kexp!(Down), &|s: &Self|SetNoteCursor(s.note_point().saturating_sub(1))), - (kexp!(Char('s')), &|s: &Self|SetNoteCursor(s.note_point().saturating_sub(1))), - (kexp!(Left), &|s: &Self|SetTimeCursor(s.time_point().saturating_sub(s.note_len()))), - (kexp!(Char('a')), &|s: &Self|SetTimeCursor(s.time_point().saturating_sub(s.note_len()))), - (kexp!(Right), &|s: &Self|SetTimeCursor((s.time_point() + s.note_len()) % s.phrase_length())), - (kexp!(Char('d')), &|s: &Self|SetTimeCursor((s.time_point() + s.note_len()) % s.phrase_length())), - (kexp!(Char('z')), &|s: &Self|SetTimeLock(!s.time_lock().get())), - (kexp!(Char('-')), &|s: &Self|SetTimeZoom(Note::next(s.time_zoom().get()))), - (kexp!(Char('_')), &|s: &Self|SetTimeZoom(Note::next(s.time_zoom().get()))), - (kexp!(Char('=')), &|s: &Self|SetTimeZoom(Note::prev(s.time_zoom().get()))), - (kexp!(Char('+')), &|s: &Self|SetTimeZoom(Note::prev(s.time_zoom().get()))), - (kexp!(Enter), &|s: &Self|PutNote), - (kexp!(Ctrl-Enter), &|s: &Self|AppendNote), - (kexp!(Char(',')), &|s: &Self|SetNoteLength(Note::prev(s.note_len()))), // TODO: no 3plet - (kexp!(Char('.')), &|s: &Self|SetNoteLength(Note::next(s.note_len()))), - (kexp!(Char('<')), &|s: &Self|SetNoteLength(Note::prev(s.note_len()))), // TODO: 3plet - (kexp!(Char('>')), &|s: &Self|SetNoteLength(Note::next(s.note_len()))), - //key_pat!(Char('`')) -> ToggleDirection, - //// TODO: key_pat!(Char('/')) => // toggle 3plet - //// TODO: key_pat!(Char('?')) => // toggle dotted - ]; - fn to_editor_command (&self, from: &TuiInput) -> Option { - event_map!(Self::KEYS).handle(self, from.event()) - } -} - -impl Command for PhraseCommand { - fn execute (self, state: &mut PhraseEditorModel) -> Perhaps { - use PhraseCommand::*; - match self { - Show(phrase) => { state.set_phrase(phrase.as_ref()); }, - PutNote => { state.put_note(false); }, - AppendNote => { state.put_note(true); }, - SetTimeZoom(x) => { state.time_zoom().set(x); state.redraw(); }, - SetTimeLock(x) => { state.time_lock().set(x); }, - SetTimeScroll(x) => { state.time_start().set(x); }, - SetNoteScroll(x) => { state.note_lo().set(x.min(127)); }, - SetNoteLength(x) => { state.set_note_len(x); }, - SetTimeCursor(x) => { state.set_time_point(x); }, - SetNoteCursor(note) => { state.set_note_point(note.min(127)); }, - _ => todo!("{:?}", self) - } - Ok(None) - } -} - -/// Contains state for viewing and editing a phrase -pub struct PhraseEditorModel { - /// Renders the phrase - pub mode: Box, - pub size: Measure -} - -impl Default for PhraseEditorModel { - fn default () -> Self { - let mut mode = Box::new(PianoHorizontal::new(None)); - mode.redraw(); - Self { mode, size: Measure::new() } - } -} - -has_size!(|self:PhraseEditorModel|&self.size); -render!(|self: PhraseEditorModel|{ - self.autoscroll(); - self.autozoom(); - &self.mode -}); -//render!(|self: PhraseEditorModel|lay!(|add|{add(&self.size)?;add(self.mode)}));//bollocks - -pub trait PhraseViewMode: Render + HasSize + MidiRange + MidiPoint + Debug + Send + Sync { - fn buffer_size (&self, phrase: &Phrase) -> (usize, usize); - fn redraw (&mut self); - fn phrase (&self) -> &Option>>; - fn phrase_mut (&mut self) -> &mut Option>>; - fn set_phrase (&mut self, phrase: Option<&Arc>>) { - *self.phrase_mut() = phrase.map(|p|p.clone()); - self.redraw(); - } -} - -impl MidiView for PhraseEditorModel {} - -impl MidiRange for PhraseEditorModel { - fn time_len (&self) -> &AtomicUsize { self.mode.time_len() } - fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() } - fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() } - fn time_start (&self) -> &AtomicUsize { self.mode.time_start() } - fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() } - fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() } - fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() } -} - -impl MidiPoint for PhraseEditorModel { - fn note_len (&self) -> usize { self.mode.note_len()} - fn set_note_len (&self, x: usize) { self.mode.set_note_len(x) } - fn note_point (&self) -> usize { self.mode.note_point() } - fn set_note_point (&self, x: usize) { self.mode.set_note_point(x) } - fn time_point (&self) -> usize { self.mode.time_point() } - fn set_time_point (&self, x: usize) { self.mode.set_time_point(x) } -} - -impl PhraseViewMode for PhraseEditorModel { - fn buffer_size (&self, phrase: &Phrase) -> (usize, usize) { - self.mode.buffer_size(phrase) - } - fn redraw (&mut self) { - self.mode.redraw() - } - fn phrase (&self) -> &Option>> { - self.mode.phrase() - } - fn phrase_mut (&mut self) -> &mut Option>> { - self.mode.phrase_mut() - } - fn set_phrase (&mut self, phrase: Option<&Arc>>) { - self.mode.set_phrase(phrase) - } -} - -impl PhraseEditorModel { - /// Put note at current position - pub fn put_note (&mut self, advance: bool) { - let mut redraw = false; - if let Some(phrase) = self.phrase() { - let mut phrase = phrase.write().unwrap(); - let note_start = self.time_point(); - let note_point = self.note_point(); - let note_len = self.note_len(); - let note_end = note_start + (note_len.saturating_sub(1)); - let key: u7 = u7::from(note_point as u8); - let vel: u7 = 100.into(); - let length = phrase.length; - let note_end = note_end % length; - let note_on = MidiMessage::NoteOn { key, vel }; - if !phrase.notes[note_start].iter().any(|msg|*msg == note_on) { - phrase.notes[note_start].push(note_on); - } - let note_off = MidiMessage::NoteOff { key, vel }; - if !phrase.notes[note_end].iter().any(|msg|*msg == note_off) { - phrase.notes[note_end].push(note_off); - } - if advance { - self.set_time_point(note_end); - } - redraw = true; - } - if redraw { - self.mode.redraw(); - } - } -} - -from!(|phrase: &Arc>|PhraseEditorModel = { - let mut model = Self::from(Some(phrase.clone())); - model.redraw(); - model -}); - -from!(|phrase: Option>>|PhraseEditorModel = { - let mut model = Self::default(); - *model.phrase_mut() = phrase; - model.redraw(); - model -}); - -impl std::fmt::Debug for PhraseEditorModel { - fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("PhraseEditorModel") - .field("mode", &self.mode) - .finish() - } -} diff --git a/crates/tek/src/tui/phrase_length.rs b/crates/tek/src/tui/phrase_length.rs deleted file mode 100644 index 08ced486..00000000 --- a/crates/tek/src/tui/phrase_length.rs +++ /dev/null @@ -1,135 +0,0 @@ -use crate::*; -use super::pool::{PoolModel, PoolMode}; -use PhraseLengthFocus::*; -use PhraseLengthCommand::*; -/// Displays and edits phrase length. -#[derive(Clone)] -pub struct PhraseLength { - /// Pulses per beat (quaver) - pub ppq: usize, - /// Beats per bar - pub bpb: usize, - /// Length of phrase in pulses - pub pulses: usize, - /// Selected subdivision - pub focus: Option, -} -impl PhraseLength { - pub fn new (pulses: usize, focus: Option) -> Self { - Self { ppq: PPQ, bpb: 4, pulses, focus } - } - pub fn bars (&self) -> usize { - self.pulses / (self.bpb * self.ppq) - } - pub fn beats (&self) -> usize { - (self.pulses % (self.bpb * self.ppq)) / self.ppq - } - pub fn ticks (&self) -> usize { - self.pulses % self.ppq - } - pub fn bars_string (&self) -> String { - format!("{}", self.bars()) - } - pub fn beats_string (&self) -> String { - format!("{}", self.beats()) - } - pub fn ticks_string (&self) -> String { - format!("{:>02}", self.ticks()) - } -} -/// Focused field of `PhraseLength` -#[derive(Copy, Clone, Debug)] -pub enum PhraseLengthFocus { - /// Editing the number of bars - Bar, - /// Editing the number of beats - Beat, - /// Editing the number of ticks - Tick, -} -impl PhraseLengthFocus { - pub fn next (&mut self) { - *self = match self { - Self::Bar => Self::Beat, - Self::Beat => Self::Tick, - Self::Tick => Self::Bar, - } - } - pub fn prev (&mut self) { - *self = match self { - Self::Bar => Self::Tick, - Self::Beat => Self::Bar, - Self::Tick => Self::Beat, - } - } -} -render!(|self: PhraseLength|{ - let bars = ||self.bars_string(); - let beats = ||self.beats_string(); - let ticks = ||self.ticks_string(); - row!(move|add|match self.focus { - None => - add(&row!([" ", bars(), ".", beats(), ".", ticks()])), - Some(PhraseLengthFocus::Bar) => - add(&row!(["[", bars(), "]", beats(), ".", ticks()])), - Some(PhraseLengthFocus::Beat) => - add(&row!([" ", bars(), "[", beats(), "]", ticks()])), - Some(PhraseLengthFocus::Tick) => - add(&row!([" ", bars(), ".", beats(), "[", ticks()])), - }) -}); -#[derive(Copy, Clone, Debug, PartialEq)] -pub enum PhraseLengthCommand { - Begin, - Cancel, - Set(usize), - Next, - Prev, - Inc, - Dec, -} -command!(|self:PhraseLengthCommand,state:PoolModel|{ - match state.phrases_mode_mut().clone() { - Some(PoolMode::Length(phrase, ref mut length, ref mut focus)) => match self { - Cancel => { *state.phrases_mode_mut() = None; }, - Prev => { focus.prev() }, - Next => { focus.next() }, - Inc => match focus { - Bar => { *length += 4 * PPQ }, - Beat => { *length += PPQ }, - Tick => { *length += 1 }, - }, - Dec => match focus { - Bar => { *length = length.saturating_sub(4 * PPQ) }, - Beat => { *length = length.saturating_sub(PPQ) }, - Tick => { *length = length.saturating_sub(1) }, - }, - Set(length) => { - let mut phrase = state.phrases()[phrase].write().unwrap(); - let old_length = phrase.length; - phrase.length = length; - std::mem::drop(phrase); - *state.phrases_mode_mut() = None; - return Ok(Some(Self::Set(old_length))) - }, - _ => unreachable!() - }, - _ => unreachable!() - }; - None -}); -input_to_command!(PhraseLengthCommand:|state:PoolModel,from|{ - if let Some(PoolMode::Length(_, length, _)) = state.phrases_mode() { - match from.event() { - key_pat!(Up) => Self::Inc, - key_pat!(Down) => Self::Dec, - key_pat!(Right) => Self::Next, - key_pat!(Left) => Self::Prev, - key_pat!(Enter) => Self::Set(*length), - key_pat!(Esc) => Self::Cancel, - _ => return None - } - } else { - unreachable!() - } -}); diff --git a/crates/tek/src/tui/phrase_rename.rs b/crates/tek/src/tui/phrase_rename.rs deleted file mode 100644 index 1b792f5b..00000000 --- a/crates/tek/src/tui/phrase_rename.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::*; - -#[derive(Clone, Debug, PartialEq)] -pub enum PhraseRenameCommand { - Begin, - Cancel, - Confirm, - Set(String), -} - -impl Command for PhraseRenameCommand { - fn execute (self, state: &mut PoolModel) -> Perhaps { - use PhraseRenameCommand::*; - match state.phrases_mode_mut().clone() { - Some(PoolMode::Rename(phrase, ref mut old_name)) => match self { - Set(s) => { - state.phrases()[phrase].write().unwrap().name = s.into(); - return Ok(Some(Self::Set(old_name.clone()))) - }, - Confirm => { - let old_name = old_name.clone(); - *state.phrases_mode_mut() = None; - return Ok(Some(Self::Set(old_name))) - }, - Cancel => { - state.phrases()[phrase].write().unwrap().name = old_name.clone(); - }, - _ => unreachable!() - }, - _ => unreachable!() - }; - Ok(None) - } -} - -impl InputToCommand for PhraseRenameCommand { - fn input_to_command (state: &PoolModel, from: &TuiInput) -> Option { - use KeyCode::{Char, Backspace, Enter, Esc}; - if let Some(PoolMode::Rename(_, ref old_name)) = state.phrases_mode() { - Some(match from.event() { - key_pat!(Char(c)) => { - let mut new_name = old_name.clone(); - new_name.push(*c); - Self::Set(new_name) - }, - key_pat!(Backspace) => { - let mut new_name = old_name.clone(); - new_name.pop(); - Self::Set(new_name) - }, - key_pat!(Enter) => Self::Confirm, - key_pat!(Esc) => Self::Cancel, - _ => return None - }) - } else { - unreachable!() - } - } -} - diff --git a/crates/tek/src/tui/piano_horizontal.rs b/crates/tek/src/tui/piano_horizontal.rs deleted file mode 100644 index 045551b3..00000000 --- a/crates/tek/src/tui/piano_horizontal.rs +++ /dev/null @@ -1,338 +0,0 @@ -use crate::*; -use super::*; - -fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) -> impl Iterator { - (note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n)) -} - -fn to_key (note: usize) -> &'static str { - match note % 12 { - 11 => "████▌", - 10 => " ", - 9 => "████▌", - 8 => " ", - 7 => "████▌", - 6 => " ", - 5 => "████▌", - 4 => "████▌", - 3 => " ", - 2 => "████▌", - 1 => " ", - 0 => "████▌", - _ => unreachable!(), - } -} - -/// A phrase, rendered as a horizontal piano roll. -pub struct PianoHorizontal { - phrase: Option>>, - /// Buffer where the whole phrase is rerendered on change - buffer: BigBuffer, - /// Size of actual notes area - size: Measure, - /// The display window - range: MidiRangeModel, - /// The note cursor - point: MidiPointModel, - /// The highlight color palette - color: ItemPalette, -} - -impl PianoHorizontal { - pub fn new (phrase: Option<&Arc>>) -> Self { - let size = Measure::new(); - let mut range = MidiRangeModel::from((24, true)); - range.time_axis = size.x.clone(); - range.note_axis = size.y.clone(); - let phrase = phrase.map(|p|p.clone()); - let color = phrase.as_ref() - .map(|p|p.read().unwrap().color) - .unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64)))); - Self { - buffer: Default::default(), - point: MidiPointModel::default(), - size, - range, - phrase, - color - } - } -} - -render!(|self: PianoHorizontal|{ - let keys = move||PianoHorizontalKeys(&self); - let timeline = move||PianoHorizontalTimeline(&self); - let notes = move||PianoHorizontalNotes(&self); - let cursor = move||PianoHorizontalCursor(&self); - let keys_width = 5; - let border = Fill::wh(Outer(Style::default().fg(self.color.dark.rgb).bg(self.color.darkest.rgb))); - let with_border = |x|lay!([border, Tui::inset_xy(1, 1, &x)]); - with_border(Fill::wh(Bsp::s( - Fixed::h(1, Bsp::e(Fixed::w(keys_width, ""), Fill::w(timeline()),)), - Bsp::e( - Fixed::w(keys_width, keys()), - Fill::wh(lay!([&self.size, Fill::wh(lay!([Fill::wh(notes()), Fill::wh(cursor()),]))])), - ), - ))) -}); - -pub struct PianoHorizontalTimeline<'a>(&'a PianoHorizontal); -render!(|self: PianoHorizontalTimeline<'a>|render(|to|{ - let [x, y, w, h] = to.area(); - let style = Some(Style::default().dim()); - let length = self.0.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); - for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) { - let t = area_x as usize * self.0.time_zoom().get(); - if t < length { - to.blit(&"|", screen_x, y, style); - } - } - Ok(()) -})); - //Tui::fg_bg( - //self.0.color.lightest.rgb, - //self.0.color.darkest.rgb, - //format!("{}*{}", self.0.time_start(), self.0.time_zoom()).as_str() -//)); - -pub struct PianoHorizontalKeys<'a>(&'a PianoHorizontal); -render!(|self: PianoHorizontalKeys<'a>|render(|to|Ok({ - let color = self.0.color; - let note_lo = self.0.note_lo().get(); - let note_hi = self.0.note_hi(); - let note_point = self.0.note_point(); - let [x, y0, w, h] = to.area().xywh(); - let key_style = Some(Style::default().fg(Color::Rgb(192, 192, 192)).bg(Color::Rgb(0, 0, 0))); - let off_style = Some(Style::default().fg(TuiTheme::g(160))); - let on_style = Some(Style::default().fg(TuiTheme::g(255)).bg(color.light.rgb).bold()); - for (area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { - to.blit(&to_key(note), x, screen_y, key_style); - if note > 127 { - continue - } - if note == note_point { - to.blit(&format!("{:<5}", to_note_name(note)), x, screen_y, on_style) - } else { - to.blit(&to_note_name(note), x, screen_y, off_style) - }; - } - //let debug = false; - //if debug { - //to.blit(&format!("XYU"), x, y0, None); - //to.blit(&format!("x={x}"), x, y0+1, None); - //to.blit(&format!("y0={y0}"), x, y0+2, None); - //to.blit(&format!("w={w}"), x, y0+3, None); - //to.blit(&format!("h={h}"), x, y0+4, None); - //to.blit(&format!("note_lo={note_lo}"), x, y0+5, None); - //to.blit(&format!("note_hi={note_hi}"), x, y0+6, None); - //} -}))); - -pub struct PianoHorizontalCursor<'a>(&'a PianoHorizontal); -render!(|self: PianoHorizontalCursor<'a>|render(|to|Ok({ - let style = Some(Style::default().fg(self.0.color.lightest.rgb)); - let note_hi = self.0.note_hi(); - let note_len = self.0.note_len(); - let note_lo = self.0.note_lo().get(); - let note_point = self.0.note_point(); - let time_point = self.0.time_point(); - let time_start = self.0.time_start().get(); - let time_zoom = self.0.time_zoom().get(); - let [x0, y0, w, _] = to.area().xywh(); - for (area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { - if note == note_point { - for x in 0..w { - let screen_x = x0 + x as u16; - let time_1 = time_start + x as usize * time_zoom; - let time_2 = time_1 + time_zoom; - if time_1 <= time_point && time_point < time_2 { - to.blit(&"█", screen_x, screen_y, style); - let tail = note_len as u16 / time_zoom as u16; - for x_tail in (screen_x + 1)..(screen_x + tail) { - to.blit(&"▂", x_tail, screen_y, style); - } - break - } - } - break - } - } -}))); - -pub struct PianoHorizontalNotes<'a>(&'a PianoHorizontal); -render!(|self: PianoHorizontalNotes<'a>|render(|to|Ok({ - let time_start = self.0.time_start().get(); - let note_lo = self.0.note_lo().get(); - let note_hi = self.0.note_hi(); - let note_point = self.0.note_point(); - let source = &self.0.buffer; - let [x0, y0, w, h] = to.area().xywh(); - if h as usize != self.0.note_axis().get() { - panic!("area height mismatch"); - } - for (area_x, screen_x) in (x0..x0+w).enumerate() { - for (area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) { - //if area_x % 10 == 0 { - //to.blit(&format!("{area_y} {note}"), screen_x, screen_y, None); - //} - let source_x = time_start + area_x; - let source_y = note_hi - area_y; - ////// TODO: enable loop rollover: - //////let source_x = (time_start + area_x) % source.width.max(1); - //////let source_y = (note_hi - area_y) % source.height.max(1); - let is_in_x = source_x < source.width; - let is_in_y = source_y < source.height; - if is_in_x && is_in_y { - if let Some(source_cell) = source.get(source_x, source_y) { - *to.buffer.get_mut(screen_x, screen_y) = source_cell.clone(); - } - } - } - } - //let debug = true; - //if debug { - //let x0=20+x0; - //to.blit(&format!("KYP "), x0, y0, None); - //to.blit(&format!("x0={x0} "), x0, y0+1, None); - //to.blit(&format!("y0={y0} "), x0, y0+2, None); - //to.blit(&format!("note_lo={note_lo}, {} ", to_note_name(note_lo)), x0, y0+3, None); - //to.blit(&format!("note_hi={note_hi}, {} ", to_note_name(note_hi)), x0, y0+4, None); - //to.blit(&format!("note_point={note_point}, {} ", to_note_name(note_point)), x0, y0+5, None); - //to.blit(&format!("time_start={time_start} "), x0, y0+6, None); - //return Ok(()); - //} -}))); - -impl PianoHorizontal { - /// Draw the piano roll foreground using full blocks on note on and half blocks on legato: █▄ █▄ █▄ - fn draw_bg (buf: &mut BigBuffer, phrase: &Phrase, zoom: usize, note_len: usize) { - for (y, note) in (0..127).rev().enumerate() { - for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) { - let cell = buf.get_mut(x, y).unwrap(); - cell.set_bg(phrase.color.darkest.rgb); - cell.set_fg(phrase.color.darker.rgb); - cell.set_char(if time % 384 == 0 { - '│' - } else if time % 96 == 0 { - '╎' - } else if time % note_len == 0 { - '┊' - } else if (127 - note) % 12 == 1 { - '=' - } else { - '·' - }); - } - } - } - /// Draw the piano roll background using full blocks on note on and half blocks on legato: █▄ █▄ █▄ - fn draw_fg (buf: &mut BigBuffer, phrase: &Phrase, zoom: usize) { - let style = Style::default().fg(phrase.color.base.rgb);//.bg(Color::Rgb(0, 0, 0)); - let mut notes_on = [false;128]; - for (x, time_start) in (0..phrase.length).step_by(zoom).enumerate() { - - for (y, note) in (0..127).rev().enumerate() { - if let Some(cell) = buf.get_mut(x, note) { - if notes_on[note] { - cell.set_char('▂'); - cell.set_style(style); - } - } - } - - let time_end = time_start + zoom; - for time in time_start..time_end.min(phrase.length) { - for event in phrase.notes[time].iter() { - match event { - MidiMessage::NoteOn { key, .. } => { - let note = key.as_int() as usize; - let cell = buf.get_mut(x, note).unwrap(); - cell.set_char('█'); - cell.set_style(style); - notes_on[note] = true - }, - MidiMessage::NoteOff { key, .. } => { - notes_on[key.as_int() as usize] = false - }, - _ => {} - } - } - } - - } - } -} - -has_size!(|self:PianoHorizontal|&self.size); - -impl MidiRange for PianoHorizontal { - fn time_len (&self) -> &AtomicUsize { self.range.time_len() } - fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() } - fn time_lock (&self) -> &AtomicBool { self.range.time_lock() } - fn time_start (&self) -> &AtomicUsize { self.range.time_start() } - fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() } - fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() } - fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() } -} -impl MidiPoint for PianoHorizontal { - fn note_len (&self) -> usize { self.point.note_len()} - fn set_note_len (&self, x: usize) { self.point.set_note_len(x) } - fn note_point (&self) -> usize { self.point.note_point() } - fn set_note_point (&self, x: usize) { self.point.set_note_point(x) } - fn time_point (&self) -> usize { self.point.time_point() } - fn set_time_point (&self, x: usize) { self.point.set_time_point(x) } -} -impl PhraseViewMode for PianoHorizontal { - fn phrase (&self) -> &Option>> { - &self.phrase - } - fn phrase_mut (&mut self) -> &mut Option>> { - &mut self.phrase - } - /// Determine the required space to render the phrase. - fn buffer_size (&self, phrase: &Phrase) -> (usize, usize) { - (phrase.length / self.range.time_zoom().get(), 128) - } - fn redraw (&mut self) { - let buffer = if let Some(phrase) = self.phrase.as_ref() { - let phrase = phrase.read().unwrap(); - let buf_size = self.buffer_size(&phrase); - let mut buffer = BigBuffer::from(buf_size); - let note_len = self.note_len(); - let time_zoom = self.time_zoom().get(); - self.time_len().set(phrase.length); - PianoHorizontal::draw_bg(&mut buffer, &phrase, time_zoom, note_len); - PianoHorizontal::draw_fg(&mut buffer, &phrase, time_zoom); - buffer - } else { - Default::default() - }; - self.buffer = buffer - } - fn set_phrase (&mut self, phrase: Option<&Arc>>) { - *self.phrase_mut() = phrase.map(|p|p.clone()); - self.color = phrase.map(|p|p.read().unwrap().color.clone()) - .unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64)))); - self.redraw(); - } -} - -impl std::fmt::Debug for PianoHorizontal { - fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("PianoHorizontal") - .field("time_zoom", &self.range.time_zoom) - .field("buffer", &format!("{}x{}", self.buffer.width, self.buffer.height)) - .finish() - } -} - // 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); - //} - //} diff --git a/crates/tek/src/tui/pool.rs b/crates/tek/src/tui/pool.rs deleted file mode 100644 index 53901fea..00000000 --- a/crates/tek/src/tui/pool.rs +++ /dev/null @@ -1,372 +0,0 @@ -use super::*; -use crate::{ - PhrasePoolCommand as Pool, - tui::phrase_rename::PhraseRenameCommand as Rename, - tui::phrase_length::PhraseLengthCommand as Length, - tui::file_browser::FileBrowserCommand as Browse, -}; - -#[derive(Debug)] -pub struct PoolModel { - pub(crate) visible: bool, - /// Collection of phrases - pub(crate) phrases: Vec>>, - /// Selected phrase - pub(crate) phrase: AtomicUsize, - /// Mode switch - pub(crate) mode: Option, - /// Rendered size - size: Measure, - /// Scroll offset - scroll: usize, -} - -/// Modes for phrase pool -#[derive(Debug, Clone)] -pub enum PoolMode { - /// Renaming a pattern - Rename(usize, String), - /// Editing the length of a pattern - Length(usize, usize, PhraseLengthFocus), - /// Load phrase from disk - Import(usize, FileBrowser), - /// Save phrase to disk - Export(usize, FileBrowser), -} - -#[derive(Clone, PartialEq, Debug)] -pub enum PhrasesCommand { - Show(bool), - /// Update the contents of the phrase pool - Phrase(Pool), - /// Select a phrase from the phrase pool - Select(usize), - /// Rename a phrase - Rename(Rename), - /// Change the length of a phrase - Length(Length), - /// Import from file - Import(Browse), - /// Export to file - Export(Browse), -} - -command!(|self:PhrasesCommand, state: PoolModel|{ - use PhrasesCommand::*; - match self { - Show(visible) => { - state.visible = visible; - Some(Self::Show(!visible)) - } - Rename(command) => match command { - PhraseRenameCommand::Begin => { - let length = state.phrases()[state.phrase_index()].read().unwrap().length; - *state.phrases_mode_mut() = Some( - PoolMode::Length(state.phrase_index(), length, PhraseLengthFocus::Bar) - ); - None - }, - _ => command.execute(state)?.map(Rename) - }, - Length(command) => match command { - PhraseLengthCommand::Begin => { - let name = state.phrases()[state.phrase_index()].read().unwrap().name.clone(); - *state.phrases_mode_mut() = Some( - PoolMode::Rename(state.phrase_index(), name) - ); - None - }, - _ => command.execute(state)?.map(Length) - }, - Import(command) => match command { - FileBrowserCommand::Begin => { - *state.phrases_mode_mut() = Some( - PoolMode::Import(state.phrase_index(), FileBrowser::new(None)?) - ); - None - }, - _ => command.execute(state)?.map(Import) - }, - Export(command) => match command { - FileBrowserCommand::Begin => { - *state.phrases_mode_mut() = Some( - PoolMode::Export(state.phrase_index(), FileBrowser::new(None)?) - ); - None - }, - _ => command.execute(state)?.map(Export) - }, - Select(phrase) => { - state.set_phrase_index(phrase); - None - }, - Phrase(command) => command.execute(state)?.map(Phrase), - } -}); - -input_to_command!(PhrasesCommand:|state: PoolModel,input|match state.phrases_mode() { - Some(PoolMode::Rename(..)) => Self::Rename(Rename::input_to_command(state, input)?), - Some(PoolMode::Length(..)) => Self::Length(Length::input_to_command(state, input)?), - Some(PoolMode::Import(..)) => Self::Import(Browse::input_to_command(state, input)?), - Some(PoolMode::Export(..)) => Self::Export(Browse::input_to_command(state, input)?), - _ => to_phrases_command(state, input)? -}); - -fn to_phrases_command (state: &PoolModel, input: &TuiInput) -> Option { - use KeyCode::{Up, Down, Delete, Char}; - use PhrasesCommand as Cmd; - let index = state.phrase_index(); - let count = state.phrases().len(); - Some(match input.event() { - key_pat!(Char('n')) => Cmd::Rename(Rename::Begin), - key_pat!(Char('t')) => Cmd::Length(Length::Begin), - key_pat!(Char('m')) => Cmd::Import(Browse::Begin), - key_pat!(Char('x')) => Cmd::Export(Browse::Begin), - key_pat!(Char('c')) => Cmd::Phrase(Pool::SetColor(index, ItemColor::random())), - key_pat!(Char('[')) | key_pat!(Up) => Cmd::Select( - index.overflowing_sub(1).0.min(state.phrases().len() - 1) - ), - key_pat!(Char(']')) | key_pat!(Down) => Cmd::Select( - index.saturating_add(1) % state.phrases().len() - ), - key_pat!(Char('<')) => if index > 1 { - state.set_phrase_index(state.phrase_index().saturating_sub(1)); - Cmd::Phrase(Pool::Swap(index - 1, index)) - } else { - return None - }, - key_pat!(Char('>')) => if index < count.saturating_sub(1) { - state.set_phrase_index(state.phrase_index() + 1); - Cmd::Phrase(Pool::Swap(index + 1, index)) - } else { - return None - }, - key_pat!(Delete) => if index > 0 { - state.set_phrase_index(index.min(count.saturating_sub(1))); - Cmd::Phrase(Pool::Delete(index)) - } else { - return None - }, - key_pat!(Char('a')) | key_pat!(Shift-Char('A')) => Cmd::Phrase(Pool::Add(count, Phrase::new( - String::from("(new)"), true, 4 * PPQ, None, Some(ItemPalette::random()) - ))), - key_pat!(Char('i')) => Cmd::Phrase(Pool::Add(index + 1, Phrase::new( - String::from("(new)"), true, 4 * PPQ, None, Some(ItemPalette::random()) - ))), - key_pat!(Char('d')) | key_pat!(Shift-Char('D')) => { - let mut phrase = state.phrases()[index].read().unwrap().duplicate(); - phrase.color = ItemPalette::random_near(phrase.color, 0.25); - Cmd::Phrase(Pool::Add(index + 1, phrase)) - }, - _ => return None - }) -} -impl Default for PoolModel { - fn default () -> Self { - Self { - visible: true, - phrases: vec![RwLock::new(Phrase::default()).into()], - phrase: 0.into(), - scroll: 0, - mode: None, - size: Measure::new(), - } - } -} -from!(|phrase:&Arc>|PoolModel = { - let mut model = Self::default(); - model.phrases.push(phrase.clone()); - model.phrase.store(1, Relaxed); - model -}); -has_phrases!(|self: PoolModel|self.phrases); -has_phrase!(|self: PoolModel|self.phrases[self.phrase_index()]); -impl PoolModel { - pub(crate) fn phrase_index (&self) -> usize { - self.phrase.load(Relaxed) - } - pub(crate) fn set_phrase_index (&self, value: usize) { - self.phrase.store(value, Relaxed); - } - pub(crate) fn phrases_mode (&self) -> &Option { - &self.mode - } - pub(crate) fn phrases_mode_mut (&mut self) -> &mut Option { - &mut self.mode - } -} -pub struct PoolView<'a>(pub(crate) &'a PoolModel); -// TODO: Display phrases always in order of appearance -render!(|self: PoolView<'a>|{ - let PoolModel { phrases, mode, .. } = self.0; - let bg = TuiTheme::g(32); - let title_color = TuiTheme::ti1(); - let upper_left = "Pool:"; - let upper_right = format!("({})", phrases.len()); - let color = self.0.phrase().read().unwrap().color; - Tui::bg(bg, lay!(move|add|{ - add(&Fill::wh(Outer(Style::default().fg(color.base.rgb).bg(bg))))?; - //add(&Lozenge(Style::default().bg(border_bg).fg(border_color)))?; - add(&Tui::inset_xy(0, 1, Fill::wh(col!(move|add|match mode { - Some(PoolMode::Import(_, ref file_picker)) => add(file_picker), - Some(PoolMode::Export(_, ref file_picker)) => add(file_picker), - _ => Ok(for (i, phrase) in phrases.iter().enumerate() { - add(&lay!(|add|{ - let Phrase { ref name, color, length, .. } = *phrase.read().unwrap(); - let mut length = PhraseLength::new(length, None); - if let Some(PoolMode::Length(phrase, new_length, focus)) = mode { - if i == *phrase { - length.pulses = *new_length; - length.focus = Some(*focus); - } - } - add(&Tui::bg(color.base.rgb, Fill::w(col!([ - Fill::w(lay!(|add|{ - add(&Fill::w(Align::w(format!(" {i}"))))?; - add(&Fill::w(Align::e(Tui::pull_x(1, length.clone())))) - })), - Tui::bold(true, { - let mut row2 = format!(" {name}"); - if let Some(PoolMode::Rename(phrase, _)) = mode { - if i == *phrase { - row2 = format!("{row2}▄"); - } - }; - row2 - }), - ]))))?; - if i == self.0.phrase_index() { - add(&CORNERS)?; - } - Ok(()) - }))?; - }) - }))))?; - add(&Fill::w(Align::nw(Tui::push_x(1, Tui::fg(title_color, upper_left.to_string())))))?; - add(&Fill::w(Align::ne(Tui::pull_x(1, Tui::fg(title_color, upper_right.to_string())))))?; - add(&self.0.size) - })) -}); -pub struct PhraseSelector { - pub(crate) title: &'static str, - pub(crate) name: String, - pub(crate) color: ItemPalette, - pub(crate) time: String, -} -// TODO: Display phrases always in order of appearance -render!(|self: PhraseSelector|Fixed::wh(24, 1, row!([ - Tui::fg(self.color.lightest.rgb, Tui::bold(true, &self.title)), - Tui::fg_bg(self.color.lighter.rgb, self.color.base.rgb, row!([ - format!("{:8}", &self.name[0..8.min(self.name.len())]), - Tui::bg(self.color.dark.rgb, &self.time), - ])), -]))); -impl PhraseSelector { - // beats elapsed - pub fn play_phrase (state: &T) -> Self { - let (name, color) = if let Some((_, Some(phrase))) = state.play_phrase() { - let Phrase { ref name, color, .. } = *phrase.read().unwrap(); - (name.clone(), color) - } else { - ("".to_string(), ItemPalette::from(TuiTheme::g(64))) - }; - let time = if let Some(elapsed) = state.pulses_since_start_looped() { - format!("+{:>}", state.clock().timebase.format_beats_0(elapsed)) - } else { - String::from(" ") - }; - Self { title: " Now|", time, name, color, } - } - // beats until switchover - pub fn next_phrase (state: &T) -> Self { - let (time, name, color) = if let Some((t, Some(phrase))) = state.next_phrase() { - let Phrase { ref name, color, .. } = *phrase.read().unwrap(); - let time = { - let target = t.pulse.get(); - let current = state.clock().playhead.pulse.get(); - if target > current { - let remaining = target - current; - format!("-{:>}", state.clock().timebase.format_beats_0(remaining)) - } else { - String::new() - } - }; - (time, name.clone(), color) - } else if let Some((_, Some(phrase))) = state.play_phrase() { - let phrase = phrase.read().unwrap(); - if phrase.looped { - (" ".into(), phrase.name.clone(), phrase.color.clone()) - } else { - (" ".into(), " ".into(), TuiTheme::g(64).into()) - } - } else { - (" ".into(), " ".into(), TuiTheme::g(64).into()) - }; - Self { title: " Next|", time, name, color, } - } -} -command!(|self: FileBrowserCommand, state: PoolModel|{ - use PoolMode::*; - use FileBrowserCommand::*; - let mode = &mut state.mode; - match mode { - Some(Import(index, ref mut browser)) => match self { - Cancel => { *mode = None; }, - Chdir(cwd) => { *mode = Some(Import(*index, FileBrowser::new(Some(cwd))?)); }, - Select(index) => { browser.index = index; }, - Confirm => if browser.is_file() { - let index = *index; - let path = browser.path(); - *mode = None; - PhrasePoolCommand::Import(index, path).execute(state)?; - } else if browser.is_dir() { - *mode = Some(Import(*index, browser.chdir()?)); - }, - _ => todo!(), - }, - Some(Export(index, ref mut browser)) => match self { - Cancel => { *mode = None; }, - Chdir(cwd) => { *mode = Some(Export(*index, FileBrowser::new(Some(cwd))?)); }, - Select(index) => { browser.index = index; }, - _ => unreachable!() - }, - _ => unreachable!(), - }; - None -}); -input_to_command!(FileBrowserCommand:|state: PoolModel,from|{ - - use FileBrowserCommand::*; - use KeyCode::{Up, Down, Left, Right, Enter, Esc, Backspace, Char}; - if let Some(PoolMode::Import(_index, browser)) = &state.mode { - match from.event() { - key_pat!(Up) => Select(browser.index.overflowing_sub(1).0 - .min(browser.len().saturating_sub(1))), - key_pat!(Down) => Select(browser.index.saturating_add(1) - % browser.len()), - key_pat!(Right) => Chdir(browser.cwd.clone()), - key_pat!(Left) => Chdir(browser.cwd.clone()), - key_pat!(Enter) => Confirm, - key_pat!(Char(_)) => { todo!() }, - key_pat!(Backspace) => { todo!() }, - key_pat!(Esc) => Cancel, - _ => return None - } - } else if let Some(PoolMode::Export(_index, browser)) = &state.mode { - match from.event() { - key_pat!(Up) => Select(browser.index.overflowing_sub(1).0 - .min(browser.len())), - key_pat!(Down) => Select(browser.index.saturating_add(1) - % browser.len()), - key_pat!(Right) => Chdir(browser.cwd.clone()), - key_pat!(Left) => Chdir(browser.cwd.clone()), - key_pat!(Enter) => Confirm, - key_pat!(Char(_)) => { todo!() }, - key_pat!(Backspace) => { todo!() }, - key_pat!(Esc) => Cancel, - _ => return None - } - } else { - unreachable!() - } -}); diff --git a/crates/tek/src/tui/port_select.rs b/crates/tek/src/tui/port_select.rs deleted file mode 100644 index 1850a253..00000000 --- a/crates/tek/src/tui/port_select.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::*; - -pub struct PortSelector { - pub(crate) title: &'static str, -} - -render!(|self: PortSelector|{}); diff --git a/crates/tek/src/tui/status_bar.rs b/crates/tek/src/tui/status_bar.rs deleted file mode 100644 index a0cc3bc1..00000000 --- a/crates/tek/src/tui/status_bar.rs +++ /dev/null @@ -1,134 +0,0 @@ -use crate::*; - -pub struct PhraseEditStatus<'a>(pub &'a PhraseEditorModel); -render!(|self:PhraseEditStatus<'a>|{ - let (color, name, length, looped) = if let Some(phrase) = self.0.phrase().as_ref().map(|p|p.read().unwrap()) { - (phrase.color, phrase.name.clone(), phrase.length, phrase.looped) - } else { - (ItemPalette::from(TuiTheme::g(64)), String::new(), 0, false) - }; - let bg = color.darkest.rgb; - let fg = color.lightest.rgb; - let field = move|x, y|row!([ - Tui::fg_bg(color.lighter.rgb, color.darker.rgb, Tui::bold(true, x)), - Tui::fg_bg(color.light.rgb, color.darker.rgb, Tui::bold(true, "│")), - Fill::w(Tui::fg_bg(color.lightest.rgb, color.dark.rgb, &y)), - ]); - Fill::w(Tui::fg_bg(fg, bg, row!([ - Fixed::wh(26, 3, col!(![ - field(" Edit", format!("{name}")), - field(" Length", format!("{length}")), - field(" Loop", format!("{looped}"))])), - Fixed::wh(30, 3, col!(![ - field(" Time", format!("{}/{}-{} ({}*{}) {}", - self.0.time_point(), self.0.time_start().get(), self.0.time_end(), - self.0.time_axis().get(), self.0.time_zoom().get(), - if self.0.time_lock().get() { "[lock]" } else { " " })), - field(" Note", format!("{} ({}) {} | {}-{} ({})", - self.0.note_point(), to_note_name(self.0.note_point()), self.0.note_len(), - to_note_name(self.0.note_lo().get()), to_note_name(self.0.note_hi()), - self.0.note_axis().get()))]))])))}); - -/// Status bar for sequencer app -#[derive(Clone)] -pub struct SequencerStatus { - pub(crate) width: usize, - pub(crate) cpu: Option, - pub(crate) size: String, - pub(crate) res: String, - pub(crate) playing: bool, -} -from!(|state:&SequencerTui|SequencerStatus = { - let samples = state.clock.chunk.load(Relaxed); - let rate = state.clock.timebase.sr.get() as f64; - 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()), - res: format!("│{}s│{:.1}kHz│{:.1}ms│", samples, rate / 1000., buffer * 1000.), - } -}); -render!(|self: SequencerStatus|Fixed::h(2, lay!([ - Self::help(), - Fill::wh(Align::se(Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), self.stats()))), -]))); -impl SequencerStatus { - fn help () -> impl Render { - 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 <'a> (&'a self) -> impl Render + use<'a> { - row!([&self.cpu, &self.res, &self.size]) - } -} - -/// Status bar for arranger app -#[derive(Clone)] -pub struct ArrangerStatus { - pub(crate) width: usize, - pub(crate) cpu: Option, - pub(crate) size: String, - pub(crate) res: String, - pub(crate) playing: bool, -} -from!(|state:&ArrangerTui|ArrangerStatus = { - let samples = state.clock.chunk.load(Relaxed); - let rate = state.clock.timebase.sr.get() as f64; - 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()), - res: format!("│{}s│{:.1}kHz│{:.1}ms│", samples, rate / 1000., buffer * 1000.), - } -}); -render!(|self: ArrangerStatus|Fixed::h(2, lay!([ - Self::help(), - Fill::wh(Align::se(Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), self.stats()))), -]))); -impl ArrangerStatus { - fn help () -> impl Render { - 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"), - single(" Ctrl", " scroll"), - single(" ▲▼▶◀", " cell"), - double(("p", "put"), ("g", "get")), - double(("q", "enqueue"), ("e", "edit")), - single(" wsad", " note"), - double(("a", "append"), ("s", "set"),), - double((",.", "length"), ("<>", "triplet"),), - double(("[]", "phrase"), ("{}", "order"),), - ])) - } - fn stats <'a> (&'a self) -> impl Render + use<'a> { - row!([&self.cpu, &self.res, &self.size]) - } -} diff --git a/crates/tek/src/tui/tui_border.rs b/crates/tek/src/tui/tui_border.rs deleted file mode 100644 index 8408ed44..00000000 --- a/crates/tek/src/tui/tui_border.rs +++ /dev/null @@ -1,246 +0,0 @@ -use crate::*; - -pub struct Bordered>(pub S, pub W); - -render!(|self: Bordered>|{ - Fill::wh(lay!([Border(self.0), Tui::inset_xy(1, 1, widget(&self.1))])) -}); - -pub struct Border(pub S); - -impl Render for Border { - fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> { - Ok(Some([0, 0])) - } - fn render (&self, to: &mut TuiOutput) -> Usually<()> { - let area = to.area(); - if area.w() > 0 && area.y() > 0 { - to.blit(&self.0.nw(), area.x(), area.y(), self.0.style()); - to.blit(&self.0.ne(), area.x() + area.w() - 1, area.y(), self.0.style()); - to.blit(&self.0.sw(), area.x(), area.y() + area.h() - 1, self.0.style()); - to.blit(&self.0.se(), area.x() + area.w() - 1, area.y() + area.h() - 1, self.0.style()); - for x in area.x()+1..area.x()+area.w()-1 { - to.blit(&self.0.n(), x, area.y(), self.0.style()); - to.blit(&self.0.s(), x, area.y() + area.h() - 1, self.0.style()); - } - for y in area.y()+1..area.y()+area.h()-1 { - to.blit(&self.0.w(), area.x(), y, self.0.style()); - to.blit(&self.0.e(), area.x() + area.w() - 1, y, self.0.style()); - } - } - Ok(()) - } -} - -pub trait BorderStyle: Send + Sync + Copy { - fn wrap > (self, w: W) -> Bordered { - Bordered(self, w) - } - const NW: &'static str = ""; - const N: &'static str = ""; - const NE: &'static str = ""; - const E: &'static str = ""; - const SE: &'static str = ""; - const S: &'static str = ""; - const SW: &'static str = ""; - const W: &'static str = ""; - - const N0: &'static str = ""; - const S0: &'static str = ""; - const W0: &'static str = ""; - const E0: &'static str = ""; - - fn n (&self) -> &str { Self::N } - fn s (&self) -> &str { Self::S } - fn e (&self) -> &str { Self::E } - fn w (&self) -> &str { Self::W } - fn nw (&self) -> &str { Self::NW } - fn ne (&self) -> &str { Self::NE } - fn sw (&self) -> &str { Self::SW } - fn se (&self) -> &str { Self::SE } - #[inline] fn draw <'a> ( - &self, to: &mut TuiOutput - ) -> Usually<()> { - self.draw_horizontal(to, None)?; - self.draw_vertical(to, None)?; - self.draw_corners(to, None)?; - Ok(()) - } - #[inline] fn draw_horizontal ( - &self, to: &mut TuiOutput, style: Option