From db5237480a5906e5d01bdf791f7ff6fef0931ae7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Wed, 28 Jan 2026 18:05:50 +0100 Subject: [PATCH] Before going crazy --- .gitignore | 4 + crates/forth/src/words.rs | 264 +++++++++++++++++++++++---- crates/project/src/file.rs | 16 +- crates/project/src/project.rs | 137 +++++++++++++- crates/ratatui/src/file_browser.rs | 8 +- src/app.rs | 6 +- src/model/mod.rs | 2 +- src/state/file_browser.rs | 6 + src/views/dict_view.rs | 30 +++- src/views/highlight.rs | 277 +++++------------------------ src/views/render.rs | 8 +- 11 files changed, 459 insertions(+), 299 deletions(-) diff --git a/.gitignore b/.gitignore index 0245993..646315b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,7 @@ Cargo.lock *.prof .DS_Store + +# Claude +.claude/ +CLAUDE.md diff --git a/crates/forth/src/words.rs b/crates/forth/src/words.rs index 283c168..0db7b37 100644 --- a/crates/forth/src/words.rs +++ b/crates/forth/src/words.rs @@ -6,12 +6,12 @@ pub enum WordCompile { Simple, Context(&'static str), Param, - Alias(&'static str), Probability(f64), } pub struct Word { pub name: &'static str, + pub aliases: &'static [&'static str], pub category: &'static str, pub stack: &'static str, pub desc: &'static str, @@ -25,6 +25,7 @@ pub const WORDS: &[Word] = &[ // Stack manipulation Word { name: "dup", + aliases: &[], category: "Stack", stack: "(a -- a a)", desc: "Duplicate top of stack", @@ -33,6 +34,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "dupn", + aliases: &[], category: "Stack", stack: "(a n -- a a ... a)", desc: "Duplicate a onto stack n times", @@ -41,6 +43,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "drop", + aliases: &[], category: "Stack", stack: "(a --)", desc: "Remove top of stack", @@ -49,6 +52,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "swap", + aliases: &[], category: "Stack", stack: "(a b -- b a)", desc: "Exchange top two items", @@ -57,6 +61,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "over", + aliases: &[], category: "Stack", stack: "(a b -- a b a)", desc: "Copy second to top", @@ -65,6 +70,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "rot", + aliases: &[], category: "Stack", stack: "(a b c -- b c a)", desc: "Rotate top three", @@ -73,6 +79,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "nip", + aliases: &[], category: "Stack", stack: "(a b -- b)", desc: "Remove second item", @@ -81,6 +88,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "tuck", + aliases: &[], category: "Stack", stack: "(a b -- b a b)", desc: "Copy top under second", @@ -90,6 +98,7 @@ pub const WORDS: &[Word] = &[ // Arithmetic Word { name: "+", + aliases: &[], category: "Arithmetic", stack: "(a b -- a+b)", desc: "Add", @@ -98,6 +107,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "-", + aliases: &[], category: "Arithmetic", stack: "(a b -- a-b)", desc: "Subtract", @@ -106,6 +116,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "*", + aliases: &[], category: "Arithmetic", stack: "(a b -- a*b)", desc: "Multiply", @@ -114,6 +125,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "/", + aliases: &[], category: "Arithmetic", stack: "(a b -- a/b)", desc: "Divide", @@ -122,6 +134,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "mod", + aliases: &[], category: "Arithmetic", stack: "(a b -- a%b)", desc: "Modulo", @@ -130,6 +143,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "neg", + aliases: &[], category: "Arithmetic", stack: "(a -- -a)", desc: "Negate", @@ -138,6 +152,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "abs", + aliases: &[], category: "Arithmetic", stack: "(a -- |a|)", desc: "Absolute value", @@ -146,6 +161,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "floor", + aliases: &[], category: "Arithmetic", stack: "(f -- n)", desc: "Round down to integer", @@ -154,6 +170,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "ceil", + aliases: &[], category: "Arithmetic", stack: "(f -- n)", desc: "Round up to integer", @@ -162,6 +179,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "round", + aliases: &[], category: "Arithmetic", stack: "(f -- n)", desc: "Round to nearest integer", @@ -170,6 +188,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "min", + aliases: &[], category: "Arithmetic", stack: "(a b -- min)", desc: "Minimum of two values", @@ -178,6 +197,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "max", + aliases: &[], category: "Arithmetic", stack: "(a b -- max)", desc: "Maximum of two values", @@ -186,6 +206,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "pow", + aliases: &[], category: "Arithmetic", stack: "(a b -- a^b)", desc: "Exponentiation", @@ -194,6 +215,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "sqrt", + aliases: &[], category: "Arithmetic", stack: "(a -- √a)", desc: "Square root", @@ -202,6 +224,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "sin", + aliases: &[], category: "Arithmetic", stack: "(a -- sin(a))", desc: "Sine (radians)", @@ -210,6 +233,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "cos", + aliases: &[], category: "Arithmetic", stack: "(a -- cos(a))", desc: "Cosine (radians)", @@ -218,6 +242,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "log", + aliases: &[], category: "Arithmetic", stack: "(a -- ln(a))", desc: "Natural logarithm", @@ -227,6 +252,7 @@ pub const WORDS: &[Word] = &[ // Comparison Word { name: "=", + aliases: &[], category: "Comparison", stack: "(a b -- bool)", desc: "Equal", @@ -235,22 +261,16 @@ pub const WORDS: &[Word] = &[ }, Word { name: "!=", + aliases: &["<>"], category: "Comparison", stack: "(a b -- bool)", desc: "Not equal", example: "3 4 != => 1", compile: Simple, }, - Word { - name: "<>", - category: "Comparison", - stack: "(a b -- bool)", - desc: "Not equal (traditional Forth)", - example: "3 4 <> => 1", - compile: Alias("!="), - }, Word { name: "lt", + aliases: &[], category: "Comparison", stack: "(a b -- bool)", desc: "Less than", @@ -259,6 +279,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "gt", + aliases: &[], category: "Comparison", stack: "(a b -- bool)", desc: "Greater than", @@ -267,6 +288,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "<=", + aliases: &[], category: "Comparison", stack: "(a b -- bool)", desc: "Less or equal", @@ -275,6 +297,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: ">=", + aliases: &[], category: "Comparison", stack: "(a b -- bool)", desc: "Greater or equal", @@ -284,6 +307,7 @@ pub const WORDS: &[Word] = &[ // Logic Word { name: "and", + aliases: &[], category: "Logic", stack: "(a b -- bool)", desc: "Logical and", @@ -292,6 +316,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "or", + aliases: &[], category: "Logic", stack: "(a b -- bool)", desc: "Logical or", @@ -300,6 +325,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "not", + aliases: &[], category: "Logic", stack: "(a -- bool)", desc: "Logical not", @@ -308,6 +334,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "xor", + aliases: &[], category: "Logic", stack: "(a b -- bool)", desc: "Exclusive or", @@ -316,6 +343,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "nand", + aliases: &[], category: "Logic", stack: "(a b -- bool)", desc: "Not and", @@ -324,6 +352,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "nor", + aliases: &[], category: "Logic", stack: "(a b -- bool)", desc: "Not or", @@ -332,6 +361,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "ifelse", + aliases: &[], category: "Logic", stack: "(true-quot false-quot bool --)", desc: "Execute true-quot if true, else false-quot", @@ -340,6 +370,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "pick", + aliases: &[], category: "Logic", stack: "(..quots n --)", desc: "Execute nth quotation (0-indexed)", @@ -349,22 +380,16 @@ pub const WORDS: &[Word] = &[ // Sound Word { name: "sound", + aliases: &["s"], category: "Sound", stack: "(name --)", desc: "Begin sound command", example: "\"kick\" sound", compile: Simple, }, - Word { - name: "s", - category: "Sound", - stack: "(name --)", - desc: "Alias for sound", - example: "\"kick\" s", - compile: Alias("sound"), - }, Word { name: ".", + aliases: &[], category: "Sound", stack: "(--)", desc: "Emit current sound, claim one time slot", @@ -373,6 +398,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "_", + aliases: &[], category: "Sound", stack: "(--)", desc: "Silence, claim one time slot", @@ -381,6 +407,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: ".!", + aliases: &[], category: "Sound", stack: "(n --)", desc: "Emit current sound n times", @@ -389,6 +416,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "div", + aliases: &[], category: "Time", stack: "(--)", desc: "Start a time subdivision scope (div claims a slot in parent)", @@ -397,6 +425,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "stack", + aliases: &[], category: "Time", stack: "(--)", desc: "Start a stacked subdivision scope (sounds stack/superpose)", @@ -405,6 +434,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "~", + aliases: &[], category: "Time", stack: "(--)", desc: "End a time subdivision scope (div or stack)", @@ -414,6 +444,7 @@ pub const WORDS: &[Word] = &[ // Variables (prefix syntax: @name to fetch, !name to store) Word { name: "@", + aliases: &[], category: "Variables", stack: "( -- val)", desc: "Fetch variable value", @@ -422,6 +453,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "!", + aliases: &[], category: "Variables", stack: "(val --)", desc: "Store value in variable", @@ -431,6 +463,7 @@ pub const WORDS: &[Word] = &[ // Randomness Word { name: "rand", + aliases: &[], category: "Randomness", stack: "(min max -- n|f)", desc: "Random in range. Int if both args are int, float otherwise", @@ -439,6 +472,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "seed", + aliases: &[], category: "Randomness", stack: "(n --)", desc: "Set random seed", @@ -447,6 +481,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "coin", + aliases: &[], category: "Randomness", stack: "(-- bool)", desc: "50/50 random boolean", @@ -455,6 +490,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "chance", + aliases: &[], category: "Probability", stack: "(quot prob --)", desc: "Execute quotation with probability (0.0-1.0)", @@ -463,6 +499,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "prob", + aliases: &[], category: "Probability", stack: "(quot pct --)", desc: "Execute quotation with probability (0-100)", @@ -471,6 +508,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "choose", + aliases: &[], category: "Randomness", stack: "(..n n -- val)", desc: "Random pick from n items", @@ -479,6 +517,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "cycle", + aliases: &[], category: "Lists", stack: "(..n n -- val)", desc: "Cycle through n items by step", @@ -487,6 +526,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "pcycle", + aliases: &[], category: "Lists", stack: "(..n n -- val)", desc: "Cycle through n items by pattern", @@ -495,6 +535,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "every", + aliases: &[], category: "Time", stack: "(n -- bool)", desc: "True every nth iteration", @@ -504,6 +545,7 @@ pub const WORDS: &[Word] = &[ // Probability shortcuts Word { name: "always", + aliases: &[], category: "Probability", stack: "(quot --)", desc: "Always execute quotation", @@ -512,6 +554,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "never", + aliases: &[], category: "Probability", stack: "(quot --)", desc: "Never execute quotation", @@ -520,6 +563,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "often", + aliases: &[], category: "Probability", stack: "(quot --)", desc: "Execute quotation 75% of the time", @@ -528,6 +572,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "sometimes", + aliases: &[], category: "Probability", stack: "(quot --)", desc: "Execute quotation 50% of the time", @@ -536,6 +581,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "rarely", + aliases: &[], category: "Probability", stack: "(quot --)", desc: "Execute quotation 25% of the time", @@ -544,6 +590,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "almostNever", + aliases: &[], category: "Probability", stack: "(quot --)", desc: "Execute quotation 10% of the time", @@ -552,6 +599,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "almostAlways", + aliases: &[], category: "Probability", stack: "(quot --)", desc: "Execute quotation 90% of the time", @@ -561,6 +609,7 @@ pub const WORDS: &[Word] = &[ // Context Word { name: "step", + aliases: &[], category: "Context", stack: "(-- n)", desc: "Current step index", @@ -569,6 +618,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "beat", + aliases: &[], category: "Context", stack: "(-- f)", desc: "Current beat position", @@ -577,6 +627,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "bank", + aliases: &[], category: "Sample", stack: "(str --)", desc: "Set sample bank suffix", @@ -585,6 +636,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "pattern", + aliases: &[], category: "Context", stack: "(-- n)", desc: "Current pattern index", @@ -593,6 +645,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "pbank", + aliases: &[], category: "Context", stack: "(-- n)", desc: "Current pattern's bank index", @@ -601,6 +654,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "tempo", + aliases: &[], category: "Context", stack: "(-- f)", desc: "Current BPM", @@ -609,6 +663,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "phase", + aliases: &[], category: "Context", stack: "(-- f)", desc: "Phase in bar (0-1)", @@ -617,6 +672,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "slot", + aliases: &[], category: "Context", stack: "(-- n)", desc: "Current slot number", @@ -625,6 +681,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "runs", + aliases: &[], category: "Context", stack: "(-- n)", desc: "Times this step ran", @@ -633,6 +690,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "iter", + aliases: &[], category: "Context", stack: "(-- n)", desc: "Pattern iteration count", @@ -641,6 +699,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "stepdur", + aliases: &[], category: "Context", stack: "(-- f)", desc: "Step duration in seconds", @@ -650,6 +709,7 @@ pub const WORDS: &[Word] = &[ // Live keys Word { name: "fill", + aliases: &[], category: "Context", stack: "(-- bool)", desc: "True when fill is on (f key)", @@ -659,6 +719,7 @@ pub const WORDS: &[Word] = &[ // Music Word { name: "mtof", + aliases: &[], category: "Music", stack: "(midi -- hz)", desc: "MIDI note to frequency", @@ -667,6 +728,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "ftom", + aliases: &[], category: "Music", stack: "(hz -- midi)", desc: "Frequency to MIDI note", @@ -676,6 +738,7 @@ pub const WORDS: &[Word] = &[ // LFO Word { name: "ramp", + aliases: &[], category: "LFO", stack: "(freq curve -- val)", desc: "Ramp [0,1]: fract(freq*beat)^curve", @@ -684,6 +747,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "range", + aliases: &[], category: "LFO", stack: "(val min max -- scaled)", desc: "Scale [0,1] to [min,max]", @@ -692,6 +756,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "linramp", + aliases: &[], category: "LFO", stack: "(freq -- val)", desc: "Linear ramp (curve=1)", @@ -700,6 +765,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "expramp", + aliases: &[], category: "LFO", stack: "(freq -- val)", desc: "Exponential ramp (curve=3)", @@ -708,6 +774,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "logramp", + aliases: &[], category: "LFO", stack: "(freq -- val)", desc: "Logarithmic ramp (curve=0.3)", @@ -716,6 +783,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "tri", + aliases: &[], category: "LFO", stack: "(freq -- val)", desc: "Triangle wave [0,1]: 0→1→0", @@ -724,6 +792,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "perlin", + aliases: &[], category: "LFO", stack: "(freq -- val)", desc: "Perlin noise [0,1] sampled at freq*beat", @@ -733,6 +802,7 @@ pub const WORDS: &[Word] = &[ // Time Word { name: "scale!", + aliases: &[], category: "Time", stack: "(factor --)", desc: "Set weight of current time scope", @@ -741,6 +811,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "loop", + aliases: &[], category: "Time", stack: "(n --)", desc: "Fit sample to n beats", @@ -749,6 +820,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "tempo!", + aliases: &[], category: "Time", stack: "(bpm --)", desc: "Set global tempo", @@ -757,6 +829,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "speed!", + aliases: &[], category: "Time", stack: "(multiplier --)", desc: "Set pattern speed multiplier", @@ -765,6 +838,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "chain", + aliases: &[], category: "Time", stack: "(bank pattern --)", desc: "Chain to bank/pattern (1-indexed) when current pattern ends", @@ -774,6 +848,7 @@ pub const WORDS: &[Word] = &[ // Lists Word { name: "[", + aliases: &["<", "<<"], category: "Lists", stack: "(-- marker)", desc: "Start list", @@ -782,38 +857,25 @@ pub const WORDS: &[Word] = &[ }, Word { name: "]", + aliases: &[], category: "Lists", stack: "(marker..n -- n)", desc: "End list, push count", example: "[ 1 2 3 ] => 3", compile: Simple, }, - Word { - name: "<", - category: "Lists", - stack: "(-- marker)", - desc: "Start cycle list", - example: "< 1 2 3 >", - compile: Alias("["), - }, Word { name: ">", + aliases: &[], category: "Lists", stack: "(marker..n -- val)", desc: "End cycle list, pick by step", example: "< 1 2 3 > => cycles through 1, 2, 3", compile: Simple, }, - Word { - name: "<<", - category: "Lists", - stack: "(-- marker)", - desc: "Start pattern cycle list", - example: "<< 1 2 3 >>", - compile: Alias("["), - }, Word { name: ">>", + aliases: &[], category: "Lists", stack: "(marker..n -- val)", desc: "End pattern cycle list, pick by pattern", @@ -823,6 +885,7 @@ pub const WORDS: &[Word] = &[ // Quotations Word { name: "?", + aliases: &[], category: "Logic", stack: "(quot bool --)", desc: "Execute quotation if true", @@ -831,6 +894,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "!?", + aliases: &[], category: "Logic", stack: "(quot bool --)", desc: "Execute quotation if false", @@ -840,6 +904,7 @@ pub const WORDS: &[Word] = &[ // Sample playback Word { name: "time", + aliases: &[], category: "Sample", stack: "(f --)", desc: "Set time offset", @@ -848,6 +913,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "repeat", + aliases: &[], category: "Sample", stack: "(n --)", desc: "Set repeat count", @@ -856,6 +922,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "dur", + aliases: &[], category: "Sample", stack: "(f --)", desc: "Set duration", @@ -864,6 +931,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "gate", + aliases: &[], category: "Sample", stack: "(f --)", desc: "Set gate time", @@ -872,6 +940,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "freq", + aliases: &[], category: "Oscillator", stack: "(f --)", desc: "Set frequency (Hz)", @@ -880,6 +949,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "detune", + aliases: &[], category: "Oscillator", stack: "(f --)", desc: "Set detune amount", @@ -888,6 +958,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "speed", + aliases: &[], category: "Sample", stack: "(f --)", desc: "Set playback speed", @@ -896,6 +967,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "glide", + aliases: &[], category: "Oscillator", stack: "(f --)", desc: "Set glide/portamento", @@ -904,6 +976,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "pw", + aliases: &[], category: "Oscillator", stack: "(f --)", desc: "Set pulse width", @@ -912,6 +985,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "spread", + aliases: &[], category: "Oscillator", stack: "(f --)", desc: "Set stereo spread", @@ -920,6 +994,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "mult", + aliases: &[], category: "Oscillator", stack: "(f --)", desc: "Set multiplier", @@ -928,6 +1003,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "warp", + aliases: &[], category: "Oscillator", stack: "(f --)", desc: "Set warp amount", @@ -936,6 +1012,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "mirror", + aliases: &[], category: "Oscillator", stack: "(f --)", desc: "Set mirror", @@ -944,6 +1021,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "harmonics", + aliases: &[], category: "Oscillator", stack: "(f --)", desc: "Set harmonics (mutable only)", @@ -952,6 +1030,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "timbre", + aliases: &[], category: "Oscillator", stack: "(f --)", desc: "Set timbre (mutable only)", @@ -960,6 +1039,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "morph", + aliases: &[], category: "Oscillator", stack: "(f --)", desc: "Set morph (mutable only)", @@ -968,6 +1048,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "begin", + aliases: &[], category: "Sample", stack: "(f --)", desc: "Set sample start (0-1)", @@ -976,6 +1057,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "end", + aliases: &[], category: "Sample", stack: "(f --)", desc: "Set sample end (0-1)", @@ -984,6 +1066,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "gain", + aliases: &[], category: "Gain", stack: "(f --)", desc: "Set volume (0-1)", @@ -992,6 +1075,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "postgain", + aliases: &[], category: "Gain", stack: "(f --)", desc: "Set post gain", @@ -1000,6 +1084,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "velocity", + aliases: &[], category: "Gain", stack: "(f --)", desc: "Set velocity", @@ -1008,6 +1093,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "pan", + aliases: &[], category: "Gain", stack: "(f --)", desc: "Set pan (-1 to 1)", @@ -1016,6 +1102,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "attack", + aliases: &["att"], category: "Envelope", stack: "(f --)", desc: "Set attack time", @@ -1024,6 +1111,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "decay", + aliases: &["dec"], category: "Envelope", stack: "(f --)", desc: "Set decay time", @@ -1032,6 +1120,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "sustain", + aliases: &["sus"], category: "Envelope", stack: "(f --)", desc: "Set sustain level", @@ -1040,6 +1129,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "release", + aliases: &["rel"], category: "Envelope", stack: "(f --)", desc: "Set release time", @@ -1048,6 +1138,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "adsr", + aliases: &[], category: "Envelope", stack: "(a d s r --)", desc: "Set attack, decay, sustain, release", @@ -1056,6 +1147,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "ad", + aliases: &[], category: "Envelope", stack: "(a d --)", desc: "Set attack, decay (sustain=0)", @@ -1064,6 +1156,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "lpf", + aliases: &[], category: "Filter", stack: "(f --)", desc: "Set lowpass frequency", @@ -1072,6 +1165,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "lpq", + aliases: &[], category: "Filter", stack: "(f --)", desc: "Set lowpass resonance", @@ -1080,6 +1174,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "lpe", + aliases: &[], category: "Filter", stack: "(f --)", desc: "Set lowpass envelope", @@ -1088,6 +1183,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "lpa", + aliases: &[], category: "Filter", stack: "(f --)", desc: "Set lowpass attack", @@ -1096,6 +1192,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "lpd", + aliases: &[], category: "Filter", stack: "(f --)", desc: "Set lowpass decay", @@ -1104,6 +1201,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "lps", + aliases: &[], category: "Filter", stack: "(f --)", desc: "Set lowpass sustain", @@ -1112,6 +1210,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "lpr", + aliases: &[], category: "Filter", stack: "(f --)", desc: "Set lowpass release", @@ -1120,6 +1219,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "hpf", + aliases: &[], category: "Filter", stack: "(f --)", desc: "Set highpass frequency", @@ -1128,6 +1228,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "hpq", + aliases: &[], category: "Filter", stack: "(f --)", desc: "Set highpass resonance", @@ -1136,6 +1237,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "hpe", + aliases: &[], category: "Filter", stack: "(f --)", desc: "Set highpass envelope", @@ -1144,6 +1246,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "hpa", + aliases: &[], category: "Filter", stack: "(f --)", desc: "Set highpass attack", @@ -1152,6 +1255,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "hpd", + aliases: &[], category: "Filter", stack: "(f --)", desc: "Set highpass decay", @@ -1160,6 +1264,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "hps", + aliases: &[], category: "Filter", stack: "(f --)", desc: "Set highpass sustain", @@ -1168,6 +1273,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "hpr", + aliases: &[], category: "Filter", stack: "(f --)", desc: "Set highpass release", @@ -1176,6 +1282,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "bpf", + aliases: &[], category: "Filter", stack: "(f --)", desc: "Set bandpass frequency", @@ -1184,6 +1291,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "bpq", + aliases: &[], category: "Filter", stack: "(f --)", desc: "Set bandpass resonance", @@ -1192,6 +1300,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "bpe", + aliases: &[], category: "Filter", stack: "(f --)", desc: "Set bandpass envelope", @@ -1200,6 +1309,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "bpa", + aliases: &[], category: "Filter", stack: "(f --)", desc: "Set bandpass attack", @@ -1208,6 +1318,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "bpd", + aliases: &[], category: "Filter", stack: "(f --)", desc: "Set bandpass decay", @@ -1216,6 +1327,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "bps", + aliases: &[], category: "Filter", stack: "(f --)", desc: "Set bandpass sustain", @@ -1224,6 +1336,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "bpr", + aliases: &[], category: "Filter", stack: "(f --)", desc: "Set bandpass release", @@ -1232,6 +1345,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "llpf", + aliases: &[], category: "Ladder Filter", stack: "(f --)", desc: "Set ladder lowpass frequency", @@ -1240,6 +1354,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "llpq", + aliases: &[], category: "Ladder Filter", stack: "(f --)", desc: "Set ladder lowpass resonance", @@ -1248,6 +1363,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "lhpf", + aliases: &[], category: "Ladder Filter", stack: "(f --)", desc: "Set ladder highpass frequency", @@ -1256,6 +1372,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "lhpq", + aliases: &[], category: "Ladder Filter", stack: "(f --)", desc: "Set ladder highpass resonance", @@ -1264,6 +1381,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "lbpf", + aliases: &[], category: "Ladder Filter", stack: "(f --)", desc: "Set ladder bandpass frequency", @@ -1272,6 +1390,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "lbpq", + aliases: &[], category: "Ladder Filter", stack: "(f --)", desc: "Set ladder bandpass resonance", @@ -1280,6 +1399,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "ftype", + aliases: &[], category: "Filter", stack: "(n --)", desc: "Set filter type", @@ -1288,6 +1408,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "penv", + aliases: &[], category: "Pitch Env", stack: "(f --)", desc: "Set pitch envelope", @@ -1296,6 +1417,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "patt", + aliases: &[], category: "Pitch Env", stack: "(f --)", desc: "Set pitch attack", @@ -1304,6 +1426,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "pdec", + aliases: &[], category: "Pitch Env", stack: "(f --)", desc: "Set pitch decay", @@ -1312,6 +1435,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "psus", + aliases: &[], category: "Pitch Env", stack: "(f --)", desc: "Set pitch sustain", @@ -1320,6 +1444,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "prel", + aliases: &[], category: "Pitch Env", stack: "(f --)", desc: "Set pitch release", @@ -1328,6 +1453,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "vib", + aliases: &[], category: "Modulation", stack: "(f --)", desc: "Set vibrato rate", @@ -1336,6 +1462,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "vibmod", + aliases: &[], category: "Modulation", stack: "(f --)", desc: "Set vibrato depth", @@ -1344,6 +1471,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "vibshape", + aliases: &[], category: "Modulation", stack: "(f --)", desc: "Set vibrato shape", @@ -1352,6 +1480,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "fm", + aliases: &[], category: "Modulation", stack: "(f --)", desc: "Set FM frequency", @@ -1360,6 +1489,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "fmh", + aliases: &[], category: "Modulation", stack: "(f --)", desc: "Set FM harmonic ratio", @@ -1368,6 +1498,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "fmshape", + aliases: &[], category: "Modulation", stack: "(f --)", desc: "Set FM shape", @@ -1376,6 +1507,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "fme", + aliases: &[], category: "Modulation", stack: "(f --)", desc: "Set FM envelope", @@ -1384,6 +1516,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "fma", + aliases: &[], category: "Modulation", stack: "(f --)", desc: "Set FM attack", @@ -1392,6 +1525,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "fmd", + aliases: &[], category: "Modulation", stack: "(f --)", desc: "Set FM decay", @@ -1400,6 +1534,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "fms", + aliases: &[], category: "Modulation", stack: "(f --)", desc: "Set FM sustain", @@ -1408,6 +1543,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "fmr", + aliases: &[], category: "Modulation", stack: "(f --)", desc: "Set FM release", @@ -1416,6 +1552,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "am", + aliases: &[], category: "Modulation", stack: "(f --)", desc: "Set AM frequency", @@ -1424,6 +1561,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "amdepth", + aliases: &[], category: "Modulation", stack: "(f --)", desc: "Set AM depth", @@ -1432,6 +1570,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "amshape", + aliases: &[], category: "Modulation", stack: "(f --)", desc: "Set AM shape", @@ -1440,6 +1579,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "rm", + aliases: &[], category: "Modulation", stack: "(f --)", desc: "Set RM frequency", @@ -1448,6 +1588,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "rmdepth", + aliases: &[], category: "Modulation", stack: "(f --)", desc: "Set RM depth", @@ -1456,6 +1597,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "rmshape", + aliases: &[], category: "Modulation", stack: "(f --)", desc: "Set RM shape", @@ -1464,6 +1606,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "phaser", + aliases: &[], category: "Mod FX", stack: "(f --)", desc: "Set phaser rate", @@ -1472,6 +1615,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "phaserdepth", + aliases: &[], category: "Mod FX", stack: "(f --)", desc: "Set phaser depth", @@ -1480,6 +1624,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "phasersweep", + aliases: &[], category: "Mod FX", stack: "(f --)", desc: "Set phaser sweep", @@ -1488,6 +1633,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "phasercenter", + aliases: &[], category: "Mod FX", stack: "(f --)", desc: "Set phaser center", @@ -1496,6 +1642,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "flanger", + aliases: &[], category: "Mod FX", stack: "(f --)", desc: "Set flanger rate", @@ -1504,6 +1651,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "flangerdepth", + aliases: &[], category: "Mod FX", stack: "(f --)", desc: "Set flanger depth", @@ -1512,6 +1660,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "flangerfeedback", + aliases: &[], category: "Mod FX", stack: "(f --)", desc: "Set flanger feedback", @@ -1520,6 +1669,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "chorus", + aliases: &[], category: "Mod FX", stack: "(f --)", desc: "Set chorus rate", @@ -1528,6 +1678,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "chorusdepth", + aliases: &[], category: "Mod FX", stack: "(f --)", desc: "Set chorus depth", @@ -1536,6 +1687,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "chorusdelay", + aliases: &[], category: "Mod FX", stack: "(f --)", desc: "Set chorus delay", @@ -1544,6 +1696,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "eqlo", + aliases: &[], category: "EQ", stack: "(f --)", desc: "Set low shelf gain (dB)", @@ -1552,6 +1705,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "eqmid", + aliases: &[], category: "EQ", stack: "(f --)", desc: "Set mid peak gain (dB)", @@ -1560,6 +1714,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "eqhi", + aliases: &[], category: "EQ", stack: "(f --)", desc: "Set high shelf gain (dB)", @@ -1568,6 +1723,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "tilt", + aliases: &[], category: "EQ", stack: "(f --)", desc: "Set tilt EQ (-1 dark, 1 bright)", @@ -1576,6 +1732,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "width", + aliases: &[], category: "Stereo", stack: "(f --)", desc: "Set stereo width (0 mono, 1 normal, 2 wide)", @@ -1584,6 +1741,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "haas", + aliases: &[], category: "Stereo", stack: "(f --)", desc: "Set Haas delay in ms (spatial placement)", @@ -1592,6 +1750,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "comb", + aliases: &[], category: "Filter", stack: "(f --)", desc: "Set comb filter mix", @@ -1600,6 +1759,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "combfreq", + aliases: &[], category: "Filter", stack: "(f --)", desc: "Set comb frequency", @@ -1608,6 +1768,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "combfeedback", + aliases: &[], category: "Filter", stack: "(f --)", desc: "Set comb feedback", @@ -1616,6 +1777,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "combdamp", + aliases: &[], category: "Filter", stack: "(f --)", desc: "Set comb damping", @@ -1624,6 +1786,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "coarse", + aliases: &[], category: "Oscillator", stack: "(f --)", desc: "Set coarse tune", @@ -1632,6 +1795,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "crush", + aliases: &[], category: "Lo-fi", stack: "(f --)", desc: "Set bit crush", @@ -1640,6 +1804,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "sub", + aliases: &[], category: "Oscillator", stack: "(f --)", desc: "Set sub oscillator level", @@ -1648,6 +1813,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "suboct", + aliases: &[], category: "Oscillator", stack: "(n --)", desc: "Set sub oscillator octave", @@ -1656,6 +1822,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "subwave", + aliases: &[], category: "Oscillator", stack: "(n --)", desc: "Set sub oscillator waveform", @@ -1664,6 +1831,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "fold", + aliases: &[], category: "Lo-fi", stack: "(f --)", desc: "Set wave fold", @@ -1672,6 +1840,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "wrap", + aliases: &[], category: "Lo-fi", stack: "(f --)", desc: "Set wave wrap", @@ -1680,6 +1849,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "distort", + aliases: &[], category: "Lo-fi", stack: "(f --)", desc: "Set distortion", @@ -1688,6 +1858,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "distortvol", + aliases: &[], category: "Lo-fi", stack: "(f --)", desc: "Set distortion volume", @@ -1696,6 +1867,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "delay", + aliases: &[], category: "Delay", stack: "(f --)", desc: "Set delay mix", @@ -1704,6 +1876,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "delaytime", + aliases: &[], category: "Delay", stack: "(f --)", desc: "Set delay time", @@ -1712,6 +1885,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "delayfeedback", + aliases: &[], category: "Delay", stack: "(f --)", desc: "Set delay feedback", @@ -1720,6 +1894,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "delaytype", + aliases: &[], category: "Delay", stack: "(n --)", desc: "Set delay type", @@ -1728,6 +1903,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "verb", + aliases: &[], category: "Reverb", stack: "(f --)", desc: "Set reverb mix", @@ -1736,6 +1912,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "verbdecay", + aliases: &[], category: "Reverb", stack: "(f --)", desc: "Set reverb decay", @@ -1744,6 +1921,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "verbdamp", + aliases: &[], category: "Reverb", stack: "(f --)", desc: "Set reverb damping", @@ -1752,6 +1930,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "verbpredelay", + aliases: &[], category: "Reverb", stack: "(f --)", desc: "Set reverb predelay", @@ -1760,6 +1939,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "verbdiff", + aliases: &[], category: "Reverb", stack: "(f --)", desc: "Set reverb diffusion", @@ -1768,6 +1948,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "voice", + aliases: &[], category: "Sample", stack: "(n --)", desc: "Set voice number", @@ -1776,6 +1957,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "orbit", + aliases: &[], category: "Sample", stack: "(n --)", desc: "Set orbit/bus", @@ -1784,6 +1966,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "note", + aliases: &[], category: "Oscillator", stack: "(n --)", desc: "Set MIDI note", @@ -1792,6 +1975,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "size", + aliases: &[], category: "Reverb", stack: "(f --)", desc: "Set size", @@ -1800,6 +1984,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "n", + aliases: &[], category: "Sample", stack: "(n --)", desc: "Set sample number", @@ -1808,6 +1993,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "cut", + aliases: &[], category: "Sample", stack: "(n --)", desc: "Set cut group", @@ -1816,6 +2002,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "reset", + aliases: &[], category: "Sample", stack: "(n --)", desc: "Reset parameter", @@ -1824,6 +2011,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: "clear", + aliases: &[], category: "Sound", stack: "(--)", desc: "Clear sound register (sound and all params)", @@ -1833,6 +2021,7 @@ pub const WORDS: &[Word] = &[ // Quotation execution Word { name: "apply", + aliases: &[], category: "Logic", stack: "(quot --)", desc: "Execute quotation unconditionally", @@ -1842,6 +2031,7 @@ pub const WORDS: &[Word] = &[ // Word definitions Word { name: ":", + aliases: &[], category: "Definitions", stack: "( -- )", desc: "Begin word definition", @@ -1850,6 +2040,7 @@ pub const WORDS: &[Word] = &[ }, Word { name: ";", + aliases: &[], category: "Definitions", stack: "( -- )", desc: "End word definition", @@ -2047,16 +2238,15 @@ pub(super) fn compile_word( } for word in WORDS { - if word.name == name { + if word.name == name || word.aliases.contains(&name) { match &word.compile { Simple => { - if let Some(op) = simple_op(name) { + if let Some(op) = simple_op(word.name) { ops.push(op); } } Context(ctx) => ops.push(Op::GetContext((*ctx).into())), - Param => ops.push(Op::SetParam(name.into())), - Alias(target) => return compile_word(target, span, ops, dict), + Param => ops.push(Op::SetParam(word.name.into())), Probability(p) => { ops.push(Op::PushFloat(*p, None)); ops.push(Op::ChanceExec); diff --git a/crates/project/src/file.rs b/crates/project/src/file.rs index 60ab66c..1ad2ac5 100644 --- a/crates/project/src/file.rs +++ b/crates/project/src/file.rs @@ -7,6 +7,15 @@ use serde::{Deserialize, Serialize}; use crate::project::{Bank, Project}; const VERSION: u8 = 1; +pub const EXTENSION: &str = "cagire"; + +pub fn ensure_extension(path: &Path) -> PathBuf { + if path.extension().map(|e| e == EXTENSION).unwrap_or(false) { + path.to_path_buf() + } else { + path.with_extension(EXTENSION) + } +} #[derive(Serialize, Deserialize)] struct ProjectFile { @@ -74,11 +83,12 @@ impl From for FileError { } } -pub fn save(project: &Project, path: &Path) -> Result<(), FileError> { +pub fn save(project: &Project, path: &Path) -> Result { + let path = ensure_extension(path); let file = ProjectFile::from(project); let json = serde_json::to_string_pretty(&file)?; - fs::write(path, json)?; - Ok(()) + fs::write(&path, json)?; + Ok(path) } pub fn load(path: &Path) -> Result { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 965b0b8..9d445ba 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -216,6 +216,12 @@ pub struct Step { pub source: Option, } +impl Step { + pub fn is_default(&self) -> bool { + self.active && self.script.is_empty() && self.source.is_none() + } +} + impl Default for Step { fn default() -> Self { Self { @@ -227,20 +233,141 @@ impl Default for Step { } } -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone)] pub struct Pattern { pub steps: Vec, pub length: usize, - #[serde(default)] pub speed: PatternSpeed, - #[serde(default)] pub name: Option, - #[serde(default)] pub quantization: LaunchQuantization, - #[serde(default)] pub sync_mode: SyncMode, } +#[derive(Serialize, Deserialize)] +struct SparseStep { + i: usize, + #[serde(default = "default_active", skip_serializing_if = "is_true")] + active: bool, + #[serde(default, skip_serializing_if = "String::is_empty")] + script: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + source: Option, +} + +fn default_active() -> bool { + true +} + +fn is_true(v: &bool) -> bool { + *v +} + +#[derive(Serialize, Deserialize)] +struct SparsePattern { + steps: Vec, + length: usize, + #[serde(default)] + speed: PatternSpeed, + #[serde(default, skip_serializing_if = "Option::is_none")] + name: Option, + #[serde(default, skip_serializing_if = "is_default_quantization")] + quantization: LaunchQuantization, + #[serde(default, skip_serializing_if = "is_default_sync_mode")] + sync_mode: SyncMode, +} + +fn is_default_quantization(q: &LaunchQuantization) -> bool { + *q == LaunchQuantization::default() +} + +fn is_default_sync_mode(s: &SyncMode) -> bool { + *s == SyncMode::default() +} + +#[derive(Deserialize)] +struct LegacyPattern { + steps: Vec, + length: usize, + #[serde(default)] + speed: PatternSpeed, + #[serde(default)] + name: Option, + #[serde(default)] + quantization: LaunchQuantization, + #[serde(default)] + sync_mode: SyncMode, +} + +impl Serialize for Pattern { + fn serialize(&self, serializer: S) -> Result { + let sparse_steps: Vec = self + .steps + .iter() + .enumerate() + .filter(|(_, step)| !step.is_default()) + .map(|(i, step)| SparseStep { + i, + active: step.active, + script: step.script.clone(), + source: step.source, + }) + .collect(); + + let sparse = SparsePattern { + steps: sparse_steps, + length: self.length, + speed: self.speed, + name: self.name.clone(), + quantization: self.quantization, + sync_mode: self.sync_mode, + }; + sparse.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Pattern { + fn deserialize>(deserializer: D) -> Result { + #[derive(Deserialize)] + #[serde(untagged)] + enum PatternFormat { + Sparse(SparsePattern), + Legacy(LegacyPattern), + } + + match PatternFormat::deserialize(deserializer)? { + PatternFormat::Sparse(sparse) => { + let mut steps: Vec = (0..MAX_STEPS).map(|_| Step::default()).collect(); + for ss in sparse.steps { + if ss.i < MAX_STEPS { + steps[ss.i] = Step { + active: ss.active, + script: ss.script, + command: None, + source: ss.source, + }; + } + } + Ok(Pattern { + steps, + length: sparse.length, + speed: sparse.speed, + name: sparse.name, + quantization: sparse.quantization, + sync_mode: sparse.sync_mode, + }) + } + PatternFormat::Legacy(legacy) => Ok(Pattern { + steps: legacy.steps, + length: legacy.length, + speed: legacy.speed, + name: legacy.name, + quantization: legacy.quantization, + sync_mode: legacy.sync_mode, + }), + } + } +} + impl Default for Pattern { fn default() -> Self { Self { diff --git a/crates/ratatui/src/file_browser.rs b/crates/ratatui/src/file_browser.rs index ac45a5c..0f82d7e 100644 --- a/crates/ratatui/src/file_browser.rs +++ b/crates/ratatui/src/file_browser.rs @@ -9,7 +9,7 @@ use super::ModalFrame; pub struct FileBrowserModal<'a> { title: &'a str, input: &'a str, - entries: &'a [(String, bool)], + entries: &'a [(String, bool, bool)], selected: usize, scroll_offset: usize, border_color: Color, @@ -18,7 +18,7 @@ pub struct FileBrowserModal<'a> { } impl<'a> FileBrowserModal<'a> { - pub fn new(title: &'a str, input: &'a str, entries: &'a [(String, bool)]) -> Self { + pub fn new(title: &'a str, input: &'a str, entries: &'a [(String, bool, bool)]) -> Self { Self { title, input, @@ -85,7 +85,7 @@ impl<'a> FileBrowserModal<'a> { let lines: Vec = visible_entries .enumerate() - .map(|(i, (name, is_dir))| { + .map(|(i, (name, is_dir, is_cagire))| { let abs_idx = i + self.scroll_offset; let is_selected = abs_idx == self.selected; let prefix = if is_selected { "> " } else { " " }; @@ -98,6 +98,8 @@ impl<'a> FileBrowserModal<'a> { Color::Yellow } else if *is_dir { Color::Blue + } else if *is_cagire { + Color::Magenta } else { Color::White }; diff --git a/src/app.rs b/src/app.rs index 36b5f36..d8d182b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -495,9 +495,9 @@ impl App { self.project_state.project.sample_paths = self.audio.config.sample_paths.clone(); self.project_state.project.tempo = link.tempo(); match model::save(&self.project_state.project, &path) { - Ok(()) => { - self.ui.set_status(format!("Saved: {}", path.display())); - self.project_state.file_path = Some(path); + Ok(final_path) => { + self.ui.set_status(format!("Saved: {}", final_path.display())); + self.project_state.file_path = Some(final_path); } Err(e) => { self.ui.set_status(format!("Save error: {e}")); diff --git a/src/model/mod.rs b/src/model/mod.rs index 45a6cbf..bd68f45 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1,6 +1,6 @@ mod script; -pub use cagire_forth::{Word, WORDS}; +pub use cagire_forth::{Word, WordCompile, WORDS}; pub use cagire_project::{ load, save, Bank, LaunchQuantization, Pattern, PatternSpeed, Project, SyncMode, MAX_BANKS, MAX_PATTERNS, diff --git a/src/state/file_browser.rs b/src/state/file_browser.rs index f54b999..36669d4 100644 --- a/src/state/file_browser.rs +++ b/src/state/file_browser.rs @@ -14,6 +14,12 @@ pub struct DirEntry { pub is_dir: bool, } +impl DirEntry { + pub fn is_cagire(&self) -> bool { + !self.is_dir && self.name.ends_with(".cagire") + } +} + #[derive(Clone, PartialEq, Eq)] pub struct FileBrowserState { pub mode: FileBrowserMode, diff --git a/src/views/dict_view.rs b/src/views/dict_view.rs index 45d5343..8d7ec33 100644 --- a/src/views/dict_view.rs +++ b/src/views/dict_view.rs @@ -109,7 +109,10 @@ fn render_words(frame: &mut Frame, app: &App, area: Rect, is_searching: bool) { let query = app.ui.dict_search_query.to_lowercase(); WORDS .iter() - .filter(|w| w.name.to_lowercase().contains(&query)) + .filter(|w| { + w.name.to_lowercase().contains(&query) + || w.aliases.iter().any(|a| a.to_lowercase().contains(&query)) + }) .collect() } else { let category = CATEGORIES[app.ui.dict_category]; @@ -144,12 +147,25 @@ fn render_words(frame: &mut Frame, app: &App, area: Rect, is_searching: bool) { .fg(Color::Green) .bg(name_bg) .add_modifier(Modifier::BOLD); - let name_line = format!(" {}", word.name); - let padding = " ".repeat(content_width.saturating_sub(name_line.chars().count())); - lines.push(RLine::from(Span::styled( - format!("{name_line}{padding}"), - name_style, - ))); + let alias_style = Style::new().fg(Color::DarkGray).bg(name_bg); + let name_text = if word.aliases.is_empty() { + format!(" {}", word.name) + } else { + format!(" {} ({})", word.name, word.aliases.join(", ")) + }; + let padding = " ".repeat(content_width.saturating_sub(name_text.chars().count())); + if word.aliases.is_empty() { + lines.push(RLine::from(Span::styled( + format!("{name_text}{padding}"), + name_style, + ))); + } else { + lines.push(RLine::from(vec![ + Span::styled(format!(" {}", word.name), name_style), + Span::styled(format!(" ({})", word.aliases.join(", ")), alias_style), + Span::styled(padding, name_style), + ])); + } let stack_style = Style::new().fg(Color::Magenta); lines.push(RLine::from(vec![ diff --git a/src/views/highlight.rs b/src/views/highlight.rs index 28f10fc..2c16887 100644 --- a/src/views/highlight.rs +++ b/src/views/highlight.rs @@ -1,6 +1,6 @@ use ratatui::style::{Color, Modifier, Style}; -use crate::model::SourceSpan; +use crate::model::{SourceSpan, WordCompile, WORDS}; #[derive(Clone, Copy, PartialEq, Eq)] pub enum TokenKind { @@ -45,223 +45,49 @@ pub struct Token { pub kind: TokenKind, } -const STACK_OPS: &[&str] = &["dup", "dupn", "drop", "swap", "over", "rot", "nip", "tuck"]; -const OPERATORS: &[&str] = &[ - "+", "-", "*", "/", "mod", "neg", "abs", "min", "max", "=", "<>", "<", ">", "<=", ">=", "and", - "or", "not", "ceil", "floor", "round", "mtof", "ftom", -]; -const KEYWORDS: &[&str] = &[ - "if", - "else", - "then", - "emit", - "rand", - "rrand", - "seed", - "cycle", - "choose", - "chance", - "[", - "]", - "zoom", - "scale!", - "stack", - "echo", - "necho", - "for", - "div", - "each", - "at", - "pop", - "adsr", - "ad", - "?", - "!?", - "<<", - ">>", - "|", - "@", - "!", - "pcycle", - "tempo!", - "prob", - "sometimes", - "often", - "rarely", - "almostAlways", - "almostNever", - "always", - "never", - "coin", - "fill", - "iter", - "every", - "gt", - "lt", - ":", - ";", - "apply", - "clear", -]; -const SOUND: &[&str] = &["sound", "s"]; -const CONTEXT: &[&str] = &[ - "step", "beat", "bank", "pattern", "tempo", "phase", "slot", "runs", "stepdur", -]; -const PARAMS: &[&str] = &[ - "time", - "repeat", - "dur", - "gate", - "freq", - "detune", - "speed", - "glide", - "pw", - "spread", - "mult", - "warp", - "mirror", - "harmonics", - "timbre", - "morph", - "begin", - "end", - "gain", - "postgain", - "velocity", - "pan", - "attack", - "decay", - "sustain", - "release", - "lpf", - "lpq", - "lpe", - "lpa", - "lpd", - "lps", - "lpr", - "hpf", - "hpq", - "hpe", - "hpa", - "hpd", - "hps", - "hpr", - "bpf", - "bpq", - "bpe", - "bpa", - "bpd", - "bps", - "bpr", - "ftype", - "penv", - "patt", - "pdec", - "psus", - "prel", - "vib", - "vibmod", - "vibshape", - "fm", - "fmh", - "fmshape", - "fme", - "fma", - "fmd", - "fms", - "fmr", - "am", - "amdepth", - "amshape", - "rm", - "rmdepth", - "rmshape", - "phaser", - "phaserdepth", - "phasersweep", - "phasercenter", - "flanger", - "flangerdepth", - "flangerfeedback", - "chorus", - "chorusdepth", - "chorusdelay", - "comb", - "combfreq", - "combfeedback", - "combdamp", - "coarse", - "crush", - "fold", - "wrap", - "distort", - "distortvol", - "delay", - "delaytime", - "delayfeedback", - "delaytype", - "verb", - "verbdecay", - "verbdamp", - "verbpredelay", - "verbdiff", - "voice", - "orbit", - "note", - "size", - "n", - "cut", - "reset", - "eqlo", - "eqmid", - "eqhi", - "tilt", - "width", - "haas", - "llpf", - "llpq", - "lhpf", - "lhpq", - "lbpf", - "lbpq", - "sub", - "suboct", - "subwave", - "bank", - "loop", -]; +fn lookup_word_kind(word: &str) -> Option { + for w in WORDS { + if w.name == word || w.aliases.contains(&word) { + return Some(match &w.compile { + WordCompile::Param => TokenKind::Param, + WordCompile::Context(_) => TokenKind::Context, + _ => match w.category { + "Stack" => TokenKind::StackOp, + "Arithmetic" | "Comparison" | "Music" => TokenKind::Operator, + "Logic" if matches!(w.name, "and" | "or" | "not" | "xor" | "nand" | "nor") => { + TokenKind::Operator + } + "Sound" => TokenKind::Sound, + _ => TokenKind::Keyword, + }, + }); + } + } + None +} + +fn is_note(word: &str) -> bool { + let bytes = word.as_bytes(); + if bytes.len() < 2 { + return false; + } + let base = matches!(bytes[0], b'c' | b'd' | b'e' | b'f' | b'g' | b'a' | b'b'); + if !base { + return false; + } + match bytes[1] { + b'#' | b's' | b'b' => bytes.len() > 2 && bytes[2..].iter().all(|b| b.is_ascii_digit()), + b'0'..=b'9' => bytes[1..].iter().all(|b| b.is_ascii_digit()), + _ => false, + } +} + const INTERVALS: &[&str] = &[ "P1", "unison", "m2", "M2", "m3", "M3", "P4", "aug4", "dim5", "tritone", "P5", "m6", "M6", "m7", "M7", "P8", "oct", "m9", "M9", "m10", "M10", "P11", "aug11", "P12", "m13", "M13", "m14", "M14", "P15", ]; -const NOTES: &[&str] = &[ - "c0", "c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "d0", "d1", "d2", "d3", "d4", "d5", - "d6", "d7", "d8", "d9", "e0", "e1", "e2", "e3", "e4", "e5", "e6", "e7", "e8", "e9", "f0", "f1", - "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "g0", "g1", "g2", "g3", "g4", "g5", "g6", "g7", - "g8", "g9", "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "b0", "b1", "b2", "b3", - "b4", "b5", "b6", "b7", "b8", "b9", "cs0", "cs1", "cs2", "cs3", "cs4", "cs5", "cs6", "cs7", - "cs8", "cs9", "ds0", "ds1", "ds2", "ds3", "ds4", "ds5", "ds6", "ds7", "ds8", "ds9", "es0", - "es1", "es2", "es3", "es4", "es5", "es6", "es7", "es8", "es9", "fs0", "fs1", "fs2", "fs3", - "fs4", "fs5", "fs6", "fs7", "fs8", "fs9", "gs0", "gs1", "gs2", "gs3", "gs4", "gs5", "gs6", - "gs7", "gs8", "gs9", "as0", "as1", "as2", "as3", "as4", "as5", "as6", "as7", "as8", "as9", - "bs0", "bs1", "bs2", "bs3", "bs4", "bs5", "bs6", "bs7", "bs8", "bs9", "cb0", "cb1", "cb2", - "cb3", "cb4", "cb5", "cb6", "cb7", "cb8", "cb9", "db0", "db1", "db2", "db3", "db4", "db5", - "db6", "db7", "db8", "db9", "eb0", "eb1", "eb2", "eb3", "eb4", "eb5", "eb6", "eb7", "eb8", - "eb9", "fb0", "fb1", "fb2", "fb3", "fb4", "fb5", "fb6", "fb7", "fb8", "fb9", "gb0", "gb1", - "gb2", "gb3", "gb4", "gb5", "gb6", "gb7", "gb8", "gb9", "ab0", "ab1", "ab2", "ab3", "ab4", - "ab5", "ab6", "ab7", "ab8", "ab9", "bb0", "bb1", "bb2", "bb3", "bb4", "bb5", "bb6", "bb7", - "bb8", "bb9", "c#0", "c#1", "c#2", "c#3", "c#4", "c#5", "c#6", "c#7", "c#8", "c#9", "d#0", - "d#1", "d#2", "d#3", "d#4", "d#5", "d#6", "d#7", "d#8", "d#9", "e#0", "e#1", "e#2", "e#3", - "e#4", "e#5", "e#6", "e#7", "e#8", "e#9", "f#0", "f#1", "f#2", "f#3", "f#4", "f#5", "f#6", - "f#7", "f#8", "f#9", "g#0", "g#1", "g#2", "g#3", "g#4", "g#5", "g#6", "g#7", "g#8", "g#9", - "a#0", "a#1", "a#2", "a#3", "a#4", "a#5", "a#6", "a#7", "a#8", "a#9", "b#0", "b#1", "b#2", - "b#3", "b#4", "b#5", "b#6", "b#7", "b#8", "b#9", -]; - pub fn tokenize_line(line: &str) -> Vec { let mut tokens = Vec::new(); let mut chars = line.char_indices().peekable(); @@ -329,36 +155,15 @@ fn classify_word(word: &str) -> TokenKind { return TokenKind::Number; } - if STACK_OPS.contains(&word) { - return TokenKind::StackOp; - } - - if OPERATORS.contains(&word) { - return TokenKind::Operator; - } - - if KEYWORDS.contains(&word) { - return TokenKind::Keyword; - } - - if SOUND.contains(&word) { - return TokenKind::Sound; - } - - if CONTEXT.contains(&word) { - return TokenKind::Context; - } - - if PARAMS.contains(&word) { - return TokenKind::Param; + if let Some(kind) = lookup_word_kind(word) { + return kind; } if INTERVALS.contains(&word) { return TokenKind::Interval; } - let lower = word.to_ascii_lowercase(); - if NOTES.contains(&lower.as_str()) { + if is_note(&word.to_ascii_lowercase()) { return TokenKind::Note; } diff --git a/src/views/render.rs b/src/views/render.rs index 374ed74..4fb67e4 100644 --- a/src/views/render.rs +++ b/src/views/render.rs @@ -431,10 +431,10 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term FileBrowserMode::Save => ("Save As", Color::Green), FileBrowserMode::Load => ("Load From", Color::Blue), }; - let entries: Vec<(String, bool)> = state + let entries: Vec<(String, bool, bool)> = state .entries .iter() - .map(|e| (e.name.clone(), e.is_dir)) + .map(|e| (e.name.clone(), e.is_dir, e.is_cagire())) .collect(); FileBrowserModal::new(title, &state.input, &entries) .selected(state.selected) @@ -483,10 +483,10 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term } Modal::AddSamplePath(state) => { use crate::widgets::FileBrowserModal; - let entries: Vec<(String, bool)> = state + let entries: Vec<(String, bool, bool)> = state .entries .iter() - .map(|e| (e.name.clone(), e.is_dir)) + .map(|e| (e.name.clone(), e.is_dir, e.is_cagire())) .collect(); FileBrowserModal::new("Add Sample Path", &state.input, &entries) .selected(state.selected)