Feat: WIP terse code documentation

This commit is contained in:
2026-02-26 01:08:16 +01:00
parent 71bd09d5ea
commit e1cf57918e
47 changed files with 499 additions and 24 deletions

263
TODO.md Normal file
View 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

View File

@@ -1,3 +1,5 @@
//! Build script — embeds Windows application resources (icon, metadata).
fn main() { fn main() {
#[cfg(windows)] #[cfg(windows)]
{ {

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
//! Music theory data — chord and scale lookup tables.
pub mod chords; pub mod chords;
mod scales; mod scales;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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