Feat: documentation
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s

This commit is contained in:
2026-02-16 23:19:06 +01:00
parent 37f5f74ec1
commit 2d8abe4af9
18 changed files with 565 additions and 227 deletions

View File

@@ -15,7 +15,7 @@ Project
└── 1024 Steps (per pattern)
```
A single project gives you 32 banks, each holding 32 patterns. You get 1024 patterns in each project, ~1.048.000 steps.
A single project gives you 32 banks, each holding 32 patterns. You get 1024 patterns in each project, ~1.048.000 steps. This means that you can create a staggering amount of things. Don't hesitate to create copies, variations, and explore the pattern system thoroughly.
## Patterns
@@ -32,11 +32,13 @@ 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:
Access the patterns view with `F2` (or `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
- `M` Muted
- `S` Soloed
### Keybindings
@@ -44,8 +46,11 @@ Access the patterns view with `Ctrl+Up` from the sequencer. The view shows all b
|-----|--------|
| `Arrows` | Navigate banks and patterns |
| `Enter` | Select and return to sequencer |
| `Space` | Toggle pattern playback |
| `p` | Stage pattern to play/stop |
| `c` | Commit staged changes |
| `m` / `x` | Stage mute / solo toggle |
| `e` | Edit pattern properties |
| `r` | Rename bank or pattern |
| `c` / `v` | Copy / Paste |
| `Ctrl+c` / `Ctrl+v` | Copy / Paste |
| `Delete` | Reset to empty pattern |
| `Esc` | Cancel staged changes |

View File

@@ -0,0 +1,79 @@
# Big Picture
Let's answer some basic questions: what exactly is Cagire? What purpose does it serve? Cagire is a small and simple piece of software that allows you to create music live while playing with scripts. At heart, it is really nothing more than a classic step sequencer, the kind you can buy in a music store. It is voluntarily kept small and simple. Adding the Forth language to program steps allows to create pattern and behaviors of any complexity. Forth also makes it super easy to extend and to customize Cagire while keeping the core mechanisms and the logic simple.
Cagire is not complex, it is just very peculiar. It has been created as an hybrid between a step sequencer and a programming environment. It allows you to create music live and to extend and customize it using the power of Forth. It has been designed to be fast and responsive, low-tech in the sense that you can run it on any decent computer. You can think of it as a musical instrument: you learn it by getting into the flow and practicing. What you ultimately do with it is up to you: improvisation, composition, etc. Cagire is also made to be autonomous, self-contained, and self-sustaining: it contains all the necessary components to make music without relying on external software or hardware.
## Scripts, Not Notes
A traditional step sequencer would offer to the musician a grid where each step represents a note or a single musical event. Cagire replaces notes and/or events in favour of **Forth scripts**. When the sequencer reaches a step to play, it runs the script associated with it. A script can do whatever it is programmed to do: play a note, trigger a sample, apply effects, generate randomness, or all of the above. Scripts can share code and data with each other. Everything else works like a regular step sequencer: you can toggle, copy, paste, and rearrange steps freely.
## What Does a Script Look Like?
A Forth script is generally kind of small, and it solves a simple problem: playing a chord, tweaking some parameters, etc. The more focused it is, the better. Using Forth doesn't feel like programming at all. It feels more like juggling with words and numbers or writing bad computer poetry. Here is a program that plays a middle C note using a sine wave:
```forth
c4 note sine sound .
```
Read it backwards and you will understand what it does:
- `.` — play a sound.
- `sine sound` — the sound is a sine wave.
- `c4 note` — the pitch is C4 (middle C).
Five tokens separated by spaces. There is pretty much no syntax to learn, just three rules:
- There are `words` and `numbers`.
- A `word` is anything that is not a space or a number.
- A `number` is anything that is not a space or a word.
- They are separated by spaces.
- Everything piles up on the **stack**.
The stack is what makes Forth tick. Think of it as a pile of things. `c4` puts a pitch on the pile. `note` picks it up. `sine` chooses a waveform. `sound` assembles everything into a voice. `.` plays it. Each word picks up what the previous ones left behind and leaves something for the next. Scripts can be simple one-liners or complex programs with conditionals, loops, and randomness. You will need to understand the stack, but it will take five minutes. See the **Forth** section for details.
## The Audio Engine
Cagire includes a complete synthesis and sampling engine. No external software is required to play music. It comes with oscillators, sample players, effects, filters, and more. Here are a few examples:
```forth
;; sawtooth wave + lowpass filter with envelope + chorus + reverb
100 199 freq saw sound 250 lpf 8 lpe 1 lpd 0.2 chorus 0.8 verb 2 dur .
```
```forth
;; sine wave + vibrato + bit crushing
0.25 vibmod 8 vib sine sound 6 crush 0.8 gain .
```
```forth
;; white noise + sine wave + envelope = percussion
white sine sound 100 freq 0.5 decay 24 penv 0.5 pdec 2 dur .
```
```forth
;; random robot noises: sine + randomized freq + ring modulation
10 1000 rand freq sine sound 1 100 rand rm 0.5 1.0 rand rmddepth .
```
By _creating words_, registering synth definitions and effects, you will form a vocabulary that can be used to create complex sounds and music. The audio engine is quite capable, and you won't ever run out of new things to try!
## Project Organization
A step sequencer can generally keep multiple patterns in memory, organized in banks. By switching from pattern to pattern, from bank to bank, you can create complex compositions and entire live performances. Cagire works exactly the same and offers banks and patterns. Cagire can also run multiple patterns at the same time. Each pattern can contain from `1` to `1024` steps. Every project is organized following a hierarchical structure:
- **32 Banks**: a (named) container holding patterns.
- **32 Patterns**: each pattern can contain from `1` to `1024` steps.
- **1024 Steps**: each step can be used to program anything you have in mind.
A single project / session can store a lot, probably more than you'll need for a gig or an album. That's up to you to decide if you prefer to keep things simple or to store your entire life in Forth.
## Timing and Synchronization
Everything in Cagire is measured in **beats**. A beat is the pulse of the music. How fast that pulse goes is controlled by a single number, the **BPM** (_beats per minute_). At 120 BPM, one beat lasts half a second. Each step in a pattern is a sixteenth note: **four steps per beat**. At 120 BPM, that is one step every 125 milliseconds. Change the BPM and every step follows. Patterns can run at different speeds. A pattern set to **2x** fires its steps twice as fast. A pattern at **0.5x** takes twice as long. The speed is a multiplier on the step rate, not a separate tempo — all patterns still share the same beat.
Cagire uses **Ableton Link** for synchronization. All devices on the same network share a common tempo automatically. If you change the BPM in Cagire, other apps follow. If they change it, Cagire follows. Most commercial music software supports Link.
## Live Coding
The sequencer runs while you edit. When you change a step's script, the new version takes effect the next time that step fires. There is no compile step, no "run" button — just write, listen, and adjust. This is the core loop: you hear a pattern playing, open a step, change a word, close it, and immediately hear the difference. Forth's brevity helps — swapping `sine` for `saw` or adding `0.3 verb` at the end is a single edit that reshapes the sound. Cagire is not a write-then-run environment. It is closer to a conversation: you propose something, the sequencer plays it back, and you refine from there.

View File

@@ -1,24 +1,27 @@
# Editing a Step
Each step in Cagire contains a Forth script. When the sequencer reaches that step, it runs the script to produce sound. This is where you write your music. Press `Enter` when hovering over any step to open the `code editor`. The editor appears as a modal overlay with the step number in the title bar. If the step is a linked step (shown with an arrow like `→05`), pressing `Enter` navigates to the source step instead.
Each step in Cagire contains a Forth script. When the sequencer reaches that step, it runs the script to produce sound. This is where you write your music. Press `Enter` when hovering over any step to open the code editor. The editor appears as a modal overlay with the step number in the title bar. If the step is a linked step (shown with an arrow like `→05`), pressing `Enter` navigates to the source step instead.
## Writing Scripts
Scripts are written in Forth. Type words and numbers separated by spaces. The simplest script that makes sound is:
```forth
;; a very simple sound
sine sound .
```
Add parameters before words to modify them:
```forth
;; the same sound with more parameters
c4 note 0.75 decay sine sound .
```
Writing long lines is not recommended because it can become quite unmanageable. Instead, break them into multiple lines for clarity:
```forth
;; the same sound on multiple lines
c4 note
0.75 decay
sine sound
@@ -28,35 +31,60 @@ sine sound
## Saving
- `Esc` - Save and close the editor
- `Ctrl+E` - Save without closing
- `Esc` Save, compile, and close the editor.
- `Ctrl+E` Save and compile without closing (evaluate in place).
When you save, the script is compiled and sent to the sequencer. If there's an error, a message appears briefly at the bottom of the screen. You will also receive visual feedback in the form of a flashing window when saving / evaluating a script.
When you save, the script is compiled and sent to the sequencer. If there's an error, a message flashes briefly at the bottom of the screen. `Esc` has layered behavior: if text is selected, it cancels the selection first. If completions are showing, it dismisses them. Otherwise it saves and closes.
## Completion
As you type, the editor suggests matching Forth words. The completion list shows all built-in words that start with your current input. Press `Tab` to insert the selected suggestion, or `Esc` to dismiss the list. Use arrow keys to navigate between suggestions.
As you type, the editor suggests matching Forth words. The completion popup appears once you've typed two or more characters of a word, showing candidates that match your input along with their stack signature and description.
Completion helps you discover words without memorizing them all. Type a few letters and browse what's available. For example, typing `ver` will suggest `verb` (reverb), typing `fil` will show filter-related words.
- `Tab` — Accept the selected suggestion.
- `Ctrl+N` / `Ctrl+P` — Navigate between suggestions.
- `Esc` — Dismiss the list.
Completion helps you discover words without memorizing them all. Type a few letters and browse what's available. For example, typing `ver` will suggest `verb` (reverb), typing `chor` will show chorus-related words.
## Sample Finder
Press `Ctrl+B` to open the sample finder. This provides fuzzy search over your loaded sample folders, making it easy to insert sample names without remembering their exact spelling.
- Type to filter by name
- `Tab` or `Enter` — Insert the selected sample name.
- `Ctrl+N` / `Ctrl+P` — Navigate matches.
- `Esc` — Dismiss.
## Search
Press `Ctrl+F` to open the search bar. Type your query, then navigate matches:
- `Ctrl+N` — Jump to next match.
- `Ctrl+P` — Jump to previous match.
- `Enter` — Confirm and close search.
- `Esc` — Cancel search.
## Debugging
Press `Ctrl+S` to toggle the stack display. This shows the stack state evaluated up to the cursor line, useful for understanding how values flow through your script. Press `Ctrl+R` to execute the script immediately without waiting for the sequencer to reach the step.
Press `Ctrl+S` to toggle the stack display. This shows the stack state evaluated up to the cursor line, useful for understanding how values flow through your script.
Press `Ctrl+R` to execute the script immediately as a one-shot, without waiting for the sequencer to reach the step. A green flash indicates success, red indicates an error.
## Keybindings
| Key | Action |
|-----|--------|
| `Esc` | Save and close |
| `Ctrl+E` | Save without closing |
| `Ctrl+E` | Evaluate (save + compile in place) |
| `Ctrl+R` | Execute script once |
| `Ctrl+S` | Toggle stack display |
| `Ctrl+B` | Open sample finder |
| `Ctrl+F` | Search |
| `Ctrl+N` | Find next |
| `Ctrl+P` | Find previous |
| `Ctrl+N` | Next match / next suggestion |
| `Ctrl+P` | Previous match / previous suggestion |
| `Ctrl+A` | Select all |
| `Ctrl+C` | Copy |
| `Ctrl+X` | Cut |
| `Ctrl+V` | Paste |
| `Shift+Arrows` | Extend selection |
| `Tab` | Accept completion |
| `Tab` | Accept completion / sample |

View File

@@ -0,0 +1,52 @@
# The Audio Engine
The Engine page (`F6`) is where you configure audio hardware, adjust performance settings, and manage your sample library. The right side of the page shows a real-time oscilloscope and spectrum analyzer. The page is divided into three sections. Press `Tab` to move between them, `Shift+Tab` to go back.
## Devices
Two columns show available output and input devices. Press `Left` or `Right` to switch between them. Browse with `Up`/`Down` and press `Enter` to select a device. Use `PageUp`/`PageDown` to scroll long device lists. Press `D` to refresh the device list if you plugged something in after launching Cagire.
## Settings
Four audio parameters are adjustable with `Left`/`Right`:
| Setting | Range | Description |
|---------|-------|-------------|
| Channels | 164 | Number of output channels |
| Buffer | 644096 | Audio buffer size in bytes |
| Voices | 1128 | Maximum polyphony (simultaneous sounds) |
| Nudge | -100 to +100 ms | Timing offset to compensate for latency |
The last two rows — sample rate and audio host — are read-only values reported by your system. After changing the buffer size or channel count, press `Shift+r` to restart the audio engine for changes to take effect.
## Samples
This section shows how many sample directories are registered and how many files have been indexed. Press `A` to open a file browser and add a new sample directory. Press `D` to remove the last one. Cagire indexes audio files (wav, mp3, ogg, flac, aac, m4a) from all registered paths.
Sample directories must be added here before you can use the sample browser or reference samples in your scripts.
## Audio Controls
A few keys work from anywhere on the Engine page:
- `h` — Hush. Silence all audio immediately.
- `p` — Panic. Hard stop, clears all active voices.
- `t` — Test tone. Plays a brief sine wave to verify audio output.
- `r` — Reset the peak voice counter.
## Keybindings
| Key | Action |
|-----|--------|
| `Tab` / `Shift+Tab` | Next / previous section |
| `Up` / `Down` | Navigate within section |
| `Left` / `Right` | Switch device column / adjust setting |
| `PageUp` / `PageDown` | Scroll device list |
| `Enter` | Select device |
| `D` | Refresh devices / remove last sample path |
| `A` | Add sample directory |
| `Shift+r` | Restart audio engine |
| `h` | Hush |
| `p` | Panic |
| `t` | Test tone |
| `r` | Reset peak voices |

View File

@@ -1,14 +1,19 @@
# The Sequencer Grid
The sequencer grid is the main view of Cagire. This is the one you see when you open the application. On this view, you can see the step sequencer grid and edit each step using the `code editor`. At the top, you can optionally display an oscilloscope and a spectrum analyzer.
The sequencer grid is the main view of Cagire (`F5`). This is the one you see when you open the application. On this view, you can see the step sequencer grid and edit each step using the code editor. You can optionally display the following widgets:
- **an oscilloscope**: visualize the current audio output.
- **a spectrum analyzer**: 32 bands spectrum analyze (mostly cosmetic).
- **a step preview**: visualize the content of the hovered script.
You can press `o` to cycle through layouts. It will basically rotate the sequencer around. Use it to find the view that makes the more sense for you.
## Navigation
Use arrow keys to move between steps. The grid wraps around at pattern boundaries. You can move in any direction.
Use arrow keys to move between steps. The grid wraps around at pattern boundaries. Press `:` to jump directly to a step by number. This keybinding is useful for very long patterns.
## Preview
Press `P` to enter preview mode. In preview mode, a view-only code editor opens so that you can see the script of the currently playing step. While in preview mode, you can still move around the grid. Press `Esc` to exit preview mode.
Press `p` to enter preview mode. A read-only code editor opens showing the script of the step under the cursor. You can still navigate the grid while previewing. Press `Esc` to exit preview mode.
## Selection
@@ -16,37 +21,88 @@ Hold `Shift` while pressing arrow keys to select multiple steps. Press `Esc` to
## Editing Steps
- `Enter` - Open the script editor.
- `t` - Toggle step active/inactive.
- `r` - Rename a step.
- `Del` - Delete selected 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).
- `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 independent copies)
`Linked steps` share the same script as their source. When you edit the source, all linked steps update automatically. This is an extremely important and powerful feature. It allows you to create complex patterns with minimal effort. `Ctrl+H` is your best friend to manage linked steps and convert them to real steps.
Linked steps share the same script as their source. When you edit the source, all linked steps update automatically. This is an extremely important and powerful feature. It allows you to create complex patterns with minimal effort. `Ctrl+H` converts linked steps back to independent copies.
## Pattern Controls
- `<` / `>` - Decrease/increase pattern length
- `[` / `]` - Decrease/increase pattern speed
- `L` - Set length directly
- `S` - Set speed directly
- `<` / `>` 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)
- `Space` Toggle play / stop
- `+` / `-` Adjust tempo
- `T` Set tempo directly
- `Ctrl+R` — Execute current step once (one-shot)
## Mute & Solo
- `m` — Mute current pattern
- `x` — Solo current pattern
- `Shift+m` — Clear all mutes
- `Shift+x` — Clear all solos
## Prelude
The prelude is a Forth script that runs before every step, useful for defining shared variables and setup code.
- `d` — Open the prelude editor
- `Shift+d` — Evaluate the prelude
## Tools
- `e` — Euclidean rhythm distribution
- `Tab` — Toggle sample browser panel
## Visual Indicators
- **Highlighted cell** - Currently playing step
- **Colored backgrounds** - Linked steps share colors by source
- **Arrow prefix** (`→05`) - Step is linked to step 05
- **Highlighted cell** Currently playing step
- **Colored backgrounds** Linked steps share colors by source
- **Arrow prefix** (`→05`) Step is linked to step 05
## Keybindings
| Key | Action |
|-----|--------|
| `Arrows` | Navigate grid |
| `Shift+Arrows` | Extend selection |
| `:` | Jump to step |
| `Enter` | Open editor |
| `p` | Preview step |
| `t` | Toggle step active |
| `r` | Rename step |
| `Del` | Delete steps |
| `Ctrl+C` / `Ctrl+V` | Copy / Paste |
| `Ctrl+B` | Paste as links |
| `Ctrl+D` | Duplicate |
| `Ctrl+H` | Harden links |
| `<` / `>` | Pattern length |
| `[` / `]` | Pattern speed |
| `L` / `S` | Set length / speed |
| `Space` | Play / Stop |
| `+` / `-` | Tempo up / down |
| `T` | Set tempo |
| `Ctrl+R` | Execute step once |
| `m` / `x` | Mute / Solo |
| `d` | Prelude editor |
| `e` | Euclidean distribution |
| `o` | Cycle layout |
| `Tab` | Sample browser |
| `Ctrl+Z` | Undo |
| `Ctrl+Shift+Z` | Redo |
| `?` | Show keybindings |

View File

@@ -1,58 +0,0 @@
# 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
- **1024 Steps** per pattern
That's over 1,000,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

@@ -1,40 +1,50 @@
# Navigation
The Cagire application is organized as a grid composed of six views:
The first thing you need to know is how to navigate in the application. Cagire's interface is organized as a 3x2 grid. There are six views in total:
```
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.
- *Dict* : Forth dictionary — learn about the language.
- *Help* : Help and tutorials — learn about the tool.
- *Patterns* : Manage your current session / project.
- *Sequencer* : The main view, where you edit sequences and play music.
- *Options* : Configuration settings for the application.
- *Engine* : Configuration settings for the audio engine.
## Switching Views
Use `Ctrl+Arrow` keys to move between views. A minimap appears briefly showing your position in the grid.
Use `Ctrl+Arrow` keys to move between views. A minimap will briefly appear to show your position in the grid. You can also click at the bottom left on the view name to open the switch view panel.
- `Ctrl+Left` / `Ctrl+Right` - move horizontally
- `Ctrl+Up` / `Ctrl+Down` - move vertically
- `Ctrl+Left` / `Ctrl+Right` move horizontally (wraps around)
- `Ctrl+Up` / `Ctrl+Down` move vertically (does not wrap)
- `Click` at bottom left — select a view
The grid wraps horizontally, so you can cycle through views on the same row.
You can also jump directly to any view with F-keys:
## 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.
| Key | View |
|------|------------|
| `F1` | Dict |
| `F2` | Patterns |
| `F3` | Options |
| `F4` | Help |
| `F5` | Sequencer |
| `F6` | Engine |
## Common Keys
These work on most views:
These shortcuts work on every view:
- `Arrow keys` - move or scroll
- `Tab` - switch focus between panels
- `/` or `Ctrl+f` - search (where available)
- `:` - jump to step number (sequencer view)
- `q` - quit (with confirmation)
| Key | Action |
|---------|---------------------------|
| `Space` | Toggle play / stop |
| `q` | Quit |
| `s` | Save project |
| `l` | Load project |
| `?` | Show keybindings for view |
## Getting Help
Press `?` on any view to see the associated keybindings. This shows all available shortcuts for the current context. The most important keybindings are displayed in the footer bar. Press `Esc` to close the keybindings panel.

View File

@@ -0,0 +1,41 @@
# Options
The Options page (`F3`) gathers all configuration settings in one place: display, synchronization and MIDI. Navigate options with `Up`/`Down` or `Tab`, change values with `Left`/`Right`. All changes are saved automatically.
## Display
| Option | Values | Description |
|--------|--------|-------------|
| Theme | (cycle) | Color scheme for the entire interface |
| Hue rotation | 0360° | Shift theme colors by a hue angle (±5° per step) |
| Refresh rate | 60 / 30 / 15 fps | Lower values reduce CPU usage |
| Runtime highlight | on / off | Highlight executed code spans during playback |
| Show scope | on / off | Oscilloscope on the engine page |
| Show spectrum | on / off | Spectrum analyzer on the engine page |
| Completion | on / off | Word completion popup in the editor |
| Show preview | on / off | Step script preview on the sequencer grid |
## Ableton Link
Cagire uses Ableton Link to synchronize tempo with other applications on the same network. Three settings control the connection:
- **Enabled** — Turn Link on or off. When enabled, Cagire listens for peers and shares its tempo.
- **Start/Stop sync** — When on, pressing play or stop in one app affects all peers.
- **Quantum** — The beat subdivision used for phase alignment.
Below these settings, a read-only session display shows the current tempo, beat position, and phase. The status line at the top shows the connection state: disabled, listening, or connected with peer count.
## MIDI
Four output slots and four input slots let you connect to MIDI devices. Cycle through available devices with `Left`/`Right`. Each slot can hold one device, and the same device cannot be assigned to multiple slots.
## Onboarding
At the bottom, you can reset the onboarding guides if you dismissed them earlier and want to see them again.
## Keybindings
| Key | Action |
|-----|--------|
| `Up` / `Down` / `Tab` | Navigate options |
| `Left` / `Right` | Change value |

View File

@@ -0,0 +1,42 @@
# The Sample Browser
Press `Tab` on the sequencer grid to open the sample browser. It appears as a side panel showing a tree of all your sample directories and files. Press `Tab` again to close it. Before using the browser, you need to register at least one sample directory on the Engine page (`F6`). Cagire indexes audio files (wav, mp3, ogg, flac, aac, m4a) from all registered paths.
## Browsing
The browser displays folders and files in a tree structure.
- `Up` / `Down` — Move through the list
- `Right` — Expand a folder or play a file
- `Left` — Collapse a folder
- `Enter` — Play the selected file
- `PageUp` / `PageDown` — Scroll quickly
When you play a sample, it sounds immediately. This makes it easy to audition sounds before writing them into a script.
## Search
Press `/` to activate the search filter. Type to narrow results by name. Press `Esc` to clear the filter and see everything again.
## Using Samples in Scripts
Sample folder names become words you can use in your Forth scripts. If you have a folder called `kick`, you can write:
```forth
kick sound .
```
See the **Samples** section in the Audio Engine documentation for details on how sample playback works.
## Keybindings
| Key | Action |
|-----|--------|
| `Tab` | Open / close browser |
| `Up` / `Down` | Navigate |
| `Right` | Expand folder / play file |
| `Left` | Collapse folder |
| `Enter` | Play file |
| `PageUp` / `PageDown` | Fast scroll |
| `/` | Search |
| `Esc` | Clear search / close |

View File

@@ -0,0 +1,37 @@
# Saving & Loading
Press `s` to save a project or `l` to load one. These keys work from any view. Both open a file browser where you navigate your filesystem, pick a location, and confirm.
## The File Browser
The browser shows a path input at the top and a directory listing below. Hidden files are filtered out.
- `Up` / `Down` — Browse entries
- `Right` — Enter a directory
- `Left` — Go up to the parent directory
- `Tab` — Autocomplete the current path
- `Enter` — Confirm selection
- `Esc` — Cancel
You can also type directly into the path input. Characters are appended to the current path, and the listing updates as you type. Press `Backspace` to delete.
## Saving
When saving, type a filename and press `Enter`. Parent directories are created automatically if they don't exist. Projects are stored as `.cagire` files.
## Loading
When loading, browse to a `.cagire` file and press `Enter`. The project replaces the current session entirely.
## Keybindings
| Key | Action |
|-----|--------|
| `s` | Save (from any view) |
| `l` | Load (from any view) |
| `Up` / `Down` | Browse entries |
| `Right` | Enter directory |
| `Left` | Parent directory |
| `Tab` | Autocomplete path |
| `Enter` | Confirm |
| `Esc` | Cancel |

View File

@@ -8,15 +8,22 @@ Cagire requires you to `stage` changes you wish to make to the playback state an
Staging is an essential feature to understand to be effective when doing live performances:
1. Open the **Patterns** view (`Ctrl+Up` from sequencer)
1. Open the **Patterns** view (`F2` or `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:
3. Press `p` 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
You can also stage mute/solo changes:
- Press `m` to stage a mute toggle
- Press `x` to stage a solo toggle
- Press `Shift+m` to clear all mutes
- Press `Shift+x` to clear all solos
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
@@ -26,8 +33,10 @@ A pattern might not start immediately depending on the sync mode you have chosen
| `>` | Currently playing |
| `+` | Staged to play |
| `-` | Staged to stop |
| `M` | Muted |
| `S` | Soloed |
A pattern can show both `>` (playing) and `-` (staged to stop).
A pattern can show combined indicators, e.g. `>` (playing) and `-` (staged to stop), or `>M` (playing and muted).
## Quantization

View File

@@ -1,27 +1,21 @@
# Welcome to Cagire
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:
Cagire is a live-codable step sequencer. Each sequencer step is defined by a **Forth** script that gets executed in due time. **Forth** is a minimal, fun and rewarding programming language. It has almost no syntax but provides infinite fun. It rewards exploration, creativity and curiosity. This documentation is both a _tutorial_ and a _reference_. All the code examples in the documentation are interactive. Use `n` and `p` (next/previous) to navigate through the examples. Press `Enter` to evaluate them! Try to evaluate the following example using `n`, `p` and `Enter`:
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?
```forth
saw sound
400 freq
1 decay
.
```
## What is 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.
Live coding is a technique where a programmer writes code in real-time to create audiovisual performances. Most often, it is practiced in front of an audience. Live coding is a way to experiment with code, to share things and thoughts openly, to think 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 with a strong emphasis on _improvisation_. 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.
## About
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.
Cagire is mainly developed 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. Help and feedback are welcome!
### Credits

View File

@@ -291,12 +291,14 @@ impl App {
let palette = scheme.to_palette();
let rotated = cagire_ratatui::theme::transform::rotate_palette(&palette, self.ui.hue_rotation);
crate::theme::set(rotated);
self.ui.invalidate_help_cache();
}
AppCommand::SetHueRotation(degrees) => {
self.ui.hue_rotation = degrees;
let palette = self.ui.color_scheme.to_palette();
let rotated = cagire_ratatui::theme::transform::rotate_palette(&palette, degrees);
crate::theme::set(rotated);
self.ui.invalidate_help_cache();
}
AppCommand::ToggleRuntimeHighlight => {
self.ui.runtime_highlight = !self.ui.runtime_highlight;

View File

@@ -17,10 +17,8 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
ctx.app.panel.visible = false;
ctx.app.panel.focus = PanelFocus::Main;
} else {
if ctx.app.panel.side.is_none() {
let state = SampleBrowserState::new(&ctx.app.audio.config.sample_paths);
ctx.app.panel.side = Some(SidePanel::SampleBrowser(state));
}
ctx.app.panel.visible = true;
ctx.app.panel.focus = PanelFocus::Side;
}
@@ -127,13 +125,21 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
if let Some(range) = ctx.app.editor_ctx.selection_range() {
let steps: Vec<usize> = range.collect();
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
action: ConfirmAction::DeleteSteps { bank, pattern, steps },
action: ConfirmAction::DeleteSteps {
bank,
pattern,
steps,
},
selected: false,
}));
} else {
let step = ctx.app.editor_ctx.step;
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
action: ConfirmAction::DeleteStep { bank, pattern, step },
action: ConfirmAction::DeleteStep {
bank,
pattern,
step,
},
selected: false,
}));
}
@@ -172,7 +178,11 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
.and_then(|s| s.name.clone())
.unwrap_or_default();
ctx.dispatch(AppCommand::OpenModal(Modal::Rename {
target: RenameTarget::Step { bank, pattern, step },
target: RenameTarget::Step {
bank,
pattern,
step,
},
name: current_name,
}));
}

View File

@@ -10,12 +10,12 @@ pub const DOCS: &[DocEntry] = &[
Section("Getting Started"),
Topic("Welcome", include_str!("../../docs/welcome.md")),
Topic(
"Moving Around",
"Navigation",
include_str!("../../docs/getting-started/navigation.md"),
),
Topic(
"How Does It Work?",
include_str!("../../docs/getting-started/how_it_works.md"),
"The Big Picture",
include_str!("../../docs/getting-started/big_picture.md"),
),
Topic(
"Banks & Patterns",
@@ -33,6 +33,22 @@ pub const DOCS: &[DocEntry] = &[
"Editing a Step",
include_str!("../../docs/getting-started/editing.md"),
),
Topic(
"The Audio Engine",
include_str!("../../docs/getting-started/engine.md"),
),
Topic(
"Options",
include_str!("../../docs/getting-started/options.md"),
),
Topic(
"Saving & Loading",
include_str!("../../docs/getting-started/saving.md"),
),
Topic(
"The Sample Browser",
include_str!("../../docs/getting-started/samples.md"),
),
// Forth fundamentals
Section("Forth"),
Topic(
@@ -60,10 +76,7 @@ pub const DOCS: &[DocEntry] = &[
Topic("Settings", include_str!("../../docs/engine/settings.md")),
Topic("Sources", include_str!("../../docs/engine/sources.md")),
Topic("Samples", include_str!("../../docs/engine/samples.md")),
Topic(
"Wavetables",
include_str!("../../docs/engine/wavetable.md"),
),
Topic("Wavetables", include_str!("../../docs/engine/wavetable.md")),
Topic("Filters", include_str!("../../docs/engine/filters.md")),
Topic(
"Modulation",
@@ -78,10 +91,7 @@ pub const DOCS: &[DocEntry] = &[
"Audio-Rate Mod",
include_str!("../../docs/engine/audio_modulation.md"),
),
Topic(
"Words & Sounds",
include_str!("../../docs/engine/words.md"),
),
Topic("Words & Sounds", include_str!("../../docs/engine/words.md")),
// MIDI
Section("MIDI"),
Topic("Introduction", include_str!("../../docs/midi/intro.md")),
@@ -101,10 +111,7 @@ pub const DOCS: &[DocEntry] = &[
"Generators",
include_str!("../../docs/tutorials/generators.md"),
),
Topic(
"Timing with at",
include_str!("../../docs/tutorials/at.md"),
),
Topic("Timing with at", include_str!("../../docs/tutorials/at.md")),
Topic(
"Using Variables",
include_str!("../../docs/tutorials/variables.md"),

View File

@@ -165,4 +165,8 @@ impl UiState {
pub fn dismiss_minimap(&mut self) {
self.minimap = MinimapMode::Hidden;
}
pub fn invalidate_help_cache(&self) {
self.help_parsed.borrow_mut().iter_mut().for_each(|slot| *slot = None);
}
}

View File

@@ -432,7 +432,7 @@ fn render_samples(frame: &mut Frame, app: &App, area: Rect) {
dim,
)));
lines.push(Line::from(Span::styled(
" Add folders containing .wav files",
" Add folders containing audio files",
dim,
)));
} else {

View File

@@ -56,11 +56,19 @@ pub fn adjust_resolved_for_line(
) -> Vec<(SourceSpan, String)> {
resolved
.iter()
.filter_map(|(s, display)| clip_span(*s, line_start, line_len).map(|cs| (cs, display.clone())))
.filter_map(|(s, display)| {
clip_span(*s, line_start, line_len).map(|cs| (cs, display.clone()))
})
.collect()
}
pub fn render(frame: &mut Frame, app: &App, link: &LinkState, snapshot: &SequencerSnapshot, elapsed: Duration) {
pub fn render(
frame: &mut Frame,
app: &App,
link: &LinkState,
snapshot: &SequencerSnapshot,
elapsed: Duration,
) {
let term = frame.area();
let theme = theme::get();
@@ -212,7 +220,11 @@ fn render_side_panel(frame: &mut Frame, app: &App, area: Rect) {
});
let duration = sample.total_frames as f32 / app.audio.config.sample_rate;
let ch_label = if sample.channels == 1 { "mono" } else { "stereo" };
let ch_label = if sample.channels == 1 {
"mono"
} else {
"stereo"
};
let info = Paragraph::new(format!(" {duration:.1}s · {ch_label}"))
.style(Style::new().fg(theme::get().ui.text_dim));
frame.render_widget(info, info_area);
@@ -349,7 +361,9 @@ fn render_header(
} else {
theme.header.stats_fg
};
let dim = Style::new().bg(theme.header.stats_bg).fg(theme.header.stats_fg);
let dim = Style::new()
.bg(theme.header.stats_bg)
.fg(theme.header.stats_fg);
let stats_line = Line::from(vec![
Span::styled(format!(" CPU {cpu_pct:.0}%"), dim.fg(cpu_color)),
Span::styled(format!(" V:{voices} L:{peers} "), dim),
@@ -407,9 +421,9 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
Page::Engine => vec![
("Tab", "Section"),
("←→", "Switch/Adjust"),
("↑↓", "Navigate"),
("Enter", "Select"),
("A", "Add path"),
("R", "Restart"),
("h", "Hush"),
("?", "Keys"),
],
Page::Options => vec![
@@ -484,14 +498,18 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
frame.render_widget(footer, area);
}
fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term: Rect) -> Option<Rect> {
fn render_modal(
frame: &mut Frame,
app: &App,
snapshot: &SequencerSnapshot,
term: Rect,
) -> Option<Rect> {
let theme = theme::get();
let user_words: HashSet<String> = app.dict.lock().keys().cloned().collect();
let inner = match &app.ui.modal {
Modal::None => return None,
Modal::Confirm { action, selected } => {
ConfirmModal::new("Confirm", &action.message(), *selected)
.render_centered(frame, term)
ConfirmModal::new("Confirm", &action.message(), *selected).render_centered(frame, term)
}
Modal::FileBrowser(state) => {
use crate::state::file_browser::FileBrowserMode;
@@ -577,8 +595,7 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
} => {
use crate::state::PatternPropsField;
let inner =
ModalFrame::new(&format!(" Pattern B{:02}:P{:02} ", bank + 1, pattern + 1))
let inner = ModalFrame::new(&format!(" Pattern B{:02}:P{:02} ", bank + 1, pattern + 1))
.width(50)
.height(12)
.border_color(theme.modal.input)
@@ -587,16 +604,33 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
let speed_label = speed.label();
let fields: Vec<(&str, &str, bool)> = vec![
("Name", name.as_str(), *field == PatternPropsField::Name),
("Length", length.as_str(), *field == PatternPropsField::Length),
(
"Length",
length.as_str(),
*field == PatternPropsField::Length,
),
("Speed", &speed_label, *field == PatternPropsField::Speed),
("Quantization", quantization.label(), *field == PatternPropsField::Quantization),
("Sync Mode", sync_mode.label(), *field == PatternPropsField::SyncMode),
(
"Quantization",
quantization.label(),
*field == PatternPropsField::Quantization,
),
(
"Sync Mode",
sync_mode.label(),
*field == PatternPropsField::SyncMode,
),
];
render_props_form(frame, inner, &fields);
let hint_area = Rect::new(inner.x, inner.y + inner.height - 1, inner.width, 1);
let hints = hint_line(&[("↑↓", "nav"), ("←→", "change"), ("Enter", "save"), ("Esc", "cancel")]);
let hints = hint_line(&[
("↑↓", "nav"),
("←→", "change"),
("Enter", "save"),
("Esc", "cancel"),
]);
frame.render_widget(Paragraph::new(hints), hint_area);
inner
@@ -625,7 +659,8 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
lines
};
let key_lines = keys.len() as u16;
let modal_height = (3 + desc_lines + 1 + key_lines + 2).min(term.height.saturating_sub(4));
let modal_height =
(3 + desc_lines + 1 + key_lines + 2).min(term.height.saturating_sub(4));
let title = if page_count > 1 {
format!(" {} ({}/{}) ", app.page.name(), page_idx + 1, page_count)
@@ -654,16 +689,13 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
}
let line = Line::from(vec![
Span::raw(" "),
Span::styled(
format!("{:>8}", key),
Style::new().fg(theme.hint.key),
),
Span::styled(
format!(" {action}"),
Style::new().fg(theme.hint.text),
),
Span::styled(format!("{:>8}", key), Style::new().fg(theme.hint.key)),
Span::styled(format!(" {action}"), Style::new().fg(theme.hint.text)),
]);
frame.render_widget(Paragraph::new(line), Rect::new(inner.x + 1, y, inner.width.saturating_sub(2), 1));
frame.render_widget(
Paragraph::new(line),
Rect::new(inner.x + 1, y, inner.width.saturating_sub(2), 1),
);
y += 1;
}
@@ -677,7 +709,10 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
}
hints_vec.push(("Enter", "don't show again"));
let hints = hint_line(&hints_vec);
frame.render_widget(Paragraph::new(hints).alignment(Alignment::Center), hint_area);
frame.render_widget(
Paragraph::new(hints).alignment(Alignment::Center),
hint_area,
);
inner
}
@@ -702,7 +737,11 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
let fields: Vec<(&str, &str, bool)> = vec![
("Pulses", pulses.as_str(), *field == EuclideanField::Pulses),
("Steps", steps.as_str(), *field == EuclideanField::Steps),
("Rotation", rotation.as_str(), *field == EuclideanField::Rotation),
(
"Rotation",
rotation.as_str(),
*field == EuclideanField::Rotation,
),
];
render_props_form(frame, inner, &fields);
@@ -723,7 +762,12 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
}
let hint_area = Rect::new(inner.x, inner.y + inner.height - 1, inner.width, 1);
let hints = hint_line(&[("↑↓", "nav"), ("←→", "adjust"), ("Enter", "apply"), ("Esc", "cancel")]);
let hints = hint_line(&[
("↑↓", "nav"),
("←→", "adjust"),
("Enter", "apply"),
("Esc", "cancel"),
]);
frame.render_widget(Paragraph::new(hints), hint_area);
inner
@@ -791,12 +835,7 @@ fn render_modal_preview(
};
let resolved_display: Vec<(SourceSpan, String)> = trace
.map(|t| {
t.resolved
.iter()
.map(|(s, v)| (*s, v.display()))
.collect()
})
.map(|t| t.resolved.iter().map(|(s, v)| (*s, v.display())).collect())
.unwrap_or_default();
let mut line_start = 0usize;
@@ -804,21 +843,10 @@ fn render_modal_preview(
.lines()
.map(|line_str| {
let tokens = if let Some(t) = trace {
let exec = adjust_spans_for_line(
&t.executed_spans,
line_start,
line_str.len(),
);
let sel = adjust_spans_for_line(
&t.selected_spans,
line_start,
line_str.len(),
);
let res = adjust_resolved_for_line(
&resolved_display,
line_start,
line_str.len(),
);
let exec = adjust_spans_for_line(&t.executed_spans, line_start, line_str.len());
let sel = adjust_spans_for_line(&t.selected_spans, line_start, line_str.len());
let res =
adjust_resolved_for_line(&resolved_display, line_start, line_str.len());
highlight_line_with_runtime(line_str, &exec, &sel, &res, user_words)
} else {
highlight_line_with_runtime(line_str, &[], &[], &[], user_words)
@@ -898,12 +926,7 @@ fn render_modal_editor(
}
let resolved_display: Vec<(SourceSpan, String)> = trace
.map(|t| {
t.resolved
.iter()
.map(|(s, v)| (*s, v.display()))
.collect()
})
.map(|t| t.resolved.iter().map(|(s, v)| (*s, v.display())).collect())
.unwrap_or_default();
let highlighter = |row: usize, line: &str| -> Vec<(Style, String, bool)> {
@@ -919,8 +942,8 @@ fn render_modal_editor(
highlight::highlight_line_with_runtime(line, &exec, &sel, &res, user_words)
};
let show_search = app.editor_ctx.editor.search_active()
|| !app.editor_ctx.editor.search_query().is_empty();
let show_search =
app.editor_ctx.editor.search_active() || !app.editor_ctx.editor.search_query().is_empty();
let reserved_lines = 1 + if show_search { 1 } else { 0 };
let editor_height = inner.height.saturating_sub(reserved_lines);
@@ -1061,10 +1084,7 @@ fn render_modal_keybindings(frame: &mut Frame, app: &App, scroll: usize, term: R
height: 1,
};
let hints = hint_line(&[("↑↓", "scroll"), ("PgUp/Dn", "page"), ("Esc/?", "close")]);
frame.render_widget(
Paragraph::new(hints).alignment(Alignment::Right),
hint_area,
);
frame.render_widget(Paragraph::new(hints).alignment(Alignment::Right), hint_area);
inner
}