Compare commits
5 Commits
3dba8213d6
...
5310b98542
| Author | SHA1 | Date | |
|---|---|---|---|
| 5310b98542 | |||
| 7099501130 | |||
| 730ddfb716 | |||
| 0a186f774c | |||
| 25a5c77344 |
@@ -1,5 +1,10 @@
|
|||||||
|
# Uncomment to use local doux for development
|
||||||
|
paths = ["/Users/bubo/doux"]
|
||||||
|
|
||||||
[alias]
|
[alias]
|
||||||
xtask = "run --package xtask --release --"
|
xtask = "run --package xtask --release --"
|
||||||
|
|
||||||
[target.x86_64-pc-windows-gnu]
|
[target.x86_64-pc-windows-gnu]
|
||||||
rustflags = ["-C", "link-args=-lstdc++ -lws2_32 -liphlpapi -lwinmm"]
|
rustflags = [
|
||||||
|
"-C", "link-args=-Wl,-Bstatic -lstdc++ -lgcc -lgcc_eh -lpthread -Wl,-Bdynamic -lmingwex -lmsvcrt -lws2_32 -liphlpapi -lwinmm -lole32 -loleaut32 -luuid -lkernel32",
|
||||||
|
]
|
||||||
|
|||||||
39
.github/workflows/release.yml
vendored
39
.github/workflows/release.yml
vendored
@@ -141,6 +141,21 @@ jobs:
|
|||||||
name: ${{ matrix.artifact }}-desktop
|
name: ${{ matrix.artifact }}-desktop
|
||||||
path: target/${{ matrix.target }}/release/cagire-desktop.exe
|
path: target/${{ matrix.target }}/release/cagire-desktop.exe
|
||||||
|
|
||||||
|
- name: Install cargo-wix (Windows)
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
run: cargo install cargo-wix
|
||||||
|
|
||||||
|
- name: Build MSI installer (Windows)
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
run: cargo wix --no-build --nocapture -C -p -C x64
|
||||||
|
|
||||||
|
- name: Upload MSI installer (Windows)
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.artifact }}-msi
|
||||||
|
path: target/wix/*.msi
|
||||||
|
|
||||||
- name: Upload CLAP artifact
|
- name: Upload CLAP artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -266,6 +281,18 @@ jobs:
|
|||||||
-output cagire-plugins.vst3/Contents/MacOS/cagire-plugins
|
-output cagire-plugins.vst3/Contents/MacOS/cagire-plugins
|
||||||
lipo -info cagire-plugins.vst3/Contents/MacOS/cagire-plugins
|
lipo -info cagire-plugins.vst3/Contents/MacOS/cagire-plugins
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
sparse-checkout: |
|
||||||
|
assets/DMG-README.txt
|
||||||
|
scripts/make-dmg.sh
|
||||||
|
clean: false
|
||||||
|
|
||||||
|
- name: Create DMG
|
||||||
|
run: |
|
||||||
|
chmod +x scripts/make-dmg.sh
|
||||||
|
scripts/make-dmg.sh Cagire.app .
|
||||||
|
|
||||||
- name: Build .pkg installer
|
- name: Build .pkg installer
|
||||||
run: |
|
run: |
|
||||||
VERSION="${GITHUB_REF_NAME#v}"
|
VERSION="${GITHUB_REF_NAME#v}"
|
||||||
@@ -303,6 +330,12 @@ jobs:
|
|||||||
name: cagire-macos-universal-vst3
|
name: cagire-macos-universal-vst3
|
||||||
path: cagire-plugins.vst3
|
path: cagire-plugins.vst3
|
||||||
|
|
||||||
|
- name: Upload DMG
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: cagire-macos-universal-dmg
|
||||||
|
path: Cagire-*.dmg
|
||||||
|
|
||||||
- name: Upload .pkg installer
|
- name: Upload .pkg installer
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -328,7 +361,9 @@ jobs:
|
|||||||
mkdir -p release
|
mkdir -p release
|
||||||
for dir in artifacts/*/; do
|
for dir in artifacts/*/; do
|
||||||
name=$(basename "$dir")
|
name=$(basename "$dir")
|
||||||
if [[ "$name" == "cagire-macos-universal-pkg" ]]; then
|
if [[ "$name" == "cagire-macos-universal-dmg" ]]; then
|
||||||
|
cp "$dir"/*.dmg release/
|
||||||
|
elif [[ "$name" == "cagire-macos-universal-pkg" ]]; then
|
||||||
cp "$dir"/*.pkg release/
|
cp "$dir"/*.pkg release/
|
||||||
elif [[ "$name" == "cagire-macos-universal-desktop" ]]; then
|
elif [[ "$name" == "cagire-macos-universal-desktop" ]]; then
|
||||||
cp "$dir/Cagire.app.zip" "release/cagire-macos-universal-desktop.app.zip"
|
cp "$dir/Cagire.app.zip" "release/cagire-macos-universal-desktop.app.zip"
|
||||||
@@ -344,6 +379,8 @@ jobs:
|
|||||||
elif [[ "$name" == *-vst3 ]]; then
|
elif [[ "$name" == *-vst3 ]]; then
|
||||||
base="${name%-vst3}"
|
base="${name%-vst3}"
|
||||||
cd "$dir" && zip -r "../../release/${base}-vst3.zip" cagire-plugins.vst3 && cd ../..
|
cd "$dir" && zip -r "../../release/${base}-vst3.zip" cagire-plugins.vst3 && cd ../..
|
||||||
|
elif [[ "$name" == *-msi ]]; then
|
||||||
|
cp "$dir"/*.msi release/
|
||||||
elif [[ "$name" == *-appimage ]]; then
|
elif [[ "$name" == *-appimage ]]; then
|
||||||
cp "$dir"/*.AppImage release/
|
cp "$dir"/*.AppImage release/
|
||||||
elif [[ "$name" == *-desktop ]]; then
|
elif [[ "$name" == *-desktop ]]; then
|
||||||
|
|||||||
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -1809,8 +1809,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "doux"
|
name = "doux"
|
||||||
version = "0.0.4"
|
version = "0.0.5"
|
||||||
source = "git+https://github.com/sova-org/doux#ba475601a448f36d4755eb1fdcaa629987485892"
|
source = "git+https://github.com/sova-org/doux#886702b4fe937d26ed681a2f6d7626d26d6890d0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"clap",
|
"clap",
|
||||||
|
|||||||
19
assets/DMG-README.txt
Normal file
19
assets/DMG-README.txt
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
Cagire - A Forth-based music sequencer
|
||||||
|
Made by BuboBubo and his friends
|
||||||
|
======================================
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
Drag Cagire.app into the Applications folder.
|
||||||
|
|
||||||
|
Unquarantine
|
||||||
|
------------
|
||||||
|
Since this app is not signed with an Apple Developer certificate,
|
||||||
|
macOS will block it from running. To fix this, open Terminal and run:
|
||||||
|
|
||||||
|
xattr -cr /Applications/Cagire.app
|
||||||
|
|
||||||
|
Support
|
||||||
|
-------
|
||||||
|
If you enjoy Cagire, consider supporting development:
|
||||||
|
https://ko-fi.com/raphaelbubo
|
||||||
7
build.rs
7
build.rs
@@ -4,10 +4,13 @@ fn main() {
|
|||||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||||
|
|
||||||
if target_os == "windows" {
|
if target_os == "windows" {
|
||||||
// rusty_link's build.rs only links stdc++ on linux — add it for windows too
|
// C++ runtime (stdc++, gcc, gcc_eh, pthread) linked statically via .cargo/config.toml
|
||||||
println!("cargo:rustc-link-lib=stdc++");
|
// using -Wl,-Bstatic. Only Windows system DLLs go here.
|
||||||
println!("cargo:rustc-link-lib=ws2_32");
|
println!("cargo:rustc-link-lib=ws2_32");
|
||||||
println!("cargo:rustc-link-lib=iphlpapi");
|
println!("cargo:rustc-link-lib=iphlpapi");
|
||||||
|
println!("cargo:rustc-link-lib=winmm");
|
||||||
|
println!("cargo:rustc-link-lib=ole32");
|
||||||
|
println!("cargo:rustc-link-lib=oleaut32");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
|||||||
"nand" => Op::Nand,
|
"nand" => Op::Nand,
|
||||||
"nor" => Op::Nor,
|
"nor" => Op::Nor,
|
||||||
"ifelse" => Op::IfElse,
|
"ifelse" => Op::IfElse,
|
||||||
"pick" => Op::Pick,
|
"select" => Op::Pick,
|
||||||
"sound" => Op::NewCmd,
|
"sound" => Op::NewCmd,
|
||||||
"." => Op::Emit,
|
"." => Op::Emit,
|
||||||
"rand" => Op::Rand(None),
|
"rand" => Op::Rand(None),
|
||||||
|
|||||||
@@ -507,12 +507,12 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
Word {
|
Word {
|
||||||
name: "pick",
|
name: "select",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
category: "Logic",
|
category: "Logic",
|
||||||
stack: "(..quots n --)",
|
stack: "(..quots n --)",
|
||||||
desc: "Execute nth quotation (0-indexed)",
|
desc: "Execute nth quotation (0-indexed)",
|
||||||
example: "{ 1 } { 2 } { 3 } 2 pick => 3",
|
example: "{ 1 } { 2 } { 3 } 2 select => 3",
|
||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: true,
|
varargs: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -186,6 +186,26 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Param,
|
compile: Param,
|
||||||
varargs: true,
|
varargs: true,
|
||||||
},
|
},
|
||||||
|
Word {
|
||||||
|
name: "slice",
|
||||||
|
aliases: &[],
|
||||||
|
category: "Sample",
|
||||||
|
stack: "(v.. --)",
|
||||||
|
desc: "Divide sample into N equal slices",
|
||||||
|
example: r#""break" s 8 slice 3 pick ."#,
|
||||||
|
compile: Param,
|
||||||
|
varargs: true,
|
||||||
|
},
|
||||||
|
Word {
|
||||||
|
name: "pick",
|
||||||
|
aliases: &[],
|
||||||
|
category: "Sample",
|
||||||
|
stack: "(v.. --)",
|
||||||
|
desc: "Select which slice to play (0-indexed, wraps)",
|
||||||
|
example: r#""break" s 8 slice 3 pick ."#,
|
||||||
|
compile: Param,
|
||||||
|
varargs: true,
|
||||||
|
},
|
||||||
Word {
|
Word {
|
||||||
name: "voice",
|
name: "voice",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ snare sound 0.5 speed . ( play snare at half speed )
|
|||||||
| `n` | 0+ | Sample index within a folder (wraps around) |
|
| `n` | 0+ | Sample index within a folder (wraps around) |
|
||||||
| `begin` | 0-1 | Playback start position |
|
| `begin` | 0-1 | Playback start position |
|
||||||
| `end` | 0-1 | Playback end position |
|
| `end` | 0-1 | Playback end position |
|
||||||
|
| `slice` | 1+ | Divide sample into N equal slices |
|
||||||
|
| `pick` | 0+ | Select which slice to play (0-indexed, wraps) |
|
||||||
| `speed` | any | Playback speed multiplier |
|
| `speed` | any | Playback speed multiplier |
|
||||||
| `freq` | Hz | Base frequency for pitch tracking |
|
| `freq` | Hz | Base frequency for pitch tracking |
|
||||||
| `fit` | seconds | Stretch/compress sample to fit duration |
|
| `fit` | seconds | Stretch/compress sample to fit duration |
|
||||||
@@ -62,6 +64,21 @@ kick sound 0.5 end . ( play first half )
|
|||||||
|
|
||||||
If begin is greater than end, they swap automatically.
|
If begin is greater than end, they swap automatically.
|
||||||
|
|
||||||
|
## Slice and Pick
|
||||||
|
|
||||||
|
For evenly-spaced slicing, `slice` divides the sample into N equal parts and `pick` selects which one (0-indexed, wraps around).
|
||||||
|
|
||||||
|
```forth
|
||||||
|
break sound 8 slice 3 pick . ( play the 4th eighth of the sample )
|
||||||
|
break sound 16 slice step pick . ( scan through 16 slices by step )
|
||||||
|
```
|
||||||
|
|
||||||
|
Combine with `fit` to time-stretch each slice to a target duration. `fit` accounts for the sliced range automatically.
|
||||||
|
|
||||||
|
```forth
|
||||||
|
break sound 4 slice 2 pick 1 loop . ( quarter of the sample, fitted to 1 beat )
|
||||||
|
```
|
||||||
|
|
||||||
## Speed and Pitch
|
## Speed and Pitch
|
||||||
|
|
||||||
The `speed` parameter affects both tempo and pitch. A speed of 2 plays twice as fast and an octave higher.
|
The `speed` parameter affects both tempo and pitch. A speed of 2 plays twice as fast and an octave higher.
|
||||||
|
|||||||
@@ -63,12 +63,12 @@ Reads naturally: "c3 or c4, depending on the coin."
|
|||||||
tri s c4 note 0.2 decay . ;; loud during fills, quiet otherwise
|
tri s c4 note 0.2 decay . ;; loud during fills, quiet otherwise
|
||||||
```
|
```
|
||||||
|
|
||||||
## pick
|
## select
|
||||||
|
|
||||||
Choose the nth option from a list of quotations:
|
Choose the nth option from a list of quotations:
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
{ c4 } { e4 } { g4 } { b4 } iter 4 mod pick
|
{ c4 } { e4 } { g4 } { b4 } iter 4 mod select
|
||||||
note sine s 0.5 decay .
|
note sine s 0.5 decay .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -229,6 +229,14 @@ bundle_plugins_native() {
|
|||||||
cargo xtask bundle "$PLUGIN_NAME" --release $tf
|
cargo xtask bundle "$PLUGIN_NAME" --release $tf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bundle_desktop_native() {
|
||||||
|
local platform="$1"
|
||||||
|
local tf
|
||||||
|
tf=$(target_flag "$platform")
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
cargo bundle --release --features desktop --bin cagire-desktop $tf
|
||||||
|
}
|
||||||
|
|
||||||
bundle_plugins_cross() {
|
bundle_plugins_cross() {
|
||||||
local platform="$1"
|
local platform="$1"
|
||||||
local rd
|
local rd
|
||||||
@@ -300,6 +308,25 @@ copy_artifacts() {
|
|||||||
local dst="$OUT/cagire-desktop-${os}-${arch}${suffix}"
|
local dst="$OUT/cagire-desktop-${os}-${arch}${suffix}"
|
||||||
cp "$src" "$dst"
|
cp "$src" "$dst"
|
||||||
echo " cagire-desktop -> $dst"
|
echo " cagire-desktop -> $dst"
|
||||||
|
|
||||||
|
# macOS .app bundle
|
||||||
|
if [[ "$os" == "macos" ]]; then
|
||||||
|
local app_src="$rd/bundle/osx/Cagire.app"
|
||||||
|
if [[ -d "$app_src" ]]; then
|
||||||
|
local app_dst="$OUT/Cagire-${arch}.app"
|
||||||
|
rm -rf "$app_dst"
|
||||||
|
cp -R "$app_src" "$app_dst"
|
||||||
|
echo " Cagire.app -> $app_dst"
|
||||||
|
scripts/make-dmg.sh "$app_dst" "$OUT"
|
||||||
|
fi
|
||||||
|
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 -C -p -C x64
|
||||||
|
cp target/wix/*.msi "$OUT/" 2>/dev/null && echo " MSI -> $OUT/" || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# AppImage for Linux targets
|
# AppImage for Linux targets
|
||||||
@@ -407,6 +434,10 @@ for platform in "${selected_platforms[@]}"; do
|
|||||||
if $build_desktop; then
|
if $build_desktop; then
|
||||||
echo " -> cagire-desktop"
|
echo " -> cagire-desktop"
|
||||||
build_binary "$platform" --features desktop --bin cagire-desktop
|
build_binary "$platform" --features desktop --bin cagire-desktop
|
||||||
|
if ! is_cross_target "$platform"; then
|
||||||
|
echo " -> bundling cagire-desktop .app"
|
||||||
|
bundle_desktop_native "$platform"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if $build_plugins; then
|
if $build_plugins; then
|
||||||
|
|||||||
@@ -7,7 +7,13 @@ RUN apt-get update && \
|
|||||||
libclang-dev \
|
libclang-dev \
|
||||||
mingw-w64-tools \
|
mingw-w64-tools \
|
||||||
mingw-w64-x86-64-dev \
|
mingw-w64-x86-64-dev \
|
||||||
|
g++-mingw-w64-x86-64 \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& ln -sf windows.h /usr/x86_64-w64-mingw32/include/Windows.h \
|
&& ln -sf windows.h /usr/x86_64-w64-mingw32/include/Windows.h \
|
||||||
&& ln -sf winsock2.h /usr/x86_64-w64-mingw32/include/WinSock2.h \
|
&& ln -sf winsock2.h /usr/x86_64-w64-mingw32/include/WinSock2.h \
|
||||||
&& ln -sf ws2tcpip.h /usr/x86_64-w64-mingw32/include/WS2tcpip.h
|
&& ln -sf ws2tcpip.h /usr/x86_64-w64-mingw32/include/WS2tcpip.h \
|
||||||
|
&& GCCDIR=$(ls -d /usr/lib/gcc/x86_64-w64-mingw32/*-posix 2>/dev/null | head -1) \
|
||||||
|
&& ln -sf "$GCCDIR/libstdc++.a" /usr/x86_64-w64-mingw32/lib/libstdc++.a \
|
||||||
|
&& ln -sf "$GCCDIR/libgcc.a" /usr/x86_64-w64-mingw32/lib/libgcc.a \
|
||||||
|
&& rm -f /usr/x86_64-w64-mingw32/lib/libstdc++.dll.a \
|
||||||
|
&& rm -f /usr/lib/gcc/x86_64-w64-mingw32/*/libstdc++.dll.a
|
||||||
|
|||||||
52
scripts/make-dmg.sh
Executable file
52
scripts/make-dmg.sh
Executable file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Usage: scripts/make-dmg.sh <app-path> <output-dir>
|
||||||
|
# Produces a .dmg from a macOS .app bundle using only hdiutil.
|
||||||
|
|
||||||
|
if [[ $# -ne 2 ]]; then
|
||||||
|
echo "Usage: $0 <app-path> <output-dir>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
APP_PATH="$1"
|
||||||
|
OUTDIR="$2"
|
||||||
|
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||||
|
|
||||||
|
if [[ ! -d "$APP_PATH" ]]; then
|
||||||
|
echo "ERROR: $APP_PATH is not a directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
LIPO_OUTPUT=$(lipo -info "$APP_PATH/Contents/MacOS/cagire-desktop" 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ -z "$LIPO_OUTPUT" ]]; then
|
||||||
|
echo "ERROR: could not determine architecture from $APP_PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if echo "$LIPO_OUTPUT" | grep -q "Architectures in the fat file"; then
|
||||||
|
ARCH="universal"
|
||||||
|
else
|
||||||
|
ARCH=$(echo "$LIPO_OUTPUT" | awk '{print $NF}')
|
||||||
|
case "$ARCH" in
|
||||||
|
arm64) ARCH="aarch64" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
STAGING="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$STAGING"' EXIT
|
||||||
|
|
||||||
|
cp -R "$APP_PATH" "$STAGING/Cagire.app"
|
||||||
|
ln -s /Applications "$STAGING/Applications"
|
||||||
|
cp "$REPO_ROOT/assets/DMG-README.txt" "$STAGING/README.txt"
|
||||||
|
|
||||||
|
DMG_NAME="Cagire-${ARCH}.dmg"
|
||||||
|
mkdir -p "$OUTDIR"
|
||||||
|
|
||||||
|
hdiutil create -volname "Cagire" \
|
||||||
|
-srcfolder "$STAGING" \
|
||||||
|
-ov -format UDZO \
|
||||||
|
"$OUTDIR/$DMG_NAME"
|
||||||
|
|
||||||
|
echo " DMG -> $OUTDIR/$DMG_NAME"
|
||||||
@@ -394,6 +394,7 @@ impl eframe::App for CagireDesktop {
|
|||||||
|
|
||||||
self.app.flush_queued_changes(&sequencer.cmd_tx);
|
self.app.flush_queued_changes(&sequencer.cmd_tx);
|
||||||
self.app.flush_dirty_patterns(&sequencer.cmd_tx);
|
self.app.flush_dirty_patterns(&sequencer.cmd_tx);
|
||||||
|
self.app.flush_dirty_script(&sequencer.cmd_tx);
|
||||||
|
|
||||||
while let Ok(midi_cmd) = self.midi_rx.try_recv() {
|
while let Ok(midi_cmd) = self.midi_rx.try_recv() {
|
||||||
match midi_cmd {
|
match midi_cmd {
|
||||||
|
|||||||
@@ -1070,7 +1070,7 @@ impl SequencerState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let script_frontier = if self.script_frontier < 0.0 {
|
let script_frontier = if self.script_frontier < 0.0 {
|
||||||
frontier.max(0.0)
|
frontier
|
||||||
} else {
|
} else {
|
||||||
self.script_frontier
|
self.script_frontier
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ use ratatui::layout::{Constraint, Layout, Rect};
|
|||||||
use crate::commands::AppCommand;
|
use crate::commands::AppCommand;
|
||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
DeviceKind, DictFocus, EngineSection, HelpFocus, MinimapMode, Modal, OptionsFocus,
|
DeviceKind, DictFocus, EditorTarget, EngineSection, HelpFocus, MinimapMode, Modal,
|
||||||
PatternsColumn, SettingKind,
|
OptionsFocus, PatternsColumn, SettingKind,
|
||||||
};
|
};
|
||||||
use crate::views::{dict_view, engine_view, help_view, main_view, patterns_view, script_view};
|
use crate::views::{dict_view, engine_view, help_view, main_view, patterns_view, script_view};
|
||||||
|
|
||||||
@@ -94,14 +94,22 @@ fn handle_editor_drag(ctx: &mut InputContext, col: u16, row: u16, term: Rect) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_editor_mouse(ctx: &mut InputContext, col: u16, row: u16, term: Rect, dragging: bool) {
|
fn editor_modal_rect(term: Rect) -> Rect {
|
||||||
// Reconstruct editor area (mirrors render_modal_editor / ModalFrame::render_centered)
|
|
||||||
let width = (term.width * 80 / 100).max(40);
|
let width = (term.width * 80 / 100).max(40);
|
||||||
let height = (term.height * 60 / 100).max(10);
|
let height = (term.height * 60 / 100).max(10);
|
||||||
let modal_w = width.min(term.width.saturating_sub(4));
|
let modal_w = width.min(term.width.saturating_sub(4));
|
||||||
let modal_h = height.min(term.height.saturating_sub(4));
|
let modal_h = height.min(term.height.saturating_sub(4));
|
||||||
let mx = term.x + (term.width.saturating_sub(modal_w)) / 2;
|
let mx = term.x + (term.width.saturating_sub(modal_w)) / 2;
|
||||||
let my = term.y + (term.height.saturating_sub(modal_h)) / 2;
|
let my = term.y + (term.height.saturating_sub(modal_h)) / 2;
|
||||||
|
Rect::new(mx, my, modal_w, modal_h)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_editor_mouse(ctx: &mut InputContext, col: u16, row: u16, term: Rect, dragging: bool) {
|
||||||
|
let modal = editor_modal_rect(term);
|
||||||
|
let mx = modal.x;
|
||||||
|
let my = modal.y;
|
||||||
|
let modal_w = modal.width;
|
||||||
|
let modal_h = modal.height;
|
||||||
// inner = area inside 1-cell border
|
// inner = area inside 1-cell border
|
||||||
let inner_x = mx + 1;
|
let inner_x = mx + 1;
|
||||||
let inner_y = my + 1;
|
let inner_y = my + 1;
|
||||||
@@ -180,6 +188,18 @@ fn handle_scroll(ctx: &mut InputContext, col: u16, row: u16, term: Rect, up: boo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if matches!(ctx.app.ui.modal, Modal::Editor) {
|
||||||
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
|
let code = if up { KeyCode::Up } else { KeyCode::Down };
|
||||||
|
for _ in 0..3 {
|
||||||
|
ctx.app
|
||||||
|
.editor_ctx
|
||||||
|
.editor
|
||||||
|
.input(KeyEvent::new(code, KeyModifiers::empty()));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if !matches!(ctx.app.ui.modal, Modal::None) {
|
if !matches!(ctx.app.ui.modal, Modal::None) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1057,13 +1077,28 @@ fn handle_engine_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect, k
|
|||||||
fn handle_modal_click(ctx: &mut InputContext, col: u16, row: u16, term: Rect) {
|
fn handle_modal_click(ctx: &mut InputContext, col: u16, row: u16, term: Rect) {
|
||||||
match &ctx.app.ui.modal {
|
match &ctx.app.ui.modal {
|
||||||
Modal::Editor => {
|
Modal::Editor => {
|
||||||
|
let modal_area = editor_modal_rect(term);
|
||||||
|
if contains(modal_area, col, row) {
|
||||||
handle_editor_mouse(ctx, col, row, term, false);
|
handle_editor_mouse(ctx, col, row, term, false);
|
||||||
|
} else {
|
||||||
|
match ctx.app.editor_ctx.target {
|
||||||
|
EditorTarget::Step => {
|
||||||
|
ctx.dispatch(AppCommand::SaveEditorToStep);
|
||||||
|
ctx.dispatch(AppCommand::CompileCurrentStep);
|
||||||
|
}
|
||||||
|
EditorTarget::Prelude => {
|
||||||
|
ctx.dispatch(AppCommand::SavePrelude);
|
||||||
|
ctx.dispatch(AppCommand::EvaluatePrelude);
|
||||||
|
ctx.dispatch(AppCommand::ClosePreludeEditor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.dispatch(AppCommand::CloseModal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Modal::Confirm { .. } => {
|
Modal::Confirm { .. } => {
|
||||||
handle_confirm_click(ctx, col, row, term);
|
handle_confirm_click(ctx, col, row, term);
|
||||||
}
|
}
|
||||||
Modal::KeybindingsHelp { .. } => {
|
Modal::KeybindingsHelp { .. } => {
|
||||||
// Click outside keybindings help to dismiss
|
|
||||||
let padded = padded(term);
|
let padded = padded(term);
|
||||||
let width = (padded.width * 80 / 100).clamp(60, 100);
|
let width = (padded.width * 80 / 100).clamp(60, 100);
|
||||||
let height = (padded.height * 80 / 100).max(15);
|
let height = (padded.height * 80 / 100).max(15);
|
||||||
@@ -1073,7 +1108,20 @@ fn handle_modal_click(ctx: &mut InputContext, col: u16, row: u16, term: Rect) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// For other modals, don't dismiss on click (they have their own input)
|
let (w, h) = match &ctx.app.ui.modal {
|
||||||
|
Modal::PatternProps { .. } => (50, 18),
|
||||||
|
Modal::EuclideanDistribution { .. } => (50, 11),
|
||||||
|
Modal::Onboarding { .. } => (57, 20),
|
||||||
|
Modal::FileBrowser(_) | Modal::AddSamplePath(_) => (60, 18),
|
||||||
|
Modal::Rename { .. } => (40, 5),
|
||||||
|
Modal::SetPattern { .. } | Modal::SetScript { .. } => (45, 5),
|
||||||
|
Modal::SetTempo(_) | Modal::JumpToStep(_) => (30, 5),
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
let modal_area = centered_rect(term, w, h);
|
||||||
|
if !contains(modal_area, col, row) {
|
||||||
|
ctx.dispatch(AppCommand::CloseModal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,21 +196,21 @@ fn ifelse_false() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pick_first() {
|
fn select_first() {
|
||||||
expect_int("{ 10 } { 20 } { 30 } 0 pick", 10);
|
expect_int("{ 10 } { 20 } { 30 } 0 select", 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pick_second() {
|
fn select_second() {
|
||||||
expect_int("{ 10 } { 20 } { 30 } 1 pick", 20);
|
expect_int("{ 10 } { 20 } { 30 } 1 select", 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pick_third() {
|
fn select_third() {
|
||||||
expect_int("{ 10 } { 20 } { 30 } 2 pick", 30);
|
expect_int("{ 10 } { 20 } { 30 } 2 select", 30);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pick_preserves_stack() {
|
fn select_preserves_stack() {
|
||||||
expect_int("5 { 10 } { 20 } 0 pick +", 15);
|
expect_int("5 { 10 } { 20 } 0 select +", 15);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,3 +255,16 @@ fn all_replaces_previous_global() {
|
|||||||
assert_eq!(outputs.len(), 1);
|
assert_eq!(outputs.len(), 1);
|
||||||
assert!(outputs[0].contains("lpf/2000"), "latest lpf should be 2000: {}", outputs[0]);
|
assert!(outputs[0].contains("lpf/2000"), "latest lpf should be 2000: {}", outputs[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn slice_param() {
|
||||||
|
let outputs = expect_outputs(r#""break" s 8 slice ."#, 1);
|
||||||
|
assert!(outputs[0].contains("slice/8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pick_param() {
|
||||||
|
let outputs = expect_outputs(r#""break" s 8 slice 3 pick ."#, 1);
|
||||||
|
assert!(outputs[0].contains("slice/8"));
|
||||||
|
assert!(outputs[0].contains("pick/3"));
|
||||||
|
}
|
||||||
|
|||||||
20
wix/License.rtf
Normal file
20
wix/License.rtf
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{\rtf1\ansi\deff0\nouicompat{\fonttbl{\f0\fswiss\fcharset0 Helvetica;}}
|
||||||
|
{\*\generator Msftedit 5.41.21.2510;}\viewkind4\uc1
|
||||||
|
\pard\sa200\sl276\slmult1\f0\fs20\lang9
|
||||||
|
|
||||||
|
CAGIRE - Forth-based Music Sequencer\par
|
||||||
|
Copyright (c) 2025 Rapha\"el Forment\par
|
||||||
|
\par
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.\par
|
||||||
|
\par
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.\par
|
||||||
|
\par
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see https://www.gnu.org/licenses/.\par
|
||||||
|
}
|
||||||
146
wix/main.wxs
Normal file
146
wix/main.wxs
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
<?xml version='1.0' encoding='windows-1252'?>
|
||||||
|
|
||||||
|
<?if $(sys.BUILDARCH) = x64 or $(sys.BUILDARCH) = intel64 ?>
|
||||||
|
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
|
||||||
|
<?else ?>
|
||||||
|
<?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
|
||||||
|
<?endif ?>
|
||||||
|
|
||||||
|
<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>
|
||||||
|
|
||||||
|
<Product
|
||||||
|
Id='*'
|
||||||
|
Name='Cagire'
|
||||||
|
UpgradeCode='F2A3D4E5-6B7C-8D9E-0F1A-2B3C4D5E6F7A'
|
||||||
|
Manufacturer='Raphael Forment'
|
||||||
|
Language='1033'
|
||||||
|
Codepage='1252'
|
||||||
|
Version='$(var.Version)'>
|
||||||
|
|
||||||
|
<Package Id='*'
|
||||||
|
Keywords='Installer'
|
||||||
|
Description='Cagire - Forth-based music sequencer'
|
||||||
|
Manufacturer='Raphael Forment'
|
||||||
|
InstallerVersion='450'
|
||||||
|
Languages='1033'
|
||||||
|
Compressed='yes'
|
||||||
|
InstallScope='perMachine'
|
||||||
|
SummaryCodepage='1252'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MajorUpgrade
|
||||||
|
Schedule='afterInstallInitialize'
|
||||||
|
DowngradeErrorMessage='A newer version of [ProductName] is already installed. Setup will now exit.'/>
|
||||||
|
|
||||||
|
<Media Id='1' Cabinet='media1.cab' EmbedCab='yes' DiskPrompt='CD-ROM #1'/>
|
||||||
|
<Property Id='DiskPrompt' Value='Cagire Installation'/>
|
||||||
|
|
||||||
|
<Directory Id='TARGETDIR' Name='SourceDir'>
|
||||||
|
<Directory Id='$(var.PlatformProgramFilesFolder)' Name='PFiles'>
|
||||||
|
<Directory Id='APPLICATIONFOLDER' Name='Cagire'>
|
||||||
|
|
||||||
|
<Component Id='CagireCLI' Guid='A1B2C3D4-E5F6-7890-ABCD-EF1234567890' Win64='yes'>
|
||||||
|
<File
|
||||||
|
Id='CagireEXE'
|
||||||
|
Name='cagire.exe'
|
||||||
|
DiskId='1'
|
||||||
|
Source='$(var.CargoTargetBinDir)\cagire.exe'
|
||||||
|
KeyPath='yes'/>
|
||||||
|
</Component>
|
||||||
|
|
||||||
|
<Component Id='CagireDesktop' Guid='B2C3D4E5-F6A7-8901-BCDE-F12345678901' Win64='yes'>
|
||||||
|
<File
|
||||||
|
Id='CagireDesktopEXE'
|
||||||
|
Name='cagire-desktop.exe'
|
||||||
|
DiskId='1'
|
||||||
|
Source='$(var.CargoTargetBinDir)\cagire-desktop.exe'
|
||||||
|
KeyPath='yes'/>
|
||||||
|
</Component>
|
||||||
|
|
||||||
|
<Component Id='PathEntry' Guid='C3D4E5F6-A7B8-9012-CDEF-123456789012' Win64='yes' KeyPath='yes'>
|
||||||
|
<Environment
|
||||||
|
Id='PATH'
|
||||||
|
Name='PATH'
|
||||||
|
Value='[APPLICATIONFOLDER]'
|
||||||
|
Permanent='no'
|
||||||
|
Part='last'
|
||||||
|
Action='set'
|
||||||
|
System='yes'/>
|
||||||
|
</Component>
|
||||||
|
|
||||||
|
</Directory>
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
<Directory Id='ProgramMenuFolder'>
|
||||||
|
<Directory Id='ApplicationProgramsFolder' Name='Cagire'>
|
||||||
|
<Component Id='StartMenuShortcut' Guid='D4E5F6A7-B8C9-0123-DEFA-234567890123' Win64='yes'>
|
||||||
|
<Shortcut
|
||||||
|
Id='CagireDesktopShortcut'
|
||||||
|
Name='Cagire'
|
||||||
|
Description='Forth-based music sequencer'
|
||||||
|
Target='[APPLICATIONFOLDER]cagire-desktop.exe'
|
||||||
|
WorkingDirectory='APPLICATIONFOLDER'
|
||||||
|
Icon='CagireIcon.exe'/>
|
||||||
|
<RemoveFolder Id='CleanUpShortcutFolder' On='uninstall'/>
|
||||||
|
<RegistryValue
|
||||||
|
Root='HKCU'
|
||||||
|
Key='Software\Cagire'
|
||||||
|
Name='installed'
|
||||||
|
Type='integer'
|
||||||
|
Value='1'
|
||||||
|
KeyPath='yes'/>
|
||||||
|
</Component>
|
||||||
|
</Directory>
|
||||||
|
</Directory>
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
<Feature
|
||||||
|
Id='Binaries'
|
||||||
|
Title='Application'
|
||||||
|
Description='Installs Cagire CLI and Desktop binaries.'
|
||||||
|
Level='1'
|
||||||
|
ConfigurableDirectory='APPLICATIONFOLDER'
|
||||||
|
AllowAdvertise='no'
|
||||||
|
Display='expand'
|
||||||
|
Absent='disallow'>
|
||||||
|
|
||||||
|
<ComponentRef Id='CagireCLI'/>
|
||||||
|
<ComponentRef Id='CagireDesktop'/>
|
||||||
|
|
||||||
|
<Feature
|
||||||
|
Id='Environment'
|
||||||
|
Title='PATH Environment Variable'
|
||||||
|
Description='Add the install location to the PATH system environment variable. This allows the cagire CLI to be called from any location.'
|
||||||
|
Level='1'
|
||||||
|
Absent='allow'>
|
||||||
|
<ComponentRef Id='PathEntry'/>
|
||||||
|
</Feature>
|
||||||
|
</Feature>
|
||||||
|
|
||||||
|
<Feature
|
||||||
|
Id='StartMenu'
|
||||||
|
Title='Start Menu Shortcut'
|
||||||
|
Description='Add a Cagire shortcut to the Start Menu.'
|
||||||
|
Level='1'
|
||||||
|
Absent='allow'>
|
||||||
|
<ComponentRef Id='StartMenuShortcut'/>
|
||||||
|
</Feature>
|
||||||
|
|
||||||
|
<SetProperty Id='ARPINSTALLLOCATION' Value='[APPLICATIONFOLDER]' After='CostFinalize'/>
|
||||||
|
|
||||||
|
<Icon Id='CagireIcon.exe' SourceFile='assets\Cagire.ico'/>
|
||||||
|
<Property Id='ARPPRODUCTICON' Value='CagireIcon.exe'/>
|
||||||
|
<Property Id='ARPHELPLINK' Value='https://cagire.raphaelforment.fr'/>
|
||||||
|
<Property Id='ARPURLINFOABOUT' Value='https://github.com/Bubobubobubobubo/cagire'/>
|
||||||
|
|
||||||
|
<UI>
|
||||||
|
<UIRef Id='WixUI_FeatureTree'/>
|
||||||
|
<Publish Dialog='WelcomeDlg' Control='Next' Event='NewDialog' Value='CustomizeDlg' Order='99'>1</Publish>
|
||||||
|
<Publish Dialog='CustomizeDlg' Control='Back' Event='NewDialog' Value='WelcomeDlg' Order='99'>1</Publish>
|
||||||
|
</UI>
|
||||||
|
|
||||||
|
<WixVariable Id='WixUILicenseRtf' Value='wix\License.rtf'/>
|
||||||
|
|
||||||
|
</Product>
|
||||||
|
|
||||||
|
</Wix>
|
||||||
Reference in New Issue
Block a user