Wip
This commit is contained in:
25
.github/workflows/build-windows.yml
vendored
25
.github/workflows/build-windows.yml
vendored
@@ -71,13 +71,24 @@ jobs:
|
||||
if: inputs.build-packages
|
||||
run: cargo xtask bundle cagire-plugins --release --target x86_64-pc-windows-msvc
|
||||
|
||||
- name: Install cargo-wix
|
||||
- name: Install NSIS
|
||||
if: inputs.build-packages
|
||||
run: cargo install cargo-wix
|
||||
run: choco install nsis
|
||||
|
||||
- name: Build MSI installer
|
||||
- name: Build NSIS installer
|
||||
if: inputs.build-packages
|
||||
run: cargo wix --no-build --nocapture --package cagire -C -arch -C x64
|
||||
shell: pwsh
|
||||
run: |
|
||||
$version = (Select-String -Path Cargo.toml -Pattern '^version\s*=\s*"(.+)"' | Select-Object -First 1).Matches.Groups[1].Value
|
||||
$root = (Get-Location).Path
|
||||
$target = "x86_64-pc-windows-msvc"
|
||||
& "C:\Program Files (x86)\NSIS\makensis.exe" `
|
||||
"-DVERSION=$version" `
|
||||
"-DCLI_EXE=$root\target\$target\release\cagire.exe" `
|
||||
"-DDESKTOP_EXE=$root\target\$target\release\cagire-desktop.exe" `
|
||||
"-DICON=$root\assets\Cagire.ico" `
|
||||
"-DOUTDIR=$root\target" `
|
||||
nsis/cagire.nsi
|
||||
|
||||
- name: Upload CLI artifact
|
||||
if: inputs.build-packages
|
||||
@@ -93,12 +104,12 @@ jobs:
|
||||
name: cagire-windows-x86_64-desktop
|
||||
path: target/x86_64-pc-windows-msvc/release/cagire-desktop.exe
|
||||
|
||||
- name: Upload MSI artifact
|
||||
- name: Upload installer artifact
|
||||
if: inputs.build-packages
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-windows-x86_64-msi
|
||||
path: target/wix/*.msi
|
||||
name: cagire-windows-x86_64-installer
|
||||
path: target/cagire-*-setup.exe
|
||||
|
||||
- name: Prepare plugin artifacts
|
||||
if: inputs.build-packages
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -78,8 +78,8 @@ jobs:
|
||||
elif [[ "$name" == *-vst3 ]]; then
|
||||
base="${name%-vst3}"
|
||||
cd "$dir" && zip -r "../../release/${base}-vst3.zip" cagire-plugins.vst3 && cd ../..
|
||||
elif [[ "$name" == *-msi ]]; then
|
||||
cp "$dir"/*.msi release/
|
||||
elif [[ "$name" == *-installer ]]; then
|
||||
cp "$dir"/*-setup.exe release/
|
||||
elif [[ "$name" == *-appimage ]]; then
|
||||
cp "$dir"/*.AppImage release/
|
||||
elif [[ "$name" == *-desktop ]]; then
|
||||
|
||||
@@ -86,7 +86,7 @@ image = { version = "0.25", default-features = false, features = ["png"], option
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
cpal = { version = "0.17", optional = true, features = ["jack"] }
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
[build-dependencies]
|
||||
winres = "0.1"
|
||||
|
||||
[profile.release]
|
||||
|
||||
17
build.rs
17
build.rs
@@ -13,13 +13,22 @@ fn main() {
|
||||
println!("cargo:rustc-link-lib=oleaut32");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if target_os == "windows" {
|
||||
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
let icon = format!("{manifest_dir}/assets/Cagire.ico");
|
||||
eprintln!("winres: manifest_dir = {manifest_dir}");
|
||||
eprintln!("winres: icon path = {icon}");
|
||||
eprintln!("winres: icon exists = {}", std::path::Path::new(&icon).exists());
|
||||
eprintln!("winres: OUT_DIR = {}", std::env::var("OUT_DIR").unwrap_or_default());
|
||||
eprintln!("winres: TARGET = {}", std::env::var("TARGET").unwrap_or_default());
|
||||
let mut res = winres::WindowsResource::new();
|
||||
res.set_icon("assets/Cagire.ico")
|
||||
res.set_icon(&icon)
|
||||
.set("ProductName", "Cagire")
|
||||
.set("FileDescription", "Forth-based music sequencer")
|
||||
.set("LegalCopyright", "Copyright (c) 2025 Raphaël Forment");
|
||||
res.compile().expect("Failed to compile Windows resources");
|
||||
if let Err(e) = res.compile() {
|
||||
eprintln!("winres: compile error: {e:?}");
|
||||
panic!("Failed to compile Windows resources: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -543,6 +543,8 @@ pub struct Bank {
|
||||
pub patterns: Vec<Pattern>,
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub prelude: String,
|
||||
}
|
||||
|
||||
impl Bank {
|
||||
@@ -560,6 +562,7 @@ impl Default for Bank {
|
||||
Self {
|
||||
patterns: (0..MAX_PATTERNS).map(|_| Pattern::default()).collect(),
|
||||
name: None,
|
||||
prelude: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
8376
demos/02.cagire
8376
demos/02.cagire
File diff suppressed because it is too large
Load Diff
@@ -1,18 +1,57 @@
|
||||
# The Prelude
|
||||
# Preludes
|
||||
|
||||
You can define words in any step and they become available to all other steps. But as a project grows, definitions get scattered across steps and become hard to find and maintain. The **prelude** is a dedicated place for this. It is a project-wide Forth script that runs once before the first step plays. Definitions, variables, settings — everything in one place. Press `d` to open the prelude editor. Press `Esc` to save and close. Press `D` (Shift+d) to re-evaluate it without opening the editor.
|
||||
Cagire has two levels of prelude: a **project prelude** shared by all banks, and **bank preludes** that travel with each bank.
|
||||
|
||||
## Bank Prelude
|
||||
|
||||
Each bank can carry its own prelude script. Press `p` to open the current bank's prelude editor. Press `Esc` to save, evaluate, and close.
|
||||
|
||||
Bank preludes make banks self-contained. When you share a bank, its prelude travels with it — recipients get all the definitions they need without merging anything into their own project.
|
||||
|
||||
```forth
|
||||
: bass pulse sound 0.8 gain 400 lpf 1 lpd 8 lpe 0.6 width . ;
|
||||
: pad sine sound 0.5 gain 2 spread 1.5 attack 0.4 verb . ;
|
||||
```
|
||||
|
||||
Every step in that bank can now use `bass` and `pad`. Share the bank and the recipient gets these definitions automatically.
|
||||
|
||||
## Project Prelude
|
||||
|
||||
The project prelude is a global script shared across all banks. Press `P` (Shift+p) to open it. Use it for truly project-wide definitions, variables, and settings that every bank should see.
|
||||
|
||||
```forth
|
||||
c2 !root
|
||||
0 !mode
|
||||
42 seed
|
||||
```
|
||||
|
||||
## Evaluation Order
|
||||
|
||||
When preludes are evaluated (on playback start, project load, or pressing `d`):
|
||||
|
||||
1. **Project prelude** runs first
|
||||
2. **Bank 0 prelude** runs next (if non-empty)
|
||||
3. **Bank 1 prelude**, then **Bank 2**, ... up to **Bank 31**
|
||||
|
||||
Only non-empty bank preludes are evaluated. Last-evaluated wins for name collisions — a bank prelude can override a project-level definition.
|
||||
|
||||
## Keybindings
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `p` | Open current bank's prelude editor |
|
||||
| `P` | Open project prelude editor |
|
||||
| `d` | Re-evaluate all preludes (project + all banks) |
|
||||
|
||||
## Naming Your Sounds
|
||||
|
||||
The most common use of the prelude is to define words for your instruments. Without a prelude, every step that plays a bass has to spell out the full sound design or to create a new word before using it:
|
||||
The most common use of a bank prelude is to define words for your instruments. Without a prelude, every step that plays a bass has to spell out the full sound design:
|
||||
|
||||
```forth
|
||||
pulse sound 0.8 gain 400 lpf 1 lpd 8 lpe 0.6 width .
|
||||
pulse sound c2 note 0.8 gain 400 lpf 1 lpd 8 lpe 0.6 width .
|
||||
```
|
||||
|
||||
Repeat this across eight steps without making a new word and you have eight copies of the same thing. Change the filter? Change it eight times.
|
||||
|
||||
In the prelude, define it once:
|
||||
In the bank prelude, define it once:
|
||||
|
||||
```forth
|
||||
: bass pulse sound 0.8 gain 400 lpf 1 lpd 8 lpe 0.6 width . ;
|
||||
@@ -20,22 +59,8 @@ In the prelude, define it once:
|
||||
|
||||
Now every step just writes `c2 note bass`. Change the sound in one place, every step follows.
|
||||
|
||||
A step that used to read:
|
||||
|
||||
```forth
|
||||
pulse sound c2 note 0.8 gain 400 lpf 1 lpd 8 lpe 0.6 width .
|
||||
```
|
||||
|
||||
Becomes:
|
||||
|
||||
```forth
|
||||
c2 note bass
|
||||
```
|
||||
|
||||
## Building a Vocabulary
|
||||
|
||||
The prelude is where you build the vocabulary for your music. Not just instruments but any combination of code / words you want to reuse:
|
||||
|
||||
```forth
|
||||
;; instruments
|
||||
: bass pulse sound 0.8 gain 400 lpf 1 lpd 8 lpe 0.6 width . ;
|
||||
@@ -49,11 +74,11 @@ The prelude is where you build the vocabulary for your music. Not just instrumen
|
||||
: loud 0.9 gain ;
|
||||
```
|
||||
|
||||
By using the prelude and predefined words, steps become expressive and short. The prelude carries the design decisions; steps carry the composition.
|
||||
Steps become expressive and short. The prelude carries the design decisions; steps carry the composition.
|
||||
|
||||
## Setting Initial State
|
||||
|
||||
The prelude also runs plain Forth, not just definitions. You can use it to set variables and seed the random generator:
|
||||
The project prelude is the right place for global state:
|
||||
|
||||
```forth
|
||||
c2 !root
|
||||
@@ -61,18 +86,18 @@ c2 !root
|
||||
42 seed
|
||||
```
|
||||
|
||||
Every step can then read `@root` and `@mode`. And `42 seed` makes randomness reproducible — same seed, same sequence every time you hit play.
|
||||
Every step can then read `@root` and `@mode`. And `42 seed` makes randomness reproducible.
|
||||
|
||||
## When It Runs
|
||||
## When Preludes Run
|
||||
|
||||
The prelude evaluates at three moments:
|
||||
Preludes evaluate at three moments:
|
||||
|
||||
1. When you press **Space** to start playback
|
||||
2. When you **load** a project
|
||||
3. When you press **D** manually
|
||||
3. When you press **d** manually
|
||||
|
||||
It runs once at these moments, not on every step. This makes it the right place for definitions and initial values. If you edit the prelude while playing, press `D` to push changes into the running session. New definitions take effect immediately; the next time a step runs, it sees the updated words.
|
||||
They run once at these moments, not on every step. If you edit a prelude while playing, press `d` to push changes into the running session.
|
||||
|
||||
## What Not to Put Here
|
||||
|
||||
The prelude has no access to sequencer state. Words like `step`, `beat`, `iter`, and `phase` are meaningless here because no step is playing yet. Use the prelude for definitions and setup, not for logic that depends on timing. The prelude also should not emit sounds. It runs silently — any `.` calls here would fire before the sequencer clock is running and produce nothing useful.
|
||||
Preludes have no access to sequencer state. Words like `step`, `beat`, `iter`, and `phase` are meaningless here because no step is playing yet. Use preludes for definitions and setup, not for logic that depends on timing. Preludes also should not emit sounds — any `.` calls here would fire before the sequencer clock is running.
|
||||
|
||||
@@ -32,12 +32,13 @@ To create mirrors: copy a step with `Ctrl+C`, then paste with `Ctrl+B` instead o
|
||||
- `Ctrl+D` — Duplicate selection
|
||||
- `Ctrl+H` — Harden mirrors (convert to independent copies)
|
||||
|
||||
## Prelude
|
||||
## Preludes
|
||||
|
||||
The prelude is a Forth script that runs before every step, useful for defining shared variables and setup code.
|
||||
Each bank has its own prelude — a Forth script for definitions and setup that travels with the bank when shared. There is also a project-wide prelude for global configuration.
|
||||
|
||||
- `p` — Open the prelude editor
|
||||
- `d` — Evaluate the prelude
|
||||
- `p` — Open current bank's prelude editor
|
||||
- `P` — Open project prelude editor
|
||||
- `d` — Evaluate all preludes (project + all banks)
|
||||
|
||||
## Pattern Controls
|
||||
|
||||
|
||||
98
nsis/cagire.nsi
Normal file
98
nsis/cagire.nsi
Normal file
@@ -0,0 +1,98 @@
|
||||
; Cagire NSIS Installer Script
|
||||
; Receives defines from command line:
|
||||
; -DVERSION=x.y.z
|
||||
; -DCLI_EXE=/path/to/cagire.exe
|
||||
; -DDESKTOP_EXE=/path/to/cagire-desktop.exe
|
||||
; -DICON=/path/to/Cagire.ico
|
||||
; -DOUTDIR=/path/to/releases
|
||||
|
||||
!include "MUI2.nsh"
|
||||
!include "WordFunc.nsh"
|
||||
|
||||
Name "Cagire ${VERSION}"
|
||||
OutFile "${OUTDIR}\cagire-${VERSION}-windows-x86_64-setup.exe"
|
||||
InstallDir "$PROGRAMFILES64\Cagire"
|
||||
InstallDirRegKey HKLM "Software\Cagire" "InstallDir"
|
||||
RequestExecutionLevel admin
|
||||
Unicode True
|
||||
|
||||
!define MUI_ICON "${ICON}"
|
||||
!define MUI_UNICON "${ICON}"
|
||||
!define MUI_ABORTWARNING
|
||||
|
||||
!define MUI_HEADERIMAGE
|
||||
!define MUI_HEADERIMAGE_BITMAP "header.bmp"
|
||||
!define MUI_WELCOMEFINISHPAGE_BITMAP "sidebar.bmp"
|
||||
!define MUI_UNWELCOMEFINISHPAGE_BITMAP "sidebar.bmp"
|
||||
|
||||
!insertmacro MUI_PAGE_WELCOME
|
||||
!insertmacro MUI_PAGE_COMPONENTS
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
!insertmacro MUI_PAGE_FINISH
|
||||
|
||||
!insertmacro MUI_UNPAGE_CONFIRM
|
||||
!insertmacro MUI_UNPAGE_INSTFILES
|
||||
|
||||
!insertmacro MUI_LANGUAGE "English"
|
||||
|
||||
Section "Cagire (required)" SecCore
|
||||
SectionIn RO
|
||||
SetOutPath "$INSTDIR"
|
||||
File "/oname=cagire.exe" "${CLI_EXE}"
|
||||
File "/oname=cagire-desktop.exe" "${DESKTOP_EXE}"
|
||||
|
||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||
WriteRegStr HKLM "Software\Cagire" "InstallDir" "$INSTDIR"
|
||||
|
||||
; Add/Remove Programs entry
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "DisplayName" "Cagire"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "DisplayVersion" "${VERSION}"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "Publisher" "Raphael Forment"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "UninstallString" '"$INSTDIR\uninstall.exe"'
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "DisplayIcon" '"$INSTDIR\cagire-desktop.exe"'
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "URLInfoAbout" "https://github.com/Bubobubobubobubo/cagire"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "HelpLink" "https://cagire.raphaelforment.fr"
|
||||
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "NoModify" 1
|
||||
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire" "NoRepair" 1
|
||||
SectionEnd
|
||||
|
||||
Section "Add to PATH" SecPath
|
||||
ReadRegStr $0 HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment" "Path"
|
||||
StrCpy $0 "$0;$INSTDIR"
|
||||
WriteRegExpandStr HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment" "Path" "$0"
|
||||
SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000
|
||||
SectionEnd
|
||||
|
||||
Section "Start Menu Shortcut" SecStartMenu
|
||||
CreateDirectory "$SMPROGRAMS\Cagire"
|
||||
CreateShortCut "$SMPROGRAMS\Cagire\Cagire.lnk" "$INSTDIR\cagire-desktop.exe" "" "$INSTDIR\cagire-desktop.exe" 0
|
||||
CreateShortCut "$SMPROGRAMS\Cagire\Uninstall.lnk" "$INSTDIR\uninstall.exe"
|
||||
SectionEnd
|
||||
|
||||
!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
|
||||
!insertmacro MUI_DESCRIPTION_TEXT ${SecCore} "Installs Cagire CLI and Desktop binaries."
|
||||
!insertmacro MUI_DESCRIPTION_TEXT ${SecPath} "Add the install location to the PATH system environment variable."
|
||||
!insertmacro MUI_DESCRIPTION_TEXT ${SecStartMenu} "Add a Cagire shortcut to the Start Menu."
|
||||
!insertmacro MUI_FUNCTION_DESCRIPTION_END
|
||||
|
||||
Section "Uninstall"
|
||||
Delete "$INSTDIR\cagire.exe"
|
||||
Delete "$INSTDIR\cagire-desktop.exe"
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
RMDir "$INSTDIR"
|
||||
|
||||
Delete "$SMPROGRAMS\Cagire\Cagire.lnk"
|
||||
Delete "$SMPROGRAMS\Cagire\Uninstall.lnk"
|
||||
RMDir "$SMPROGRAMS\Cagire"
|
||||
|
||||
; Remove from PATH
|
||||
ReadRegStr $0 HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment" "Path"
|
||||
; Remove ";$INSTDIR" from the path string
|
||||
${WordReplace} $0 ";$INSTDIR" "" "+" $0
|
||||
WriteRegExpandStr HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment" "Path" "$0"
|
||||
SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000
|
||||
|
||||
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cagire"
|
||||
DeleteRegKey HKLM "Software\Cagire"
|
||||
SectionEnd
|
||||
BIN
nsis/header.bmp
Normal file
BIN
nsis/header.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
nsis/sidebar.bmp
Normal file
BIN
nsis/sidebar.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 151 KiB |
@@ -327,11 +327,20 @@ copy_artifacts() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# MSI installer for Windows targets
|
||||
if [[ "$os" == "windows" ]] && command -v cargo-wix &>/dev/null; then
|
||||
echo " Building MSI installer..."
|
||||
cargo wix --no-build --nocapture --package cagire -C -arch -C x64
|
||||
cp target/wix/*.msi "$OUT/" 2>/dev/null && echo " MSI -> $OUT/" || true
|
||||
# NSIS installer for Windows targets
|
||||
if [[ "$os" == "windows" ]] && command -v makensis &>/dev/null; then
|
||||
echo " Building NSIS installer..."
|
||||
local version
|
||||
version=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
|
||||
local abs_root
|
||||
abs_root=$(pwd)
|
||||
makensis -DVERSION="$version" \
|
||||
-DCLI_EXE="$abs_root/$rd/cagire.exe" \
|
||||
-DDESKTOP_EXE="$abs_root/$rd/cagire-desktop.exe" \
|
||||
-DICON="$abs_root/assets/Cagire.ico" \
|
||||
-DOUTDIR="$abs_root/$OUT" \
|
||||
nsis/cagire.nsi
|
||||
echo " Installer -> $OUT/cagire-${version}-windows-x86_64-setup.exe"
|
||||
fi
|
||||
|
||||
# AppImage for Linux targets
|
||||
|
||||
@@ -469,6 +469,13 @@ impl App {
|
||||
AppCommand::SavePrelude => self.save_prelude(),
|
||||
AppCommand::EvaluatePrelude => self.evaluate_prelude(link),
|
||||
AppCommand::ClosePreludeEditor => self.close_prelude_editor(),
|
||||
AppCommand::OpenBankPreludeEditor => self.open_bank_prelude_editor(),
|
||||
AppCommand::SaveBankPrelude => self.save_bank_prelude(),
|
||||
AppCommand::EvaluateBankPrelude => {
|
||||
let bank = self.editor_ctx.bank;
|
||||
self.evaluate_bank_prelude(bank, link);
|
||||
}
|
||||
AppCommand::CloseBankPreludeEditor => self.close_bank_prelude_editor(),
|
||||
|
||||
// Periodic script
|
||||
AppCommand::OpenScriptModal(field) => self.open_script_modal(field),
|
||||
|
||||
@@ -109,24 +109,101 @@ impl App {
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
|
||||
/// Evaluate the project prelude to seed variables and definitions.
|
||||
pub fn evaluate_prelude(&mut self, link: &LinkState) {
|
||||
let prelude = &self.project_state.project.prelude;
|
||||
/// Switch the editor to the current bank's prelude script.
|
||||
pub fn open_bank_prelude_editor(&mut self) {
|
||||
let bank = self.editor_ctx.bank;
|
||||
let prelude = &self.project_state.project.banks[bank].prelude;
|
||||
let lines: Vec<String> = if prelude.is_empty() {
|
||||
vec![String::new()]
|
||||
} else {
|
||||
prelude.lines().map(String::from).collect()
|
||||
};
|
||||
self.editor_ctx.editor.set_content(lines);
|
||||
self.editor_ctx.editor.set_candidates(Arc::clone(&COMPLETION_CANDIDATES));
|
||||
self.editor_ctx
|
||||
.editor
|
||||
.set_completion_enabled(self.ui.show_completion);
|
||||
let tree = SampleTree::from_paths(&self.audio.config.sample_paths);
|
||||
self.editor_ctx.editor.set_sample_folders(tree.all_folder_names());
|
||||
self.editor_ctx.target = EditorTarget::BankPrelude;
|
||||
self.ui.modal = Modal::Editor;
|
||||
}
|
||||
|
||||
pub fn save_bank_prelude(&mut self) {
|
||||
let bank = self.editor_ctx.bank;
|
||||
let text = self.editor_ctx.editor.content();
|
||||
self.project_state.project.banks[bank].prelude = text;
|
||||
}
|
||||
|
||||
pub fn close_bank_prelude_editor(&mut self) {
|
||||
self.editor_ctx.target = EditorTarget::Step;
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
|
||||
/// Evaluate a single bank's prelude.
|
||||
pub fn evaluate_bank_prelude(&mut self, bank: usize, link: &LinkState) {
|
||||
let prelude = &self.project_state.project.banks[bank].prelude;
|
||||
if prelude.trim().is_empty() {
|
||||
return;
|
||||
}
|
||||
let ctx = self.create_step_context(0, link);
|
||||
match self.script_engine.evaluate(prelude, &ctx) {
|
||||
Ok(_) => {
|
||||
self.ui.flash("Prelude evaluated", 150, FlashKind::Info);
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
self.ui
|
||||
.flash(&format!("Prelude error: {e}"), 300, FlashKind::Error);
|
||||
let fallback = format!("Bank {}", bank + 1);
|
||||
let bank_name = self.project_state.project.banks[bank]
|
||||
.name
|
||||
.as_deref()
|
||||
.unwrap_or(&fallback);
|
||||
self.ui.flash(
|
||||
&format!("{bank_name} prelude error: {e}"),
|
||||
300,
|
||||
FlashKind::Error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate the project prelude and all bank preludes.
|
||||
pub fn evaluate_prelude(&mut self, link: &LinkState) {
|
||||
let project_prelude = &self.project_state.project.prelude;
|
||||
if !project_prelude.trim().is_empty() {
|
||||
let ctx = self.create_step_context(0, link);
|
||||
match self.script_engine.evaluate(project_prelude, &ctx) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
self.ui
|
||||
.flash(&format!("Project prelude error: {e}"), 300, FlashKind::Error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
for bank_idx in 0..self.project_state.project.banks.len() {
|
||||
let prelude = &self.project_state.project.banks[bank_idx].prelude;
|
||||
if prelude.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
let ctx = self.create_step_context(0, link);
|
||||
match self.script_engine.evaluate(prelude, &ctx) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
let bank_name = self.project_state.project.banks[bank_idx]
|
||||
.name
|
||||
.as_deref()
|
||||
.map(String::from)
|
||||
.unwrap_or_else(|| format!("Bank {}", bank_idx + 1));
|
||||
self.ui.flash(
|
||||
&format!("{bank_name} prelude error: {e}"),
|
||||
300,
|
||||
FlashKind::Error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.ui.flash("Preludes evaluated", 150, FlashKind::Info);
|
||||
}
|
||||
|
||||
/// Evaluate a script and immediately send its audio commands.
|
||||
/// Returns collected `print` output, if any.
|
||||
pub fn execute_script_oneshot(
|
||||
|
||||
@@ -42,6 +42,11 @@ impl App {
|
||||
let data = self.project_state.project.pattern_at(*bank, *pattern).clone();
|
||||
Some(UndoScope::Pattern { bank: *bank, pattern: *pattern, data })
|
||||
}
|
||||
AppCommand::SaveBankPrelude => {
|
||||
let bank = self.editor_ctx.bank;
|
||||
let data = self.project_state.project.banks[bank].clone();
|
||||
Some(UndoScope::Bank { bank, data })
|
||||
}
|
||||
AppCommand::ResetBank { bank }
|
||||
| AppCommand::PasteBank { bank }
|
||||
| AppCommand::ImportBank { bank } => {
|
||||
|
||||
@@ -301,6 +301,10 @@ pub enum AppCommand {
|
||||
SavePrelude,
|
||||
EvaluatePrelude,
|
||||
ClosePreludeEditor,
|
||||
OpenBankPreludeEditor,
|
||||
SaveBankPrelude,
|
||||
EvaluateBankPrelude,
|
||||
CloseBankPreludeEditor,
|
||||
|
||||
// Onboarding
|
||||
DismissOnboarding,
|
||||
|
||||
@@ -82,7 +82,8 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
|
||||
KeyCode::Char(']') => ctx.dispatch(AppCommand::SpeedIncrease),
|
||||
KeyCode::Char('L') => ctx.dispatch(AppCommand::OpenPatternModal(PatternField::Length)),
|
||||
KeyCode::Char('S') => ctx.dispatch(AppCommand::OpenPatternModal(PatternField::Speed)),
|
||||
KeyCode::Char('p') => ctx.dispatch(AppCommand::OpenPreludeEditor),
|
||||
KeyCode::Char('p') => ctx.dispatch(AppCommand::OpenBankPreludeEditor),
|
||||
KeyCode::Char('P') => ctx.dispatch(AppCommand::OpenPreludeEditor),
|
||||
KeyCode::Delete | KeyCode::Backspace => {
|
||||
let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern);
|
||||
if let Some(range) = ctx.app.editor_ctx.selection_range() {
|
||||
|
||||
@@ -351,6 +351,11 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
ctx.dispatch(AppCommand::EvaluatePrelude);
|
||||
ctx.dispatch(AppCommand::ClosePreludeEditor);
|
||||
}
|
||||
EditorTarget::BankPrelude => {
|
||||
ctx.dispatch(AppCommand::SaveBankPrelude);
|
||||
ctx.dispatch(AppCommand::EvaluateBankPrelude);
|
||||
ctx.dispatch(AppCommand::CloseBankPreludeEditor);
|
||||
}
|
||||
}
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
@@ -364,6 +369,10 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
ctx.dispatch(AppCommand::SavePrelude);
|
||||
ctx.dispatch(AppCommand::EvaluatePrelude);
|
||||
}
|
||||
EditorTarget::BankPrelude => {
|
||||
ctx.dispatch(AppCommand::SaveBankPrelude);
|
||||
ctx.dispatch(AppCommand::EvaluateBankPrelude);
|
||||
}
|
||||
},
|
||||
KeyCode::Char('b') if ctrl => {
|
||||
editor.activate_sample_finder();
|
||||
|
||||
@@ -941,6 +941,11 @@ fn handle_modal_click(ctx: &mut InputContext, col: u16, row: u16, term: Rect) {
|
||||
ctx.dispatch(AppCommand::EvaluatePrelude);
|
||||
ctx.dispatch(AppCommand::ClosePreludeEditor);
|
||||
}
|
||||
EditorTarget::BankPrelude => {
|
||||
ctx.dispatch(AppCommand::SaveBankPrelude);
|
||||
ctx.dispatch(AppCommand::EvaluateBankPrelude);
|
||||
ctx.dispatch(AppCommand::CloseBankPreludeEditor);
|
||||
}
|
||||
}
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ pub const DOCS: &[DocEntry] = &[
|
||||
),
|
||||
Topic("Brackets", include_str!("../../docs/forth/brackets.md")),
|
||||
Topic("Cycling", include_str!("../../docs/forth/cycling.md")),
|
||||
Topic("The Prelude", include_str!("../../docs/forth/prelude.md")),
|
||||
Topic("Preludes", include_str!("../../docs/forth/prelude.md")),
|
||||
Topic(
|
||||
"Cagire vs Classic",
|
||||
include_str!("../../docs/forth/oddities.md"),
|
||||
|
||||
@@ -766,17 +766,26 @@ pub static COMMANDS: LazyLock<Vec<CommandEntry>> = LazyLock::new(|| {
|
||||
|
||||
// === Prelude ===
|
||||
CommandEntry {
|
||||
name: "Edit Prelude",
|
||||
description: "Open prelude editor",
|
||||
name: "Edit Bank Prelude",
|
||||
description: "Open current bank's prelude editor",
|
||||
category: "Prelude",
|
||||
keybinding: "p",
|
||||
pages: &[Main],
|
||||
normal_mode: false,
|
||||
action: Some(PaletteAction::Resolve(|_| Some(AppCommand::OpenBankPreludeEditor))),
|
||||
},
|
||||
CommandEntry {
|
||||
name: "Edit Project Prelude",
|
||||
description: "Open project-wide prelude editor",
|
||||
category: "Prelude",
|
||||
keybinding: "P",
|
||||
pages: &[Main],
|
||||
normal_mode: false,
|
||||
action: Some(PaletteAction::Resolve(|_| Some(AppCommand::OpenPreludeEditor))),
|
||||
},
|
||||
CommandEntry {
|
||||
name: "Evaluate Prelude",
|
||||
description: "Re-evaluate prelude script",
|
||||
name: "Evaluate All Preludes",
|
||||
description: "Re-evaluate project and all bank preludes",
|
||||
category: "Prelude",
|
||||
keybinding: "d",
|
||||
pages: &[Main],
|
||||
|
||||
@@ -8,6 +8,7 @@ pub enum EditorTarget {
|
||||
#[default]
|
||||
Step,
|
||||
Prelude,
|
||||
BankPrelude,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
|
||||
@@ -76,7 +76,11 @@ fn render_top_layout(
|
||||
}
|
||||
if has_preview {
|
||||
let user_words: HashSet<String> = app.dict.lock().keys().cloned().collect();
|
||||
let has_prelude = !app.project_state.project.prelude.trim().is_empty();
|
||||
let has_prelude = !app.project_state.project.prelude.trim().is_empty()
|
||||
|| !app.project_state.project.banks[app.editor_ctx.bank]
|
||||
.prelude
|
||||
.trim()
|
||||
.is_empty();
|
||||
if has_prelude {
|
||||
let [script_area, prelude_area] =
|
||||
Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)]).areas(areas[idx]);
|
||||
@@ -195,7 +199,11 @@ fn render_viz_area(
|
||||
VizPanel::Lissajous => render_lissajous(frame, app, *panel_area),
|
||||
VizPanel::Preview => {
|
||||
let user_words = user_words_once.as_ref().expect("user_words initialized");
|
||||
let has_prelude = !app.project_state.project.prelude.trim().is_empty();
|
||||
let has_prelude = !app.project_state.project.prelude.trim().is_empty()
|
||||
|| !app.project_state.project.banks[app.editor_ctx.bank]
|
||||
.prelude
|
||||
.trim()
|
||||
.is_empty();
|
||||
if has_prelude {
|
||||
let [script_area, prelude_area] = if is_vertical_layout {
|
||||
Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)])
|
||||
@@ -655,11 +663,20 @@ pub(crate) fn render_prelude_preview(
|
||||
area: Rect,
|
||||
) {
|
||||
let theme = theme::get();
|
||||
let prelude = &app.project_state.project.prelude;
|
||||
let bank_prelude = &app.project_state.project.banks[app.editor_ctx.bank].prelude;
|
||||
let (prelude, title) = if !bank_prelude.trim().is_empty() {
|
||||
let bank_name = app.project_state.project.banks[app.editor_ctx.bank]
|
||||
.name
|
||||
.as_deref()
|
||||
.unwrap_or("Bank");
|
||||
(bank_prelude.as_str(), format!(" {bank_name} Prelude "))
|
||||
} else {
|
||||
(app.project_state.project.prelude.as_str(), " Prelude ".to_string())
|
||||
};
|
||||
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(" Prelude ")
|
||||
.title(title)
|
||||
.border_style(Style::new().fg(theme.ui.border));
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
@@ -911,7 +911,13 @@ fn render_modal_editor(
|
||||
};
|
||||
|
||||
let title = match app.editor_ctx.target {
|
||||
EditorTarget::Prelude => "Prelude".to_string(),
|
||||
EditorTarget::Prelude => "Project Prelude".to_string(),
|
||||
EditorTarget::BankPrelude => {
|
||||
let bank = &app.project_state.project.banks[app.editor_ctx.bank];
|
||||
let fallback = format!("Bank {}", app.editor_ctx.bank + 1);
|
||||
let bank_name = bank.name.as_deref().unwrap_or(&fallback);
|
||||
format!("{bank_name} Prelude")
|
||||
}
|
||||
EditorTarget::Step => {
|
||||
let step_num = app.editor_ctx.step + 1;
|
||||
let step = app.current_edit_pattern().step(app.editor_ctx.step);
|
||||
|
||||
@@ -114,7 +114,11 @@ fn render_sidebar(frame: &mut Frame, app: &App, area: Rect) {
|
||||
if app.audio.config.show_lissajous {
|
||||
constraints.push(Constraint::Fill(1));
|
||||
}
|
||||
let has_prelude = !app.project_state.project.prelude.trim().is_empty();
|
||||
let has_prelude = !app.project_state.project.prelude.trim().is_empty()
|
||||
|| !app.project_state.project.banks[app.editor_ctx.bank]
|
||||
.prelude
|
||||
.trim()
|
||||
.is_empty();
|
||||
if has_prelude {
|
||||
constraints.push(Constraint::Fill(1));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user