Feat: WIP terse code documentation

This commit is contained in:
2026-02-26 01:08:16 +01:00
parent c2eeebcfb7
commit 8af17c01d8
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() {
#[cfg(windows)]
{

View File

@@ -15,6 +15,7 @@ enum Token {
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> {
let tokens = tokenize(input);
compile(&tokens, dict)

View File

@@ -4,6 +4,7 @@ use std::sync::Arc;
use super::types::SourceSpan;
/// Single VM instruction produced by the compiler.
#[derive(Clone, Debug, PartialEq)]
pub enum Op {
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 name: &'static str,
pub intervals: &'static [i64],
}
/// All built-in chord types.
pub static CHORDS: &[Chord] = &[
// Triads
Chord {
@@ -169,6 +173,7 @@ pub static CHORDS: &[Chord] = &[
},
];
/// Find a chord's intervals by name.
pub fn lookup(name: &str) -> Option<&'static [i64]> {
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;
mod scales;

View File

@@ -1,8 +1,12 @@
//! Scale definitions as semitone offset arrays.
/// Named scale with its semitone pattern.
pub struct Scale {
pub name: &'static str,
pub pattern: &'static [i64],
}
/// All built-in scale types.
pub static SCALES: &[Scale] = &[
Scale {
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]> {
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;
}
/// Byte range in source text.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct SourceSpan {
pub start: u32,
pub end: u32,
}
/// Concrete value resolved from a nondeterministic op, used for trace annotations.
#[derive(Clone, Debug)]
pub enum ResolvedValue {
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)]
pub struct ExecutionTrace {
pub executed_spans: Vec<SourceSpan>,
@@ -46,6 +49,7 @@ pub struct ExecutionTrace {
pub resolved: Vec<(SourceSpan, ResolvedValue)>,
}
/// Per-step sequencer state passed into the VM.
pub struct StepContext<'a> {
pub step: usize,
pub beat: f64,
@@ -72,13 +76,18 @@ impl StepContext<'_> {
}
}
/// Underlying map for user-defined variables.
pub type VariablesMap = HashMap<String, Value>;
/// Shared variable store, swapped atomically after each step.
pub type Variables = Arc<ArcSwap<VariablesMap>>;
/// Shared user-defined word dictionary.
pub type Dictionary = Arc<Mutex<HashMap<String, Vec<Op>>>>;
/// Shared random number generator.
pub type Rng = Arc<Mutex<StdRng>>;
pub type Stack = Mutex<Vec<Value>>;
pub(super) type CmdSnapshot<'a> = (Option<&'a Value>, &'a [(&'static str, Value)]);
/// Stack value in the Forth VM.
#[derive(Clone, Debug)]
pub enum Value {
Int(i64, Option<SourceSpan>),

View File

@@ -14,6 +14,7 @@ use super::types::{
Value, Variables, VariablesMap,
};
/// Forth VM instance. Holds the stack, variables, dictionary, and RNG.
pub struct Forth {
stack: Stack,
vars: Variables,
@@ -45,12 +46,14 @@ impl Forth {
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> {
let (outputs, var_writes) = self.evaluate_impl(script, ctx, None)?;
self.apply_var_writes(var_writes);
Ok(outputs)
}
/// Evaluate and collect an execution trace for UI highlighting.
pub fn evaluate_with_trace(
&self,
script: &str,
@@ -62,6 +65,7 @@ impl Forth {
Ok(outputs)
}
/// Evaluate and return both outputs and pending variable writes (without applying them).
pub fn evaluate_raw(
&self,
script: &str,

View File

@@ -1,3 +1,5 @@
//! Word-to-Op translation: maps Forth word names to compiled instructions.
use std::sync::Arc;
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] = &[
// Stack manipulation
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] = &[
// Envelope
Word {

View File

@@ -1,6 +1,7 @@
//! MIDI word definitions: channel, CC, pitch bend, transport, and device routing.
use super::{Word, WordCompile::*};
// MIDI
pub(super) const WORDS: &[Word] = &[
Word {
name: "chan",

View File

@@ -1,3 +1,5 @@
//! Built-in word definitions and lookup for the Forth VM.
mod compile;
mod core;
mod effects;
@@ -11,6 +13,7 @@ use std::sync::LazyLock;
pub(crate) use compile::compile_word;
/// How a word is compiled into ops.
#[derive(Clone, Copy)]
pub enum WordCompile {
Simple,
@@ -19,6 +22,7 @@ pub enum WordCompile {
Probability(f64),
}
/// Metadata for a built-in Forth word.
#[derive(Clone, Copy)]
pub struct Word {
pub name: &'static str,
@@ -31,6 +35,7 @@ pub struct Word {
pub varargs: bool,
}
/// All built-in words, aggregated from every category module.
pub static WORDS: LazyLock<Vec<Word>> = LazyLock::new(|| {
let mut words = Vec::new();
words.extend_from_slice(self::core::WORDS);
@@ -42,6 +47,7 @@ pub static WORDS: LazyLock<Vec<Word>> = LazyLock::new(|| {
words
});
/// Index mapping word names and aliases to their definitions.
static WORD_MAP: LazyLock<HashMap<&'static str, &'static Word>> = LazyLock::new(|| {
let mut map = HashMap::with_capacity(WORDS.len() * 2);
for word in WORDS.iter() {
@@ -53,6 +59,7 @@ static WORD_MAP: LazyLock<HashMap<&'static str, &'static Word>> = LazyLock::new(
map
});
/// Find a word by name or alias.
pub fn lookup_word(name: &str) -> Option<&'static Word> {
WORD_MAP.get(name).copied()
}

View File

@@ -1,6 +1,7 @@
//! Word definitions for music theory, harmony, and chord construction.
use super::{Word, WordCompile::*};
// Music, Chord
pub(super) const WORDS: &[Word] = &[
// Music
Word {

View File

@@ -1,6 +1,7 @@
//! Word metadata for sequencing: probability, timing, context queries, generators.
use super::{Word, WordCompile::*};
// Time, Context, Probability, Generator, Desktop
pub(super) const WORDS: &[Word] = &[
// Probability
Word {

View File

@@ -1,6 +1,7 @@
//! Word metadata for sound commands, sample/oscillator params, FM, modulation, and LFO.
use super::{Word, WordCompile::*};
// Sound, Oscillator, Sample, Wavetable, FM, Modulation, LFO
pub(super) const WORDS: &[Word] = &[
// Sound
Word {

View File

@@ -1,9 +1,13 @@
//! Syntax highlighting trait for fenced code blocks in markdown.
use ratatui::style::Style;
/// Produce styled spans from a single line of source code.
pub trait CodeHighlighter {
fn highlight(&self, line: &str) -> Vec<(Style, String)>;
}
/// Pass-through highlighter that applies no styling.
pub struct NoHighlight;
impl CodeHighlighter for NoHighlight {

View File

@@ -1,3 +1,5 @@
//! Parse markdown into styled ratatui lines with pluggable syntax highlighting.
mod highlighter;
mod parser;
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 ratatui::style::{Modifier, Style};
use ratatui::text::{Line as RLine, Span};
@@ -5,17 +7,20 @@ use ratatui::text::{Line as RLine, Span};
use crate::highlighter::CodeHighlighter;
use crate::theme::MarkdownTheme;
/// Span of lines within a parsed document that form a fenced code block.
pub struct CodeBlock {
pub start_line: usize,
pub end_line: usize,
pub source: String,
}
/// Result of parsing a markdown string: styled lines and extracted code blocks.
pub struct ParsedMarkdown {
pub lines: Vec<RLine<'static>>,
pub code_blocks: Vec<CodeBlock>,
}
/// Parse markdown text into themed, syntax-highlighted ratatui lines.
pub fn parse<T: MarkdownTheme, H: CodeHighlighter>(
md: &str,
theme: &T,
@@ -44,7 +49,7 @@ pub fn parse<T: MarkdownTheme, H: CodeHighlighter>(
let close_block = |start: Option<usize>,
source: &mut Vec<String>,
blocks: &mut Vec<CodeBlock>,
lines: &Vec<RLine<'static>>| {
lines: &[RLine<'static>]| {
if let Some(start) = start {
blocks.push(CodeBlock {
start_line: start,
@@ -118,7 +123,7 @@ pub fn parse<T: MarkdownTheme, H: CodeHighlighter>(
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());
for line in md.lines() {
let line = convert_dash_lists(line);
@@ -162,7 +167,7 @@ pub fn preprocess_markdown(md: &str) -> String {
out
}
pub fn convert_dash_lists(line: &str) -> String {
fn convert_dash_lists(line: &str) -> String {
let trimmed = line.trim_start();
if let Some(rest) = trimmed.strip_prefix("- ") {
let indent = line.len() - trimmed.len();

View File

@@ -1,5 +1,8 @@
//! Style provider trait for markdown rendering.
use ratatui::style::{Color, Modifier, Style};
/// Style provider for each markdown element type.
pub trait MarkdownTheme {
fn h1(&self) -> Style;
fn h2(&self) -> Style;
@@ -16,6 +19,7 @@ pub trait MarkdownTheme {
fn table_row_odd(&self) -> Color;
}
/// Fallback theme with hardcoded terminal colors, used in tests.
pub struct DefaultTheme;
impl MarkdownTheme for DefaultTheme {

View File

@@ -1,3 +1,5 @@
//! JSON-based project file persistence with versioned format.
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
@@ -7,9 +9,9 @@ use serde::{Deserialize, Serialize};
use crate::project::{Bank, Project};
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) {
path.to_path_buf()
} else {
@@ -62,6 +64,7 @@ impl From<ProjectFile> for Project {
}
}
/// Error returned by project save/load operations.
#[derive(Debug)]
pub enum FileError {
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> {
let path = ensure_extension(path);
let file = ProjectFile::from(project);
@@ -99,11 +103,13 @@ pub fn save(project: &Project, path: &Path) -> Result<PathBuf, FileError> {
Ok(path)
}
/// Read a project from a `.cagire` file on disk.
pub fn load(path: &Path) -> Result<Project, FileError> {
let json = fs::read_to_string(path)?;
load_str(&json)
}
/// Parse a project from a JSON string.
pub fn load_str(json: &str) -> Result<Project, FileError> {
let file: ProjectFile = serde_json::from_str(json)?;
if file.version > VERSION {

View File

@@ -4,9 +4,13 @@ mod file;
mod project;
pub mod share;
/// Maximum number of banks in a project.
pub const MAX_BANKS: usize = 32;
/// Maximum number of patterns per bank.
pub const MAX_PATTERNS: usize = 32;
/// Maximum number of steps per pattern.
pub const MAX_STEPS: usize = 1024;
/// Default pattern length in steps.
pub const DEFAULT_LENGTH: usize = 16;
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};
/// Speed multiplier for a pattern, expressed as a rational fraction.
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct PatternSpeed {
pub num: u8,
@@ -37,10 +38,12 @@ impl PatternSpeed {
Self::OCTO,
];
/// Return the speed as a floating-point multiplier.
pub fn multiplier(&self) -> 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 {
if self.denom == 1 {
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 {
let current = self.multiplier();
Self::PRESETS
@@ -58,6 +62,7 @@ impl PatternSpeed {
.unwrap_or(*self)
}
/// Return the next slower preset, or self if already at minimum.
pub fn prev(&self) -> Self {
let current = self.multiplier();
Self::PRESETS
@@ -68,6 +73,7 @@ impl PatternSpeed {
.unwrap_or(*self)
}
/// Parse a speed label like "2x" or "1/4x" into a `PatternSpeed`.
pub fn from_label(s: &str) -> Option<Self> {
let s = s.trim().trim_end_matches('x');
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)]
pub enum LaunchQuantization {
Immediate,
@@ -151,6 +158,7 @@ pub enum LaunchQuantization {
}
impl LaunchQuantization {
/// Human-readable label for display.
pub fn label(&self) -> &'static str {
match self {
Self::Immediate => "Immediate",
@@ -162,6 +170,7 @@ impl LaunchQuantization {
}
}
/// Cycle to the next longer quantization, clamped at `Bars8`.
pub fn next(&self) -> Self {
match self {
Self::Immediate => Self::Beat,
@@ -173,6 +182,7 @@ impl LaunchQuantization {
}
}
/// Cycle to the next shorter quantization, clamped at `Immediate`.
pub fn prev(&self) -> Self {
match self {
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)]
pub enum SyncMode {
#[default]
@@ -193,6 +204,7 @@ pub enum SyncMode {
}
impl SyncMode {
/// Human-readable label for display.
pub fn label(&self) -> &'static str {
match self {
Self::Reset => "Reset",
@@ -200,6 +212,7 @@ impl SyncMode {
}
}
/// Toggle between Reset and PhaseLock.
pub fn toggle(&self) -> Self {
match self {
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)]
pub enum FollowUp {
#[default]
@@ -217,6 +231,7 @@ pub enum FollowUp {
}
impl FollowUp {
/// Human-readable label for display.
pub fn label(&self) -> &'static str {
match self {
Self::Loop => "Loop",
@@ -225,6 +240,7 @@ impl FollowUp {
}
}
/// Cycle forward through follow-up modes.
pub fn next_mode(&self) -> Self {
match self {
Self::Loop => Self::Stop,
@@ -233,6 +249,7 @@ impl FollowUp {
}
}
/// Cycle backward through follow-up modes.
pub fn prev_mode(&self) -> Self {
match self {
Self::Loop => Self::Chain { bank: 0, pattern: 0 },
@@ -246,6 +263,7 @@ fn is_default_follow_up(f: &FollowUp) -> bool {
*f == FollowUp::default()
}
/// Single step in a pattern, holding a Forth script and optional metadata.
#[derive(Clone, Serialize, Deserialize)]
pub struct Step {
pub active: bool,
@@ -257,10 +275,12 @@ pub struct Step {
}
impl Step {
/// True if all fields are at their default values.
pub fn is_default(&self) -> bool {
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 {
!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)]
pub struct Pattern {
pub steps: Vec<Step>,
@@ -447,14 +468,17 @@ impl Default for Pattern {
}
impl Pattern {
/// Borrow a step by index.
pub fn step(&self, index: usize) -> Option<&Step> {
self.steps.get(index)
}
/// Mutably borrow a step by index.
pub fn step_mut(&mut self, index: usize) -> Option<&mut Step> {
self.steps.get_mut(index)
}
/// Set the active length, clamped to `[1, MAX_STEPS]`.
pub fn set_length(&mut self, length: usize) {
let length = length.clamp(1, MAX_STEPS);
while self.steps.len() < length {
@@ -463,6 +487,7 @@ impl Pattern {
self.length = length;
}
/// Follow the source chain from `index` to find the originating step.
pub fn resolve_source(&self, index: usize) -> usize {
let mut current = index;
for _ in 0..self.steps.len() {
@@ -479,20 +504,22 @@ impl Pattern {
index
}
/// Return the script at the resolved source of `index`.
pub fn resolve_script(&self, index: usize) -> Option<&str> {
let source_idx = self.resolve_source(index);
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 {
self.steps[..self.length]
.iter()
.filter(|s| s.has_content() || s.source.is_some())
.count()
}
}
/// Collection of patterns forming a bank.
#[derive(Clone, Serialize, Deserialize)]
pub struct Bank {
pub patterns: Vec<Pattern>,
@@ -501,6 +528,7 @@ pub struct Bank {
}
impl Bank {
/// Count patterns that contain at least one non-empty step.
pub fn content_pattern_count(&self) -> usize {
self.patterns
.iter()
@@ -518,6 +546,7 @@ impl Default for Bank {
}
}
/// Top-level project: banks, tempo, sample paths, and prelude script.
#[derive(Clone, Serialize, Deserialize)]
pub struct Project {
pub banks: Vec<Bank>,
@@ -548,14 +577,17 @@ impl Default for Project {
}
impl Project {
/// Borrow a pattern by bank and pattern index.
pub fn pattern_at(&self, bank: usize, pattern: usize) -> &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 {
&mut self.banks[bank].patterns[pattern]
}
/// Pad banks, patterns, and steps to their maximum sizes after deserialization.
pub fn normalize(&mut self) {
self.banks.resize_with(MAX_BANKS, Bank::default);
for bank in &mut self.banks {

View File

@@ -11,6 +11,7 @@ use crate::{Bank, Pattern};
const PATTERN_PREFIX: &str = "cgr:";
const BANK_PREFIX: &str = "cgrb:";
/// Error during pattern or bank import/export.
#[derive(Debug)]
pub enum ShareError {
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)
}
/// Encode a pattern as a shareable `cgr:` string.
pub fn export(pattern: &Pattern) -> Result<String, ShareError> {
encode(pattern, PATTERN_PREFIX)
}
/// Decode a `cgr:` string back into a pattern.
pub fn import(text: &str) -> Result<Pattern, ShareError> {
decode(text, PATTERN_PREFIX)
}
/// Encode a bank as a shareable `cgrb:` string.
pub fn export_bank(bank: &Bank) -> Result<String, ShareError> {
encode(bank, BANK_PREFIX)
}
/// Decode a `cgrb:` string back into a bank.
pub fn import_bank(text: &str) -> Result<Bank, ShareError> {
decode(text, BANK_PREFIX)
}

View File

@@ -1,3 +1,5 @@
//! Collapsible categorized list widget with section headers.
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::widgets::{Block, Borders, List, ListItem};
@@ -5,17 +7,20 @@ use ratatui::Frame;
use crate::theme;
/// Entry in a category list: either a section header or a leaf item.
pub struct CategoryItem<'a> {
pub label: &'a str,
pub is_section: bool,
pub collapsed: bool,
}
/// What is currently selected: a leaf item or a section header.
pub enum Selection {
Item(usize),
Section(usize),
}
/// Scrollable list with collapsible section headers.
pub struct CategoryList<'a> {
items: &'a [CategoryItem<'a>],
selection: Selection,

View File

@@ -1,3 +1,5 @@
//! Yes/No confirmation dialog widget.
use crate::theme;
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::style::Style;
@@ -7,6 +9,7 @@ use ratatui::Frame;
use super::ModalFrame;
/// Modal dialog with Yes/No buttons.
pub struct ConfirmModal<'a> {
title: &'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 crate::theme;
@@ -10,8 +12,10 @@ use ratatui::{
};
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)>;
/// Metadata for a single autocomplete entry.
#[derive(Clone)]
pub struct CompletionCandidate {
pub name: String,
@@ -78,6 +82,7 @@ impl SearchState {
}
}
/// Multi-line text editor backed by tui_textarea.
pub struct Editor {
text: TextArea<'static>,
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> {
let target_lower: Vec<char> = target.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 ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Color, Style};
@@ -7,6 +9,7 @@ use ratatui::Frame;
use super::ModalFrame;
/// Modal listing files and directories with a filter input line.
pub struct FileBrowserModal<'a> {
title: &'a str,
input: &'a str,

View File

@@ -1,8 +1,11 @@
//! Bottom-bar keyboard hint renderer.
use ratatui::text::{Line, Span};
use ratatui::style::Style;
use crate::theme;
/// Build a styled line of key/action pairs for the hint bar.
pub fn hint_line(pairs: &[(&str, &str)]) -> Line<'static> {
let theme = theme::get();
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 ratatui::buffer::Buffer;
use ratatui::layout::Rect;
@@ -9,6 +11,7 @@ thread_local! {
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> {
left: &'a [f32],
right: &'a [f32],

View File

@@ -1,3 +1,5 @@
//! Scrollable single-select list widget with cursor highlight.
use crate::theme;
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
@@ -5,6 +7,7 @@ use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use ratatui::Frame;
/// Scrollable list with a highlighted cursor and selected-item marker.
pub struct ListSelect<'a> {
items: &'a [String],
selected: usize,

View File

@@ -1,9 +1,12 @@
//! Centered modal frame with border and title.
use crate::theme;
use ratatui::layout::Rect;
use ratatui::style::{Color, Style};
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
use ratatui::Frame;
/// Centered modal overlay with titled border.
pub struct ModalFrame<'a> {
title: &'a str,
width: u16,

View File

@@ -1,3 +1,5 @@
//! Page navigation minimap showing a 3x2 grid of tiles.
use crate::theme;
use ratatui::layout::{Alignment, Rect};
use ratatui::style::Style;

View File

@@ -1,3 +1,5 @@
//! Vertical label/value property form renderer.
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::widgets::Paragraph;
@@ -5,6 +7,7 @@ use ratatui::Frame;
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)]) {
let theme = theme::get();

View File

@@ -1,3 +1,5 @@
//! Tree-view sample browser with search filtering.
use crate::theme;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Modifier, Style};
@@ -5,6 +7,7 @@ use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph};
use ratatui::Frame;
/// Node type in the sample tree.
#[derive(Clone, Copy)]
pub enum TreeLineKind {
Root { expanded: bool },
@@ -12,6 +15,7 @@ pub enum TreeLineKind {
File,
}
/// A single row in the sample browser tree.
#[derive(Clone)]
pub struct TreeLine {
pub depth: u8,
@@ -21,6 +25,7 @@ pub struct TreeLine {
pub index: usize,
}
/// Tree-view browser for navigating sample folders.
pub struct SampleBrowser<'a> {
entries: &'a [TreeLine],
cursor: usize,

View File

@@ -1,3 +1,5 @@
//! Oscilloscope waveform widget using braille characters.
use crate::theme;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
@@ -9,12 +11,14 @@ thread_local! {
static PATTERNS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
}
/// Rendering direction for the oscilloscope.
#[derive(Clone, Copy)]
pub enum Orientation {
Horizontal,
Vertical,
}
/// Single-channel oscilloscope using braille dot plotting.
pub struct Scope<'a> {
data: &'a [f32],
orientation: Orientation,

View File

@@ -1,13 +1,17 @@
//! Up/down arrow scroll indicators for bounded lists.
use ratatui::layout::Rect;
use ratatui::style::{Color, Style};
use ratatui::widgets::Paragraph;
use ratatui::Frame;
/// Horizontal alignment for scroll indicators.
pub enum IndicatorAlign {
Center,
Right,
}
/// Render up/down scroll arrows when content overflows.
pub fn render_scroll_indicators(
frame: &mut Frame,
area: Rect,

View File

@@ -1,3 +1,5 @@
//! Inline search bar with active/inactive styling.
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::text::{Line, Span};
@@ -6,6 +8,7 @@ use ratatui::Frame;
use crate::theme;
/// Render a `/query` search bar.
pub fn render_search_bar(frame: &mut Frame, area: Rect, query: &str, active: bool) {
let theme = theme::get();
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::style::{Modifier, Style};
use ratatui::widgets::Paragraph;
@@ -5,6 +7,7 @@ use ratatui::Frame;
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) {
let theme = theme::get();
let [header_area, divider_area] =

View File

@@ -1,3 +1,5 @@
//! Decorative particle effect using random Unicode glyphs.
use crate::theme;
use rand::Rng;
use ratatui::buffer::Buffer;
@@ -14,6 +16,7 @@ struct Sparkle {
life: u8,
}
/// Animated sparkle particles for visual flair.
#[derive(Default)]
pub struct Sparkles {
sparkles: Vec<Sparkle>,

View File

@@ -1,3 +1,5 @@
//! 32-band frequency spectrum bar display.
use crate::theme;
use ratatui::buffer::Buffer;
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}'];
/// 32-band spectrum analyzer using block characters.
pub struct Spectrum<'a> {
data: &'a [f32; 32],
gain: f32,

View File

@@ -1,3 +1,5 @@
//! Single-line text input modal with optional hint.
use crate::theme;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Color, Style};
@@ -7,6 +9,7 @@ use ratatui::Frame;
use super::ModalFrame;
/// Modal dialog with a single-line text input.
pub struct TextInputModal<'a> {
title: &'a str,
input: &'a str,

View File

@@ -1,3 +1,5 @@
//! Stereo VU meter with dB-scaled level display.
use crate::theme;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
@@ -8,6 +10,7 @@ const DB_MIN: f32 = -48.0;
const DB_MAX: f32 = 3.0;
const DB_RANGE: f32 = DB_MAX - DB_MIN;
/// Stereo VU meter displaying left/right levels in dB.
pub struct VuMeter {
left: f32,
right: f32,

View File

@@ -1,3 +1,5 @@
//! Filled waveform display using braille characters.
use crate::scope::Orientation;
use crate::theme;
use ratatui::buffer::Buffer;
@@ -10,6 +12,7 @@ thread_local! {
static PATTERNS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
}
/// Filled waveform renderer using braille dot plotting.
pub struct Waveform<'a> {
data: &'a [f32],
orientation: Orientation,

View File

@@ -56,7 +56,26 @@ struct Args {
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<()> {
#[cfg(unix)]
let mut stderr_pipe = redirect_stderr();
#[cfg(unix)]
engine::realtime::lock_memory();
@@ -171,6 +190,26 @@ fn main() -> io::Result<()> {
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);
while let Ok(midi_cmd) = midi_rx.try_recv() {

View File

@@ -2,6 +2,7 @@
import fs from 'node:fs';
const cargo = fs.readFileSync('../Cargo.toml', 'utf-8');
const version = cargo.match(/\[workspace\.package\]\s*\nversion\s*=\s*"([^"]+)"/)?.[1];
const DL = 'https://dlcagire.raphaelforment.fr';
---
<!DOCTYPE html>
@@ -60,30 +61,30 @@ const version = cargo.match(/\[workspace\.package\]\s*\nversion\s*=\s*"([^"]+)"/
</tr>
<tr>
<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>
<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="https://github.com/Bubobubobubobubo/cagire/releases/latest/download/cagire-macos-aarch64.tar.gz">.tar.gz</a></td>
<td><a href={`${DL}/cagire-macos-aarch64-desktop.app.zip`}>.app</a></td>
<td><a href={`${DL}/cagire-macos-aarch64.tar.gz`}>.tar.gz</a></td>
</tr>
<tr>
<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="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-desktop.app.zip`}>.app</a></td>
<td><a href={`${DL}/cagire-macos-x86_64.tar.gz`}>.tar.gz</a></td>
</tr>
<tr>
<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="https://github.com/Bubobubobubobubo/cagire/releases/latest/download/cagire-windows-x86_64.zip">.zip</a></td>
<td><a href={`${DL}/cagire-windows-x86_64-desktop.exe`}>.exe</a></td>
<td><a href={`${DL}/cagire-windows-x86_64.zip`}>.zip</a></td>
</tr>
<tr>
<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="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-desktop.deb`}>.deb</a></td>
<td><a href={`${DL}/cagire-linux-x86_64.tar.gz`}>.tar.gz</a></td>
</tr>
</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>