Feat: WIP terse code documentation
This commit is contained in:
263
TODO.md
Normal file
263
TODO.md
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
# Rustdoc & Cleanup Review
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
**Strictly one file at a time, in list order.** When the user says "review" (or
|
||||||
|
similar), process the next unchecked file — never skip ahead, never batch.
|
||||||
|
|
||||||
|
1. Read the file.
|
||||||
|
2. Read any imports, callers, or sibling files needed to understand what the code
|
||||||
|
does and how it fits in the codebase. Gathering context is encouraged.
|
||||||
|
3. Apply the changes described below.
|
||||||
|
4. **`cargo build`** to confirm nothing broke.
|
||||||
|
5. Check the file off in this list.
|
||||||
|
6. Stop. Wait for the user before moving to the next file.
|
||||||
|
|
||||||
|
## What to do
|
||||||
|
|
||||||
|
1. **Add `//!` module doc** at the top if missing — one or two lines explaining what
|
||||||
|
the module does and its role in the crate.
|
||||||
|
2. **Add `///` on public items** (structs, enums, functions, traits, type aliases).
|
||||||
|
Keep it to one line when possible. Document struct fields only when the name alone
|
||||||
|
is not self-explanatory.
|
||||||
|
3. **Light cleanup** — remove dead code, fix misleading names, apply trivial
|
||||||
|
simplifications. No behavior changes.
|
||||||
|
4. **`cargo build`** after each file to confirm nothing broke.
|
||||||
|
|
||||||
|
## What NOT to do
|
||||||
|
|
||||||
|
- No comments on private internals unless truly obscure.
|
||||||
|
- No fluff ("This struct represents…"). Be direct.
|
||||||
|
- No feature changes, refactoring sprees, or reformatting of unrelated code.
|
||||||
|
- No over-commenting. If the code is clear, leave it alone.
|
||||||
|
|
||||||
|
## Style
|
||||||
|
|
||||||
|
Sparse, english, imperative where possible. Match the tone of the existing codebase.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- [x] build.rs
|
||||||
|
- [x] crates/forth/src/compiler.rs
|
||||||
|
- [x] crates/forth/src/lib.rs
|
||||||
|
- [x] crates/forth/src/ops.rs
|
||||||
|
- [x] crates/forth/src/theory/chords.rs
|
||||||
|
- [x] crates/forth/src/theory/mod.rs
|
||||||
|
- [x] crates/forth/src/theory/scales.rs
|
||||||
|
- [x] crates/forth/src/types.rs
|
||||||
|
- [x] crates/forth/src/vm.rs
|
||||||
|
- [x] crates/forth/src/words/compile.rs
|
||||||
|
- [x] crates/forth/src/words/core.rs
|
||||||
|
- [x] crates/forth/src/words/effects.rs
|
||||||
|
- [x] crates/forth/src/words/midi.rs
|
||||||
|
- [x] crates/forth/src/words/mod.rs
|
||||||
|
- [x] crates/forth/src/words/music.rs
|
||||||
|
- [x] crates/forth/src/words/sequencing.rs
|
||||||
|
- [x] crates/forth/src/words/sound.rs
|
||||||
|
- [x] crates/markdown/src/highlighter.rs
|
||||||
|
- [x] crates/markdown/src/lib.rs
|
||||||
|
- [x] crates/markdown/src/parser.rs
|
||||||
|
- [x] crates/markdown/src/theme.rs
|
||||||
|
- [x] crates/project/src/file.rs
|
||||||
|
- [x] crates/project/src/lib.rs
|
||||||
|
- [x] crates/project/src/project.rs
|
||||||
|
- [x] crates/project/src/share.rs
|
||||||
|
- [x] crates/ratatui/src/category_list.rs
|
||||||
|
- [x] crates/ratatui/src/confirm.rs
|
||||||
|
- [x] crates/ratatui/src/editor.rs
|
||||||
|
- [x] crates/ratatui/src/file_browser.rs
|
||||||
|
- [x] crates/ratatui/src/hint_bar.rs
|
||||||
|
- [ ] crates/ratatui/src/lib.rs
|
||||||
|
- [ ] crates/ratatui/src/lissajous.rs
|
||||||
|
- [ ] crates/ratatui/src/list_select.rs
|
||||||
|
- [ ] crates/ratatui/src/modal.rs
|
||||||
|
- [ ] crates/ratatui/src/nav_minimap.rs
|
||||||
|
- [ ] crates/ratatui/src/props_form.rs
|
||||||
|
- [ ] crates/ratatui/src/sample_browser.rs
|
||||||
|
- [ ] crates/ratatui/src/scope.rs
|
||||||
|
- [ ] crates/ratatui/src/scroll_indicators.rs
|
||||||
|
- [ ] crates/ratatui/src/search_bar.rs
|
||||||
|
- [ ] crates/ratatui/src/section_header.rs
|
||||||
|
- [ ] crates/ratatui/src/sparkles.rs
|
||||||
|
- [ ] crates/ratatui/src/spectrum.rs
|
||||||
|
- [ ] crates/ratatui/src/text_input.rs
|
||||||
|
- [ ] crates/ratatui/src/theme/build.rs
|
||||||
|
- [ ] crates/ratatui/src/theme/catppuccin_latte.rs
|
||||||
|
- [ ] crates/ratatui/src/theme/catppuccin_mocha.rs
|
||||||
|
- [ ] crates/ratatui/src/theme/dracula.rs
|
||||||
|
- [ ] crates/ratatui/src/theme/eden.rs
|
||||||
|
- [ ] crates/ratatui/src/theme/ember.rs
|
||||||
|
- [ ] crates/ratatui/src/theme/everforest.rs
|
||||||
|
- [ ] crates/ratatui/src/theme/fairyfloss.rs
|
||||||
|
- [ ] crates/ratatui/src/theme/fauve.rs
|
||||||
|
- [ ] crates/ratatui/src/theme/georges.rs
|
||||||
|
- [ ] crates/ratatui/src/theme/gruvbox_dark.rs
|
||||||
|
- [ ] crates/ratatui/src/theme/hot_dog_stand.rs
|
||||||
|
- [ ] crates/ratatui/src/theme/iceberg.rs
|
||||||
|
- [ ] crates/ratatui/src/theme/jaipur.rs
|
||||||
|
- [ ] crates/ratatui/src/theme/kanagawa.rs
|
||||||
|
- [ ] crates/ratatui/src/theme/letz_light.rs
|
||||||
|
- [ ] crates/ratatui/src/theme/mod.rs
|
||||||
|
- [ ] crates/ratatui/src/theme/monochrome_black.rs
|
||||||
|
- [ ] crates/ratatui/src/theme/monochrome_white.rs
|
||||||
|
- [ ] crates/ratatui/src/theme/monokai.rs
|
||||||
|
- [ ] crates/ratatui/src/theme/nord.rs
|
||||||
|
- [ ] crates/ratatui/src/theme/palette.rs
|
||||||
|
- [ ] crates/ratatui/src/theme/pitch_black.rs
|
||||||
|
- [ ] crates/ratatui/src/theme/rose_pine.rs
|
||||||
|
- [ ] crates/ratatui/src/theme/tokyo_night.rs
|
||||||
|
- [ ] crates/ratatui/src/theme/transform.rs
|
||||||
|
- [ ] crates/ratatui/src/theme/tropicalia.rs
|
||||||
|
- [ ] crates/ratatui/src/vu_meter.rs
|
||||||
|
- [ ] crates/ratatui/src/waveform.rs
|
||||||
|
- [ ] plugins/baseview/src/clipboard.rs
|
||||||
|
- [ ] plugins/baseview/src/event.rs
|
||||||
|
- [ ] plugins/baseview/src/gl/macos.rs
|
||||||
|
- [ ] plugins/baseview/src/gl/mod.rs
|
||||||
|
- [ ] plugins/baseview/src/gl/win.rs
|
||||||
|
- [ ] plugins/baseview/src/gl/x11.rs
|
||||||
|
- [ ] plugins/baseview/src/gl/x11/errors.rs
|
||||||
|
- [ ] plugins/baseview/src/keyboard.rs
|
||||||
|
- [ ] plugins/baseview/src/lib.rs
|
||||||
|
- [ ] plugins/baseview/src/macos/keyboard.rs
|
||||||
|
- [ ] plugins/baseview/src/macos/mod.rs
|
||||||
|
- [ ] plugins/baseview/src/macos/view.rs
|
||||||
|
- [ ] plugins/baseview/src/macos/window.rs
|
||||||
|
- [ ] plugins/baseview/src/mouse_cursor.rs
|
||||||
|
- [ ] plugins/baseview/src/win/cursor.rs
|
||||||
|
- [ ] plugins/baseview/src/win/drop_target.rs
|
||||||
|
- [ ] plugins/baseview/src/win/hook.rs
|
||||||
|
- [ ] plugins/baseview/src/win/keyboard.rs
|
||||||
|
- [ ] plugins/baseview/src/win/mod.rs
|
||||||
|
- [ ] plugins/baseview/src/win/window.rs
|
||||||
|
- [ ] plugins/baseview/src/window_info.rs
|
||||||
|
- [ ] plugins/baseview/src/window_open_options.rs
|
||||||
|
- [ ] plugins/baseview/src/window.rs
|
||||||
|
- [ ] plugins/baseview/src/x11/cursor.rs
|
||||||
|
- [ ] plugins/baseview/src/x11/event_loop.rs
|
||||||
|
- [ ] plugins/baseview/src/x11/keyboard.rs
|
||||||
|
- [ ] plugins/baseview/src/x11/mod.rs
|
||||||
|
- [ ] plugins/baseview/src/x11/visual_info.rs
|
||||||
|
- [ ] plugins/baseview/src/x11/window.rs
|
||||||
|
- [ ] plugins/baseview/src/x11/xcb_connection.rs
|
||||||
|
- [ ] plugins/cagire-plugins/src/editor.rs
|
||||||
|
- [ ] plugins/cagire-plugins/src/lib.rs
|
||||||
|
- [ ] plugins/cagire-plugins/src/main.rs
|
||||||
|
- [ ] plugins/cagire-plugins/src/params.rs
|
||||||
|
- [ ] plugins/egui-baseview/src/lib.rs
|
||||||
|
- [ ] plugins/egui-baseview/src/renderer.rs
|
||||||
|
- [ ] plugins/egui-baseview/src/renderer/opengl.rs
|
||||||
|
- [ ] plugins/egui-baseview/src/renderer/opengl/renderer.rs
|
||||||
|
- [ ] plugins/egui-baseview/src/translate.rs
|
||||||
|
- [ ] plugins/egui-baseview/src/window.rs
|
||||||
|
- [ ] plugins/nih-plug-egui/src/editor.rs
|
||||||
|
- [ ] plugins/nih-plug-egui/src/lib.rs
|
||||||
|
- [ ] plugins/nih-plug-egui/src/resizable_window.rs
|
||||||
|
- [ ] plugins/nih-plug-egui/src/widgets.rs
|
||||||
|
- [ ] plugins/nih-plug-egui/src/widgets/generic_ui.rs
|
||||||
|
- [ ] plugins/nih-plug-egui/src/widgets/param_slider.rs
|
||||||
|
- [ ] plugins/nih-plug-egui/src/widgets/util.rs
|
||||||
|
- [ ] src/app/clipboard.rs
|
||||||
|
- [ ] src/app/dispatch.rs
|
||||||
|
- [ ] src/app/editing.rs
|
||||||
|
- [ ] src/app/mod.rs
|
||||||
|
- [ ] src/app/navigation.rs
|
||||||
|
- [ ] src/app/persistence.rs
|
||||||
|
- [ ] src/app/scripting.rs
|
||||||
|
- [ ] src/app/sequencer.rs
|
||||||
|
- [ ] src/app/staging.rs
|
||||||
|
- [ ] src/app/undo.rs
|
||||||
|
- [ ] src/bin/desktop/main.rs
|
||||||
|
- [ ] src/block_renderer.rs
|
||||||
|
- [ ] src/commands.rs
|
||||||
|
- [ ] src/engine/audio.rs
|
||||||
|
- [ ] src/engine/dispatcher.rs
|
||||||
|
- [ ] src/engine/link.rs
|
||||||
|
- [ ] src/engine/mod.rs
|
||||||
|
- [ ] src/engine/realtime.rs
|
||||||
|
- [ ] src/engine/sequencer.rs
|
||||||
|
- [ ] src/engine/timing.rs
|
||||||
|
- [ ] src/init.rs
|
||||||
|
- [ ] src/input_egui.rs
|
||||||
|
- [ ] src/input/engine_page.rs
|
||||||
|
- [ ] src/input/help_page.rs
|
||||||
|
- [ ] src/input/main_page.rs
|
||||||
|
- [ ] src/input/mod.rs
|
||||||
|
- [ ] src/input/modal.rs
|
||||||
|
- [ ] src/input/mouse.rs
|
||||||
|
- [ ] src/input/options_page.rs
|
||||||
|
- [ ] src/input/panel.rs
|
||||||
|
- [ ] src/input/patterns_page.rs
|
||||||
|
- [ ] src/lib.rs
|
||||||
|
- [ ] src/main.rs
|
||||||
|
- [ ] src/midi.rs
|
||||||
|
- [ ] src/model/categories.rs
|
||||||
|
- [ ] src/model/demos.rs
|
||||||
|
- [ ] src/model/docs.rs
|
||||||
|
- [ ] src/model/mod.rs
|
||||||
|
- [ ] src/model/onboarding.rs
|
||||||
|
- [ ] src/model/script.rs
|
||||||
|
- [ ] src/page.rs
|
||||||
|
- [ ] src/services/clipboard.rs
|
||||||
|
- [ ] src/services/dict_nav.rs
|
||||||
|
- [ ] src/services/euclidean.rs
|
||||||
|
- [ ] src/services/help_nav.rs
|
||||||
|
- [ ] src/services/mod.rs
|
||||||
|
- [ ] src/services/pattern_editor.rs
|
||||||
|
- [ ] src/services/stack_preview.rs
|
||||||
|
- [ ] src/settings.rs
|
||||||
|
- [ ] src/state/audio.rs
|
||||||
|
- [ ] src/state/color_scheme.rs
|
||||||
|
- [ ] src/state/editor.rs
|
||||||
|
- [ ] src/state/effects.rs
|
||||||
|
- [ ] src/state/file_browser.rs
|
||||||
|
- [ ] src/state/live_keys.rs
|
||||||
|
- [ ] src/state/mod.rs
|
||||||
|
- [ ] src/state/modal.rs
|
||||||
|
- [ ] src/state/mute.rs
|
||||||
|
- [ ] src/state/options.rs
|
||||||
|
- [ ] src/state/panel.rs
|
||||||
|
- [ ] src/state/patterns_nav.rs
|
||||||
|
- [ ] src/state/playback.rs
|
||||||
|
- [ ] src/state/project.rs
|
||||||
|
- [ ] src/state/sample_browser.rs
|
||||||
|
- [ ] src/state/ui.rs
|
||||||
|
- [ ] src/state/undo.rs
|
||||||
|
- [ ] src/theme.rs
|
||||||
|
- [ ] src/views/dict_view.rs
|
||||||
|
- [ ] src/views/engine_view.rs
|
||||||
|
- [ ] src/views/help_view.rs
|
||||||
|
- [ ] src/views/highlight.rs
|
||||||
|
- [ ] src/views/keybindings.rs
|
||||||
|
- [ ] src/views/main_view.rs
|
||||||
|
- [ ] src/views/mod.rs
|
||||||
|
- [ ] src/views/options_view.rs
|
||||||
|
- [ ] src/views/patterns_view.rs
|
||||||
|
- [ ] src/views/render.rs
|
||||||
|
- [ ] src/views/title_view.rs
|
||||||
|
- [ ] src/widgets/mod.rs
|
||||||
|
- [ ] tests/forth.rs
|
||||||
|
- [ ] tests/forth/arithmetic.rs
|
||||||
|
- [ ] tests/forth/case_statement.rs
|
||||||
|
- [ ] tests/forth/chords.rs
|
||||||
|
- [ ] tests/forth/comparison.rs
|
||||||
|
- [ ] tests/forth/context.rs
|
||||||
|
- [ ] tests/forth/control_flow.rs
|
||||||
|
- [ ] tests/forth/definitions.rs
|
||||||
|
- [ ] tests/forth/errors.rs
|
||||||
|
- [ ] tests/forth/euclidean.rs
|
||||||
|
- [ ] tests/forth/generator.rs
|
||||||
|
- [ ] tests/forth/harmony.rs
|
||||||
|
- [ ] tests/forth/harness.rs
|
||||||
|
- [ ] tests/forth/intervals.rs
|
||||||
|
- [ ] tests/forth/list_words.rs
|
||||||
|
- [ ] tests/forth/midi.rs
|
||||||
|
- [ ] tests/forth/notes.rs
|
||||||
|
- [ ] tests/forth/quotations.rs
|
||||||
|
- [ ] tests/forth/ramps.rs
|
||||||
|
- [ ] tests/forth/randomness.rs
|
||||||
|
- [ ] tests/forth/sound.rs
|
||||||
|
- [ ] tests/forth/stack.rs
|
||||||
|
- [ ] tests/forth/temporal.rs
|
||||||
|
- [ ] tests/forth/variables.rs
|
||||||
|
- [ ] xtask/src/main.rs
|
||||||
2
build.rs
2
build.rs
@@ -1,3 +1,5 @@
|
|||||||
|
//! Build script — embeds Windows application resources (icon, metadata).
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ enum Token {
|
|||||||
Word(String, SourceSpan),
|
Word(String, SourceSpan),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compile Forth source text into an executable Op sequence.
|
||||||
pub(super) fn compile_script(input: &str, dict: &Dictionary) -> Result<Vec<Op>, String> {
|
pub(super) fn compile_script(input: &str, dict: &Dictionary) -> Result<Vec<Op>, String> {
|
||||||
let tokens = tokenize(input);
|
let tokens = tokenize(input);
|
||||||
compile(&tokens, dict)
|
compile(&tokens, dict)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use super::types::SourceSpan;
|
use super::types::SourceSpan;
|
||||||
|
|
||||||
|
/// Single VM instruction produced by the compiler.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum Op {
|
pub enum Op {
|
||||||
PushInt(i64, Option<SourceSpan>),
|
PushInt(i64, Option<SourceSpan>),
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
//! Chord definitions as semitone interval arrays.
|
||||||
|
|
||||||
|
/// Named chord with its interval pattern.
|
||||||
pub struct Chord {
|
pub struct Chord {
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
pub intervals: &'static [i64],
|
pub intervals: &'static [i64],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// All built-in chord types.
|
||||||
pub static CHORDS: &[Chord] = &[
|
pub static CHORDS: &[Chord] = &[
|
||||||
// Triads
|
// Triads
|
||||||
Chord {
|
Chord {
|
||||||
@@ -169,6 +173,7 @@ pub static CHORDS: &[Chord] = &[
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Find a chord's intervals by name.
|
||||||
pub fn lookup(name: &str) -> Option<&'static [i64]> {
|
pub fn lookup(name: &str) -> Option<&'static [i64]> {
|
||||||
CHORDS.iter().find(|c| c.name == name).map(|c| c.intervals)
|
CHORDS.iter().find(|c| c.name == name).map(|c| c.intervals)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! Music theory data — chord and scale lookup tables.
|
||||||
|
|
||||||
pub mod chords;
|
pub mod chords;
|
||||||
mod scales;
|
mod scales;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
//! Scale definitions as semitone offset arrays.
|
||||||
|
|
||||||
|
/// Named scale with its semitone pattern.
|
||||||
pub struct Scale {
|
pub struct Scale {
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
pub pattern: &'static [i64],
|
pub pattern: &'static [i64],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// All built-in scale types.
|
||||||
pub static SCALES: &[Scale] = &[
|
pub static SCALES: &[Scale] = &[
|
||||||
Scale {
|
Scale {
|
||||||
name: "major",
|
name: "major",
|
||||||
@@ -125,6 +129,7 @@ pub static SCALES: &[Scale] = &[
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Find a scale's pattern by name.
|
||||||
pub fn lookup(name: &str) -> Option<&'static [i64]> {
|
pub fn lookup(name: &str) -> Option<&'static [i64]> {
|
||||||
SCALES.iter().find(|s| s.name == name).map(|s| s.pattern)
|
SCALES.iter().find(|s| s.name == name).map(|s| s.pattern)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ pub trait CcAccess: Send + Sync {
|
|||||||
fn get_cc(&self, device: usize, channel: usize, cc: usize) -> u8;
|
fn get_cc(&self, device: usize, channel: usize, cc: usize) -> u8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Byte range in source text.
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
pub struct SourceSpan {
|
pub struct SourceSpan {
|
||||||
pub start: u32,
|
pub start: u32,
|
||||||
pub end: u32,
|
pub end: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Concrete value resolved from a nondeterministic op, used for trace annotations.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum ResolvedValue {
|
pub enum ResolvedValue {
|
||||||
Int(i64),
|
Int(i64),
|
||||||
@@ -39,6 +41,7 @@ impl ResolvedValue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Spans and resolved values collected during a single evaluation, used for UI highlighting.
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct ExecutionTrace {
|
pub struct ExecutionTrace {
|
||||||
pub executed_spans: Vec<SourceSpan>,
|
pub executed_spans: Vec<SourceSpan>,
|
||||||
@@ -46,6 +49,7 @@ pub struct ExecutionTrace {
|
|||||||
pub resolved: Vec<(SourceSpan, ResolvedValue)>,
|
pub resolved: Vec<(SourceSpan, ResolvedValue)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-step sequencer state passed into the VM.
|
||||||
pub struct StepContext<'a> {
|
pub struct StepContext<'a> {
|
||||||
pub step: usize,
|
pub step: usize,
|
||||||
pub beat: f64,
|
pub beat: f64,
|
||||||
@@ -72,13 +76,18 @@ impl StepContext<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Underlying map for user-defined variables.
|
||||||
pub type VariablesMap = HashMap<String, Value>;
|
pub type VariablesMap = HashMap<String, Value>;
|
||||||
|
/// Shared variable store, swapped atomically after each step.
|
||||||
pub type Variables = Arc<ArcSwap<VariablesMap>>;
|
pub type Variables = Arc<ArcSwap<VariablesMap>>;
|
||||||
|
/// Shared user-defined word dictionary.
|
||||||
pub type Dictionary = Arc<Mutex<HashMap<String, Vec<Op>>>>;
|
pub type Dictionary = Arc<Mutex<HashMap<String, Vec<Op>>>>;
|
||||||
|
/// Shared random number generator.
|
||||||
pub type Rng = Arc<Mutex<StdRng>>;
|
pub type Rng = Arc<Mutex<StdRng>>;
|
||||||
pub type Stack = Mutex<Vec<Value>>;
|
pub type Stack = Mutex<Vec<Value>>;
|
||||||
pub(super) type CmdSnapshot<'a> = (Option<&'a Value>, &'a [(&'static str, Value)]);
|
pub(super) type CmdSnapshot<'a> = (Option<&'a Value>, &'a [(&'static str, Value)]);
|
||||||
|
|
||||||
|
/// Stack value in the Forth VM.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Value {
|
pub enum Value {
|
||||||
Int(i64, Option<SourceSpan>),
|
Int(i64, Option<SourceSpan>),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use super::types::{
|
|||||||
Value, Variables, VariablesMap,
|
Value, Variables, VariablesMap,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Forth VM instance. Holds the stack, variables, dictionary, and RNG.
|
||||||
pub struct Forth {
|
pub struct Forth {
|
||||||
stack: Stack,
|
stack: Stack,
|
||||||
vars: Variables,
|
vars: Variables,
|
||||||
@@ -45,12 +46,14 @@ impl Forth {
|
|||||||
self.global_params.lock().clear();
|
self.global_params.lock().clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Evaluate a Forth script and return audio command strings.
|
||||||
pub fn evaluate(&self, script: &str, ctx: &StepContext) -> Result<Vec<String>, String> {
|
pub fn evaluate(&self, script: &str, ctx: &StepContext) -> Result<Vec<String>, String> {
|
||||||
let (outputs, var_writes) = self.evaluate_impl(script, ctx, None)?;
|
let (outputs, var_writes) = self.evaluate_impl(script, ctx, None)?;
|
||||||
self.apply_var_writes(var_writes);
|
self.apply_var_writes(var_writes);
|
||||||
Ok(outputs)
|
Ok(outputs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Evaluate and collect an execution trace for UI highlighting.
|
||||||
pub fn evaluate_with_trace(
|
pub fn evaluate_with_trace(
|
||||||
&self,
|
&self,
|
||||||
script: &str,
|
script: &str,
|
||||||
@@ -62,6 +65,7 @@ impl Forth {
|
|||||||
Ok(outputs)
|
Ok(outputs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Evaluate and return both outputs and pending variable writes (without applying them).
|
||||||
pub fn evaluate_raw(
|
pub fn evaluate_raw(
|
||||||
&self,
|
&self,
|
||||||
script: &str,
|
script: &str,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! Word-to-Op translation: maps Forth word names to compiled instructions.
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::ops::Op;
|
use crate::ops::Op;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use super::{Word, WordCompile::*};
|
//! Word metadata for core language primitives (stack, arithmetic, logic, variables, definitions).
|
||||||
|
|
||||||
// Stack, Arithmetic, Comparison, Logic, Control, Variables, Definitions
|
use super::{Word, WordCompile::*};
|
||||||
pub(super) const WORDS: &[Word] = &[
|
pub(super) const WORDS: &[Word] = &[
|
||||||
// Stack manipulation
|
// Stack manipulation
|
||||||
Word {
|
Word {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use super::{Word, WordCompile::*};
|
//! Word metadata for audio effect parameters (filter, envelope, reverb, delay, lo-fi, stereo, mod FX).
|
||||||
|
|
||||||
// Filter, Envelope, Reverb, Delay, Lo-fi, Stereo, Mod FX
|
use super::{Word, WordCompile::*};
|
||||||
pub(super) const WORDS: &[Word] = &[
|
pub(super) const WORDS: &[Word] = &[
|
||||||
// Envelope
|
// Envelope
|
||||||
Word {
|
Word {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
//! MIDI word definitions: channel, CC, pitch bend, transport, and device routing.
|
||||||
|
|
||||||
use super::{Word, WordCompile::*};
|
use super::{Word, WordCompile::*};
|
||||||
|
|
||||||
// MIDI
|
|
||||||
pub(super) const WORDS: &[Word] = &[
|
pub(super) const WORDS: &[Word] = &[
|
||||||
Word {
|
Word {
|
||||||
name: "chan",
|
name: "chan",
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! Built-in word definitions and lookup for the Forth VM.
|
||||||
|
|
||||||
mod compile;
|
mod compile;
|
||||||
mod core;
|
mod core;
|
||||||
mod effects;
|
mod effects;
|
||||||
@@ -11,6 +13,7 @@ use std::sync::LazyLock;
|
|||||||
|
|
||||||
pub(crate) use compile::compile_word;
|
pub(crate) use compile::compile_word;
|
||||||
|
|
||||||
|
/// How a word is compiled into ops.
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub enum WordCompile {
|
pub enum WordCompile {
|
||||||
Simple,
|
Simple,
|
||||||
@@ -19,6 +22,7 @@ pub enum WordCompile {
|
|||||||
Probability(f64),
|
Probability(f64),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Metadata for a built-in Forth word.
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub struct Word {
|
pub struct Word {
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
@@ -31,6 +35,7 @@ pub struct Word {
|
|||||||
pub varargs: bool,
|
pub varargs: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// All built-in words, aggregated from every category module.
|
||||||
pub static WORDS: LazyLock<Vec<Word>> = LazyLock::new(|| {
|
pub static WORDS: LazyLock<Vec<Word>> = LazyLock::new(|| {
|
||||||
let mut words = Vec::new();
|
let mut words = Vec::new();
|
||||||
words.extend_from_slice(self::core::WORDS);
|
words.extend_from_slice(self::core::WORDS);
|
||||||
@@ -42,6 +47,7 @@ pub static WORDS: LazyLock<Vec<Word>> = LazyLock::new(|| {
|
|||||||
words
|
words
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Index mapping word names and aliases to their definitions.
|
||||||
static WORD_MAP: LazyLock<HashMap<&'static str, &'static Word>> = LazyLock::new(|| {
|
static WORD_MAP: LazyLock<HashMap<&'static str, &'static Word>> = LazyLock::new(|| {
|
||||||
let mut map = HashMap::with_capacity(WORDS.len() * 2);
|
let mut map = HashMap::with_capacity(WORDS.len() * 2);
|
||||||
for word in WORDS.iter() {
|
for word in WORDS.iter() {
|
||||||
@@ -53,6 +59,7 @@ static WORD_MAP: LazyLock<HashMap<&'static str, &'static Word>> = LazyLock::new(
|
|||||||
map
|
map
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Find a word by name or alias.
|
||||||
pub fn lookup_word(name: &str) -> Option<&'static Word> {
|
pub fn lookup_word(name: &str) -> Option<&'static Word> {
|
||||||
WORD_MAP.get(name).copied()
|
WORD_MAP.get(name).copied()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
//! Word definitions for music theory, harmony, and chord construction.
|
||||||
|
|
||||||
use super::{Word, WordCompile::*};
|
use super::{Word, WordCompile::*};
|
||||||
|
|
||||||
// Music, Chord
|
|
||||||
pub(super) const WORDS: &[Word] = &[
|
pub(super) const WORDS: &[Word] = &[
|
||||||
// Music
|
// Music
|
||||||
Word {
|
Word {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
//! Word metadata for sequencing: probability, timing, context queries, generators.
|
||||||
|
|
||||||
use super::{Word, WordCompile::*};
|
use super::{Word, WordCompile::*};
|
||||||
|
|
||||||
// Time, Context, Probability, Generator, Desktop
|
|
||||||
pub(super) const WORDS: &[Word] = &[
|
pub(super) const WORDS: &[Word] = &[
|
||||||
// Probability
|
// Probability
|
||||||
Word {
|
Word {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
//! Word metadata for sound commands, sample/oscillator params, FM, modulation, and LFO.
|
||||||
|
|
||||||
use super::{Word, WordCompile::*};
|
use super::{Word, WordCompile::*};
|
||||||
|
|
||||||
// Sound, Oscillator, Sample, Wavetable, FM, Modulation, LFO
|
|
||||||
pub(super) const WORDS: &[Word] = &[
|
pub(super) const WORDS: &[Word] = &[
|
||||||
// Sound
|
// Sound
|
||||||
Word {
|
Word {
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
|
//! Syntax highlighting trait for fenced code blocks in markdown.
|
||||||
|
|
||||||
use ratatui::style::Style;
|
use ratatui::style::Style;
|
||||||
|
|
||||||
|
/// Produce styled spans from a single line of source code.
|
||||||
pub trait CodeHighlighter {
|
pub trait CodeHighlighter {
|
||||||
fn highlight(&self, line: &str) -> Vec<(Style, String)>;
|
fn highlight(&self, line: &str) -> Vec<(Style, String)>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pass-through highlighter that applies no styling.
|
||||||
pub struct NoHighlight;
|
pub struct NoHighlight;
|
||||||
|
|
||||||
impl CodeHighlighter for NoHighlight {
|
impl CodeHighlighter for NoHighlight {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! Parse markdown into styled ratatui lines with pluggable syntax highlighting.
|
||||||
|
|
||||||
mod highlighter;
|
mod highlighter;
|
||||||
mod parser;
|
mod parser;
|
||||||
mod theme;
|
mod theme;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! Parse markdown text into styled ratatui lines with syntax-highlighted code blocks.
|
||||||
|
|
||||||
use minimad::{Composite, CompositeStyle, Compound, Line, TableRow};
|
use minimad::{Composite, CompositeStyle, Compound, Line, TableRow};
|
||||||
use ratatui::style::{Modifier, Style};
|
use ratatui::style::{Modifier, Style};
|
||||||
use ratatui::text::{Line as RLine, Span};
|
use ratatui::text::{Line as RLine, Span};
|
||||||
@@ -5,17 +7,20 @@ use ratatui::text::{Line as RLine, Span};
|
|||||||
use crate::highlighter::CodeHighlighter;
|
use crate::highlighter::CodeHighlighter;
|
||||||
use crate::theme::MarkdownTheme;
|
use crate::theme::MarkdownTheme;
|
||||||
|
|
||||||
|
/// Span of lines within a parsed document that form a fenced code block.
|
||||||
pub struct CodeBlock {
|
pub struct CodeBlock {
|
||||||
pub start_line: usize,
|
pub start_line: usize,
|
||||||
pub end_line: usize,
|
pub end_line: usize,
|
||||||
pub source: String,
|
pub source: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Result of parsing a markdown string: styled lines and extracted code blocks.
|
||||||
pub struct ParsedMarkdown {
|
pub struct ParsedMarkdown {
|
||||||
pub lines: Vec<RLine<'static>>,
|
pub lines: Vec<RLine<'static>>,
|
||||||
pub code_blocks: Vec<CodeBlock>,
|
pub code_blocks: Vec<CodeBlock>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse markdown text into themed, syntax-highlighted ratatui lines.
|
||||||
pub fn parse<T: MarkdownTheme, H: CodeHighlighter>(
|
pub fn parse<T: MarkdownTheme, H: CodeHighlighter>(
|
||||||
md: &str,
|
md: &str,
|
||||||
theme: &T,
|
theme: &T,
|
||||||
@@ -44,7 +49,7 @@ pub fn parse<T: MarkdownTheme, H: CodeHighlighter>(
|
|||||||
let close_block = |start: Option<usize>,
|
let close_block = |start: Option<usize>,
|
||||||
source: &mut Vec<String>,
|
source: &mut Vec<String>,
|
||||||
blocks: &mut Vec<CodeBlock>,
|
blocks: &mut Vec<CodeBlock>,
|
||||||
lines: &Vec<RLine<'static>>| {
|
lines: &[RLine<'static>]| {
|
||||||
if let Some(start) = start {
|
if let Some(start) = start {
|
||||||
blocks.push(CodeBlock {
|
blocks.push(CodeBlock {
|
||||||
start_line: start,
|
start_line: start,
|
||||||
@@ -118,7 +123,7 @@ pub fn parse<T: MarkdownTheme, H: CodeHighlighter>(
|
|||||||
ParsedMarkdown { lines, code_blocks }
|
ParsedMarkdown { lines, code_blocks }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn preprocess_markdown(md: &str) -> String {
|
fn preprocess_markdown(md: &str) -> String {
|
||||||
let mut out = String::with_capacity(md.len());
|
let mut out = String::with_capacity(md.len());
|
||||||
for line in md.lines() {
|
for line in md.lines() {
|
||||||
let line = convert_dash_lists(line);
|
let line = convert_dash_lists(line);
|
||||||
@@ -162,7 +167,7 @@ pub fn preprocess_markdown(md: &str) -> String {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn convert_dash_lists(line: &str) -> String {
|
fn convert_dash_lists(line: &str) -> String {
|
||||||
let trimmed = line.trim_start();
|
let trimmed = line.trim_start();
|
||||||
if let Some(rest) = trimmed.strip_prefix("- ") {
|
if let Some(rest) = trimmed.strip_prefix("- ") {
|
||||||
let indent = line.len() - trimmed.len();
|
let indent = line.len() - trimmed.len();
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
//! Style provider trait for markdown rendering.
|
||||||
|
|
||||||
use ratatui::style::{Color, Modifier, Style};
|
use ratatui::style::{Color, Modifier, Style};
|
||||||
|
|
||||||
|
/// Style provider for each markdown element type.
|
||||||
pub trait MarkdownTheme {
|
pub trait MarkdownTheme {
|
||||||
fn h1(&self) -> Style;
|
fn h1(&self) -> Style;
|
||||||
fn h2(&self) -> Style;
|
fn h2(&self) -> Style;
|
||||||
@@ -16,6 +19,7 @@ pub trait MarkdownTheme {
|
|||||||
fn table_row_odd(&self) -> Color;
|
fn table_row_odd(&self) -> Color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fallback theme with hardcoded terminal colors, used in tests.
|
||||||
pub struct DefaultTheme;
|
pub struct DefaultTheme;
|
||||||
|
|
||||||
impl MarkdownTheme for DefaultTheme {
|
impl MarkdownTheme for DefaultTheme {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! JSON-based project file persistence with versioned format.
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -7,9 +9,9 @@ use serde::{Deserialize, Serialize};
|
|||||||
use crate::project::{Bank, Project};
|
use crate::project::{Bank, Project};
|
||||||
|
|
||||||
const VERSION: u8 = 1;
|
const VERSION: u8 = 1;
|
||||||
pub const EXTENSION: &str = "cagire";
|
const EXTENSION: &str = "cagire";
|
||||||
|
|
||||||
pub fn ensure_extension(path: &Path) -> PathBuf {
|
fn ensure_extension(path: &Path) -> PathBuf {
|
||||||
if path.extension().map(|e| e == EXTENSION).unwrap_or(false) {
|
if path.extension().map(|e| e == EXTENSION).unwrap_or(false) {
|
||||||
path.to_path_buf()
|
path.to_path_buf()
|
||||||
} else {
|
} else {
|
||||||
@@ -62,6 +64,7 @@ impl From<ProjectFile> for Project {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Error returned by project save/load operations.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum FileError {
|
pub enum FileError {
|
||||||
Io(io::Error),
|
Io(io::Error),
|
||||||
@@ -91,6 +94,7 @@ impl From<serde_json::Error> for FileError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Write a project to disk as pretty-printed JSON, returning the final path.
|
||||||
pub fn save(project: &Project, path: &Path) -> Result<PathBuf, FileError> {
|
pub fn save(project: &Project, path: &Path) -> Result<PathBuf, FileError> {
|
||||||
let path = ensure_extension(path);
|
let path = ensure_extension(path);
|
||||||
let file = ProjectFile::from(project);
|
let file = ProjectFile::from(project);
|
||||||
@@ -99,11 +103,13 @@ pub fn save(project: &Project, path: &Path) -> Result<PathBuf, FileError> {
|
|||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read a project from a `.cagire` file on disk.
|
||||||
pub fn load(path: &Path) -> Result<Project, FileError> {
|
pub fn load(path: &Path) -> Result<Project, FileError> {
|
||||||
let json = fs::read_to_string(path)?;
|
let json = fs::read_to_string(path)?;
|
||||||
load_str(&json)
|
load_str(&json)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse a project from a JSON string.
|
||||||
pub fn load_str(json: &str) -> Result<Project, FileError> {
|
pub fn load_str(json: &str) -> Result<Project, FileError> {
|
||||||
let file: ProjectFile = serde_json::from_str(json)?;
|
let file: ProjectFile = serde_json::from_str(json)?;
|
||||||
if file.version > VERSION {
|
if file.version > VERSION {
|
||||||
|
|||||||
@@ -4,9 +4,13 @@ mod file;
|
|||||||
mod project;
|
mod project;
|
||||||
pub mod share;
|
pub mod share;
|
||||||
|
|
||||||
|
/// Maximum number of banks in a project.
|
||||||
pub const MAX_BANKS: usize = 32;
|
pub const MAX_BANKS: usize = 32;
|
||||||
|
/// Maximum number of patterns per bank.
|
||||||
pub const MAX_PATTERNS: usize = 32;
|
pub const MAX_PATTERNS: usize = 32;
|
||||||
|
/// Maximum number of steps per pattern.
|
||||||
pub const MAX_STEPS: usize = 1024;
|
pub const MAX_STEPS: usize = 1024;
|
||||||
|
/// Default pattern length in steps.
|
||||||
pub const DEFAULT_LENGTH: usize = 16;
|
pub const DEFAULT_LENGTH: usize = 16;
|
||||||
|
|
||||||
pub use file::{load, load_str, save, FileError};
|
pub use file::{load, load_str, save, FileError};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
|||||||
|
|
||||||
use crate::{DEFAULT_LENGTH, MAX_BANKS, MAX_PATTERNS, MAX_STEPS};
|
use crate::{DEFAULT_LENGTH, MAX_BANKS, MAX_PATTERNS, MAX_STEPS};
|
||||||
|
|
||||||
|
/// Speed multiplier for a pattern, expressed as a rational fraction.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct PatternSpeed {
|
pub struct PatternSpeed {
|
||||||
pub num: u8,
|
pub num: u8,
|
||||||
@@ -37,10 +38,12 @@ impl PatternSpeed {
|
|||||||
Self::OCTO,
|
Self::OCTO,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Return the speed as a floating-point multiplier.
|
||||||
pub fn multiplier(&self) -> f64 {
|
pub fn multiplier(&self) -> f64 {
|
||||||
self.num as f64 / self.denom as f64
|
self.num as f64 / self.denom as f64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Format as a human-readable label (e.g. "2x", "1/4x").
|
||||||
pub fn label(&self) -> String {
|
pub fn label(&self) -> String {
|
||||||
if self.denom == 1 {
|
if self.denom == 1 {
|
||||||
format!("{}x", self.num)
|
format!("{}x", self.num)
|
||||||
@@ -49,6 +52,7 @@ impl PatternSpeed {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the next faster preset, or self if already at maximum.
|
||||||
pub fn next(&self) -> Self {
|
pub fn next(&self) -> Self {
|
||||||
let current = self.multiplier();
|
let current = self.multiplier();
|
||||||
Self::PRESETS
|
Self::PRESETS
|
||||||
@@ -58,6 +62,7 @@ impl PatternSpeed {
|
|||||||
.unwrap_or(*self)
|
.unwrap_or(*self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the next slower preset, or self if already at minimum.
|
||||||
pub fn prev(&self) -> Self {
|
pub fn prev(&self) -> Self {
|
||||||
let current = self.multiplier();
|
let current = self.multiplier();
|
||||||
Self::PRESETS
|
Self::PRESETS
|
||||||
@@ -68,6 +73,7 @@ impl PatternSpeed {
|
|||||||
.unwrap_or(*self)
|
.unwrap_or(*self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse a speed label like "2x" or "1/4x" into a `PatternSpeed`.
|
||||||
pub fn from_label(s: &str) -> Option<Self> {
|
pub fn from_label(s: &str) -> Option<Self> {
|
||||||
let s = s.trim().trim_end_matches('x');
|
let s = s.trim().trim_end_matches('x');
|
||||||
if let Some((num, denom)) = s.split_once('/') {
|
if let Some((num, denom)) = s.split_once('/') {
|
||||||
@@ -139,6 +145,7 @@ impl<'de> Deserialize<'de> for PatternSpeed {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Quantization grid for launching patterns.
|
||||||
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||||
pub enum LaunchQuantization {
|
pub enum LaunchQuantization {
|
||||||
Immediate,
|
Immediate,
|
||||||
@@ -151,6 +158,7 @@ pub enum LaunchQuantization {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl LaunchQuantization {
|
impl LaunchQuantization {
|
||||||
|
/// Human-readable label for display.
|
||||||
pub fn label(&self) -> &'static str {
|
pub fn label(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Immediate => "Immediate",
|
Self::Immediate => "Immediate",
|
||||||
@@ -162,6 +170,7 @@ impl LaunchQuantization {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cycle to the next longer quantization, clamped at `Bars8`.
|
||||||
pub fn next(&self) -> Self {
|
pub fn next(&self) -> Self {
|
||||||
match self {
|
match self {
|
||||||
Self::Immediate => Self::Beat,
|
Self::Immediate => Self::Beat,
|
||||||
@@ -173,6 +182,7 @@ impl LaunchQuantization {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cycle to the next shorter quantization, clamped at `Immediate`.
|
||||||
pub fn prev(&self) -> Self {
|
pub fn prev(&self) -> Self {
|
||||||
match self {
|
match self {
|
||||||
Self::Immediate => Self::Immediate,
|
Self::Immediate => Self::Immediate,
|
||||||
@@ -185,6 +195,7 @@ impl LaunchQuantization {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How a pattern synchronizes when launched: restart or phase-lock.
|
||||||
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||||
pub enum SyncMode {
|
pub enum SyncMode {
|
||||||
#[default]
|
#[default]
|
||||||
@@ -193,6 +204,7 @@ pub enum SyncMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SyncMode {
|
impl SyncMode {
|
||||||
|
/// Human-readable label for display.
|
||||||
pub fn label(&self) -> &'static str {
|
pub fn label(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Reset => "Reset",
|
Self::Reset => "Reset",
|
||||||
@@ -200,6 +212,7 @@ impl SyncMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Toggle between Reset and PhaseLock.
|
||||||
pub fn toggle(&self) -> Self {
|
pub fn toggle(&self) -> Self {
|
||||||
match self {
|
match self {
|
||||||
Self::Reset => Self::PhaseLock,
|
Self::Reset => Self::PhaseLock,
|
||||||
@@ -208,6 +221,7 @@ impl SyncMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What happens when a pattern finishes: loop, stop, or chain to another.
|
||||||
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||||
pub enum FollowUp {
|
pub enum FollowUp {
|
||||||
#[default]
|
#[default]
|
||||||
@@ -217,6 +231,7 @@ pub enum FollowUp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FollowUp {
|
impl FollowUp {
|
||||||
|
/// Human-readable label for display.
|
||||||
pub fn label(&self) -> &'static str {
|
pub fn label(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Loop => "Loop",
|
Self::Loop => "Loop",
|
||||||
@@ -225,6 +240,7 @@ impl FollowUp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cycle forward through follow-up modes.
|
||||||
pub fn next_mode(&self) -> Self {
|
pub fn next_mode(&self) -> Self {
|
||||||
match self {
|
match self {
|
||||||
Self::Loop => Self::Stop,
|
Self::Loop => Self::Stop,
|
||||||
@@ -233,6 +249,7 @@ impl FollowUp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cycle backward through follow-up modes.
|
||||||
pub fn prev_mode(&self) -> Self {
|
pub fn prev_mode(&self) -> Self {
|
||||||
match self {
|
match self {
|
||||||
Self::Loop => Self::Chain { bank: 0, pattern: 0 },
|
Self::Loop => Self::Chain { bank: 0, pattern: 0 },
|
||||||
@@ -246,6 +263,7 @@ fn is_default_follow_up(f: &FollowUp) -> bool {
|
|||||||
*f == FollowUp::default()
|
*f == FollowUp::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Single step in a pattern, holding a Forth script and optional metadata.
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct Step {
|
pub struct Step {
|
||||||
pub active: bool,
|
pub active: bool,
|
||||||
@@ -257,10 +275,12 @@ pub struct Step {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Step {
|
impl Step {
|
||||||
|
/// True if all fields are at their default values.
|
||||||
pub fn is_default(&self) -> bool {
|
pub fn is_default(&self) -> bool {
|
||||||
self.active && self.script.is_empty() && self.source.is_none() && self.name.is_none()
|
self.active && self.script.is_empty() && self.source.is_none() && self.name.is_none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True if the script is non-empty.
|
||||||
pub fn has_content(&self) -> bool {
|
pub fn has_content(&self) -> bool {
|
||||||
!self.script.is_empty()
|
!self.script.is_empty()
|
||||||
}
|
}
|
||||||
@@ -277,6 +297,7 @@ impl Default for Step {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sequence of steps with playback settings (speed, quantization, sync, follow-up).
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Pattern {
|
pub struct Pattern {
|
||||||
pub steps: Vec<Step>,
|
pub steps: Vec<Step>,
|
||||||
@@ -447,14 +468,17 @@ impl Default for Pattern {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Pattern {
|
impl Pattern {
|
||||||
|
/// Borrow a step by index.
|
||||||
pub fn step(&self, index: usize) -> Option<&Step> {
|
pub fn step(&self, index: usize) -> Option<&Step> {
|
||||||
self.steps.get(index)
|
self.steps.get(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mutably borrow a step by index.
|
||||||
pub fn step_mut(&mut self, index: usize) -> Option<&mut Step> {
|
pub fn step_mut(&mut self, index: usize) -> Option<&mut Step> {
|
||||||
self.steps.get_mut(index)
|
self.steps.get_mut(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the active length, clamped to `[1, MAX_STEPS]`.
|
||||||
pub fn set_length(&mut self, length: usize) {
|
pub fn set_length(&mut self, length: usize) {
|
||||||
let length = length.clamp(1, MAX_STEPS);
|
let length = length.clamp(1, MAX_STEPS);
|
||||||
while self.steps.len() < length {
|
while self.steps.len() < length {
|
||||||
@@ -463,6 +487,7 @@ impl Pattern {
|
|||||||
self.length = length;
|
self.length = length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Follow the source chain from `index` to find the originating step.
|
||||||
pub fn resolve_source(&self, index: usize) -> usize {
|
pub fn resolve_source(&self, index: usize) -> usize {
|
||||||
let mut current = index;
|
let mut current = index;
|
||||||
for _ in 0..self.steps.len() {
|
for _ in 0..self.steps.len() {
|
||||||
@@ -479,20 +504,22 @@ impl Pattern {
|
|||||||
index
|
index
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the script at the resolved source of `index`.
|
||||||
pub fn resolve_script(&self, index: usize) -> Option<&str> {
|
pub fn resolve_script(&self, index: usize) -> Option<&str> {
|
||||||
let source_idx = self.resolve_source(index);
|
let source_idx = self.resolve_source(index);
|
||||||
self.steps.get(source_idx).map(|s| s.script.as_str())
|
self.steps.get(source_idx).map(|s| s.script.as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Count active-length steps that have a script or a source reference.
|
||||||
pub fn content_step_count(&self) -> usize {
|
pub fn content_step_count(&self) -> usize {
|
||||||
self.steps[..self.length]
|
self.steps[..self.length]
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|s| s.has_content() || s.source.is_some())
|
.filter(|s| s.has_content() || s.source.is_some())
|
||||||
.count()
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collection of patterns forming a bank.
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct Bank {
|
pub struct Bank {
|
||||||
pub patterns: Vec<Pattern>,
|
pub patterns: Vec<Pattern>,
|
||||||
@@ -501,6 +528,7 @@ pub struct Bank {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Bank {
|
impl Bank {
|
||||||
|
/// Count patterns that contain at least one non-empty step.
|
||||||
pub fn content_pattern_count(&self) -> usize {
|
pub fn content_pattern_count(&self) -> usize {
|
||||||
self.patterns
|
self.patterns
|
||||||
.iter()
|
.iter()
|
||||||
@@ -518,6 +546,7 @@ impl Default for Bank {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Top-level project: banks, tempo, sample paths, and prelude script.
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct Project {
|
pub struct Project {
|
||||||
pub banks: Vec<Bank>,
|
pub banks: Vec<Bank>,
|
||||||
@@ -548,14 +577,17 @@ impl Default for Project {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Project {
|
impl Project {
|
||||||
|
/// Borrow a pattern by bank and pattern index.
|
||||||
pub fn pattern_at(&self, bank: usize, pattern: usize) -> &Pattern {
|
pub fn pattern_at(&self, bank: usize, pattern: usize) -> &Pattern {
|
||||||
&self.banks[bank].patterns[pattern]
|
&self.banks[bank].patterns[pattern]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mutably borrow a pattern by bank and pattern index.
|
||||||
pub fn pattern_at_mut(&mut self, bank: usize, pattern: usize) -> &mut Pattern {
|
pub fn pattern_at_mut(&mut self, bank: usize, pattern: usize) -> &mut Pattern {
|
||||||
&mut self.banks[bank].patterns[pattern]
|
&mut self.banks[bank].patterns[pattern]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pad banks, patterns, and steps to their maximum sizes after deserialization.
|
||||||
pub fn normalize(&mut self) {
|
pub fn normalize(&mut self) {
|
||||||
self.banks.resize_with(MAX_BANKS, Bank::default);
|
self.banks.resize_with(MAX_BANKS, Bank::default);
|
||||||
for bank in &mut self.banks {
|
for bank in &mut self.banks {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use crate::{Bank, Pattern};
|
|||||||
const PATTERN_PREFIX: &str = "cgr:";
|
const PATTERN_PREFIX: &str = "cgr:";
|
||||||
const BANK_PREFIX: &str = "cgrb:";
|
const BANK_PREFIX: &str = "cgrb:";
|
||||||
|
|
||||||
|
/// Error during pattern or bank import/export.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ShareError {
|
pub enum ShareError {
|
||||||
InvalidPrefix,
|
InvalidPrefix,
|
||||||
@@ -67,18 +68,22 @@ fn decode<T: serde::de::DeserializeOwned>(text: &str, prefix: &str) -> Result<T,
|
|||||||
rmp_serde::from_slice(&packed).map_err(ShareError::Deserialize)
|
rmp_serde::from_slice(&packed).map_err(ShareError::Deserialize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Encode a pattern as a shareable `cgr:` string.
|
||||||
pub fn export(pattern: &Pattern) -> Result<String, ShareError> {
|
pub fn export(pattern: &Pattern) -> Result<String, ShareError> {
|
||||||
encode(pattern, PATTERN_PREFIX)
|
encode(pattern, PATTERN_PREFIX)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Decode a `cgr:` string back into a pattern.
|
||||||
pub fn import(text: &str) -> Result<Pattern, ShareError> {
|
pub fn import(text: &str) -> Result<Pattern, ShareError> {
|
||||||
decode(text, PATTERN_PREFIX)
|
decode(text, PATTERN_PREFIX)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Encode a bank as a shareable `cgrb:` string.
|
||||||
pub fn export_bank(bank: &Bank) -> Result<String, ShareError> {
|
pub fn export_bank(bank: &Bank) -> Result<String, ShareError> {
|
||||||
encode(bank, BANK_PREFIX)
|
encode(bank, BANK_PREFIX)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Decode a `cgrb:` string back into a bank.
|
||||||
pub fn import_bank(text: &str) -> Result<Bank, ShareError> {
|
pub fn import_bank(text: &str) -> Result<Bank, ShareError> {
|
||||||
decode(text, BANK_PREFIX)
|
decode(text, BANK_PREFIX)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! Collapsible categorized list widget with section headers.
|
||||||
|
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::style::{Color, Modifier, Style};
|
use ratatui::style::{Color, Modifier, Style};
|
||||||
use ratatui::widgets::{Block, Borders, List, ListItem};
|
use ratatui::widgets::{Block, Borders, List, ListItem};
|
||||||
@@ -5,17 +7,20 @@ use ratatui::Frame;
|
|||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
|
|
||||||
|
/// Entry in a category list: either a section header or a leaf item.
|
||||||
pub struct CategoryItem<'a> {
|
pub struct CategoryItem<'a> {
|
||||||
pub label: &'a str,
|
pub label: &'a str,
|
||||||
pub is_section: bool,
|
pub is_section: bool,
|
||||||
pub collapsed: bool,
|
pub collapsed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What is currently selected: a leaf item or a section header.
|
||||||
pub enum Selection {
|
pub enum Selection {
|
||||||
Item(usize),
|
Item(usize),
|
||||||
Section(usize),
|
Section(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Scrollable list with collapsible section headers.
|
||||||
pub struct CategoryList<'a> {
|
pub struct CategoryList<'a> {
|
||||||
items: &'a [CategoryItem<'a>],
|
items: &'a [CategoryItem<'a>],
|
||||||
selection: Selection,
|
selection: Selection,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! Yes/No confirmation dialog widget.
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||||
use ratatui::style::Style;
|
use ratatui::style::Style;
|
||||||
@@ -7,6 +9,7 @@ use ratatui::Frame;
|
|||||||
|
|
||||||
use super::ModalFrame;
|
use super::ModalFrame;
|
||||||
|
|
||||||
|
/// Modal dialog with Yes/No buttons.
|
||||||
pub struct ConfirmModal<'a> {
|
pub struct ConfirmModal<'a> {
|
||||||
title: &'a str,
|
title: &'a str,
|
||||||
message: &'a str,
|
message: &'a str,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! Script editor widget with completion, search, and sample finder popups.
|
||||||
|
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
@@ -10,8 +12,10 @@ use ratatui::{
|
|||||||
};
|
};
|
||||||
use tui_textarea::TextArea;
|
use tui_textarea::TextArea;
|
||||||
|
|
||||||
|
/// Callback that syntax-highlights a single line, returning styled spans (bool = annotation).
|
||||||
pub type Highlighter<'a> = &'a dyn Fn(usize, &str) -> Vec<(Style, String, bool)>;
|
pub type Highlighter<'a> = &'a dyn Fn(usize, &str) -> Vec<(Style, String, bool)>;
|
||||||
|
|
||||||
|
/// Metadata for a single autocomplete entry.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct CompletionCandidate {
|
pub struct CompletionCandidate {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -78,6 +82,7 @@ impl SearchState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Multi-line text editor backed by tui_textarea.
|
||||||
pub struct Editor {
|
pub struct Editor {
|
||||||
text: TextArea<'static>,
|
text: TextArea<'static>,
|
||||||
completion: CompletionState,
|
completion: CompletionState,
|
||||||
@@ -702,6 +707,7 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Score a fuzzy match of `query` against `target`. Lower is better; `None` if no match.
|
||||||
pub fn fuzzy_match(query: &str, target: &str) -> Option<usize> {
|
pub fn fuzzy_match(query: &str, target: &str) -> Option<usize> {
|
||||||
let target_lower: Vec<char> = target.to_lowercase().chars().collect();
|
let target_lower: Vec<char> = target.to_lowercase().chars().collect();
|
||||||
let query_lower: Vec<char> = query.to_lowercase().chars().collect();
|
let query_lower: Vec<char> = query.to_lowercase().chars().collect();
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! File/directory browser modal widget.
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use ratatui::layout::{Constraint, Layout, Rect};
|
use ratatui::layout::{Constraint, Layout, Rect};
|
||||||
use ratatui::style::{Color, Style};
|
use ratatui::style::{Color, Style};
|
||||||
@@ -7,6 +9,7 @@ use ratatui::Frame;
|
|||||||
|
|
||||||
use super::ModalFrame;
|
use super::ModalFrame;
|
||||||
|
|
||||||
|
/// Modal listing files and directories with a filter input line.
|
||||||
pub struct FileBrowserModal<'a> {
|
pub struct FileBrowserModal<'a> {
|
||||||
title: &'a str,
|
title: &'a str,
|
||||||
input: &'a str,
|
input: &'a str,
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
//! Bottom-bar keyboard hint renderer.
|
||||||
|
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
use ratatui::style::Style;
|
use ratatui::style::Style;
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
|
|
||||||
|
/// Build a styled line of key/action pairs for the hint bar.
|
||||||
pub fn hint_line(pairs: &[(&str, &str)]) -> Line<'static> {
|
pub fn hint_line(pairs: &[(&str, &str)]) -> Line<'static> {
|
||||||
let theme = theme::get();
|
let theme = theme::get();
|
||||||
let key_style = Style::default().fg(theme.hint.key);
|
let key_style = Style::default().fg(theme.hint.key);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! Lissajous XY oscilloscope widget using braille characters.
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use ratatui::buffer::Buffer;
|
use ratatui::buffer::Buffer;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
@@ -9,6 +11,7 @@ thread_local! {
|
|||||||
static PATTERNS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
|
static PATTERNS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// XY oscilloscope plotting left vs right channels as a Lissajous curve.
|
||||||
pub struct Lissajous<'a> {
|
pub struct Lissajous<'a> {
|
||||||
left: &'a [f32],
|
left: &'a [f32],
|
||||||
right: &'a [f32],
|
right: &'a [f32],
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! Scrollable single-select list widget with cursor highlight.
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::style::{Modifier, Style};
|
use ratatui::style::{Modifier, Style};
|
||||||
@@ -5,6 +7,7 @@ use ratatui::text::{Line, Span};
|
|||||||
use ratatui::widgets::Paragraph;
|
use ratatui::widgets::Paragraph;
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
|
|
||||||
|
/// Scrollable list with a highlighted cursor and selected-item marker.
|
||||||
pub struct ListSelect<'a> {
|
pub struct ListSelect<'a> {
|
||||||
items: &'a [String],
|
items: &'a [String],
|
||||||
selected: usize,
|
selected: usize,
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
//! Centered modal frame with border and title.
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::style::{Color, Style};
|
use ratatui::style::{Color, Style};
|
||||||
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
|
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
|
|
||||||
|
/// Centered modal overlay with titled border.
|
||||||
pub struct ModalFrame<'a> {
|
pub struct ModalFrame<'a> {
|
||||||
title: &'a str,
|
title: &'a str,
|
||||||
width: u16,
|
width: u16,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! Page navigation minimap showing a 3x2 grid of tiles.
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use ratatui::layout::{Alignment, Rect};
|
use ratatui::layout::{Alignment, Rect};
|
||||||
use ratatui::style::Style;
|
use ratatui::style::Style;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! Vertical label/value property form renderer.
|
||||||
|
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::style::{Modifier, Style};
|
use ratatui::style::{Modifier, Style};
|
||||||
use ratatui::widgets::Paragraph;
|
use ratatui::widgets::Paragraph;
|
||||||
@@ -5,6 +7,7 @@ use ratatui::Frame;
|
|||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
|
|
||||||
|
/// Render a vertical list of label/value pairs with selection highlight.
|
||||||
pub fn render_props_form(frame: &mut Frame, area: Rect, fields: &[(&str, &str, bool)]) {
|
pub fn render_props_form(frame: &mut Frame, area: Rect, fields: &[(&str, &str, bool)]) {
|
||||||
let theme = theme::get();
|
let theme = theme::get();
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! Tree-view sample browser with search filtering.
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use ratatui::layout::{Constraint, Layout, Rect};
|
use ratatui::layout::{Constraint, Layout, Rect};
|
||||||
use ratatui::style::{Modifier, Style};
|
use ratatui::style::{Modifier, Style};
|
||||||
@@ -5,6 +7,7 @@ use ratatui::text::{Line, Span};
|
|||||||
use ratatui::widgets::{Block, Borders, Paragraph};
|
use ratatui::widgets::{Block, Borders, Paragraph};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
|
|
||||||
|
/// Node type in the sample tree.
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub enum TreeLineKind {
|
pub enum TreeLineKind {
|
||||||
Root { expanded: bool },
|
Root { expanded: bool },
|
||||||
@@ -12,6 +15,7 @@ pub enum TreeLineKind {
|
|||||||
File,
|
File,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A single row in the sample browser tree.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TreeLine {
|
pub struct TreeLine {
|
||||||
pub depth: u8,
|
pub depth: u8,
|
||||||
@@ -21,6 +25,7 @@ pub struct TreeLine {
|
|||||||
pub index: usize,
|
pub index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tree-view browser for navigating sample folders.
|
||||||
pub struct SampleBrowser<'a> {
|
pub struct SampleBrowser<'a> {
|
||||||
entries: &'a [TreeLine],
|
entries: &'a [TreeLine],
|
||||||
cursor: usize,
|
cursor: usize,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! Oscilloscope waveform widget using braille characters.
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use ratatui::buffer::Buffer;
|
use ratatui::buffer::Buffer;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
@@ -9,12 +11,14 @@ thread_local! {
|
|||||||
static PATTERNS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
|
static PATTERNS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Rendering direction for the oscilloscope.
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub enum Orientation {
|
pub enum Orientation {
|
||||||
Horizontal,
|
Horizontal,
|
||||||
Vertical,
|
Vertical,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Single-channel oscilloscope using braille dot plotting.
|
||||||
pub struct Scope<'a> {
|
pub struct Scope<'a> {
|
||||||
data: &'a [f32],
|
data: &'a [f32],
|
||||||
orientation: Orientation,
|
orientation: Orientation,
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
|
//! Up/down arrow scroll indicators for bounded lists.
|
||||||
|
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::style::{Color, Style};
|
use ratatui::style::{Color, Style};
|
||||||
use ratatui::widgets::Paragraph;
|
use ratatui::widgets::Paragraph;
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
|
|
||||||
|
/// Horizontal alignment for scroll indicators.
|
||||||
pub enum IndicatorAlign {
|
pub enum IndicatorAlign {
|
||||||
Center,
|
Center,
|
||||||
Right,
|
Right,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Render up/down scroll arrows when content overflows.
|
||||||
pub fn render_scroll_indicators(
|
pub fn render_scroll_indicators(
|
||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! Inline search bar with active/inactive styling.
|
||||||
|
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::style::Style;
|
use ratatui::style::Style;
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
@@ -6,6 +8,7 @@ use ratatui::Frame;
|
|||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
|
|
||||||
|
/// Render a `/query` search bar.
|
||||||
pub fn render_search_bar(frame: &mut Frame, area: Rect, query: &str, active: bool) {
|
pub fn render_search_bar(frame: &mut Frame, area: Rect, query: &str, active: bool) {
|
||||||
let theme = theme::get();
|
let theme = theme::get();
|
||||||
let style = if active {
|
let style = if active {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! Section header with horizontal divider for engine-view panels.
|
||||||
|
|
||||||
use ratatui::layout::{Constraint, Layout, Rect};
|
use ratatui::layout::{Constraint, Layout, Rect};
|
||||||
use ratatui::style::{Modifier, Style};
|
use ratatui::style::{Modifier, Style};
|
||||||
use ratatui::widgets::Paragraph;
|
use ratatui::widgets::Paragraph;
|
||||||
@@ -5,6 +7,7 @@ use ratatui::Frame;
|
|||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
|
|
||||||
|
/// Render a section title with a horizontal divider below it.
|
||||||
pub fn render_section_header(frame: &mut Frame, title: &str, focused: bool, area: Rect) {
|
pub fn render_section_header(frame: &mut Frame, title: &str, focused: bool, area: Rect) {
|
||||||
let theme = theme::get();
|
let theme = theme::get();
|
||||||
let [header_area, divider_area] =
|
let [header_area, divider_area] =
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! Decorative particle effect using random Unicode glyphs.
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use ratatui::buffer::Buffer;
|
use ratatui::buffer::Buffer;
|
||||||
@@ -14,6 +16,7 @@ struct Sparkle {
|
|||||||
life: u8,
|
life: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Animated sparkle particles for visual flair.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Sparkles {
|
pub struct Sparkles {
|
||||||
sparkles: Vec<Sparkle>,
|
sparkles: Vec<Sparkle>,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! 32-band frequency spectrum bar display.
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use ratatui::buffer::Buffer;
|
use ratatui::buffer::Buffer;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
@@ -6,6 +8,7 @@ use ratatui::widgets::Widget;
|
|||||||
|
|
||||||
const BLOCKS: [char; 8] = ['\u{2581}', '\u{2582}', '\u{2583}', '\u{2584}', '\u{2585}', '\u{2586}', '\u{2587}', '\u{2588}'];
|
const BLOCKS: [char; 8] = ['\u{2581}', '\u{2582}', '\u{2583}', '\u{2584}', '\u{2585}', '\u{2586}', '\u{2587}', '\u{2588}'];
|
||||||
|
|
||||||
|
/// 32-band spectrum analyzer using block characters.
|
||||||
pub struct Spectrum<'a> {
|
pub struct Spectrum<'a> {
|
||||||
data: &'a [f32; 32],
|
data: &'a [f32; 32],
|
||||||
gain: f32,
|
gain: f32,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! Single-line text input modal with optional hint.
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use ratatui::layout::{Constraint, Layout, Rect};
|
use ratatui::layout::{Constraint, Layout, Rect};
|
||||||
use ratatui::style::{Color, Style};
|
use ratatui::style::{Color, Style};
|
||||||
@@ -7,6 +9,7 @@ use ratatui::Frame;
|
|||||||
|
|
||||||
use super::ModalFrame;
|
use super::ModalFrame;
|
||||||
|
|
||||||
|
/// Modal dialog with a single-line text input.
|
||||||
pub struct TextInputModal<'a> {
|
pub struct TextInputModal<'a> {
|
||||||
title: &'a str,
|
title: &'a str,
|
||||||
input: &'a str,
|
input: &'a str,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! Stereo VU meter with dB-scaled level display.
|
||||||
|
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use ratatui::buffer::Buffer;
|
use ratatui::buffer::Buffer;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
@@ -8,6 +10,7 @@ const DB_MIN: f32 = -48.0;
|
|||||||
const DB_MAX: f32 = 3.0;
|
const DB_MAX: f32 = 3.0;
|
||||||
const DB_RANGE: f32 = DB_MAX - DB_MIN;
|
const DB_RANGE: f32 = DB_MAX - DB_MIN;
|
||||||
|
|
||||||
|
/// Stereo VU meter displaying left/right levels in dB.
|
||||||
pub struct VuMeter {
|
pub struct VuMeter {
|
||||||
left: f32,
|
left: f32,
|
||||||
right: f32,
|
right: f32,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//! Filled waveform display using braille characters.
|
||||||
|
|
||||||
use crate::scope::Orientation;
|
use crate::scope::Orientation;
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use ratatui::buffer::Buffer;
|
use ratatui::buffer::Buffer;
|
||||||
@@ -10,6 +12,7 @@ thread_local! {
|
|||||||
static PATTERNS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
|
static PATTERNS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Filled waveform renderer using braille dot plotting.
|
||||||
pub struct Waveform<'a> {
|
pub struct Waveform<'a> {
|
||||||
data: &'a [f32],
|
data: &'a [f32],
|
||||||
orientation: Orientation,
|
orientation: Orientation,
|
||||||
|
|||||||
39
src/main.rs
39
src/main.rs
@@ -56,7 +56,26 @@ struct Args {
|
|||||||
buffer: Option<u32>,
|
buffer: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn redirect_stderr() -> Option<std::fs::File> {
|
||||||
|
use std::os::fd::FromRawFd;
|
||||||
|
|
||||||
|
let mut fds = [0i32; 2];
|
||||||
|
if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
libc::dup2(fds[1], libc::STDERR_FILENO);
|
||||||
|
libc::close(fds[1]);
|
||||||
|
libc::fcntl(fds[0], libc::F_SETFL, libc::O_NONBLOCK);
|
||||||
|
Some(std::fs::File::from_raw_fd(fds[0]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> io::Result<()> {
|
fn main() -> io::Result<()> {
|
||||||
|
#[cfg(unix)]
|
||||||
|
let mut stderr_pipe = redirect_stderr();
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
engine::realtime::lock_memory();
|
engine::realtime::lock_memory();
|
||||||
|
|
||||||
@@ -171,6 +190,26 @@ fn main() -> io::Result<()> {
|
|||||||
app.ui.flash(&err, 3000, state::FlashKind::Error);
|
app.ui.flash(&err, 3000, state::FlashKind::Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
if let Some(ref mut pipe) = stderr_pipe {
|
||||||
|
use std::io::Read;
|
||||||
|
let max_len = terminal.size().map(|s| s.width as usize).unwrap_or(80).saturating_sub(16);
|
||||||
|
let mut buf = [0u8; 1024];
|
||||||
|
while let Ok(n) = pipe.read(&mut buf) {
|
||||||
|
if n == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let text = String::from_utf8_lossy(&buf[..n]);
|
||||||
|
for line in text.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if !line.is_empty() {
|
||||||
|
let capped = if line.len() > max_len { &line[..max_len] } else { line };
|
||||||
|
app.ui.flash(capped, 5000, state::FlashKind::Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
app.playback.playing = playing.load(Ordering::Relaxed);
|
app.playback.playing = playing.load(Ordering::Relaxed);
|
||||||
|
|
||||||
while let Ok(midi_cmd) = midi_rx.try_recv() {
|
while let Ok(midi_cmd) = midi_rx.try_recv() {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
const cargo = fs.readFileSync('../Cargo.toml', 'utf-8');
|
const cargo = fs.readFileSync('../Cargo.toml', 'utf-8');
|
||||||
const version = cargo.match(/\[workspace\.package\]\s*\nversion\s*=\s*"([^"]+)"/)?.[1];
|
const version = cargo.match(/\[workspace\.package\]\s*\nversion\s*=\s*"([^"]+)"/)?.[1];
|
||||||
|
const DL = 'https://dlcagire.raphaelforment.fr';
|
||||||
---
|
---
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@@ -60,30 +61,30 @@ const version = cargo.match(/\[workspace\.package\]\s*\nversion\s*=\s*"([^"]+)"/
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>macOS (Universal)</td>
|
<td>macOS (Universal)</td>
|
||||||
<td colspan="2"><a href="https://github.com/Bubobubobubobubo/cagire/releases/latest/download/cagire-macos-universal.pkg">.pkg</a></td>
|
<td colspan="2"><a href={`${DL}/cagire-macos-universal.pkg`}>.pkg</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>macOS (ARM)</td>
|
<td>macOS (ARM)</td>
|
||||||
<td><a href="https://github.com/Bubobubobubobubo/cagire/releases/latest/download/cagire-macos-aarch64-desktop.app.zip">.app</a></td>
|
<td><a href={`${DL}/cagire-macos-aarch64-desktop.app.zip`}>.app</a></td>
|
||||||
<td><a href="https://github.com/Bubobubobubobubo/cagire/releases/latest/download/cagire-macos-aarch64.tar.gz">.tar.gz</a></td>
|
<td><a href={`${DL}/cagire-macos-aarch64.tar.gz`}>.tar.gz</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>macOS (Intel)</td>
|
<td>macOS (Intel)</td>
|
||||||
<td><a href="https://github.com/Bubobubobubobubo/cagire/releases/latest/download/cagire-macos-x86_64-desktop.app.zip">.app</a></td>
|
<td><a href={`${DL}/cagire-macos-x86_64-desktop.app.zip`}>.app</a></td>
|
||||||
<td><a href="https://github.com/Bubobubobubobubo/cagire/releases/latest/download/cagire-macos-x86_64.tar.gz">.tar.gz</a></td>
|
<td><a href={`${DL}/cagire-macos-x86_64.tar.gz`}>.tar.gz</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Windows</td>
|
<td>Windows</td>
|
||||||
<td><a href="https://github.com/Bubobubobubobubo/cagire/releases/latest/download/cagire-windows-x86_64-desktop.exe">.exe</a></td>
|
<td><a href={`${DL}/cagire-windows-x86_64-desktop.exe`}>.exe</a></td>
|
||||||
<td><a href="https://github.com/Bubobubobubobubo/cagire/releases/latest/download/cagire-windows-x86_64.zip">.zip</a></td>
|
<td><a href={`${DL}/cagire-windows-x86_64.zip`}>.zip</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Linux</td>
|
<td>Linux</td>
|
||||||
<td><a href="https://github.com/Bubobubobubobubo/cagire/releases/latest/download/cagire-linux-x86_64-desktop.deb">.deb</a></td>
|
<td><a href={`${DL}/cagire-linux-x86_64-desktop.deb`}>.deb</a></td>
|
||||||
<td><a href="https://github.com/Bubobubobubobubo/cagire/releases/latest/download/cagire-linux-x86_64.tar.gz">.tar.gz</a></td>
|
<td><a href={`${DL}/cagire-linux-x86_64.tar.gz`}>.tar.gz</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<p class="note">All releases are available on <a href="https://github.com/Bubobubobubobubo/cagire/releases/latest">GitHub</a>. You can also compile the software yourself by getting it from Cargo!</p>
|
<p class="note">Source code and issue tracker on <a href="https://github.com/Bubobubobubobubo/cagire">GitHub</a>. You can also compile the software yourself from source!</p>
|
||||||
|
|
||||||
|
|
||||||
<h2>About</h2>
|
<h2>About</h2>
|
||||||
|
|||||||
Reference in New Issue
Block a user