diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 72e8ffc0..00000000 --- a/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -* diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 7b08ef33..00000000 --- a/.editorconfig +++ /dev/null @@ -1,3 +0,0 @@ -root = true -[*] -max_line_length = 132 diff --git a/.envrc b/.envrc deleted file mode 100644 index 1d953f4b..00000000 --- a/.envrc +++ /dev/null @@ -1 +0,0 @@ -use nix diff --git a/.forgejo/workflows/build.yaml b/.forgejo/workflows/build.yaml new file mode 100644 index 00000000..316ad5c0 --- /dev/null +++ b/.forgejo/workflows/build.yaml @@ -0,0 +1,6 @@ +on: [push] +jobs: + build: + runs-on: rust + stepS: + - run: cargo build diff --git a/.forgejo/workflows/release.yml.off b/.forgejo/workflows/release.yml.off deleted file mode 100644 index e2371720..00000000 --- a/.forgejo/workflows/release.yml.off +++ /dev/null @@ -1,36 +0,0 @@ -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 deleted file mode 100644 index 16d33122..00000000 --- a/.forgejo/workflows/test.yaml +++ /dev/null @@ -1,49 +0,0 @@ -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 e5790860..1a417612 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,5 @@ -target/* -!target/.gitkeep +target 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 15f065ba..e69de29b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,10 +0,0 @@ -[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/.old/from_arranger.rs b/.old/from_arranger.rs deleted file mode 100644 index 07502a6b..00000000 --- a/.old/from_arranger.rs +++ /dev/null @@ -1,188 +0,0 @@ - - -//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 deleted file mode 100644 index 8602766a..00000000 --- a/.old/midi.scratch.rs +++ /dev/null @@ -1,31 +0,0 @@ -/////////////////////////////////////////////////////////////////////////////////////////////////// -//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 deleted file mode 100644 index d2cceae2..00000000 --- a/.old/midi_import.rs +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index 82009355..00000000 --- a/.old/sampler_scratch.rs +++ /dev/null @@ -1,105 +0,0 @@ -//handle!(TuiIn: |self: Sampler, input|SamplerCommand::execute_with_state(self, input.event())); -//input_to_command!(SamplerCommand: |state: Sampler, input: Event|match state.mode{ - //Some(SamplerMode::Import(..)) => Self::Import( - //FileBrowserCommand::input_to_command(state, input)? - //), - //_ => match input { - //// load sample - //kpat!(Shift-Char('L')) => Self::Import(FileBrowserCommand::Begin), - //kpat!(KeyCode::Up) => Self::Select(state.note_pos().overflowing_add(1).0.min(127)), - //kpat!(KeyCode::Down) => Self::Select(state.note_pos().overflowing_sub(1).0.min(127)), - //_ => return None - //} -//}); -//impl Handle for AddSampleModal { - //fn handle (&mut self, from: &TuiIn) -> Perhaps { - //if from.handle_keymap(self, KEYMAP_ADD_SAMPLE)? { - //return Ok(Some(true)) - //} - //Ok(Some(true)) - //} -//} -//pub const KEYMAP_ADD_SAMPLE: &'static [KeyBinding] = keymap!(AddSampleModal { - //[Esc, NONE, "sampler/add/close", "close help dialog", |modal: &mut AddSampleModal|{ - //modal.exit(); - //Ok(true) - //}], - //[Up, NONE, "sampler/add/prev", "select previous entry", |modal: &mut AddSampleModal|{ - //modal.prev(); - //Ok(true) - //}], - //[Down, NONE, "sampler/add/next", "select next entry", |modal: &mut AddSampleModal|{ - //modal.next(); - //Ok(true) - //}], - //[Enter, NONE, "sampler/add/enter", "activate selected entry", |modal: &mut AddSampleModal|{ - //if modal.pick()? { - //modal.exit(); - //} - //Ok(true) - //}], - //[Char('p'), NONE, "sampler/add/preview", "preview selected entry", |modal: &mut AddSampleModal|{ - //modal.try_preview()?; - //Ok(true) - //}] -//}); -//from_atom!("sampler" => |jack: &Jack, args| -> crate::Sampler { - //let mut name = String::new(); - //let mut dir = String::new(); - //let mut samples = BTreeMap::new(); - //atom!(atom in args { - //Atom::Map(map) => { - //if let Some(Atom::Str(n)) = map.get(&Atom::Key(":name")) { - //name = String::from(*n); - //} - //if let Some(Atom::Str(n)) = map.get(&Atom::Key(":dir")) { - //dir = String::from(*n); - //} - //}, - //Atom::List(args) => match args.first() { - //Some(Atom::Symbol("sample")) => { - //let (midi, sample) = MidiSample::from_atom((jack, &dir), &args[1..])?; - //if let Some(midi) = midi { - //samples.insert(midi, sample); - //} else { - //panic!("sample without midi binding: {}", sample.read().unwrap().name); - //} - //}, - //_ => panic!("unexpected in sampler {name}: {args:?}") - //}, - //_ => panic!("unexpected in sampler {name}: {atom:?}") - //}); - //Self::new(jack, &name) -//}); -//from_atom!("sample" => |(_jack, dir): (&Jack, &str), args| -> MidiSample { - //let mut name = String::new(); - //let mut file = String::new(); - //let mut midi = None; - //let mut start = 0usize; - //atom!(atom in args { - //Atom::Map(map) => { - //if let Some(Atom::Str(n)) = map.get(&Atom::Key(":name")) { - //name = String::from(*n); - //} - //if let Some(Atom::Str(f)) = map.get(&Atom::Key(":file")) { - //file = String::from(*f); - //} - //if let Some(Atom::Int(i)) = map.get(&Atom::Key(":start")) { - //start = *i as usize; - //} - //if let Some(Atom::Int(m)) = map.get(&Atom::Key(":midi")) { - //midi = Some(u7::from(*m as u8)); - //} - //}, - //_ => panic!("unexpected in sample {name}"), - //}); - //let (end, data) = Sample::read_data(&format!("{dir}/{file}"))?; - //Ok((midi, Arc::new(RwLock::new(crate::Sample { - //name, - //start, - //end, - //channels: data, - //rate: None, - //gain: 1.0 - //})))) -//}); diff --git a/.old/scratch.rs b/.old/scratch.rs deleted file mode 100644 index 9bd28f30..00000000 --- a/.old/scratch.rs +++ /dev/null @@ -1,375 +0,0 @@ -//impl Bar for ArrangerStatus { - //type State = (ArrangerFocus, ArrangerSelection, bool); - //fn hotkey_fg () -> Color where Self: Sized { - //TuiTheme::HOTKEY_FG - //} - //fn update (&mut self, (focused, selected, entered): &Self::State) { - //*self = match focused { - ////ArrangerFocus::Menu => { todo!() }, - //ArrangerFocus::Transport(_) => ArrangerStatus::Transport, - //ArrangerFocus::Arranger => match selected { - //ArrangerSelection::Mix => ArrangerStatus::ArrangerMix, - //ArrangerSelection::Track(_) => ArrangerStatus::ArrangerTrack, - //ArrangerSelection::Scene(_) => ArrangerStatus::ArrangerScene, - //ArrangerSelection::Clip(_, _) => ArrangerStatus::ArrangerClip, - //}, - //ArrangerFocus::Phrases => ArrangerStatus::PhrasePool, - //ArrangerFocus::PhraseEditor => match entered { - //true => ArrangerStatus::PhraseEdit, - //false => ArrangerStatus::PhraseView, - //}, - //} - //} -//} - -//render!(|self: ArrangerStatus|{ - - //let label = match self { - //Self::Transport => "TRANSPORT", - //Self::ArrangerMix => "PROJECT", - //Self::ArrangerTrack => "TRACK", - //Self::ArrangerScene => "SCENE", - //Self::ArrangerClip => "CLIP", - //Self::PhrasePool => "SEQ LIST", - //Self::PhraseView => "VIEW SEQ", - //Self::PhraseEdit => "EDIT SEQ", - //}; - - //let status_bar_bg = TuiTheme::status_bar_bg(); - - //let mode_bg = TuiTheme::mode_bg(); - //let mode_fg = TuiTheme::mode_fg(); - //let mode = Tui::fg(mode_fg, Tui::bg(mode_bg, Tui::bold(true, format!(" {label} ")))); - - //let commands = match self { - //Self::ArrangerMix => Self::command(&[ - //["", "c", "olor"], - //["", "<>", "resize"], - //["", "+-", "zoom"], - //["", "n", "ame/number"], - //["", "Enter", " stop all"], - //]), - //Self::ArrangerClip => Self::command(&[ - //["", "g", "et"], - //["", "s", "et"], - //["", "a", "dd"], - //["", "i", "ns"], - //["", "d", "up"], - //["", "e", "dit"], - //["", "c", "olor"], - //["re", "n", "ame"], - //["", ",.", "select"], - //["", "Enter", " launch"], - //]), - //Self::ArrangerTrack => Self::command(&[ - //["re", "n", "ame"], - //["", ",.", "resize"], - //["", "<>", "move"], - //["", "i", "nput"], - //["", "o", "utput"], - //["", "m", "ute"], - //["", "s", "olo"], - //["", "Del", "ete"], - //["", "Enter", " stop"], - //]), - //Self::ArrangerScene => Self::command(&[ - //["re", "n", "ame"], - //["", "Del", "ete"], - //["", "Enter", " launch"], - //]), - //Self::PhrasePool => Self::command(&[ - //["", "a", "ppend"], - //["", "i", "nsert"], - //["", "d", "uplicate"], - //["", "Del", "ete"], - //["", "c", "olor"], - //["re", "n", "ame"], - //["leng", "t", "h"], - //["", ",.", "move"], - //["", "+-", "resize view"], - //]), - //Self::PhraseView => Self::command(&[ - //["", "enter", " edit"], - //["", "arrows/pgup/pgdn", " scroll"], - //["", "+=", "zoom"], - //]), - //Self::PhraseEdit => Self::command(&[ - //["", "esc", " exit"], - //["", "a", "ppend"], - //["", "s", "et"], - //["", "][", "length"], - //["", "+-", "zoom"], - //]), - //_ => Self::command(&[]) - //}; - - ////let commands = commands.iter().reduce(String::new(), |s, (a, b, c)| format!("{s} {a}{b}{c}")); - //Tui::bg(status_bar_bg, Fill::w(row!([mode, commands]))) - -//}); - -///// Status bar for arranger app -//#[derive(Copy, Clone, Debug)] -//pub enum ArrangerStatus { - //Transport, - //ArrangerMix, - //ArrangerTrack, - //ArrangerScene, - //ArrangerClip, - //PhrasePool, - //PhraseView, - //PhraseEdit, -//} - - //let focused = true; - //let _tracks = view.tracks(); - //lay!( - //focused.then_some(Background(TuiTheme::border_bg())), - //row!( - //// name - //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ - //todo!() - ////let Self(tracks, selected) = self; - ////let yellow = Some(Style::default().yellow().bold().not_dim()); - ////let white = Some(Style::default().white().bold().not_dim()); - ////let area = to.area(); - ////let area = [area.x(), area.y(), 3 + 5.max(track_name_max_len(tracks)) as u16, area.h()]; - ////let offset = 0; // track scroll offset - ////for y in 0..area.h() { - ////if y == 0 { - ////to.blit(&"Mixer", area.x() + 1, area.y() + y, Some(DIM))?; - ////} else if y % 2 == 0 { - ////let index = (y as usize - 2) / 2 + offset; - ////if let Some(track) = tracks.get(index) { - ////let selected = selected.track() == Some(index); - ////let style = if selected { yellow } else { white }; - ////to.blit(&format!(" {index:>02} "), area.x(), area.y() + y, style)?; - ////to.blit(&*track.name.read().unwrap(), area.x() + 4, area.y() + y, style)?; - ////} - ////} - ////} - ////Ok(Some(area)) - //}), - //// monitor - //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ - //todo!() - ////let Self(tracks) = self; - ////let mut area = to.area(); - ////let on = Some(Style::default().not_dim().green().bold()); - ////let off = Some(DIM); - ////area.x += 1; - ////for y in 0..area.h() { - ////if y == 0 { - //////" MON ".blit(to.buffer, area.x, area.y + y, style2)?; - ////} else if y % 2 == 0 { - ////let index = (y as usize - 2) / 2; - ////if let Some(track) = tracks.get(index) { - ////let style = if track.monitoring { on } else { off }; - ////to.blit(&" MON ", area.x(), area.y() + y, style)?; - ////} else { - ////area.height = y; - ////break - ////} - ////} - ////} - ////area.width = 4; - ////Ok(Some(area)) - //}), - //// record - //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ - //todo!() - ////let Self(tracks) = self; - ////let mut area = to.area(); - ////let on = Some(Style::default().not_dim().red().bold()); - ////let off = Some(Style::default().dim()); - ////area.x += 1; - ////for y in 0..area.h() { - ////if y == 0 { - //////" REC ".blit(to.buffer, area.x, area.y + y, style2)?; - ////} else if y % 2 == 0 { - ////let index = (y as usize - 2) / 2; - ////if let Some(track) = tracks.get(index) { - ////let style = if track.recording { on } else { off }; - ////to.blit(&" REC ", area.x(), area.y() + y, style)?; - ////} else { - ////area.height = y; - ////break - ////} - ////} - ////} - ////area.width = 4; - ////Ok(Some(area)) - //}), - //// overdub - //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ - //todo!() - ////let Self(tracks) = self; - ////let mut area = to.area(); - ////let on = Some(Style::default().not_dim().yellow().bold()); - ////let off = Some(Style::default().dim()); - ////area.x = area.x + 1; - ////for y in 0..area.h() { - ////if y == 0 { - //////" OVR ".blit(to.buffer, area.x, area.y + y, style2)?; - ////} else if y % 2 == 0 { - ////let index = (y as usize - 2) / 2; - ////if let Some(track) = tracks.get(index) { - ////to.blit(&" OVR ", area.x(), area.y() + y, if track.overdub { - ////on - ////} else { - ////off - ////})?; - ////} else { - ////area.height = y; - ////break - ////} - ////} - ////} - ////area.width = 4; - ////Ok(Some(area)) - //}), - //// erase - //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ - //todo!() - ////let Self(tracks) = self; - ////let mut area = to.area(); - ////let off = Some(Style::default().dim()); - ////area.x = area.x + 1; - ////for y in 0..area.h() { - ////if y == 0 { - //////" DEL ".blit(to.buffer, area.x, area.y + y, style2)?; - ////} else if y % 2 == 0 { - ////let index = (y as usize - 2) / 2; - ////if let Some(_) = tracks.get(index) { - ////to.blit(&" DEL ", area.x(), area.y() + y, off)?; - ////} else { - ////area.height = y; - ////break - ////} - ////} - ////} - ////area.width = 4; - ////Ok(Some(area)) - //}), - //// gain - //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ - //todo!() - ////let Self(tracks) = self; - ////let mut area = to.area(); - ////let off = Some(Style::default().dim()); - ////area.x = area.x() + 1; - ////for y in 0..area.h() { - ////if y == 0 { - //////" GAIN ".blit(to.buffer, area.x, area.y + y, style2)?; - ////} else if y % 2 == 0 { - ////let index = (y as usize - 2) / 2; - ////if let Some(_) = tracks.get(index) { - ////to.blit(&" +0.0 ", area.x(), area.y() + y, off)?; - ////} else { - ////area.height = y; - ////break - ////} - ////} - ////} - ////area.width = 7; - ////Ok(Some(area)) - //}), - //// scenes - //Widget::new(|_|{todo!()}, |to: &mut TuiOutput|{ - //let [x, y, _, height] = to.area(); - //let mut x2 = 0; - //Ok(for (scene_index, scene) in view.scenes().iter().enumerate() { - //let active_scene = view.selected.scene() == Some(scene_index); - //let sep = Some(if active_scene { - //Style::default().yellow().not_dim() - //} else { - //Style::default().dim() - //}); - //for y in y+1..y+height { - //to.blit(&"│", x + x2, y, sep); - //} - //let name = scene.name.read().unwrap(); - //let mut x3 = name.len() as u16; - //to.blit(&*name, x + x2, y, sep); - //for (i, clip) in scene.clips.iter().enumerate() { - //let active_track = view.selected.track() == Some(i); - //if let Some(clip) = clip { - //let y2 = y + 2 + i as u16 * 2; - //let label = format!("{}", clip.read().unwrap().name); - //to.blit(&label, x + x2, y2, Some(if active_track && active_scene { - //Style::default().not_dim().yellow().bold() - //} else { - //Style::default().not_dim() - //})); - //x3 = x3.max(label.len() as u16) - //} - //} - //x2 = x2 + x3 + 1; - //}) - //}), - //) - //) -//} - -//impl Command for ArrangerSceneCommand { -//} - //Edit(phrase) => { state.state.phrase = phrase.clone() }, - //ToggleViewMode => { state.state.mode.to_next(); }, - //Delete => { state.state.delete(); }, - //Activate => { state.state.activate(); }, - //ZoomIn => { state.state.zoom_in(); }, - //ZoomOut => { state.state.zoom_out(); }, - //MoveBack => { state.state.move_back(); }, - //MoveForward => { state.state.move_forward(); }, - //RandomColor => { state.state.randomize_color(); }, - //Put => { state.state.phrase_put(); }, - //Get => { state.state.phrase_get(); }, - //AddScene => { state.state.scene_add(None, None)?; }, - //AddTrack => { state.state.track_add(None, None)?; }, - //ToggleLoop => { state.state.toggle_loop() }, - //pub fn zoom_in (&mut self) { - //if let ArrangerEditorMode::V(factor) = self.mode { - //self.mode = ArrangerEditorMode::V(factor + 1) - //} - //} - //pub fn zoom_out (&mut self) { - //if let ArrangerEditorMode::V(factor) = self.mode { - //self.mode = ArrangerEditorMode::V(factor.saturating_sub(1)) - //} - //} - //pub fn move_back (&mut self) { - //match self.selected { - //ArrangerEditorFocus::Scene(s) => { - //if s > 0 { - //self.scenes.swap(s, s - 1); - //self.selected = ArrangerEditorFocus::Scene(s - 1); - //} - //}, - //ArrangerEditorFocus::Track(t) => { - //if t > 0 { - //self.tracks.swap(t, t - 1); - //self.selected = ArrangerEditorFocus::Track(t - 1); - //// FIXME: also swap clip order in scenes - //} - //}, - //_ => todo!("arrangement: move forward") - //} - //} - //pub fn move_forward (&mut self) { - //match self.selected { - //ArrangerEditorFocus::Scene(s) => { - //if s < self.scenes.len().saturating_sub(1) { - //self.scenes.swap(s, s + 1); - //self.selected = ArrangerEditorFocus::Scene(s + 1); - //} - //}, - //ArrangerEditorFocus::Track(t) => { - //if t < self.tracks.len().saturating_sub(1) { - //self.tracks.swap(t, t + 1); - //self.selected = ArrangerEditorFocus::Track(t + 1); - //// FIXME: also swap clip order in scenes - //} - //}, - //_ => todo!("arrangement: move forward") - //} - //} diff --git a/.old/tek.rs.old b/.old/tek.rs.old deleted file mode 100644 index e64fd51b..00000000 --- a/.old/tek.rs.old +++ /dev/null @@ -1,2113 +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'); - - //} -//} -//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 deleted file mode 100644 index 81fc7dc7..00000000 --- a/.old/todo_arranger.edn +++ /dev/null @@ -1,83 +0,0 @@ - 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/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index f40cf8e8..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,18 +0,0 @@ -## 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 34c84187..bfd59c5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,22 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -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" +version = 3 [[package]] name = "addr2line" @@ -29,22 +13,9 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -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", -] +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "allocator-api2" @@ -52,38 +23,11 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "android-activity" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -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" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -96,37 +40,36 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -138,30 +81,12 @@ 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" @@ -170,15 +95,15 @@ checksum = "628d228f918ac3b82fe590352cc719d30664a0c13ca3a60266fe02c7132d480a" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" -version = "0.3.75" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", "cfg-if", @@ -199,21 +124,6 @@ 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" @@ -222,24 +132,15 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.3" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" - -[[package]] -name = "block2" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" -dependencies = [ - "objc2", -] +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "by_address" @@ -249,41 +150,15 @@ checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" [[package]] name = "bytemuck" -version = "1.23.2" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" [[package]] -name = "bytes" -version = "1.10.1" +name = "byteorder" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -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", -] +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cassowary" @@ -293,47 +168,24 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "castaway" -version = "0.2.4" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" dependencies = [ "rustversion", ] -[[package]] -name = "cc" -version = "1.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -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" +version = "1.0.0" 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" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.5.45" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ "clap_builder", "clap_derive", @@ -341,9 +193,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.44" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ "anstream", "anstyle", @@ -353,9 +205,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.45" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck", "proc-macro2", @@ -365,146 +217,55 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.5" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] -name = "colorchoice" -version = "1.0.4" +name = "clojure-reader" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +checksum = "8755ac891a1a9a21101250e9e8ab55d742ec7957928cbc1b728dc1cbc234e5ce" dependencies = [ - "bytes", - "memchr", + "ordered-float", ] [[package]] -name = "compact_str" -version = "0.8.1" +name = "colorchoice" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" dependencies = [ "castaway", "cfg-if", "itoa", - "rustversion", "ryu", "static_assertions", ] -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "console" -version = "0.15.11" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode", + "lazy_static", "libc", - "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", + "windows-sys 0.52.0", ] [[package]] name = "crossbeam-deque" -version = "0.8.6" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -521,39 +282,21 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crossterm" -version = "0.28.1" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.6.0", "crossterm_winapi", + "libc", "mio", - "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", + "parking_lot 0.12.3", "signal-hook", "signal-hook-mio", "winapi", @@ -568,125 +311,17 @@ dependencies = [ "winapi", ] -[[package]] -name = "ctor" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -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" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "encode_unicode" -version = "1.0.0" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" @@ -699,19 +334,9 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.2" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -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", -] +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "extended" @@ -725,82 +350,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -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" +version = "0.1.3" 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", -] +checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "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", + "wasi", ] [[package]] @@ -811,9 +375,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" dependencies = [ "allocator-api2", "equivalent", @@ -826,57 +390,16 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -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" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" 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" @@ -892,6 +415,15 @@ 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" @@ -901,29 +433,19 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jack" version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78a4ae24f4ee29676aef8330fed1104e72f314cab16643dbeb61bfd99b4a8273" dependencies = [ - "approx", - "bitflags 2.9.3", - "crossbeam-channel", - "ctor", + "bitflags 2.6.0", "jack-sys", "lazy_static", "libc", @@ -933,8 +455,10 @@ dependencies = [ [[package]] name = "jack-sys" version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6013b7619b95a22b576dfb43296faa4ecbe40abbdb97dfd22ead520775fc86ab" dependencies = [ - "bitflags 2.9.3", + "bitflags 1.3.2", "lazy_static", "libc", "libloading", @@ -942,75 +466,16 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -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" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" 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" @@ -1019,29 +484,18 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.175" +version = "0.2.168" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" [[package]] name = "libloading" -version = "0.8.8" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" dependencies = [ "cfg-if", - "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", + "winapi", ] [[package]] @@ -1065,24 +519,6 @@ 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" @@ -1098,9 +534,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -1108,9 +544,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lru" @@ -1138,18 +574,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" - -[[package]] -name = "memmap2" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" -dependencies = [ - "libc", -] +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "midly" @@ -1162,53 +589,23 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.8.9" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.4" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", - "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", + "wasi", + "windows-sys 0.48.0", ] [[package]] @@ -1220,268 +617,28 @@ dependencies = [ "autocfg", ] -[[package]] -name = "num_enum" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -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" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.21.3" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] -name = "once_cell_polyfill" -version = "1.70.1" +name = "ordered-float" +version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" - -[[package]] -name = "orbclient" -version = "0.3.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43" +checksum = "c65ee1f9701bf938026630b455d5315f490640234259037edb259798b3bcf85e" dependencies = [ - "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", + "num-traits", ] [[package]] @@ -1494,7 +651,7 @@ dependencies = [ "fast-srgb8", "palette_derive", "phf", - "rand 0.8.5", + "rand", ] [[package]] @@ -1522,12 +679,12 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", - "parking_lot_core 0.9.11", + "parking_lot_core 0.9.10", ] [[package]] @@ -1546,13 +703,13 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.17", + "redox_syscall 0.5.7", "smallvec", "windows-targets 0.52.6", ] @@ -1563,17 +720,11 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - [[package]] name = "phf" -version = "0.11.3" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ "phf_macros", "phf_shared", @@ -1581,19 +732,19 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.11.3" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" dependencies = [ "phf_shared", - "rand 0.8.5", + "rand", ] [[package]] name = "phf_macros" -version = "0.11.3" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" dependencies = [ "phf_generator", "phf_shared", @@ -1604,162 +755,61 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.11.3" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" dependencies = [ "siphasher", ] -[[package]] -name = "pin-project" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -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" +version = "0.3.31" 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", -] +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "ppv-lite86" -version = "0.2.21" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ "zerocopy", ] -[[package]] -name = "proc-macro-crate" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" -dependencies = [ - "toml_edit", -] - [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] -[[package]] -name = "proptest" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -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" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5" dependencies = [ "crossbeam-utils", "libc", "once_cell", "raw-cpuid", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "web-sys", "winapi", ] -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -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" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 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" @@ -1767,18 +817,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "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", + "rand_chacha", + "rand_core", ] [[package]] @@ -1788,17 +828,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "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", + "rand_core", ] [[package]] @@ -1807,68 +837,43 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "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", + "getrandom", ] [[package]] name = "ratatui" -version = "0.29.0" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.6.0", "cassowary", "compact_str", - "crossterm 0.28.1", - "indoc", - "instability", - "itertools 0.13.0", + "crossterm", + "itertools 0.12.1", "lru", "paste", + "stability", "strum", "unicode-segmentation", "unicode-truncate", - "unicode-width 0.2.0", + "unicode-width", ] [[package]] name = "raw-cpuid" -version = "11.5.0" +version = "11.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" +checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.6.0", ] -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - [[package]] name = "rayon" -version = "1.11.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -1876,9 +881,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.13.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -1895,28 +900,13 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", ] -[[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" @@ -1928,74 +918,21 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.26" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -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", -] +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -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", -] +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -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" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "scopeguard" @@ -2003,33 +940,20 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sctk-adwaita" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" -dependencies = [ - "ab_glyph", - "log", - "memmap2", - "smithay-client-toolkit", - "tiny-skia", -] - [[package]] name = "serde" -version = "1.0.219" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", @@ -2038,24 +962,18 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.0" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - [[package]] name = "signal-hook" -version = "0.3.18" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" dependencies = [ "libc", "signal-hook-registry", @@ -2074,63 +992,33 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] [[package]] name = "siphasher" -version = "1.0.1" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] -name = "smithay-client-toolkit" -version = "0.19.2" +name = "stability" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" dependencies = [ - "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", + "quote", + "syn", ] [[package]] @@ -2139,12 +1027,6 @@ 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" @@ -2370,9 +1252,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -2381,137 +1263,26 @@ dependencies = [ [[package]] name = "tek" -version = "0.3.0" +version = "0.2.0" dependencies = [ "atomic_float", "backtrace", - "clap", - "jack", - "konst", - "livi", - "midly", - "palette", - "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", + "clap", + "clojure-reader", + "crossterm", + "jack", + "livi", + "midly", + "once_cell", "palette", "quanta", - "rand 0.8.5", + "rand", "ratatui", - "tengri_core", - "tengri_dsl", - "tengri_input", - "tengri_output", - "unicode-width 0.2.0", + "symphonia", + "toml", + "uuid", + "wavers", ] [[package]] @@ -2520,16 +1291,7 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "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", + "thiserror-impl", ] [[package]] @@ -2543,146 +1305,45 @@ dependencies = [ "syn", ] -[[package]] -name = "thiserror-impl" -version = "2.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -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" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ - "indexmap", "serde", "serde_spanned", - "toml_datetime 0.7.0", - "toml_parser", - "toml_writer", - "winnow", + "toml_datetime", + "toml_edit", ] [[package]] name = "toml_datetime" -version = "0.6.11" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" - -[[package]] -name = "toml_datetime" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.27" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", - "toml_datetime 0.6.11", + "serde", + "serde_spanned", + "toml_datetime", "winnow", ] -[[package]] -name = "toml_parser" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -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" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-segmentation" @@ -2698,7 +1359,7 @@ checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" dependencies = [ "itertools 0.13.0", "unicode-segmentation", - "unicode-width 0.1.14", + "unicode-width", ] [[package]] @@ -2707,18 +1368,6 @@ 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" @@ -2727,72 +1376,35 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ - "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", + "getrandom", ] [[package]] name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -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", -] +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", "once_cell", - "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", @@ -2802,24 +1414,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2827,9 +1426,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", @@ -2840,150 +1439,27 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" -dependencies = [ - "unicode-ident", -] +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "wavers" -version = "1.5.1" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d599ddd98c95ad3d3fc898cfb5c4023430f6ea62f96083f46d13cc8b82589bd3" +checksum = "ab501e9e5b13446d3a6e846de0c190c96b85ca3eced3bd00460edc67654500c8" dependencies = [ "bytemuck", - "i24", "num-traits", "paste", - "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", + "thiserror", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -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" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" dependencies = [ "js-sys", "wasm-bindgen", @@ -3005,34 +1481,19 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - [[package]] name = "windows-sys" -version = "0.45.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.42.2", + "windows-targets 0.48.5", ] [[package]] @@ -3053,30 +1514,6 @@ 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" @@ -3101,36 +1538,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "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" @@ -3143,18 +1557,6 @@ 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" @@ -3167,18 +1569,6 @@ 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" @@ -3191,30 +1581,12 @@ 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" @@ -3227,18 +1599,6 @@ 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" @@ -3251,18 +1611,6 @@ 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" @@ -3275,18 +1623,6 @@ 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" @@ -3299,159 +1635,30 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -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" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] -[[package]] -name = "wit-bindgen-rt" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -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" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 9f379dc6..301685bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,53 +1,10 @@ [workspace] resolver = "2" -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" } +members = [ + "crates/tek", + #"crates/tek_core", + #"crates/tek_api", + #"crates/tek_tui", + #"crates/tek_cli", + #"crates/tek_layout" +] diff --git a/Justfile b/Justfile index 94551690..07b49870 100644 --- a/Justfile +++ b/Justfile @@ -1,117 +1,34 @@ -export RUSTFLAGS := "--cfg procmacro2_semver_exempt -Zmacro-backtrace -Clink-arg=-fuse-ld=mold" -export RUST_BACKTRACE := "1" - 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 + just -l +status: + cargo c + cloc --by-file src/ + git status 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 - -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*'" + git push --tags -fu codeberg + git push --tags -fu origin +transport: + cargo run --bin tek_transport arranger: - {{debug}} {{name}} {{bpm}} arranger -arranger-ext: - {{debug}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} arranger -arranger-release: - {{release}} {{name}} {{bpm}} arranger -arranger-release-ext: - {{release}} {{name}} {{bpm}} {{midi-in}} {{firefox-in}} {{midi-out}} arranger - -groovebox: - {{debug}} {{name}} {{bpm}} groovebox -groovebox-ext: - reset - {{debug}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} {{audio-in}} {{audio-out}} groovebox -groovebox-browser: - {{debug}} {{name}} {{bpm}} {{audio-in}} groovebox -groovebox-release: - {{release}} {{name}} {{bpm}} groovebox -groovebox-release-ext: - {{release}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} {{audio-in}} {{audio-out}} groovebox -groovebox-release-ext-browser: - {{release}} {{name}} {{bpm}} {{midi-in}} {{firefox-in}} {{audio-out}} groovebox - + cargo run --bin tek_arranger sequencer: - {{debug}} {{name}} {{bpm}} sequencer -sequencer-ext: - {{debug}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} sequencer + cargo run --bin tek_sequencer sequencer-release: - {{release}} {{name}} {{bpm}} sequencer -sequencer-release-ext: - {{release}} {{name}} {{bpm}} {{midi-in}} {{midi-out}} sequencer - + cargo run --release --bin tek_sequencer mixer: - {{debug}} mixer + cargo run --bin tek_mixer track: - {{debug}} track + cargo run --bin tek_track sampler: - {{debug}} sampler + cargo run --bin tek_sampler plugin: - {{debug}} plugin + cargo run --bin tek_plugin diff --git a/LICENSE b/LICENSE index be3f7b28..4b61f9fc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,661 +1,8 @@ - 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 -. +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. diff --git a/README.md b/README.md index 64655591..c927ac6f 100644 --- a/README.md +++ b/README.md @@ -1,90 +1,116 @@ -# tek [![Please don't upload to GitHub](https://nogithub.codeberg.page/badge.svg)](https://nogithub.codeberg.page) +# tek -a music making program for [24-bit unicode terminals](https://sw.kovidgoyal.net/kitty/). +[![Please don't upload to GitHub](https://nogithub.codeberg.page/badge.svg)](https://nogithub.codeberg.page) -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/). +a music making program for your terminal -**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). +## project status -author is reachable via [**mastodon** `@unspeaker@mastodon.social`](https://mastodon.social/@unspeaker) -or [**matrix** `@unspeaker:matrix.org`](https://matrix.to/#/@unspeaker:matrix.org) +for roadmap, see https://codeberg.org/unspeaker/tek/milestones -| | | -|-|-| -|![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)| +> [!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) -## usage +## what it does -* **requirements:** linux; jack or pipewire; 24-bit terminal (i use `kitty`) -* **recommended:** midi controller; samples in wav format; lv2 plugins. - -## keymaps - -* 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 - -## installation - -### 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 -``` +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 -* inspired by trackers and hardware sequencers, - but with the critical feature that 90s samplers lack: - able to **resample, i.e. record while playing!** +### lightweight -* **pop-up scratchpad for musical ideas.** - low resource consumption, can stay open in background. - but flexible enough to allow expanding on compositions +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! -* **human- and machine- readable project format** - simple representation for project data - enable scripting and remapping. +### 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 +``` + +## usage + +> [!WARNING] +> +> The following applies to `tek 0.1.0`. I will update it as part of the `0.2.0` release. + +### Overview + +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: + +* 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**. + +> [!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. diff --git a/app/Cargo.toml b/app/Cargo.toml deleted file mode 100644 index d6c3afa9..00000000 --- a/app/Cargo.toml +++ /dev/null @@ -1,58 +0,0 @@ -[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 deleted file mode 100644 index 17b02a6c..00000000 --- a/app/tek.edn +++ /dev/null @@ -1,224 +0,0 @@ -(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 deleted file mode 100644 index 1ce0c252..00000000 --- a/app/tek.rs +++ /dev/null @@ -1,468 +0,0 @@ -#![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 deleted file mode 100644 index 84763b70..00000000 --- a/app/tek_bind.rs +++ /dev/null @@ -1,325 +0,0 @@ -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 deleted file mode 100644 index ee323bb6..00000000 --- a/app/tek_cfg.rs +++ /dev/null @@ -1,65 +0,0 @@ -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 deleted file mode 100644 index 3562ecab..00000000 --- a/app/tek_cli.rs +++ /dev/null @@ -1,132 +0,0 @@ -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 deleted file mode 100644 index b4af1e3f..00000000 --- a/app/tek_deps.rs +++ /dev/null @@ -1,38 +0,0 @@ -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 deleted file mode 100644 index f304b517..00000000 --- a/app/tek_mode.rs +++ /dev/null @@ -1,58 +0,0 @@ -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 deleted file mode 100644 index 71f604be..00000000 --- a/app/tek_test.rs +++ /dev/null @@ -1,103 +0,0 @@ -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 deleted file mode 100644 index 9198ca0c..00000000 --- a/app/tek_view.rs +++ /dev/null @@ -1,353 +0,0 @@ -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/bacon.toml b/bacon.toml deleted file mode 100644 index 4af91cd0..00000000 --- a/bacon.toml +++ /dev/null @@ -1,64 +0,0 @@ -# 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 deleted file mode 100644 index ec33045e..00000000 --- a/build/Dockerfile.glibc +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index ed350cdc..00000000 --- a/build/Dockerfile.musl +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index 2f70feaa..00000000 --- a/build/README.md +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100755 index 3a22285a..00000000 --- a/build/release-glibc-shell.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/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 deleted file mode 100755 index b7c01fc1..00000000 --- a/build/release-glibc.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/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 deleted file mode 100755 index 8e5c047c..00000000 --- a/build/release-musl-shell.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/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 deleted file mode 100755 index b8526fb5..00000000 --- a/build/release-musl.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/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/deps/suil/Cargo.toml b/crates/suil/Cargo.toml similarity index 74% rename from deps/suil/Cargo.toml rename to crates/suil/Cargo.toml index 7e6211bd..db586670 100644 --- a/deps/suil/Cargo.toml +++ b/crates/suil/Cargo.toml @@ -1,12 +1,11 @@ [package] -name = "tek_suil" +name = "suil-rs" version = "0.1.0" edition = "2021" [dependencies] gtk = "0.18.1" livi = "0.7.4" -#winit = { version = "0.30.4", features = [ "x11" ] } [build-dependencies] bindgen = "0.69.4" diff --git a/deps/suil/build.rs b/crates/suil/build.rs similarity index 100% rename from deps/suil/build.rs rename to crates/suil/build.rs diff --git a/deps/suil/src/bound.rs b/crates/suil/src/bound.rs similarity index 100% rename from deps/suil/src/bound.rs rename to crates/suil/src/bound.rs diff --git a/deps/suil/src/gtk.rs b/crates/suil/src/gtk.rs similarity index 100% rename from deps/suil/src/gtk.rs rename to crates/suil/src/gtk.rs diff --git a/deps/suil/src/lib.rs b/crates/suil/src/lib.rs similarity index 100% rename from deps/suil/src/lib.rs rename to crates/suil/src/lib.rs diff --git a/deps/suil/src/test.rs b/crates/suil/src/test.rs similarity index 100% rename from deps/suil/src/test.rs rename to crates/suil/src/test.rs diff --git a/deps/suil/stdbool.h b/crates/suil/stdbool.h similarity index 100% rename from deps/suil/stdbool.h rename to crates/suil/stdbool.h diff --git a/deps/suil/stdint.h b/crates/suil/stdint.h similarity index 100% rename from deps/suil/stdint.h rename to crates/suil/stdint.h diff --git a/deps/suil/wrapper.h b/crates/suil/wrapper.h similarity index 100% rename from deps/suil/wrapper.h rename to crates/suil/wrapper.h diff --git a/.old/Cargo.toml b/crates/tek.old/Cargo.toml similarity index 100% rename from .old/Cargo.toml rename to crates/tek.old/Cargo.toml diff --git a/.old/README.md b/crates/tek.old/README.md similarity index 100% rename from .old/README.md rename to crates/tek.old/README.md diff --git a/.old/example.edn b/crates/tek.old/example.edn similarity index 100% rename from .old/example.edn rename to crates/tek.old/example.edn diff --git a/.old/src/app.rs b/crates/tek.old/src/app.rs similarity index 100% rename from .old/src/app.rs rename to crates/tek.old/src/app.rs diff --git a/.old/src/app_focus.rs b/crates/tek.old/src/app_focus.rs similarity index 100% rename from .old/src/app_focus.rs rename to crates/tek.old/src/app_focus.rs diff --git a/.old/src/app_paths.rs b/crates/tek.old/src/app_paths.rs similarity index 100% rename from .old/src/app_paths.rs rename to crates/tek.old/src/app_paths.rs diff --git a/.old/src/cli.rs b/crates/tek.old/src/cli.rs similarity index 100% rename from .old/src/cli.rs rename to crates/tek.old/src/cli.rs diff --git a/.old/src/control.rs b/crates/tek.old/src/control.rs similarity index 97% rename from .old/src/control.rs rename to crates/tek.old/src/control.rs index ff45ff11..46bd349f 100644 --- a/.old/src/control.rs +++ b/crates/tek.old/src/control.rs @@ -78,19 +78,19 @@ pub const KEYMAP_GLOBAL: &'static [KeyBinding] = keymap!(App { Ok(true) }], [Char('+'), NONE, "quant_inc", "quantize coarser", |app: &mut App| { - app.transport.quant = Note::next(app.transport.quant); + app.transport.quant = next_note_length(app.transport.quant); Ok(true) }], [Char('_'), NONE, "quant_dec", "quantize finer", |app: &mut App| { - app.transport.quant = Note::prev(app.transport.quant); + app.transport.quant = prev_note_length(app.transport.quant); Ok(true) }], [Char('='), NONE, "zoom_in", "show fewer ticks per block", |app: &mut App| { - app.arranger.sequencer_mut().map(|s|s.time_axis.scale_mut(&Note::prev)); + app.arranger.sequencer_mut().map(|s|s.time_axis.scale_mut(&prev_note_length)); Ok(true) }], [Char('-'), NONE, "zoom_out", "show more ticks per block", |app: &mut App| { - app.arranger.sequencer_mut().map(|s|s.time_axis.scale_mut(&Note::next)); + app.arranger.sequencer_mut().map(|s|s.time_axis.scale_mut(&next_note_length)); Ok(true) }], [Char('x'), NONE, "extend", "double the current clip", |app: &mut App| { diff --git a/.old/src/edn.rs b/crates/tek.old/src/edn.rs similarity index 100% rename from .old/src/edn.rs rename to crates/tek.old/src/edn.rs diff --git a/.old/src/help.rs b/crates/tek.old/src/help.rs similarity index 100% rename from .old/src/help.rs rename to crates/tek.old/src/help.rs diff --git a/.old/src/main.rs b/crates/tek.old/src/main.rs similarity index 100% rename from .old/src/main.rs rename to crates/tek.old/src/main.rs diff --git a/.old/src/setup.rs b/crates/tek.old/src/setup.rs similarity index 100% rename from .old/src/setup.rs rename to crates/tek.old/src/setup.rs diff --git a/crates/tek/Cargo.toml b/crates/tek/Cargo.toml new file mode 100644 index 00000000..2a39075e --- /dev/null +++ b/crates/tek/Cargo.toml @@ -0,0 +1,60 @@ +[package] +name = "tek" +edition = "2021" +version = "0.2.0" + +[dependencies] +#no_deadlocks = "1.3.2" +#vst3 = "0.1.0" +atomic_float = "1.0.0" +backtrace = "0.3.72" +better-panic = "0.3.0" +clap = { version = "4.5.4", features = [ "derive" ] } +clojure-reader = "0.1.0" +crossterm = "0.27" +jack = "0.13" +livi = "0.7.4" +midly = "0.5" +once_cell = "1.19.0" +palette = { version = "0.7.6", features = [ "random" ] } +quanta = "0.12.3" +rand = "0.8.5" +ratatui = { version = "0.26.3", features = [ "unstable-widget-ref", "underline-color" ] } +#suil-rs = { path = "../suil" } +symphonia = { version = "0.5.4", features = [ "all" ] } +toml = "0.8.12" +uuid = { version = "1.10.0", features = [ "v4" ] } +#vst = "0.4.0" +wavers = "1.4.3" +#winit = { version = "0.30.4", features = [ "x11" ] } + +[dev-dependencies] +#tek_app = { version = "0.1.0", path = "../tek_app" } + +[[bin]] +name = "tek_arranger" +path = "src/cli/cli_arranger.rs" + +[[bin]] +name = "tek_sequencer" +path = "src/cli/cli_sequencer.rs" + +[[bin]] +name = "tek_transport" +path = "src/cli/cli_transport.rs" + +#[[bin]] +#name = "tek_mixer" +#path = "src/cli_mixer.rs" + +#[[bin]] +#name = "tek_track" +#path = "src/cli_track.rs" + +#[[bin]] +#name = "tek_sampler" +#path = "src/cli_sampler.rs" + +#[[bin]] +#name = "tek_plugin" +#path = "src/cli_plugin.rs" diff --git a/crates/tek/README.md b/crates/tek/README.md new file mode 100644 index 00000000..0d8195be --- /dev/null +++ b/crates/tek/README.md @@ -0,0 +1,49 @@ + + +# `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/architecture.svg b/crates/tek/architecture.svg similarity index 100% rename from architecture.svg rename to crates/tek/architecture.svg diff --git a/.old/demo.rs.old b/crates/tek/examples/demo.rs similarity index 56% rename from .old/demo.rs.old rename to crates/tek/examples/demo.rs index 6b205580..ba0e022c 100644 --- a/.old/demo.rs.old +++ b/crates/tek/examples/demo.rs @@ -1,4 +1,5 @@ -use tek::*; +use tek_core::*; +use tek_core::jack::*; fn main () -> Usually<()> { Tui::run(Arc::new(RwLock::new(Demo::new())))?; @@ -14,7 +15,20 @@ impl Demo { fn new () -> Self { Self { index: 0, - items: vec![] + 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 + //}), + ] } } } @@ -27,26 +41,26 @@ impl Content for Demo { add(&Background(Color::Rgb(0,128,128)))?; - add(&Margin::XY(1, 1, Stack::down(|add|{ + add(&Outset::XY(1, 1, Stack::down(|add|{ add(&Layers::new(|add|{ add(&Background(Color::Rgb(128,96,0)))?; add(&Border(Square(border_style)))?; - add(&Margin::XY(2, 1, "..."))?; + add(&Outset::XY(2, 1, "..."))?; Ok(()) }).debug())?; add(&Layers::new(|add|{ add(&Background(Color::Rgb(128,64,0)))?; add(&Border(Lozenge(border_style)))?; - add(&Margin::XY(4, 2, "---"))?; + add(&Outset::XY(4, 2, "---"))?; Ok(()) }).debug())?; add(&Layers::new(|add|{ add(&Background(Color::Rgb(96,64,0)))?; add(&Border(SquareBold(border_style)))?; - add(&Margin::XY(6, 3, "~~~"))?; + add(&Outset::XY(6, 3, "~~~"))?; Ok(()) }).debug())?; @@ -56,15 +70,15 @@ impl Content for Demo { Ok(()) })) - //Align::Center(Margin::X(1, Layers::new(|add|{ + //Align::Center(Outset::X(1, Layers::new(|add|{ //add(&Background(Color::Rgb(128,0,0)))?; //add(&Stack::down(|add|{ - //add(&Margin::Y(1, Layers::new(|add|{ + //add(&Outset::Y(1, Layers::new(|add|{ //add(&Background(Color::Rgb(0,128,0)))?; //add(&Align::Center("12345"))?; //add(&Align::Center("FOO")) //})))?; - //add(&Margin::XY(1, 1, Layers::new(|add|{ + //add(&Outset::XY(1, 1, Layers::new(|add|{ //add(&Align::Center("1234567"))?; //add(&Align::Center("BAR"))?; //add(&Background(Color::Rgb(0,0,128))) @@ -74,13 +88,13 @@ impl Content for Demo { //Align::Y(Layers::new(|add|{ //add(&Background(Color::Rgb(128,0,0)))?; - //add(&Margin::X(1, Align::Center(Stack::down(|add|{ - //add(&Align::X(Margin::Y(1, Layers::new(|add|{ + //add(&Outset::X(1, Align::Center(Stack::down(|add|{ + //add(&Align::X(Outset::Y(1, Layers::new(|add|{ //add(&Background(Color::Rgb(0,128,0)))?; //add(&Align::Center("12345"))?; //add(&Align::Center("FOO")) //})))?; - //add(&Margin::XY(1, 1, Layers::new(|add|{ + //add(&Outset::XY(1, 1, Layers::new(|add|{ //add(&Align::Center("1234567"))?; //add(&Align::Center("BAR"))?; //add(&Background(Color::Rgb(0,0,128))) @@ -91,14 +105,13 @@ impl Content for Demo { } } -impl Handle for Demo { - fn handle (&mut self, from: &TuiIn) -> Perhaps { - use KeyCode::{PageUp, PageDown}; +impl Handle for Demo { + fn handle (&mut self, from: &TuiInput) -> Perhaps { match from.event() { - kexp!(PageUp) => { + key!(KeyCode::PageUp) => { self.index = (self.index + 1) % self.items.len(); }, - kexp!(PageDown) => { + key!(KeyCode::PageDown) => { self.index = if self.index > 1 { self.index - 1 } else { @@ -110,3 +123,22 @@ 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/examples/midi_import.rs b/crates/tek/examples/midi_import.rs new file mode 100644 index 00000000..b67d9e03 --- /dev/null +++ b/crates/tek/examples/midi_import.rs @@ -0,0 +1,18 @@ +use tek_api::*; + +struct ExamplePhrases(Vec>>); + +impl HasPhrases for ExamplePhrases { + fn phrases (&self) -> &Vec>> { + &self.0 + } + fn phrases_mut (&mut self) -> &mut Vec>> { + &mut self.0 + } +} + +fn main () -> Usually<()> { + let mut phrases = ExamplePhrases(vec![]); + PhrasePoolCommand::Import(0, String::from("./example.mid")).execute(&mut phrases)?; + Ok(()) +} diff --git a/.old/todo_project1.edn b/crates/tek/examples/mixer.edn similarity index 100% rename from .old/todo_project1.edn rename to crates/tek/examples/mixer.edn diff --git a/.old/todo_project0.edn b/crates/tek/examples/sequencer.edn similarity index 100% rename from .old/todo_project0.edn rename to crates/tek/examples/sequencer.edn diff --git a/crates/tek/src/api.rs b/crates/tek/src/api.rs new file mode 100644 index 00000000..a3c2e9ca --- /dev/null +++ b/crates/tek/src/api.rs @@ -0,0 +1,10 @@ +use crate::*; + +mod phrase; pub(crate) use phrase::*; +mod jack; pub(crate) use self::jack::*; +mod clip; pub(crate) use clip::*; +mod color; pub(crate) use color::*; +mod clock; pub(crate) use clock::*; +mod player; pub(crate) use player::*; +mod scene; pub(crate) use scene::*; +mod track; pub(crate) use track::*; diff --git a/crates/tek/src/api/_todo_api_channel.rs b/crates/tek/src/api/_todo_api_channel.rs new file mode 100644 index 00000000..513a6934 --- /dev/null +++ b/crates/tek/src/api/_todo_api_channel.rs @@ -0,0 +1,83 @@ +use crate::*; + +pub enum MixerTrackCommand {} + +/// A mixer track. +#[derive(Debug)] +pub struct MixerTrack { + pub name: String, + /// Inputs of 1st device + pub audio_ins: Vec>, + /// Outputs of last device + pub audio_outs: Vec>, + /// Device chain + pub devices: Vec>, +} + +//impl MixerTrackDevice for LV2Plugin {} + +impl MixerTrack { + const SYM_NAME: &'static str = ":name"; + const SYM_GAIN: &'static str = ":gain"; + const SYM_SAMPLER: &'static str = "sampler"; + const SYM_LV2: &'static str = "lv2"; + pub fn from_edn <'a, 'e> (jack: &Arc>, args: &[Edn<'e>]) -> Usually { + let mut _gain = 0.0f64; + let mut track = MixerTrack { + name: String::new(), + audio_ins: vec![], + audio_outs: vec![], + devices: vec![], + }; + edn!(edn in args { + Edn::Map(map) => { + if let Some(Edn::Str(n)) = map.get(&Edn::Key(Self::SYM_NAME)) { + track.name = n.to_string(); + } + if let Some(Edn::Double(g)) = map.get(&Edn::Key(Self::SYM_GAIN)) { + _gain = f64::from(*g); + } + }, + Edn::List(args) => match args.get(0) { + // Add a sampler device to the track + Some(Edn::Symbol(Self::SYM_SAMPLER)) => { + track.devices.push( + Box::new(Sampler::from_edn(jack, &args[1..])?) as Box + ); + panic!( + "unsupported in track {}: {:?}; tek_mixer not compiled with feature \"sampler\"", + &track.name, + args.get(0).unwrap() + ) + }, + // Add a LV2 plugin to the track. + Some(Edn::Symbol(Self::SYM_LV2)) => { + track.devices.push( + Box::new(LV2Plugin::from_edn(jack, &args[1..])?) as Box + ); + panic!( + "unsupported in track {}: {:?}; tek_mixer not compiled with feature \"plugin\"", + &track.name, + args.get(0).unwrap() + ) + }, + None => + panic!("empty list track {}", &track.name), + _ => + panic!("unexpected in track {}: {:?}", &track.name, args.get(0).unwrap()) + }, + _ => {} + }); + Ok(track) + } +} + +pub trait MixerTrackDevice: Debug + Send + Sync { + fn boxed (self) -> Box where Self: Sized + 'static { + Box::new(self) + } +} + +impl MixerTrackDevice for Sampler {} + +impl MixerTrackDevice for Plugin {} diff --git a/crates/tek/src/api/_todo_api_mixer.rs b/crates/tek/src/api/_todo_api_mixer.rs new file mode 100644 index 00000000..cd8df774 --- /dev/null +++ b/crates/tek/src/api/_todo_api_mixer.rs @@ -0,0 +1,27 @@ +use crate::*; + +#[derive(Debug)] +pub struct Mixer { + /// JACK client handle (needs to not be dropped for standalone mode to work). + pub jack: Arc>, + pub name: String, + pub tracks: Vec, + pub selected_track: usize, + pub selected_column: usize, +} + +pub struct MixerAudio { + model: Arc> +} + +impl From<&Arc>> for MixerAudio { + fn from (model: &Arc>) -> Self { + Self { model: model.clone() } + } +} + +impl Audio for MixerAudio { + fn process (&mut self, _: &Client, _: &ProcessScope) -> Control { + Control::Continue + } +} diff --git a/crates/tek/src/api/_todo_api_plugin.rs b/crates/tek/src/api/_todo_api_plugin.rs new file mode 100644 index 00000000..3edbac42 --- /dev/null +++ b/crates/tek/src/api/_todo_api_plugin.rs @@ -0,0 +1,114 @@ +use crate::*; + +/// A plugin device. +#[derive(Debug)] +pub struct Plugin { + /// JACK client handle (needs to not be dropped for standalone mode to work). + pub jack: Arc>, + pub name: String, + pub path: Option, + pub plugin: Option, + pub selected: usize, + pub mapping: bool, + pub midi_ins: Vec>, + pub midi_outs: Vec>, + pub audio_ins: Vec>, + pub audio_outs: Vec>, +} +impl Plugin { + pub fn new_lv2 ( + jack: &Arc>, + name: &str, + path: &str, + ) -> Usually { + Ok(Self { + jack: jack.clone(), + name: name.into(), + path: Some(String::from(path)), + plugin: Some(PluginKind::LV2(LV2Plugin::new(path)?)), + selected: 0, + mapping: false, + midi_ins: vec![], + midi_outs: vec![], + audio_ins: vec![], + audio_outs: vec![], + }) + } + + //fn jack_from_lv2 (name: &str, plugin: &::livi::Plugin) -> Usually { + //let counts = plugin.port_counts(); + //let mut jack = Jack::new(name)?; + //for i in 0..counts.atom_sequence_inputs { + //jack = jack.midi_in(&format!("midi-in-{i}")) + //} + //for i in 0..counts.atom_sequence_outputs { + //jack = jack.midi_out(&format!("midi-out-{i}")); + //} + //for i in 0..counts.audio_inputs { + //jack = jack.audio_in(&format!("audio-in-{i}")); + //} + //for i in 0..counts.audio_outputs { + //jack = jack.audio_out(&format!("audio-out-{i}")); + //} + //Ok(jack) + //} +} + +pub struct PluginAudio(Arc>); + +impl From<&Arc>> for PluginAudio { + fn from (model: &Arc>) -> Self { + Self(model.clone()) + } +} + +impl Audio for PluginAudio { + fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { + let state = &mut*self.0.write().unwrap(); + match state.plugin.as_mut() { + Some(PluginKind::LV2(LV2Plugin { + features, + ref mut instance, + ref mut input_buffer, + .. + })) => { + let urid = features.midi_urid(); + input_buffer.clear(); + for port in state.midi_ins.iter() { + let mut atom = ::livi::event::LV2AtomSequence::new( + &features, + scope.n_frames() as usize + ); + for event in port.iter(scope) { + match event.bytes.len() { + 3 => atom.push_midi_event::<3>( + event.time as i64, + urid, + &event.bytes[0..3] + ).unwrap(), + _ => {} + } + } + input_buffer.push(atom); + } + let mut outputs = vec![]; + for _ in state.midi_outs.iter() { + outputs.push(::livi::event::LV2AtomSequence::new( + &features, + scope.n_frames() as usize + )); + } + let ports = ::livi::EmptyPortConnections::new() + .with_atom_sequence_inputs(input_buffer.iter()) + .with_atom_sequence_outputs(outputs.iter_mut()) + .with_audio_inputs(state.audio_ins.iter().map(|o|o.as_slice(scope))) + .with_audio_outputs(state.audio_outs.iter_mut().map(|o|o.as_mut_slice(scope))); + unsafe { + instance.run(scope.n_frames() as usize, ports).unwrap() + }; + }, + _ => {} + } + Control::Continue + } +} diff --git a/crates/tek/src/api/_todo_api_plugin_kind.rs b/crates/tek/src/api/_todo_api_plugin_kind.rs new file mode 100644 index 00000000..0f35ca3a --- /dev/null +++ b/crates/tek/src/api/_todo_api_plugin_kind.rs @@ -0,0 +1,21 @@ +use crate::*; + +/// Supported plugin formats. +#[derive(Default)] +pub enum PluginKind { + #[default] None, + LV2(LV2Plugin), + VST2 { instance: ::vst::host::PluginInstance }, + VST3, +} + +impl Debug for PluginKind { + fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), Error> { + write!(f, "{}", match self { + Self::None => "(none)", + Self::LV2(_) => "LV2", + Self::VST2{..} => "VST2", + Self::VST3 => "VST3", + }) + } +} diff --git a/crates/tek/src/api/_todo_api_plugin_lv2.rs b/crates/tek/src/api/_todo_api_plugin_lv2.rs new file mode 100644 index 00000000..b47c159b --- /dev/null +++ b/crates/tek/src/api/_todo_api_plugin_lv2.rs @@ -0,0 +1,61 @@ +use crate::*; + +/// A LV2 plugin. +#[derive(Debug)] +pub struct LV2Plugin { + pub world: livi::World, + pub instance: livi::Instance, + pub plugin: livi::Plugin, + pub features: Arc, + pub port_list: Vec, + pub input_buffer: Vec, + pub ui_thread: Option>, +} + +impl LV2Plugin { + const INPUT_BUFFER: usize = 1024; + pub fn new (uri: &str) -> Usually { + let world = livi::World::with_load_bundle(&uri); + let features = world + .build_features(livi::FeaturesBuilder { + min_block_length: 1, + max_block_length: 65536, + }); + let plugin = world + .iter_plugins() + .nth(0) + .expect(&format!("plugin not found: {uri}")); + Ok(Self { + instance: unsafe { + plugin + .instantiate(features.clone(), 48000.0) + .expect(&format!("instantiate failed: {uri}")) + }, + port_list: plugin.ports().collect::>(), + input_buffer: Vec::with_capacity(Self::INPUT_BUFFER), + ui_thread: None, + world, + features, + plugin, + }) + } +} + +impl LV2Plugin { + pub fn from_edn <'e> (jack: &Arc>, args: &[Edn<'e>]) -> Usually { + let mut name = String::new(); + let mut path = String::new(); + edn!(edn in args { + Edn::Map(map) => { + if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { + name = String::from(*n); + } + if let Some(Edn::Str(p)) = map.get(&Edn::Key(":path")) { + path = String::from(*p); + } + }, + _ => panic!("unexpected in lv2 '{name}'"), + }); + Plugin::new_lv2(jack, &name, &path) + } +} diff --git a/crates/tek/src/api/_todo_api_sampler.rs b/crates/tek/src/api/_todo_api_sampler.rs new file mode 100644 index 00000000..2976c08a --- /dev/null +++ b/crates/tek/src/api/_todo_api_sampler.rs @@ -0,0 +1,135 @@ +use crate::*; + +/// The sampler plugin plays sounds. +#[derive(Debug)] +pub struct Sampler { + pub jack: Arc>, + pub name: String, + pub mapped: BTreeMap>>, + pub unmapped: Vec>>, + pub voices: Arc>>, + pub midi_in: Port, + pub audio_outs: Vec>, + pub buffer: Vec>, + pub output_gain: f32 +} + +impl Sampler { + pub fn from_edn <'e> (jack: &Arc>, args: &[Edn<'e>]) -> Usually { + let mut name = String::new(); + let mut dir = String::new(); + let mut samples = BTreeMap::new(); + edn!(edn in args { + Edn::Map(map) => { + if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { + name = String::from(*n); + } + if let Some(Edn::Str(n)) = map.get(&Edn::Key(":dir")) { + dir = String::from(*n); + } + }, + Edn::List(args) => match args.get(0) { + Some(Edn::Symbol("sample")) => { + let (midi, sample) = Sample::from_edn(jack, &dir, &args[1..])?; + if let Some(midi) = midi { + samples.insert(midi, sample); + } else { + panic!("sample without midi binding: {}", sample.read().unwrap().name); + } + }, + _ => panic!("unexpected in sampler {name}: {args:?}") + }, + _ => panic!("unexpected in sampler {name}: {edn:?}") + }); + let midi_in = jack.read().unwrap().client().register_port("in", MidiIn::default())?; + Ok(Sampler { + jack: jack.clone(), + name: name.into(), + mapped: samples, + unmapped: Default::default(), + voices: Default::default(), + buffer: Default::default(), + midi_in: midi_in, + audio_outs: vec![], + output_gain: 0. + }) + } +} + +pub struct SamplerAudio { + model: Arc> +} + +impl From<&Arc>> for SamplerAudio { + fn from (model: &Arc>) -> Self { + Self { model: model.clone() } + } +} + +impl Audio for SamplerAudio { + #[inline] fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { + self.process_midi_in(scope); + self.clear_output_buffer(); + self.process_audio_out(scope); + self.write_output_buffer(scope); + Control::Continue + } +} + +impl SamplerAudio { + + /// Create [Voice]s from [Sample]s in response to MIDI input. + pub fn process_midi_in (&mut self, scope: &ProcessScope) { + let Sampler { midi_in, mapped, voices, .. } = &*self.model.read().unwrap(); + for RawMidi { time, bytes } in midi_in.iter(scope) { + if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { + if let MidiMessage::NoteOn { ref key, ref vel } = message { + if let Some(sample) = mapped.get(key) { + voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); + } + } + } + } + } + + /// Zero the output buffer. + pub fn clear_output_buffer (&mut self) { + for buffer in self.model.write().unwrap().buffer.iter_mut() { + buffer.fill(0.0); + } + } + + /// Mix all currently playing samples into the output. + pub fn process_audio_out (&mut self, scope: &ProcessScope) { + let Sampler { ref mut buffer, voices, output_gain, .. } = &mut*self.model.write().unwrap(); + let channel_count = buffer.len(); + voices.write().unwrap().retain_mut(|voice|{ + for index in 0..scope.n_frames() as usize { + if let Some(frame) = voice.next() { + for (channel, sample) in frame.iter().enumerate() { + // Averaging mixer: + //self.buffer[channel % channel_count][index] = ( + //(self.buffer[channel % channel_count][index] + sample * self.output_gain) / 2.0 + //); + buffer[channel % channel_count][index] += sample * *output_gain; + } + } else { + return false + } + } + return true + }); + } + + /// Write output buffer to output ports. + pub fn write_output_buffer (&mut self, scope: &ProcessScope) { + let Sampler { ref mut audio_outs, buffer, .. } = &mut*self.model.write().unwrap(); + for (i, port) in audio_outs.iter_mut().enumerate() { + let buffer = &buffer[i]; + for (i, value) in port.as_mut_slice(scope).iter_mut().enumerate() { + *value = *buffer.get(i).unwrap_or(&0.0); + } + } + } + +} diff --git a/crates/tek/src/api/_todo_api_sampler_sample.rs b/crates/tek/src/api/_todo_api_sampler_sample.rs new file mode 100644 index 00000000..aa85676e --- /dev/null +++ b/crates/tek/src/api/_todo_api_sampler_sample.rs @@ -0,0 +1,72 @@ +use crate::*; + +/// A sound sample. +#[derive(Default, Debug)] +pub struct Sample { + pub name: String, + pub start: usize, + pub end: usize, + pub channels: Vec>, + pub rate: Option, +} + +impl Sample { + pub fn new (name: &str, start: usize, end: usize, channels: Vec>) -> Self { + Self { name: name.to_string(), start, end, channels, rate: None } + } + pub fn play (sample: &Arc>, after: usize, velocity: &u7) -> Voice { + Voice { + sample: sample.clone(), + after, + position: sample.read().unwrap().start, + velocity: velocity.as_int() as f32 / 127.0, + } + } + pub fn from_edn <'e> (jack: &Arc>, dir: &str, args: &[Edn<'e>]) -> Usually<(Option, Arc>)> { + let mut name = String::new(); + let mut file = String::new(); + let mut midi = None; + let mut start = 0usize; + edn!(edn in args { + Edn::Map(map) => { + if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { + name = String::from(*n); + } + if let Some(Edn::Str(f)) = map.get(&Edn::Key(":file")) { + file = String::from(*f); + } + if let Some(Edn::Int(i)) = map.get(&Edn::Key(":start")) { + start = *i as usize; + } + if let Some(Edn::Int(m)) = map.get(&Edn::Key(":midi")) { + midi = Some(u7::from(*m as u8)); + } + }, + _ => panic!("unexpected in sample {name}"), + }); + let (end, data) = Sample::read_data(&format!("{dir}/{file}"))?; + Ok((midi, Arc::new(RwLock::new(Self { + name: name.into(), + start, + end, + channels: data, + rate: None + })))) + } + + /// Read WAV from file + pub fn read_data (src: &str) -> Usually<(usize, Vec>)> { + let mut channels: Vec> = vec![]; + for channel in wavers::Wav::from_path(src)?.channels() { + channels.push(channel); + } + let mut end = 0; + let mut data: Vec> = vec![]; + for samples in channels.iter() { + let channel = Vec::from(samples.as_ref()); + end = end.max(channel.len()); + data.push(channel); + } + Ok((end, data)) + } +} diff --git a/crates/tek/src/api/_todo_api_sampler_voice.rs b/crates/tek/src/api/_todo_api_sampler_voice.rs new file mode 100644 index 00000000..1dd3ba4a --- /dev/null +++ b/crates/tek/src/api/_todo_api_sampler_voice.rs @@ -0,0 +1,30 @@ +use crate::*; + +/// A currently playing instance of a sample. +#[derive(Default, Debug, Clone)] +pub struct Voice { + pub sample: Arc>, + pub after: usize, + pub position: usize, + pub velocity: f32, +} + +impl Iterator for Voice { + type Item = [f32;2]; + fn next (&mut self) -> Option { + if self.after > 0 { + self.after = self.after - 1; + return Some([0.0, 0.0]) + } + let sample = self.sample.read().unwrap(); + if self.position < sample.end { + let position = self.position; + self.position = self.position + 1; + return sample.channels[0].get(position).map(|_amplitude|[ + sample.channels[0][position] * self.velocity, + sample.channels[0][position] * self.velocity, + ]) + } + None + } +} diff --git a/crates/tek/src/api/clip.rs b/crates/tek/src/api/clip.rs new file mode 100644 index 00000000..fe8781ce --- /dev/null +++ b/crates/tek/src/api/clip.rs @@ -0,0 +1,20 @@ +use crate::*; + +#[derive(Clone, Debug)] +pub enum ArrangerClipCommand { + Play, + Get(usize, usize), + Set(usize, usize, Option>>), + Edit(Option>>), + SetLoop(bool), + RandomColor, +} + +//impl Command for ArrangerClipCommand { + //fn execute (self, state: &mut T) -> Perhaps { + //match self { + //_ => todo!() + //} + //Ok(None) + //} +//} diff --git a/crates/tek/src/api/clock.rs b/crates/tek/src/api/clock.rs new file mode 100644 index 00000000..8e6e8574 --- /dev/null +++ b/crates/tek/src/api/clock.rs @@ -0,0 +1,198 @@ +use crate::*; + +pub trait HasClock: Send + Sync { + fn clock (&self) -> &ClockModel; +} + +#[derive(Clone, Debug, PartialEq)] +pub enum ClockCommand { + Play(Option), + Pause(Option), + SeekUsec(f64), + SeekSample(f64), + SeekPulse(f64), + SetBpm(f64), + SetQuant(f64), + SetSync(f64), +} + +impl Command for ClockCommand { + fn execute (self, state: &mut T) -> Perhaps { + use ClockCommand::*; + match self { + Play(start) => state.clock().play_from(start)?, + Pause(pause) => state.clock().pause_at(pause)?, + SeekUsec(usec) => state.clock().playhead.update_from_usec(usec), + SeekSample(sample) => state.clock().playhead.update_from_sample(sample), + SeekPulse(pulse) => state.clock().playhead.update_from_pulse(pulse), + SetBpm(bpm) => return Ok(Some(SetBpm(state.clock().timebase().bpm.set(bpm)))), + SetQuant(quant) => return Ok(Some(SetQuant(state.clock().quant.set(quant)))), + SetSync(sync) => return Ok(Some(SetSync(state.clock().sync.set(sync)))), + }; + Ok(None) + } +} + +#[derive(Clone)] +pub struct Timeline { + pub timebase: Arc, + pub started: Arc>>, + pub loopback: Arc>>, +} + +impl Default for Timeline { + fn default () -> Self { + Self { + timebase: Arc::new(Timebase::default()), + started: RwLock::new(None).into(), + loopback: RwLock::new(None).into(), + } + } +} + +#[derive(Clone)] +pub struct ClockModel { + /// JACK transport handle. + pub transport: Arc, + /// Global temporal resolution (shared by [Moment] fields) + pub timebase: Arc, + /// Current global sample and usec (monotonic from JACK clock) + pub global: Arc, + /// Global sample and usec at which playback started + pub started: Arc>>, + /// Current playhead position + pub playhead: Arc, + /// Note quantization factor + pub quant: Arc, + /// Launch quantization factor + pub sync: Arc, + /// Size of buffer in samples + pub chunk: Arc, +} + +impl From<&Arc>> for ClockModel { + fn from (jack: &Arc>) -> Self { + let jack = jack.read().unwrap(); + let chunk = jack.client().buffer_size(); + let transport = jack.client().transport(); + let timebase = Arc::new(Timebase::default()); + Self { + quant: Arc::new(24.into()), + sync: Arc::new(384.into()), + transport: Arc::new(transport), + chunk: Arc::new((chunk as usize).into()), + global: Arc::new(Moment::zero(&timebase)), + playhead: Arc::new(Moment::zero(&timebase)), + started: RwLock::new(None).into(), + timebase, + } + } +} + +impl std::fmt::Debug for ClockModel { + fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("ClockModel") + .field("timebase", &self.timebase) + .field("chunk", &self.chunk) + .field("quant", &self.quant) + .field("sync", &self.sync) + .field("global", &self.global) + .field("playhead", &self.playhead) + .field("started", &self.started) + .finish() + } +} + +impl ClockModel { + pub fn timebase (&self) -> &Arc { + &self.timebase + } + /// Current sample rate + pub fn sr (&self) -> &SampleRate { + &self.timebase.sr + } + /// Current tempo + pub fn bpm (&self) -> &BeatsPerMinute { + &self.timebase.bpm + } + /// Current MIDI resolution + pub fn ppq (&self) -> &PulsesPerQuaver { + &self.timebase.ppq + } + /// Next pulse that matches launch sync (for phrase switchover) + pub fn next_launch_pulse (&self) -> usize { + let sync = self.sync.get() as usize; + let pulse = self.playhead.pulse.get() as usize; + if pulse % sync == 0 { + pulse + } else { + (pulse / sync + 1) * sync + } + } + /// Start playing, optionally seeking to a given location beforehand + pub fn play_from (&self, start: Option) -> Usually<()> { + if let Some(start) = start { + self.transport.locate(start)?; + } + self.transport.start()?; + Ok(()) + } + /// Pause, optionally seeking to a given location afterwards + pub fn pause_at (&self, pause: Option) -> Usually<()> { + self.transport.stop()?; + if let Some(pause) = pause { + self.transport.locate(pause)?; + } + Ok(()) + } + /// Is currently paused? + pub fn is_stopped (&self) -> bool { + self.started.read().unwrap().is_none() + } + /// Is currently playing? + pub fn is_rolling (&self) -> bool { + self.started.read().unwrap().is_some() + } + /// Update chunk size + pub fn set_chunk (&self, n_frames: usize) { + self.chunk.store(n_frames, Ordering::Relaxed); + } + pub fn update_from_scope (&self, scope: &ProcessScope) -> Usually<()> { + self.set_chunk(scope.n_frames() as usize); + let CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?; + self.global.sample.set(current_frames as f64); + self.global.usec.set(current_usecs as f64); + let mut started = self.started.write().unwrap(); + match self.transport.query_state()? { + TransportState::Rolling => { + if started.is_none() { + let moment = Moment::zero(&self.timebase); + moment.sample.set(current_frames as f64); + moment.usec.set(current_usecs as f64); + *started = Some(moment); + } + }, + TransportState::Stopped => { + if started.is_some() { + *started = None; + } + }, + _ => {} + }; + self.playhead.update_from_sample(match *started { + Some(ref instant) => current_frames as f64 - instant.sample.get(), + None => 0. + }); + Ok(()) + } +} + +/// Hosts the JACK callback for updating the temporal pointer and playback status. +pub struct ClockAudio<'a, T: HasClock>(pub &'a mut T); + +impl<'a, T: HasClock> Audio for ClockAudio<'a, T> { + #[inline] fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { + self.0.clock().update_from_scope(scope).unwrap(); + Control::Continue + } +} diff --git a/crates/tek/src/api/color.rs b/crates/tek/src/api/color.rs new file mode 100644 index 00000000..c23e6ba7 --- /dev/null +++ b/crates/tek/src/api/color.rs @@ -0,0 +1,6 @@ +use crate::*; + +pub trait HasColor { + fn color (&self) -> ItemColor; + fn color_mut (&self) -> &mut ItemColor; +} diff --git a/crates/tek/src/api/jack.rs b/crates/tek/src/api/jack.rs new file mode 100644 index 00000000..97ba47d8 --- /dev/null +++ b/crates/tek/src/api/jack.rs @@ -0,0 +1,463 @@ +use crate::*; + +pub trait JackApi { + fn jack (&self) -> &Arc>; +} + +pub trait HasMidiIns { + fn midi_ins (&self) -> &Vec>; + fn midi_ins_mut (&mut self) -> &mut Vec>; + fn has_midi_ins (&self) -> bool { + self.midi_ins().len() > 0 + } +} + +pub trait HasMidiOuts { + fn midi_outs (&self) -> &Vec>; + fn midi_outs_mut (&mut self) -> &mut Vec>; + fn midi_note (&mut self) -> &mut Vec; + fn has_midi_outs (&self) -> bool { + self.midi_outs().len() > 0 + } +} + +//////////////////////////////////////////////////////////////////////////////////// + +pub trait JackActivate: Sized { + fn activate_with ( + self, + init: impl FnOnce(&Arc>)->Usually + ) + -> Usually>>; +} + +impl JackActivate for JackClient { + fn activate_with ( + self, + init: impl FnOnce(&Arc>)->Usually + ) + -> Usually>> + { + let client = Arc::new(RwLock::new(self)); + let target = Arc::new(RwLock::new(init(&client)?)); + let event = Box::new(move|_|{/*TODO*/}) as Box; + let events = Notifications(event); + let frame = Box::new({ + let target = target.clone(); + move|c: &_, s: &_|if let Ok(mut target) = target.write() { + target.process(c, s) + } else { + Control::Quit + } + }); + let frames = ClosureProcessHandler::new(frame as BoxedAudioHandler); + let mut buffer = Self::Activating; + std::mem::swap(&mut*client.write().unwrap(), &mut buffer); + *client.write().unwrap() = Self::Active(Client::from(buffer).activate_async(events, frames)?); + Ok(target) + } +} + +/// Trait for things that have a JACK process callback. +pub trait Audio: Send + Sync { + fn process(&mut self, _: &Client, _: &ProcessScope) -> Control { + Control::Continue + } + fn callback( + state: &Arc>, client: &Client, scope: &ProcessScope + ) -> Control where Self: Sized { + if let Ok(mut state) = state.write() { + state.process(client, scope) + } else { + Control::Quit + } + } +} + +/// A UI component that may be associated with a JACK client by the `Jack` factory. +pub trait AudioComponent: Component + Audio { + /// Perform type erasure for collecting heterogeneous devices. + fn boxed(self) -> Box> + where + Self: Sized + 'static, + { + Box::new(self) + } +} + +/// All things that implement the required traits can be treated as `AudioComponent`. +impl + Audio> AudioComponent for W {} + +/// Trait for things that may expose JACK ports. +pub trait Ports { + fn audio_ins(&self) -> Usually>> { + Ok(vec![]) + } + fn audio_outs(&self) -> Usually>> { + Ok(vec![]) + } + fn midi_ins(&self) -> Usually>> { + Ok(vec![]) + } + fn midi_outs(&self) -> Usually>> { + Ok(vec![]) + } +} + +fn register_ports( + client: &Client, + names: Vec, + spec: T, +) -> Usually>> { + names + .into_iter() + .try_fold(BTreeMap::new(), |mut ports, name| { + let port = client.register_port(&name, spec)?; + ports.insert(name, port); + Ok(ports) + }) +} + +fn query_ports(client: &Client, names: Vec) -> BTreeMap> { + names.into_iter().fold(BTreeMap::new(), |mut ports, name| { + let port = client.port_by_name(&name).unwrap(); + ports.insert(name, port); + ports + }) +} + +///// A [AudioComponent] bound to a JACK client and a set of ports. +//pub struct JackDevice { + ///// The active JACK client of this device. + //pub client: DynamicAsyncClient, + ///// The device state, encapsulated for sharing between threads. + //pub state: Arc>>>, + ///// Unowned copies of the device's JACK ports, for connecting to the device. + ///// The "real" readable/writable `Port`s are owned by the `state`. + //pub ports: UnownedJackPorts, +//} + +//impl std::fmt::Debug for JackDevice { + //fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + //f.debug_struct("JackDevice") + //.field("ports", &self.ports) + //.finish() + //} +//} + +//impl Render for JackDevice { + //type Engine = E; + //fn min_size(&self, to: E::Size) -> Perhaps { + //self.state.read().unwrap().layout(to) + //} + //fn render(&self, to: &mut E::Output) -> Usually<()> { + //self.state.read().unwrap().render(to) + //} +//} + +//impl Handle for JackDevice { + //fn handle(&mut self, from: &E::Input) -> Perhaps { + //self.state.write().unwrap().handle(from) + //} +//} + +//impl Ports for JackDevice { + //fn audio_ins(&self) -> Usually>> { + //Ok(self.ports.audio_ins.values().collect()) + //} + //fn audio_outs(&self) -> Usually>> { + //Ok(self.ports.audio_outs.values().collect()) + //} + //fn midi_ins(&self) -> Usually>> { + //Ok(self.ports.midi_ins.values().collect()) + //} + //fn midi_outs(&self) -> Usually>> { + //Ok(self.ports.midi_outs.values().collect()) + //} +//} + +//impl JackDevice { + ///// Returns a locked mutex of the state's contents. + //pub fn state(&self) -> LockResult>>> { + //self.state.read() + //} + ///// Returns a locked mutex of the state's contents. + //pub fn state_mut(&self) -> LockResult>>> { + //self.state.write() + //} + //pub fn connect_midi_in(&self, index: usize, port: &Port) -> Usually<()> { + //Ok(self + //.client + //.as_client() + //.connect_ports(port, self.midi_ins()?[index])?) + //} + //pub fn connect_midi_out(&self, index: usize, port: &Port) -> Usually<()> { + //Ok(self + //.client + //.as_client() + //.connect_ports(self.midi_outs()?[index], port)?) + //} + //pub fn connect_audio_in(&self, index: usize, port: &Port) -> Usually<()> { + //Ok(self + //.client + //.as_client() + //.connect_ports(port, self.audio_ins()?[index])?) + //} + //pub fn connect_audio_out(&self, index: usize, port: &Port) -> Usually<()> { + //Ok(self + //.client + //.as_client() + //.connect_ports(self.audio_outs()?[index], port)?) + //} +//} + +///// Collection of JACK ports as [AudioIn]/[AudioOut]/[MidiIn]/[MidiOut]. +//#[derive(Default, Debug)] +//pub struct JackPorts { + //pub audio_ins: BTreeMap>, + //pub midi_ins: BTreeMap>, + //pub audio_outs: BTreeMap>, + //pub midi_outs: BTreeMap>, +//} + +///// Collection of JACK ports as [Unowned]. +//#[derive(Default, Debug)] +//pub struct UnownedJackPorts { + //pub audio_ins: BTreeMap>, + //pub midi_ins: BTreeMap>, + //pub audio_outs: BTreeMap>, + //pub midi_outs: BTreeMap>, +//} + +//impl JackPorts { + //pub fn clone_unowned(&self) -> UnownedJackPorts { + //let mut unowned = UnownedJackPorts::default(); + //for (name, port) in self.midi_ins.iter() { + //unowned.midi_ins.insert(name.clone(), port.clone_unowned()); + //} + //for (name, port) in self.midi_outs.iter() { + //unowned.midi_outs.insert(name.clone(), port.clone_unowned()); + //} + //for (name, port) in self.audio_ins.iter() { + //unowned.audio_ins.insert(name.clone(), port.clone_unowned()); + //} + //for (name, port) in self.audio_outs.iter() { + //unowned + //.audio_outs + //.insert(name.clone(), port.clone_unowned()); + //} + //unowned + //} +//} + +///// Implement the `Ports` trait. +//#[macro_export] +//macro_rules! ports { + //($T:ty $({ $(audio: { + //$(ins: |$ai_arg:ident|$ai_impl:expr,)? + //$(outs: |$ao_arg:ident|$ao_impl:expr,)? + //})? $(midi: { + //$(ins: |$mi_arg:ident|$mi_impl:expr,)? + //$(outs: |$mo_arg:ident|$mo_impl:expr,)? + //})?})?) => { + //impl Ports for $T {$( + //$( + //$(fn audio_ins <'a> (&'a self) -> Usually>> { + //let cb = |$ai_arg:&'a Self|$ai_impl; + //cb(self) + //})? + //)? + //$( + //$(fn audio_outs <'a> (&'a self) -> Usually>> { + //let cb = (|$ao_arg:&'a Self|$ao_impl); + //cb(self) + //})? + //)? + //)? $( + //$( + //$(fn midi_ins <'a> (&'a self) -> Usually>> { + //let cb = (|$mi_arg:&'a Self|$mi_impl); + //cb(self) + //})? + //)? + //$( + //$(fn midi_outs <'a> (&'a self) -> Usually>> { + //let cb = (|$mo_arg:&'a Self|$mo_impl); + //cb(self) + //})? + //)? + //)?} + //}; +//} + +///// `JackDevice` factory. Creates JACK `Client`s, performs port registration +///// and activation, and encapsulates a `AudioComponent` into a `JackDevice`. +//pub struct Jack { + //pub client: Client, + //pub midi_ins: Vec, + //pub audio_ins: Vec, + //pub midi_outs: Vec, + //pub audio_outs: Vec, +//} + +//impl Jack { + //pub fn new(name: &str) -> Usually { + //Ok(Self { + //midi_ins: vec![], + //audio_ins: vec![], + //midi_outs: vec![], + //audio_outs: vec![], + //client: Client::new(name, ClientOptions::NO_START_SERVER)?.0, + //}) + //} + //pub fn run<'a: 'static, D, E>( + //self, + //state: impl FnOnce(JackPorts) -> Box, + //) -> Usually> + //where + //D: AudioComponent + Sized + 'static, + //E: Engine + 'static, + //{ + //let owned_ports = JackPorts { + //audio_ins: register_ports(&self.client, self.audio_ins, AudioIn::default())?, + //audio_outs: register_ports(&self.client, self.audio_outs, AudioOut::default())?, + //midi_ins: register_ports(&self.client, self.midi_ins, MidiIn::default())?, + //midi_outs: register_ports(&self.client, self.midi_outs, MidiOut::default())?, + //}; + //let midi_outs = owned_ports + //.midi_outs + //.values() + //.map(|p| Ok(p.name()?)) + //.collect::>>()?; + //let midi_ins = owned_ports + //.midi_ins + //.values() + //.map(|p| Ok(p.name()?)) + //.collect::>>()?; + //let audio_outs = owned_ports + //.audio_outs + //.values() + //.map(|p| Ok(p.name()?)) + //.collect::>>()?; + //let audio_ins = owned_ports + //.audio_ins + //.values() + //.map(|p| Ok(p.name()?)) + //.collect::>>()?; + //let state = Arc::new(RwLock::new(state(owned_ports) as Box>)); + //let client = self.client.activate_async( + //Notifications(Box::new({ + //let _state = state.clone(); + //move |_event| { + //// FIXME: this deadlocks + ////state.lock().unwrap().handle(&event).unwrap(); + //} + //}) as Box), + //contrib::ClosureProcessHandler::new(Box::new({ + //let state = state.clone(); + //move |c: &Client, s: &ProcessScope| state.write().unwrap().process(c, s) + //}) as BoxedAudioHandler), + //)?; + //Ok(JackDevice { + //ports: UnownedJackPorts { + //audio_ins: query_ports(&client.as_client(), audio_ins), + //audio_outs: query_ports(&client.as_client(), audio_outs), + //midi_ins: query_ports(&client.as_client(), midi_ins), + //midi_outs: query_ports(&client.as_client(), midi_outs), + //}, + //client, + //state, + //}) + //} + //pub fn audio_in(mut self, name: &str) -> Self { + //self.audio_ins.push(name.to_string()); + //self + //} + //pub fn audio_out(mut self, name: &str) -> Self { + //self.audio_outs.push(name.to_string()); + //self + //} + //pub fn midi_in(mut self, name: &str) -> Self { + //self.midi_ins.push(name.to_string()); + //self + //} + //pub fn midi_out(mut self, name: &str) -> Self { + //self.midi_outs.push(name.to_string()); + //self + //} +//} + +//impl Command for ArrangerSceneCommand { +//} + //Edit(phrase) => { state.state.phrase = phrase.clone() }, + //ToggleViewMode => { state.state.mode.to_next(); }, + //Delete => { state.state.delete(); }, + //Activate => { state.state.activate(); }, + //ZoomIn => { state.state.zoom_in(); }, + //ZoomOut => { state.state.zoom_out(); }, + //MoveBack => { state.state.move_back(); }, + //MoveForward => { state.state.move_forward(); }, + //RandomColor => { state.state.randomize_color(); }, + //Put => { state.state.phrase_put(); }, + //Get => { state.state.phrase_get(); }, + //AddScene => { state.state.scene_add(None, None)?; }, + //AddTrack => { state.state.track_add(None, None)?; }, + //ToggleLoop => { state.state.toggle_loop() }, + //pub fn zoom_in (&mut self) { + //if let ArrangerEditorMode::Vertical(factor) = self.mode { + //self.mode = ArrangerEditorMode::Vertical(factor + 1) + //} + //} + //pub fn zoom_out (&mut self) { + //if let ArrangerEditorMode::Vertical(factor) = self.mode { + //self.mode = ArrangerEditorMode::Vertical(factor.saturating_sub(1)) + //} + //} + //pub fn move_back (&mut self) { + //match self.selected { + //ArrangerEditorFocus::Scene(s) => { + //if s > 0 { + //self.scenes.swap(s, s - 1); + //self.selected = ArrangerEditorFocus::Scene(s - 1); + //} + //}, + //ArrangerEditorFocus::Track(t) => { + //if t > 0 { + //self.tracks.swap(t, t - 1); + //self.selected = ArrangerEditorFocus::Track(t - 1); + //// FIXME: also swap clip order in scenes + //} + //}, + //_ => todo!("arrangement: move forward") + //} + //} + //pub fn move_forward (&mut self) { + //match self.selected { + //ArrangerEditorFocus::Scene(s) => { + //if s < self.scenes.len().saturating_sub(1) { + //self.scenes.swap(s, s + 1); + //self.selected = ArrangerEditorFocus::Scene(s + 1); + //} + //}, + //ArrangerEditorFocus::Track(t) => { + //if t < self.tracks.len().saturating_sub(1) { + //self.tracks.swap(t, t + 1); + //self.selected = ArrangerEditorFocus::Track(t + 1); + //// FIXME: also swap clip order in scenes + //} + //}, + //_ => todo!("arrangement: move forward") + //} + //} + +//impl From for Clock { + //fn from (current: Moment) -> Self { + //Self { + //playing: Some(TransportState::Stopped).into(), + //started: None.into(), + //quant: 24.into(), + //sync: (current.timebase.ppq.get() * 4.).into(), + //current, + //} + //} +//} diff --git a/device/clap.rs b/crates/tek/src/api/name.rs similarity index 100% rename from device/clap.rs rename to crates/tek/src/api/name.rs diff --git a/crates/tek/src/api/phrase.rs b/crates/tek/src/api/phrase.rs new file mode 100644 index 00000000..7125856a --- /dev/null +++ b/crates/tek/src/api/phrase.rs @@ -0,0 +1,172 @@ +use crate::*; + +pub trait HasPhrases { + fn phrases (&self) -> &Vec>>; + fn phrases_mut (&mut self) -> &mut Vec>>; +} + +#[derive(Clone, Debug, PartialEq)] +pub enum PhrasePoolCommand { + Add(usize, Phrase), + Delete(usize), + Swap(usize, usize), + Import(usize, PathBuf), + Export(usize, PathBuf), + SetName(usize, String), + SetLength(usize, usize), + SetColor(usize, ItemColor), +} + +impl Command for PhrasePoolCommand { + fn execute (self, model: &mut T) -> Perhaps { + use PhrasePoolCommand::*; + Ok(match self { + Add(mut index, phrase) => { + let phrase = Arc::new(RwLock::new(phrase)); + let phrases = model.phrases_mut(); + if index >= phrases.len() { + index = phrases.len(); + phrases.push(phrase) + } else { + phrases.insert(index, phrase); + } + Some(Self::Delete(index)) + }, + Delete(index) => { + let phrase = model.phrases_mut().remove(index).read().unwrap().clone(); + Some(Self::Add(index, phrase)) + }, + Swap(index, other) => { + model.phrases_mut().swap(index, other); + Some(Self::Swap(index, other)) + }, + Import(index, path) => { + let bytes = std::fs::read(&path)?; + let smf = Smf::parse(bytes.as_slice())?; + let mut t = 0u32; + let mut events = vec![]; + for track in smf.tracks.iter() { + for event in track.iter() { + t += event.delta.as_int(); + if let TrackEventKind::Midi { channel, message } = event.kind { + events.push((t, channel.as_int(), message)); + } + } + } + let mut phrase = Phrase::new("imported", true, t as usize + 1, None, None); + for event in events.iter() { + phrase.notes[event.0 as usize].push(event.2); + } + Self::Add(index, phrase).execute(model)? + }, + Export(_index, _path) => { + todo!("export phrase to midi file"); + }, + SetName(index, name) => { + let mut phrase = model.phrases()[index].write().unwrap(); + let old_name = phrase.name.clone(); + phrase.name = name; + Some(Self::SetName(index, old_name)) + }, + SetLength(index, length) => { + let mut phrase = model.phrases()[index].write().unwrap(); + let old_len = phrase.length; + phrase.length = length; + Some(Self::SetLength(index, old_len)) + }, + SetColor(index, color) => { + let mut color = ItemColorTriplet::from(color); + std::mem::swap(&mut color, &mut model.phrases()[index].write().unwrap().color); + Some(Self::SetColor(index, color.base)) + }, + }) + } +} + +/// A MIDI sequence. +#[derive(Debug, Clone)] +pub struct Phrase { + pub uuid: uuid::Uuid, + /// Name of phrase + pub name: String, + /// Temporal resolution in pulses per quarter note + pub ppq: usize, + /// Length of phrase in pulses + pub length: usize, + /// Notes in phrase + pub notes: PhraseData, + /// Whether to loop the phrase or play it once + pub loop_on: bool, + /// Start of loop + pub loop_start: usize, + /// Length of loop + pub loop_length: usize, + /// All notes are displayed with minimum length + pub percussive: bool, + /// Identifying color of phrase + pub color: ItemColorTriplet, +} + +/// MIDI message structural +pub type PhraseData = Vec>; + +impl Phrase { + pub fn new ( + name: impl AsRef, + loop_on: bool, + length: usize, + notes: Option, + color: Option, + ) -> Self { + Self { + uuid: uuid::Uuid::new_v4(), + name: name.as_ref().to_string(), + ppq: PPQ, + length, + notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]), + loop_on, + loop_start: 0, + loop_length: length, + percussive: true, + color: color.unwrap_or_else(ItemColorTriplet::random) + } + } + pub fn set_length (&mut self, length: usize) { + self.length = length; + self.notes = vec![Vec::with_capacity(16);length]; + } + pub fn duplicate (&self) -> Self { + let mut clone = self.clone(); + clone.uuid = uuid::Uuid::new_v4(); + clone + } + pub fn toggle_loop (&mut self) { self.loop_on = !self.loop_on; } + pub fn record_event (&mut self, pulse: usize, message: MidiMessage) { + if pulse >= self.length { panic!("extend phrase first") } + self.notes[pulse].push(message); + } + /// Check if a range `start..end` contains MIDI Note On `k` + pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool { + //panic!("{:?} {start} {end}", &self); + for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() { + for event in events.iter() { + if let MidiMessage::NoteOn {key,..} = event { if *key == k { return true } } + } + } + return false + } +} + +impl Default for Phrase { + fn default () -> Self { + Self::new("(empty)", false, 0, None, Some(ItemColor::from(Color::Rgb(0, 0, 0)).into())) + } +} + +impl PartialEq for Phrase { + fn eq (&self, other: &Self) -> bool { + self.uuid == other.uuid + } +} + +impl Eq for Phrase {} diff --git a/crates/tek/src/api/player.rs b/crates/tek/src/api/player.rs new file mode 100644 index 00000000..f728e3b8 --- /dev/null +++ b/crates/tek/src/api/player.rs @@ -0,0 +1,362 @@ +use crate::*; + +pub trait HasPlayer { + fn player (&self) -> &impl MidiPlayerApi; + fn player_mut (&mut self) -> &mut impl MidiPlayerApi; +} + +pub trait MidiPlayerApi: MidiRecordApi + MidiPlaybackApi + Send + Sync {} + +pub trait HasPlayPhrase: HasClock { + fn reset (&self) -> bool; + fn reset_mut (&mut self) -> &mut bool; + fn play_phrase (&self) -> &Option<(Moment, Option>>)>; + fn play_phrase_mut (&mut self) -> &mut Option<(Moment, Option>>)>; + fn next_phrase (&self) -> &Option<(Moment, Option>>)>; + fn next_phrase_mut (&mut self) -> &mut Option<(Moment, Option>>)>; + fn pulses_since_start (&self) -> Option { + if let Some((started, Some(_))) = self.play_phrase().as_ref() { + Some(self.clock().playhead.pulse.get() - started.pulse.get()) + } else { + None + } + } + fn enqueue_next (&mut self, phrase: Option<&Arc>>) { + let start = self.clock().next_launch_pulse() as f64; + let instant = Moment::from_pulse(&self.clock().timebase(), start); + let phrase = phrase.map(|p|p.clone()); + *self.next_phrase_mut() = Some((instant, phrase)); + *self.reset_mut() = true; + } +} + +pub trait MidiRecordApi: HasClock + HasPlayPhrase + HasMidiIns { + fn notes_in (&self) -> &Arc>; + + fn recording (&self) -> bool; + fn recording_mut (&mut self) -> &mut bool; + fn toggle_record (&mut self) { + *self.recording_mut() = !self.recording(); + } + fn record (&mut self, scope: &ProcessScope, midi_buf: &mut Vec>>) { + let sample0 = scope.last_frame_time() as usize; + // For highlighting keys and note repeat + let notes_in = self.notes_in().clone(); + if self.clock().is_rolling() { + if let Some((started, ref phrase)) = self.play_phrase().clone() { + let start = started.sample.get() as usize; + let quant = self.clock().quant.get(); + let timebase = self.clock().timebase().clone(); + let monitoring = self.monitoring(); + let recording = self.recording(); + for input in self.midi_ins_mut().iter() { + for (sample, event, bytes) in parse_midi_input(input.iter(scope)) { + if let LiveEvent::Midi { message, .. } = event { + if monitoring { + midi_buf[sample].push(bytes.to_vec()) + } + if recording { + if let Some(phrase) = phrase { + let mut phrase = phrase.write().unwrap(); + let length = phrase.length; + phrase.record_event({ + let sample = (sample0 + sample - start) as f64; + let pulse = timebase.samples_to_pulse(sample); + let quantized = (pulse / quant).round() * quant; + let looped = quantized as usize % length; + looped + }, message); + } + } + update_keys(&mut*notes_in.write().unwrap(), &message); + } + } + } + } + if let Some((start_at, phrase)) = &self.next_phrase() { + // TODO switch to next phrase and record into it + } + } + } + + fn monitoring (&self) -> bool; + fn monitoring_mut (&mut self) -> &mut bool; + fn toggle_monitor (&mut self) { + *self.monitoring_mut() = !self.monitoring(); + } + fn monitor (&mut self, scope: &ProcessScope, midi_buf: &mut Vec>>) { + // For highlighting keys and note repeat + let notes_in = self.notes_in().clone(); + for input in self.midi_ins_mut().iter() { + for (sample, event, bytes) in parse_midi_input(input.iter(scope)) { + if let LiveEvent::Midi { message, .. } = event { + midi_buf[sample].push(bytes.to_vec()); + update_keys(&mut*notes_in.write().unwrap(), &message); + } + } + } + } + + fn overdub (&self) -> bool; + fn overdub_mut (&mut self) -> &mut bool; + fn toggle_overdub (&mut self) { + *self.overdub_mut() = !self.overdub(); + } +} + +pub trait MidiPlaybackApi: HasPlayPhrase + HasClock + HasMidiOuts { + + fn notes_out (&self) -> &Arc>; + + /// Clear the section of the output buffer that we will be using, + /// emitting "all notes off" at start of buffer if requested. + fn clear ( + &mut self, scope: &ProcessScope, out_buf: &mut Vec>>, reset: bool + ) { + for frame in &mut out_buf[0..scope.n_frames() as usize] { + frame.clear(); + } + if reset { + all_notes_off(out_buf); + } + } + + /// Output notes from phrase to MIDI output ports. + fn play ( + &mut self, scope: &ProcessScope, note_buf: &mut Vec, out_buf: &mut Vec>> + ) -> bool { + let mut next = false; + // Write MIDI events from currently playing phrase (if any) to MIDI output buffer + if self.clock().is_rolling() { + let sample0 = scope.last_frame_time() as usize; + let samples = scope.n_frames() as usize; + // If no phrase is playing, prepare for switchover immediately + next = self.play_phrase().is_none(); + let phrase = self.play_phrase(); + let started0 = &self.clock().started; + let timebase = self.clock().timebase(); + let notes_out = self.notes_out(); + let next_phrase = self.next_phrase(); + if let Some((started, phrase)) = phrase { + // First sample to populate. Greater than 0 means that the first + // pulse of the phrase falls somewhere in the middle of the chunk. + let sample = started.sample.get() as usize; + let sample = sample + started0.read().unwrap().as_ref().unwrap().sample.get() as usize; + let sample = sample0.saturating_sub(sample); + // Iterator that emits sample (index into output buffer at which to write MIDI event) + // paired with pulse (index into phrase from which to take the MIDI event) for each + // sample of the output buffer that corresponds to a MIDI pulse. + let pulses = timebase.pulses_between_samples(sample, sample + samples); + // Notes active during current chunk. + let notes = &mut notes_out.write().unwrap(); + for (sample, pulse) in pulses { + // If a next phrase is enqueued, and we're past the end of the current one, + // break the loop here (FIXME count pulse correctly) + next = next_phrase.is_some() && if let Some(ref phrase) = phrase { + pulse >= phrase.read().unwrap().length + } else { + true + }; + if next { + break + } + // If there's a currently playing phrase, output notes from it to buffer: + if let Some(ref phrase) = phrase { + // Source phrase from which the MIDI events will be taken. + let phrase = phrase.read().unwrap(); + // Phrase with zero length is not processed + if phrase.length > 0 { + // Current pulse index in source phrase + let pulse = pulse % phrase.length; + // Output each MIDI event from phrase at appropriate frames of output buffer: + for message in phrase.notes[pulse].iter() { + // Clear output buffer for this MIDI event. + note_buf.clear(); + // TODO: support MIDI channels other than CH1. + let channel = 0.into(); + // Serialize MIDI event into message buffer. + LiveEvent::Midi { channel, message: *message } + .write(note_buf) + .unwrap(); + // Append serialized message to output buffer. + out_buf[sample].push(note_buf.clone()); + // Update the list of currently held notes. + update_keys(&mut*notes, &message); + } + } + } + } + } + } + next + } + + /// Handle switchover from current to next playing phrase. + fn switchover ( + &mut self, scope: &ProcessScope, note_buf: &mut Vec, out_buf: &mut Vec>> + ) { + if self.clock().is_rolling() { + let sample0 = scope.last_frame_time() as usize; + //let samples = scope.n_frames() as usize; + if let Some((start_at, phrase)) = &self.next_phrase() { + let start = start_at.sample.get() as usize; + let sample = self.clock().started.read().unwrap().as_ref().unwrap().sample.get() as usize; + // If it's time to switch to the next phrase: + if start <= sample0.saturating_sub(sample) { + // Samples elapsed since phrase was supposed to start + let skipped = sample0 - start; + // Switch over to enqueued phrase + let started = Moment::from_sample(&self.clock().timebase(), start as f64); + *self.play_phrase_mut() = Some((started, phrase.clone())); + // Unset enqueuement (TODO: where to implement looping?) + *self.next_phrase_mut() = None + } + // TODO fill in remaining ticks of chunk from next phrase. + // ?? just call self.play(scope) again, since enqueuement is off ??? + self.play(scope, note_buf, out_buf); + // ?? or must it be with modified scope ?? + // likely not because start time etc + } + } + } + + /// Write a chunk of MIDI notes to the output buffer. + fn write ( + &mut self, scope: &ProcessScope, out_buf: &Vec>> + ) { + let samples = scope.n_frames() as usize; + for port in self.midi_outs_mut().iter_mut() { + let writer = &mut port.writer(scope); + for time in 0..samples { + for event in out_buf[time].iter() { + writer.write(&RawMidi { time: time as u32, bytes: &event }) + .expect(&format!("{event:?}")); + } + } + } + } +} + +/// Add "all notes off" to the start of a buffer. +pub fn all_notes_off (output: &mut [Vec>]) { + let mut buf = vec![]; + let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() }; + let evt = LiveEvent::Midi { channel: 0.into(), message: msg }; + evt.write(&mut buf).unwrap(); + output[0].push(buf); +} + +/// Return boxed iterator of MIDI events +pub fn parse_midi_input (input: MidiIter) -> Box + '_> { + Box::new(input.map(|RawMidi { time, bytes }|( + time as usize, + LiveEvent::parse(bytes).unwrap(), + bytes + ))) +} + +/// Update notes_in array +pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) { + match message { + MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; } + MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; }, + _ => {} + } +} + +/// Hosts the JACK callback for a single MIDI player +pub struct PlayerAudio<'a, T: MidiPlayerApi>( + /// Player + pub &'a mut T, + /// Note buffer + pub &'a mut Vec, + /// Note chunk buffer + pub &'a mut Vec>>, +); + +/// JACK process callback for a sequencer's phrase player/recorder. +impl<'a, T: MidiPlayerApi> Audio for PlayerAudio<'a, T> { + fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { + let model = &mut self.0; + let note_buf = &mut self.1; + let midi_buf = &mut self.2; + // Clear output buffer(s) + model.clear(scope, midi_buf, false); + // Write chunk of phrase to output, handle switchover + if model.play(scope, note_buf, midi_buf) { + model.switchover(scope, note_buf, midi_buf); + } + if model.has_midi_ins() { + if model.recording() || model.monitoring() { + // Record and/or monitor input + model.record(scope, midi_buf) + } else if model.has_midi_outs() && model.monitoring() { + // Monitor input to output + model.monitor(scope, midi_buf) + } + } + // Write to output port(s) + model.write(scope, midi_buf); + Control::Continue + } +} + +//#[derive(Debug)] +//pub struct MIDIPlayer { + ///// Global timebase + //pub clock: Arc, + ///// Start time and phrase being played + //pub play_phrase: Option<(Moment, Option>>)>, + ///// Start time and next phrase + //pub next_phrase: Option<(Moment, Option>>)>, + ///// Play input through output. + //pub monitoring: bool, + ///// Write input to sequence. + //pub recording: bool, + ///// Overdub input to sequence. + //pub overdub: bool, + ///// Send all notes off + //pub reset: bool, // TODO?: after Some(nframes) + ///// Record from MIDI ports to current sequence. + //pub midi_inputs: Vec>, + ///// Play from current sequence to MIDI ports + //pub midi_outputs: Vec>, + ///// MIDI output buffer + //pub midi_note: Vec, + ///// MIDI output buffer + //pub midi_chunk: Vec>>, + ///// Notes currently held at input + //pub notes_in: Arc>, + ///// Notes currently held at output + //pub notes_out: Arc>, +//} + +///// Methods used primarily by the process callback +//impl MIDIPlayer { + //pub fn new ( + //jack: &Arc>, + //clock: &Arc, + //name: &str + //) -> Usually { + //let jack = jack.read().unwrap(); + //Ok(Self { + //clock: clock.clone(), + //phrase: None, + //next_phrase: None, + //notes_in: Arc::new(RwLock::new([false;128])), + //notes_out: Arc::new(RwLock::new([false;128])), + //monitoring: false, + //recording: false, + //overdub: true, + //reset: true, + //midi_note: Vec::with_capacity(8), + //midi_chunk: vec![Vec::with_capacity(16);16384], + //midi_outputs: vec![ + //jack.client().register_port(format!("{name}_out0").as_str(), MidiOut::default())? + //], + //midi_inputs: vec![ + //jack.client().register_port(format!("{name}_in0").as_str(), MidiIn::default())? + //], + //}) + //} +//} diff --git a/crates/tek/src/api/scene.rs b/crates/tek/src/api/scene.rs new file mode 100644 index 00000000..662569bc --- /dev/null +++ b/crates/tek/src/api/scene.rs @@ -0,0 +1,127 @@ +use crate::*; + +pub trait HasScenes { + fn scenes (&self) -> &Vec; + fn scenes_mut (&mut self) -> &mut Vec; + fn scene_add (&mut self, name: Option<&str>, color: Option) -> Usually<&mut S>; + fn scene_del (&mut self, index: usize) { + self.scenes_mut().remove(index); + } + fn scene_default_name (&self) -> String { + format!("Scene {}", self.scenes().len() + 1) + } + fn selected_scene (&self) -> Option<&S> { + None + } + fn selected_scene_mut (&mut self) -> Option<&mut S> { + None + } +} + +#[derive(Clone, Debug)] +pub enum ArrangerSceneCommand { + Add, + Delete(usize), + RandomColor, + Play(usize), + Swap(usize, usize), + SetSize(usize), + SetZoom(usize), +} + +//impl Command for ArrangerSceneCommand { + //fn execute (self, state: &mut T) -> Perhaps { + //match self { + //Self::Delete(index) => { state.scene_del(index); }, + //_ => todo!() + //} + //Ok(None) + //} +//} + +pub trait ArrangerSceneApi: Sized { + fn name (&self) -> &Arc>; + fn clips (&self) -> &Vec>>>; + fn color (&self) -> ItemColor; + + fn ppqs (scenes: &[Self], factor: usize) -> Vec<(usize, usize)> { + let mut total = 0; + if factor == 0 { + scenes.iter().map(|scene|{ + let pulses = scene.pulses().max(PPQ); + total = total + pulses; + (pulses, total - pulses) + }).collect() + } else { + (0..=scenes.len()).map(|i|{ + (factor*PPQ, factor*PPQ*i) + }).collect() + } + } + + fn longest_name (scenes: &[Self]) -> usize { + scenes.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max) + } + + /// Returns the pulse length of the longest phrase in the scene + fn pulses (&self) -> usize { + self.clips().iter().fold(0, |a, p|{ + a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0)) + }) + } + + /// Returns true if all phrases in the scene are + /// currently playing on the given collection of tracks. + fn is_playing (&self, tracks: &[T]) -> bool { + self.clips().iter().any(|clip|clip.is_some()) && self.clips().iter().enumerate() + .all(|(track_index, clip)|match clip { + Some(clip) => tracks + .get(track_index) + .map(|track|{ + if let Some((_, Some(phrase))) = track.player().play_phrase() { + *phrase.read().unwrap() == *clip.read().unwrap() + } else { + false + } + }) + .unwrap_or(false), + None => true + }) + } + + fn clip (&self, index: usize) -> Option<&Arc>> { + match self.clips().get(index) { Some(Some(clip)) => Some(clip), _ => None } + } + +} + +//impl ArrangerScene { + + ////TODO + ////pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually { + ////let mut name = None; + ////let mut clips = vec![]; + ////edn!(edn in args { + ////Edn::Map(map) => { + ////let key = map.get(&Edn::Key(":name")); + ////if let Some(Edn::Str(n)) = key { + ////name = Some(*n); + ////} else { + ////panic!("unexpected key in scene '{name:?}': {key:?}") + ////} + ////}, + ////Edn::Symbol("_") => { + ////clips.push(None); + ////}, + ////Edn::Int(i) => { + ////clips.push(Some(*i as usize)); + ////}, + ////_ => panic!("unexpected in scene '{name:?}': {edn:?}") + ////}); + ////Ok(ArrangerScene { + ////name: Arc::new(name.unwrap_or("").to_string().into()), + ////color: ItemColor::random(), + ////clips, + ////}) + ////} +//} diff --git a/device/sf2.rs b/crates/tek/src/api/timeline.ts similarity index 100% rename from device/sf2.rs rename to crates/tek/src/api/timeline.ts diff --git a/crates/tek/src/api/track.rs b/crates/tek/src/api/track.rs new file mode 100644 index 00000000..43229a71 --- /dev/null +++ b/crates/tek/src/api/track.rs @@ -0,0 +1,87 @@ +use crate::*; + +pub trait HasTracks: Send + Sync { + fn tracks (&self) -> &Vec; + fn tracks_mut (&mut self) -> &mut Vec; +} + +impl HasTracks for Vec { + fn tracks (&self) -> &Vec { + self + } + fn tracks_mut (&mut self) -> &mut Vec { + self + } +} + +pub trait ArrangerTracksApi: HasTracks { + fn track_add (&mut self, name: Option<&str>, color: Option)-> Usually<&mut T>; + fn track_del (&mut self, index: usize); + fn track_default_name (&self) -> String { + format!("Track {}", self.tracks().len() + 1) + } +} + +#[derive(Clone, Debug)] +pub enum ArrangerTrackCommand { + Add, + Delete(usize), + RandomColor, + Stop, + Swap(usize, usize), + SetSize(usize), + SetZoom(usize), +} + +pub trait ArrangerTrackApi: HasPlayer + Send + Sync + Sized { + /// Name of track + fn name (&self) -> &Arc>; + /// Preferred width of track column + fn width (&self) -> usize; + /// Preferred width of track column + fn width_mut (&mut self) -> &mut usize; + /// Identifying color of track + fn color (&self) -> ItemColor; + + fn longest_name (tracks: &[Self]) -> usize { + tracks.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max) + } + + const MIN_WIDTH: usize = 3; + + fn width_inc (&mut self) { + *self.width_mut() += 1; + } + + fn width_dec (&mut self) { + if self.width() > Self::MIN_WIDTH { + *self.width_mut() -= 1; + } + } +} + +/// Hosts the JACK callback for a collection of tracks +pub struct TracksAudio<'a, T: ArrangerTrackApi, H: HasTracks>( + // Track collection + pub &'a mut H, + /// Note buffer + pub &'a mut Vec, + /// Note chunk buffer + pub &'a mut Vec>>, + /// Marker + pub PhantomData, +); + +impl<'a, T: ArrangerTrackApi, H: HasTracks> Audio for TracksAudio<'a, T, H> { + #[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { + let model = &mut self.0; + let note_buffer = &mut self.1; + let output_buffer = &mut self.2; + for track in model.tracks_mut().iter_mut() { + if PlayerAudio(track.player_mut(), note_buffer, output_buffer).process(client, scope) == Control::Quit { + return Control::Quit + } + } + Control::Continue + } +} diff --git a/crates/tek/src/cli/cli_arranger.rs b/crates/tek/src/cli/cli_arranger.rs new file mode 100644 index 00000000..4c11ea1f --- /dev/null +++ b/crates/tek/src/cli/cli_arranger.rs @@ -0,0 +1,49 @@ +include!("../lib.rs"); + +pub fn main () -> Usually<()> { + ArrangerCli::parse().run() +} + +/// Parses CLI arguments to the `tek_arranger` invocation. +#[derive(Debug, Parser)] +#[command(version, about, long_about = None)] +pub struct ArrangerCli { + /// Name of JACK client + #[arg(short, long)] name: Option, + /// Whether to include a transport toolbar (default: true) + #[arg(short, long, default_value_t = true)] transport: bool, + /// Number of tracks + #[arg(short = 'x', long, default_value_t = 8)] tracks: usize, + /// Number of scenes + #[arg(short, long, default_value_t = 8)] scenes: usize, +} + +impl ArrangerCli { + /// Run the arranger TUI from CLI arguments. + fn run (&self) -> Usually<()> { + Tui::run(JackClient::new("tek_arranger")?.activate_with(|jack|{ + let mut app = ArrangerTui::try_from(jack)?; + if let Some(name) = self.name.as_ref() { + *app.name.write().unwrap() = name.clone(); + } + let track_color_1 = ItemColor::random(); + let track_color_2 = ItemColor::random(); + for i in 0..self.tracks { + let _track = app.track_add( + None, + Some(track_color_1.mix(track_color_2, i as f32 / self.tracks as f32)) + )?; + } + let scene_color_1 = ItemColor::random(); + let scene_color_2 = ItemColor::random(); + for i in 0..self.scenes { + let _scene = app.scene_add( + None, + Some(scene_color_1.mix(scene_color_2, i as f32 / self.scenes as f32)) + )?; + } + Ok(app) + })?)?; + Ok(()) + } +} diff --git a/crates/tek/src/cli/cli_sequencer.rs b/crates/tek/src/cli/cli_sequencer.rs new file mode 100644 index 00000000..ec18266e --- /dev/null +++ b/crates/tek/src/cli/cli_sequencer.rs @@ -0,0 +1,45 @@ +include!("../lib.rs"); + +pub fn main () -> Usually<()> { + SequencerCli::parse().run() +} + +#[derive(Debug, Parser)] +#[command(version, about, long_about = None)] +pub struct SequencerCli { + /// Name of JACK client + #[arg(short, long)] name: Option, + /// Pulses per quarter note (sequencer resolution; default: 96) + #[arg(short, long)] ppq: Option, + /// Default phrase duration (in pulses; default: 4 * PPQ = 1 bar) + #[arg(short, long)] length: Option, + /// Whether to include a transport toolbar (default: true) + #[arg(short, long, default_value_t = true)] transport: bool +} + +impl SequencerCli { + fn run (&self) -> Usually<()> { + Tui::run(JackClient::new("tek_sequencer")?.activate_with(|jack|{ + let mut app = SequencerTui::try_from(jack)?; + //app.editor.view_mode.set_time_zoom(1); + // TODO: create from arguments + let midi_in = app.jack.read().unwrap().register_port("in", MidiIn::default())?; + app.player.midi_ins.push(midi_in); + let midi_out = app.jack.read().unwrap().register_port("out", MidiOut::default())?; + app.player.midi_outs.push(midi_out); + if let Some(_) = self.name.as_ref() { + // TODO: sequencer.name = Arc::new(RwLock::new(name.clone())); + } + if let Some(_) = self.ppq { + // TODO: sequencer.ppq = ppq; + } + if let Some(_) = self.length { + // TODO: if let Some(phrase) = sequencer.phrase.as_mut() { + //phrase.write().unwrap().length = length; + //} + } + Ok(app) + })?)?; + Ok(()) + } +} diff --git a/crates/tek/src/cli/cli_transport.rs b/crates/tek/src/cli/cli_transport.rs new file mode 100644 index 00000000..68f47c94 --- /dev/null +++ b/crates/tek/src/cli/cli_transport.rs @@ -0,0 +1,9 @@ +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/.old/todo_cli_mixer.rs b/crates/tek/src/cli/todo_cli_mixer.rs similarity index 87% rename from .old/todo_cli_mixer.rs rename to crates/tek/src/cli/todo_cli_mixer.rs index e419ffed..d14f9807 100644 --- a/.old/todo_cli_mixer.rs +++ b/crates/tek/src/cli/todo_cli_mixer.rs @@ -1,4 +1,4 @@ -include!("./lib.rs"); +use crate::*; pub fn main () -> Usually<()> { MixerCli::parse().run() @@ -13,7 +13,7 @@ pub fn main () -> Usually<()> { impl MixerCli { fn run (&self) -> Usually<()> { - Tui::run(JackConnection::new("tek_mixer")?.activate_with(|jack|{ + Tui::run(JackClient::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/.old/todo_cli_plugin.rs b/crates/tek/src/cli/todo_cli_plugin.rs similarity index 87% rename from .old/todo_cli_plugin.rs rename to crates/tek/src/cli/todo_cli_plugin.rs index cfb81fe2..06acc9bf 100644 --- a/.old/todo_cli_plugin.rs +++ b/crates/tek/src/cli/todo_cli_plugin.rs @@ -1,4 +1,4 @@ -include!("./lib.rs"); +use crate::*; pub fn main () -> Usually<()> { PluginCli::parse().run() @@ -13,7 +13,7 @@ pub fn main () -> Usually<()> { impl PluginCli { fn run (&self) -> Usually<()> { - Tui::run(JackConnection::new("tek_plugin")?.activate_with(|jack|{ + Tui::run(JackClient::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/.old/todo_cli_sampler.rs b/crates/tek/src/cli/todo_cli_sampler.rs similarity index 86% rename from .old/todo_cli_sampler.rs rename to crates/tek/src/cli/todo_cli_sampler.rs index 75e4c62a..0fcc8a7d 100644 --- a/.old/todo_cli_sampler.rs +++ b/crates/tek/src/cli/todo_cli_sampler.rs @@ -1,4 +1,4 @@ -include!("./lib.rs"); +use crate::*; pub fn main () -> Usually<()> { SamplerCli::parse().run() @@ -13,7 +13,7 @@ pub fn main () -> Usually<()> { impl SamplerCli { fn run (&self) -> Usually<()> { - Tui::run(JackConnection::new("tek_sampler")?.activate_with(|jack|{ + Tui::run(JackClient::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/src/core.rs b/crates/tek/src/core.rs new file mode 100644 index 00000000..8a3cd53f --- /dev/null +++ b/crates/tek/src/core.rs @@ -0,0 +1,13 @@ +use crate::*; + +mod audio; pub(crate) use audio::*; +mod color; pub(crate) use color::*; +mod command; pub(crate) use command::*; +mod edn; pub(crate) use edn::*; +mod engine; pub(crate) use engine::*; +mod focus; pub(crate) use focus::*; +mod input; pub(crate) use input::*; +mod output; pub(crate) use output::*; +mod pitch; pub(crate) use pitch::*; +mod space; pub(crate) use space::*; +mod time; pub(crate) use time::*; diff --git a/crates/tek/src/core/audio.rs b/crates/tek/src/core/audio.rs new file mode 100644 index 00000000..176b62a6 --- /dev/null +++ b/crates/tek/src/core/audio.rs @@ -0,0 +1,181 @@ +use crate::*; +use jack::*; + +#[derive(Debug)] +/// Event enum for JACK events. +pub enum JackEvent { + ThreadInit, + Shutdown(ClientStatus, String), + Freewheel(bool), + SampleRate(Frames), + ClientRegistration(String, bool), + PortRegistration(PortId, bool), + PortRename(PortId, String, String), + PortsConnected(PortId, PortId, bool), + GraphReorder, + XRun, +} + +/// Wraps [Client] or [DynamicAsyncClient] in place. +#[derive(Debug)] +pub enum JackClient { + /// Before activation. + Inactive(Client), + /// During activation. + Activating, + /// After activation. Must not be dropped for JACK thread to persist. + Active(DynamicAsyncClient), +} + +/// Trait for things that wrap a JACK client. +pub trait AudioEngine { + + fn transport (&self) -> Transport { + self.client().transport() + } + + fn port_by_name (&self, name: &str) -> Option> { + self.client().port_by_name(name) + } + + fn register_port (&self, name: &str, spec: PS) -> Usually> { + Ok(self.client().register_port(name, spec)?) + } + + fn client (&self) -> &Client; + + fn activate ( + self, + process: impl FnMut(&Arc>, &Client, &ProcessScope) -> Control + Send + 'static + ) -> Usually>> where Self: Send + Sync + 'static; + + fn thread_init (&self, _: &Client) {} + + unsafe fn shutdown (&mut self, status: ClientStatus, reason: &str) {} + + fn freewheel (&mut self, _: &Client, enabled: bool) {} + + fn client_registration (&mut self, _: &Client, name: &str, reg: bool) {} + + fn port_registration (&mut self, _: &Client, id: PortId, reg: bool) {} + + fn ports_connected (&mut self, _: &Client, a: PortId, b: PortId, are: bool) {} + + fn sample_rate (&mut self, _: &Client, frames: Frames) -> Control { + Control::Continue + } + + fn port_rename (&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { + Control::Continue + } + + fn graph_reorder (&mut self, _: &Client) -> Control { + Control::Continue + } + + fn xrun (&mut self, _: &Client) -> Control { + Control::Continue + } +} + +impl AudioEngine for JackClient { + fn client(&self) -> &Client { + match self { + Self::Inactive(ref client) => client, + Self::Activating => panic!("jack client has not finished activation"), + Self::Active(ref client) => client.as_client(), + } + } + fn activate( + self, + mut cb: impl FnMut(&Arc>, &Client, &ProcessScope) -> Control + Send + 'static, + ) -> Usually>> + where + Self: Send + Sync + 'static + { + let client = Client::from(self); + let state = Arc::new(RwLock::new(Self::Activating)); + let event = Box::new(move|_|{/*TODO*/}) as Box; + let events = Notifications(event); + let frame = Box::new({let state = state.clone(); move|c: &_, s: &_|cb(&state, c, s)}); + let frames = contrib::ClosureProcessHandler::new(frame as BoxedAudioHandler); + *state.write().unwrap() = Self::Active(client.activate_async(events, frames)?); + Ok(state) + } +} + +pub type DynamicAsyncClient = AsyncClient; + +pub type DynamicAudioHandler = contrib::ClosureProcessHandler<(), BoxedAudioHandler>; + +pub type BoxedAudioHandler = Box Control + Send>; + +impl JackClient { + pub fn new (name: &str) -> Usually { + let (client, _) = Client::new(name, ClientOptions::NO_START_SERVER)?; + Ok(Self::Inactive(client)) + } +} + +impl From for Client { + fn from (jack: JackClient) -> Client { + match jack { + JackClient::Inactive(client) => client, + JackClient::Activating => panic!("jack client still activating"), + JackClient::Active(_) => panic!("jack client already activated"), + } + } +} + +/// Notification handler used by the [Jack] factory +/// when constructing [JackDevice]s. +pub type DynamicNotifications = Notifications>; + +/// Generic notification handler that emits [JackEvent] +pub struct Notifications(pub T); + +impl NotificationHandler for Notifications { + fn thread_init(&self, _: &Client) { + self.0(JackEvent::ThreadInit); + } + + unsafe fn shutdown(&mut self, status: ClientStatus, reason: &str) { + self.0(JackEvent::Shutdown(status, reason.into())); + } + + fn freewheel(&mut self, _: &Client, enabled: bool) { + self.0(JackEvent::Freewheel(enabled)); + } + + fn sample_rate(&mut self, _: &Client, frames: Frames) -> Control { + self.0(JackEvent::SampleRate(frames)); + Control::Quit + } + + fn client_registration(&mut self, _: &Client, name: &str, reg: bool) { + self.0(JackEvent::ClientRegistration(name.into(), reg)); + } + + fn port_registration(&mut self, _: &Client, id: PortId, reg: bool) { + self.0(JackEvent::PortRegistration(id, reg)); + } + + fn port_rename(&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { + self.0(JackEvent::PortRename(id, old.into(), new.into())); + Control::Continue + } + + fn ports_connected(&mut self, _: &Client, a: PortId, b: PortId, are: bool) { + self.0(JackEvent::PortsConnected(a, b, are)); + } + + fn graph_reorder(&mut self, _: &Client) -> Control { + self.0(JackEvent::GraphReorder); + Control::Continue + } + + fn xrun(&mut self, _: &Client) -> Control { + self.0(JackEvent::XRun); + Control::Continue + } +} diff --git a/device/vst3.rs b/crates/tek/src/core/collect.rs similarity index 100% rename from device/vst3.rs rename to crates/tek/src/core/collect.rs diff --git a/crates/tek/src/core/color.rs b/crates/tek/src/core/color.rs new file mode 100644 index 00000000..9464e1f2 --- /dev/null +++ b/crates/tek/src/core/color.rs @@ -0,0 +1,75 @@ +use crate::*; +use rand::{thread_rng, distributions::uniform::UniformSampler}; +pub use ratatui::prelude::Color; + +/// A color in OKHSL and RGB representations. +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub struct ItemColor { + pub okhsl: Okhsl, + pub rgb: Color, +} +/// A color in OKHSL and RGB with lighter and darker variants. +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub struct ItemColorTriplet { + pub base: ItemColor, + pub light: ItemColor, + pub dark: ItemColor, +} +/// Adds TUI RGB representation to an OKHSL value. +impl From> for ItemColor { + fn from (okhsl: Okhsl) -> Self { Self { okhsl, rgb: okhsl_to_rgb(okhsl) } } +} +/// Adds OKHSL representation to a TUI RGB value. +impl From for ItemColor { + fn from (rgb: Color) -> Self { Self { rgb, okhsl: rgb_to_okhsl(rgb) } } +} +impl ItemColor { + pub fn random () -> Self { + let mut rng = thread_rng(); + let lo = Okhsl::new(-180.0, 0.01, 0.25); + let hi = Okhsl::new( 180.0, 0.9, 0.5); + UniformOkhsl::new(lo, hi).sample(&mut rng).into() + } + pub fn random_dark () -> Self { + let mut rng = thread_rng(); + let lo = Okhsl::new(-180.0, 0.025, 0.075); + let hi = Okhsl::new( 180.0, 0.5, 0.150); + UniformOkhsl::new(lo, hi).sample(&mut rng).into() + } + pub fn random_near (color: Self, distance: f32) -> Self { + color.mix(Self::random(), distance) + } + pub fn mix (&self, other: Self, distance: f32) -> Self { + if distance > 1.0 { panic!("color mixing takes distance between 0.0 and 1.0"); } + self.okhsl.mix(other.okhsl, distance).into() + } +} +impl From for ItemColorTriplet { + fn from (base: ItemColor) -> Self { + let mut light = base.okhsl.clone(); + light.lightness = (light.lightness * 1.15).min(Okhsl::::max_lightness()); + let mut dark = base.okhsl.clone(); + dark.lightness = (dark.lightness * 0.85).max(Okhsl::::min_lightness()); + dark.saturation = (dark.saturation * 0.85).max(Okhsl::::min_saturation()); + Self { base, light: light.into(), dark: dark.into() } + } +} +impl ItemColorTriplet { + pub fn random () -> Self { + ItemColor::random().into() + } + pub fn random_near (color: Self, distance: f32) -> Self { + color.base.mix(ItemColor::random(), distance).into() + } +} +pub fn okhsl_to_rgb (color: Okhsl) -> Color { + let Srgb { red, green, blue, .. }: Srgb = Srgb::from_color_unclamped(color); + Color::Rgb((red * 255.0) as u8, (green * 255.0) as u8, (blue * 255.0) as u8,) +} +pub fn rgb_to_okhsl (color: Color) -> Okhsl { + if let Color::Rgb(r, g, b) = color { + Okhsl::from_color(Srgb::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0)) + } else { + unreachable!("only Color::Rgb is supported") + } +} diff --git a/crates/tek/src/core/command.rs b/crates/tek/src/core/command.rs new file mode 100644 index 00000000..480a42e8 --- /dev/null +++ b/crates/tek/src/core/command.rs @@ -0,0 +1,96 @@ +use crate::*; + +#[derive(Clone)] +pub enum NextPrev { + Next, + Prev, +} + +pub trait Execute { + fn command (&mut self, command: T) -> Perhaps; +} + +pub trait Command: Send + Sync + Sized { + fn execute (self, state: &mut S) -> Perhaps; +} +pub fn delegate , S> ( + cmd: C, + wrap: impl Fn(C)->B, + state: &mut S, +) -> Perhaps { + Ok(cmd.execute(state)?.map(|x|wrap(x))) +} + +pub trait InputToCommand: Command + Sized { + fn input_to_command (state: &S, input: &E::Input) -> Option; + fn execute_with_state (state: &mut S, input: &E::Input) -> Perhaps { + Ok(if let Some(command) = Self::input_to_command(state, input) { + let _undo = command.execute(state)?; + Some(true) + } else { + None + }) + } +} +pub struct MenuBar> { + pub menus: Vec>, + pub index: usize, +} +impl> MenuBar { + pub fn new () -> Self { Self { menus: vec![], index: 0 } } + pub fn add (mut self, menu: Menu) -> Self { + self.menus.push(menu); + self + } +} +pub struct Menu> { + pub title: String, + pub items: Vec>, + pub index: Option, +} +impl> Menu { + pub fn new (title: impl AsRef) -> Self { + Self { + title: title.as_ref().to_string(), + items: vec![], + index: None, + } + } + pub fn add (mut self, item: MenuItem) -> Self { + self.items.push(item); + self + } + pub fn sep (mut self) -> Self { + self.items.push(MenuItem::sep()); + self + } + pub fn cmd (mut self, hotkey: &'static str, text: &'static str, command: C) -> Self { + self.items.push(MenuItem::cmd(hotkey, text, command)); + self + } + pub fn off (mut self, hotkey: &'static str, text: &'static str) -> Self { + self.items.push(MenuItem::off(hotkey, text)); + self + } +} +pub enum MenuItem> { + /// Unused. + __(PhantomData, PhantomData), + /// A separator. Skip it. + Separator, + /// A menu item with command, description and hotkey. + Command(&'static str, &'static str, C), + /// A menu item that can't be activated but has description and hotkey + Disabled(&'static str, &'static str) +} +impl> MenuItem { + pub fn sep () -> Self { + Self::Separator + } + pub fn cmd (hotkey: &'static str, text: &'static str, command: C) -> Self { + Self::Command(hotkey, text, command) + } + pub fn off (hotkey: &'static str, text: &'static str) -> Self { + Self::Disabled(hotkey, text) + } +} diff --git a/crates/tek/src/core/edn.rs b/crates/tek/src/core/edn.rs new file mode 100644 index 00000000..2709043e --- /dev/null +++ b/crates/tek/src/core/edn.rs @@ -0,0 +1,14 @@ +pub use clojure_reader::{edn::{read, Edn}, error::Error as EdnError}; + +/// EDN parsing helper. +#[macro_export] macro_rules! edn { + ($edn:ident { $($pat:pat => $expr:expr),* $(,)? }) => { + match $edn { $($pat => $expr),* } + }; + ($edn:ident in $args:ident { $($pat:pat => $expr:expr),* $(,)? }) => { + for $edn in $args { + edn!($edn { $($pat => $expr),* }) + } + }; +} + diff --git a/crates/tek/src/core/engine.rs b/crates/tek/src/core/engine.rs new file mode 100644 index 00000000..10effc1b --- /dev/null +++ b/crates/tek/src/core/engine.rs @@ -0,0 +1,54 @@ +use crate::*; + +/// Entry point for main loop +pub trait App { + fn run (self, context: T) -> Usually; +} + +/// Platform backend. +pub trait Engine: Send + Sync + Sized { + /// Input event type + type Input: Input; + /// Result of handling input + type Handled; + /// Render target + type Output: Output; + /// Unit of length + type Unit: Coordinate; + /// Rectangle without offset + type Size: Size + From<[Self::Unit;2]> + Debug + Copy; + /// Rectangle with offset + type Area: Area + From<[Self::Unit;4]> + Debug + Copy; + /// Prepare before run + fn setup (&mut self) -> Usually<()> { Ok(()) } + /// True if done + fn exited (&self) -> bool; + /// Clean up after run + fn teardown (&mut self) -> Usually<()> { Ok(()) } +} + +/// A UI component that can render itself as a [Render], and [Handle] input. +pub trait Component: Render + Handle {} + +/// Everything that implements [Render] and [Handle] is a [Component]. +impl + Handle> Component for C {} + +/// A component that can exit. +pub trait Exit: Send { + fn exited (&self) -> bool; + fn exit (&mut self); + fn boxed (self) -> Box where Self: Sized + 'static { + Box::new(self) + } +} + +/// Marker trait for [Component]s that can [Exit]. +pub trait ExitableComponent: Exit + Component where E: Engine { + /// Perform type erasure for collecting heterogeneous components. + fn boxed (self) -> Box> where Self: Sized + 'static { + Box::new(self) + } +} + +/// All [Components]s that implement [Exit] implement [ExitableComponent]. +impl + Exit> ExitableComponent for C {} diff --git a/crates/tek/src/core/focus.rs b/crates/tek/src/core/focus.rs new file mode 100644 index 00000000..b153cfde --- /dev/null +++ b/crates/tek/src/core/focus.rs @@ -0,0 +1,303 @@ +use crate::*; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum FocusState { + Focused(T), + Entered(T), +} + +impl FocusState { + pub fn inner (&self) -> T { + match self { + Self::Focused(inner) => *inner, + Self::Entered(inner) => *inner, + } + } + pub fn set_inner (&mut self, inner: T) { + *self = match self { + Self::Focused(_) => Self::Focused(inner), + Self::Entered(_) => Self::Entered(inner), + } + } + pub fn is_focused (&self) -> bool { + if let Self::Focused(_) = self { true } else { false } + } + pub fn is_entered (&self) -> bool { + if let Self::Entered(_) = self { true } else { false } + } + pub fn to_focused (&mut self) { + *self = Self::Focused(self.inner()) + } + pub fn to_entered (&mut self) { + *self = Self::Entered(self.inner()) + } +} + +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum FocusCommand { + Up, + Down, + Left, + Right, + Next, + Prev, + Enter, + Exit, +} + +impl Command for FocusCommand { + fn execute (self, state: &mut F) -> Perhaps { + use FocusCommand::*; + match self { + Next => { state.focus_next(); }, + Prev => { state.focus_prev(); }, + Up => { state.focus_up(); }, + Down => { state.focus_down(); }, + Left => { state.focus_left(); }, + Right => { state.focus_right(); }, + Enter => { state.focus_enter(); }, + Exit => { state.focus_exit(); }, + } + Ok(None) + } +} + +/// Trait for things that have focusable subparts. +pub trait HasFocus { + type Item: Copy + PartialEq + Debug; + /// Get the currently focused item. + fn focused (&self) -> Self::Item; + /// Get the currently focused item. + fn set_focused (&mut self, to: Self::Item); + /// Loop forward until a specific item is focused. + fn focus_to (&mut self, to: Self::Item) { + self.set_focused(to); + self.focus_updated(); + } + /// Run this on focus update + fn focus_updated (&mut self) {} +} + +/// Trait for things that have enterable subparts. +pub trait HasEnter: HasFocus { + /// Get the currently focused item. + fn entered (&self) -> bool; + /// Get the currently focused item. + fn set_entered (&mut self, entered: bool); + /// Enter into the currently focused component + fn focus_enter (&mut self) { + self.set_entered(true); + self.focus_updated(); + } + /// Exit the currently entered component + fn focus_exit (&mut self) { + self.set_entered(false); + self.focus_updated(); + } +} + +/// Trait for things that implement directional navigation between focusable elements. +pub trait FocusGrid: HasFocus { + fn focus_layout (&self) -> &[&[Self::Item]]; + fn focus_cursor (&self) -> (usize, usize); + fn focus_cursor_mut (&mut self) -> &mut (usize, usize); + fn focus_current (&self) -> Self::Item { + let (x, y) = self.focus_cursor(); + self.focus_layout()[y][x] + } + fn focus_update (&mut self) { + self.focus_to(self.focus_current()); + self.focus_updated() + } + fn focus_up (&mut self) { + let original_focused = self.focused(); + let (_, original_y) = self.focus_cursor(); + loop { + let (x, y) = self.focus_cursor(); + let next_y = if y == 0 { + self.focus_layout().len().saturating_sub(1) + } else { + y - 1 + }; + if next_y == original_y { + break + } + let next_x = if self.focus_layout()[y].len() == self.focus_layout()[next_y].len() { + x + } else { + ((x as f32 / self.focus_layout()[original_y].len() as f32) + * self.focus_layout()[next_y].len() as f32) as usize + }; + *self.focus_cursor_mut() = (next_x, next_y); + if self.focus_current() != original_focused { + break + } + } + self.focus_update(); + } + fn focus_down (&mut self) { + let original_focused = self.focused(); + let (_, original_y) = self.focus_cursor(); + loop { + let (x, y) = self.focus_cursor(); + let next_y = if y >= self.focus_layout().len().saturating_sub(1) { + 0 + } else { + y + 1 + }; + if next_y == original_y { + break + } + let next_x = if self.focus_layout()[y].len() == self.focus_layout()[next_y].len() { + x + } else { + ((x as f32 / self.focus_layout()[original_y].len() as f32) + * self.focus_layout()[next_y].len() as f32) as usize + }; + *self.focus_cursor_mut() = (next_x, next_y); + if self.focus_current() != original_focused { + break + } + } + self.focus_update(); + } + fn focus_left (&mut self) { + let original_focused = self.focused(); + let (original_x, y) = self.focus_cursor(); + loop { + let x = self.focus_cursor().0; + let next_x = if x == 0 { + self.focus_layout()[y].len().saturating_sub(1) + } else { + x - 1 + }; + if next_x == original_x { + break + } + *self.focus_cursor_mut() = (next_x, y); + if self.focus_current() != original_focused { + break + } + } + self.focus_update(); + } + fn focus_right (&mut self) { + let original_focused = self.focused(); + let (original_x, y) = self.focus_cursor(); + loop { + let x = self.focus_cursor().0; + let next_x = if x >= self.focus_layout()[y].len().saturating_sub(1) { + 0 + } else { + x + 1 + }; + if next_x == original_x { + break + } + self.focus_cursor_mut().0 = next_x; + if self.focus_current() != original_focused { + break + } + } + self.focus_update(); + } +} + +/// Trait for things that implement next/prev navigation between focusable elements. +pub trait FocusOrder { + /// Focus the next item. + fn focus_next (&mut self); + /// Focus the previous item. + fn focus_prev (&mut self); +} + +/// Next/prev navigation for directional focusables works in the given way. +impl FocusOrder for T { + /// Focus the next item. + fn focus_next (&mut self) { + let current = self.focused(); + let (x, y) = self.focus_cursor(); + if x < self.focus_layout()[y].len().saturating_sub(1) { + self.focus_right(); + } else { + self.focus_down(); + self.focus_cursor_mut().0 = 0; + } + if self.focused() == current { // FIXME: prevent infinite loop + self.focus_next() + } + self.focus_exit(); + self.focus_update(); + } + /// Focus the previous item. + fn focus_prev (&mut self) { + let current = self.focused(); + let (x, _) = self.focus_cursor(); + if x > 0 { + self.focus_left(); + } else { + self.focus_up(); + let (_, y) = self.focus_cursor(); + let next_x = self.focus_layout()[y].len().saturating_sub(1); + self.focus_cursor_mut().0 = next_x; + } + if self.focused() == current { // FIXME: prevent infinite loop + self.focus_prev() + } + self.focus_exit(); + self.focus_update(); + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_focus () { + + struct FocusTest { + focused: char, + cursor: (usize, usize) + } + + impl HasFocus for FocusTest { + type Item = char; + fn focused (&self) -> Self::Item { + self.focused + } + fn set_focused (&mut self, to: Self::Item) { + self.focused = to + } + } + + impl FocusGrid for FocusTest { + fn focus_cursor (&self) -> (usize, usize) { + self.cursor + } + fn focus_cursor_mut (&mut self) -> &mut (usize, usize) { + &mut self.cursor + } + fn focus_layout (&self) -> &[&[Self::Item]] { + &[ + &['a', 'a', 'a', 'b', 'b', 'd'], + &['a', 'a', 'a', 'b', 'b', 'd'], + &['a', 'a', 'a', 'c', 'c', 'd'], + &['a', 'a', 'a', 'c', 'c', 'd'], + &['e', 'e', 'e', 'e', 'e', 'e'], + ] + } + } + + let mut tester = FocusTest { focused: 'a', cursor: (0, 0) }; + + tester.focus_right(); + assert_eq!(tester.cursor.0, 3); + assert_eq!(tester.focused, 'b'); + + tester.focus_down(); + assert_eq!(tester.cursor.1, 2); + assert_eq!(tester.focused, 'c'); + + } +} diff --git a/crates/tek/src/core/input.rs b/crates/tek/src/core/input.rs new file mode 100644 index 00000000..e9388793 --- /dev/null +++ b/crates/tek/src/core/input.rs @@ -0,0 +1,58 @@ +use crate::*; + +/// Current input state +pub trait Input { + /// Type of input event + type Event; + /// Currently handled event + fn event (&self) -> &Self::Event; + /// Whether component should exit + fn is_done (&self) -> bool; + /// Mark component as done + fn done (&self); +} + +/// Handle input +pub trait Handle: Send + Sync { + fn handle (&mut self, context: &E::Input) -> Perhaps; +} + +impl> Handle for &mut H { + fn handle (&mut self, context: &E::Input) -> Perhaps { + (*self).handle(context) + } +} + +impl> Handle for Option { + fn handle (&mut self, context: &E::Input) -> Perhaps { + if let Some(ref mut handle) = self { + handle.handle(context) + } else { + Ok(None) + } + } +} + +impl Handle for Mutex where H: Handle { + fn handle (&mut self, context: &E::Input) -> Perhaps { + self.lock().unwrap().handle(context) + } +} + +impl Handle for Arc> where H: Handle { + fn handle (&mut self, context: &E::Input) -> Perhaps { + self.lock().unwrap().handle(context) + } +} + +impl Handle for RwLock where H: Handle { + fn handle (&mut self, context: &E::Input) -> Perhaps { + self.write().unwrap().handle(context) + } +} + +impl Handle for Arc> where H: Handle { + fn handle (&mut self, context: &E::Input) -> Perhaps { + self.write().unwrap().handle(context) + } +} diff --git a/crates/tek/src/core/output.rs b/crates/tek/src/core/output.rs new file mode 100644 index 00000000..11871962 --- /dev/null +++ b/crates/tek/src/core/output.rs @@ -0,0 +1,161 @@ +use crate::*; + +/// Rendering target +pub trait Output { + /// Current output area + fn area (&self) -> E::Area; + /// Mutable pointer to area + fn area_mut (&mut self) -> &mut E::Area; + /// Render widget in area + fn render_in (&mut self, area: E::Area, widget: &dyn Render) -> Usually<()>; +} + +/// Cast to dynamic pointer +pub fn widget > (w: &T) -> &dyn Render { + w as &dyn Render +} + +/// A [Render] that contains other [Render]s +pub trait Content: Send + Sync { + fn content (&self) -> impl Render; +} + +//impl> Render for &C { + //fn min_size (&self, to: E::Size) -> Perhaps { + //self.content().min_size(to) + //} + //fn render (&self, to: &mut E::Output) -> Usually<()> { + //match self.min_size(to.area().wh().into())? { + //Some(wh) => to.render_in(to.area().clip(wh).into(), &self.content()), + //None => Ok(()) + //} + //} +//} + +/* + +/// Every struct that has [Content] is a renderable [Render]. +impl> Render for C { + fn min_size (&self, to: E::Size) -> Perhaps { + self.content().min_size(to) + } + fn render (&self, to: &mut E::Output) -> Usually<()> { + match self.min_size(to.area().wh().into())? { + Some(wh) => to.render_in(to.area().clip(wh).into(), &self.content()), + None => Ok(()) + } + } +} +*/ + +/// A renderable component +pub trait Render: Send + Sync { + /// Minimum size to use + fn min_size (&self, to: E::Size) -> Perhaps { + Ok(Some(to)) + } + /// Draw to output render target + fn render (&self, to: &mut E::Output) -> Usually<()>; +} + +impl> Render for &R { + fn min_size (&self, to: E::Size) -> Perhaps { + (*self).min_size(to) + } + fn render (&self, to: &mut E::Output) -> Usually<()> { + (*self).render(to) + } +} + +impl Render for &dyn Render { + fn min_size (&self, to: E::Size) -> Perhaps { + (*self).min_size(to) + } + fn render (&self, to: &mut E::Output) -> Usually<()> { + (*self).render(to) + } +} + +//impl Render for &mut dyn Render { + //fn min_size (&self, to: E::Size) -> Perhaps { + //(*self).min_size(to) + //} + //fn render (&self, to: &mut E::Output) -> Usually<()> { + //(*self).render(to) + //} +//} + +impl<'a, E: Engine> Render for Box + 'a> { + fn min_size (&self, to: E::Size) -> Perhaps { + (**self).min_size(to) + } + fn render (&self, to: &mut E::Output) -> Usually<()> { + (**self).render(to) + } +} + +impl> Render for Arc { + fn min_size (&self, to: E::Size) -> Perhaps { + self.as_ref().min_size(to) + } + fn render (&self, to: &mut E::Output) -> Usually<()> { + self.as_ref().render(to) + } +} + +impl> Render for Mutex { + fn min_size (&self, to: E::Size) -> Perhaps { + self.lock().unwrap().min_size(to) + } + fn render (&self, to: &mut E::Output) -> Usually<()> { + self.lock().unwrap().render(to) + } +} + +impl> Render for RwLock { + fn min_size (&self, to: E::Size) -> Perhaps { + self.read().unwrap().min_size(to) + } + fn render (&self, to: &mut E::Output) -> Usually<()> { + self.read().unwrap().render(to) + } +} + +impl> Render for Option { + fn min_size (&self, to: E::Size) -> Perhaps { + Ok(self.as_ref().map(|widget|widget.min_size(to)).transpose()?.flatten()) + } + fn render (&self, to: &mut E::Output) -> Usually<()> { + self.as_ref().map(|widget|widget.render(to)).unwrap_or(Ok(())) + } +} + +/// A custom [Render] defined by passing layout and render closures in place. +pub struct Widget< + E: Engine, + L: Send + Sync + Fn(E::Size)->Perhaps, + R: Send + Sync + Fn(&mut E::Output)->Usually<()> +>(L, R, PhantomData); + +impl< + E: Engine, + L: Send + Sync + Fn(E::Size)->Perhaps, + R: Send + Sync + Fn(&mut E::Output)->Usually<()> +> Widget { + pub fn new (layout: L, render: R) -> Self { + Self(layout, render, Default::default()) + } +} + +impl< + E: Engine, + L: Send + Sync + Fn(E::Size)->Perhaps, + R: Send + Sync + Fn(&mut E::Output)->Usually<()> +> Render for Widget { + fn min_size (&self, to: E::Size) -> Perhaps { + self.0(to) + } + fn render (&self, to: &mut E::Output) -> Usually<()> { + self.1(to) + } +} diff --git a/crates/tek/src/core/pitch.rs b/crates/tek/src/core/pitch.rs new file mode 100644 index 00000000..6a2ac714 --- /dev/null +++ b/crates/tek/src/core/pitch.rs @@ -0,0 +1,23 @@ +use crate::*; +use midly::num::u7; + +pub fn to_note_name (n: usize) -> &'static str { + if n > 127 { + panic!("to_note_name({n}): must be 0-127"); + } + MIDI_NOTE_NAMES[n] +} + +pub const MIDI_NOTE_NAMES: [&'static str;128] = [ + "C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0", + "C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1", + "C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2", + "C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3", + "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4", + "C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5", + "C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6", + "C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7", + "C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8", + "C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9", + "C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10", +]; diff --git a/crates/tek/src/core/space.rs b/crates/tek/src/core/space.rs new file mode 100644 index 00000000..a83f78c7 --- /dev/null +++ b/crates/tek/src/core/space.rs @@ -0,0 +1,157 @@ +use crate::*; + +/// Standard numeric type. +pub trait Coordinate: Send + Sync + Copy + + Add + + Sub + + Mul + + Div + + Ord + PartialEq + Eq + + Debug + Display + Default + + From + Into + + Into + + Into +{ + fn minus (self, other: Self) -> Self { + if self >= other { + self - other + } else { + 0.into() + } + } + fn ZERO () -> Self { + 0.into() + } +} + +impl Coordinate for T where T: Send + Sync + Copy + + Add + + Sub + + Mul + + Div + + Ord + PartialEq + Eq + + Debug + Display + Default + + From + Into + + Into + + Into +{} + +// TODO: return impl Point and impl Size instead of [N;x] +// to disambiguate between usage of 2-"tuple"s + +pub trait Size { + fn x (&self) -> N; + fn y (&self) -> N; + #[inline] fn w (&self) -> N { self.x() } + #[inline] fn h (&self) -> N { self.y() } + #[inline] fn wh (&self) -> [N;2] { [self.x(), self.y()] } + #[inline] fn clip_w (&self, w: N) -> [N;2] { [self.w().min(w.into()), self.h()] } + #[inline] fn clip_h (&self, h: N) -> [N;2] { [self.w(), self.h().min(h.into())] } + #[inline] fn expect_min (&self, w: N, h: N) -> Usually<&Self> { + if self.w() < w || self.h() < h { + Err(format!("min {w}x{h}").into()) + } else { + Ok(self) + } + } +} +impl Size for (N, N) { + fn x (&self) -> N { self.0 } + fn y (&self) -> N { self.1 } +} +impl Size for [N;2] { + fn x (&self) -> N { self[0] } + fn y (&self) -> N { self[1] } +} + +pub trait Area: Copy { + fn x (&self) -> N; + fn y (&self) -> N; + fn w (&self) -> N; + fn h (&self) -> N; + fn x2 (&self) -> N { self.x() + self.w() } + fn y2 (&self) -> N { self.y() + self.h() } + #[inline] fn wh (&self) -> [N;2] { [self.w(), self.h()] } + #[inline] fn xywh (&self) -> [N;4] { [self.x(), self.y(), self.w(), self.h()] } + #[inline] fn lrtb (&self) -> [N;4] { [self.x(), self.x2(), self.y(), self.y2()] } + #[inline] fn push_x (&self, x: N) -> [N;4] { [self.x() + x, self.y(), self.w(), self.h()] } + #[inline] fn push_y (&self, y: N) -> [N;4] { [self.x(), self.y() + y, self.w(), self.h()] } + #[inline] fn shrink_x (&self, x: N) -> [N;4] { [self.x(), self.y(), self.w() - x, self.h()] } + #[inline] fn shrink_y (&self, y: N) -> [N;4] { [self.x(), self.y(), self.w(), self.h() - y] } + #[inline] fn set_w (&self, w: N) -> [N;4] { [self.x(), self.y(), w, self.h()] } + #[inline] fn set_h (&self, h: N) -> [N;4] { [self.x(), self.y(), self.w(), h] } + #[inline] fn clip_h (&self, h: N) -> [N;4] { + [self.x(), self.y(), self.w(), self.h().min(h.into())] + } + #[inline] fn clip_w (&self, w: N) -> [N;4] { + [self.x(), self.y(), self.w().min(w.into()), self.h()] + } + #[inline] fn clip (&self, wh: impl Size) -> [N;4] { + [self.x(), self.y(), wh.w(), wh.h()] + } + #[inline] fn expect_min (&self, w: N, h: N) -> Usually<&Self> { + if self.w() < w || self.h() < h { + Err(format!("min {w}x{h}").into()) + } else { + Ok(self) + } + } + #[inline] fn split_fixed (&self, direction: Direction, a: N) -> ([N;4],[N;4]) { + match direction { + Direction::Up => ( + [self.x(), (self.y()+self.h()).minus(a), self.w(), a], + [self.x(), self.y(), self.w(), self.h().minus(a)], + ), + Direction::Down => ( + [self.x(), self.y(), self.w(), a], + [self.x(), self.y() + a, self.w(), self.h().minus(a)], + ), + Direction::Right => ( + [self.x(), self.y(), a, self.h()], + [self.x() + a, self.y(), self.w().minus(a), self.h()], + ), + _ => todo!(), + } + } +} + +impl Area for (N, N, N, N) { + #[inline] fn x (&self) -> N { self.0 } + #[inline] fn y (&self) -> N { self.1 } + #[inline] fn w (&self) -> N { self.2 } + #[inline] fn h (&self) -> N { self.3 } +} + +impl Area for [N;4] { + #[inline] fn x (&self) -> N { self[0] } + #[inline] fn y (&self) -> N { self[1] } + #[inline] fn w (&self) -> N { self[2] } + #[inline] fn h (&self) -> N { self[3] } +} + +#[derive(Copy, Clone, PartialEq)] +pub enum Direction { Up, Down, Left, Right, } +impl Direction { + pub fn is_up (&self) -> bool { match self { Self::Up => true, _ => false } } + pub fn is_down (&self) -> bool { match self { Self::Down => true, _ => false } } + pub fn is_left (&self) -> bool { match self { Self::Left => true, _ => false } } + pub fn is_right (&self) -> bool { match self { Self::Right => true, _ => false } } + /// Return next direction clockwise + pub fn cw (&self) -> Self { + match self { + Self::Up => Self::Right, + Self::Down => Self::Left, + Self::Left => Self::Up, + Self::Right => Self::Down, + } + } + /// Return next direction counterclockwise + pub fn ccw (&self) -> Self { + match self { + Self::Up => Self::Left, + Self::Down => Self::Right, + Self::Left => Self::Down, + Self::Right => Self::Up, + } + } +} diff --git a/crates/tek/src/core/time.rs b/crates/tek/src/core/time.rs new file mode 100644 index 00000000..536e403b --- /dev/null +++ b/crates/tek/src/core/time.rs @@ -0,0 +1,450 @@ +use crate::*; +use std::iter::Iterator; + +pub const DEFAULT_PPQ: f64 = 96.0; +/// FIXME: remove this and use PPQ from timebase everywhere: +pub const PPQ: usize = 96; + +/// A unit of time, represented as an atomic 64-bit float. +/// +/// According to https://stackoverflow.com/a/873367, as per IEEE754, +/// every integer between 1 and 2^53 can be represented exactly. +/// This should mean that, even at 192kHz sampling rate, over 1 year of audio +/// can be clocked in microseconds with f64 without losing precision. +pub trait TimeUnit { + /// Returns current value + fn get (&self) -> f64; + /// Sets new value, returns old + fn set (&self, value: f64) -> f64; +} +/// Implement arithmetic for a unit of time +macro_rules! impl_op { + ($T:ident, $Op:ident, $method:ident, |$a:ident,$b:ident|{$impl:expr}) => { + impl $Op for $T { + type Output = Self; #[inline] fn $method (self, other: Self) -> Self::Output { + let $a = self.get(); let $b = other.get(); Self($impl.into()) + } + } + impl $Op for $T { + type Output = Self; #[inline] fn $method (self, other: usize) -> Self::Output { + let $a = self.get(); let $b = other as f64; Self($impl.into()) + } + } + impl $Op for $T { + type Output = Self; #[inline] fn $method (self, other: f64) -> Self::Output { + let $a = self.get(); let $b = other; Self($impl.into()) + } + } + } +} +/// Define and implement a unit of time +macro_rules! impl_time_unit { + ($T:ident) => { + impl TimeUnit for $T { + fn get (&self) -> f64 { self.0.load(Ordering::Relaxed) } + fn set (&self, value: f64) -> f64 { + let old = self.get(); + self.0.store(value, Ordering::Relaxed); + old + } + } + impl_op!($T, Add, add, |a, b|{a + b}); + impl_op!($T, Sub, sub, |a, b|{a - b}); + impl_op!($T, Mul, mul, |a, b|{a * b}); + impl_op!($T, Div, div, |a, b|{a / b}); + impl_op!($T, Rem, rem, |a, b|{a % b}); + impl From for $T { fn from (value: f64) -> Self { Self(value.into()) } } + impl From for $T { fn from (value: usize) -> Self { Self((value as f64).into()) } } + impl Into for $T { fn into (self) -> f64 { self.get() } } + impl Into for $T { fn into (self) -> usize { self.get() as usize } } + impl Into for &$T { fn into (self) -> f64 { self.get() } } + impl Into for &$T { fn into (self) -> usize { self.get() as usize } } + impl Clone for $T { fn clone (&self) -> Self { Self(self.get().into()) } } + } +} + +/// Audio sample rate in Hz (samples per second) +#[derive(Debug, Default)] pub struct SampleRate(AtomicF64); +impl_time_unit!(SampleRate); +impl SampleRate { + /// Return the duration of a sample in microseconds (floating) + #[inline] pub fn usec_per_sample (&self) -> f64 { + 1_000_000f64 / self.get() + } + /// Return the duration of a sample in microseconds (floating) + #[inline] pub fn sample_per_usec (&self) -> f64 { + self.get() / 1_000_000f64 + } + /// Convert a number of samples to microseconds (floating) + #[inline] pub fn samples_to_usec (&self, samples: f64) -> f64 { + self.usec_per_sample() * samples + } + /// Convert a number of microseconds to samples (floating) + #[inline] pub fn usecs_to_sample (&self, usecs: f64) -> f64 { + self.sample_per_usec() * usecs + } +} + +/// Tempo in beats per minute +#[derive(Debug, Default)] pub struct BeatsPerMinute(AtomicF64); +impl_time_unit!(BeatsPerMinute); + +/// MIDI resolution in PPQ (pulses per quarter note) +#[derive(Debug, Default)] pub struct PulsesPerQuaver(AtomicF64); +impl_time_unit!(PulsesPerQuaver); + +/// Timestamp in microseconds +#[derive(Debug, Default)] pub struct Microsecond(AtomicF64); +impl_time_unit!(Microsecond); +impl Microsecond { + #[inline] pub fn format_msu (&self) -> String { + let usecs = self.get() as usize; + let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000); + let (minutes, seconds) = (seconds / 60, seconds % 60); + format!("{minutes}:{seconds:02}:{msecs:03}") + } +} + +/// Timestamp in audio samples +#[derive(Debug, Default)] pub struct SampleCount(AtomicF64); +impl_time_unit!(SampleCount); + +/// Timestamp in MIDI pulses +#[derive(Debug, Default)] pub struct Pulse(AtomicF64); +impl_time_unit!(Pulse); + +/// Quantization setting for launching clips +#[derive(Debug, Default)] pub struct LaunchSync(AtomicF64); +impl_time_unit!(LaunchSync); +impl LaunchSync { + pub fn next (&self) -> f64 { + next_note_length(self.get() as usize) as f64 + } + pub fn prev (&self) -> f64 { + prev_note_length(self.get() as usize) as f64 + } +} + +/// Quantization setting for notes +#[derive(Debug, Default)] pub struct Quantize(AtomicF64); +impl_time_unit!(Quantize); +impl Quantize { + pub fn next (&self) -> f64 { + next_note_length(self.get() as usize) as f64 + } + pub fn prev (&self) -> f64 { + prev_note_length(self.get() as usize) as f64 + } +} + +/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat) +#[derive(Debug, Clone)] +pub struct Timebase { + /// Audio samples per second + pub sr: SampleRate, + /// MIDI beats per minute + pub bpm: BeatsPerMinute, + /// MIDI ticks per beat + pub ppq: PulsesPerQuaver, +} +impl Timebase { + /// Specify sample rate, BPM and PPQ + pub fn new ( + s: impl Into, + b: impl Into, + p: impl Into + ) -> Self { + Self { sr: s.into(), bpm: b.into(), ppq: p.into() } + } + /// Iterate over ticks between start and end. + #[inline] pub fn pulses_between_samples (&self, start: usize, end: usize) -> TicksIterator { + TicksIterator { spp: self.samples_per_pulse(), sample: start, start, end } + } + /// Return the duration fo a beat in microseconds + #[inline] pub fn usec_per_beat (&self) -> f64 { 60_000_000f64 / self.bpm.get() } + /// Return the number of beats in a second + #[inline] pub fn beat_per_second (&self) -> f64 { self.bpm.get() / 60f64 } + /// Return the number of microseconds corresponding to a note of the given duration + #[inline] pub fn note_to_usec (&self, (num, den): (f64, f64)) -> f64 { + 4.0 * self.usec_per_beat() * num / den + } + /// Return duration of a pulse in microseconds (BPM-dependent) + #[inline] pub fn pulse_per_usec (&self) -> f64 { self.ppq.get() / self.usec_per_beat() } + /// Return duration of a pulse in microseconds (BPM-dependent) + #[inline] pub fn usec_per_pulse (&self) -> f64 { self.usec_per_beat() / self.ppq.get() } + /// Return number of pulses to which a number of microseconds corresponds (BPM-dependent) + #[inline] pub fn usecs_to_pulse (&self, usec: f64) -> f64 { usec * self.pulse_per_usec() } + /// Convert a number of pulses to a sample number (SR- and BPM-dependent) + #[inline] pub fn pulses_to_usec (&self, pulse: f64) -> f64 { pulse / self.usec_per_pulse() } + /// Return number of pulses in a second (BPM-dependent) + #[inline] pub fn pulses_per_second (&self) -> f64 { self.beat_per_second() * self.ppq.get() } + /// Return fraction of a pulse to which a sample corresponds (SR- and BPM-dependent) + #[inline] pub fn pulses_per_sample (&self) -> f64 { + self.usec_per_pulse() / self.sr.usec_per_sample() + } + /// Return number of samples in a pulse (SR- and BPM-dependent) + #[inline] pub fn samples_per_pulse (&self) -> f64 { + self.sr.get() / self.pulses_per_second() + } + /// Convert a number of pulses to a sample number (SR- and BPM-dependent) + #[inline] pub fn pulses_to_sample (&self, p: f64) -> f64 { + self.pulses_per_sample() * p + } + /// Convert a number of samples to a pulse number (SR- and BPM-dependent) + #[inline] pub fn samples_to_pulse (&self, s: f64) -> f64 { + s / self.pulses_per_sample() + } + /// Return the number of samples corresponding to a note of the given duration + #[inline] pub fn note_to_samples (&self, note: (f64, f64)) -> f64 { + self.usec_to_sample(self.note_to_usec(note)) + } + /// Return the number of samples corresponding to the given number of microseconds + #[inline] pub fn usec_to_sample (&self, usec: f64) -> f64 { + usec * self.sr.get() / 1000f64 + } + /// Return the quantized position of a moment in time given a step + #[inline] pub fn quantize (&self, step: (f64, f64), time: f64) -> (f64, f64) { + let step = self.note_to_usec(step); + (time / step, time % step) + } + /// Quantize a collection of events + #[inline] pub fn quantize_into + Sized, T> ( + &self, step: (f64, f64), events: E + ) -> Vec<(f64, f64)> { + events.map(|(time, event)|(self.quantize(step, time).0, event)).collect() + } + /// Format a number of pulses into Beat.Bar.Pulse starting from 0 + #[inline] pub fn format_beats_0 (&self, pulse: f64) -> String { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; + format!("{}.{}.{pulses:02}", beats / 4, beats % 4) + } + /// Format a number of pulses into Beat.Bar starting from 0 + #[inline] pub fn format_beats_0_short (&self, pulse: f64) -> String { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let beats = if ppq > 0 { pulse / ppq } else { 0 }; + format!("{}.{}", beats / 4, beats % 4) + } + /// Format a number of pulses into Beat.Bar.Pulse starting from 1 + #[inline] pub fn format_beats_1 (&self, pulse: f64) -> String { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; + format!("{}.{}.{pulses:02}", beats / 4 + 1, beats % 4 + 1) + } + /// Format a number of pulses into Beat.Bar.Pulse starting from 1 + #[inline] pub fn format_beats_1_short (&self, pulse: f64) -> String { + let pulse = pulse as usize; + let ppq = self.ppq.get() as usize; + let beats = if ppq > 0 { pulse / ppq } else { 0 }; + format!("{}.{}", beats / 4 + 1, beats % 4 + 1) + } +} +impl Default for Timebase { + fn default () -> Self { Self::new(48000f64, 150f64, DEFAULT_PPQ) } +} + +#[derive(Debug, Clone)] +pub enum Moment2 { + None, + Zero, + Usec(Microsecond), + Sample(SampleCount), + Pulse(Pulse), +} + +/// A point in time in all time scales (microsecond, sample, MIDI pulse) +#[derive(Debug, Default, Clone)] +pub struct Moment { + pub timebase: Arc, + /// Current time in microseconds + pub usec: Microsecond, + /// Current time in audio samples + pub sample: SampleCount, + /// Current time in MIDI pulses + pub pulse: Pulse, +} +impl Moment { + pub fn zero (timebase: &Arc) -> Self { + Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() } + } + pub fn from_usec (timebase: &Arc, usec: f64) -> Self { + Self { + usec: usec.into(), + sample: timebase.sr.usecs_to_sample(usec).into(), + pulse: timebase.usecs_to_pulse(usec).into(), + timebase: timebase.clone(), + } + } + pub fn from_sample (timebase: &Arc, sample: f64) -> Self { + Self { + sample: sample.into(), + usec: timebase.sr.samples_to_usec(sample).into(), + pulse: timebase.samples_to_pulse(sample).into(), + timebase: timebase.clone(), + } + } + pub fn from_pulse (timebase: &Arc, pulse: f64) -> Self { + Self { + pulse: pulse.into(), + sample: timebase.pulses_to_sample(pulse).into(), + usec: timebase.pulses_to_usec(pulse).into(), + timebase: timebase.clone(), + } + } + #[inline] pub fn update_from_usec (&self, usec: f64) { + self.usec.set(usec); + self.pulse.set(self.timebase.usecs_to_pulse(usec)); + self.sample.set(self.timebase.sr.usecs_to_sample(usec)); + } + #[inline] pub fn update_from_sample (&self, sample: f64) { + self.usec.set(self.timebase.sr.samples_to_usec(sample)); + self.pulse.set(self.timebase.samples_to_pulse(sample)); + self.sample.set(sample); + } + #[inline] pub fn update_from_pulse (&self, pulse: f64) { + self.usec.set(self.timebase.pulses_to_usec(pulse)); + self.pulse.set(pulse); + self.sample.set(self.timebase.pulses_to_sample(pulse)); + } + #[inline] pub fn format_beat (&self) -> String { + self.timebase.format_beats_1(self.pulse.get()) + } +} +/// Iterator that emits subsequent ticks within a range. +pub struct TicksIterator { + spp: f64, + sample: usize, + start: usize, + end: usize, +} +impl Iterator for TicksIterator { + type Item = (usize, usize); + fn next (&mut self) -> Option { + loop { + if self.sample > self.end { return None } + let spp = self.spp; + let sample = self.sample as f64; + let start = self.start; + let end = self.end; + self.sample += 1; + //println!("{spp} {sample} {start} {end}"); + let jitter = sample.rem_euclid(spp); // ramps + let next_jitter = (sample + 1.0).rem_euclid(spp); + if jitter > next_jitter { // at crossing: + let time = (sample as usize) % (end as usize-start as usize); + let tick = (sample / spp) as usize; + return Some((time, tick)) + } + } + } +} + +/// (pulses, name), assuming 96 PPQ +pub const NOTE_DURATIONS: [(usize, &str);26] = [ + (1, "1/384"), + (2, "1/192"), + (3, "1/128"), + (4, "1/96"), + (6, "1/64"), + (8, "1/48"), + (12, "1/32"), + (16, "1/24"), + (24, "1/16"), + (32, "1/12"), + (48, "1/8"), + (64, "1/6"), + (96, "1/4"), + (128, "1/3"), + (192, "1/2"), + (256, "2/3"), + (384, "1/1"), + (512, "4/3"), + (576, "3/2"), + (768, "2/1"), + (1152, "3/1"), + (1536, "4/1"), + (2304, "6/1"), + (3072, "8/1"), + (3456, "9/1"), + (6144, "16/1"), +]; +/// Returns the next shorter length +pub fn prev_note_length (pulses: usize) -> usize { + for i in 1..=16 { let length = NOTE_DURATIONS[16-i].0; if length < pulses { return length } } + pulses +} +/// Returns the next longer length +pub fn next_note_length (pulses: usize) -> usize { + for (length, _) in &NOTE_DURATIONS { if *length > pulses { return *length } } + pulses +} +pub fn pulses_to_name (pulses: usize) -> &'static str { + for (length, name) in &NOTE_DURATIONS { if *length == pulses { return name } } + "" +} + +/// Performance counter +pub struct PerfModel { + pub enabled: bool, + clock: quanta::Clock, + // In nanoseconds + used: AtomicF64, + // In microseconds + period: AtomicF64, +} + +impl Default for PerfModel { + fn default () -> Self { + Self { + enabled: true, + clock: quanta::Clock::new(), + used: Default::default(), + period: Default::default(), + } + } +} + +impl PerfModel { + pub fn get_t0 (&self) -> Option { + if self.enabled { + Some(self.clock.raw()) + } else { + None + } + } + pub fn update (&self, t0: Option, scope: &jack::ProcessScope) { + if let Some(t0) = t0 { + let t1 = self.clock.raw(); + self.used.store( + self.clock.delta_as_nanos(t0, t1) as f64, + Ordering::Relaxed, + ); + self.period.store( + scope.cycle_times().unwrap().period_usecs as f64, + Ordering::Relaxed, + ); + } + } + pub fn percentage (&self) -> Option { + let period = self.period.load(Ordering::Relaxed) * 1000.0; + if period > 0.0 { + let used = self.used.load(Ordering::Relaxed); + Some(100.0 * used / period) + } else { + None + } + } +} + +//#[cfg(test)] +//mod test { + //use super::*; + //#[test] + //fn test_samples_to_ticks () { + //let ticks = Ticks(12.3).between_samples(0, 100).collect::>(); + //println!("{ticks:?}"); + //} +//} diff --git a/crates/tek/src/core/tui.rs b/crates/tek/src/core/tui.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/tek/src/layout.rs b/crates/tek/src/layout.rs new file mode 100644 index 00000000..168e2022 --- /dev/null +++ b/crates/tek/src/layout.rs @@ -0,0 +1,17 @@ +use crate::*; + +mod align; pub(crate) use align::*; +mod bsp; pub(crate) use bsp::*; +mod cond; pub(crate) use cond::*; +mod debug; pub(crate) use debug::*; +mod fill; pub(crate) use fill::*; +mod fixed; pub(crate) use fixed::*; +mod inset_outset; pub(crate) use inset_outset::*; +mod layers; pub(crate) use layers::*; +mod measure; pub(crate) use measure::*; +mod min_max; pub(crate) use min_max::*; +mod push_pull; pub(crate) use push_pull::*; +mod scroll; pub(crate) use scroll::*; +mod shrink_grow; pub(crate) use shrink_grow::*; +mod split; pub(crate) use split::*; +mod stack; pub(crate) use stack::*; diff --git a/crates/tek/src/layout/align.rs b/crates/tek/src/layout/align.rs new file mode 100644 index 00000000..48c64874 --- /dev/null +++ b/crates/tek/src/layout/align.rs @@ -0,0 +1,102 @@ +use crate::*; + +impl LayoutAlign for E {} + +pub trait LayoutAlign { + fn center_x > (w: W) -> Align { Align::X(w) } + fn center_y > (w: W) -> Align { Align::Y(w) } + fn center > (w: W) -> Align { Align::Center(w) } + fn at_n > (w: W) -> Align { Align::N(w) } + fn at_s > (w: W) -> Align { Align::S(w) } + fn at_e > (w: W) -> Align { Align::E(w) } + fn at_w > (w: W) -> Align { Align::W(w) } + fn at_nw > (w: W) -> Align { Align::NW(w) } + fn at_sw > (w: W) -> Align { Align::SW(w) } + fn at_ne > (w: W) -> Align { Align::NE(w) } + fn at_se > (w: W) -> Align { Align::SE(w) } +} + +/// Override X and Y coordinates, aligning to corner, side, or center of area +pub enum Align { + /// Draw at center of container + Center(L), + /// Draw at center of X axis + X(L), + /// Draw at center of Y axis + Y(L), + /// Draw at upper left corner of contaier + NW(L), + /// Draw at center of upper edge of container + N(L), + /// Draw at right left corner of contaier + NE(L), + /// Draw at center of left edge of container + W(L), + /// Draw at center of right edge of container + E(L), + /// Draw at lower left corner of container + SW(L), + /// Draw at center of lower edge of container + S(L), + /// Draw at lower right edge of container + SE(L) +} + +impl Align { + pub fn inner (&self) -> &T { + match self { + Self::Center(inner) => inner, + Self::X(inner) => inner, + Self::Y(inner) => inner, + Self::NW(inner) => inner, + Self::N(inner) => inner, + Self::NE(inner) => inner, + Self::W(inner) => inner, + Self::E(inner) => inner, + Self::SW(inner) => inner, + Self::S(inner) => inner, + Self::SE(inner) => inner, + } + } +} + +fn align + From<[N;4]>> (align: &Align, outer: R, inner: R) -> Option { + if outer.w() < inner.w() || outer.h() < inner.h() { + None + } else { + let [ox, oy, ow, oh] = outer.xywh(); + let [ix, iy, iw, ih] = inner.xywh(); + Some(match align { + Align::Center(_) => [ox + (ow - iw) / 2.into(), oy + (oh - ih) / 2.into(), iw, ih,].into(), + Align::X(_) => [ox + (ow - iw) / 2.into(), iy, iw, ih,].into(), + Align::Y(_) => [ix, oy + (oh - ih) / 2.into(), iw, ih,].into(), + Align::NW(_) => [ox, oy, iw, ih,].into(), + Align::N(_) => [ox + (ow - iw) / 2.into(), oy, iw, ih,].into(), + Align::NE(_) => [ox + ow - iw, oy, iw, ih,].into(), + Align::W(_) => [ox, oy + (oh - ih) / 2.into(), iw, ih,].into(), + Align::E(_) => [ox + ow - iw, oy + (oh - ih) / 2.into(), iw, ih,].into(), + Align::SW(_) => [ox, oy + oh - ih, iw, ih,].into(), + Align::S(_) => [ox + (ow - iw) / 2.into(), oy + oh - ih, iw, ih,].into(), + Align::SE(_) => [ox + ow - iw, oy + oh - ih, iw, ih,].into(), + }) + } +} + +impl> Render for Align { + fn min_size (&self, outer_area: E::Size) -> Perhaps { + self.inner().min_size(outer_area) + } + fn render (&self, to: &mut E::Output) -> Usually<()> { + let outer_area = to.area(); + Ok(if let Some(inner_size) = self.min_size(outer_area.wh().into())? { + let inner_area = outer_area.clip(inner_size); + if let Some(aligned) = align(&self, outer_area.into(), inner_area.into()) { + to.render_in(aligned, self.inner())? + } else { + () + } + } else { + () + }) + } +} diff --git a/crates/tek/src/layout/bsp.rs b/crates/tek/src/layout/bsp.rs new file mode 100644 index 00000000..a0c3c07c --- /dev/null +++ b/crates/tek/src/layout/bsp.rs @@ -0,0 +1,105 @@ +use crate::*; + +impl LayoutBspStatic for E {} + +pub trait LayoutBspStatic: { + fn over , B: Render> (a: A, b: B) -> Over { + Over(Default::default(), a, b) + } + fn under , B: Render> (a: A, b: B) -> Under { + Under(Default::default(), a, b) + } + fn to_north , B: Render> (a: A, b: B) -> ToNorth { + ToNorth(None, a, b) + } + fn to_south , B: Render> (a: A, b: B) -> ToSouth { + ToSouth(None, a, b) + } + fn to_east , B: Render> (a: A, b: B) -> ToEast { + ToEast(None, a, b) + } + fn to_west , B: Render> (a: A, b: B) -> ToWest { + ToWest(None, a, b) + } +} + +pub trait LayoutBspFixedStatic: { + fn to_north , B: Render> (n: E::Unit, a: A, b: B) -> ToNorth { + ToNorth(Some(n), a, b) + } + fn to_south , B: Render> (n: E::Unit, a: A, b: B) -> ToSouth { + ToSouth(Some(n), a, b) + } + fn to_east , B: Render> (n: E::Unit, a: A, b: B) -> ToEast { + ToEast(Some(n), a, b) + } + fn to_west , B: Render> (n: E::Unit, a: A, b: B) -> ToWest { + ToWest(Some(n), a, b) + } +} + +pub struct Over, B: Render>(PhantomData, A, B); + +pub struct Under, B: Render>(PhantomData, A, B); + +pub struct ToNorth, B: Render>(Option, A, B); + +pub struct ToSouth, B: Render>(Option, A, B); + +pub struct ToEast(Option, A, B); + +pub struct ToWest, B: Render>(Option, A, B); + +impl, B: Render> Render for Over { + fn min_size (&self, _: E::Size) -> Perhaps { + todo!(); + } + fn render (&self, _: &mut E::Output) -> Usually<()> { + Ok(()) + } +} + +impl, B: Render> Render for Under { + fn min_size (&self, _: E::Size) -> Perhaps { + todo!(); + } + fn render (&self, _: &mut E::Output) -> Usually<()> { + Ok(()) + } +} + +impl, B: Render> Render for ToNorth { + fn min_size (&self, _: E::Size) -> Perhaps { + todo!(); + } + fn render (&self, _: &mut E::Output) -> Usually<()> { + Ok(()) + } +} + +impl, B: Render> Render for ToSouth { + fn min_size (&self, _: E::Size) -> Perhaps { + todo!(); + } + fn render (&self, _: &mut E::Output) -> Usually<()> { + Ok(()) + } +} + +impl, B: Render> Render for ToWest { + fn min_size (&self, _: E::Size) -> Perhaps { + todo!(); + } + fn render (&self, _: &mut E::Output) -> Usually<()> { + Ok(()) + } +} + +impl, B: Render> Render for ToEast { + fn min_size (&self, _: E::Size) -> Perhaps { + todo!(); + } + fn render (&self, _: &mut E::Output) -> Usually<()> { + Ok(()) + } +} diff --git a/crates/tek/src/layout/collect.rs b/crates/tek/src/layout/collect.rs new file mode 100644 index 00000000..141992f1 --- /dev/null +++ b/crates/tek/src/layout/collect.rs @@ -0,0 +1,79 @@ +use crate::*; + +pub enum Collect<'a, E: Engine, const N: usize> { + Callback(CallbackCollection<'a, E>), + //Iterator(IteratorCollection<'a, E>), + Array(ArrayCollection<'a, E, N>), + Slice(SliceCollection<'a, E>), +} + +impl<'a, E: Engine, const N: usize> Collect<'a, E, N> { + pub fn iter (&'a self) -> CollectIterator<'a, E, N> { + CollectIterator(0, &self) + } +} + +impl<'a, E: Engine, const N: usize> From> for Collect<'a, E, N> { + fn from (callback: CallbackCollection<'a, E>) -> Self { + Self::Callback(callback) + } +} + +impl<'a, E: Engine, const N: usize> From> for Collect<'a, E, N> { + fn from (slice: SliceCollection<'a, E>) -> Self { + Self::Slice(slice) + } +} + +impl<'a, E: Engine, const N: usize> From> for Collect<'a, E, N>{ + fn from (array: ArrayCollection<'a, E, N>) -> Self { + Self::Array(array) + } +} + +type CallbackCollection<'a, E> = + &'a dyn Fn(&'a mut dyn FnMut(&dyn Render)->Usually<()>); + +//type IteratorCollection<'a, E> = + //&'a mut dyn Iterator>; + +type SliceCollection<'a, E> = + &'a [&'a dyn Render]; + +type ArrayCollection<'a, E, const N: usize> = + [&'a dyn Render; N]; + +pub struct CollectIterator<'a, E: Engine, const N: usize>(usize, &'a Collect<'a, E, N>); + +impl<'a, E: Engine, const N: usize> Iterator for CollectIterator<'a, E, N> { + type Item = &'a dyn Render; + fn next (&mut self) -> Option { + match self.1 { + Collect::Callback(callback) => { + todo!() + }, + //Collection::Iterator(iterator) => { + //iterator.next() + //}, + Collect::Array(array) => { + if let Some(item) = array.get(self.0) { + self.0 += 1; + //Some(item) + None + } else { + None + } + } + Collect::Slice(slice) => { + if let Some(item) = slice.get(self.0) { + self.0 += 1; + //Some(item) + None + } else { + None + } + } + } + } +} + diff --git a/crates/tek/src/layout/cond.rs b/crates/tek/src/layout/cond.rs new file mode 100644 index 00000000..c4a6314d --- /dev/null +++ b/crates/tek/src/layout/cond.rs @@ -0,0 +1,67 @@ +use crate::*; + +impl> LayoutCond for R {} + +pub trait LayoutCond: Render + Sized { + fn when (self, cond: bool) -> If { + If(Default::default(), cond, self) + } + fn or > (self, cond: bool, other: B) -> Either { + Either(Default::default(), cond, self, other) + } +} + +impl LayoutCondStatic for E {} + +pub trait LayoutCondStatic { + fn either , B: Render> ( + condition: bool, + a: A, + b: B, + ) -> Either { + Either(Default::default(), condition, a, b) + } +} + +/// Render widget if predicate is true +pub struct If>(PhantomData, bool, A); + +impl> Render for If { + fn min_size (&self, to: E::Size) -> Perhaps { + if self.1 { + return self.2.min_size(to) + } + Ok(None) + } + fn render (&self, to: &mut E::Output) -> Usually<()> { + if self.1 { + return self.2.render(to) + } + Ok(()) + } +} + +/// Render widget A if predicate is true, otherwise widget B +pub struct Either, B: Render>( + PhantomData, + bool, + A, + B, +); + +impl, B: Render> Render for Either { + fn min_size (&self, to: E::Size) -> Perhaps { + if self.1 { + return self.2.min_size(to) + } else { + return self.3.min_size(to) + } + } + fn render (&self, to: &mut E::Output) -> Usually<()> { + if self.1 { + return self.2.render(to) + } else { + return self.3.render(to) + } + } +} diff --git a/crates/tek/src/layout/debug.rs b/crates/tek/src/layout/debug.rs new file mode 100644 index 00000000..6de7cbce --- /dev/null +++ b/crates/tek/src/layout/debug.rs @@ -0,0 +1,11 @@ +use crate::*; + +impl> LayoutDebug for W {} + +pub trait LayoutDebug: Render + Sized { + fn debug (self) -> DebugOverlay { + DebugOverlay(Default::default(), self) + } +} + +pub struct DebugOverlay>(PhantomData, pub W); diff --git a/crates/tek/src/layout/fill.rs b/crates/tek/src/layout/fill.rs new file mode 100644 index 00000000..28ba7066 --- /dev/null +++ b/crates/tek/src/layout/fill.rs @@ -0,0 +1,52 @@ +use crate::*; + +impl LayoutFill for E {} + +pub trait LayoutFill { + fn fill_x > (fill: W) -> Fill { + Fill::X(fill) + } + fn fill_y > (fill: W) -> Fill { + Fill::Y(fill) + } + fn fill_xy > (fill: W) -> Fill { + Fill::XY(fill) + } +} + +pub enum Fill> { + X(W), + Y(W), + XY(W), + _Unused(PhantomData) +} + +impl> Fill { + fn inner (&self) -> &W { + match self { + Self::X(inner) => &inner, + Self::Y(inner) => &inner, + Self::XY(inner) => &inner, + _ => unreachable!(), + } + } +} + +impl> Render for Fill { + fn min_size (&self, to: E::Size) -> Perhaps { + let area = self.inner().min_size(to.into())?; + if let Some(area) = area { + Ok(Some(match self { + Self::X(_) => [to.w().into(), area.h()], + Self::Y(_) => [area.w(), to.h().into()], + Self::XY(_) => [to.w().into(), to.h().into()], + _ => unreachable!(), + }.into())) + } else { + Ok(None) + } + } + fn render (&self, to: &mut E::Output) -> Usually<()> { + self.inner().render(to) + } +} diff --git a/crates/tek/src/layout/fixed.rs b/crates/tek/src/layout/fixed.rs new file mode 100644 index 00000000..a73c54ba --- /dev/null +++ b/crates/tek/src/layout/fixed.rs @@ -0,0 +1,58 @@ +use crate::*; + +impl LayoutFixed for E {} + +pub trait LayoutFixed { + fn fixed_x > (x: E::Unit, w: W) -> Fixed { + Fixed::X(x, w) + } + fn fixed_y > (y: E::Unit, w: W) -> Fixed { + Fixed::Y(y, w) + } + fn fixed_xy > (x: E::Unit, y: E::Unit, w: W) -> Fixed { + Fixed::XY(x, y, w) + } +} + +/// Enforce fixed size of drawing area +pub enum Fixed { + _Unused(PhantomData), + /// Enforce fixed width + X(E::Unit, T), + /// Enforce fixed height + Y(E::Unit, T), + /// Enforce fixed width and height + XY(E::Unit, E::Unit, T), +} + +impl Fixed { + pub fn inner (&self) -> &T { + match self { + Self::X(_, i) => i, + Self::Y(_, i) => i, + Self::XY(_, _, i) => i, + _ => unreachable!(), + } + } +} +impl> Render for Fixed { + fn min_size (&self, to: E::Size) -> Perhaps { + Ok(match self { + Self::X(w, _) => + if to.w() >= *w { Some([*w, to.h()].into()) } else { None }, + Self::Y(h, _) => + if to.h() >= *h { Some([to.w(), *h].into()) } else { None }, + Self::XY(w, h, _) + => if to.w() >= *w && to.h() >= *h { Some([*w, *h].into()) } else { None }, + _ => unreachable!(), + }) + } + fn render (&self, to: &mut E::Output) -> Usually<()> { + // 🡘 🡙 ←🡙→ + if let Some(size) = self.min_size(to.area().wh().into())? { + to.render_in(to.area().clip(size).into(), self.inner()) + } else { + Ok(()) + } + } +} diff --git a/crates/tek/src/layout/inset_outset.rs b/crates/tek/src/layout/inset_outset.rs new file mode 100644 index 00000000..2985aecf --- /dev/null +++ b/crates/tek/src/layout/inset_outset.rs @@ -0,0 +1,92 @@ +use crate::*; + +impl + LayoutShrinkGrow> LayoutInsetOutset for E {} + +pub trait LayoutInsetOutset: LayoutPushPull + LayoutShrinkGrow { + fn inset_x > (x: E::Unit, w: W) -> Inset { + Inset::X(x, w) + } + fn inset_y > (y: E::Unit, w: W) -> Inset { + Inset::Y(y, w) + } + fn inset_xy > (x: E::Unit, y: E::Unit, w: W) -> Inset { + Inset::XY(x, y, w) + } + fn outset_x > (x: E::Unit, w: W) -> Outset { + Outset::X(x, w) + } + fn outset_y > (y: E::Unit, w: W) -> Outset { + Outset::Y(y, w) + } + fn outset_xy > (x: E::Unit, y: E::Unit, w: W) -> Outset { + Outset::XY(x, y, w) + } +} + +/// Shrink from each side +pub enum Inset { + /// Decrease width + X(E::Unit, T), + /// Decrease height + Y(E::Unit, T), + /// Decrease width and height + XY(E::Unit, E::Unit, T), +} + +impl> Inset { + pub fn inner (&self) -> &T { + match self { + Self::X(_, i) => i, + Self::Y(_, i) => i, + Self::XY(_, _, i) => i, + } + } +} + +impl> Render for Inset { + fn render (&self, to: &mut E::Output) -> Usually<()> { + match self { + Self::X(x, inner) => E::push_x(*x, E::shrink_x(*x, inner)), + Self::Y(y, inner) => E::push_y(*y, E::shrink_y(*y, inner)), + Self::XY(x, y, inner) => E::push_xy(*x, *y, E::shrink_xy(*x, *y, inner)), + }.render(to) + } +} + +/// Grow on each side +pub enum Outset> { + /// Increase width + X(E::Unit, T), + /// Increase height + Y(E::Unit, T), + /// Increase width and height + XY(E::Unit, E::Unit, T), +} + + +impl> Outset { + pub fn inner (&self) -> &T { + match self { + Self::X(_, i) => i, + Self::Y(_, i) => i, + Self::XY(_, _, i) => i, + } + } +} + +impl> Render for Outset { + fn min_size (&self, to: E::Size) -> Perhaps { + match *self { + Self::X(x, ref inner) => E::grow_x(x + x, inner), + Self::Y(y, ref inner) => E::grow_y(y + y, inner), + Self::XY(x, y, ref inner) => E::grow_xy(x + x, y + y, inner), + }.min_size(to) + } + fn render (&self, to: &mut E::Output) -> Usually<()> { + match *self { + Self::X(x, ref inner) => E::push_x(x, inner), + Self::Y(y, ref inner) => E::push_y(y, inner), + Self::XY(x, y, ref inner) => E::push_xy(x, y, inner), + }.render(to) + } +} diff --git a/crates/tek/src/layout/layers.rs b/crates/tek/src/layout/layers.rs new file mode 100644 index 00000000..353198ab --- /dev/null +++ b/crates/tek/src/layout/layers.rs @@ -0,0 +1,53 @@ +use crate::*; + +#[macro_export] macro_rules! lay { + ([$($expr:expr),* $(,)?]) => { + Layers::new(move|add|{ $(add(&$expr)?;)* Ok(()) }) + }; + (![$($expr:expr),* $(,)?]) => { + Layers::new(|add|{ $(add(&$expr)?;)* Ok(()) }) + }; + ($expr:expr) => { + Layers::new($expr) + }; +} + +pub struct Layers< + E: Engine, + F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render)->Usually<()>)->Usually<()> +>(pub F, PhantomData); + +impl< + E: Engine, + F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render)->Usually<()>)->Usually<()> +> Layers { + #[inline] + pub fn new (build: F) -> Self { + Self(build, Default::default()) + } +} + +impl Render for Layers +where + F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render)->Usually<()>)->Usually<()> +{ + fn min_size (&self, area: E::Size) -> Perhaps { + let mut w: E::Unit = 0.into(); + let mut h: E::Unit = 0.into(); + (self.0)(&mut |layer| { + if let Some(layer_area) = layer.min_size(area)? { + w = w.max(layer_area.w()); + h = h.max(layer_area.h()); + } + Ok(()) + })?; + Ok(Some([w, h].into())) + } + fn render (&self, to: &mut E::Output) -> Usually<()> { + if let Some(size) = self.min_size(to.area().wh().into())? { + (self.0)(&mut |layer|to.render_in(to.area().clip(size).into(), layer)) + } else { + Ok(()) + } + } +} diff --git a/crates/tek/src/layout/map_reduce.rs b/crates/tek/src/layout/map_reduce.rs new file mode 100644 index 00000000..833973b2 --- /dev/null +++ b/crates/tek/src/layout/map_reduce.rs @@ -0,0 +1,33 @@ +use crate::*; + +impl+Send+Sync, T, R: Render> LayoutMapReduce for E {} + +pub trait LayoutMapReduce+Send+Sync, T, R: Render> { + fn map R> (iterator: I, callback: F) -> Map { + Map(Default::default(), iterator, callback) + } + fn reduce , T)->R+Send+Sync> (iterator: I, callback: F) -> Reduce { + Reduce(Default::default(), iterator, callback) + } +} + +pub struct Map, R: Render, F: Fn(T)->R>( + PhantomData, + I, + F +); + +pub struct Reduce, R: Render, F: Fn(&dyn Render, T)->R>( + PhantomData<(E, R)>, + I, + F +); + +impl+Send+Sync, R: Render, F: Fn(&dyn Render, T)->R+Send+Sync> Render for Reduce { + fn min_size (&self, to: E::Size) -> Perhaps { + todo!() + } + fn render (&self, to: &mut E::Output) -> Usually<()> { + todo!() + } +} diff --git a/crates/tek/src/layout/measure.rs b/crates/tek/src/layout/measure.rs new file mode 100644 index 00000000..aaffbc18 --- /dev/null +++ b/crates/tek/src/layout/measure.rs @@ -0,0 +1,46 @@ +use crate::*; + +/// A widget that tracks its render width and height +pub struct Measure(PhantomData, AtomicUsize, AtomicUsize); + +impl Clone for Measure { + fn clone (&self) -> Self { + Self( + Default::default(), + AtomicUsize::from(self.1.load(Ordering::Relaxed)), + AtomicUsize::from(self.2.load(Ordering::Relaxed)), + ) + } +} + +impl std::fmt::Debug for Measure { + fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("Measure") + .field("width", &self.0) + .field("height", &self.1) + .finish() + } +} + +impl Measure { + pub fn w (&self) -> usize { self.1.load(Ordering::Relaxed) } + pub fn h (&self) -> usize { self.2.load(Ordering::Relaxed) } + pub fn wh (&self) -> [usize;2] { [self.w(), self.h()] } + pub fn set_w (&self, w: impl Into) { self.1.store(w.into(), Ordering::Relaxed) } + pub fn set_h (&self, h: impl Into) { self.2.store(h.into(), Ordering::Relaxed) } + pub fn set_wh (&self, w: impl Into, h: impl Into) { self.set_w(w); self.set_h(h); } + pub fn new () -> Self { Self(PhantomData::default(), 0.into(), 0.into()) } + pub fn format (&self) -> String { format!("{}x{}", self.w(), self.h()) } +} + +impl Render for Measure { + fn min_size (&self, _: E::Size) -> Perhaps { + Ok(Some([0u16.into(), 0u16.into()].into())) + } + fn render (&self, to: &mut E::Output) -> Usually<()> { + self.set_w(to.area().w()); + self.set_h(to.area().h()); + Ok(()) + } +} + diff --git a/crates/tek/src/layout/min_max.rs b/crates/tek/src/layout/min_max.rs new file mode 100644 index 00000000..42312bab --- /dev/null +++ b/crates/tek/src/layout/min_max.rs @@ -0,0 +1,95 @@ +use crate::*; + +impl LayoutMinMax for E {} + +pub trait LayoutMinMax { + fn min_x > (x: E::Unit, w: W) -> Min { + Min::X(x, w) + } + fn min_y > (y: E::Unit, w: W) -> Min { + Min::Y(y, w) + } + fn min_xy > (x: E::Unit, y: E::Unit, w: W) -> Min { + Min::XY(x, y, w) + } + fn max_x > (x: E::Unit, w: W) -> Max { + Max::X(x, w) + } + fn max_y > (y: E::Unit, w: W) -> Max { + Max::Y(y, w) + } + fn max_xy > (x: E::Unit, y: E::Unit, w: W) -> Max { + Max::XY(x, y, w) + } +} + +/// Enforce minimum size of drawing area +pub enum Min> { + /// Enforce minimum width + X(E::Unit, T), + /// Enforce minimum height + Y(E::Unit, T), + /// Enforce minimum width and height + XY(E::Unit, E::Unit, T), +} + +/// Enforce maximum size of drawing area +pub enum Max> { + /// Enforce maximum width + X(E::Unit, T), + /// Enforce maximum height + Y(E::Unit, T), + /// Enforce maximum width and height + XY(E::Unit, E::Unit, T), +} + +impl> Min { + pub fn inner (&self) -> &T { + match self { + Self::X(_, i) => i, + Self::Y(_, i) => i, + Self::XY(_, _, i) => i, + } + } +} + +impl> Render for Min { + fn min_size (&self, to: E::Size) -> Perhaps { + Ok(self.inner().min_size(to)?.map(|to|match *self { + Self::X(w, _) => [to.w().max(w), to.h()], + Self::Y(h, _) => [to.w(), to.h().max(h)], + Self::XY(w, h, _) => [to.w().max(w), to.h().max(h)], + }.into())) + } + // TODO: 🡘 🡙 ←🡙→ + fn render (&self, to: &mut E::Output) -> Usually<()> { + Ok(self.min_size(to.area().wh().into())? + .map(|size|to.render_in(to.area().clip(size).into(), self.inner())) + .transpose()?.unwrap_or(())) + } +} + +impl> Max { + fn inner (&self) -> &T { + match self { + Self::X(_, i) => i, + Self::Y(_, i) => i, + Self::XY(_, _, i) => i, + } + } +} + +impl> Render for Max { + fn min_size (&self, to: E::Size) -> Perhaps { + Ok(self.inner().min_size(to)?.map(|to|match *self { + Self::X(w, _) => [to.w().min(w), to.h()], + Self::Y(h, _) => [to.w(), to.h().min(h)], + Self::XY(w, h, _) => [to.w().min(w), to.h().min(h)], + }.into())) + } + fn render (&self, to: &mut E::Output) -> Usually<()> { + Ok(self.min_size(to.area().wh().into())? + .map(|size|to.render_in(to.area().clip(size).into(), self.inner())) + .transpose()?.unwrap_or(())) + } +} diff --git a/crates/tek/src/layout/push_pull.rs b/crates/tek/src/layout/push_pull.rs new file mode 100644 index 00000000..2b83bd0e --- /dev/null +++ b/crates/tek/src/layout/push_pull.rs @@ -0,0 +1,129 @@ +use crate::*; + +impl LayoutPushPull for E {} + +pub trait LayoutPushPull { + fn push_x > (x: E::Unit, w: W) -> Push { + Push::X(x, w) + } + fn push_y > (y: E::Unit, w: W) -> Push { + Push::Y(y, w) + } + fn push_xy > (x: E::Unit, y: E::Unit, w: W) -> Push { + Push::XY(x, y, w) + } + fn pull_x > (x: E::Unit, w: W) -> Pull { + Pull::X(x, w) + } + fn pull_y > (y: E::Unit, w: W) -> Pull { + Pull::Y(y, w) + } + fn pull_xy > (x: E::Unit, y: E::Unit, w: W) -> Pull { + Pull::XY(x, y, w) + } +} + +/// Increment origin point of drawing area +pub enum Push> { + /// Move origin to the right + X(E::Unit, T), + /// Move origin downwards + Y(E::Unit, T), + /// Move origin to the right and downwards + XY(E::Unit, E::Unit, T), +} + +impl> Push { + pub fn inner (&self) -> &T { + match self { + Self::X(_, i) => i, + Self::Y(_, i) => i, + Self::XY(_, _, i) => i, + } + } + pub fn x (&self) -> E::Unit { + match self { + Self::X(x, _) => *x, + Self::Y(_, _) => E::Unit::default(), + Self::XY(x, _, _) => *x, + } + } + pub fn y (&self) -> E::Unit { + match self { + Self::X(_, _) => E::Unit::default(), + Self::Y(y, _) => *y, + Self::XY(_, y, _) => *y, + } + } +} + +impl> Render for Push { + fn min_size (&self, to: E::Size) -> Perhaps { + self.inner().min_size(to) + } + fn render (&self, to: &mut E::Output) -> Usually<()> { + let area = to.area(); + Ok(self.min_size(area.wh().into())? + .map(|size|to.render_in(match *self { + Self::X(x, _) => [area.x() + x, area.y(), size.w(), size.h()], + Self::Y(y, _) => [area.x(), area.y() + y, size.w(), size.h()], + Self::XY(x, y, _) => [area.x() + x, area.y() + y, size.w(), size.h()], + _ => unreachable!(), + }.into(), self.inner())).transpose()?.unwrap_or(())) + } +} + +/// Decrement origin point of drawing area +pub enum Pull> { + _Unused(PhantomData), + /// Move origin to the right + X(E::Unit, T), + /// Move origin downwards + Y(E::Unit, T), + /// Move origin to the right and downwards + XY(E::Unit, E::Unit, T), +} + +impl> Pull { + pub fn inner (&self) -> &T { + match self { + Self::X(_, i) => i, + Self::Y(_, i) => i, + Self::XY(_, _, i) => i, + _ => unreachable!(), + } + } + pub fn x (&self) -> E::Unit { + match self { + Self::X(x, _) => *x, + Self::Y(_, _) => E::Unit::default(), + Self::XY(x, _, _) => *x, + _ => unreachable!(), + } + } + pub fn y (&self) -> E::Unit { + match self { + Self::X(_, _) => E::Unit::default(), + Self::Y(y, _) => *y, + Self::XY(_, y, _) => *y, + _ => unreachable!(), + } + } +} + +impl> Render for Pull { + fn min_size (&self, to: E::Size) -> Perhaps { + self.inner().min_size(to) + } + fn render (&self, to: &mut E::Output) -> Usually<()> { + let area = to.area(); + Ok(self.min_size(area.wh().into())? + .map(|size|to.render_in(match *self { + Self::X(x, _) => [area.x().minus(x), area.y(), size.w(), size.h()], + Self::Y(y, _) => [area.x(), area.y().minus(y), size.w(), size.h()], + Self::XY(x, y, _) => [area.x().minus(x), area.y().minus(y), size.w(), size.h()], + _ => unreachable!(), + }.into(), self.inner())).transpose()?.unwrap_or(())) + } +} + diff --git a/crates/tek/src/layout/scroll.rs b/crates/tek/src/layout/scroll.rs new file mode 100644 index 00000000..326f6ab6 --- /dev/null +++ b/crates/tek/src/layout/scroll.rs @@ -0,0 +1,8 @@ +use crate::*; + +/// A scrollable area. +pub struct Scroll< + E: Engine, + F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render)->Usually<()>)->Usually<()> +>(pub F, pub Direction, pub u64, PhantomData); + diff --git a/crates/tek/src/layout/shrink_grow.rs b/crates/tek/src/layout/shrink_grow.rs new file mode 100644 index 00000000..9c725ef4 --- /dev/null +++ b/crates/tek/src/layout/shrink_grow.rs @@ -0,0 +1,104 @@ +use crate::*; + +impl LayoutShrinkGrow for E {} + +pub trait LayoutShrinkGrow { + fn shrink_x > (x: E::Unit, w: W) -> Shrink { + Shrink::X(x, w) + } + fn shrink_y > (y: E::Unit, w: W) -> Shrink { + Shrink::Y(y, w) + } + fn shrink_xy > (x: E::Unit, y: E::Unit, w: W) -> Shrink { + Shrink::XY(x, y, w) + } + fn grow_x > (x: E::Unit, w: W) -> Grow { + Grow::X(x, w) + } + fn grow_y > (y: E::Unit, w: W) -> Grow { + Grow::Y(y, w) + } + fn grow_xy > (x: E::Unit, y: E::Unit, w: W) -> Grow { + Grow::XY(x, y, w) + } +} + +/// Shrink drawing area +pub enum Shrink> { + /// Decrease width + X(E::Unit, T), + /// Decrease height + Y(E::Unit, T), + /// Decrease width and height + XY(E::Unit, E::Unit, T), +} + +/// Expand drawing area +pub enum Grow> { + /// Increase width + X(E::Unit, T), + /// Increase height + Y(E::Unit, T), + /// Increase width and height + XY(E::Unit, E::Unit, T) +} + +impl> Shrink { + fn inner (&self) -> &T { + match self { + Self::X(_, i) => i, + Self::Y(_, i) => i, + Self::XY(_, _, i) => i, + _ => unreachable!(), + } + } +} + +impl> Grow { + fn inner (&self) -> &T { + match self { + Self::X(_, i) => i, + Self::Y(_, i) => i, + Self::XY(_, _, i) => i, + } + } +} + +impl> Render for Shrink { + fn min_size (&self, to: E::Size) -> Perhaps { + Ok(self.inner().min_size(to)?.map(|to|match *self { + Self::X(w, _) => [ + if to.w() > w { to.w() - w } else { 0.into() }, + to.h() + ], + Self::Y(h, _) => [ + to.w(), + if to.h() > h { to.h() - h } else { 0.into() } + ], + Self::XY(w, h, _) => [ + if to.w() > w { to.w() - w } else { 0.into() }, + if to.h() > h { to.h() - h } else { 0.into() } + ], + }.into())) + } + fn render (&self, to: &mut E::Output) -> Usually<()> { + Ok(self.min_size(to.area().wh().into())? + .map(|size|to.render_in(to.area().clip(size).into(), self.inner())) + .transpose()?.unwrap_or(())) + } +} + +impl> Render for Grow { + fn min_size (&self, to: E::Size) -> Perhaps { + Ok(self.inner().min_size(to)?.map(|to|match *self { + Self::X(w, _) => [to.w() + w, to.h()], + Self::Y(h, _) => [to.w(), to.h() + h], + Self::XY(w, h, _) => [to.w() + w, to.h() + h], + }.into())) + } + fn render (&self, to: &mut E::Output) -> Usually<()> { + Ok(self.min_size(to.area().wh().into())? + .map(|size|to.render_in(to.area().clip(size).into(), self.inner())) + .transpose()?.unwrap_or(())) + } +} diff --git a/crates/tek/src/layout/split.rs b/crates/tek/src/layout/split.rs new file mode 100644 index 00000000..5b78d405 --- /dev/null +++ b/crates/tek/src/layout/split.rs @@ -0,0 +1,55 @@ +use crate::*; + +impl LayoutSplit for E {} + +pub trait LayoutSplit { + fn split , B: Render> ( + direction: Direction, amount: E::Unit, a: A, b: B + ) -> Split { + Split::new(direction, amount, a, b) + } + fn split_up , B: Render> ( + amount: E::Unit, a: A, b: B + ) -> Split { + Split::new(Direction::Up, amount, a, b) + } + + //fn split_flip > ( + //self, direction: Direction, amount: E::Unit, other: W + //) -> Split { Split::new(direction, amount, other, self) } +} + +/// A binary split with fixed proportion +pub struct Split, B: Render>( + pub Direction, pub E::Unit, A, B, PhantomData +); + +impl, B: Render> Split { + pub fn new (direction: Direction, proportion: E::Unit, a: A, b: B) -> Self { + Self(direction, proportion, a, b, Default::default()) + } + pub fn up (proportion: E::Unit, a: A, b: B) -> Self { + Self(Direction::Up, proportion, a, b, Default::default()) + } + pub fn down (proportion: E::Unit, a: A, b: B) -> Self { + Self(Direction::Down, proportion, a, b, Default::default()) + } + pub fn left (proportion: E::Unit, a: A, b: B) -> Self { + Self(Direction::Left, proportion, a, b, Default::default()) + } + pub fn right (proportion: E::Unit, a: A, b: B) -> Self { + Self(Direction::Right, proportion, a, b, Default::default()) + } +} + +impl, B: Render> Render for Split { + fn min_size (&self, to: E::Size) -> Perhaps { + Ok(Some(to)) + } + fn render (&self, to: &mut E::Output) -> Usually<()> { + let (a, b) = to.area().split_fixed(self.0, self.1); + to.render_in(a.into(), &self.2)?; + to.render_in(b.into(), &self.3)?; + Ok(()) + } +} diff --git a/crates/tek/src/layout/stack.rs b/crates/tek/src/layout/stack.rs new file mode 100644 index 00000000..24daf4be --- /dev/null +++ b/crates/tek/src/layout/stack.rs @@ -0,0 +1,197 @@ +use crate::*; + +#[macro_export] macro_rules! col { + ([$($expr:expr),* $(,)?]) => { + Stack::down(move|add|{ $(add(&$expr)?;)* Ok(()) }) + }; + (![$($expr:expr),* $(,)?]) => { + Stack::down(|add|{ $(add(&$expr)?;)* Ok(()) }) + }; + ($expr:expr) => { + Stack::down($expr) + }; + ($pat:pat in $collection:expr => $item:expr) => { + Stack::down(move|add|{ for $pat in $collection { add(&$item)?; } Ok(()) }) + }; +} + +#[macro_export] macro_rules! col_up { + ([$($expr:expr),* $(,)?]) => { + Stack::up(move|add|{ $(add(&$expr)?;)* Ok(()) }) + }; + (![$($expr:expr),* $(,)?]) => { + Stack::up(|add|{ $(add(&$expr)?;)* Ok(()) }) + }; + ($expr:expr) => { + Stack::up(expr) + }; + ($pat:pat in $collection:expr => $item:expr) => { + Stack::up(move |add|{ for $pat in $collection { add(&$item)?; } Ok(()) }) + }; +} + +#[macro_export] macro_rules! row { + ([$($expr:expr),* $(,)?]) => { + Stack::right(move|add|{ $(add(&$expr)?;)* Ok(()) }) + }; + (![$($expr:expr),* $(,)?]) => { + Stack::right(|add|{ $(add(&$expr)?;)* Ok(()) }) + }; + ($expr:expr) => { + Stack::right($expr) + }; + ($pat:pat in $collection:expr => $item:expr) => { + Stack::right(move|add|{ for $pat in $collection { add(&$item)?; } Ok(()) }) + }; +} + +pub struct Stack< + E: Engine, + F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render)->Usually<()>)->Usually<()> +>(pub F, pub Direction, PhantomData); + +impl< + E: Engine, + F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render)->Usually<()>)->Usually<()> +> Stack { + #[inline] pub fn new (direction: Direction, build: F) -> Self { + Self(build, direction, Default::default()) + } + #[inline] pub fn right (build: F) -> Self { + Self::new(Direction::Right, build) + } + #[inline] pub fn down (build: F) -> Self { + Self::new(Direction::Down, build) + } + #[inline] pub fn up (build: F) -> Self { + Self::new(Direction::Up, build) + } +} + +impl Render for Stack +where + F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render)->Usually<()>)->Usually<()> +{ + fn min_size (&self, to: E::Size) -> Perhaps { + match self.1 { + + Direction::Down => { + let mut w: E::Unit = 0.into(); + let mut h: E::Unit = 0.into(); + (self.0)(&mut |component: &dyn Render| { + let max = to.h().minus(h); + if max > E::Unit::ZERO() { + let item = E::max_y(max, E::push_y(h, component)); + let size = item.min_size(to)?.map(|size|size.wh()); + if let Some([width, height]) = size { + h = h + height.into(); + w = w.max(width); + } + } + Ok(()) + })?; + Ok(Some([w, h].into())) + }, + + Direction::Right => { + let mut w: E::Unit = 0.into(); + let mut h: E::Unit = 0.into(); + (self.0)(&mut |component: &dyn Render| { + let max = to.w().minus(w); + if max > E::Unit::ZERO() { + let item = E::max_x(max, E::push_x(h, component)); + let size = item.min_size(to)?.map(|size|size.wh()); + if let Some([width, height]) = size { + w = w + width.into(); + h = h.max(height); + } + } + Ok(()) + })?; + Ok(Some([w, h].into())) + }, + + Direction::Up => { + let mut w: E::Unit = 0.into(); + let mut h: E::Unit = 0.into(); + (self.0)(&mut |component: &dyn Render| { + let max = to.h().minus(h); + if max > E::Unit::ZERO() { + let item = E::max_y(to.h() - h, component); + let size = item.min_size(to)?.map(|size|size.wh()); + if let Some([width, height]) = size { + h = h + height.into(); + w = w.max(width); + } + } + Ok(()) + })?; + Ok(Some([w, h].into())) + }, + + Direction::Left => { + let mut w: E::Unit = 0.into(); + let mut h: E::Unit = 0.into(); + (self.0)(&mut |component: &dyn Render| { + if w < to.w() { + todo!(); + } + Ok(()) + })?; + Ok(Some([w, h].into())) + }, + } + } + + fn render (&self, to: &mut E::Output) -> Usually<()> { + let area = to.area(); + let mut w = 0.into(); + let mut h = 0.into(); + match self.1 { + Direction::Down => { + (self.0)(&mut |item| { + if h < area.h() { + let item = E::max_y(area.h() - h, E::push_y(h, item)); + let show = item.min_size(area.wh().into())?.map(|s|s.wh()); + if let Some([width, height]) = show { + item.render(to)?; + h = h + height; + if width > w { w = width } + }; + } + Ok(()) + })?; + }, + Direction::Right => { + (self.0)(&mut |item| { + if w < area.w() { + let item = E::max_x(area.w() - w, E::push_x(w, item)); + let show = item.min_size(area.wh().into())?.map(|s|s.wh()); + if let Some([width, height]) = show { + item.render(to)?; + w = width + w; + if height > h { h = height } + }; + } + Ok(()) + })?; + }, + Direction::Up => { + (self.0)(&mut |item| { + if h < area.h() { + let show = item.min_size([area.w(), area.h().minus(h)].into())?.map(|s|s.wh()); + if let Some([width, height]) = show { + E::shrink_y(height, E::push_y(area.h() - height, item)) + .render(to)?; + h = h + height; + if width > w { w = width } + }; + } + Ok(()) + })?; + }, + _ => todo!() + }; + Ok(()) + } +} diff --git a/crates/tek/src/lib.rs b/crates/tek/src/lib.rs new file mode 100644 index 00000000..6faafea0 --- /dev/null +++ b/crates/tek/src/lib.rs @@ -0,0 +1,83 @@ +pub(crate) use std::sync::{Arc, Mutex, RwLock}; +pub(crate) use std::sync::atomic::{Ordering, AtomicBool, AtomicUsize}; +pub(crate) use std::collections::BTreeMap; +pub(crate) use std::marker::PhantomData; +pub(crate) use std::thread::{spawn, JoinHandle}; +pub(crate) use std::path::PathBuf; +pub(crate) use std::ffi::OsString; +pub(crate) use std::time::Duration; +pub(crate) use std::io::{Stdout, stdout}; +pub(crate) use std::error::Error; + +pub(crate) use ratatui; +pub(crate) use ratatui::{ + prelude::{Style, Color, Buffer}, + style::{Stylize, Modifier}, + backend::{Backend, CrosstermBackend, ClearType} +}; + +pub(crate) use jack; +pub(crate) use jack::{ + Client, ProcessScope, Control, CycleTimes, + Port, PortSpec, MidiIn, MidiOut, AudioIn, AudioOut, Unowned, + Transport, TransportState, MidiIter, RawMidi, + contrib::ClosureProcessHandler, +}; + +pub(crate) use midly; +pub(crate) use midly::{ + Smf, + MidiMessage, + TrackEventKind, + live::LiveEvent, + num::u7 +}; + +pub(crate) use palette::{ + *, + convert::*, + okhsl::* +}; + +pub(crate) use clap::{self, Parser}; + +pub(crate) use crossterm::{ExecutableCommand}; +pub(crate) use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}; +pub(crate) use crossterm::event::{KeyCode, KeyModifiers, KeyEvent, KeyEventKind, KeyEventState}; +pub(crate) use better_panic::{Settings, Verbosity}; + +pub(crate) use atomic_float::*; + +use std::ops::{Add, Sub, Mul, Div, Rem}; +use std::cmp::{Ord, Eq, PartialEq}; +use std::fmt::{Debug, Display}; + +/// Standard result type. +pub type Usually = Result>; + +/// Standard optional result type. +pub type Perhaps = Result, Box>; + +/// Define and reexport submodules. +#[macro_export] macro_rules! submod { + ($($name:ident)*) => { $(mod $name; pub use self::$name::*;)* }; +} + +/// Define public modules. +#[macro_export] macro_rules! pubmod { + ($($name:ident)*) => { $(pub mod $name;)* }; +} + +/// Define test modules. +#[macro_export] macro_rules! testmod { + ($($name:ident)*) => { $(#[cfg(test)] mod $name;)* }; +} + +mod core; pub(crate) use core::*; +mod layout; pub(crate) use layout::*; +mod api; pub(crate) use api::*; +mod tui; pub(crate) use tui::*; + +testmod! { + test +} diff --git a/crates/tek/src/test.rs b/crates/tek/src/test.rs new file mode 100644 index 00000000..fe6a77b2 --- /dev/null +++ b/crates/tek/src/test.rs @@ -0,0 +1,190 @@ +use crate::*; + +struct TestEngine([u16;4], Vec>); + +impl Engine for TestEngine { + type Unit = u16; + type Size = [Self::Unit;2]; + type Area = [Self::Unit;4]; + type Input = Self; + type Handled = bool; + fn exited (&self) -> bool { + true + } + fn area (&self) -> Self::Area { + self.0 + } + fn area_mut (&mut self) -> &mut Self::Area { + &mut self.0 + } +} + +#[derive(Copy, Clone)] +struct TestArea(u16, u16); + +impl Render for TestArea { + type Engine = TestEngine; + fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> { + Ok(Some([to[0], to[1], self.0, self.1])) + } + fn render (&self, to: &mut Self::Engine) -> Perhaps<[u16;4]> { + if let Some(layout) = self.layout(to.area())? { + for y in layout.y()..layout.y()+layout.h()-1 { + for x in layout.x()..layout.x()+layout.w()-1 { + to.1[y as usize][x as usize] = '*'; + } + } + Ok(Some(layout)) + } else { + Ok(None) + } + } +} + +#[test] +fn test_plus_minus () -> Usually<()> { + let area = [0, 0, 10, 10]; + let engine = TestEngine(area, vec![vec![' ';10];10]); + let test = TestArea(4, 4); + assert_eq!(test.layout(area)?, Some([0, 0, 4, 4])); + assert_eq!(Push::X(1, test).layout(area)?, Some([1, 0, 4, 4])); + Ok(()) +} + +#[test] +fn test_outset_align () -> Usually<()> { + let area = [0, 0, 10, 10]; + let engine = TestEngine(area, vec![vec![' ';10];10]); + let test = TestArea(4, 4); + assert_eq!(test.layout(area)?, Some([0, 0, 4, 4])); + assert_eq!(Outset::X(1, test).layout(area)?, Some([0, 0, 6, 4])); + assert_eq!(Align::X(test).layout(area)?, Some([3, 0, 4, 4])); + assert_eq!(Align::X(Outset::X(1, test)).layout(area)?, Some([2, 0, 6, 4])); + assert_eq!(Outset::X(1, Align::X(test)).layout(area)?, Some([2, 0, 6, 4])); + Ok(()) +} + +//#[test] +//fn test_misc () -> Usually<()> { + //let area: [u16;4] = [0, 0, 10, 10]; + //let test = TestArea(4, 4); + //assert_eq!(test.layout(area)?, + //Some([0, 0, 4, 4])); + //assert_eq!(Align::Center(test).layout(area)?, + //Some([3, 3, 4, 4])); + //assert_eq!(Align::Center(Stack::down(|add|{ + //add(&test)?; + //add(&test) + //})).layout(area)?, + //Some([3, 1, 4, 8])); + //assert_eq!(Align::Center(Stack::down(|add|{ + //add(&Outset::XY(2, 2, test))?; + //add(&test) + //})).layout(area)?, + //Some([2, 0, 6, 10])); + //assert_eq!(Align::Center(Stack::down(|add|{ + //add(&Outset::XY(2, 2, test))?; + //add(&Inset::XY(2, 2, test)) + //})).layout(area)?, + //Some([2, 1, 6, 8])); + //assert_eq!(Stack::down(|add|{ + //add(&Outset::XY(2, 2, test))?; + //add(&Inset::XY(2, 2, test)) + //}).layout(area)?, + //Some([0, 0, 6, 8])); + //assert_eq!(Stack::right(|add|{ + //add(&Stack::down(|add|{ + //add(&Outset::XY(2, 2, test))?; + //add(&Inset::XY(2, 2, test)) + //}))?; + //add(&Align::Center(TestArea(2 ,2))) + //}).layout(area)?, + //Some([0, 0, 8, 8])); + //Ok(()) +//} + +//#[test] +//fn test_offset () -> Usually<()> { + //let area: [u16;4] = [50, 50, 100, 100]; + //let test = TestArea(3, 3); + //assert_eq!(Push::X(1, test).layout(area)?, Some([51, 50, 3, 3])); + //assert_eq!(Push::Y(1, test).layout(area)?, Some([50, 51, 3, 3])); + //assert_eq!(Push::XY(1, 1, test).layout(area)?, Some([51, 51, 3, 3])); + //Ok(()) +//} + +//#[test] +//fn test_outset () -> Usually<()> { + //let area: [u16;4] = [50, 50, 100, 100]; + //let test = TestArea(3, 3); + //assert_eq!(Outset::X(1, test).layout(area)?, Some([49, 50, 5, 3])); + //assert_eq!(Outset::Y(1, test).layout(area)?, Some([50, 49, 3, 5])); + //assert_eq!(Outset::XY(1, 1, test).layout(area)?, Some([49, 49, 5, 5])); + //Ok(()) +//} + +//#[test] +//fn test_inset () -> Usually<()> { + //let area: [u16;4] = [50, 50, 100, 100]; + //let test = TestArea(3, 3); + //assert_eq!(Inset::X(1, test).layout(area)?, Some([51, 50, 1, 3])); + //assert_eq!(Inset::Y(1, test).layout(area)?, Some([50, 51, 3, 1])); + //assert_eq!(Inset::XY(1, 1, test).layout(area)?, Some([51, 51, 1, 1])); + //Ok(()) +//} + +//#[test] +//fn test_stuff () -> Usually<()> { + //let area: [u16;4] = [0, 0, 100, 100]; + //assert_eq!("1".layout(area)?, + //Some([0, 0, 1, 1])); + //assert_eq!("333".layout(area)?, + //Some([0, 0, 3, 1])); + //assert_eq!(Layers::new(|add|{add(&"1")?;add(&"333")}).layout(area)?, + //Some([0, 0, 3, 1])); + //assert_eq!(Stack::down(|add|{add(&"1")?;add(&"333")}).layout(area)?, + //Some([0, 0, 3, 2])); + //assert_eq!(Stack::right(|add|{add(&"1")?;add(&"333")}).layout(area)?, + //Some([0, 0, 4, 1])); + //assert_eq!(Stack::down(|add|{ + //add(&Stack::right(|add|{add(&"1")?;add(&"333")}))?; + //add(&"55555") + //}).layout(area)?, + //Some([0, 0, 5, 2])); + //let area: [u16;4] = [1, 1, 100, 100]; + //assert_eq!(Outset::X(1, Stack::right(|add|{add(&"1")?;add(&"333")})).layout(area)?, + //Some([0, 1, 6, 1])); + //assert_eq!(Outset::Y(1, Stack::right(|add|{add(&"1")?;add(&"333")})).layout(area)?, + //Some([1, 0, 4, 3])); + //assert_eq!(Outset::XY(1, 1, Stack::right(|add|{add(&"1")?;add(&"333")})).layout(area)?, + //Some([0, 0, 6, 3])); + //assert_eq!(Stack::down(|add|{ + //add(&Outset::XY(1, 1, "1"))?; + //add(&Outset::XY(1, 1, "333")) + //}).layout(area)?, + //Some([1, 1, 5, 6])); + //let area: [u16;4] = [1, 1, 95, 100]; + //assert_eq!(Align::Center(Stack::down(|add|{ + //add(&Outset::XY(1, 1, "1"))?; + //add(&Outset::XY(1, 1, "333")) + //})).layout(area)?, + //Some([46, 48, 5, 6])); + //assert_eq!(Align::Center(Stack::down(|add|{ + //add(&Layers::new(|add|{ + ////add(&Outset::XY(1, 1, Background(Color::Rgb(0,128,0))))?; + //add(&Outset::XY(1, 1, "1"))?; + //add(&Outset::XY(1, 1, "333"))?; + ////add(&Background(Color::Rgb(0,128,0)))?; + //Ok(()) + //}))?; + //add(&Layers::new(|add|{ + ////add(&Outset::XY(1, 1, Background(Color::Rgb(0,0,128))))?; + //add(&Outset::XY(1, 1, "555"))?; + //add(&Outset::XY(1, 1, "777777"))?; + ////add(&Background(Color::Rgb(0,0,128)))?; + //Ok(()) + //})) + //})).layout(area)?, + //Some([46, 48, 5, 6])); + //Ok(()) +//} diff --git a/crates/tek/src/tui.rs b/crates/tek/src/tui.rs new file mode 100644 index 00000000..645073db --- /dev/null +++ b/crates/tek/src/tui.rs @@ -0,0 +1,270 @@ +use crate::*; + +mod engine_focus; pub(crate) use engine_focus::*; +mod engine_input; pub(crate) use engine_input::*; +mod engine_style; pub(crate) use engine_style::*; +mod engine_theme; pub(crate) use engine_theme::*; +mod engine_output; pub(crate) use engine_output::*; + +//////////////////////////////////////////////////////// + +mod app_transport; pub(crate) use app_transport::*; +mod app_sequencer; pub(crate) use app_sequencer::*; +mod app_arranger; pub(crate) use app_arranger::*; + +//////////////////////////////////////////////////////// + +mod status_bar; pub(crate) use status_bar::*; +mod file_browser; pub(crate) use file_browser::*; +mod phrase_editor; pub(crate) use phrase_editor::*; +mod phrase_length; pub(crate) use phrase_length::*; +mod phrase_rename; pub(crate) use phrase_rename::*; +mod phrase_list; pub(crate) use phrase_list::*; +mod phrase_player; pub(crate) use phrase_player::*; +mod phrase_select; pub(crate) use phrase_select::*; + +//////////////////////////////////////////////////////// + +#[macro_export] macro_rules! render { + (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { + impl $(<$($L),*$($T $(: $U)?),*>)? Render for $Struct $(<$($L),*$($T),*>)? { + fn min_size (&$self, to: [u16;2]) -> Perhaps<[u16;2]> { + $cb.min_size(to) + } + fn render (&$self, to: &mut TuiOutput) -> Usually<()> { + $cb.render(to) + } + } + } +} + +pub fn render Usually<()>+Send+Sync> (render: F) -> impl Render { + Widget::new(|_|Ok(Some([0u16,0u16].into())), render) +} + +//////////////////////////////////////////////////////// + +pub struct Tui { + pub exited: Arc, + pub buffer: Buffer, + pub backend: CrosstermBackend, + pub area: [u16;4], // FIXME auto resize +} + +impl crate::core::Engine for Tui { + type Unit = u16; + type Size = [Self::Unit;2]; + type Area = [Self::Unit;4]; + type Input = TuiInput; + type Handled = bool; + type Output = TuiOutput; + fn exited (&self) -> bool { + self.exited.fetch_and(true, Ordering::Relaxed) + } + fn setup (&mut self) -> Usually<()> { + let better_panic_handler = Settings::auto().verbosity(Verbosity::Full).create_panic_handler(); + std::panic::set_hook(Box::new(move |info: &std::panic::PanicHookInfo|{ + stdout().execute(LeaveAlternateScreen).unwrap(); + CrosstermBackend::new(stdout()).show_cursor().unwrap(); + disable_raw_mode().unwrap(); + better_panic_handler(info); + })); + stdout().execute(EnterAlternateScreen)?; + self.backend.hide_cursor()?; + enable_raw_mode().map_err(Into::into) + } + fn teardown (&mut self) -> Usually<()> { + stdout().execute(LeaveAlternateScreen)?; + self.backend.show_cursor()?; + disable_raw_mode().map_err(Into::into) + } +} + +impl Tui { + /// Run the main loop. + pub fn run + Sized + 'static> ( + state: Arc> + ) -> Usually>> { + let backend = CrosstermBackend::new(stdout()); + let area = backend.size()?; + let engine = Self { + exited: Arc::new(AtomicBool::new(false)), + buffer: Buffer::empty(area), + area: [area.x, area.y, area.width, area.height], + backend, + }; + let engine = Arc::new(RwLock::new(engine)); + let _input_thread = Self::spawn_input_thread(&engine, &state, Duration::from_millis(100)); + engine.write().unwrap().setup()?; + let render_thread = Self::spawn_render_thread(&engine, &state, Duration::from_millis(10)); + render_thread.join().expect("main thread failed"); + engine.write().unwrap().teardown()?; + Ok(state) + } + fn spawn_input_thread + Sized + 'static> ( + engine: &Arc>, state: &Arc>, poll: Duration + ) -> JoinHandle<()> { + let exited = engine.read().unwrap().exited.clone(); + let state = state.clone(); + spawn(move || loop { + if exited.fetch_and(true, Ordering::Relaxed) { + break + } + if ::crossterm::event::poll(poll).is_ok() { + let event = TuiEvent::Input(::crossterm::event::read().unwrap()); + match event { + key!(Ctrl-KeyCode::Char('c')) => { + exited.store(true, Ordering::Relaxed); + }, + _ => { + let exited = exited.clone(); + if let Err(e) = state.write().unwrap().handle(&TuiInput { event, exited }) { + panic!("{e}") + } + } + } + } + }) + } + fn spawn_render_thread + Sized + 'static> ( + engine: &Arc>, state: &Arc>, sleep: Duration + ) -> JoinHandle<()> { + let exited = engine.read().unwrap().exited.clone(); + let engine = engine.clone(); + let state = state.clone(); + let size = engine.read().unwrap().backend.size().expect("get size failed"); + let mut buffer = Buffer::empty(size); + spawn(move || loop { + if exited.fetch_and(true, Ordering::Relaxed) { + break + } + let size = engine.read().unwrap().backend.size() + .expect("get size failed"); + if let Ok(state) = state.try_read() { + if buffer.area != size { + engine.write().unwrap().backend.clear_region(ClearType::All) + .expect("clear failed"); + buffer.resize(size); + buffer.reset(); + } + let mut output = TuiOutput { + buffer, + area: [size.x, size.y, size.width, size.height] + }; + state.render(&mut output).expect("render failed"); + buffer = engine.write().unwrap().flip(output.buffer, size); + } + std::thread::sleep(sleep); + }) + } + fn flip (&mut self, mut buffer: Buffer, size: ratatui::prelude::Rect) -> Buffer { + if self.buffer.area != size { + self.backend.clear_region(ClearType::All).unwrap(); + self.buffer.resize(size); + self.buffer.reset(); + } + let updates = self.buffer.diff(&buffer); + self.backend.draw(updates.into_iter()).expect("failed to render"); + self.backend.flush().expect("failed to flush output buffer"); + std::mem::swap(&mut self.buffer, &mut buffer); + buffer.reset(); + buffer + } +} + +struct Field(&'static str, String); + +render!(|self: Field|{ + Tui::to_east("│", Tui::to_east( + Tui::bold(true, self.0), + Tui::bg(Color::Rgb(0, 0, 0), self.1.as_str()), + )) +}); + +//pub struct TransportView { + //pub(crate) state: Option, + //pub(crate) selected: Option, + //pub(crate) focused: bool, + //pub(crate) bpm: f64, + //pub(crate) sync: f64, + //pub(crate) quant: f64, + //pub(crate) beat: String, + //pub(crate) msu: String, +//} + ////)?; + ////match *state { + ////Some(TransportState::Rolling) => { + ////add(&row!( + ////"│", + ////TuiStyle::fg("▶ PLAYING", Color::Rgb(0, 255, 0)), + ////format!("│0 (0)"), + ////format!("│00m00s000u"), + ////format!("│00B 0b 00/00") + ////))?; + ////add(&row!("│Now ", row!( + ////format!("│0 (0)"), //sample(chunk) + ////format!("│00m00s000u"), //msu + ////format!("│00B 0b 00/00"), //bbt + ////)))?; + ////}, + ////_ => { + ////add(&row!("│", TuiStyle::fg("⏹ STOPPED", Color::Rgb(255, 128, 0))))?; + ////add(&"")?; + ////} + ////} + ////Ok(()) + ////}).fill_x().bg(Color::Rgb(40, 50, 30)) +////}); + +//impl<'a, T: HasClock> From<&'a T> for TransportView where Option: From<&'a T> { + //fn from (state: &'a T) -> Self { + //let selected = state.into(); + //Self { + //selected, + //focused: selected.is_some(), + //state: Some(state.clock().transport.query_state().unwrap()), + //bpm: state.clock().bpm().get(), + //sync: state.clock().sync.get(), + //quant: state.clock().quant.get(), + //beat: state.clock().playhead.format_beat(), + //msu: state.clock().playhead.usec.format_msu(), + //} + //} +//} + + //row!( + ////selected.wrap(TransportFocus::PlayPause, &play_pause.fixed_xy(10, 3)), + //row!( + //col!( + //Field("SR ", format!("192000")), + //Field("BUF ", format!("1024")), + //Field("LEN ", format!("21300")), + //Field("CPU ", format!("00.0%")) + //), + //col!( + //Field("PUL ", format!("000000000")), + //Field("PPQ ", format!("96")), + //Field("BBT ", format!("00B0b00p")) + //), + //col!( + //Field("SEC ", format!("000000.000")), + //Field("BPM ", format!("000.000")), + //Field("MSU ", format!("00m00s00u")) + //), + //), + //selected.wrap(TransportFocus::Bpm, &Outset::X(1u16, { + //row! { + //"BPM ", + //format!("{}.{:03}", *bpm as usize, (bpm * 1000.0) % 1000.0) + //} + //})), + //selected.wrap(TransportFocus::Sync, &Outset::X(1u16, row! { + //"SYNC ", pulses_to_name(*sync as usize) + //})), + //selected.wrap(TransportFocus::Quant, &Outset::X(1u16, row! { + //"QUANT ", pulses_to_name(*quant as usize) + //})), + //selected.wrap(TransportFocus::Clock, &{ + //row!("B" , beat.as_str(), " T", msu.as_str()).outset_x(1) + //}).align_e().fill_x(), + //).fill_x().bg(Color::Rgb(40, 50, 30)) diff --git a/crates/tek/src/tui/_todo_tui_mixer.rs b/crates/tek/src/tui/_todo_tui_mixer.rs new file mode 100644 index 00000000..c8203770 --- /dev/null +++ b/crates/tek/src/tui/_todo_tui_mixer.rs @@ -0,0 +1,251 @@ +use crate::*; + +pub struct Mixer { + /// JACK client handle (needs to not be dropped for standalone mode to work). + pub jack: Arc>, + pub name: String, + pub tracks: Vec>, + pub selected_track: usize, + pub selected_column: usize, +} +impl Mixer { + pub fn new (jack: &Arc>, name: &str) -> Usually { + Ok(Self { + jack: jack.clone(), + name: name.into(), + selected_column: 0, + selected_track: 1, + tracks: vec![], + }) + } + pub fn track_add (&mut self, name: &str, channels: usize) -> Usually<&mut Self> { + let track = Track::new(name)?; + self.tracks.push(track); + Ok(self) + } + pub fn track (&self) -> Option<&Track> { + self.tracks.get(self.selected_track) + } +} + +//pub const ACTIONS: [(&'static str, &'static str);2] = [ + //("+/-", "Adjust"), + //("Ins/Del", "Add/remove track"), +//]; + + +/// A sequencer track. +#[derive(Debug)] +pub struct Track { + pub name: String, + /// Inputs and outputs of 1st and last device + pub ports: JackPorts, + /// Device chain + pub devices: Vec>, + /// Device selector + pub device: usize, +} + +impl Track { + pub fn new (name: &str) -> Usually { + Ok(Self { + name: name.to_string(), + ports: JackPorts::default(), + devices: vec![], + device: 0, + }) + } + fn get_device_mut (&self, i: usize) -> Option>>> { + self.devices.get(i).map(|d|d.state.write().unwrap()) + } + pub fn device_mut (&self) -> Option>>> { + self.get_device_mut(self.device) + } + /// Add a device to the end of the chain. + pub fn append_device (&mut self, device: JackDevice) -> Usually<&mut JackDevice> { + self.devices.push(device); + let index = self.devices.len() - 1; + Ok(&mut self.devices[index]) + } + pub fn add_device (&mut self, device: JackDevice) { + self.devices.push(device); + } + //pub fn connect_first_device (&self) -> Usually<()> { + //if let (Some(port), Some(device)) = (&self.midi_out, self.devices.get(0)) { + //device.client.as_client().connect_ports(&port, &device.midi_ins()?[0])?; + //} + //Ok(()) + //} + //pub fn connect_last_device (&self, app: &Track) -> Usually<()> { + //Ok(match self.devices.get(self.devices.len().saturating_sub(1)) { + //Some(device) => { + //app.audio_out(0).map(|left|device.connect_audio_out(0, &left)).transpose()?; + //app.audio_out(1).map(|right|device.connect_audio_out(1, &right)).transpose()?; + //() + //}, + //None => () + //}) + //} +} + +pub struct TrackView<'a, E: Engine> { + pub chain: Option<&'a Track>, + pub direction: Direction, + pub focused: bool, + pub entered: bool, +} + +impl<'a> Render for TrackView<'a, Tui> { + fn min_size (&self, area: [u16;2]) -> Perhaps<[u16;2]> { + todo!() + } + fn render (&self, to: &mut TuiOutput) -> Usually<()> { + todo!(); + //let mut area = to.area(); + //if let Some(chain) = self.chain { + //match self.direction { + //Direction::Down => area.width = area.width.min(40), + //Direction::Right => area.width = area.width.min(10), + //_ => { unimplemented!() }, + //} + //to.fill_bg(to.area(), Nord::bg_lo(self.focused, self.entered)); + //let mut split = Stack::new(self.direction); + //for device in chain.devices.as_slice().iter() { + //split = split.add_ref(device); + //} + //let (area, areas) = split.render_areas(to)?; + //if self.focused && self.entered && areas.len() > 0 { + //Corners(Style::default().green().not_dim()).draw(to.with_rect(areas[0]))?; + //} + //Ok(Some(area)) + //} else { + //let [x, y, width, height] = area; + //let label = "No chain selected"; + //let x = x + (width - label.len() as u16) / 2; + //let y = y + height / 2; + //to.blit(&label, x, y, Some(Style::default().dim().bold()))?; + //Ok(Some(area)) + //} + } +} + +impl Content for Mixer { + fn content (&self) -> impl Render { + Stack::right(|add| { + for channel in self.tracks.iter() { + add(channel)?; + } + Ok(()) + }) + } +} + +impl Content for Track { + fn content (&self) -> impl Render { + TrackView { + chain: Some(&self), + direction: tek_core::Direction::Right, + focused: true, + entered: true, + //pub channels: u8, + //pub input_ports: Vec>, + //pub pre_gain_meter: f64, + //pub gain: f64, + //pub insert_ports: Vec>, + //pub return_ports: Vec>, + //pub post_gain_meter: f64, + //pub post_insert_meter: f64, + //pub level: f64, + //pub pan: f64, + //pub output_ports: Vec>, + //pub post_fader_meter: f64, + //pub route: String, + } + } +} + +impl Handle for Mixer { + fn handle (&mut self, engine: &TuiInput) -> Perhaps { + if let TuiEvent::Input(crossterm::event::Event::Key(event)) = engine.event() { + + match event.code { + //KeyCode::Char('c') => { + //if event.modifiers == KeyModifiers::CONTROL { + //self.exit(); + //} + //}, + KeyCode::Down => { + self.selected_track = (self.selected_track + 1) % self.tracks.len(); + println!("{}", self.selected_track); + return Ok(Some(true)) + }, + KeyCode::Up => { + if self.selected_track == 0 { + self.selected_track = self.tracks.len() - 1; + } else { + self.selected_track -= 1; + } + println!("{}", self.selected_track); + return Ok(Some(true)) + }, + KeyCode::Left => { + if self.selected_column == 0 { + self.selected_column = 6 + } else { + self.selected_column -= 1; + } + return Ok(Some(true)) + }, + KeyCode::Right => { + if self.selected_column == 6 { + self.selected_column = 0 + } else { + self.selected_column += 1; + } + return Ok(Some(true)) + }, + _ => { + println!("\n{event:?}"); + } + } + + } + Ok(None) + } +} +impl Handle for Track { + fn handle (&mut self, from: &TuiInput) -> Perhaps { + match from.event() { + //, NONE, "chain_cursor_up", "move cursor up", || { + key!(KeyCode::Up) => { + Ok(Some(true)) + }, + // , NONE, "chain_cursor_down", "move cursor down", || { + key!(KeyCode::Down) => { + Ok(Some(true)) + }, + // Left, NONE, "chain_cursor_left", "move cursor left", || { + key!(KeyCode::Left) => { + //if let Some(track) = app.arranger.track_mut() { + //track.device = track.device.saturating_sub(1); + //return Ok(true) + //} + Ok(Some(true)) + }, + // , NONE, "chain_cursor_right", "move cursor right", || { + key!(KeyCode::Right) => { + //if let Some(track) = app.arranger.track_mut() { + //track.device = (track.device + 1).min(track.devices.len().saturating_sub(1)); + //return Ok(true) + //} + Ok(Some(true)) + }, + // , NONE, "chain_mode_switch", "switch the display mode", || { + key!(KeyCode::Char('`')) => { + //app.chain_mode = !app.chain_mode; + Ok(Some(true)) + }, + _ => Ok(None) + } + } +} diff --git a/crates/tek/src/tui/_todo_tui_plugin.rs b/crates/tek/src/tui/_todo_tui_plugin.rs new file mode 100644 index 00000000..54cf0704 --- /dev/null +++ b/crates/tek/src/tui/_todo_tui_plugin.rs @@ -0,0 +1,148 @@ +use crate::*; + +/// A plugin device. +pub struct Plugin { + _engine: PhantomData, + /// JACK client handle (needs to not be dropped for standalone mode to work). + pub jack: Arc>, + pub name: String, + pub path: Option, + pub plugin: Option, + pub selected: usize, + pub mapping: bool, + pub ports: JackPorts, +} + +impl Plugin { + /// Create a plugin host device. + pub fn new ( + jack: &Arc>, + name: &str, + ) -> Usually { + Ok(Self { + _engine: Default::default(), + jack: jack.clone(), + name: name.into(), + path: None, + plugin: None, + selected: 0, + mapping: false, + ports: JackPorts::default() + }) + } +} +impl Render for Plugin { + fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> { + Ok(Some(to)) + } + fn render (&self, to: &mut TuiOutput) -> Usually<()> { + let area = to.area(); + let [x, y, _, height] = area; + let mut width = 20u16; + match &self.plugin { + Some(PluginKind::LV2(LV2Plugin { port_list, instance, .. })) => { + let start = self.selected.saturating_sub((height as usize / 2).saturating_sub(1)); + let end = start + height as usize - 2; + //draw_box(buf, Rect { x, y, width, height }); + for i in start..end { + if let Some(port) = port_list.get(i) { + let value = if let Some(value) = instance.control_input(port.index) { + value + } else { + port.default_value + }; + //let label = &format!("C·· M·· {:25} = {value:.03}", port.name); + let label = &format!("{:25} = {value:.03}", port.name); + width = width.max(label.len() as u16 + 4); + let style = if i == self.selected { + Some(Style::default().green()) + } else { + None + } ; + to.blit(&label, x + 2, y + 1 + i as u16 - start as u16, style); + } else { + break + } + } + }, + _ => {} + }; + draw_header(self, to, x, y, width)?; + Ok(()) + } +} + +fn draw_header (state: &Plugin, to: &mut TuiOutput, x: u16, y: u16, w: u16) -> Usually { + let style = Style::default().gray(); + let label1 = format!(" {}", state.name); + to.blit(&label1, x + 1, y, Some(style.white().bold())); + if let Some(ref path) = state.path { + let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]); + to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim())); + } + Ok(Rect { x, y, width: w, height: 1 }) +} + +impl Handle for Plugin { + fn handle (&mut self, from: &TuiInput) -> Perhaps { + match from.event() { + key!(KeyCode::Up) => { + self.selected = self.selected.saturating_sub(1); + Ok(Some(true)) + }, + key!(KeyCode::Down) => { + self.selected = (self.selected + 1).min(match &self.plugin { + Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1, + _ => unimplemented!() + }); + Ok(Some(true)) + }, + key!(KeyCode::PageUp) => { + self.selected = self.selected.saturating_sub(8); + Ok(Some(true)) + }, + key!(KeyCode::PageDown) => { + self.selected = (self.selected + 10).min(match &self.plugin { + Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1, + _ => unimplemented!() + }); + Ok(Some(true)) + }, + key!(KeyCode::Char(',')) => { + match self.plugin.as_mut() { + Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => { + let index = port_list[self.selected].index; + if let Some(value) = instance.control_input(index) { + instance.set_control_input(index, value - 0.01); + } + }, + _ => {} + } + Ok(Some(true)) + }, + key!(KeyCode::Char('.')) => { + match self.plugin.as_mut() { + Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => { + let index = port_list[self.selected].index; + if let Some(value) = instance.control_input(index) { + instance.set_control_input(index, value + 0.01); + } + }, + _ => {} + } + Ok(Some(true)) + }, + key!(KeyCode::Char('g')) => { + match self.plugin { + Some(PluginKind::LV2(ref mut plugin)) => { + plugin.ui_thread = Some(run_lv2_ui(LV2PluginUI::new()?)?); + }, + Some(_) => unreachable!(), + None => {} + } + Ok(Some(true)) + }, + _ => Ok(None) + } + } +} diff --git a/crates/tek/src/tui/_todo_tui_plugin_lv2.rs b/crates/tek/src/tui/_todo_tui_plugin_lv2.rs new file mode 100644 index 00000000..a71fea12 --- /dev/null +++ b/crates/tek/src/tui/_todo_tui_plugin_lv2.rs @@ -0,0 +1,46 @@ +use super::*; +use ::livi::{ + World, + Instance, + Plugin as LiviPlugin, + Features, + FeaturesBuilder, + Port, + event::LV2AtomSequence, +}; +use std::thread::JoinHandle; + +/// A LV2 plugin. +pub struct LV2Plugin { + pub world: World, + pub instance: Instance, + pub plugin: LiviPlugin, + pub features: Arc, + pub port_list: Vec, + pub input_buffer: Vec, + pub ui_thread: Option>, +} + +impl LV2Plugin { + const INPUT_BUFFER: usize = 1024; + pub fn new (uri: &str) -> Usually { + // Get 1st plugin at URI + let world = World::with_load_bundle(&uri); + let features = FeaturesBuilder { min_block_length: 1, max_block_length: 65536 }; + let features = world.build_features(features); + let mut plugin = None; + if let Some(p) = world.iter_plugins().next() { plugin = Some(p); } + let plugin = plugin.expect("plugin not found"); + let err = &format!("init {uri}"); + let instance = unsafe { plugin.instantiate(features.clone(), 48000.0).expect(&err) }; + let mut port_list = vec![]; + for port in plugin.ports() { + port_list.push(port); + } + let input_buffer = Vec::with_capacity(Self::INPUT_BUFFER); + // Instantiate + Ok(Self { + world, instance, port_list, plugin, features, input_buffer, ui_thread: None + }) + } +} diff --git a/crates/tek/src/tui/_todo_tui_plugin_lv2_gui.rs b/crates/tek/src/tui/_todo_tui_plugin_lv2_gui.rs new file mode 100644 index 00000000..a296eee5 --- /dev/null +++ b/crates/tek/src/tui/_todo_tui_plugin_lv2_gui.rs @@ -0,0 +1,58 @@ +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/device/vst2.rs b/crates/tek/src/tui/_todo_tui_plugin_vst2.rs similarity index 99% rename from device/vst2.rs rename to crates/tek/src/tui/_todo_tui_plugin_vst2.rs index 6f50cf96..cb04e7e0 100644 --- a/device/vst2.rs +++ b/crates/tek/src/tui/_todo_tui_plugin_vst2.rs @@ -11,4 +11,3 @@ fn set_vst_plugin (host: &Arc>>, _path: &str) -> Usu instance: loader.instance()? }) } - diff --git a/crates/tek/src/tui/_todo_tui_plugin_vst3.rs b/crates/tek/src/tui/_todo_tui_plugin_vst3.rs new file mode 100644 index 00000000..46330df3 --- /dev/null +++ b/crates/tek/src/tui/_todo_tui_plugin_vst3.rs @@ -0,0 +1 @@ +//! TODO diff --git a/crates/tek/src/tui/_todo_tui_sampler.rs b/crates/tek/src/tui/_todo_tui_sampler.rs new file mode 100644 index 00000000..791144dc --- /dev/null +++ b/crates/tek/src/tui/_todo_tui_sampler.rs @@ -0,0 +1,412 @@ +use crate::*; + +/// The sampler plugin plays sounds. +pub struct SamplerView { + _engine: PhantomData, + pub state: Sampler, + pub cursor: (usize, usize), + pub editing: Option>>, + pub buffer: Vec>, + pub modal: Arc>>>, +} + +impl SamplerView { + pub fn new ( + jack: &Arc>, + name: &str, + mapped: Option>>> + ) -> Usually> { + Jack::new(name)? + .midi_in("midi") + .audio_in("recL") + .audio_in("recR") + .audio_out("outL") + .audio_out("outR") + .run(|ports|Box::new(Self { + _engine: Default::default(), + jack: jack.clone(), + name: name.into(), + cursor: (0, 0), + editing: None, + mapped: mapped.unwrap_or_else(||BTreeMap::new()), + unmapped: vec![], + voices: Arc::new(RwLock::new(vec![])), + ports, + buffer: vec![vec![0.0;16384];2], + output_gain: 0.5, + modal: Default::default() + })) + } + /// Immutable reference to sample at cursor. + pub fn sample (&self) -> Option<&Arc>> { + for (i, sample) in self.mapped.values().enumerate() { + if i == self.cursor.0 { + return Some(sample) + } + } + for (i, sample) in self.unmapped.iter().enumerate() { + if i + self.mapped.len() == self.cursor.0 { + return Some(sample) + } + } + None + } +} + +/// A sound sample. +#[derive(Default, Debug)] +pub struct Sample { + pub name: String, + pub start: usize, + pub end: usize, + pub channels: Vec>, + pub rate: Option, +} + +/// Load sample from WAV and assign to MIDI note. +#[macro_export] macro_rules! sample { + ($note:expr, $name:expr, $src:expr) => {{ + let (end, data) = read_sample_data($src)?; + ( + u7::from_int_lossy($note).into(), + Sample::new($name, 0, end, data).into() + ) + }}; +} + +use std::fs::File; +use symphonia::core::codecs::CODEC_TYPE_NULL; +use symphonia::core::errors::Error; +use symphonia::core::io::MediaSourceStream; +use symphonia::core::probe::Hint; +use symphonia::core::audio::SampleBuffer; +use symphonia::default::get_codecs; + +pub struct AddSampleModal { + exited: bool, + dir: PathBuf, + subdirs: Vec, + files: Vec, + cursor: usize, + offset: usize, + sample: Arc>, + voices: Arc>>, + _search: Option, +} + +impl Exit for AddSampleModal { + fn exited (&self) -> bool { + self.exited + } + fn exit (&mut self) { + self.exited = true + } +} + +impl AddSampleModal { + pub fn new ( + sample: &Arc>, + voices: &Arc>> + ) -> Usually { + let dir = std::env::current_dir()?; + let (subdirs, files) = scan(&dir)?; + Ok(Self { + exited: false, + dir, + subdirs, + files, + cursor: 0, + offset: 0, + sample: sample.clone(), + voices: voices.clone(), + _search: None + }) + } + fn rescan (&mut self) -> Usually<()> { + scan(&self.dir).map(|(subdirs, files)|{ + self.subdirs = subdirs; + self.files = files; + }) + } + fn prev (&mut self) { + self.cursor = self.cursor.saturating_sub(1); + } + fn next (&mut self) { + self.cursor = self.cursor + 1; + } + fn try_preview (&mut self) -> Usually<()> { + if let Some(path) = self.cursor_file() { + if let Ok(sample) = Sample::from_file(&path) { + *self.sample.write().unwrap() = sample; + self.voices.write().unwrap().push( + Sample::play(&self.sample, 0, &u7::from(100u8)) + ); + } + //load_sample(&path)?; + //let src = std::fs::File::open(&path)?; + //let mss = MediaSourceStream::new(Box::new(src), Default::default()); + //let mut hint = Hint::new(); + //if let Some(ext) = path.extension() { + //hint.with_extension(&ext.to_string_lossy()); + //} + //let meta_opts: MetadataOptions = Default::default(); + //let fmt_opts: FormatOptions = Default::default(); + //if let Ok(mut probed) = symphonia::default::get_probe() + //.format(&hint, mss, &fmt_opts, &meta_opts) + //{ + //panic!("{:?}", probed.format.metadata()); + //}; + } + Ok(()) + } + fn cursor_dir (&self) -> Option { + if self.cursor < self.subdirs.len() { + Some(self.dir.join(&self.subdirs[self.cursor])) + } else { + None + } + } + fn cursor_file (&self) -> Option { + if self.cursor < self.subdirs.len() { + return None + } + let index = self.cursor.saturating_sub(self.subdirs.len()); + if index < self.files.len() { + Some(self.dir.join(&self.files[index])) + } else { + None + } + } + fn pick (&mut self) -> Usually { + if self.cursor == 0 { + if let Some(parent) = self.dir.parent() { + self.dir = parent.into(); + self.rescan()?; + self.cursor = 0; + return Ok(false) + } + } + if let Some(dir) = self.cursor_dir() { + self.dir = dir; + self.rescan()?; + self.cursor = 0; + return Ok(false) + } + if let Some(path) = self.cursor_file() { + let (end, channels) = read_sample_data(&path.to_string_lossy())?; + let mut sample = self.sample.write().unwrap(); + sample.name = path.file_name().unwrap().to_string_lossy().into(); + sample.end = end; + sample.channels = channels; + return Ok(true) + } + return Ok(false) + } +} + +pub const KEYMAP_ADD_SAMPLE: &'static [KeyBinding] = keymap!(AddSampleModal { + [Esc, NONE, "sampler/add/close", "close help dialog", |modal: &mut AddSampleModal|{ + modal.exit(); + Ok(true) + }], + [Up, NONE, "sampler/add/prev", "select previous entry", |modal: &mut AddSampleModal|{ + modal.prev(); + Ok(true) + }], + [Down, NONE, "sampler/add/next", "select next entry", |modal: &mut AddSampleModal|{ + modal.next(); + Ok(true) + }], + [Enter, NONE, "sampler/add/enter", "activate selected entry", |modal: &mut AddSampleModal|{ + if modal.pick()? { + modal.exit(); + } + Ok(true) + }], + [Char('p'), NONE, "sampler/add/preview", "preview selected entry", |modal: &mut AddSampleModal|{ + modal.try_preview()?; + Ok(true) + }] +}); + +fn scan (dir: &PathBuf) -> Usually<(Vec, Vec)> { + let (mut subdirs, mut files) = read_dir(dir)? + .fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{ + let entry = entry.expect("failed to read drectory entry"); + let meta = entry.metadata().expect("failed to read entry metadata"); + if meta.is_file() { + files.push(entry.file_name()); + } else if meta.is_dir() { + subdirs.push(entry.file_name()); + } + (subdirs, files) + }); + subdirs.sort(); + files.sort(); + Ok((subdirs, files)) +} + +impl Sample { + fn from_file (path: &PathBuf) -> Usually { + let mut sample = Self::default(); + sample.name = path.file_name().unwrap().to_string_lossy().into(); + // Use file extension if present + let mut hint = Hint::new(); + if let Some(ext) = path.extension() { + hint.with_extension(&ext.to_string_lossy()); + } + let probed = symphonia::default::get_probe().format( + &hint, + MediaSourceStream::new( + Box::new(File::open(path)?), + Default::default(), + ), + &Default::default(), + &Default::default() + )?; + let mut format = probed.format; + let mut decoder = get_codecs().make( + &format.tracks().iter() + .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) + .expect("no tracks found") + .codec_params, + &Default::default() + )?; + loop { + match format.next_packet() { + Ok(packet) => { + // Decode a packet + let decoded = match decoder.decode(&packet) { + Ok(decoded) => decoded, + Err(err) => { return Err(err.into()); } + }; + // Determine sample rate + let spec = *decoded.spec(); + if let Some(rate) = sample.rate { + if rate != spec.rate as usize { + panic!("sample rate changed"); + } + } else { + sample.rate = Some(spec.rate as usize); + } + // Determine channel count + while sample.channels.len() < spec.channels.count() { + sample.channels.push(vec![]); + } + // Load sample + let mut samples = SampleBuffer::new( + decoded.frames() as u64, + spec + ); + if samples.capacity() > 0 { + samples.copy_interleaved_ref(decoded); + for frame in samples.samples().chunks(spec.channels.count()) { + for (chan, frame) in frame.iter().enumerate() { + sample.channels[chan].push(*frame) + } + } + } + }, + Err(Error::IoError(_)) => break decoder.last_decoded(), + Err(err) => return Err(err.into()), + }; + }; + sample.end = sample.channels.iter().fold(0, |l, c|l + c.len()); + Ok(sample) + } +} + +impl Render for SamplerView { + fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> { + todo!() + } + fn render (&self, to: &mut TuiOutput) -> Usually<()> { + tui_render_sampler(self, to) + } +} + +pub fn tui_render_sampler (sampler: &SamplerView, to: &mut TuiOutput) -> Usually<()> { + let [x, y, _, height] = to.area(); + let style = Style::default().gray(); + let title = format!(" {} ({})", sampler.name, sampler.voices.read().unwrap().len()); + to.blit(&title, x+1, y, Some(style.white().bold().not_dim())); + let mut width = title.len() + 2; + let mut y1 = 1; + let mut j = 0; + for (note, sample) in sampler.mapped.iter() + .map(|(note, sample)|(Some(note), sample)) + .chain(sampler.unmapped.iter().map(|sample|(None, sample))) + { + if y1 >= height { + break + } + let active = j == sampler.cursor.0; + width = width.max( + draw_sample(to, x, y + y1, note, &*sample.read().unwrap(), active)? + ); + y1 = y1 + 1; + j = j + 1; + } + let height = ((2 + y1) as u16).min(height); + //Ok(Some([x, y, (width as u16).min(to.area().w()), height])) + Ok(()) +} + +fn draw_sample ( + to: &mut TuiOutput, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool +) -> Usually { + let style = if focus { Style::default().green() } else { Style::default() }; + if focus { + to.blit(&"🬴", x+1, y, Some(style.bold())); + } + let label1 = format!("{:3} {:12}", + note.map(|n|n.to_string()).unwrap_or(String::default()), + sample.name); + let label2 = format!("{:>6} {:>6} +0.0", + sample.start, + sample.end); + to.blit(&label1, x+2, y, Some(style.bold())); + to.blit(&label2, x+3+label1.len()as u16, y, Some(style)); + Ok(label1.len() + label2.len() + 4) +} + +impl Render for AddSampleModal { + fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> { + todo!() + //Align::Center(()).layout(to) + } + fn render (&self, to: &mut TuiOutput) -> Usually<()> { + todo!() + //let area = to.area(); + //to.make_dim(); + //let area = center_box( + //area, + //64.max(area.w().saturating_sub(8)), + //20.max(area.w().saturating_sub(8)), + //); + //to.fill_fg(area, Color::Reset); + //to.fill_bg(area, Nord::bg_lo(true, true)); + //to.fill_char(area, ' '); + //to.blit(&format!("{}", &self.dir.to_string_lossy()), area.x()+2, area.y()+1, Some(Style::default().bold()))?; + //to.blit(&"Select sample:", area.x()+2, area.y()+2, Some(Style::default().bold()))?; + //for (i, (is_dir, name)) in self.subdirs.iter() + //.map(|path|(true, path)) + //.chain(self.files.iter().map(|path|(false, path))) + //.enumerate() + //.skip(self.offset) + //{ + //if i >= area.h() as usize - 4 { + //break + //} + //let t = if is_dir { "" } else { "" }; + //let line = format!("{t} {}", name.to_string_lossy()); + //let line = &line[..line.len().min(area.w() as usize - 4)]; + //to.blit(&line, area.x() + 2, area.y() + 3 + i as u16, Some(if i == self.cursor { + //Style::default().green() + //} else { + //Style::default().white() + //}))?; + //} + //Lozenge(Style::default()).draw(to) + } +} diff --git a/crates/tek/src/tui/_todo_tui_sampler_cmd.rs b/crates/tek/src/tui/_todo_tui_sampler_cmd.rs new file mode 100644 index 00000000..a1b9e16a --- /dev/null +++ b/crates/tek/src/tui/_todo_tui_sampler_cmd.rs @@ -0,0 +1,52 @@ +use crate::*; +impl Handle for Sampler { + fn handle (&mut self, from: &TuiInput) -> Perhaps { + match from.event() { + key!(KeyCode::Up) => { + self.cursor.0 = if self.cursor.0 == 0 { + self.mapped.len() + self.unmapped.len() - 1 + } else { + self.cursor.0 - 1 + }; + Ok(Some(true)) + }, + key!(KeyCode::Down) => { + self.cursor.0 = (self.cursor.0 + 1) % (self.mapped.len() + self.unmapped.len()); + Ok(Some(true)) + }, + key!(KeyCode::Char('p')) => { + if let Some(sample) = self.sample() { + self.voices.write().unwrap().push(Sample::play(sample, 0, &100.into())); + } + Ok(Some(true)) + }, + key!(KeyCode::Char('a')) => { + let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![]))); + *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &self.voices)?)); + self.unmapped.push(sample); + Ok(Some(true)) + }, + key!(KeyCode::Char('r')) => { + if let Some(sample) = self.sample() { + *self.modal.lock().unwrap() = Some(Exit::boxed(AddSampleModal::new(&sample, &self.voices)?)); + } + Ok(Some(true)) + }, + key!(KeyCode::Enter) => { + if let Some(sample) = self.sample() { + self.editing = Some(sample.clone()); + } + Ok(Some(true)) + } + _ => Ok(None) + } + } +} +impl Handle for AddSampleModal { + fn handle (&mut self, from: &TuiInput) -> Perhaps { + if from.handle_keymap(self, KEYMAP_ADD_SAMPLE)? { + return Ok(Some(true)) + } + Ok(Some(true)) + } +} diff --git a/crates/tek/src/tui/app_arranger.rs b/crates/tek/src/tui/app_arranger.rs new file mode 100644 index 00000000..f5cc1afe --- /dev/null +++ b/crates/tek/src/tui/app_arranger.rs @@ -0,0 +1,1311 @@ +use crate::{ + *, + api::{ + ArrangerTrackCommand, + ArrangerSceneCommand, + ArrangerClipCommand + } +}; + + +impl TryFrom<&Arc>> for ArrangerTui { + type Error = Box; + fn try_from (jack: &Arc>) -> Usually { + Ok(Self { + jack: jack.clone(), + clock: ClockModel::from(jack), + phrases: PhraseListModel::default(), + editor: PhraseEditorModel::default(), + selected: ArrangerSelection::Clip(0, 0), + scenes: vec![], + tracks: vec![], + color: Color::Rgb(28, 35, 25).into(), + history: vec![], + mode: ArrangerMode::Vertical(2), + name: Arc::new(RwLock::new(String::new())), + size: Measure::new(), + cursor: (0, 0), + splits: [20, 20], + entered: false, + menu_bar: None, + status_bar: None, + midi_buf: vec![vec![];65536], + note_buf: vec![], + perf: PerfModel::default(), + focus: FocusState::Entered(ArrangerFocus::Transport(TransportFocus::PlayPause)), + }) + } +} + +/// Root view for standalone `tek_arranger` +pub struct ArrangerTui { + pub jack: Arc>, + pub clock: ClockModel, + pub phrases: PhraseListModel, + pub tracks: Vec, + pub scenes: Vec, + pub name: Arc>, + pub splits: [u16;2], + pub selected: ArrangerSelection, + pub mode: ArrangerMode, + pub color: ItemColor, + pub entered: bool, + pub size: Measure, + pub cursor: (usize, usize), + pub menu_bar: Option>, + pub status_bar: Option, + pub history: Vec, + pub note_buf: Vec, + pub midi_buf: Vec>>, + pub editor: PhraseEditorModel, + pub focus: FocusState, + pub perf: PerfModel, +} + +impl JackApi for ArrangerTui { + fn jack (&self) -> &Arc> { + &self.jack + } +} + +impl Audio for ArrangerTui { + #[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { + // Start profiling cycle + let t0 = self.perf.get_t0(); + + // Update transport clock + if ClockAudio(self).process(client, scope) == Control::Quit { + return Control::Quit + } + + // Update MIDI sequencers + if TracksAudio( + &mut self.tracks, + &mut self.note_buf, + &mut self.midi_buf, + Default::default(), + ).process(client, scope) == Control::Quit { + return Control::Quit + } + + // FIXME: one of these per playing track + //self.now.set(0.); + //if let ArrangerSelection::Clip(t, s) = self.selected { + //let phrase = self.scenes().get(s).map(|scene|scene.clips.get(t)); + //if let Some(Some(Some(phrase))) = phrase { + //if let Some(track) = self.tracks().get(t) { + //if let Some((ref started_at, Some(ref playing))) = track.player.play_phrase { + //let phrase = phrase.read().unwrap(); + //if *playing.read().unwrap() == *phrase { + //let pulse = self.current().pulse.get(); + //let start = started_at.pulse.get(); + //let now = (pulse - start) % phrase.length as f64; + //self.now.set(now); + //} + //} + //} + //} + //} + + // End profiling cycle + self.perf.update(t0, scope); + + return Control::Continue + } +} + +// Layout for standalone arranger app. +render!(|self: ArrangerTui|{ + let arranger_focused = self.arranger_focused(); + let border = Lozenge(Style::default().bg(TuiTheme::border_bg()).fg(TuiTheme::border_fg(arranger_focused))); + col!([ + TransportView::from((self, if let ArrangerFocus::Transport(_) = self.focus.inner() { + true + } else { + false + })), + col!([ + Tui::fixed_y(self.splits[0], lay!([ + border.wrap(Tui::grow_y(1, Layers::new(move |add|{ + match self.mode { + ArrangerMode::Horizontal => + add(&arranger_content_horizontal(self))?, + ArrangerMode::Vertical(factor) => + add(&arranger_content_vertical(self, factor))? + }; + add(&self.size) + }))), + self.size, + Tui::push_x(1, Tui::fg( + TuiTheme::title_fg(arranger_focused), + format!("[{}] Arranger", if self.entered { + "■" + } else { + " " + }) + )) + ])), + Split::right( + self.splits[1], + PhraseListView::from(self), + PhraseView::from(self), + ) + ]) + ]) +}); + +impl HasClock for ArrangerTui { + fn clock (&self) -> &ClockModel { + &self.clock + } +} +impl HasClock for ArrangerTrack { + fn clock (&self) -> &ClockModel { + &self.player.clock() + } +} +impl HasPhrases for ArrangerTui { + fn phrases (&self) -> &Vec>> { + &self.phrases.phrases + } + fn phrases_mut (&mut self) -> &mut Vec>> { + &mut self.phrases.phrases + } +} + +/// Sections in the arranger app that may be focused +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum ArrangerFocus { + /// The transport (toolbar) is focused + Transport(TransportFocus), + /// The arrangement (grid) is focused + Arranger, + /// The phrase list (pool) is focused + Phrases, + /// The phrase editor (sequencer) is focused + PhraseEditor, +} + +impl From<&ArrangerTui> for Option { + fn from (state: &ArrangerTui) -> Self { + match state.focus.inner() { + ArrangerFocus::Transport(focus) => Some(focus), + _ => None + } + } +} + +impl_focus!(ArrangerTui ArrangerFocus [ + //&[ + //Menu, + //Menu, + //Menu, + //Menu, + //Menu, + //], + &[ + Transport(TransportFocus::PlayPause), + Transport(TransportFocus::Bpm), + Transport(TransportFocus::Sync), + Transport(TransportFocus::Quant), + Transport(TransportFocus::Clock), + ], &[ + Arranger, + Arranger, + Arranger, + Arranger, + Arranger, + ], &[ + Phrases, + Phrases, + PhraseEditor, + PhraseEditor, + PhraseEditor, + ], +]); + +/// Status bar for arranger app +#[derive(Copy, Clone, Debug)] +pub enum ArrangerStatus { + Transport, + ArrangerMix, + ArrangerTrack, + ArrangerScene, + ArrangerClip, + PhrasePool, + PhraseView, + PhraseEdit, +} + +impl StatusBar for ArrangerStatus { + type State = (ArrangerFocus, ArrangerSelection, bool); + fn hotkey_fg () -> Color where Self: Sized { + TuiTheme::HOTKEY_FG + } + fn update (&mut self, (focused, selected, entered): &Self::State) { + *self = match focused { + //ArrangerFocus::Menu => { todo!() }, + ArrangerFocus::Transport(_) => ArrangerStatus::Transport, + ArrangerFocus::Arranger => match selected { + ArrangerSelection::Mix => ArrangerStatus::ArrangerMix, + ArrangerSelection::Track(_) => ArrangerStatus::ArrangerTrack, + ArrangerSelection::Scene(_) => ArrangerStatus::ArrangerScene, + ArrangerSelection::Clip(_, _) => ArrangerStatus::ArrangerClip, + }, + ArrangerFocus::Phrases => ArrangerStatus::PhrasePool, + ArrangerFocus::PhraseEditor => match entered { + true => ArrangerStatus::PhraseEdit, + false => ArrangerStatus::PhraseView, + }, + } + } +} + +render!(|self: ArrangerStatus|{ + + let label = match self { + Self::Transport => "TRANSPORT", + Self::ArrangerMix => "PROJECT", + Self::ArrangerTrack => "TRACK", + Self::ArrangerScene => "SCENE", + Self::ArrangerClip => "CLIP", + Self::PhrasePool => "SEQ LIST", + Self::PhraseView => "VIEW SEQ", + Self::PhraseEdit => "EDIT SEQ", + }; + + let status_bar_bg = TuiTheme::status_bar_bg(); + + let mode_bg = TuiTheme::mode_bg(); + let mode_fg = TuiTheme::mode_fg(); + let mode = Tui::fg(mode_fg, Tui::bg(mode_bg, Tui::bold(true, format!(" {label} ")))); + + let commands = match self { + Self::ArrangerMix => Self::command(&[ + ["", "c", "olor"], + ["", "<>", "resize"], + ["", "+-", "zoom"], + ["", "n", "ame/number"], + ["", "Enter", " stop all"], + ]), + Self::ArrangerClip => Self::command(&[ + ["", "g", "et"], + ["", "s", "et"], + ["", "a", "dd"], + ["", "i", "ns"], + ["", "d", "up"], + ["", "e", "dit"], + ["", "c", "olor"], + ["re", "n", "ame"], + ["", ",.", "select"], + ["", "Enter", " launch"], + ]), + Self::ArrangerTrack => Self::command(&[ + ["re", "n", "ame"], + ["", ",.", "resize"], + ["", "<>", "move"], + ["", "i", "nput"], + ["", "o", "utput"], + ["", "m", "ute"], + ["", "s", "olo"], + ["", "Del", "ete"], + ["", "Enter", " stop"], + ]), + Self::ArrangerScene => Self::command(&[ + ["re", "n", "ame"], + ["", "Del", "ete"], + ["", "Enter", " launch"], + ]), + Self::PhrasePool => Self::command(&[ + ["", "a", "ppend"], + ["", "i", "nsert"], + ["", "d", "uplicate"], + ["", "Del", "ete"], + ["", "c", "olor"], + ["re", "n", "ame"], + ["leng", "t", "h"], + ["", ",.", "move"], + ["", "+-", "resize view"], + ]), + Self::PhraseView => Self::command(&[ + ["", "enter", " edit"], + ["", "arrows/pgup/pgdn", " scroll"], + ["", "+=", "zoom"], + ]), + Self::PhraseEdit => Self::command(&[ + ["", "esc", " exit"], + ["", "a", "ppend"], + ["", "s", "et"], + ["", "][", "length"], + ["", "+-", "zoom"], + ]), + _ => Self::command(&[]) + }; + + //let commands = commands.iter().reduce(String::new(), |s, (a, b, c)| format!("{s} {a}{b}{c}")); + Tui::bg(status_bar_bg, Tui::fill_x(row!([mode, commands]))) + +}); + +/// Display mode of arranger +#[derive(Clone, PartialEq)] +pub enum ArrangerMode { + /// Tracks are rows + Horizontal, + /// Tracks are columns + Vertical(usize), +} + +/// Arranger display mode can be cycled +impl ArrangerMode { + /// Cycle arranger display mode + pub fn to_next (&mut self) { + *self = match self { + Self::Horizontal => Self::Vertical(1), + Self::Vertical(1) => Self::Vertical(2), + Self::Vertical(2) => Self::Vertical(2), + Self::Vertical(0) => Self::Horizontal, + Self::Vertical(_) => Self::Vertical(0), + } + } +} + +pub trait ArrangerViewState { + fn arranger_focused (&self) -> bool; +} +impl ArrangerViewState for ArrangerTui { + fn arranger_focused (&self) -> bool { + self.focused() == ArrangerFocus::Arranger + } +} + +fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> { + let mut widths = vec![]; + let mut total = 0; + for track in tracks.iter() { + let width = track.width; + widths.push((width, total)); + total += width; + } + widths.push((0, total)); + widths +} + +fn any_size (_: E::Size) -> Perhaps{ + Ok(Some([0.into(),0.into()].into())) +} + +pub fn arranger_content_vertical ( + view: &ArrangerTui, + factor: usize +) -> impl Render + use<'_> { + lay!([ + Tui::at_se(Tui::fill_xy(Tui::pull_x(1, Tui::fg(TuiTheme::title_fg(view.arranger_focused()), + format!("{}x{}", view.size.w(), view.size.h())) + ))), + Tui::bg(view.color.rgb, lay!(![ + ArrangerVerticalColumnSeparator::from(view), + ArrangerVerticalRowSeparator::from((view, factor)), + col!(![ + ArrangerVerticalHeader::from(view), + ArrangerVerticalContent::from((view, factor)), + ]), + ArrangerVerticalCursor::from((view, factor)), + ])), + ]) +} + +struct ArrangerVerticalColumnSeparator { + cols: Vec<(usize, usize)>, + scenes_w: u16, + sep_fg: Color, +} +impl From<&ArrangerTui> for ArrangerVerticalColumnSeparator { + fn from (state: &ArrangerTui) -> Self { + Self { + cols: track_widths(state.tracks()), + scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, + sep_fg: TuiTheme::separator_fg(false), + } + } +} +render!(|self: ArrangerVerticalColumnSeparator|render(move|to: &mut TuiOutput|{ + let style = Some(Style::default().fg(self.sep_fg)); + Ok(for x in self.cols.iter().map(|col|col.1) { + let x = self.scenes_w + to.area().x() + x as u16; + for y in to.area().y()..to.area().y2() { + to.blit(&"▎", x, y, style); + } + }) +})); + +struct ArrangerVerticalRowSeparator { + rows: Vec<(usize, usize)>, + sep_fg: Color, +} +impl From<(&ArrangerTui, usize)> for ArrangerVerticalRowSeparator { + fn from ((state, factor): (&ArrangerTui, usize)) -> Self { + Self { + rows: ArrangerScene::ppqs(state.scenes(), factor), + sep_fg: TuiTheme::separator_fg(false), + } + } +} + +render!(|self: ArrangerVerticalRowSeparator|render(move|to: &mut TuiOutput|{ + Ok(for y in self.rows.iter().map(|row|row.1) { + let y = to.area().y() + (y / PPQ) as u16 + 1; + if y >= to.buffer.area.height { break } + for x in to.area().x()..to.area().x2().saturating_sub(2) { + if x < to.buffer.area.x && y < to.buffer.area.y { + let cell = to.buffer.get_mut(x, y); + cell.modifier = Modifier::UNDERLINED; + cell.underline_color = self.sep_fg; + } + } + }) +})); + +struct ArrangerVerticalCursor { + cols: Vec<(usize, usize)>, + rows: Vec<(usize, usize)>, + focused: bool, + selected: ArrangerSelection, + scenes_w: u16, + header_h: u16, +} +impl From<(&ArrangerTui, usize)> for ArrangerVerticalCursor { + fn from ((state, factor): (&ArrangerTui, usize)) -> Self { + Self { + cols: track_widths(state.tracks()), + rows: ArrangerScene::ppqs(state.scenes(), factor), + focused: state.arranger_focused(), + selected: state.selected, + scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, + header_h: 3, + } + } +} +render!(|self: ArrangerVerticalCursor|render(move|to: &mut TuiOutput|{ + let area = to.area(); + let focused = self.focused; + let selected = self.selected; + let get_track_area = |t: usize| [ + self.scenes_w + area.x() + self.cols[t].1 as u16, area.y(), + self.cols[t].0 as u16, area.h(), + ]; + let get_scene_area = |s: usize| [ + area.x(), self.header_h + area.y() + (self.rows[s].1 / PPQ) as u16, + area.w(), (self.rows[s].0 / PPQ) as u16 + ]; + let get_clip_area = |t: usize, s: usize| [ + self.scenes_w + area.x() + self.cols[t].1 as u16, + self.header_h + area.y() + (self.rows[s].1/PPQ) as u16, + self.cols[t].0 as u16, + (self.rows[s].0 / PPQ) as u16 + ]; + let mut track_area: Option<[u16;4]> = None; + let mut scene_area: Option<[u16;4]> = None; + let mut clip_area: Option<[u16;4]> = None; + let area = match selected { + ArrangerSelection::Mix => area, + ArrangerSelection::Track(t) => { + track_area = Some(get_track_area(t)); + area + }, + ArrangerSelection::Scene(s) => { + scene_area = Some(get_scene_area(s)); + area + }, + ArrangerSelection::Clip(t, s) => { + track_area = Some(get_track_area(t)); + scene_area = Some(get_scene_area(s)); + clip_area = Some(get_clip_area(t, s)); + area + }, + }; + let bg = TuiTheme::border_bg(); + if let Some([x, y, width, height]) = track_area { + to.fill_fg([x, y, 1, height], bg); + to.fill_fg([x + width, y, 1, height], bg); + } + if let Some([_, y, _, height]) = scene_area { + to.fill_ul([area.x(), y - 1, area.w(), 1], bg); + to.fill_ul([area.x(), y + height - 1, area.w(), 1], bg); + } + Ok(if focused { + to.render_in(if let Some(clip_area) = clip_area { clip_area } + else if let Some(track_area) = track_area { track_area.clip_h(self.header_h) } + else if let Some(scene_area) = scene_area { scene_area.clip_w(self.scenes_w) } + else { area.clip_w(self.scenes_w).clip_h(self.header_h) }, &CORNERS)? + }) +})); + +struct ArrangerVerticalHeader<'a> { + tracks: &'a Vec, + cols: Vec<(usize, usize)>, + focused: bool, + selected: ArrangerSelection, + scenes_w: u16, + header_h: u16, + timebase: &'a Arc, + current: &'a Arc, +} +impl<'a> From<&'a ArrangerTui> for ArrangerVerticalHeader<'a> { + fn from (state: &'a ArrangerTui) -> Self { + Self { + tracks: &state.tracks, + cols: track_widths(state.tracks()), + focused: state.arranger_focused(), + selected: state.selected, + scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16, + header_h: 3, + timebase: state.clock().timebase(), + current: &state.clock().playhead, + } + } +} +render!(|self: ArrangerVerticalHeader<'a>|row!( + (track, w) in self.tracks.iter().zip(self.cols.iter().map(|col|col.0)) => { + // name and width of track + let name = track.name().read().unwrap(); + let max_w = w.saturating_sub(1).min(name.len()).max(2); + let name = format!("▎{}", &name[0..max_w]); + let name = Tui::bold(true, name); + // beats elapsed + let elapsed = if let Some((_, Some(phrase))) = track.player.play_phrase().as_ref() { + let length = phrase.read().unwrap().length; + let elapsed = track.player.pulses_since_start().unwrap(); + let elapsed = self.timebase.format_beats_1_short( + (elapsed as usize % length) as f64 + ); + format!("▎+{elapsed:>}") + } else { + String::from("▎") + }; + // beats until switchover + let until_next = track.player.next_phrase().as_ref().map(|(t, _)|{ + let target = t.pulse.get(); + let current = self.current.pulse.get(); + if target > current { + let remaining = target - current; + format!("▎-{:>}", self.timebase.format_beats_0_short(remaining)) + } else { + String::new() + } + }).unwrap_or(String::from("▎")); + let timer = col!([until_next, elapsed]); + // name of active MIDI input + let _input = format!("▎>{}", track.player.midi_ins().get(0) + .map(|port|port.short_name()) + .transpose()? + .unwrap_or("(none)".into())); + // name of active MIDI output + let _output = format!("▎<{}", track.player.midi_outs().get(0) + .map(|port|port.short_name()) + .transpose()? + .unwrap_or("(none)".into())); + Tui::push_x(self.scenes_w, + Tui::bg(track.color().rgb, + Tui::min_xy(w as u16, self.header_h, + col!([name, timer])))) + } +)); + +struct ArrangerVerticalContent<'a> { + size: &'a Measure, + scenes: &'a Vec, + tracks: &'a Vec, + rows: Vec<(usize, usize)>, + cols: Vec<(usize, usize)>, + header_h: u16, +} +impl<'a> From<(&'a ArrangerTui, usize)> for ArrangerVerticalContent<'a> { + fn from ((state, factor): (&'a ArrangerTui, usize)) -> Self { + Self { + size: &state.size, + scenes: &state.scenes, + tracks: &state.tracks, + rows: ArrangerScene::ppqs(state.scenes(), factor), + cols: track_widths(state.tracks()), + header_h: 3, + } + } +} +render!(|self: ArrangerVerticalContent<'a>|Tui::fixed_y( + (self.size.h() as u16).saturating_sub(self.header_h), + col!((scene, pulses) in self.scenes.iter().zip(self.rows.iter().map(|row|row.0)) => { + let height = 1.max((pulses / PPQ) as u16); + let playing = scene.is_playing(self.tracks); + Tui::fixed_y(height, row!([ + if playing { "▶ " } else { " " }, + Tui::bold(true, scene.name.read().unwrap().as_str()), + row!((track, w) in self.cols.iter().map(|col|col.0).enumerate() => { + Tui::fixed_xy(w as u16, height, Layers::new(move |add|{ + let mut bg = TuiTheme::border_bg(); + match (self.tracks.get(track), scene.clips.get(track)) { + (Some(track), Some(Some(phrase))) => { + let name = &(phrase as &Arc>).read().unwrap().name; + let name = format!("{}", name); + let max_w = name.len().min((w as usize).saturating_sub(2)); + let color = phrase.read().unwrap().color; + bg = color.dark.rgb; + if let Some((_, Some(ref playing))) = track.player.play_phrase() { + if *playing.read().unwrap() == *phrase.read().unwrap() { + bg = color.light.rgb + } + }; + add(&Tui::fixed_x(w as u16, Tui::push_x(1, &name.as_str()[0..max_w])))?; + }, + _ => {} + }; + //add(&Background(bg)) + Ok(()) + })) + })]) + ) + }) +)); + +pub fn arranger_content_horizontal ( + view: &ArrangerTui, +) -> impl Render + use<'_> { + todo!() +} + //let focused = view.arranger_focused(); + //let _tracks = view.tracks(); + //lay!( + //focused.then_some(Background(TuiTheme::border_bg())), + //row!( + //// name + //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ + //todo!() + ////let Self(tracks, selected) = self; + ////let yellow = Some(Style::default().yellow().bold().not_dim()); + ////let white = Some(Style::default().white().bold().not_dim()); + ////let area = to.area(); + ////let area = [area.x(), area.y(), 3 + 5.max(track_name_max_len(tracks)) as u16, area.h()]; + ////let offset = 0; // track scroll offset + ////for y in 0..area.h() { + ////if y == 0 { + ////to.blit(&"Mixer", area.x() + 1, area.y() + y, Some(DIM))?; + ////} else if y % 2 == 0 { + ////let index = (y as usize - 2) / 2 + offset; + ////if let Some(track) = tracks.get(index) { + ////let selected = selected.track() == Some(index); + ////let style = if selected { yellow } else { white }; + ////to.blit(&format!(" {index:>02} "), area.x(), area.y() + y, style)?; + ////to.blit(&*track.name.read().unwrap(), area.x() + 4, area.y() + y, style)?; + ////} + ////} + ////} + ////Ok(Some(area)) + //}), + //// monitor + //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ + //todo!() + ////let Self(tracks) = self; + ////let mut area = to.area(); + ////let on = Some(Style::default().not_dim().green().bold()); + ////let off = Some(DIM); + ////area.x += 1; + ////for y in 0..area.h() { + ////if y == 0 { + //////" MON ".blit(to.buffer, area.x, area.y + y, style2)?; + ////} else if y % 2 == 0 { + ////let index = (y as usize - 2) / 2; + ////if let Some(track) = tracks.get(index) { + ////let style = if track.monitoring { on } else { off }; + ////to.blit(&" MON ", area.x(), area.y() + y, style)?; + ////} else { + ////area.height = y; + ////break + ////} + ////} + ////} + ////area.width = 4; + ////Ok(Some(area)) + //}), + //// record + //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ + //todo!() + ////let Self(tracks) = self; + ////let mut area = to.area(); + ////let on = Some(Style::default().not_dim().red().bold()); + ////let off = Some(Style::default().dim()); + ////area.x += 1; + ////for y in 0..area.h() { + ////if y == 0 { + //////" REC ".blit(to.buffer, area.x, area.y + y, style2)?; + ////} else if y % 2 == 0 { + ////let index = (y as usize - 2) / 2; + ////if let Some(track) = tracks.get(index) { + ////let style = if track.recording { on } else { off }; + ////to.blit(&" REC ", area.x(), area.y() + y, style)?; + ////} else { + ////area.height = y; + ////break + ////} + ////} + ////} + ////area.width = 4; + ////Ok(Some(area)) + //}), + //// overdub + //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ + //todo!() + ////let Self(tracks) = self; + ////let mut area = to.area(); + ////let on = Some(Style::default().not_dim().yellow().bold()); + ////let off = Some(Style::default().dim()); + ////area.x = area.x + 1; + ////for y in 0..area.h() { + ////if y == 0 { + //////" OVR ".blit(to.buffer, area.x, area.y + y, style2)?; + ////} else if y % 2 == 0 { + ////let index = (y as usize - 2) / 2; + ////if let Some(track) = tracks.get(index) { + ////to.blit(&" OVR ", area.x(), area.y() + y, if track.overdub { + ////on + ////} else { + ////off + ////})?; + ////} else { + ////area.height = y; + ////break + ////} + ////} + ////} + ////area.width = 4; + ////Ok(Some(area)) + //}), + //// erase + //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ + //todo!() + ////let Self(tracks) = self; + ////let mut area = to.area(); + ////let off = Some(Style::default().dim()); + ////area.x = area.x + 1; + ////for y in 0..area.h() { + ////if y == 0 { + //////" DEL ".blit(to.buffer, area.x, area.y + y, style2)?; + ////} else if y % 2 == 0 { + ////let index = (y as usize - 2) / 2; + ////if let Some(_) = tracks.get(index) { + ////to.blit(&" DEL ", area.x(), area.y() + y, off)?; + ////} else { + ////area.height = y; + ////break + ////} + ////} + ////} + ////area.width = 4; + ////Ok(Some(area)) + //}), + //// gain + //Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{ + //todo!() + ////let Self(tracks) = self; + ////let mut area = to.area(); + ////let off = Some(Style::default().dim()); + ////area.x = area.x() + 1; + ////for y in 0..area.h() { + ////if y == 0 { + //////" GAIN ".blit(to.buffer, area.x, area.y + y, style2)?; + ////} else if y % 2 == 0 { + ////let index = (y as usize - 2) / 2; + ////if let Some(_) = tracks.get(index) { + ////to.blit(&" +0.0 ", area.x(), area.y() + y, off)?; + ////} else { + ////area.height = y; + ////break + ////} + ////} + ////} + ////area.width = 7; + ////Ok(Some(area)) + //}), + //// scenes + //Widget::new(|_|{todo!()}, |to: &mut TuiOutput|{ + //let [x, y, _, height] = to.area(); + //let mut x2 = 0; + //Ok(for (scene_index, scene) in view.scenes().iter().enumerate() { + //let active_scene = view.selected.scene() == Some(scene_index); + //let sep = Some(if active_scene { + //Style::default().yellow().not_dim() + //} else { + //Style::default().dim() + //}); + //for y in y+1..y+height { + //to.blit(&"│", x + x2, y, sep); + //} + //let name = scene.name.read().unwrap(); + //let mut x3 = name.len() as u16; + //to.blit(&*name, x + x2, y, sep); + //for (i, clip) in scene.clips.iter().enumerate() { + //let active_track = view.selected.track() == Some(i); + //if let Some(clip) = clip { + //let y2 = y + 2 + i as u16 * 2; + //let label = format!("{}", clip.read().unwrap().name); + //to.blit(&label, x + x2, y2, Some(if active_track && active_scene { + //Style::default().not_dim().yellow().bold() + //} else { + //Style::default().not_dim() + //})); + //x3 = x3.max(label.len() as u16) + //} + //} + //x2 = x2 + x3 + 1; + //}) + //}), + //) + //) +//} + +impl HasScenes for ArrangerTui { + fn scenes (&self) -> &Vec { + &self.scenes + } + fn scenes_mut (&mut self) -> &mut Vec { + &mut self.scenes + } + fn scene_add (&mut self, name: Option<&str>, color: Option) + -> Usually<&mut ArrangerScene> + { + let name = name.map_or_else(||self.scene_default_name(), |x|x.to_string()); + let scene = ArrangerScene { + name: Arc::new(name.into()), + clips: vec![None;self.tracks().len()], + color: color.unwrap_or_else(||ItemColor::random()), + }; + self.scenes_mut().push(scene); + let index = self.scenes().len() - 1; + Ok(&mut self.scenes_mut()[index]) + } + fn selected_scene (&self) -> Option<&ArrangerScene> { + self.selected.scene().map(|s|self.scenes().get(s)).flatten() + } + fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> { + self.selected.scene().map(|s|self.scenes_mut().get_mut(s)).flatten() + } +} + +#[derive(Default, Debug, Clone)] +pub struct ArrangerScene { + /// Name of scene + pub(crate) name: Arc>, + /// Clips in scene, one per track + pub(crate) clips: Vec>>>, + /// Identifying color of scene + pub(crate) color: ItemColor, +} + +impl ArrangerSceneApi for ArrangerScene { + fn name (&self) -> &Arc> { + &self.name + } + fn clips (&self) -> &Vec>>> { + &self.clips + } + fn color (&self) -> ItemColor { + self.color + } +} + +impl HasTracks for ArrangerTui { + fn tracks (&self) -> &Vec { + &self.tracks + } + fn tracks_mut (&mut self) -> &mut Vec { + &mut self.tracks + } +} + +impl ArrangerTracksApi for ArrangerTui { + fn track_add (&mut self, name: Option<&str>, color: Option) + -> Usually<&mut ArrangerTrack> + { + let name = name.map_or_else(||self.track_default_name(), |x|x.to_string()); + let track = ArrangerTrack { + width: name.len() + 2, + name: Arc::new(name.into()), + color: color.unwrap_or_else(||ItemColor::random()), + player: PhrasePlayerModel::from(&self.clock), + }; + self.tracks_mut().push(track); + let index = self.tracks().len() - 1; + Ok(&mut self.tracks_mut()[index]) + } + fn track_del (&mut self, index: usize) { + self.tracks_mut().remove(index); + for scene in self.scenes_mut().iter_mut() { + scene.clips.remove(index); + } + } +} + +#[derive(Debug)] +pub struct ArrangerTrack { + /// Name of track + pub(crate) name: Arc>, + /// Preferred width of track column + pub(crate) width: usize, + /// Identifying color of track + pub(crate) color: ItemColor, + /// MIDI player state + pub(crate) player: PhrasePlayerModel, +} + +impl HasPlayer for ArrangerTrack { + fn player (&self) -> &impl MidiPlayerApi { + &self.player + } + fn player_mut (&mut self) -> &mut impl MidiPlayerApi { + &mut self.player + } +} + +impl ArrangerTrackApi for ArrangerTrack { + /// Name of track + fn name (&self) -> &Arc> { + &self.name + } + /// Preferred width of track column + fn width (&self) -> usize { + self.width + } + /// Preferred width of track column + fn width_mut (&mut self) -> &mut usize { + &mut self.width + } + /// Identifying color of track + fn color (&self) -> ItemColor { + self.color + } +} + +#[derive(PartialEq, Clone, Copy, Debug)] +/// Represents the current user selection in the arranger +pub enum ArrangerSelection { + /// The whole mix is selected + Mix, + /// A track is selected. + Track(usize), + /// A scene is selected. + Scene(usize), + /// A clip (track × scene) is selected. + Clip(usize, usize), +} + +/// Focus identification methods +impl ArrangerSelection { + pub fn description ( + &self, + tracks: &Vec, + scenes: &Vec, + ) -> String { + format!("Selected: {}", match self { + Self::Mix => format!("Everything"), + Self::Track(t) => match tracks.get(*t) { + Some(track) => format!("T{t}: {}", &track.name.read().unwrap()), + None => format!("T??"), + }, + Self::Scene(s) => match scenes.get(*s) { + Some(scene) => format!("S{s}: {}", &scene.name.read().unwrap()), + None => format!("S??"), + }, + Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) { + (Some(_), Some(scene)) => match scene.clip(*t) { + Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name), + None => format!("T{t} S{s}: Empty") + }, + _ => format!("T{t} S{s}: Empty"), + } + }) + } + pub fn is_mix (&self) -> bool { + match self { Self::Mix => true, _ => false } + } + pub fn is_track (&self) -> bool { + match self { Self::Track(_) => true, _ => false } + } + pub fn is_scene (&self) -> bool { + match self { Self::Scene(_) => true, _ => false } + } + pub fn is_clip (&self) -> bool { + match self { Self::Clip(_, _) => true, _ => false } + } + pub fn track (&self) -> Option { + use ArrangerSelection::*; + match self { + Clip(t, _) => Some(*t), + Track(t) => Some(*t), + _ => None + } + } + pub fn scene (&self) -> Option { + use ArrangerSelection::*; + match self { + Clip(_, s) => Some(*s), + Scene(s) => Some(*s), + _ => None + } + } +} + +impl Handle for ArrangerTui { + fn handle (&mut self, i: &TuiInput) -> Perhaps { + ArrangerCommand::execute_with_state(self, i) + } +} + +#[derive(Clone, Debug)] +pub enum ArrangerCommand { + Focus(FocusCommand), + Undo, + Redo, + Clear, + Color(ItemColor), + Clock(ClockCommand), + Scene(ArrangerSceneCommand), + Track(ArrangerTrackCommand), + Clip(ArrangerClipCommand), + Select(ArrangerSelection), + Zoom(usize), + Phrases(PhrasesCommand), + Editor(PhraseCommand), +} + +impl Command for ArrangerCommand { + fn execute (self, state: &mut ArrangerTui) -> Perhaps { + use ArrangerCommand::*; + Ok(match self { + Focus(cmd) => cmd.execute(state)?.map(Focus), + Scene(cmd) => cmd.execute(state)?.map(Scene), + Track(cmd) => cmd.execute(state)?.map(Track), + Clip(cmd) => cmd.execute(state)?.map(Clip), + Phrases(cmd) => cmd.execute(&mut state.phrases)?.map(Phrases), + Editor(cmd) => cmd.execute(&mut state.editor)?.map(Editor), + Clock(cmd) => cmd.execute(state)?.map(Clock), + Zoom(_) => { todo!(); }, + Select(selected) => { + *state.selected_mut() = selected; + None + }, + _ => { todo!() } + }) + } +} + +impl Command for ArrangerSceneCommand { + fn execute (self, _state: &mut ArrangerTui) -> Perhaps { + //todo!(); + Ok(None) + } +} + +impl Command for ArrangerTrackCommand { + fn execute (self, _state: &mut ArrangerTui) -> Perhaps { + //todo!(); + Ok(None) + } +} + +impl Command for ArrangerClipCommand { + fn execute (self, _state: &mut ArrangerTui) -> Perhaps { + //todo!(); + Ok(None) + } +} + +pub trait ArrangerControl: TransportControl { + fn selected (&self) -> ArrangerSelection; + fn selected_mut (&mut self) -> &mut ArrangerSelection; + fn activate (&mut self) -> Usually<()>; + fn selected_phrase (&self) -> Option>>; + fn toggle_loop (&mut self); + fn randomize_color (&mut self); +} + +impl ArrangerControl for ArrangerTui { + fn selected (&self) -> ArrangerSelection { + self.selected + } + fn selected_mut (&mut self) -> &mut ArrangerSelection { + &mut self.selected + } + fn activate (&mut self) -> Usually<()> { + if let ArrangerSelection::Scene(s) = self.selected { + for (t, track) in self.tracks.iter_mut().enumerate() { + let phrase = self.scenes[s].clips[t].clone(); + if track.player.play_phrase.is_some() || phrase.is_some() { + track.player.enqueue_next(phrase.as_ref()); + } + } + if self.clock().is_stopped() { + self.clock().play_from(Some(0))?; + } + } else if let ArrangerSelection::Clip(t, s) = self.selected { + let phrase = self.scenes()[s].clips[t].clone(); + self.tracks_mut()[t].player.enqueue_next(phrase.as_ref()); + }; + Ok(()) + } + fn selected_phrase (&self) -> Option>> { + self.selected_scene()?.clips.get(self.selected.track()?)?.clone() + } + fn toggle_loop (&mut self) { + if let Some(phrase) = self.selected_phrase() { + phrase.write().unwrap().toggle_loop() + } + } + fn randomize_color (&mut self) { + match self.selected { + ArrangerSelection::Mix => { + self.color = ItemColor::random_dark() + }, + ArrangerSelection::Track(t) => { + self.tracks_mut()[t].color = ItemColor::random() + }, + ArrangerSelection::Scene(s) => { + self.scenes_mut()[s].color = ItemColor::random() + }, + ArrangerSelection::Clip(t, s) => { + if let Some(phrase) = &self.scenes_mut()[s].clips[t] { + phrase.write().unwrap().color = ItemColorTriplet::random(); + } + } + } + } +} +impl InputToCommand for ArrangerCommand { + fn input_to_command (state: &ArrangerTui, input: &TuiInput) -> Option { + to_arranger_command(state, input) + .or_else(||to_focus_command(input).map(ArrangerCommand::Focus)) + } +} + + +fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option { + use ArrangerCommand as Cmd; + use KeyCode::Char; + if !state.entered() { + return None + } + Some(match input.event() { + key!(Char('e')) => Cmd::Editor(PhraseCommand::Show(Some( + state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone() + ))), + _ => match state.focused() { + ArrangerFocus::Transport(_) => { + match TransportCommand::input_to_command(state, input)? { + TransportCommand::Clock(command) => Cmd::Clock(command), + _ => return None, + } + }, + ArrangerFocus::PhraseEditor => { + Cmd::Editor(PhraseCommand::input_to_command(&state.editor, input)?) + }, + ArrangerFocus::Phrases => { + Cmd::Phrases(PhrasesCommand::input_to_command(&state.phrases, input)?) + }, + ArrangerFocus::Arranger => { + use ArrangerSelection::*; + match input.event() { + key!(Char('l')) => Cmd::Clip(ArrangerClipCommand::SetLoop(false)), + key!(Char('+')) => Cmd::Zoom(0), // TODO + key!(Char('=')) => Cmd::Zoom(0), // TODO + key!(Char('_')) => Cmd::Zoom(0), // TODO + key!(Char('-')) => Cmd::Zoom(0), // TODO + key!(Char('`')) => { todo!("toggle state mode") }, + key!(Ctrl-Char('a')) => Cmd::Scene(ArrangerSceneCommand::Add), + key!(Ctrl-Char('t')) => Cmd::Track(ArrangerTrackCommand::Add), + _ => match state.selected() { + Mix => to_arranger_mix_command(input)?, + Track(t) => to_arranger_track_command(input, t)?, + Scene(s) => to_arranger_scene_command(input, s)?, + Clip(t, s) => to_arranger_clip_command(input, t, s)?, + } + } + } + } + }) +} + +fn to_arranger_mix_command (input: &TuiInput) -> Option { + use KeyCode::{Char, Down, Right, Delete}; + use ArrangerCommand as Cmd; + use ArrangerSelection as Select; + Some(match input.event() { + key!(Down) => Cmd::Select(Select::Scene(0)), + key!(Right) => Cmd::Select(Select::Track(0)), + key!(Char(',')) => Cmd::Zoom(0), + key!(Char('.')) => Cmd::Zoom(0), + key!(Char('<')) => Cmd::Zoom(0), + key!(Char('>')) => Cmd::Zoom(0), + key!(Delete) => Cmd::Clear, + key!(Char('c')) => Cmd::Color(ItemColor::random()), + _ => return None + }) +} + +fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option { + use KeyCode::{Char, Down, Left, Right, Delete}; + use ArrangerCommand as Cmd; + use ArrangerSelection as Select; + use ArrangerTrackCommand as Track; + Some(match input.event() { + key!(Down) => Cmd::Select(Select::Clip(t, 0)), + key!(Left) => Cmd::Select(if t > 0 { Select::Track(t - 1) } else { Select::Mix }), + key!(Right) => Cmd::Select(Select::Track(t + 1)), + key!(Char(',')) => Cmd::Track(Track::Swap(t, t - 1)), + key!(Char('.')) => Cmd::Track(Track::Swap(t, t + 1)), + key!(Char('<')) => Cmd::Track(Track::Swap(t, t - 1)), + key!(Char('>')) => Cmd::Track(Track::Swap(t, t + 1)), + key!(Delete) => Cmd::Track(Track::Delete(t)), + //key!(Char('c')) => Cmd::Track(Track::Color(t, ItemColor::random())), + _ => return None + }) +} + +fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option { + use KeyCode::{Char, Up, Down, Right, Enter, Delete}; + use ArrangerCommand as Cmd; + use ArrangerSelection as Select; + use ArrangerSceneCommand as Scene; + Some(match input.event() { + key!(Up) => Cmd::Select(if s > 0 { Select::Scene(s - 1) } else { Select::Mix }), + key!(Down) => Cmd::Select(Select::Scene(s + 1)), + key!(Right) => Cmd::Select(Select::Clip(0, s)), + key!(Char(',')) => Cmd::Scene(Scene::Swap(s, s - 1)), + key!(Char('.')) => Cmd::Scene(Scene::Swap(s, s + 1)), + key!(Char('<')) => Cmd::Scene(Scene::Swap(s, s - 1)), + key!(Char('>')) => Cmd::Scene(Scene::Swap(s, s + 1)), + key!(Enter) => Cmd::Scene(Scene::Play(s)), + key!(Delete) => Cmd::Scene(Scene::Delete(s)), + //key!(Char('c')) => Cmd::Track(Scene::Color(s, ItemColor::random())), + _ => return None + }) +} + +fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option { + use KeyCode::{Char, Up, Down, Left, Right, Delete}; + use ArrangerCommand as Cmd; + use ArrangerSelection as Select; + use ArrangerClipCommand as Clip; + Some(match input.event() { + key!(Up) => Cmd::Select(if s > 0 { Select::Clip(t, s - 1) } else { Select::Track(t) }), + key!(Down) => Cmd::Select(Select::Clip(t, s + 1)), + key!(Left) => Cmd::Select(if t > 0 { Select::Clip(t - 1, s) } else { Select::Scene(s) }), + key!(Right) => Cmd::Select(Select::Clip(t + 1, s)), + key!(Char(',')) => Cmd::Clip(Clip::Set(t, s, None)), + key!(Char('.')) => Cmd::Clip(Clip::Set(t, s, None)), + key!(Char('<')) => Cmd::Clip(Clip::Set(t, s, None)), + key!(Char('>')) => Cmd::Clip(Clip::Set(t, s, None)), + key!(Delete) => Cmd::Clip(Clip::Set(t, s, None)), + //key!(Char('c')) => Cmd::Clip(Clip::Color(t, s, ItemColor::random())), + //key!(Char('g')) => Cmd::Clip(Clip(Clip::Get(t, s))), + //key!(Char('s')) => Cmd::Clip(Clip(Clip::Set(t, s))), + _ => return None + }) +} diff --git a/crates/tek/src/tui/app_sequencer.rs b/crates/tek/src/tui/app_sequencer.rs new file mode 100644 index 00000000..75d0b9b5 --- /dev/null +++ b/crates/tek/src/tui/app_sequencer.rs @@ -0,0 +1,490 @@ +use crate::{*, api::ClockCommand::{Play, Pause}}; +use super::phrase_editor::PhraseCommand::Show; +use super::app_transport::TransportCommand; +use KeyCode::{Char, Enter}; +use SequencerCommand::*; +use SequencerFocus::*; + +/// Create app state from JACK handle. +impl TryFrom<&Arc>> for SequencerTui { + type Error = Box; + fn try_from (jack: &Arc>) -> Usually { + + let clock = ClockModel::from(jack); + + let mut phrase = Phrase::default(); + phrase.name = "New".into(); + phrase.color = ItemColor::random().into(); + phrase.set_length(384); + + let mut phrases = PhraseListModel::default(); + let phrase = Arc::new(RwLock::new(phrase)); + phrases.phrases.push(phrase.clone()); + phrases.phrase.store(1, Ordering::Relaxed); + + let mut editor = PhraseEditorModel::default(); + editor.show_phrase(Some(phrase.clone())); + + let mut player = PhrasePlayerModel::from(&clock); + player.play_phrase = Some((Moment::zero(&clock.timebase), Some(phrase))); + + Ok(Self { + clock, + phrases, + player, + editor, + jack: jack.clone(), + size: Measure::new(), + cursor: (0, 0), + entered: false, + split: 20, + midi_buf: vec![vec![];65536], + note_buf: vec![], + perf: PerfModel::default(), + focus: FocusState::Focused(SequencerFocus::PhraseEditor) + }) + + } +} + +/// Root view for standalone `tek_sequencer`. +pub struct SequencerTui { + pub jack: Arc>, + pub clock: ClockModel, + pub phrases: PhraseListModel, + pub player: PhrasePlayerModel, + pub editor: PhraseEditorModel, + pub size: Measure, + pub cursor: (usize, usize), + pub split: u16, + pub entered: bool, + pub note_buf: Vec, + pub midi_buf: Vec>>, + pub focus: FocusState, + pub perf: PerfModel, +} + +impl JackApi for SequencerTui { + fn jack (&self) -> &Arc> { + &self.jack + } +} + +impl Audio for SequencerTui { + fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { + // Start profiling cycle + let t0 = self.perf.get_t0(); + + // Update transport clock + if ClockAudio(self).process(client, scope) == Control::Quit { + return Control::Quit + } + + // Update MIDI sequencer + if PlayerAudio( + &mut self.player, + &mut self.note_buf, + &mut self.midi_buf, + ).process(client, scope) == Control::Quit { + return Control::Quit + } + + // Update sequencer playhead indicator + //self.now().set(0.); + //if let Some((ref started_at, Some(ref playing))) = self.player.play_phrase { + //let phrase = phrase.read().unwrap(); + //if *playing.read().unwrap() == *phrase { + //let pulse = self.current().pulse.get(); + //let start = started_at.pulse.get(); + //let now = (pulse - start) % phrase.length as f64; + //self.now().set(now); + //} + //} + // End profiling cycle + self.perf.update(t0, scope); + + Control::Continue + } +} + +render!(|self: SequencerTui|lay!([ + self.size, + Tui::shrink_y(1, col!([ + TransportView::from((self, if let SequencerFocus::Transport(_) = self.focus.inner() { + true + } else { + false + })), + row!([ + Tui::fixed_x(20, Tui::split_up(2, PhraseSelector::edit_phrase( + &self.editor.phrase, + self.focused() == SequencerFocus::PhraseEditor, + self.entered() + ), col!([ + PhraseSelector::play_phrase( + &self.player, + self.focused() == SequencerFocus::PhrasePlay, + self.entered() + ), + PhraseSelector::next_phrase( + &self.player, + self.focused() == SequencerFocus::PhraseNext, + self.entered() + ), + PhraseListView::from(self), + + ]))), + PhraseView::from(self) + ]) + ])), + Tui::fill_xy(Tui::at_s(SequencerStatusBar::from(self))), +])); + +impl HasClock for SequencerTui { + fn clock (&self) -> &ClockModel { + &self.clock + } +} + +impl HasPhrases for SequencerTui { + fn phrases (&self) -> &Vec>> { + &self.phrases.phrases + } + fn phrases_mut (&mut self) -> &mut Vec>> { + &mut self.phrases.phrases + } +} + +/// Sections in the sequencer app that may be focused +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum SequencerFocus { + /// The transport (toolbar) is focused + Transport(TransportFocus), + /// The phrase list (pool) is focused + PhraseList, + /// The phrase editor (sequencer) is focused + PhraseEditor, + + PhrasePlay, + PhraseNext, +} + +impl From<&SequencerTui> for Option { + fn from (state: &SequencerTui) -> Self { + match state.focus.inner() { + SequencerFocus::Transport(focus) => Some(focus), + _ => None + } + } +} + +impl_focus!(SequencerTui SequencerFocus [ + //&[ + //Menu, + //Menu, + //Menu, + //Menu, + //Menu, + //], + &[ + Transport(TransportFocus::PlayPause), + Transport(TransportFocus::Bpm), + Transport(TransportFocus::Sync), + Transport(TransportFocus::Quant), + Transport(TransportFocus::Clock), + ], + &[ + PhrasePlay, + PhrasePlay, + PhraseEditor, + PhraseEditor, + PhraseEditor, + ], + &[ + PhraseNext, + PhraseNext, + PhraseEditor, + PhraseEditor, + PhraseEditor, + ], + &[ + PhraseList, + PhraseList, + PhraseEditor, + PhraseEditor, + PhraseEditor, + ], +] => [self: { + if self.focus.is_entered() && self.focus.inner() == SequencerFocus::PhraseEditor { + self.editor.edit_mode = PhraseEditMode::Note + } else { + self.editor.edit_mode = PhraseEditMode::Scroll + } +}]); + +/// Status bar for sequencer app +#[derive(Clone)] +pub struct SequencerStatusBar { + pub(crate) width: usize, + pub(crate) cpu: Option, + pub(crate) size: String, + pub(crate) res: String, + pub(crate) mode: &'static str, + pub(crate) help: &'static [(&'static str, &'static str, &'static str)] +} + +impl StatusBar for SequencerStatusBar { + type State = SequencerTui; + fn hotkey_fg () -> Color { + TuiTheme::HOTKEY_FG + } + fn update (&mut self, _: &SequencerTui) { + todo!() + } +} + +impl From<&SequencerTui> for SequencerStatusBar { + fn from (state: &SequencerTui) -> Self { + use super::app_transport::TransportFocus::*; + let samples = state.clock.chunk.load(Ordering::Relaxed); + let rate = state.clock.timebase.sr.get() as f64; + let buffer = samples as f64 / rate; + let width = state.size.w(); + let default_help = &[("", "⏎", " enter"), ("", "✣", " navigate")]; + Self { + width, + cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%")), + size: format!("{}x{}│", width, state.size.h()), + res: format!("│{}s│{:.1}kHz│{:.1}ms│", samples, rate / 1000., buffer * 1000.), + mode: match state.focused() { + Transport(PlayPause) => " PLAY/PAUSE ", + Transport(Bpm) => " TEMPO ", + Transport(Sync) => " LAUNCH SYNC ", + Transport(Quant) => " REC QUANT ", + Transport(Clock) => " SEEK ", + PhrasePlay => " TO PLAY ", + PhraseNext => " UP NEXT ", + PhraseList => " PHRASES ", + PhraseEditor => match state.editor.edit_mode { + PhraseEditMode::Note => " EDIT MIDI ", + PhraseEditMode::Scroll => " VIEW MIDI ", + }, + }, + help: match state.focused() { + Transport(PlayPause) => &[ + ("", "⏎", " play/pause"), + ("", "✣", " navigate"), + ], + Transport(Bpm) => &[ + ("", ".,", " inc/dec"), + ("", "><", " fine"), + ], + Transport(Sync) => &[ + ("", ".,", " inc/dec"), + ], + Transport(Quant) => &[ + ("", ".,", " inc/dec"), + ], + Transport(Clock) => &[ + ("", ".,", " by beat"), + ("", "<>", " by time"), + ], + PhraseList => if state.entered() { + &[ + ("", "↕", " pick"), + ("", ".,", " move"), + ("", "⏎", " play"), + ("", "e", " edit"), + ] + } else { + default_help + }, + PhraseEditor => match state.editor.edit_mode { + PhraseEditMode::Note => &[ + ("", "✣", " cursor"), + ], + PhraseEditMode::Scroll => &[ + ("", "✣", " scroll"), + ], + } + _ => if state.entered() { + &[ + ("", "Esc", " exit") + ] + } else { + default_help + } + } + } + } +} + +render!(|self: SequencerStatusBar|{ + lay!(|add|if self.width > 60 { + add(&row!(![ + SequencerMode::from(self), + SequencerStats::from(self), + ])) + } else { + add(&col!(![ + SequencerMode::from(self), + SequencerStats::from(self), + ])) + }) +}); + +struct SequencerMode { + mode: &'static str, + help: &'static [(&'static str, &'static str, &'static str)] +} +impl From<&SequencerStatusBar> for SequencerMode { + fn from (state: &SequencerStatusBar) -> Self { + Self { + mode: state.mode, + help: state.help, + } + } +} +render!(|self:SequencerMode|{ + let orange = Color::Rgb(255,128,0); + let light = Color::Rgb(50,50,50); + let white = Color::Rgb(255,255,255); + let yellow = Color::Rgb(255,255,0); + let black = Color::Rgb(0,0,0); + row!([ + Tui::bg(orange, Tui::fg(black, Tui::bold(true, self.mode))), + Tui::bg(light, Tui::fg(white, row!((prefix, hotkey, suffix) in self.help.iter() => { + row!([" ", prefix, Tui::fg(yellow, *hotkey), suffix]) + }))) + ]) +}); + +struct SequencerStats<'a> { + cpu: &'a Option, + size: &'a String, + res: &'a String, +} +impl<'a> From<&'a SequencerStatusBar> for SequencerStats<'a> { + fn from (state: &'a SequencerStatusBar) -> Self { + Self { + cpu: &state.cpu, + size: &state.size, + res: &state.res, + } + } +} +render!(|self:SequencerStats<'a>|{ + let orange = Color::Rgb(255,128,0); + let dark = Color::Rgb(25,25,25); + let cpu = &self.cpu; + let res = &self.res; + let size = &self.size; + Tui::bg(dark, row!([ + Tui::fg(orange, cpu), + Tui::fg(orange, res), + Tui::fg(orange, size), + ])) +}); + +impl Handle for SequencerTui { + fn handle (&mut self, i: &TuiInput) -> Perhaps { + SequencerCommand::execute_with_state(self, i) + } +} + +#[derive(Clone, Debug)] +pub enum SequencerCommand { + Focus(FocusCommand), + Clock(ClockCommand), + Phrases(PhrasesCommand), + Editor(PhraseCommand), + Enqueue(Option>>), + Clear, + Undo, + Redo, +} + +impl Command for SequencerCommand { + fn execute (self, state: &mut SequencerTui) -> Perhaps { + Ok(match self { + Self::Focus(cmd) => cmd.execute(state)?.map(Focus), + Self::Phrases(cmd) => cmd.execute(&mut state.phrases)?.map(Phrases), + Self::Editor(cmd) => cmd.execute(&mut state.editor)?.map(Editor), + Self::Clock(cmd) => cmd.execute(state)?.map(Clock), + Self::Enqueue(phrase) => { + state.player.enqueue_next(phrase.as_ref()); + None + }, + Self::Undo => { todo!() }, + Self::Redo => { todo!() }, + Self::Clear => { todo!() }, + }) + } +} + +impl InputToCommand for SequencerCommand { + fn input_to_command (state: &SequencerTui, input: &TuiInput) -> Option { + if state.entered() { + to_sequencer_command(state, input) + .or_else(||to_focus_command(input).map(SequencerCommand::Focus)) + } else { + to_focus_command(input).map(SequencerCommand::Focus) + .or_else(||to_sequencer_command(state, input)) + }.or_else(||Some({ + let time_zoom = state.editor.view_mode.time_zoom(); + let next_zoom = next_note_length(time_zoom); + let prev_zoom = prev_note_length(time_zoom); + match input.event() { + key!(Char('-')) => SequencerCommand::Editor(PhraseCommand::SetTimeZoom(next_zoom)), + key!(Char('_')) => SequencerCommand::Editor(PhraseCommand::SetTimeZoom(next_zoom)), + key!(Char('=')) => SequencerCommand::Editor(PhraseCommand::SetTimeZoom(prev_zoom)), + key!(Char('+')) => SequencerCommand::Editor(PhraseCommand::SetTimeZoom(prev_zoom)), + _ => return None + } + })) + } +} + +pub fn to_sequencer_command (state: &SequencerTui, input: &TuiInput) -> Option { + Some(match input.event() { + // Play/pause + key!(Char(' ')) => Clock( + if state.clock().is_stopped() { Play(None) } else { Pause(None) } + ), + // Play from start/rewind to start + key!(Shift-Char(' ')) => Clock( + if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) } + ), + // Edit phrase + key!(Char('e')) => match state.focused() { + SequencerFocus::PhrasePlay => Editor(Show( + state.player.play_phrase().as_ref().map(|x|x.1.as_ref()).flatten().map(|x|x.clone()) + )), + SequencerFocus::PhraseNext => Editor(Show( + state.player.next_phrase().as_ref().map(|x|x.1.as_ref()).flatten().map(|x|x.clone()) + )), + SequencerFocus::PhraseList => Editor(Show( + Some(state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone()) + )), + _ => return None, + }, + _ => match state.focused() { + SequencerFocus::Transport(_) => match TransportCommand::input_to_command(state, input)? { + TransportCommand::Clock(command) => Clock(command), + _ => return None, + }, + SequencerFocus::PhraseEditor => Editor( + PhraseCommand::input_to_command(&state.editor, input)? + ), + SequencerFocus::PhraseList => match input.event() { + key!(Enter) => Enqueue(Some( + state.phrases.phrases[state.phrases.phrase.load(Ordering::Relaxed)].clone() + )), + _ => Phrases( + PhrasesCommand::input_to_command(&state.phrases, input)? + ), + } + _ => return None + } + }) +} diff --git a/crates/tek/src/tui/app_transport.rs b/crates/tek/src/tui/app_transport.rs new file mode 100644 index 00000000..1f05267a --- /dev/null +++ b/crates/tek/src/tui/app_transport.rs @@ -0,0 +1,344 @@ +use crate::*; +use crate::api::ClockCommand::{Play, Pause, SetBpm, SetQuant, SetSync}; +use TransportCommand::{Focus, Clock}; +use FocusCommand::{Next, Prev}; +use KeyCode::{Enter, Left, Right, Char}; + +/// Transport clock app. +pub struct TransportTui { + pub jack: Arc>, + pub clock: ClockModel, + pub size: Measure, + pub cursor: (usize, usize), + pub focus: FocusState, +} + +/// Create app state from JACK handle. +impl TryFrom<&Arc>> for TransportTui { + type Error = Box; + fn try_from (jack: &Arc>) -> Usually { + Ok(Self { + jack: jack.clone(), + clock: ClockModel::from(jack), + size: Measure::new(), + cursor: (0, 0), + focus: FocusState::Entered(TransportFocus::PlayPause) + }) + } +} + +impl std::fmt::Debug for TransportTui { + fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + f.debug_struct("TransportTui") + .field("jack", &self.jack) + .field("size", &self.size) + .field("cursor", &self.cursor) + .finish() + } +} + +impl HasClock for TransportTui { + fn clock (&self) -> &ClockModel { + &self.clock + } +} + +impl JackApi for TransportTui { + fn jack (&self) -> &Arc> { + &self.jack + } +} + +impl Audio for TransportTui { + fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { + ClockAudio(self).process(client, scope) + } +} + +render!(|self: TransportTui|TransportView::from((self, true))); + +pub struct TransportView { + focused: bool, + + sr: String, + bpm: String, + ppq: String, + beat: String, + + global_sample: String, + global_second: String, + + started: bool, + + current_sample: f64, + current_second: f64, +} + +impl From<(&T, bool)> for TransportView { + fn from ((state, focused): (&T, bool)) -> Self { + let clock = state.clock(); + let sr = format!("{:.1}k", clock.timebase.sr.get() / 1000.0); + let bpm = format!("{:.3}", clock.timebase.bpm.get()); + let ppq = format!("{:.0}", clock.timebase.ppq.get()); + if let Some(started) = clock.started.read().unwrap().as_ref() { + let current_sample = (clock.global.sample.get() - started.sample.get())/1000.; + let current_usec = clock.global.usec.get() - started.usec.get(); + let current_second = current_usec/1000000.; + Self { + focused, + sr, + bpm, + ppq, + started: true, + global_sample: format!("{:.0}k", started.sample.get()/1000.), + global_second: format!("{:.1}s", started.usec.get()/1000.), + current_sample, + current_second, + beat: clock.timebase.format_beats_0( + clock.timebase.usecs_to_pulse(current_usec) + ), + } + } else { + Self { + focused, + sr, + bpm, + ppq, + started: false, + global_sample: format!("{:.0}k", clock.global.sample.get()/1000.), + global_second: format!("{:.1}s", clock.global.usec.get()/1000000.), + current_sample: 0.0, + current_second: 0.0, + beat: format!("0.0.00") + } + } + } +} + +struct TransportField<'a>(&'a str, &'a str); +render!(|self: TransportField<'a>|{ + col!([ + Tui::fg(Color::Rgb(150, 150, 150), self.0), + Tui::bold(true, Tui::fg(Color::Rgb(200, 200, 200), self.1)), + ]) +}); + +render!(|self: TransportView|{ + let bg = if self.focused { TuiTheme::border_bg() } else { TuiTheme::bg() }; + let border_style = Style::default() + .bg(bg) + .fg(TuiTheme::border_fg(self.focused)); + Tui::bg(bg, lay!(move|add|{ + add(&Tui::fill_x(Tui::at_w(lay!(move|add|{ + add(&Lozenge(border_style))?; + add(&Tui::outset_x(1, row!([ + TransportField("Beat", self.beat.as_str()), " ", + TransportField("BPM ", self.bpm.as_str()), " ", + ]))) + }))))?; + add(&Tui::fill_x(Tui::center_x(Tui::pull_x(2, row!([ + col!(|add|{ + if self.started { + add(&col!([Tui::fg(Color::Rgb(0, 255, 0), "▶ PLAYING"), ""])) + } else { + add(&col!(["", Tui::fg(Color::Rgb(255, 128, 0), "⏹ STOPPED")])) + } + }), + ])))))?; + add(&Tui::fill_x(Tui::at_e(lay!(move|add|{ + add(&Lozenge(border_style))?; + add(&Tui::outset_x(1, row!([ + TransportField("Second", format!("{:.1}s", self.current_second).as_str()), " ", + TransportField("Rate ", self.sr.as_str()), " ", + TransportField("Sample", format!("{:.0}k", self.current_sample).as_str()), + ]))) + })))) + })) +}); + +/// Which item of the transport toolbar is focused +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum TransportFocus { + Bpm, + Sync, + PlayPause, + Clock, + Quant, +} + +impl From<&TransportTui> for Option { + fn from (state: &TransportTui) -> Self { + Some(state.focus.inner()) + } +} + +impl FocusWrap for TransportFocus { + fn wrap <'a, W: Render> (self, focus: TransportFocus, content: &'a W) + -> impl Render + 'a + { + let focused = focus == self; + let corners = focused.then_some(CORNERS); + //let highlight = focused.then_some(Tui::bg(Color::Rgb(60, 70, 50))); + lay!([corners, /*highlight,*/ *content]) + } +} + +impl FocusWrap for Option { + fn wrap <'a, W: Render> (self, focus: TransportFocus, content: &'a W) + -> impl Render + 'a + { + let focused = Some(focus) == self; + let corners = focused.then_some(CORNERS); + //let highlight = focused.then_some(Background(Color::Rgb(60, 70, 50))); + lay!([corners, /*highlight,*/ *content]) + } +} + +impl_focus!(TransportTui TransportFocus [ + //&[Menu], + &[ + PlayPause, + Bpm, + Sync, + Quant, + Clock, + ], +]); + +#[derive(Copy, Clone)] +pub struct TransportStatusBar; + +impl StatusBar for TransportStatusBar { + type State = (); + fn hotkey_fg () -> Color { + TuiTheme::HOTKEY_FG + } + fn update (&mut self, _: &()) { + todo!() + } +} + +render!(|self: TransportStatusBar|"todo"); + +impl Handle for TransportTui { + fn handle (&mut self, from: &TuiInput) -> Perhaps { + TransportCommand::execute_with_state(self, from) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum TransportCommand { + Focus(FocusCommand), + Clock(ClockCommand), +} + +impl Command for TransportCommand { + fn execute (self, state: &mut T) -> Perhaps { + Ok(match self { + Self::Focus(cmd) => cmd.execute(state)?.map(Self::Focus), + Self::Clock(cmd) => cmd.execute(state)?.map(Self::Clock), + }) + } +} + +pub trait TransportControl: HasClock + FocusGrid + HasEnter { + fn transport_focused (&self) -> Option; +} + +impl TransportControl for TransportTui { + fn transport_focused (&self) -> Option { + Some(self.focus.inner()) + } +} + +impl TransportControl for SequencerTui { + fn transport_focused (&self) -> Option { + match self.focus.inner() { + SequencerFocus::Transport(focus) => Some(focus), + _ => None + } + } +} + +impl TransportControl for ArrangerTui { + fn transport_focused (&self) -> Option { + match self.focus.inner() { + ArrangerFocus::Transport(focus) => Some(focus), + _ => None + } + } +} + +impl InputToCommand for TransportCommand { + fn input_to_command (state: &T, input: &TuiInput) -> Option { + to_transport_command(state, input) + .or_else(||to_focus_command(input).map(TransportCommand::Focus)) + } +} + +pub fn to_transport_command (state: &T, input: &TuiInput) -> Option +where + T: TransportControl +{ + Some(match input.event() { + key!(Left) => Focus(Prev), + key!(Right) => Focus(Next), + key!(Char(' ')) => Clock(if state.clock().is_stopped() { + Play(None) + } else { + Pause(None) + }), + key!(Shift-Char(' ')) => Clock(if state.clock().is_stopped() { + Play(Some(0)) + } else { + Pause(Some(0)) + }), + _ => match state.transport_focused().unwrap() { + TransportFocus::Bpm => match input.event() { + key!(Char(',')) => Clock(SetBpm(state.clock().bpm().get() - 1.0)), + key!(Char('.')) => Clock(SetBpm(state.clock().bpm().get() + 1.0)), + key!(Char('<')) => Clock(SetBpm(state.clock().bpm().get() - 0.001)), + key!(Char('>')) => Clock(SetBpm(state.clock().bpm().get() + 0.001)), + _ => return None, + }, + TransportFocus::Quant => match input.event() { + key!(Char(',')) => Clock(SetQuant(state.clock().quant.prev())), + key!(Char('.')) => Clock(SetQuant(state.clock().quant.next())), + key!(Char('<')) => Clock(SetQuant(state.clock().quant.prev())), + key!(Char('>')) => Clock(SetQuant(state.clock().quant.next())), + _ => return None, + }, + TransportFocus::Sync => match input.event() { + key!(Char(',')) => Clock(SetSync(state.clock().sync.prev())), + key!(Char('.')) => Clock(SetSync(state.clock().sync.next())), + key!(Char('<')) => Clock(SetSync(state.clock().sync.prev())), + key!(Char('>')) => Clock(SetSync(state.clock().sync.next())), + _ => return None, + }, + TransportFocus::Clock => match input.event() { + key!(Char(',')) => todo!("transport seek bar"), + key!(Char('.')) => todo!("transport seek bar"), + key!(Char('<')) => todo!("transport seek beat"), + key!(Char('>')) => todo!("transport seek beat"), + _ => return None, + }, + TransportFocus::PlayPause => match input.event() { + key!(Enter) => Clock( + if state.clock().is_stopped() { + Play(None) + } else { + Pause(None) + } + ), + key!(Shift-Enter) => Clock( + if state.clock().is_stopped() { + Play(Some(0)) + } else { + Pause(Some(0)) + } + ), + _ => return None, + }, + } + }) +} diff --git a/crates/tek/src/tui/engine_focus.rs b/crates/tek/src/tui/engine_focus.rs new file mode 100644 index 00000000..4b0c417a --- /dev/null +++ b/crates/tek/src/tui/engine_focus.rs @@ -0,0 +1,66 @@ +use crate::*; + +pub trait FocusWrap { + fn wrap <'a, W: Render> (self, focus: T, content: &'a W) + -> impl Render + 'a; +} + +pub fn to_focus_command (input: &TuiInput) -> Option { + use KeyCode::{Tab, BackTab, Up, Down, Left, Right, Enter, Esc}; + Some(match input.event() { + key!(Tab) => FocusCommand::Next, + key!(Shift-Tab) => FocusCommand::Prev, + key!(BackTab) => FocusCommand::Prev, + key!(Shift-BackTab) => FocusCommand::Prev, + key!(Up) => FocusCommand::Up, + key!(Down) => FocusCommand::Down, + key!(Left) => FocusCommand::Left, + key!(Right) => FocusCommand::Right, + key!(Enter) => FocusCommand::Enter, + key!(Esc) => FocusCommand::Exit, + _ => return None + }) +} + +#[macro_export] macro_rules! impl_focus { + ($Struct:ident $Focus:ident $Grid:expr $(=> [$self:ident : $update_focus:expr])?) => { + impl HasFocus for $Struct { + type Item = $Focus; + /// Get the currently focused item. + fn focused (&self) -> Self::Item { + self.focus.inner() + } + /// Get the currently focused item. + fn set_focused (&mut self, to: Self::Item) { + self.focus.set_inner(to) + } + $(fn focus_updated (&mut $self) { $update_focus })? + } + impl HasEnter for $Struct { + /// Get the currently focused item. + fn entered (&self) -> bool { + self.focus.is_entered() + } + /// Get the currently focused item. + fn set_entered (&mut self, entered: bool) { + if entered { + self.focus.to_entered() + } else { + self.focus.to_focused() + } + } + } + impl FocusGrid for $Struct { + fn focus_cursor (&self) -> (usize, usize) { + self.cursor + } + fn focus_cursor_mut (&mut self) -> &mut (usize, usize) { + &mut self.cursor + } + fn focus_layout (&self) -> &[&[$Focus]] { + use $Focus::*; + &$Grid + } + } + } +} diff --git a/crates/tek/src/tui/engine_input.rs b/crates/tek/src/tui/engine_input.rs new file mode 100644 index 00000000..7508bf22 --- /dev/null +++ b/crates/tek/src/tui/engine_input.rs @@ -0,0 +1,181 @@ +use crate::*; + +pub struct TuiInput { + pub(crate) exited: Arc, + pub(crate) event: TuiEvent, +} + +#[derive(Debug, Clone)] +pub enum TuiEvent { + /// Terminal input + Input(::crossterm::event::Event), + /// Update values but not the whole form. + Update, + /// Update the whole form. + Redraw, + /// Device gains focus + Focus, + /// Device loses focus + Blur, + // /// JACK notification + // Jack(JackEvent) +} + +impl Input for TuiInput { + type Event = TuiEvent; + fn event (&self) -> &TuiEvent { &self.event } + fn is_done (&self) -> bool { self.exited.fetch_and(true, Ordering::Relaxed) } + fn done (&self) { self.exited.store(true, Ordering::Relaxed); } +} + +impl TuiInput { + // TODO remove + pub fn handle_keymap (&self, state: &mut T, keymap: &KeyMap) -> Usually { + match self.event() { + TuiEvent::Input(crossterm::event::Event::Key(event)) => { + for (code, modifiers, _, _, command) in keymap.iter() { + if *code == event.code && modifiers.bits() == event.modifiers.bits() { + return command(state) + } + } + }, + _ => {} + }; + Ok(false) + } +} + +pub type KeyHandler = &'static dyn Fn(&mut T)->Usually; + +pub type KeyBinding = (KeyCode, KeyModifiers, &'static str, &'static str, KeyHandler); + +pub type KeyMap = [KeyBinding]; + +/// Define a key +pub const fn key (code: KeyCode) -> KeyEvent { + let modifiers = KeyModifiers::NONE; + let kind = KeyEventKind::Press; + let state = KeyEventState::NONE; + KeyEvent { code, modifiers, kind, state } +} + +/// Add Ctrl modifier to key +pub const fn ctrl (key: KeyEvent) -> KeyEvent { + KeyEvent { modifiers: key.modifiers.union(KeyModifiers::CONTROL), ..key } +} + +/// Add Alt modifier to key +pub const fn alt (key: KeyEvent) -> KeyEvent { + KeyEvent { modifiers: key.modifiers.union(KeyModifiers::ALT), ..key } +} + +/// Add Shift modifier to key +pub const fn shift (key: KeyEvent) -> KeyEvent { + KeyEvent { modifiers: key.modifiers.union(KeyModifiers::SHIFT), ..key } +} + +/// Define a keymap +#[macro_export] macro_rules! keymap { + ($T:ty { $([$k:ident $(($char:literal))?, $m:ident, $n: literal, $d: literal, $f: expr]),* $(,)? }) => { + &[ + $((KeyCode::$k $(($char))?, KeyModifiers::$m, $n, $d, &$f as KeyHandler<$T>)),* + ] as &'static [KeyBinding<$T>] + } +} + +/// Define a key in a keymap +#[macro_export] macro_rules! map_key { + ($k:ident $(($char:literal))?, $m:ident, $n: literal, $d: literal, $f: expr) => { + (KeyCode::$k $(($char))?, KeyModifiers::$m, $n, $d, &$f as &dyn Fn()->Usually) + } +} + +/// Shorthand for key match statement +#[macro_export] macro_rules! match_key { + ($event:expr, { + $($key:pat=>$block:expr),* $(,)? + }) => { + match $event { + $(crossterm::event::Event::Key(crossterm::event::KeyEvent { + code: $key, + modifiers: crossterm::event::KeyModifiers::NONE, + kind: crossterm::event::KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE + }) => { + $block + })* + _ => Ok(None) + } + } +} + +/// Define key pattern in key match statement +#[macro_export] macro_rules! key { + ($code:pat) => { + TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { + code: $code, + modifiers: crossterm::event::KeyModifiers::NONE, + kind: crossterm::event::KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE + })) + }; + (Ctrl-$code:pat) => { + TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { + code: $code, + modifiers: crossterm::event::KeyModifiers::CONTROL, + kind: crossterm::event::KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE + })) + }; + (Alt-$code:pat) => { + TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { + code: $code, + modifiers: crossterm::event::KeyModifiers::ALT, + kind: crossterm::event::KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE + })) + }; + (Shift-$code:pat) => { + TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { + code: $code, + modifiers: crossterm::event::KeyModifiers::SHIFT, + kind: crossterm::event::KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE + })) + } +} + +#[macro_export] macro_rules! key_lit { + ($code:expr) => { + TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { + code: $code, + modifiers: crossterm::event::KeyModifiers::NONE, + kind: crossterm::event::KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE + })) + }; + (Ctrl-$code:expr) => { + TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { + code: $code, + modifiers: crossterm::event::KeyModifiers::CONTROL, + kind: crossterm::event::KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE + })) + }; + (Alt-$code:expr) => { + TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { + code: $code, + modifiers: crossterm::event::KeyModifiers::ALT, + kind: crossterm::event::KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE + })) + }; + (Shift-$code:expr) => { + TuiEvent::Input(crossterm::event::Event::Key(crossterm::event::KeyEvent { + code: $code, + modifiers: crossterm::event::KeyModifiers::SHIFT, + kind: crossterm::event::KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE + })) + } +} diff --git a/crates/tek/src/tui/engine_output.rs b/crates/tek/src/tui/engine_output.rs new file mode 100644 index 00000000..5fac2e11 --- /dev/null +++ b/crates/tek/src/tui/engine_output.rs @@ -0,0 +1,184 @@ +use crate::*; +use ratatui::buffer::Cell; + +/// Every struct that has [Content]<[Tui]> is a renderable [Render]<[Tui]>. +//impl> Render for C { + //fn min_size (&self, to: [u16;2]) -> Perhaps { + //self.content().min_size(to) + //} + //fn render (&self, to: &mut TuiOutput) -> Usually<()> { + //match self.min_size(to.area().wh().into())? { + //Some(wh) => to.render_in(to.area().clip(wh).into(), &self.content()), + //None => Ok(()) + //} + //} +//} + +pub struct TuiOutput { + pub buffer: Buffer, + pub area: [u16;4] +} + +impl Output for TuiOutput { + #[inline] fn area (&self) -> [u16;4] { self.area } + #[inline] fn area_mut (&mut self) -> &mut [u16;4] { &mut self.area } + #[inline] fn render_in (&mut self, + area: [u16;4], + widget: &dyn Render + ) -> Usually<()> { + let last = self.area(); + *self.area_mut() = area; + widget.render(self)?; + *self.area_mut() = last; + Ok(()) + } +} + +impl TuiOutput { + pub fn buffer_update (&mut self, + area: [u16;4], + callback: &impl Fn(&mut Cell, u16, u16) + ) { + buffer_update(&mut self.buffer, area, callback); + } + pub fn fill_bold (&mut self, area: [u16;4], on: bool) { + if on { + self.buffer_update(area, &|cell,_,_|cell.modifier.insert(Modifier::BOLD)) + } else { + self.buffer_update(area, &|cell,_,_|cell.modifier.remove(Modifier::BOLD)) + } + } + pub fn fill_bg (&mut self, area: [u16;4], color: Color) { + self.buffer_update(area, &|cell,_,_|{cell.set_bg(color);}) + } + pub fn fill_fg (&mut self, area: [u16;4], color: Color) { + self.buffer_update(area, &|cell,_,_|{cell.set_fg(color);}) + } + pub fn fill_ul (&mut self, area: [u16;4], color: Color) { + self.buffer_update(area, &|cell,_,_|{ + cell.modifier = ratatui::prelude::Modifier::UNDERLINED; + cell.underline_color = color; + }) + } + pub fn fill_char (&mut self, area: [u16;4], c: char) { + self.buffer_update(area, &|cell,_,_|{cell.set_char(c);}) + } + pub fn make_dim (&mut self) { + for cell in self.buffer.content.iter_mut() { + cell.bg = ratatui::style::Color::Rgb(30,30,30); + cell.fg = ratatui::style::Color::Rgb(100,100,100); + cell.modifier = ratatui::style::Modifier::DIM; + } + } + pub fn blit ( + &mut self, text: &impl AsRef, x: u16, y: u16, style: Option