Write some amount of documentation

This commit is contained in:
2026-01-31 01:46:18 +01:00
parent 4772b02f77
commit 4f9b1f39f9
57 changed files with 2096 additions and 198 deletions

View File

@@ -422,11 +422,11 @@ impl ThemeColors {
(166, 227, 161), // Green
],
link_dim: [
(70, 55, 85), // Mauve dimmed
(85, 65, 80), // Pink dimmed
(85, 60, 45), // Peach dimmed
(45, 75, 80), // Sky dimmed
(55, 80, 55), // Green dimmed
(70, 55, 85), // Mauve dimmed
(85, 65, 80), // Pink dimmed
(85, 60, 45), // Peach dimmed
(45, 75, 80), // Sky dimmed
(55, 80, 55), // Green dimmed
],
},
header: HeaderColors {
@@ -799,10 +799,7 @@ impl ThemeColors {
key: peach,
text: overlay1,
},
view_badge: ViewBadgeColors {
bg: text,
fg: base,
},
view_badge: ViewBadgeColors { bg: text, fg: base },
nav: NavColors {
selected_bg: Color::Rgb(215, 205, 245),
selected_fg: text,
@@ -923,21 +920,21 @@ impl ThemeColors {
pub fn nord() -> Self {
// Nord color palette
let polar_night0 = Color::Rgb(46, 52, 64); // nord0
let polar_night1 = Color::Rgb(59, 66, 82); // nord1
let polar_night2 = Color::Rgb(67, 76, 94); // nord2
let polar_night3 = Color::Rgb(76, 86, 106); // nord3
let polar_night0 = Color::Rgb(46, 52, 64); // nord0
let polar_night1 = Color::Rgb(59, 66, 82); // nord1
let polar_night2 = Color::Rgb(67, 76, 94); // nord2
let polar_night3 = Color::Rgb(76, 86, 106); // nord3
let snow_storm0 = Color::Rgb(216, 222, 233); // nord4
let _snow_storm1 = Color::Rgb(229, 233, 240); // nord5
let snow_storm2 = Color::Rgb(236, 239, 244); // nord6
let frost0 = Color::Rgb(143, 188, 187); // nord7 (teal)
let frost1 = Color::Rgb(136, 192, 208); // nord8 (light blue)
let frost2 = Color::Rgb(129, 161, 193); // nord9 (blue)
let _frost3 = Color::Rgb(94, 129, 172); // nord10 (dark blue)
let aurora_red = Color::Rgb(191, 97, 106); // nord11
let frost0 = Color::Rgb(143, 188, 187); // nord7 (teal)
let frost1 = Color::Rgb(136, 192, 208); // nord8 (light blue)
let frost2 = Color::Rgb(129, 161, 193); // nord9 (blue)
let _frost3 = Color::Rgb(94, 129, 172); // nord10 (dark blue)
let aurora_red = Color::Rgb(191, 97, 106); // nord11
let aurora_orange = Color::Rgb(208, 135, 112); // nord12
let aurora_yellow = Color::Rgb(235, 203, 139); // nord13
let aurora_green = Color::Rgb(163, 190, 140); // nord14
let aurora_green = Color::Rgb(163, 190, 140); // nord14
let aurora_purple = Color::Rgb(180, 142, 173); // nord15
Self {
@@ -992,11 +989,11 @@ impl ThemeColors {
(163, 190, 140), // Aurora green
],
link_dim: [
(55, 75, 85), // Frost1 dimmed
(70, 60, 70), // Purple dimmed
(75, 55, 50), // Orange dimmed
(55, 75, 75), // Frost0 dimmed
(60, 75, 55), // Green dimmed
(55, 75, 85), // Frost1 dimmed
(70, 60, 70), // Purple dimmed
(75, 55, 50), // Orange dimmed
(55, 75, 75), // Frost0 dimmed
(60, 75, 55), // Green dimmed
],
},
header: HeaderColors {
@@ -1270,11 +1267,11 @@ impl ThemeColors {
(80, 250, 123), // Green
],
link_dim: [
(75, 60, 95), // Purple dimmed
(95, 55, 80), // Pink dimmed
(95, 70, 50), // Orange dimmed
(55, 90, 95), // Cyan dimmed
(40, 95, 55), // Green dimmed
(75, 60, 95), // Purple dimmed
(95, 55, 80), // Pink dimmed
(95, 70, 50), // Orange dimmed
(55, 90, 95), // Cyan dimmed
(40, 95, 55), // Green dimmed
],
},
header: HeaderColors {
@@ -1481,21 +1478,21 @@ impl ThemeColors {
pub fn gruvbox_dark() -> Self {
// Gruvbox Dark palette
let bg0 = Color::Rgb(40, 40, 40); // #282828
let bg1 = Color::Rgb(60, 56, 54); // #3c3836
let bg2 = Color::Rgb(80, 73, 69); // #504945
let _bg3 = Color::Rgb(102, 92, 84); // #665c54
let fg = Color::Rgb(235, 219, 178); // #ebdbb2
let fg2 = Color::Rgb(213, 196, 161); // #d5c4a1
let fg3 = Color::Rgb(189, 174, 147); // #bdae93
let fg4 = Color::Rgb(168, 153, 132); // #a89984
let red = Color::Rgb(251, 73, 52); // #fb4934
let green = Color::Rgb(184, 187, 38); // #b8bb26
let yellow = Color::Rgb(250, 189, 47); // #fabd2f
let blue = Color::Rgb(131, 165, 152); // #83a598
let bg0 = Color::Rgb(40, 40, 40); // #282828
let bg1 = Color::Rgb(60, 56, 54); // #3c3836
let bg2 = Color::Rgb(80, 73, 69); // #504945
let _bg3 = Color::Rgb(102, 92, 84); // #665c54
let fg = Color::Rgb(235, 219, 178); // #ebdbb2
let fg2 = Color::Rgb(213, 196, 161); // #d5c4a1
let fg3 = Color::Rgb(189, 174, 147); // #bdae93
let fg4 = Color::Rgb(168, 153, 132); // #a89984
let red = Color::Rgb(251, 73, 52); // #fb4934
let green = Color::Rgb(184, 187, 38); // #b8bb26
let yellow = Color::Rgb(250, 189, 47); // #fabd2f
let blue = Color::Rgb(131, 165, 152); // #83a598
let purple = Color::Rgb(211, 134, 155); // #d3869b
let aqua = Color::Rgb(142, 192, 124); // #8ec07c
let orange = Color::Rgb(254, 128, 25); // #fe8019
let aqua = Color::Rgb(142, 192, 124); // #8ec07c
let orange = Color::Rgb(254, 128, 25); // #fe8019
let darker_bg = Color::Rgb(29, 32, 33); // #1d2021
@@ -1551,11 +1548,11 @@ impl ThemeColors {
(184, 187, 38), // Green
],
link_dim: [
(85, 55, 35), // Orange dimmed
(75, 55, 65), // Purple dimmed
(80, 70, 40), // Yellow dimmed
(50, 60, 60), // Blue dimmed
(60, 65, 35), // Green dimmed
(85, 55, 35), // Orange dimmed
(75, 55, 65), // Purple dimmed
(80, 70, 40), // Yellow dimmed
(50, 60, 60), // Blue dimmed
(60, 65, 35), // Green dimmed
],
},
header: HeaderColors {
@@ -1638,10 +1635,7 @@ impl ThemeColors {
key: orange,
text: fg4,
},
view_badge: ViewBadgeColors {
bg: fg,
fg: bg0,
},
view_badge: ViewBadgeColors { bg: fg, fg: bg0 },
nav: NavColors {
selected_bg: Color::Rgb(80, 65, 50),
selected_fg: fg,
@@ -1762,18 +1756,18 @@ impl ThemeColors {
pub fn monokai() -> Self {
// Monokai palette
let bg = Color::Rgb(39, 40, 34); // #272822
let bg_light = Color::Rgb(53, 54, 47); // #35362f
let bg = Color::Rgb(39, 40, 34); // #272822
let bg_light = Color::Rgb(53, 54, 47); // #35362f
let bg_lighter = Color::Rgb(70, 71, 62);
let fg = Color::Rgb(248, 248, 242); // #f8f8f2
let fg = Color::Rgb(248, 248, 242); // #f8f8f2
let fg_dim = Color::Rgb(190, 190, 180);
let comment = Color::Rgb(117, 113, 94); // #75715e
let pink = Color::Rgb(249, 38, 114); // #f92672
let green = Color::Rgb(166, 226, 46); // #a6e22e
let pink = Color::Rgb(249, 38, 114); // #f92672
let green = Color::Rgb(166, 226, 46); // #a6e22e
let yellow = Color::Rgb(230, 219, 116); // #e6db74
let blue = Color::Rgb(102, 217, 239); // #66d9ef
let blue = Color::Rgb(102, 217, 239); // #66d9ef
let purple = Color::Rgb(174, 129, 255); // #ae81ff
let orange = Color::Rgb(253, 151, 31); // #fd971f
let orange = Color::Rgb(253, 151, 31); // #fd971f
let darker_bg = Color::Rgb(30, 31, 26);
@@ -1829,11 +1823,11 @@ impl ThemeColors {
(166, 226, 46), // Green
],
link_dim: [
(90, 40, 60), // Pink dimmed
(70, 55, 90), // Purple dimmed
(85, 60, 35), // Orange dimmed
(50, 75, 85), // Blue dimmed
(60, 80, 40), // Green dimmed
(90, 40, 60), // Pink dimmed
(70, 55, 90), // Purple dimmed
(85, 60, 35), // Orange dimmed
(50, 75, 85), // Blue dimmed
(60, 80, 40), // Green dimmed
],
},
header: HeaderColors {
@@ -1916,10 +1910,7 @@ impl ThemeColors {
key: orange,
text: comment,
},
view_badge: ViewBadgeColors {
bg: fg,
fg: bg,
},
view_badge: ViewBadgeColors { bg: fg, fg: bg },
nav: NavColors {
selected_bg: Color::Rgb(80, 60, 75),
selected_fg: fg,
@@ -2040,11 +2031,11 @@ impl ThemeColors {
pub fn pitch_black() -> Self {
// Pitch Black (OLED) palette - pure black background with high contrast
let bg = Color::Rgb(0, 0, 0); // Pure black
let surface = Color::Rgb(10, 10, 10); // Very subtle surface
let surface2 = Color::Rgb(21, 21, 21); // Slightly visible surface
let border = Color::Rgb(40, 40, 40); // Subtle borders
let fg = Color::Rgb(230, 230, 230); // Bright white text
let bg = Color::Rgb(0, 0, 0); // Pure black
let surface = Color::Rgb(10, 10, 10); // Very subtle surface
let surface2 = Color::Rgb(21, 21, 21); // Slightly visible surface
let border = Color::Rgb(40, 40, 40); // Subtle borders
let fg = Color::Rgb(230, 230, 230); // Bright white text
let fg_dim = Color::Rgb(160, 160, 160);
let fg_muted = Color::Rgb(100, 100, 100);
@@ -2109,11 +2100,11 @@ impl ThemeColors {
(80, 255, 120), // Green
],
link_dim: [
(25, 60, 60), // Cyan dimmed
(50, 35, 65), // Purple dimmed
(60, 45, 20), // Orange dimmed
(25, 50, 70), // Blue dimmed
(25, 65, 35), // Green dimmed
(25, 60, 60), // Cyan dimmed
(50, 35, 65), // Purple dimmed
(60, 45, 20), // Orange dimmed
(25, 50, 70), // Blue dimmed
(25, 65, 35), // Green dimmed
],
},
header: HeaderColors {
@@ -2196,10 +2187,7 @@ impl ThemeColors {
key: orange,
text: fg_muted,
},
view_badge: ViewBadgeColors {
bg: fg,
fg: bg,
},
view_badge: ViewBadgeColors { bg: fg, fg: bg },
nav: NavColors {
selected_bg: Color::Rgb(40, 45, 55),
selected_fg: fg,
@@ -2324,16 +2312,36 @@ impl ThemeColors {
pub mod ui {
use super::*;
pub fn bg() -> Color { get().ui.bg }
pub fn bg_rgb() -> (u8, u8, u8) { get().ui.bg_rgb }
pub fn text_primary() -> Color { get().ui.text_primary }
pub fn text_muted() -> Color { get().ui.text_muted }
pub fn text_dim() -> Color { get().ui.text_dim }
pub fn border() -> Color { get().ui.border }
pub fn header() -> Color { get().ui.header }
pub fn unfocused() -> Color { get().ui.unfocused }
pub fn accent() -> Color { get().ui.accent }
pub fn surface() -> Color { get().ui.surface }
pub fn bg() -> Color {
get().ui.bg
}
pub fn bg_rgb() -> (u8, u8, u8) {
get().ui.bg_rgb
}
pub fn text_primary() -> Color {
get().ui.text_primary
}
pub fn text_muted() -> Color {
get().ui.text_muted
}
pub fn text_dim() -> Color {
get().ui.text_dim
}
pub fn border() -> Color {
get().ui.border
}
pub fn header() -> Color {
get().ui.header
}
pub fn unfocused() -> Color {
get().ui.unfocused
}
pub fn accent() -> Color {
get().ui.accent
}
pub fn surface() -> Color {
get().ui.surface
}
// Constants for backward compatibility
pub const BG: Color = Color::Rgb(30, 30, 46);
@@ -2385,10 +2393,18 @@ pub mod tile {
pub const ACTIVE_SELECTED_BG: Color = Color::Rgb(70, 60, 80);
pub const ACTIVE_IN_RANGE_BG: Color = Color::Rgb(55, 55, 70);
pub const LINK_BRIGHT: [(u8, u8, u8); 5] = [
(203, 166, 247), (245, 194, 231), (250, 179, 135), (137, 220, 235), (166, 227, 161),
(203, 166, 247),
(245, 194, 231),
(250, 179, 135),
(137, 220, 235),
(166, 227, 161),
];
pub const LINK_DIM: [(u8, u8, u8); 5] = [
(70, 55, 85), (85, 65, 80), (85, 60, 45), (45, 75, 80), (55, 80, 55),
(70, 55, 85),
(85, 65, 80),
(85, 60, 45),
(45, 75, 80),
(55, 80, 55),
];
}
@@ -2619,7 +2635,11 @@ pub mod meter {
pub mod sparkle {
pub const COLORS: &[(u8, u8, u8)] = &[
(200, 220, 255), (250, 179, 135), (166, 227, 161), (245, 194, 231), (203, 166, 247),
(200, 220, 255),
(250, 179, 135),
(166, 227, 161),
(245, 194, 231),
(203, 166, 247),
];
}

View File

@@ -1,18 +0,0 @@
# About
Cagire is an experimental step sequencer built by BuboBubo (Raphaël Maurice Forment). It is a free and open-source project licensed under the AGPL-3.0 License. Cagire has been developed as a side project. I wanted to learn more about using Forth and needed a playground for experimentating with this audio engine! You are free to contribute to the project by making direct contributions to the codebase or by providing feedback and suggestions.
## Credits
- **Doux** (audio engine) is a Rust port of Dough, originally written in C by Felix Roos.
- **mi-plaits-dsp-rs** is a Rust port of the code used by the Mutable Instruments Plaits.
* _Author_: Oliver Rockstedt [info@sourcebox.de](info@sourcebox.de).
* _Original author_: Emilie Gillet [emilie.o.gillet@gmail.com](emilie.o.gillet@gmail.com).
## About live coding
Live coding is a technique where a programmer writes code in real-time in front of an audience. It is a way to experiment with code, to share things and thoughts openly, to express yourself through code. It can be technical, poetical, weird, preferably all at once. Live coding can be used to create music, visual art, and other forms of media. Learn more about live coding on [https://toplap.org](https://toplap.org) or [https://livecoding.fr](https://livecoding.fr). Live coding is an autotelic activity: it is an activity that is intrinsically rewarding, and the act of doing it is its own reward. There are no errors, only fun to be found by playing music.
## About the tool
I do not want to pair it with a DAW, I do not want to make it fit with other commercial software. I'm not interested in VSTs or other proprietary workstations. Please, try to think of Cagire as an alternative to other tools.

84
docs/arithmetic.md Normal file
View File

@@ -0,0 +1,84 @@
# Arithmetic
Basic math operations. All arithmetic words pop their operands and push the result.
## Basic Operations
```
3 4 + ( 7 )
10 3 - ( 7 )
3 4 * ( 12 )
10 3 / ( 3.333... )
10 3 mod ( 1 )
```
Division always produces a float. Use `floor` if you need an integer result.
## Negative Numbers
```
5 neg ( -5 )
-3 abs ( 3 )
```
## Rounding
```
3.7 floor ( 3 )
3.2 ceil ( 4 )
3.5 round ( 4 )
```
## Min and Max
```
3 7 min ( 3 )
3 7 max ( 7 )
```
## Power and Root
```
2 3 pow ( 8 )
9 sqrt ( 3 )
```
## Examples
Calculate a frequency ratio:
```
440 2 12 / pow * ( 440 * 2^(1/12) ≈ 466.16 )
```
Clamp a value between 0 and 1:
```
1.5 0 max 1 min ( 1 )
-0.5 0 max 1 min ( 0 )
```
Scale a 0-1 range to 200-800:
```
0.5 600 * 200 + ( 500 )
```
## Words
| Word | Stack | Description |
|------|-------|-------------|
| `+` | (a b -- sum) | Add |
| `-` | (a b -- diff) | Subtract |
| `*` | (a b -- prod) | Multiply |
| `/` | (a b -- quot) | Divide |
| `mod` | (a b -- rem) | Modulo |
| `neg` | (a -- -a) | Negate |
| `abs` | (a -- \|a\|) | Absolute value |
| `floor` | (a -- n) | Round down |
| `ceil` | (a -- n) | Round up |
| `round` | (a -- n) | Round to nearest |
| `min` | (a b -- min) | Minimum |
| `max` | (a b -- max) | Maximum |
| `pow` | (a b -- a^b) | Power |
| `sqrt` | (a -- √a) | Square root |

51
docs/banks_patterns.md Normal file
View File

@@ -0,0 +1,51 @@
# Banks & Patterns
Cagire organizes all your patterns and data following a strict hierarchy:
- **Projects** contain **Banks**.
- **Banks** contain **Patterns**.
- **Patterns** contain **Steps**.
## Structure
```
Project
└── 32 Banks
└── 32 Patterns (per bank)
└── 128 Steps (per pattern)
```
A single project gives you 32 banks, each holding 32 patterns. You get 1024 patterns in each project, ~131.000 steps.
## Patterns
Each pattern is an independent sequence of steps with its own properties:
| Property | Description | Default |
|----------|-------------|---------|
| Length | Steps before the pattern loops (`1`-`128`) | `16` |
| Speed | Playback rate (`1/8x` to `8x`) | `1x` |
| Quantization | When the pattern launches | `Bar` |
| Sync Mode | Reset or Phase-Lock on re-trigger | `Reset` |
Press `e` in the patterns view to edit these settings.
## Patterns View
Access the patterns view with `Ctrl+Up` from the sequencer. The view shows all banks and patterns in a grid. Indicators show pattern state:
- `>` Currently playing
- `+` Staged to play
- `-` Staged to stop
### Keybindings
| Key | Action |
|-----|--------|
| `Arrows` | Navigate banks and patterns |
| `Enter` | Select and return to sequencer |
| `Space` | Toggle pattern playback |
| `e` | Edit pattern properties |
| `r` | Rename bank or pattern |
| `c` / `v` | Copy / Paste |
| `Delete` | Reset to empty pattern |

2
docs/chaining.md Normal file
View File

@@ -0,0 +1,2 @@
# Chaining

2
docs/chords.md Normal file
View File

@@ -0,0 +1,2 @@
# Chords

41
docs/comparison.md Normal file
View File

@@ -0,0 +1,41 @@
# Comparison
Compare values and produce boolean results (0 or 1).
## Equality
```forth
3 3 = ( 1 - equal )
3 4 = ( 0 - not equal )
3 4 != ( 1 - not equal )
3 4 <> ( 1 - not equal, alternative )
```
## Ordering
```forth
2 3 lt ( 1 - less than )
3 2 gt ( 1 - greater than )
3 3 <= ( 1 - less or equal )
3 3 >= ( 1 - greater or equal )
```
## With Conditionals
```forth
step 4 lt { "kick" s . } ? ( kick on first 4 steps )
beat 8 >= { 0.5 gain } ? ( quieter after beat 8 )
```
## Words
| Word | Stack | Description |
|------|-------|-------------|
| `=` | (a b -- bool) | Equal |
| `!=` | (a b -- bool) | Not equal |
| `<>` | (a b -- bool) | Not equal (alias) |
| `lt` | (a b -- bool) | Less than |
| `gt` | (a b -- bool) | Greater than |
| `<=` | (a b -- bool) | Less or equal |
| `>=` | (a b -- bool) | Greater or equal |

2
docs/conditionals.md Normal file
View File

@@ -0,0 +1,2 @@
# Conditionals

2
docs/context.md Normal file
View File

@@ -0,0 +1,2 @@
# Context

2
docs/cycles.md Normal file
View File

@@ -0,0 +1,2 @@
# Cycles

2
docs/definitions.md Normal file
View File

@@ -0,0 +1,2 @@
# Custom Words

51
docs/delay_reverb.md Normal file
View File

@@ -0,0 +1,51 @@
# Delay & Reverb
Add space and depth to your sounds with time-based effects.
## Delay
```forth
"snare" s 0.3 delay . ( delay mix )
"snare" s 0.25 delaytime . ( delay time in seconds )
"snare" s 0.5 delayfeedback . ( feedback amount )
"snare" s 1 delaytype . ( delay type )
```
## Reverb
```forth
"pad" s 0.3 verb . ( reverb mix )
"pad" s 2 verbdecay . ( decay time )
"pad" s 0.5 verbdamp . ( high frequency damping )
"pad" s 0.02 verbpredelay . ( predelay time )
"pad" s 0.7 verbdiff . ( diffusion )
"pad" s 1 size . ( room size )
```
## Combined Example
```forth
"keys" s
c4 note
0.2 delay
0.375 delaytime
0.4 delayfeedback
0.25 verb
1.5 verbdecay
.
```
## Words
| Word | Stack | Description |
|------|-------|-------------|
| `delay` | (f --) | Set delay mix |
| `delaytime` | (f --) | Set delay time |
| `delayfeedback` | (f --) | Set delay feedback |
| `delaytype` | (n --) | Set delay type |
| `verb` | (f --) | Set reverb mix |
| `verbdecay` | (f --) | Set reverb decay |
| `verbdamp` | (f --) | Set reverb damping |
| `verbpredelay` | (f --) | Set reverb predelay |
| `verbdiff` | (f --) | Set reverb diffusion |
| `size` | (f --) | Set reverb size |

43
docs/dictionary.md Normal file
View File

@@ -0,0 +1,43 @@
# The Dictionary
Cagire includes a built-in dictionary of all Forth words. Press `Ctrl+Down` repeatedly to reach the **Dict** view, or navigate directly with `Ctrl+Arrow` keys.
## Using the Dictionary
The dictionary shows every available word organized by category:
- **Stack**: Manipulation words like `dup`, `swap`, `drop`
- **Arithmetic**: Math operations
- **Sound**: Sound sources and emission
- **Filter**, **Envelope**, **Effects**: Sound shaping
- **Context**: Sequencer state like `step`, `beat`, `tempo`
- And many more...
## Navigation
| Key | Action |
|-----|--------|
| `Tab` | Switch between categories and words |
| `↑/↓` or `j/k` | Navigate items |
| `PgUp/PgDn` | Page through lists |
| `/` or `Ctrl+F` | Search |
| `Esc` | Clear search |
## Word Information
Each word entry shows:
- **Name** and aliases
- **Stack effect**: `( before -- after )`
- **Description**: What the word does
- **Example**: How to use it
## Search
Press `/` to search across all words. The search matches word names, aliases, and descriptions. Press `Esc` to clear and return to browsing.
## Tips
- Use the dictionary while writing scripts to check stack effects
- Categories group related words together
- Some words have shorter aliases (e.g., `sound``s`)

2
docs/effects.md Normal file
View File

@@ -0,0 +1,2 @@
# Effects

48
docs/emitting.md Normal file
View File

@@ -0,0 +1,48 @@
# Emitting Sounds
The core of Cagire is emitting sounds. Every step script builds up sound commands and emits them to the audio engine.
## The Sound Register
Before emitting, you must select a sound source using `sound` (or its alias `s`):
```forth
"kick" sound
"kick" s ( same thing, shorter )
```
This sets the current sound register. All subsequent parameter words modify this sound until you emit or clear it.
## Emitting
The `.` word emits the current sound:
```forth
"kick" s . ( emit one kick )
"kick" s . . . ( emit three kicks )
```
Use `.!` to emit multiple times:
```forth
"kick" s 4 .! ( emit four kicks )
```
## Clearing
The `clear` word resets the sound register and all parameters:
```forth
"kick" s 0.5 gain . clear "hat" s .
```
This is useful when you want to emit different sounds with independent parameters in the same step.
## Words
| Word | Stack | Description |
|------|-------|-------------|
| `sound` / `s` | (name --) | Set sound source |
| `.` | (--) | Emit current sound |
| `.!` | (n --) | Emit current sound n times |
| `clear` | (--) | Clear sound register and params |

2
docs/envelopes.md Normal file
View File

@@ -0,0 +1,2 @@
# Envelopes

65
docs/eq_stereo.md Normal file
View File

@@ -0,0 +1,65 @@
# EQ & Stereo
Shape the frequency balance and stereo image of your sounds.
## Three-Band EQ
```forth
"mix" s
3 eqlo ( boost low shelf by 3dB )
-2 eqmid ( cut mids by 2dB )
1 eqhi ( boost high shelf by 1dB )
.
```
## Tilt EQ
A simple one-knob EQ that tilts the frequency balance:
```forth
"bright" s 0.5 tilt . ( brighter )
"dark" s -0.5 tilt . ( darker )
```
Range: -1 (dark) to 1 (bright)
## Gain and Pan
```forth
"kick" s 0.8 gain . ( volume 0-1 )
"hat" s 0.5 pan . ( pan right )
"hat" s -0.5 pan . ( pan left )
"snare" s 1.2 postgain . ( post-processing gain )
"lead" s 100 velocity . ( velocity/dynamics )
```
## Stereo Width
```forth
"pad" s 0 width . ( mono )
"pad" s 1 width . ( normal stereo )
"pad" s 2 width . ( extra wide )
```
## Haas Effect
Create spatial positioning with subtle delay between channels:
```forth
"vocal" s 8 haas . ( 8ms delay for spatial effect )
```
## Words
| Word | Stack | Description |
|------|-------|-------------|
| `eqlo` | (f --) | Set low shelf gain (dB) |
| `eqmid` | (f --) | Set mid peak gain (dB) |
| `eqhi` | (f --) | Set high shelf gain (dB) |
| `tilt` | (f --) | Set tilt EQ (-1 dark, 1 bright) |
| `gain` | (f --) | Set volume (0-1) |
| `postgain` | (f --) | Set post gain |
| `velocity` | (f --) | Set velocity |
| `pan` | (f --) | Set pan (-1 to 1) |
| `width` | (f --) | Set stereo width |
| `haas` | (f --) | Set Haas delay (ms) |

2
docs/filters.md Normal file
View File

@@ -0,0 +1,2 @@
# Filters

65
docs/generators.md Normal file
View File

@@ -0,0 +1,65 @@
# Generators
Create sequences of values on the stack.
## Arithmetic Range
The `..` operator pushes an arithmetic sequence:
```forth
1 4 .. ( pushes 1 2 3 4 )
60 67 .. ( pushes 60 61 62 63 64 65 66 67 )
```
## Geometric Range
The `geom..` operator creates a geometric sequence:
```forth
1 2 4 geom.. ( pushes 1 2 4 8 )
100 0.5 4 geom.. ( pushes 100 50 25 12.5 )
```
Stack: `(start ratio count -- values...)`
## Generate
The `gen` word executes a quotation multiple times:
```forth
{ 1 6 rand } 4 gen ( 4 random values from 1-6 )
{ coin } 8 gen ( 8 random 0/1 values )
```
## Examples
Random melody:
```forth
"pluck" s
{ 48 72 rand } 4 gen 4 choose note
.
```
Chord from range:
```forth
"pad" s
60 67 .. 8 choose note ( random note in octave )
.
```
Euclidean-style rhythm values:
```forth
0 1 0 0 1 0 1 0 ( manual )
{ coin } 8 gen ( random 8-step pattern )
```
## Words
| Word | Stack | Description |
|------|-------|-------------|
| `..` | (start end -- values...) | Arithmetic sequence |
| `geom..` | (start ratio count -- values...) | Geometric sequence |
| `gen` | (quot n -- results...) | Execute quotation n times |

54
docs/grid.md Normal file
View File

@@ -0,0 +1,54 @@
# The Sequencer Grid
The sequencer grid is the heart of Cagire. Each cell represents a step that can contain a Forth script to generate sound.
## Structure
- **Steps**: Up to 128 per pattern, displayed 32 at a time
- **Patterns**: Up to 32 per bank
- **Banks**: Up to 32 per project
## Navigation
Use arrow keys to move between steps. The grid wraps around at pattern boundaries.
## Selection
Hold `Shift` while pressing arrow keys to select multiple steps. Press `Esc` to clear the selection.
## Editing Steps
- `Enter` - Open the script editor
- `t` - Toggle step active/inactive
- `r` - Rename a step
- `Del` - Delete selected steps
## Copy & Paste
- `Ctrl+C` - Copy selected steps
- `Ctrl+V` - Paste as copies
- `Ctrl+B` - Paste as linked steps
- `Ctrl+D` - Duplicate selection
- `Ctrl+H` - Harden links (convert to copies)
Linked steps share the same script as their source. When you edit the source, all linked steps update automatically.
## Pattern Controls
- `<` / `>` - Decrease/increase pattern length
- `[` / `]` - Decrease/increase pattern speed
- `L` - Set length directly
- `S` - Set speed directly
## Playback
- `Space` - Toggle play/stop
- `+` / `-` - Adjust tempo
- `T` - Set tempo directly
- `Ctrl+R` - Run current step once (preview)
## Visual Indicators
- **Highlighted cell** - Currently playing step
- **Colored backgrounds** - Linked steps share colors by source
- **Arrow prefix** (→05) - Step is linked to step 05

58
docs/how_it_works.md Normal file
View File

@@ -0,0 +1,58 @@
# How Does It Work?
Cagire is a step sequencer where each step contains a **Forth script** instead of the typical note data. When the sequencer reaches a step, it runs the script. A script _can do whatever it is programed to do_, such as producing sound commands sent to an internal audio engine. Everything else is similar to a step sequencer: you can `toggle` / `untoggle`, `copy` / `paste` any step or group of steps, etc. You are completely free to define what your scripts will do. It can be as simple as playing a note, or as complex as triggering random audio samples with complex effects. Scripts can also share code and data with each other.
## Project / session organization
Cagire can run multiple patterns concurrently. Each pattern contains a given number of steps. Every session / project is organized hierarchically:
- **32 Banks**
- **32 Patterns** per bank
- **128 Steps** per pattern
That's over 130,000 possible steps per project. Most of my sessions use 15-20 at best.
## What does a script look like?
Forth is a stack-based programming language. It is very minimalistic and emphasizes simplicity and readability. Using Forth doesn't feel like programming at all. It feels more like juggling with words and numbers or writing bad computer poetry. There is pretty much no syntax to learn, just a few rules to follow. Forth is ancient, powerful, flexible, and... super fun to live code with! Here is a minimal program that will play a middle C note using a sine wave:
```forth
c4 note sine sound .
```
Read the program backwards and you will understand what it does instantly:
- `.`: we want to play a sound.
- `sine sound`: the sound is a sinewave.
- `c4 note`: the pitch is C4 (middle-C).
Scripts can be simple one-liners or complex programs with conditionals, loops, and randomness. They tend to look like an accumulation of words and numbers. Use space and line returns to your advantage. The Forth language can be learned... on the spot. You just need to understand the following basic rules:
- there are `words` and `numbers`.
- they are delimited by spaces.
- everything piles up on the `stack`.
Obviously you will need to understand what the **stack** is, but it will take you five minutes. That's it. See the **Forth** section for details.
## The Audio Engine
Cagire includes a complete synthesis engine. No external software is required to play music. It comes with a large number of sound sources and sound shaping tools: oscillators, sample players, effects, filters, and more. The audio engine is quite capable and versatile, and can accomodate a vast array of genres / styles. Here are a few examples :
```forth
;; sawtooth wave with lowpass filter, chorus and reverb
saw sound 1200 lpf 0.2 chorus 0.8 verb .
```
```forth
;; pure sine wave with vibrato and bit crushing
0.5 vibmod 4 vib sine sound 8 crush 0.8 gain .
```
```forth
;; very loud and pitched-down kick drum using an audio sample
kkick sound 1.5 distort 0.9 postgain 0.8 speed .
```
## Timing & Synchronization
Cagire uses **Ableton Link** to manage timing and synchronization. This means that all devices using the same protocol can be synchronized to the same tempo. Most commercial softwares support this protocol. The playback speed is defined as a BPM (beats per minute) value. Patterns can run at different speeds relative to the master tempo. Most of the durations in Cagire are defined in terms of beats.

View File

@@ -32,6 +32,10 @@
- **Ctrl+C**: Copy step script
- **Ctrl+V**: Paste step script
### Execution
- **Ctrl+R**: Run current step's script immediately (one-shot)
### Tempo
- **+ / =**: Increase tempo
@@ -41,6 +45,7 @@
- **Tab / Esc**: Return to sequencer focus
- **Ctrl+E**: Compile current step script
- **Ctrl+R**: Run script in editor immediately (one-shot)
## Audio Page

47
docs/ladder_filters.md Normal file
View File

@@ -0,0 +1,47 @@
# Ladder Filters
Ladder filters provide a classic analog-style filter sound with stronger resonance character than the standard SVF filters.
## Ladder Lowpass
```forth
"saw" s 2000 llpf . ( ladder lowpass frequency )
"saw" s 0.7 llpq . ( ladder lowpass resonance )
```
## Ladder Highpass
```forth
"noise" s 500 lhpf . ( ladder highpass frequency )
"noise" s 0.5 lhpq . ( ladder highpass resonance )
```
## Ladder Bandpass
```forth
"pad" s 1000 lbpf . ( ladder bandpass frequency )
"pad" s 0.6 lbpq . ( ladder bandpass resonance )
```
## Comparison with SVF
Ladder filters have a different resonance character:
- More aggressive self-oscillation at high resonance
- Classic "squelchy" acid sound
- 24dB/octave slope
Standard SVF filters (`lpf`, `hpf`, `bpf`) have:
- Cleaner resonance
- Full envelope control (attack, decay, sustain, release)
- 12dB/octave slope
## Words
| Word | Stack | Description |
|------|-------|-------------|
| `llpf` | (f --) | Set ladder lowpass frequency |
| `llpq` | (f --) | Set ladder lowpass resonance |
| `lhpf` | (f --) | Set ladder highpass frequency |
| `lhpq` | (f --) | Set ladder highpass resonance |
| `lbpf` | (f --) | Set ladder bandpass frequency |
| `lbpq` | (f --) | Set ladder bandpass resonance |

74
docs/lfo.md Normal file
View File

@@ -0,0 +1,74 @@
# LFO & Ramps
Generate time-varying values synchronized to the beat for modulation.
## Ramp
The `ramp` word creates a sawtooth wave from 0 to 1:
```forth
0.25 1.0 ramp ( one cycle per 4 beats, linear )
1.0 2.0 ramp ( one cycle per beat, exponential curve )
```
Stack: `(freq curve -- val)`
## Shortcut Ramps
```forth
0.5 linramp ( linear ramp, curve=1 )
0.25 expramp ( exponential ramp, curve=3 )
2.0 logramp ( logarithmic ramp, curve=0.3 )
```
## Triangle Wave
```forth
0.5 tri ( triangle wave 0→1→0 )
```
## Perlin Noise
Smooth random modulation:
```forth
0.25 perlin ( smooth noise at 0.25x beat rate )
```
## Range Scaling
Scale a 0-1 value to any range:
```forth
0.5 200 800 range ( 0.5 becomes 500 )
```
## Examples
Modulate filter cutoff:
```forth
"saw" s
0.25 1.0 ramp 500 2000 range lpf
.
```
Random panning:
```forth
"hat" s
0.5 perlin -1 1 range pan
.
```
## Words
| Word | Stack | Description |
|------|-------|-------------|
| `ramp` | (freq curve -- val) | Ramp 0-1: fract(freq*beat)^curve |
| `linramp` | (freq -- val) | Linear ramp (curve=1) |
| `expramp` | (freq -- val) | Exponential ramp (curve=3) |
| `logramp` | (freq -- val) | Logarithmic ramp (curve=0.3) |
| `tri` | (freq -- val) | Triangle wave 0→1→0 |
| `perlin` | (freq -- val) | Perlin noise 0-1 |
| `range` | (val min max -- scaled) | Scale 0-1 to min-max |

2
docs/link.md Normal file
View File

@@ -0,0 +1,2 @@
# Ableton Link

58
docs/lofi.md Normal file
View File

@@ -0,0 +1,58 @@
# Lo-fi Effects
Add grit, warmth, and character with bit crushing, folding, and distortion.
## Bit Crush
Reduce bit depth for digital artifacts:
```forth
"drum" s 8 crush . ( 8-bit crush )
"drum" s 4 crush . ( heavy 4-bit crush )
```
## Wave Folding
Fold the waveform back on itself for complex harmonics:
```forth
"sine" s 2 fold . ( moderate folding )
"sine" s 4 fold . ( aggressive folding )
```
## Wave Wrap
Wrap the waveform for a different flavor of distortion:
```forth
"bass" s 0.5 wrap .
```
## Distortion
Classic overdrive/distortion:
```forth
"guitar" s 0.5 distort . ( distortion amount )
"guitar" s 0.8 distortvol . ( output volume )
```
## Combining Effects
```forth
"drum" s
12 crush
1.5 fold
0.3 distort
.
```
## Words
| Word | Stack | Description |
|------|-------|-------------|
| `crush` | (f --) | Set bit crush depth |
| `fold` | (f --) | Set wave fold amount |
| `wrap` | (f --) | Set wave wrap amount |
| `distort` | (f --) | Set distortion amount |
| `distortvol` | (f --) | Set distortion output volume |

89
docs/logic.md Normal file
View File

@@ -0,0 +1,89 @@
# Logic
Boolean operations and conditional execution.
## Boolean Operators
```forth
1 1 and ( 1 )
0 1 or ( 1 )
1 not ( 0 )
1 0 xor ( 1 )
1 1 nand ( 0 )
0 0 nor ( 1 )
```
## Conditional Execution
Execute a quotation if condition is true:
```forth
{ "kick" s . } coin ? ( 50% chance )
{ 0.5 gain } step 0 = ? ( only on step 0 )
```
Execute if false:
```forth
{ "snare" s . } coin !? ( if NOT coin )
```
## If-Else
```forth
{ "kick" s } { "snare" s } coin ifelse .
```
Stack: `(true-quot false-quot bool --)`
## Pick
Choose from multiple quotations:
```forth
{ 60 } { 64 } { 67 } step 3 mod pick note
```
Stack: `(quot1 quot2 ... quotN n -- result)`
## Apply
Execute a quotation unconditionally:
```forth
{ 2 * } apply ( doubles top of stack )
```
## Examples
Conditional parameter:
```forth
"kick" s
{ 0.8 } { 0.4 } iter 2 mod = ifelse gain
.
```
Multi-way branching:
```forth
"synth" s
{ c4 } { e4 } { g4 } { c5 } step 4 mod pick note
.
```
## Words
| Word | Stack | Description |
|------|-------|-------------|
| `and` | (a b -- bool) | Logical and |
| `or` | (a b -- bool) | Logical or |
| `not` | (a -- bool) | Logical not |
| `xor` | (a b -- bool) | Exclusive or |
| `nand` | (a b -- bool) | Not and |
| `nor` | (a b -- bool) | Not or |
| `?` | (quot bool --) | Execute if true |
| `!?` | (quot bool --) | Execute if false |
| `ifelse` | (t-quot f-quot bool --) | If-else |
| `pick` | (..quots n --) | Execute nth quotation |
| `apply` | (quot --) | Execute unconditionally |

61
docs/mod_fx.md Normal file
View File

@@ -0,0 +1,61 @@
# Modulation Effects
Phaser, flanger, and chorus effects for movement and width.
## Phaser
```forth
"pad" s
1 phaser ( rate in Hz )
0.5 phaserdepth ( depth )
0.5 phasersweep ( sweep range )
1000 phasercenter ( center frequency )
.
```
## Flanger
```forth
"guitar" s
0.5 flanger ( rate )
0.5 flangerdepth ( depth )
0.5 flangerfeedback ( feedback for metallic sound )
.
```
## Chorus
```forth
"keys" s
1 chorus ( rate )
0.5 chorusdepth ( depth )
0.02 chorusdelay ( base delay time )
.
```
## Combining Effects
```forth
"pad" s
c4 note
0.3 phaserdepth
0.5 phaser
0.2 chorus
0.3 chorusdepth
.
```
## Words
| Word | Stack | Description |
|------|-------|-------------|
| `phaser` | (f --) | Set phaser rate |
| `phaserdepth` | (f --) | Set phaser depth |
| `phasersweep` | (f --) | Set phaser sweep |
| `phasercenter` | (f --) | Set phaser center frequency |
| `flanger` | (f --) | Set flanger rate |
| `flangerdepth` | (f --) | Set flanger depth |
| `flangerfeedback` | (f --) | Set flanger feedback |
| `chorus` | (f --) | Set chorus rate |
| `chorusdepth` | (f --) | Set chorus depth |
| `chorusdelay` | (f --) | Set chorus delay |

2
docs/modulation.md Normal file
View File

@@ -0,0 +1,2 @@
# Modulation

39
docs/navigation.md Normal file
View File

@@ -0,0 +1,39 @@
# Navigation
The Cagire application is organized as a grid composed of six views:
```
Dict Patterns Options
Help Sequencer Engine
```
- `Dict` (Dictionary): A comprehensive list of all the `Forth` words used in the application.
- `Help`: Provides detailed information about the application's features and functionalities.
- `Patterns`: Pattern banks and pattern manager. Used to organize a session / project.
- `Sequencer`: The main view, where you edit sequences and play music.
- `Options`: Configuration settings for the application.
- `Engine`: Configuration settings for the internal audio engine.
## Switching Views
Use `Ctrl+Arrow` keys to move between views. A minimap appears briefly showing your position in the grid.
- `Ctrl+Left` / `Ctrl+Right` - move horizontally
- `Ctrl+Up` / `Ctrl+Down` - move vertically
The grid wraps horizontally, so you can cycle through views on the same row.
## Getting Help
Press `?` on any view to see its keybindings. This shows all available shortcuts for the current context.
Press `Esc` to close the keybindings panel.
## Common Keys
These work on most views:
- `Arrow keys` - move or scroll
- `Tab` - switch focus between panels
- `/` or `Ctrl+f` - search (where available)
- `q` - quit (with confirmation)

2
docs/notes.md Normal file
View File

@@ -0,0 +1,2 @@
# Notes & MIDI

75
docs/oscillators.md Normal file
View File

@@ -0,0 +1,75 @@
# Oscillators
Control synthesis oscillator parameters for synth sounds.
## Frequency and Pitch
```forth
"sine" s 440 freq . ( set frequency in Hz )
"saw" s 60 note . ( set MIDI note )
"square" s 0.01 detune . ( slight detune )
"lead" s 12 coarse . ( coarse tune in semitones )
"bass" s 0.1 glide . ( portamento )
```
## Pulse Width
For pulse/square oscillators:
```forth
"pulse" s 0.3 pw . ( narrow pulse )
"pulse" s 0.5 pw . ( square wave )
```
## Stereo Spread
```forth
"pad" s 0.5 spread . ( stereo spread amount )
```
## Harmonics and Timbre
For oscillators that support it (like mutable):
```forth
"mutable" s 4 harmonics . ( harmonic content )
"mutable" s 0.5 timbre . ( timbre control )
"mutable" s 0.3 morph . ( morph parameter )
```
## Multiplier and Warp
```forth
"fm" s 2 mult . ( frequency multiplier )
"wt" s 0.5 warp . ( warp amount )
"wt" s 1 mirror . ( mirror waveform )
```
## Sub Oscillator
```forth
"bass" s 0.5 sub . ( sub oscillator level )
"bass" s 2 suboct . ( sub octave down )
"bass" s 1 subwave . ( sub waveform )
```
## Words
| Word | Stack | Description |
|------|-------|-------------|
| `freq` | (f --) | Set frequency (Hz) |
| `note` | (n --) | Set MIDI note |
| `detune` | (f --) | Set detune amount |
| `coarse` | (f --) | Set coarse tune |
| `glide` | (f --) | Set glide/portamento |
| `pw` | (f --) | Set pulse width |
| `spread` | (f --) | Set stereo spread |
| `mult` | (f --) | Set multiplier |
| `warp` | (f --) | Set warp amount |
| `mirror` | (f --) | Set mirror |
| `harmonics` | (f --) | Set harmonics |
| `timbre` | (f --) | Set timbre |
| `morph` | (f --) | Set morph |
| `sub` | (f --) | Set sub oscillator level |
| `suboct` | (n --) | Set sub octave |
| `subwave` | (n --) | Set sub waveform |

2
docs/parameters.md Normal file
View File

@@ -0,0 +1,2 @@
# Parameters

2
docs/patterns.md Normal file
View File

@@ -0,0 +1,2 @@
# Pattern Management

44
docs/pitch_envelope.md Normal file
View File

@@ -0,0 +1,44 @@
# Pitch Envelope
Control pitch modulation over time with a dedicated ADSR envelope.
## Envelope Amount
```forth
"bass" s 0.5 penv . ( pitch envelope depth )
"bass" s -0.5 penv . ( negative envelope )
```
## Envelope Shape
```forth
"kick" s
1.0 penv ( full envelope depth )
0.001 patt ( instant attack )
0.1 pdec ( fast decay )
0 psus ( no sustain )
0.1 prel ( short release )
.
```
## Classic Kick Drum
```forth
"sine" s
40 note
2.0 penv
0.001 patt
0.05 pdec
0 psus
.
```
## Words
| Word | Stack | Description |
|------|-------|-------------|
| `penv` | (f --) | Set pitch envelope amount |
| `patt` | (f --) | Set pitch attack time |
| `pdec` | (f --) | Set pitch decay time |
| `psus` | (f --) | Set pitch sustain level |
| `prel` | (f --) | Set pitch release time |

2
docs/probability.md Normal file
View File

@@ -0,0 +1,2 @@
# Probability

2
docs/randomness.md Normal file
View File

@@ -0,0 +1,2 @@
# Randomness

76
docs/samples.md Normal file
View File

@@ -0,0 +1,76 @@
# Samples
Control sample playback with timing, looping, and slice parameters.
## Basic Playback
```forth
"break" s . ( play sample from start )
"break" s 0.5 speed . ( half speed )
"break" s -1 speed . ( reverse )
```
## Time and Position
Control where in the sample to start and end:
```forth
"break" s 0.25 begin . ( start at 25% )
"break" s 0.5 end . ( end at 50% )
"break" s 0.1 time . ( offset start time )
```
## Duration and Gate
```forth
"pad" s 0.5 dur . ( play for 0.5 seconds )
"pad" s 0.8 gate . ( gate time as fraction )
```
## Looping
Fit a sample to a number of beats:
```forth
"break" s 4 loop . ( fit to 4 beats )
```
## Repetition
```forth
"hat" s 4 repeat . ( trigger 4 times )
```
## Voice and Routing
```forth
"kick" s 1 voice . ( assign to voice 1 )
"snare" s 0 orbit . ( route to bus 0 )
```
## Sample Selection
```forth
"kick" s 2 n . ( select sample #2 from folder )
"kit" s "a" bank . ( use bank suffix )
1 cut ( cut group - stops other sounds in same group )
```
## Words
| Word | Stack | Description |
|------|-------|-------------|
| `time` | (f --) | Set time offset |
| `repeat` | (n --) | Set repeat count |
| `dur` | (f --) | Set duration |
| `gate` | (f --) | Set gate time |
| `speed` | (f --) | Set playback speed |
| `begin` | (f --) | Set sample start (0-1) |
| `end` | (f --) | Set sample end (0-1) |
| `loop` | (n --) | Fit sample to n beats |
| `voice` | (n --) | Set voice number |
| `orbit` | (n --) | Set orbit/bus |
| `n` | (n --) | Set sample number |
| `bank` | (str --) | Set sample bank suffix |
| `cut` | (n --) | Set cut group |
| `reset` | (n --) | Reset parameter |

2
docs/scales.md Normal file
View File

@@ -0,0 +1,2 @@
# Scales

62
docs/selection.md Normal file
View File

@@ -0,0 +1,62 @@
# Selection
Cycle through values over time for evolving patterns.
## Step Cycle
`cycle` cycles through values based on step runs:
```forth
60 64 67 3 cycle note ( cycle through C, E, G )
```
Each time the step runs, it picks the next value.
## Pattern Cycle
`pcycle` cycles based on pattern iteration:
```forth
60 64 67 3 pcycle note ( change note each pattern loop )
```
## Emit-Time Cycle
`tcycle` creates a cycle list resolved at emit time, useful with `.!`:
```forth
60 64 67 3 tcycle note 3 .! ( emit C, E, G in sequence )
```
## Examples
Rotating bass notes:
```forth
"bass" s
c3 e3 g3 b3 4 cycle note
.
```
Evolving pattern over loops:
```forth
"lead" s
0.5 1.0 0.75 0.25 4 pcycle gain
.
```
Arpeggiated chord:
```forth
"pluck" s
c4 e4 g4 c5 4 tcycle note 4 .!
```
## Words
| Word | Stack | Description |
|------|-------|-------------|
| `cycle` | (v1..vn n -- val) | Cycle by step runs |
| `pcycle` | (v1..vn n -- val) | Cycle by pattern iteration |
| `tcycle` | (v1..vn n -- list) | Create cycle list for emit-time |

2
docs/sound_basics.md Normal file
View File

@@ -0,0 +1,2 @@
# Sound Basics

118
docs/stack.md Normal file
View File

@@ -0,0 +1,118 @@
# The Stack
Forth is a stack-based language. Instead of variables and expressions, you push values onto a stack and use words that consume and produce values.
## How It Works
The stack is a last-in, first-out (LIFO) structure. Values you type get pushed on top. Words pop values off and push results back.
```
3 4 +
```
Step by step:
1. `3` → push 3 onto stack: `[3]`
2. `4` → push 4 onto stack: `[3, 4]`
3. `+` → pop two values, add them, push result: `[7]`
## Values
Three types can live on the stack:
- **Integers**: `42`, `-7`, `0`
- **Floats**: `3.14`, `0.5`, `-1.0`
- **Strings**: `"kick"`, `"hello"`
## Stack Notation
Documentation uses stack effect notation:
```
( before -- after )
```
For example, `+` has effect `( a b -- sum )` meaning it takes two values and leaves one.
## Core Words
### dup
Duplicate the top value.
```
3 dup ( 3 3 )
```
### drop
Discard the top value.
```
3 4 drop ( 3 )
```
### swap
Swap the top two values.
```
3 4 swap ( 4 3 )
```
### over
Copy the second value to the top.
```
3 4 over ( 3 4 3 )
```
### rot
Rotate the top three values.
```
1 2 3 rot ( 2 3 1 )
```
### nip
Drop the second value.
```
3 4 nip ( 4 )
```
### tuck
Copy top value below second.
```
3 4 tuck ( 4 3 4 )
```
## Examples
Build a chord by duplicating and adding:
```
60 dup 4 + swap 7 + ( 64 67 60 )
```
Use `over` to keep a base value:
```
c4 over M3 swap P5 ( e4 g4 c4 )
```
## Words
| Word | Stack | Description |
|------|-------|-------------|
| `dup` | (a -- a a) | Duplicate top |
| `drop` | (a --) | Discard top |
| `swap` | (a b -- b a) | Swap top two |
| `over` | (a b -- a b a) | Copy second to top |
| `rot` | (a b c -- b c a) | Rotate three |
| `nip` | (a b -- b) | Drop second |
| `tuck` | (a b -- b a b) | Copy top below second |

50
docs/staging.md Normal file
View File

@@ -0,0 +1,50 @@
# Stage / Commit
Cagire requires you to `stage` changes you wish to make to the playback state and then `commit` it. It is way more simple than it seems. For instance, you mark pattern `04` and `05` to start playing, and _then_ you send the order to start the playback (`commit`). The same goes for stopping patterns. You mark which pattern to stop (`stage`) and then you give the order to stop them (`commit`). Why is staging useful? Here are some reasons why this design choice was made:
- **To apply multiple changes**: Queue several patterns to start/stop, commit them together.
- **To get clean timing**: All changes happen on beat/bar boundaries.
- **To help with live performance**: Prepare the next section without affecting current playback.
Staging is an essential feature to understand to be effective when doing live performances:
1. Open the **Patterns** view (`Ctrl+Up` from sequencer)
2. Navigate to a pattern you wish to change/play
3. Press `Space` to stage it. The pending change is going to be displayed:
- `+` (staged to play)
- `-` (staged to stop)
4. Repeat for other patterns you want to change
5. Press `c` to commit all changes
6. Or press `Esc` to cancel
A pattern might not start immediately depending on the sync mode you have chosen. It might wait for the next beat/bar boundary.
## Status Indicators
| Indicator | Meaning |
|-----------|---------|
| `>` | Currently playing |
| `+` | Staged to play |
| `-` | Staged to stop |
A pattern can show both `>` (playing) and `-` (staged to stop).
## Quantization
Committed changes don't execute immediately. They wait for a quantization boundary:
| Setting | Behavior |
|---------|----------|
| Immediate | Next sequencer tick |
| Beat | Next beat |
| 1 Bar | Next bar (default) |
| 2/4/8 Bars | Next 2, 4, or 8-bar boundary |
Edit quantization in pattern properties (press `e` on a pattern).
## Sync Mode
When a pattern starts, its playback position depends on sync mode:
- **Reset**: Always start at step 0
- **Phase-Lock**: Start at the current beat-aligned position (stays in sync with other patterns)

2
docs/tempo.md Normal file
View File

@@ -0,0 +1,2 @@
# Tempo & Speed

2
docs/timing.md Normal file
View File

@@ -0,0 +1,2 @@
# Timing

2
docs/variables.md Normal file
View File

@@ -0,0 +1,2 @@
# Variables

44
docs/vibrato.md Normal file
View File

@@ -0,0 +1,44 @@
# Vibrato
Add pitch vibrato to oscillator sounds.
## Basic Vibrato
```forth
"lead" s
60 note
5 vib ( vibrato rate in Hz )
0.5 vibmod ( vibrato depth )
.
```
## Vibrato Shape
Control the LFO waveform:
```forth
"pad" s
c4 note
4 vib
0.3 vibmod
0 vibshape ( 0=sine, other values for different shapes )
.
```
## Subtle vs Expressive
```forth
( subtle vibrato for pads )
"pad" s 3 vib 0.1 vibmod .
( expressive vibrato for leads )
"lead" s 6 vib 0.8 vibmod .
```
## Words
| Word | Stack | Description |
|------|-------|-------------|
| `vib` | (f --) | Set vibrato rate (Hz) |
| `vibmod` | (f --) | Set vibrato depth |
| `vibshape` | (f --) | Set vibrato LFO shape |

55
docs/wavetables.md Normal file
View File

@@ -0,0 +1,55 @@
# Wavetables
Control wavetable synthesis parameters for scanning through waveforms.
## Scan Position
The `scan` parameter controls which waveform in the wavetable is active:
```forth
"wt" s 0.0 scan . ( first waveform )
"wt" s 0.5 scan . ( middle of table )
"wt" s 1.0 scan . ( last waveform )
```
## Wavetable Length
Set the cycle length in samples:
```forth
"wt" s 2048 wtlen . ( standard wavetable size )
```
## Scan Modulation
Animate the scan position with an LFO:
```forth
"wt" s 0.2 scanlfo . ( LFO rate in Hz )
"wt" s 0.4 scandepth . ( LFO depth 0-1 )
"wt" s "tri" scanshape . ( LFO shape )
```
Available shapes: `sine`, `tri`, `saw`, `square`, `sh` (sample & hold)
## Example
```forth
"wavetable" s
60 note
0.25 scan
0.1 scanlfo
0.3 scandepth
"sine" scanshape
.
```
## Words
| Word | Stack | Description |
|------|-------|-------------|
| `scan` | (f --) | Set wavetable scan position (0-1) |
| `wtlen` | (n --) | Set wavetable cycle length in samples |
| `scanlfo` | (f --) | Set scan LFO rate (Hz) |
| `scandepth` | (f --) | Set scan LFO depth (0-1) |
| `scanshape` | (s --) | Set scan LFO shape |

View File

@@ -1,19 +1,31 @@
# Welcome to Cagire
Cagire is a terminal-based step sequencer for live coding music. Each step in a pattern contains a **Forth** script that produces sound and create events. It is made by BuboBubo (Raphaël Maurice Forment): [https://raphaelforment.fr](https://raphaelforment.fr). Cagire is open-source (AGPL-3.0 licensed) and available on GitHub : [https://github.com/BuboBubo/cagire](https://github.com/BuboBubo/cagire). This help view will teach you everything you need to know to start using Cagire and and to live code with it. To use Cagire, you will need to understand two things:
Cagire is a terminal-based step sequencer for live coding music. Each step on the sequencer is defined by a **Forth** script that produces sound and create events. The documentation you are currently reading acts both as _tutorial_ and _reference_. It contains everything you need to know to use Cagire effectively. We recommend you to dive in and explore by picking subjects that interest you before slowly learning about everything else. Here are some recommended topics to start with:
1) How the sequencer works: dealing with steps, patterns and banks.
2) How to write a script: how to make sound using code.
1) How the sequencer works? Banks, patterns and steps.
* the sequencer model, the pattern model, the step sequencer.
2) How to write a script? How to make sound using code.
* how to write simple scripts that play `musical events`.
* how to extend these scripts with `logic` and/or `randomness`.
* how define `WORDS`, `variables`, and share data between steps.
3) What can I do with the audio engine?
* audio sources: samples, oscillators, wavetables, noise generators.
* audio effects: filters, delay, reverb, distortion, modulations.
4) How far can it go?
* how to live code with Cagire.
* how fast can I break things?
## Pages
## What is live coding?
Cagire is organized in several views. Navigate between them using **Ctrl+Left/Right/Up/Down**:
Live coding is a technique where a programmer writes code in real-time in front of an audience. It is a way to experiment with code, to share things and thoughts openly, to express yourself through code. It can be technical, poetical, weird, preferably all at once. Live coding can be used to create music, visual art, and other forms of media. Learn more about live coding on [https://toplap.org](https://toplap.org) or [https://livecoding.fr](https://livecoding.fr). Live coding is an autotelic activity: it is an activity that is intrinsically rewarding, and the act of doing it is its own reward. There are no errors, only fun.
- **Sequencer**: Main view. Edit or preview patterns and scripts. Write Forth scripts.
- **Patterns**: Project patterns management. 32 banks of 32 patterns per project. Edit pattern properties (name, length, etc).
- **Engine**: Internal audio engine management: device selection, sample loading, performance options and voice / state monitoring.
- **Options**: General application settings.
- **Dict**: Forth word dictionary, organized by category. Learn about the internal programming language and its features.
- **Help**: Documentation. This is the page view you are looking at right now.
## About
Have fun with Cagire! Remember that live coding is all about experimentation and exploration!
Cagire is built by BuboBubo (Raphaël Maurice Forment, [https://raphaelforment.fr](https://raphaelforment.fr)). It is a free and open-source project licensed under the `AGPL-3.0 License`. You are free to contribute to the project by making direct contributions to the codebase or by providing feedback and suggestions.
### Credits
* **Doux** (audio engine) is a Rust port of Dough, originally written in C by Felix Roos.
* **mi-plaits-dsp-rs** is a Rust port of the code used by the Mutable Instruments Plaits.
* _Author_: Oliver Rockstedt [info@sourcebox.de](info@sourcebox.de).
* _Original author_: Emilie Gillet [emilie.o.gillet@gmail.com](emilie.o.gillet@gmail.com).

View File

@@ -281,6 +281,45 @@ impl App {
self.project_state.mark_dirty(change.bank, change.pattern);
}
pub fn execute_script_oneshot(
&self,
script: &str,
link: &LinkState,
audio_tx: &arc_swap::ArcSwap<Sender<crate::engine::AudioCommand>>,
) -> Result<(), String> {
let (bank, pattern) = self.current_bank_pattern();
let step_idx = self.editor_ctx.step;
let speed = self
.project_state
.project
.pattern_at(bank, pattern)
.speed
.multiplier();
let ctx = StepContext {
step: step_idx,
beat: link.beat(),
bank,
pattern,
tempo: link.tempo(),
phase: link.phase(),
slot: 0,
runs: 0,
iter: 0,
speed,
fill: false,
nudge_secs: 0.0,
};
let cmds = self.script_engine.evaluate(script, &ctx)?;
for cmd in cmds {
let _ = audio_tx
.load()
.send(crate::engine::AudioCommand::Evaluate { cmd, time: None });
}
Ok(())
}
pub fn compile_current_step(&mut self, link: &LinkState) {
let step_idx = self.editor_ctx.step;
let (bank, pattern) = self.current_bank_pattern();
@@ -1117,12 +1156,20 @@ impl App {
AppCommand::PageDown => self.page.down(),
// Help navigation
AppCommand::HelpNextTopic => {
self.ui.help_topic = (self.ui.help_topic + 1) % help_view::topic_count();
AppCommand::HelpToggleFocus => {
use crate::state::HelpFocus;
self.ui.help_focus = match self.ui.help_focus {
HelpFocus::Topics => HelpFocus::Content,
HelpFocus::Content => HelpFocus::Topics,
};
}
AppCommand::HelpPrevTopic => {
AppCommand::HelpNextTopic(n) => {
let count = help_view::topic_count();
self.ui.help_topic = (self.ui.help_topic + count - 1) % count;
self.ui.help_topic = (self.ui.help_topic + n) % count;
}
AppCommand::HelpPrevTopic(n) => {
let count = help_view::topic_count();
self.ui.help_topic = (self.ui.help_topic + count - (n % count)) % count;
}
AppCommand::HelpScrollDown(n) => {
let s = self.ui.help_scroll_mut();

View File

@@ -138,8 +138,9 @@ pub enum AppCommand {
PageDown,
// Help navigation
HelpNextTopic,
HelpPrevTopic,
HelpToggleFocus,
HelpNextTopic(usize),
HelpPrevTopic(usize),
HelpScrollDown(usize),
HelpScrollUp(usize),
HelpActivateSearch,

View File

@@ -508,6 +508,13 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
KeyCode::Char('s') if ctrl => {
ctx.app.editor_ctx.show_stack = !ctx.app.editor_ctx.show_stack;
}
KeyCode::Char('r') if ctrl => {
let script = ctx.app.editor_ctx.editor.lines().join("\n");
match ctx.app.execute_script_oneshot(&script, ctx.link, ctx.audio_tx) {
Ok(()) => ctx.app.ui.flash("Executed", 100, crate::state::FlashKind::Info),
Err(e) => ctx.app.ui.flash(&format!("Error: {e}"), 200, crate::state::FlashKind::Error),
}
}
KeyCode::Char('a') if ctrl => {
editor.select_all();
}
@@ -899,6 +906,17 @@ fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputR
}));
}
}
KeyCode::Char('r') if ctrl => {
let pattern = ctx.app.current_edit_pattern();
if let Some(script) = pattern.resolve_script(ctx.app.editor_ctx.step) {
if !script.trim().is_empty() {
match ctx.app.execute_script_oneshot(script, ctx.link, ctx.audio_tx) {
Ok(()) => ctx.app.ui.flash("Executed", 100, crate::state::FlashKind::Info),
Err(e) => ctx.app.ui.flash(&format!("Error: {e}"), 200, crate::state::FlashKind::Error),
}
}
}
}
KeyCode::Char('r') => {
let (bank, pattern, step) = (
ctx.app.editor_ctx.bank,
@@ -1269,26 +1287,43 @@ fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
}
fn handle_help_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
use crate::state::HelpFocus;
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
if ctx.app.ui.help_search_active {
match key.code {
KeyCode::Esc => ctx.dispatch(AppCommand::HelpClearSearch),
KeyCode::Enter => ctx.dispatch(AppCommand::HelpSearchConfirm),
KeyCode::Backspace => ctx.dispatch(AppCommand::HelpSearchBackspace),
KeyCode::Char(c) => ctx.dispatch(AppCommand::HelpSearchInput(c)),
KeyCode::Char(c) if !ctrl => ctx.dispatch(AppCommand::HelpSearchInput(c)),
_ => {}
}
return InputResult::Continue;
}
match key.code {
KeyCode::Char('/') => ctx.dispatch(AppCommand::HelpActivateSearch),
KeyCode::Char('/') | KeyCode::Char('f') if key.code == KeyCode::Char('/') || ctrl => {
ctx.dispatch(AppCommand::HelpActivateSearch);
}
KeyCode::Esc if !ctx.app.ui.help_search_query.is_empty() => {
ctx.dispatch(AppCommand::HelpClearSearch);
}
KeyCode::Char('j') | KeyCode::Down => ctx.dispatch(AppCommand::HelpScrollDown(1)),
KeyCode::Char('k') | KeyCode::Up => ctx.dispatch(AppCommand::HelpScrollUp(1)),
KeyCode::Tab => ctx.dispatch(AppCommand::HelpNextTopic),
KeyCode::BackTab => ctx.dispatch(AppCommand::HelpPrevTopic),
KeyCode::Tab => ctx.dispatch(AppCommand::HelpToggleFocus),
KeyCode::Char('j') | KeyCode::Down if ctrl => {
ctx.dispatch(AppCommand::HelpNextTopic(5));
}
KeyCode::Char('k') | KeyCode::Up if ctrl => {
ctx.dispatch(AppCommand::HelpPrevTopic(5));
}
KeyCode::Char('j') | KeyCode::Down => match ctx.app.ui.help_focus {
HelpFocus::Topics => ctx.dispatch(AppCommand::HelpNextTopic(1)),
HelpFocus::Content => ctx.dispatch(AppCommand::HelpScrollDown(1)),
},
KeyCode::Char('k') | KeyCode::Up => match ctx.app.ui.help_focus {
HelpFocus::Topics => ctx.dispatch(AppCommand::HelpPrevTopic(1)),
HelpFocus::Content => ctx.dispatch(AppCommand::HelpScrollUp(1)),
},
KeyCode::PageDown => ctx.dispatch(AppCommand::HelpScrollDown(10)),
KeyCode::PageUp => ctx.dispatch(AppCommand::HelpScrollUp(10)),
KeyCode::Char('q') => {

View File

@@ -26,16 +26,16 @@ impl Page {
/// Grid position (col, row) for each page
/// Layout:
/// col 0 col 1 col 2
/// row 0 Options Patterns Help
/// row 1 Dict Sequencer Engine
/// row 0 Dict Patterns Options
/// row 1 Help Sequencer Engine
pub const fn grid_pos(self) -> (i8, i8) {
match self {
Page::Options => (0, 0),
Page::Dict => (0, 1),
Page::Main => (1, 1),
Page::Dict => (0, 0),
Page::Help => (0, 1),
Page::Patterns => (1, 0),
Page::Main => (1, 1),
Page::Options => (2, 0),
Page::Engine => (2, 1),
Page::Help => (2, 0),
}
}

View File

@@ -23,4 +23,4 @@ pub use patterns_nav::{PatternsColumn, PatternsNav};
pub use playback::{PlaybackState, StagedChange};
pub use project::ProjectState;
pub use sample_browser::SampleBrowserState;
pub use ui::{DictFocus, FlashKind, UiState};
pub use ui::{DictFocus, FlashKind, HelpFocus, UiState};

View File

@@ -19,12 +19,20 @@ pub enum DictFocus {
Words,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum HelpFocus {
#[default]
Topics,
Content,
}
pub struct UiState {
pub sparkles: Sparkles,
pub status_message: Option<String>,
pub flash_until: Option<Instant>,
pub flash_kind: FlashKind,
pub modal: Modal,
pub help_focus: HelpFocus,
pub help_topic: usize,
pub help_scrolls: Vec<usize>,
pub help_search_active: bool,
@@ -52,6 +60,7 @@ impl Default for UiState {
flash_until: None,
flash_kind: FlashKind::Success,
modal: Modal::None,
help_focus: HelpFocus::default(),
help_topic: 0,
help_scrolls: vec![0; crate::views::help_view::topic_count()],
help_search_active: false,

View File

@@ -1,4 +1,4 @@
use minimad::{Composite, CompositeStyle, Compound, Line};
use minimad::{Composite, CompositeStyle, Compound, Line, TableRow};
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line as RLine, Span};
@@ -7,25 +7,121 @@ use ratatui::Frame;
use tui_big_text::{BigText, PixelSize};
use crate::app::App;
use crate::state::HelpFocus;
use crate::theme;
use crate::views::highlight;
// To add a new help topic: drop a .md file in docs/ and add one line here.
const DOCS: &[(&str, &str)] = &[
("Welcome", include_str!("../../docs/welcome.md")),
("Audio Engine", include_str!("../../docs/audio_engine.md")),
("Keybindings", include_str!("../../docs/keybindings.md")),
("Sequencer", include_str!("../../docs/sequencer.md")),
("About", include_str!("../../docs/about.md")),
enum DocEntry {
Section(&'static str),
Topic(&'static str, &'static str),
}
use DocEntry::{Section, Topic};
const DOCS: &[DocEntry] = &[
// Getting Started
Section("Getting Started"),
Topic("Welcome", include_str!("../../docs/welcome.md")),
Topic("Moving Around", include_str!("../../docs/navigation.md")),
Topic(
"How Does It Work?",
include_str!("../../docs/how_it_works.md"),
),
Topic(
"Banks & Patterns",
include_str!("../../docs/banks_patterns.md"),
),
Topic("Stage / Commit", include_str!("../../docs/staging.md")),
Topic("Using the Sequencer", include_str!("../../docs/grid.md")),
// Forth fundamentals
Section("Forth"),
Topic("The Dictionary", include_str!("../../docs/dictionary.md")),
Topic("The Stack", include_str!("../../docs/stack.md")),
Topic("Arithmetic", include_str!("../../docs/arithmetic.md")),
Topic("Comparison", include_str!("../../docs/comparison.md")),
Topic("Logic", include_str!("../../docs/logic.md")),
// Sound generation
Section("Sounds"),
Topic("Emitting", include_str!("../../docs/emitting.md")),
Topic("Samples", include_str!("../../docs/samples.md")),
Topic("Oscillators", include_str!("../../docs/oscillators.md")),
Topic("Wavetables", include_str!("../../docs/wavetables.md")),
// Sound shaping
Section("Shaping"),
Topic("Envelopes", include_str!("../../docs/envelopes.md")),
Topic(
"Pitch Envelope",
include_str!("../../docs/pitch_envelope.md"),
),
Topic("Filters", include_str!("../../docs/filters.md")),
Topic(
"Ladder Filters",
include_str!("../../docs/ladder_filters.md"),
),
// Movement and modulation
Section("Movement"),
Topic("LFO & Ramps", include_str!("../../docs/lfo.md")),
Topic("Modulation", include_str!("../../docs/modulation.md")),
Topic("Vibrato", include_str!("../../docs/vibrato.md")),
// Effects
Section("Effects"),
Topic("Delay & Reverb", include_str!("../../docs/delay_reverb.md")),
Topic("Mod FX", include_str!("../../docs/mod_fx.md")),
Topic("EQ & Stereo", include_str!("../../docs/eq_stereo.md")),
Topic("Lo-fi", include_str!("../../docs/lofi.md")),
// Variation and randomness
Section("Variation"),
Topic("Randomness", include_str!("../../docs/randomness.md")),
Topic("Probability", include_str!("../../docs/probability.md")),
Topic("Selection", include_str!("../../docs/selection.md")),
// Timing
Section("Timing"),
Topic("Context", include_str!("../../docs/context.md")),
Topic("Cycles", include_str!("../../docs/cycles.md")),
Topic("Timing", include_str!("../../docs/timing.md")),
Topic("Patterns", include_str!("../../docs/patterns.md")),
Topic("Chaining", include_str!("../../docs/chaining.md")),
// Music theory
Section("Music"),
Topic("Notes", include_str!("../../docs/notes.md")),
Topic("Scales", include_str!("../../docs/scales.md")),
Topic("Chords", include_str!("../../docs/chords.md")),
Topic("Generators", include_str!("../../docs/generators.md")),
// Advanced
Section("Advanced"),
Topic("Variables", include_str!("../../docs/variables.md")),
Topic("Conditionals", include_str!("../../docs/conditionals.md")),
Topic("Custom Words", include_str!("../../docs/definitions.md")),
Topic("Ableton Link", include_str!("../../docs/link.md")),
// Reference
Section("Reference"),
Topic("Audio Engine", include_str!("../../docs/audio_engine.md")),
Topic("Keybindings", include_str!("../../docs/keybindings.md")),
Topic("Sequencer", include_str!("../../docs/sequencer.md")),
// Archive - old files to sort
Section("Archive"),
Topic("Sound Basics", include_str!("../../docs/sound_basics.md")),
Topic("Parameters", include_str!("../../docs/parameters.md")),
Topic("Tempo & Speed", include_str!("../../docs/tempo.md")),
Topic("Effects (old)", include_str!("../../docs/effects.md")),
];
pub fn topic_count() -> usize {
DOCS.len()
DOCS.iter().filter(|e| matches!(e, Topic(_, _))).count()
}
fn get_topic(index: usize) -> Option<(&'static str, &'static str)> {
DOCS.iter()
.filter_map(|e| match e {
Topic(name, content) => Some((*name, *content)),
Section(_) => None,
})
.nth(index)
}
pub fn render(frame: &mut Frame, app: &App, area: Rect) {
let [topics_area, content_area] =
Layout::horizontal([Constraint::Length(18), Constraint::Fill(1)]).areas(area);
Layout::horizontal([Constraint::Length(24), Constraint::Fill(1)]).areas(area);
render_topics(frame, app, topics_area);
render_content(frame, app, content_area);
@@ -33,25 +129,77 @@ pub fn render(frame: &mut Frame, app: &App, area: Rect) {
fn render_topics(frame: &mut Frame, app: &App, area: Rect) {
let theme = theme::get();
let visible_height = area.height.saturating_sub(2) as usize;
let total_items = DOCS.len();
// Find the visual index of the selected topic (including sections)
let selected_visual_idx = {
let mut visual = 0;
let mut topic_count = 0;
for entry in DOCS.iter() {
if let Topic(_, _) = entry {
if topic_count == app.ui.help_topic {
break;
}
topic_count += 1;
}
visual += 1;
}
visual
};
// Calculate scroll to keep selection visible (centered when possible)
let scroll = if selected_visual_idx < visible_height / 2 {
0
} else if selected_visual_idx > total_items.saturating_sub(visible_height / 2) {
total_items.saturating_sub(visible_height)
} else {
selected_visual_idx.saturating_sub(visible_height / 2)
};
// Count topics before the scroll offset to track topic_idx correctly
let mut topic_idx = DOCS
.iter()
.take(scroll)
.filter(|e| matches!(e, Topic(_, _)))
.count();
let items: Vec<ListItem> = DOCS
.iter()
.enumerate()
.map(|(i, (name, _))| {
let selected = i == app.ui.help_topic;
let style = if selected {
Style::new().fg(theme.dict.category_selected).add_modifier(Modifier::BOLD)
} else {
Style::new().fg(theme.ui.text_primary)
};
let prefix = if selected { "> " } else { " " };
ListItem::new(format!("{prefix}{name}")).style(style)
.skip(scroll)
.take(visible_height)
.map(|entry| match entry {
Section(name) => {
let style = Style::new().fg(theme.ui.text_dim);
ListItem::new(format!("{name}")).style(style)
}
Topic(name, _) => {
let selected = topic_idx == app.ui.help_topic;
let style = if selected {
Style::new()
.fg(theme.dict.category_selected)
.add_modifier(Modifier::BOLD)
} else {
Style::new().fg(theme.ui.text_primary)
};
let prefix = if selected { "> " } else { " " };
topic_idx += 1;
ListItem::new(format!("{prefix}{name}")).style(style)
}
})
.collect();
let focused = app.ui.help_focus == HelpFocus::Topics;
let border_color = if focused {
theme.dict.border_focused
} else {
theme.dict.border_normal
};
let list = List::new(items).block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::new().fg(theme.dict.border_focused))
.border_style(Style::new().fg(border_color))
.title("Topics"),
);
frame.render_widget(list, area);
@@ -62,7 +210,9 @@ const BIG_TITLE_HEIGHT: u16 = 6;
fn render_content(frame: &mut Frame, app: &App, area: Rect) {
let theme = theme::get();
let (_, md) = DOCS[app.ui.help_topic];
let Some((_, md)) = get_topic(app.ui.help_topic) else {
return;
};
let is_welcome = app.ui.help_topic == WELCOME_TOPIC;
let md_area = if is_welcome {
@@ -95,24 +245,6 @@ fn render_content(frame: &mut Frame, app: &App, area: Rect) {
let lines = parse_markdown(md);
let has_search_bar = app.ui.help_search_active || has_query;
let search_bar_height: u16 = u16::from(has_search_bar);
let visible_height = md_area.height.saturating_sub(6 + search_bar_height) as usize;
let max_scroll = lines.len().saturating_sub(visible_height);
let scroll = app.ui.help_scroll().min(max_scroll);
let visible: Vec<RLine> = lines
.into_iter()
.skip(scroll)
.take(visible_height)
.map(|line| {
if has_query {
highlight_line(line, &query_lower)
} else {
line
}
})
.collect();
let content_area = if has_search_bar {
let [content, search] =
Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(md_area);
@@ -122,17 +254,55 @@ fn render_content(frame: &mut Frame, app: &App, area: Rect) {
md_area
};
let para = Paragraph::new(visible)
// Calculate dimensions: 2 borders + 4 padding (2 left + 2 right)
let content_width = content_area.width.saturating_sub(6) as usize;
// 2 borders + 4 padding (2 top + 2 bottom)
let visible_height = content_area.height.saturating_sub(6) as usize;
// Calculate total wrapped line count for accurate max_scroll
let total_wrapped: usize = lines
.iter()
.map(|l| wrapped_line_count(l, content_width))
.sum();
let max_scroll = total_wrapped.saturating_sub(visible_height);
let scroll = app.ui.help_scroll().min(max_scroll);
let lines: Vec<RLine> = if has_query {
lines
.into_iter()
.map(|line| highlight_line(line, &query_lower))
.collect()
} else {
lines
};
let focused = app.ui.help_focus == HelpFocus::Content;
let border_color = if focused {
theme.dict.border_focused
} else {
theme.dict.border_normal
};
let para = Paragraph::new(lines)
.scroll((scroll as u16, 0))
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::new().fg(theme.ui.border))
.border_style(Style::new().fg(border_color))
.padding(Padding::new(2, 2, 2, 2)),
)
.wrap(Wrap { trim: false });
frame.render_widget(para, content_area);
}
fn wrapped_line_count(line: &RLine, width: usize) -> usize {
let char_count: usize = line.spans.iter().map(|s| s.content.chars().count()).sum();
if char_count == 0 || width == 0 {
1
} else {
(char_count + width - 1) / width
}
}
fn render_search_bar(frame: &mut Frame, app: &App, area: Rect) {
let theme = theme::get();
let style = if app.ui.help_search_active {
@@ -156,7 +326,9 @@ fn highlight_line<'a>(line: RLine<'a>, query: &str) -> RLine<'a> {
}
let content = span.content.to_string();
let base_style = span.style;
let hl_style = base_style.bg(theme.search.match_bg).fg(theme.search.match_fg);
let hl_style = base_style
.bg(theme.search.match_bg)
.fg(theme.search.match_fg);
let mut start = 0;
let lower_bytes = lower.as_bytes();
let query_bytes = query.as_bytes();
@@ -185,7 +357,14 @@ fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
/// Find first line matching query across all topics. Returns (topic_index, line_index).
pub fn find_match(query: &str) -> Option<(usize, usize)> {
let query = query.to_lowercase();
for (topic_idx, (_, content)) in DOCS.iter().enumerate() {
for (topic_idx, (_, content)) in DOCS
.iter()
.filter_map(|e| match e {
Topic(name, content) => Some((*name, *content)),
Section(_) => None,
})
.enumerate()
{
for (line_idx, line) in content.lines().enumerate() {
if line.to_lowercase().contains(&query) {
return Some((topic_idx, line_idx));
@@ -200,9 +379,11 @@ fn code_border_style() -> Style {
Style::new().fg(theme.markdown.code_border)
}
fn preprocess_underscores(md: &str) -> String {
fn preprocess_markdown(md: &str) -> String {
let mut out = String::with_capacity(md.len());
for line in md.lines() {
// Convert dash list markers to asterisks (minimad only recognizes *)
let line = convert_dash_lists(line);
let mut result = String::with_capacity(line.len());
let mut chars = line.char_indices().peekable();
let bytes = line.as_bytes();
@@ -243,18 +424,44 @@ fn preprocess_underscores(md: &str) -> String {
out
}
fn convert_dash_lists(line: &str) -> String {
let trimmed = line.trim_start();
if trimmed.starts_with("- ") {
let indent = line.len() - trimmed.len();
format!("{}* {}", " ".repeat(indent), &trimmed[2..])
} else {
line.to_string()
}
}
fn parse_markdown(md: &str) -> Vec<RLine<'static>> {
let processed = preprocess_underscores(md);
let processed = preprocess_markdown(md);
let text = minimad::Text::from(processed.as_str());
let mut lines = Vec::new();
let mut code_line_nr: usize = 0;
let mut table_buffer: Vec<TableRow> = Vec::new();
let flush_table = |buf: &mut Vec<TableRow>, out: &mut Vec<RLine<'static>>| {
if buf.is_empty() {
return;
}
let col_widths = compute_column_widths(buf);
for (row_idx, row) in buf.drain(..).enumerate() {
out.push(render_table_row(row, row_idx, &col_widths));
}
};
for line in text.lines {
match line {
Line::Normal(composite) if composite.style == CompositeStyle::Code => {
flush_table(&mut table_buffer, &mut lines);
code_line_nr += 1;
let raw: String = composite.compounds.iter().map(|c: &minimad::Compound| c.src).collect();
let raw: String = composite
.compounds
.iter()
.map(|c: &minimad::Compound| c.src)
.collect();
let mut spans = vec![
Span::styled(format!(" {code_line_nr:>2} "), code_border_style()),
Span::styled("", code_border_style()),
@@ -267,41 +474,125 @@ fn parse_markdown(md: &str) -> Vec<RLine<'static>> {
lines.push(RLine::from(spans));
}
Line::Normal(composite) => {
flush_table(&mut table_buffer, &mut lines);
code_line_nr = 0;
lines.push(composite_to_line(composite));
}
Line::TableRow(row) => {
code_line_nr = 0;
table_buffer.push(row);
}
Line::TableRule(_) => {
// Skip the separator line (---|---|---)
}
_ => {
flush_table(&mut table_buffer, &mut lines);
code_line_nr = 0;
lines.push(RLine::from(""));
}
}
}
flush_table(&mut table_buffer, &mut lines);
lines
}
fn cell_text_width(cell: &Composite) -> usize {
cell.compounds.iter().map(|c| c.src.chars().count()).sum()
}
fn compute_column_widths(rows: &[TableRow]) -> Vec<usize> {
let mut widths: Vec<usize> = Vec::new();
for row in rows {
for (i, cell) in row.cells.iter().enumerate() {
let w = cell_text_width(cell);
if i >= widths.len() {
widths.push(w);
} else if w > widths[i] {
widths[i] = w;
}
}
}
widths
}
fn render_table_row(row: TableRow, row_idx: usize, col_widths: &[usize]) -> RLine<'static> {
let theme = theme::get();
let is_header = row_idx == 0;
let bg = if is_header {
theme.ui.surface
} else if row_idx % 2 == 0 {
theme.table.row_even
} else {
theme.table.row_odd
};
let base_style = if is_header {
Style::new()
.fg(theme.markdown.text)
.bg(bg)
.add_modifier(Modifier::BOLD)
} else {
Style::new().fg(theme.markdown.text).bg(bg)
};
let sep_style = Style::new().fg(theme.markdown.code_border).bg(bg);
let mut spans: Vec<Span<'static>> = Vec::new();
for (i, cell) in row.cells.into_iter().enumerate() {
if i > 0 {
spans.push(Span::styled("", sep_style));
}
let target_width = col_widths.get(i).copied().unwrap_or(0);
let cell_width = cell
.compounds
.iter()
.map(|c| c.src.chars().count())
.sum::<usize>();
for compound in cell.compounds {
compound_to_spans(compound, base_style, &mut spans);
}
let padding = target_width.saturating_sub(cell_width);
if padding > 0 {
spans.push(Span::styled(" ".repeat(padding), base_style));
}
}
RLine::from(spans)
}
fn composite_to_line(composite: Composite) -> RLine<'static> {
let theme = theme::get();
let base_style = match composite.style {
CompositeStyle::Header(1) => Style::new()
.fg(theme.markdown.h1)
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
CompositeStyle::Header(2) => Style::new().fg(theme.markdown.h2).add_modifier(Modifier::BOLD),
CompositeStyle::Header(_) => Style::new().fg(theme.markdown.h3).add_modifier(Modifier::BOLD),
CompositeStyle::Header(2) => Style::new()
.fg(theme.markdown.h2)
.add_modifier(Modifier::BOLD),
CompositeStyle::Header(_) => Style::new()
.fg(theme.markdown.h3)
.add_modifier(Modifier::BOLD),
CompositeStyle::ListItem(_) => Style::new().fg(theme.markdown.list),
CompositeStyle::Quote => Style::new().fg(theme.markdown.quote),
CompositeStyle::Code => Style::new().fg(theme.markdown.code),
CompositeStyle::Paragraph => Style::new().fg(theme.markdown.text),
};
let prefix = match composite.style {
CompositeStyle::ListItem(_) => "",
CompositeStyle::Quote => " ",
_ => "",
let prefix: String = match composite.style {
CompositeStyle::ListItem(depth) => {
let indent = " ".repeat(depth as usize);
format!("{indent}")
}
CompositeStyle::Quote => "".to_string(),
_ => String::new(),
};
let mut spans: Vec<Span<'static>> = Vec::new();
if !prefix.is_empty() {
spans.push(Span::styled(prefix.to_string(), base_style));
spans.push(Span::styled(prefix, base_style));
}
for compound in composite.compounds {

View File

@@ -34,6 +34,8 @@ pub fn bindings_for(page: Page) -> Vec<(&'static str, &'static str, &'static str
bindings.push(("s", "Save", "Save project"));
bindings.push(("l", "Load", "Load project"));
bindings.push(("f", "Fill", "Toggle fill mode (hold)"));
bindings.push(("r", "Rename", "Rename current step"));
bindings.push(("Ctrl+R", "Run", "Run step script immediately"));
}
Page::Patterns => {
bindings.push(("←→↑↓", "Navigate", "Move between banks/patterns"));