Compare commits
2 Commits
5b3252cc31
...
a50059cf19
| Author | SHA1 | Date | |
|---|---|---|---|
| a50059cf19 | |||
| 6cdb4d9d2c |
5
.cargo/config.toml
Normal file
5
.cargo/config.toml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[alias]
|
||||||
|
xtask = "run --package xtask --release --"
|
||||||
|
|
||||||
|
[target.x86_64-pc-windows-gnu]
|
||||||
|
rustflags = ["-C", "link-args=-lstdc++ -lws2_32 -liphlpapi -lwinmm"]
|
||||||
248
.github/workflows/ci.yml
vendored
248
.github/workflows/ci.yml
vendored
@@ -1,9 +1,8 @@
|
|||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
push:
|
||||||
tags: ['v*']
|
branches: [main]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
@@ -15,26 +14,20 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
check:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
target: x86_64-unknown-linux-gnu
|
target: x86_64-unknown-linux-gnu
|
||||||
artifact: cagire-linux-x86_64
|
|
||||||
- os: macos-15-intel
|
|
||||||
target: x86_64-apple-darwin
|
|
||||||
artifact: cagire-macos-x86_64
|
|
||||||
- os: macos-14
|
- os: macos-14
|
||||||
target: aarch64-apple-darwin
|
target: aarch64-apple-darwin
|
||||||
artifact: cagire-macos-aarch64
|
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
target: x86_64-pc-windows-msvc
|
target: x86_64-pc-windows-msvc
|
||||||
artifact: cagire-windows-x86_64
|
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 30
|
timeout-minutes: 20
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -45,6 +38,7 @@ jobs:
|
|||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
|
components: clippy
|
||||||
|
|
||||||
- name: Cache Rust dependencies
|
- name: Cache Rust dependencies
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
@@ -57,13 +51,10 @@ jobs:
|
|||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y build-essential cmake pkg-config libasound2-dev libclang-dev libjack-dev \
|
sudo apt-get install -y build-essential cmake pkg-config libasound2-dev libclang-dev libjack-dev \
|
||||||
libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev libgl1-mesa-dev
|
libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev libgl1-mesa-dev
|
||||||
cargo install cargo-bundle
|
|
||||||
|
|
||||||
- name: Install dependencies (macOS)
|
- name: Install dependencies (macOS)
|
||||||
if: runner.os == 'macOS'
|
if: runner.os == 'macOS'
|
||||||
run: |
|
run: brew list cmake &>/dev/null || brew install cmake
|
||||||
brew list cmake &>/dev/null || brew install cmake
|
|
||||||
cargo install cargo-bundle
|
|
||||||
|
|
||||||
- name: Install dependencies (Windows)
|
- name: Install dependencies (Windows)
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
@@ -77,229 +68,8 @@ jobs:
|
|||||||
- name: Build desktop
|
- name: Build desktop
|
||||||
run: cargo build --release --features desktop --bin cagire-desktop --target ${{ matrix.target }}
|
run: cargo build --release --features desktop --bin cagire-desktop --target ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Bundle desktop app
|
- name: Test
|
||||||
if: runner.os != 'Windows'
|
run: cargo test --target ${{ matrix.target }}
|
||||||
run: cargo bundle --release --features desktop --bin cagire-desktop --target ${{ matrix.target }}
|
|
||||||
|
|
||||||
- name: Bundle CLAP plugin
|
- name: Clippy
|
||||||
run: cargo xtask bundle cagire-plugins --release --target ${{ matrix.target }}
|
run: cargo clippy --target ${{ matrix.target }} -- -D warnings
|
||||||
|
|
||||||
- name: Zip macOS app bundle
|
|
||||||
if: runner.os == 'macOS'
|
|
||||||
run: |
|
|
||||||
cd target/${{ matrix.target }}/release/bundle/osx
|
|
||||||
zip -r Cagire.app.zip Cagire.app
|
|
||||||
|
|
||||||
- name: Upload artifact (Unix)
|
|
||||||
if: runner.os != 'Windows'
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: ${{ matrix.artifact }}
|
|
||||||
path: target/${{ matrix.target }}/release/cagire
|
|
||||||
|
|
||||||
- name: Upload artifact (Windows)
|
|
||||||
if: runner.os == 'Windows'
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: ${{ matrix.artifact }}
|
|
||||||
path: target/${{ matrix.target }}/release/cagire.exe
|
|
||||||
|
|
||||||
- name: Upload desktop artifact (Linux deb)
|
|
||||||
if: runner.os == 'Linux'
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: ${{ matrix.artifact }}-desktop
|
|
||||||
path: target/${{ matrix.target }}/release/bundle/deb/*.deb
|
|
||||||
|
|
||||||
- name: Upload desktop artifact (macOS app bundle)
|
|
||||||
if: runner.os == 'macOS'
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: ${{ matrix.artifact }}-desktop
|
|
||||||
path: target/${{ matrix.target }}/release/bundle/osx/Cagire.app.zip
|
|
||||||
|
|
||||||
- name: Upload desktop artifact (Windows exe)
|
|
||||||
if: runner.os == 'Windows'
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: ${{ matrix.artifact }}-desktop
|
|
||||||
path: target/${{ matrix.target }}/release/cagire-desktop.exe
|
|
||||||
|
|
||||||
- name: Upload CLAP artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: ${{ matrix.artifact }}-clap
|
|
||||||
path: target/bundled/cagire-plugins.clap
|
|
||||||
|
|
||||||
- name: Upload VST3 artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: ${{ matrix.artifact }}-vst3
|
|
||||||
path: target/bundled/cagire-plugins.vst3
|
|
||||||
|
|
||||||
universal-macos:
|
|
||||||
needs: build
|
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
|
||||||
runs-on: macos-14
|
|
||||||
timeout-minutes: 10
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Download macOS artifacts
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
pattern: cagire-macos-*
|
|
||||||
path: artifacts
|
|
||||||
|
|
||||||
- name: Create universal CLI binary
|
|
||||||
run: |
|
|
||||||
lipo -create \
|
|
||||||
artifacts/cagire-macos-x86_64/cagire \
|
|
||||||
artifacts/cagire-macos-aarch64/cagire \
|
|
||||||
-output cagire
|
|
||||||
chmod +x cagire
|
|
||||||
lipo -info cagire
|
|
||||||
|
|
||||||
- name: Create universal app bundle
|
|
||||||
run: |
|
|
||||||
cd artifacts/cagire-macos-aarch64-desktop
|
|
||||||
unzip Cagire.app.zip
|
|
||||||
cd ../cagire-macos-x86_64-desktop
|
|
||||||
unzip Cagire.app.zip
|
|
||||||
cd ../..
|
|
||||||
cp -R artifacts/cagire-macos-aarch64-desktop/Cagire.app Cagire.app
|
|
||||||
lipo -create \
|
|
||||||
artifacts/cagire-macos-x86_64-desktop/Cagire.app/Contents/MacOS/cagire-desktop \
|
|
||||||
artifacts/cagire-macos-aarch64-desktop/Cagire.app/Contents/MacOS/cagire-desktop \
|
|
||||||
-output Cagire.app/Contents/MacOS/cagire-desktop
|
|
||||||
lipo -info Cagire.app/Contents/MacOS/cagire-desktop
|
|
||||||
zip -r Cagire.app.zip Cagire.app
|
|
||||||
|
|
||||||
- name: Create universal CLAP plugin
|
|
||||||
run: |
|
|
||||||
mkdir -p cagire-plugins.clap/Contents/MacOS
|
|
||||||
cp artifacts/cagire-macos-aarch64-clap/cagire-plugins.clap/Contents/Info.plist \
|
|
||||||
cagire-plugins.clap/Contents/ 2>/dev/null || true
|
|
||||||
cp artifacts/cagire-macos-aarch64-clap/cagire-plugins.clap/Contents/PkgInfo \
|
|
||||||
cagire-plugins.clap/Contents/ 2>/dev/null || true
|
|
||||||
lipo -create \
|
|
||||||
artifacts/cagire-macos-x86_64-clap/cagire-plugins.clap/Contents/MacOS/cagire-plugins \
|
|
||||||
artifacts/cagire-macos-aarch64-clap/cagire-plugins.clap/Contents/MacOS/cagire-plugins \
|
|
||||||
-output cagire-plugins.clap/Contents/MacOS/cagire-plugins
|
|
||||||
lipo -info cagire-plugins.clap/Contents/MacOS/cagire-plugins
|
|
||||||
|
|
||||||
- name: Create universal VST3 plugin
|
|
||||||
run: |
|
|
||||||
mkdir -p cagire-plugins.vst3/Contents/MacOS
|
|
||||||
cp -R artifacts/cagire-macos-aarch64-vst3/cagire-plugins.vst3/Contents/Info.plist \
|
|
||||||
cagire-plugins.vst3/Contents/ 2>/dev/null || true
|
|
||||||
cp artifacts/cagire-macos-aarch64-vst3/cagire-plugins.vst3/Contents/PkgInfo \
|
|
||||||
cagire-plugins.vst3/Contents/ 2>/dev/null || true
|
|
||||||
cp -R artifacts/cagire-macos-aarch64-vst3/cagire-plugins.vst3/Contents/Resources \
|
|
||||||
cagire-plugins.vst3/Contents/ 2>/dev/null || true
|
|
||||||
lipo -create \
|
|
||||||
artifacts/cagire-macos-x86_64-vst3/cagire-plugins.vst3/Contents/MacOS/cagire-plugins \
|
|
||||||
artifacts/cagire-macos-aarch64-vst3/cagire-plugins.vst3/Contents/MacOS/cagire-plugins \
|
|
||||||
-output cagire-plugins.vst3/Contents/MacOS/cagire-plugins
|
|
||||||
lipo -info cagire-plugins.vst3/Contents/MacOS/cagire-plugins
|
|
||||||
|
|
||||||
- name: Build .pkg installer
|
|
||||||
run: |
|
|
||||||
VERSION="${GITHUB_REF_NAME#v}"
|
|
||||||
mkdir -p pkg-root/Applications pkg-root/usr/local/bin
|
|
||||||
cp -R Cagire.app pkg-root/Applications/
|
|
||||||
cp cagire pkg-root/usr/local/bin/
|
|
||||||
pkgbuild --analyze --root pkg-root component.plist
|
|
||||||
plutil -replace BundleIsRelocatable -bool NO component.plist
|
|
||||||
pkgbuild --root pkg-root --identifier com.sova.cagire \
|
|
||||||
--version "$VERSION" --install-location / \
|
|
||||||
--component-plist component.plist \
|
|
||||||
"Cagire-${VERSION}-universal.pkg"
|
|
||||||
|
|
||||||
- name: Upload universal CLI
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: cagire-macos-universal
|
|
||||||
path: cagire
|
|
||||||
|
|
||||||
- name: Upload universal app bundle
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: cagire-macos-universal-desktop
|
|
||||||
path: Cagire.app.zip
|
|
||||||
|
|
||||||
- name: Upload universal CLAP plugin
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: cagire-macos-universal-clap
|
|
||||||
path: cagire-plugins.clap
|
|
||||||
|
|
||||||
- name: Upload universal VST3 plugin
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: cagire-macos-universal-vst3
|
|
||||||
path: cagire-plugins.vst3
|
|
||||||
|
|
||||||
- name: Upload .pkg installer
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: cagire-macos-universal-pkg
|
|
||||||
path: Cagire-*-universal.pkg
|
|
||||||
|
|
||||||
release:
|
|
||||||
needs: [build, universal-macos]
|
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 10
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Download all artifacts
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
path: artifacts
|
|
||||||
|
|
||||||
- name: Prepare release files
|
|
||||||
run: |
|
|
||||||
mkdir -p release
|
|
||||||
for dir in artifacts/*/; do
|
|
||||||
name=$(basename "$dir")
|
|
||||||
if [[ "$name" == "cagire-macos-universal-pkg" ]]; then
|
|
||||||
cp "$dir"/*.pkg release/
|
|
||||||
elif [[ "$name" == "cagire-macos-universal-desktop" ]]; then
|
|
||||||
cp "$dir/Cagire.app.zip" "release/cagire-macos-universal-desktop.app.zip"
|
|
||||||
elif [[ "$name" == "cagire-macos-universal" ]]; then
|
|
||||||
cp "$dir/cagire" "release/cagire-macos-universal"
|
|
||||||
elif [[ "$name" == "cagire-macos-universal-clap" ]]; then
|
|
||||||
cd "$dir" && zip -r "../../release/cagire-macos-universal-clap.zip" cagire-plugins.clap && cd ../..
|
|
||||||
elif [[ "$name" == "cagire-macos-universal-vst3" ]]; then
|
|
||||||
cd "$dir" && zip -r "../../release/cagire-macos-universal-vst3.zip" cagire-plugins.vst3 && cd ../..
|
|
||||||
elif [[ "$name" == *-clap ]]; then
|
|
||||||
base="${name%-clap}"
|
|
||||||
cd "$dir" && zip -r "../../release/${base}-clap.zip" cagire-plugins.clap && cd ../..
|
|
||||||
elif [[ "$name" == *-vst3 ]]; then
|
|
||||||
base="${name%-vst3}"
|
|
||||||
cd "$dir" && zip -r "../../release/${base}-vst3.zip" cagire-plugins.vst3 && cd ../..
|
|
||||||
elif [[ "$name" == *-desktop ]]; then
|
|
||||||
base="${name%-desktop}"
|
|
||||||
if ls "$dir"/*.deb 1>/dev/null 2>&1; then
|
|
||||||
cp "$dir"/*.deb "release/${base}-desktop.deb"
|
|
||||||
elif [ -f "$dir/Cagire.app.zip" ]; then
|
|
||||||
cp "$dir/Cagire.app.zip" "release/${base}-desktop.app.zip"
|
|
||||||
elif [ -f "$dir/cagire-desktop.exe" ]; then
|
|
||||||
cp "$dir/cagire-desktop.exe" "release/${base}-desktop.exe"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if [ -f "$dir/cagire.exe" ]; then
|
|
||||||
cp "$dir/cagire.exe" "release/${name}.exe"
|
|
||||||
elif [ -f "$dir/cagire" ]; then
|
|
||||||
cp "$dir/cagire" "release/${name}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Create Release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
files: release/*
|
|
||||||
generate_release_notes: true
|
|
||||||
|
|||||||
368
.github/workflows/release.yml
vendored
Normal file
368
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags: ['v*']
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
target: x86_64-unknown-linux-gnu
|
||||||
|
artifact: cagire-linux-x86_64
|
||||||
|
- os: macos-15-intel
|
||||||
|
target: x86_64-apple-darwin
|
||||||
|
artifact: cagire-macos-x86_64
|
||||||
|
- os: macos-14
|
||||||
|
target: aarch64-apple-darwin
|
||||||
|
artifact: cagire-macos-aarch64
|
||||||
|
- os: windows-latest
|
||||||
|
target: x86_64-pc-windows-msvc
|
||||||
|
artifact: cagire-windows-x86_64
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 30
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Cache Rust dependencies
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
key: ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Install cargo-binstall
|
||||||
|
uses: cargo-bins/cargo-binstall@main
|
||||||
|
|
||||||
|
- name: Install dependencies (Linux)
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y build-essential cmake pkg-config libasound2-dev libclang-dev libjack-dev \
|
||||||
|
libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev libgl1-mesa-dev
|
||||||
|
cargo binstall -y cargo-bundle
|
||||||
|
|
||||||
|
- name: Install dependencies (macOS)
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
run: |
|
||||||
|
brew list cmake &>/dev/null || brew install cmake
|
||||||
|
cargo binstall -y cargo-bundle
|
||||||
|
|
||||||
|
- name: Install dependencies (Windows)
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
run: |
|
||||||
|
choco install cmake --installargs 'ADD_CMAKE_TO_PATH=System'
|
||||||
|
echo "C:\Program Files\CMake\bin" >> $env:GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --release --target ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Build desktop
|
||||||
|
run: cargo build --release --features desktop --bin cagire-desktop --target ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Bundle desktop app
|
||||||
|
if: runner.os != 'Windows'
|
||||||
|
run: cargo bundle --release --features desktop --bin cagire-desktop --target ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Build AppImages (Linux)
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
run: |
|
||||||
|
mkdir -p target/releases
|
||||||
|
scripts/make-appimage.sh target/${{ matrix.target }}/release/cagire x86_64 target/releases
|
||||||
|
scripts/make-appimage.sh target/${{ matrix.target }}/release/cagire-desktop x86_64 target/releases
|
||||||
|
|
||||||
|
- name: Upload AppImage artifacts (Linux)
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.artifact }}-appimage
|
||||||
|
path: target/releases/*.AppImage
|
||||||
|
|
||||||
|
- name: Bundle CLAP plugin
|
||||||
|
run: cargo xtask bundle cagire-plugins --release --target ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Zip macOS app bundle
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
run: |
|
||||||
|
cd target/${{ matrix.target }}/release/bundle/osx
|
||||||
|
zip -r Cagire.app.zip Cagire.app
|
||||||
|
|
||||||
|
- name: Upload artifact (Unix)
|
||||||
|
if: runner.os != 'Windows'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.artifact }}
|
||||||
|
path: target/${{ matrix.target }}/release/cagire
|
||||||
|
|
||||||
|
- name: Upload artifact (Windows)
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.artifact }}
|
||||||
|
path: target/${{ matrix.target }}/release/cagire.exe
|
||||||
|
|
||||||
|
- name: Upload desktop artifact (Linux deb)
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.artifact }}-desktop
|
||||||
|
path: target/${{ matrix.target }}/release/bundle/deb/*.deb
|
||||||
|
|
||||||
|
- name: Upload desktop artifact (macOS app bundle)
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.artifact }}-desktop
|
||||||
|
path: target/${{ matrix.target }}/release/bundle/osx/Cagire.app.zip
|
||||||
|
|
||||||
|
- name: Upload desktop artifact (Windows exe)
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.artifact }}-desktop
|
||||||
|
path: target/${{ matrix.target }}/release/cagire-desktop.exe
|
||||||
|
|
||||||
|
- name: Upload CLAP artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.artifact }}-clap
|
||||||
|
path: target/bundled/cagire-plugins.clap
|
||||||
|
|
||||||
|
- name: Upload VST3 artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.artifact }}-vst3
|
||||||
|
path: target/bundled/cagire-plugins.vst3
|
||||||
|
|
||||||
|
build-cross:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 45
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- target: aarch64-unknown-linux-gnu
|
||||||
|
artifact: cagire-linux-aarch64
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Cache Rust dependencies
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
key: ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Install cross
|
||||||
|
run: cargo install cross --git https://github.com/cross-rs/cross
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cross build --release --target ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Build desktop
|
||||||
|
run: cross build --release --features desktop --bin cagire-desktop --target ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.artifact }}
|
||||||
|
path: target/${{ matrix.target }}/release/cagire
|
||||||
|
|
||||||
|
- name: Upload desktop artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.artifact }}-desktop
|
||||||
|
path: target/${{ matrix.target }}/release/cagire-desktop
|
||||||
|
|
||||||
|
universal-macos:
|
||||||
|
needs: build
|
||||||
|
runs-on: macos-14
|
||||||
|
timeout-minutes: 10
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download macOS artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
pattern: cagire-macos-*
|
||||||
|
path: artifacts
|
||||||
|
|
||||||
|
- name: Create universal CLI binary
|
||||||
|
run: |
|
||||||
|
lipo -create \
|
||||||
|
artifacts/cagire-macos-x86_64/cagire \
|
||||||
|
artifacts/cagire-macos-aarch64/cagire \
|
||||||
|
-output cagire
|
||||||
|
chmod +x cagire
|
||||||
|
lipo -info cagire
|
||||||
|
|
||||||
|
- name: Create universal app bundle
|
||||||
|
run: |
|
||||||
|
cd artifacts/cagire-macos-aarch64-desktop
|
||||||
|
unzip Cagire.app.zip
|
||||||
|
cd ../cagire-macos-x86_64-desktop
|
||||||
|
unzip Cagire.app.zip
|
||||||
|
cd ../..
|
||||||
|
cp -R artifacts/cagire-macos-aarch64-desktop/Cagire.app Cagire.app
|
||||||
|
lipo -create \
|
||||||
|
artifacts/cagire-macos-x86_64-desktop/Cagire.app/Contents/MacOS/cagire-desktop \
|
||||||
|
artifacts/cagire-macos-aarch64-desktop/Cagire.app/Contents/MacOS/cagire-desktop \
|
||||||
|
-output Cagire.app/Contents/MacOS/cagire-desktop
|
||||||
|
lipo -info Cagire.app/Contents/MacOS/cagire-desktop
|
||||||
|
zip -r Cagire.app.zip Cagire.app
|
||||||
|
|
||||||
|
- name: Create universal CLAP plugin
|
||||||
|
run: |
|
||||||
|
mkdir -p cagire-plugins.clap/Contents/MacOS
|
||||||
|
cp artifacts/cagire-macos-aarch64-clap/cagire-plugins.clap/Contents/Info.plist \
|
||||||
|
cagire-plugins.clap/Contents/ 2>/dev/null || true
|
||||||
|
cp artifacts/cagire-macos-aarch64-clap/cagire-plugins.clap/Contents/PkgInfo \
|
||||||
|
cagire-plugins.clap/Contents/ 2>/dev/null || true
|
||||||
|
lipo -create \
|
||||||
|
artifacts/cagire-macos-x86_64-clap/cagire-plugins.clap/Contents/MacOS/cagire-plugins \
|
||||||
|
artifacts/cagire-macos-aarch64-clap/cagire-plugins.clap/Contents/MacOS/cagire-plugins \
|
||||||
|
-output cagire-plugins.clap/Contents/MacOS/cagire-plugins
|
||||||
|
lipo -info cagire-plugins.clap/Contents/MacOS/cagire-plugins
|
||||||
|
|
||||||
|
- name: Create universal VST3 plugin
|
||||||
|
run: |
|
||||||
|
mkdir -p cagire-plugins.vst3/Contents/MacOS
|
||||||
|
cp -R artifacts/cagire-macos-aarch64-vst3/cagire-plugins.vst3/Contents/Info.plist \
|
||||||
|
cagire-plugins.vst3/Contents/ 2>/dev/null || true
|
||||||
|
cp artifacts/cagire-macos-aarch64-vst3/cagire-plugins.vst3/Contents/PkgInfo \
|
||||||
|
cagire-plugins.vst3/Contents/ 2>/dev/null || true
|
||||||
|
cp -R artifacts/cagire-macos-aarch64-vst3/cagire-plugins.vst3/Contents/Resources \
|
||||||
|
cagire-plugins.vst3/Contents/ 2>/dev/null || true
|
||||||
|
lipo -create \
|
||||||
|
artifacts/cagire-macos-x86_64-vst3/cagire-plugins.vst3/Contents/MacOS/cagire-plugins \
|
||||||
|
artifacts/cagire-macos-aarch64-vst3/cagire-plugins.vst3/Contents/MacOS/cagire-plugins \
|
||||||
|
-output cagire-plugins.vst3/Contents/MacOS/cagire-plugins
|
||||||
|
lipo -info cagire-plugins.vst3/Contents/MacOS/cagire-plugins
|
||||||
|
|
||||||
|
- name: Build .pkg installer
|
||||||
|
run: |
|
||||||
|
VERSION="${GITHUB_REF_NAME#v}"
|
||||||
|
mkdir -p pkg-root/Applications pkg-root/usr/local/bin
|
||||||
|
cp -R Cagire.app pkg-root/Applications/
|
||||||
|
cp cagire pkg-root/usr/local/bin/
|
||||||
|
pkgbuild --analyze --root pkg-root component.plist
|
||||||
|
plutil -replace BundleIsRelocatable -bool NO component.plist
|
||||||
|
pkgbuild --root pkg-root --identifier com.sova.cagire \
|
||||||
|
--version "$VERSION" --install-location / \
|
||||||
|
--component-plist component.plist \
|
||||||
|
"Cagire-${VERSION}-universal.pkg"
|
||||||
|
|
||||||
|
- name: Upload universal CLI
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: cagire-macos-universal
|
||||||
|
path: cagire
|
||||||
|
|
||||||
|
- name: Upload universal app bundle
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: cagire-macos-universal-desktop
|
||||||
|
path: Cagire.app.zip
|
||||||
|
|
||||||
|
- name: Upload universal CLAP plugin
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: cagire-macos-universal-clap
|
||||||
|
path: cagire-plugins.clap
|
||||||
|
|
||||||
|
- name: Upload universal VST3 plugin
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: cagire-macos-universal-vst3
|
||||||
|
path: cagire-plugins.vst3
|
||||||
|
|
||||||
|
- name: Upload .pkg installer
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: cagire-macos-universal-pkg
|
||||||
|
path: Cagire-*-universal.pkg
|
||||||
|
|
||||||
|
release:
|
||||||
|
needs: [build, build-cross, universal-macos]
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download all artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: artifacts
|
||||||
|
|
||||||
|
- name: Prepare release files
|
||||||
|
run: |
|
||||||
|
mkdir -p release
|
||||||
|
for dir in artifacts/*/; do
|
||||||
|
name=$(basename "$dir")
|
||||||
|
if [[ "$name" == "cagire-macos-universal-pkg" ]]; then
|
||||||
|
cp "$dir"/*.pkg release/
|
||||||
|
elif [[ "$name" == "cagire-macos-universal-desktop" ]]; then
|
||||||
|
cp "$dir/Cagire.app.zip" "release/cagire-macos-universal-desktop.app.zip"
|
||||||
|
elif [[ "$name" == "cagire-macos-universal" ]]; then
|
||||||
|
cp "$dir/cagire" "release/cagire-macos-universal"
|
||||||
|
elif [[ "$name" == "cagire-macos-universal-clap" ]]; then
|
||||||
|
cd "$dir" && zip -r "../../release/cagire-macos-universal-clap.zip" cagire-plugins.clap && cd ../..
|
||||||
|
elif [[ "$name" == "cagire-macos-universal-vst3" ]]; then
|
||||||
|
cd "$dir" && zip -r "../../release/cagire-macos-universal-vst3.zip" cagire-plugins.vst3 && cd ../..
|
||||||
|
elif [[ "$name" == *-clap ]]; then
|
||||||
|
base="${name%-clap}"
|
||||||
|
cd "$dir" && zip -r "../../release/${base}-clap.zip" cagire-plugins.clap && cd ../..
|
||||||
|
elif [[ "$name" == *-vst3 ]]; then
|
||||||
|
base="${name%-vst3}"
|
||||||
|
cd "$dir" && zip -r "../../release/${base}-vst3.zip" cagire-plugins.vst3 && cd ../..
|
||||||
|
elif [[ "$name" == *-appimage ]]; then
|
||||||
|
cp "$dir"/*.AppImage release/
|
||||||
|
elif [[ "$name" == *-desktop ]]; then
|
||||||
|
base="${name%-desktop}"
|
||||||
|
if ls "$dir"/*.deb 1>/dev/null 2>&1; then
|
||||||
|
cp "$dir"/*.deb "release/${base}-desktop.deb"
|
||||||
|
elif [ -f "$dir/Cagire.app.zip" ]; then
|
||||||
|
cp "$dir/Cagire.app.zip" "release/${base}-desktop.app.zip"
|
||||||
|
elif [ -f "$dir/cagire-desktop.exe" ]; then
|
||||||
|
cp "$dir/cagire-desktop.exe" "release/${base}-desktop.exe"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [ -f "$dir/cagire.exe" ]; then
|
||||||
|
cp "$dir/cagire.exe" "release/${name}.exe"
|
||||||
|
elif [ -f "$dir/cagire" ]; then
|
||||||
|
cp "$dir/cagire" "release/${name}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: release/*
|
||||||
|
generate_release_notes: true
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,10 +1,11 @@
|
|||||||
/target
|
/target
|
||||||
|
/.cache
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
*.prof
|
*.prof
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# Cargo config
|
# Local cargo overrides (doux path patch)
|
||||||
.cargo/config.toml
|
.cargo/config.local.toml
|
||||||
|
|
||||||
# Claude
|
# Claude
|
||||||
.claude/
|
.claude/
|
||||||
|
|||||||
96
BUILDING.md
96
BUILDING.md
@@ -1,5 +1,15 @@
|
|||||||
# Building Cagire
|
# Building Cagire
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone --recursive https://github.com/Bubobubobubobubo/cagire
|
||||||
|
cd cagire
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
The `doux` audio engine is fetched automatically from git. No local path setup needed.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
**Rust** (stable toolchain): https://rustup.rs
|
**Rust** (stable toolchain): https://rustup.rs
|
||||||
@@ -68,6 +78,14 @@ Desktop (egui window):
|
|||||||
cargo build --release --features desktop --bin cagire-desktop
|
cargo build --release --features desktop --bin cagire-desktop
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Plugins (CLAP/VST3):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo xtask bundle cagire-plugins --release
|
||||||
|
```
|
||||||
|
|
||||||
|
The xtask alias is defined in `.cargo/config.toml` (committed). Plugin bundles are output to `target/bundled/`.
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
Terminal (default):
|
Terminal (default):
|
||||||
@@ -89,3 +107,81 @@ cargo run --release --features desktop --bin cagire-desktop
|
|||||||
| `-i, --input <device>` | Input audio device |
|
| `-i, --input <device>` | Input audio device |
|
||||||
| `-c, --channels <n>` | Output channel count |
|
| `-c, --channels <n>` | Output channel count |
|
||||||
| `-b, --buffer <size>` | Audio buffer size |
|
| `-b, --buffer <size>` | Audio buffer size |
|
||||||
|
|
||||||
|
## Cross-Compilation
|
||||||
|
|
||||||
|
[cross](https://github.com/cross-rs/cross) uses Docker to build for other platforms without installing their toolchains locally. It works on any OS that runs Docker.
|
||||||
|
|
||||||
|
### Targets
|
||||||
|
|
||||||
|
| Target | Method | Binaries |
|
||||||
|
|--------|--------|----------|
|
||||||
|
| aarch64-apple-darwin | Native (macOS ARM only) | `cagire`, `cagire-desktop` |
|
||||||
|
| x86_64-apple-darwin | Native (macOS only) | `cagire`, `cagire-desktop` |
|
||||||
|
| x86_64-unknown-linux-gnu | `cross build` | `cagire`, `cagire-desktop` |
|
||||||
|
| aarch64-unknown-linux-gnu (RPi 64-bit) | `cross build` | `cagire`, `cagire-desktop` |
|
||||||
|
| x86_64-pc-windows-gnu | `cross build` | `cagire`, `cagire-desktop` |
|
||||||
|
|
||||||
|
macOS targets can only be built on macOS — Apple does not support cross-compilation to macOS from other platforms. Linux and Windows targets can be cross-compiled from any OS. The aarch64-unknown-linux-gnu target covers Raspberry Pi (64-bit OS).
|
||||||
|
|
||||||
|
### Windows ABI
|
||||||
|
|
||||||
|
CI produces `x86_64-pc-windows-msvc` binaries (native Windows build, better compatibility). Local cross-compilation from non-Windows hosts produces `x86_64-pc-windows-gnu` binaries (MinGW via Docker). Both work; MSVC is preferred for releases.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
1. **Docker**: https://docs.docker.com/get-docker/
|
||||||
|
2. **cross**: `cargo install cross --git https://github.com/cross-rs/cross`
|
||||||
|
3. On macOS, add the Intel target: `rustup target add x86_64-apple-darwin`
|
||||||
|
|
||||||
|
Docker must be running before invoking `cross` or `scripts/build-all.sh`.
|
||||||
|
|
||||||
|
### Building Individual Targets
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux x86_64
|
||||||
|
cross build --release --target x86_64-unknown-linux-gnu
|
||||||
|
cross build --release --features desktop --bin cagire-desktop --target x86_64-unknown-linux-gnu
|
||||||
|
|
||||||
|
# Linux aarch64
|
||||||
|
cross build --release --target aarch64-unknown-linux-gnu
|
||||||
|
cross build --release --features desktop --bin cagire-desktop --target aarch64-unknown-linux-gnu
|
||||||
|
|
||||||
|
# Windows x86_64
|
||||||
|
cross build --release --target x86_64-pc-windows-gnu
|
||||||
|
cross build --release --features desktop --bin cagire-desktop --target x86_64-pc-windows-gnu
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building All Targets (macOS only)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Interactive (prompts for platform/target selection):
|
||||||
|
scripts/build-all.sh
|
||||||
|
|
||||||
|
# Non-interactive:
|
||||||
|
scripts/build-all.sh --platforms macos-arm64,linux-x86_64 --targets cli,desktop --yes
|
||||||
|
scripts/build-all.sh --all --yes
|
||||||
|
```
|
||||||
|
|
||||||
|
Builds selected targets, producing binaries in `target/releases/`.
|
||||||
|
|
||||||
|
Platform aliases: `macos-arm64`, `macos-x86_64`, `linux-x86_64`, `linux-aarch64`, `windows-x86_64`.
|
||||||
|
Target aliases: `cli`, `desktop`, `plugins`.
|
||||||
|
|
||||||
|
### Linux AppImage Packaging
|
||||||
|
|
||||||
|
Linux releases ship as AppImages — self-contained executables that bundle all shared library dependencies (ALSA, JACK, X11, OpenGL). No runtime dependencies required.
|
||||||
|
|
||||||
|
After building a Linux target, produce an AppImage with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/make-appimage.sh target/x86_64-unknown-linux-gnu/release/cagire x86_64 target/releases
|
||||||
|
```
|
||||||
|
|
||||||
|
`scripts/build-all.sh` does this automatically for every Linux target selected. The CI pipeline produces AppImages for the x86_64 Linux build. Cross-arch AppImage building (e.g. aarch64 on x86_64) is not supported — run on a matching host or in CI.
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- Custom Dockerfiles in `cross/` install the native libraries Cagire depends on (ALSA, JACK, X11, cmake, libclang, etc.). `Cross.toml` maps each target to its Dockerfile.
|
||||||
|
- The first build per target downloads Docker base images and installs packages. Subsequent builds use cached layers.
|
||||||
|
- Cross-architecture Docker builds (e.g. aarch64 on x86_64 or vice versa) run under QEMU emulation and are significantly slower.
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ cagire-forth = { path = "crates/forth" }
|
|||||||
cagire-markdown = { path = "crates/markdown" }
|
cagire-markdown = { path = "crates/markdown" }
|
||||||
cagire-project = { path = "crates/project" }
|
cagire-project = { path = "crates/project" }
|
||||||
cagire-ratatui = { path = "crates/ratatui" }
|
cagire-ratatui = { path = "crates/ratatui" }
|
||||||
doux = { path = "/Users/bubo/doux", features = ["native", "soundfont"] }
|
doux = { git = "https://github.com/sova-org/doux", features = ["native", "soundfont"] }
|
||||||
rusty_link = "0.4"
|
rusty_link = "0.4"
|
||||||
ratatui = "0.30"
|
ratatui = "0.30"
|
||||||
crossterm = "0.29"
|
crossterm = "0.29"
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
[build]
|
|
||||||
volumes = ["/Users/bubo/doux:/Users/bubo/doux"]
|
|
||||||
|
|
||||||
[target.aarch64-unknown-linux-gnu]
|
[target.aarch64-unknown-linux-gnu]
|
||||||
dockerfile = "./cross/aarch64-linux.Dockerfile"
|
dockerfile = "./cross/aarch64-linux.Dockerfile"
|
||||||
|
|
||||||
|
[target.x86_64-unknown-linux-gnu]
|
||||||
|
dockerfile = "./cross/x86_64-linux.Dockerfile"
|
||||||
|
|
||||||
|
[target.x86_64-pc-windows-gnu]
|
||||||
|
dockerfile = "./cross/x86_64-windows.Dockerfile"
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 72 KiB |
7
assets/cagire.desktop
Normal file
7
assets/cagire.desktop
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Cagire
|
||||||
|
Comment=Forth-based music sequencer
|
||||||
|
Exec=cagire
|
||||||
|
Icon=cagire
|
||||||
|
Categories=Audio;Music;AudioVideo;
|
||||||
9
build.rs
9
build.rs
@@ -1,6 +1,15 @@
|
|||||||
//! Build script — embeds Windows application resources (icon, metadata).
|
//! Build script — embeds Windows application resources (icon, metadata).
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||||
|
|
||||||
|
if target_os == "windows" {
|
||||||
|
// rusty_link's build.rs only links stdc++ on linux — add it for windows too
|
||||||
|
println!("cargo:rustc-link-lib=stdc++");
|
||||||
|
println!("cargo:rustc-link-lib=ws2_32");
|
||||||
|
println!("cargo:rustc-link-lib=iphlpapi");
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
let mut res = winres::WindowsResource::new();
|
let mut res = winres::WindowsResource::new();
|
||||||
|
|||||||
@@ -11,4 +11,4 @@ description = "TUI components for cagire sequencer"
|
|||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
ratatui = "0.30"
|
ratatui = "0.30"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
tui-textarea = { git = "https://github.com/phsym/tui-textarea", branch = "main", features = ["search"] }
|
tui-textarea = { git = "https://github.com/phsym/tui-textarea", rev = "e2ec4d3", features = ["search"] }
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ RUN dpkg --add-architecture arm64 && \
|
|||||||
libclang-dev \
|
libclang-dev \
|
||||||
libasound2-dev:arm64 \
|
libasound2-dev:arm64 \
|
||||||
libjack-dev:arm64 \
|
libjack-dev:arm64 \
|
||||||
|
libx11-dev:arm64 \
|
||||||
|
libx11-xcb-dev:arm64 \
|
||||||
libxcb-render0-dev:arm64 \
|
libxcb-render0-dev:arm64 \
|
||||||
libxcb-shape0-dev:arm64 \
|
libxcb-shape0-dev:arm64 \
|
||||||
libxcb-xfixes0-dev:arm64 \
|
libxcb-xfixes0-dev:arm64 \
|
||||||
|
|||||||
16
cross/x86_64-linux.Dockerfile
Normal file
16
cross/x86_64-linux.Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
FROM ghcr.io/cross-rs/x86_64-unknown-linux-gnu:main
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
cmake \
|
||||||
|
libclang-dev \
|
||||||
|
libasound2-dev \
|
||||||
|
libjack-dev \
|
||||||
|
libx11-dev \
|
||||||
|
libx11-xcb-dev \
|
||||||
|
libxcb-render0-dev \
|
||||||
|
libxcb-shape0-dev \
|
||||||
|
libxcb-xfixes0-dev \
|
||||||
|
libxkbcommon-dev \
|
||||||
|
libgl1-mesa-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
13
cross/x86_64-windows.Dockerfile
Normal file
13
cross/x86_64-windows.Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
FROM ghcr.io/cross-rs/x86_64-pc-windows-gnu:main
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
cmake \
|
||||||
|
clang \
|
||||||
|
libclang-dev \
|
||||||
|
mingw-w64-tools \
|
||||||
|
mingw-w64-x86-64-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& 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 ws2tcpip.h /usr/x86_64-w64-mingw32/include/WS2tcpip.h
|
||||||
@@ -14,7 +14,7 @@ cagire = { path = "../..", default-features = false, features = ["block-renderer
|
|||||||
cagire-forth = { path = "../../crates/forth" }
|
cagire-forth = { path = "../../crates/forth" }
|
||||||
cagire-project = { path = "../../crates/project" }
|
cagire-project = { path = "../../crates/project" }
|
||||||
cagire-ratatui = { path = "../../crates/ratatui" }
|
cagire-ratatui = { path = "../../crates/ratatui" }
|
||||||
doux = { path = "/Users/bubo/doux", features = ["native", "soundfont"] }
|
doux = { git = "https://github.com/sova-org/doux", features = ["native", "soundfont"] }
|
||||||
nih_plug = { git = "https://github.com/robbert-vdh/nih-plug", features = ["standalone"] }
|
nih_plug = { git = "https://github.com/robbert-vdh/nih-plug", features = ["standalone"] }
|
||||||
nih_plug_egui = { git = "https://github.com/robbert-vdh/nih-plug" }
|
nih_plug_egui = { git = "https://github.com/robbert-vdh/nih-plug" }
|
||||||
egui_ratatui = "2.1"
|
egui_ratatui = "2.1"
|
||||||
|
|||||||
428
scripts/build-all.sh
Executable file
428
scripts/build-all.sh
Executable file
@@ -0,0 +1,428 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
|
||||||
|
PLUGIN_NAME="cagire-plugins"
|
||||||
|
LIB_NAME="cagire_plugins" # cargo converts hyphens to underscores
|
||||||
|
OUT="target/releases"
|
||||||
|
|
||||||
|
PLATFORMS=(
|
||||||
|
"aarch64-apple-darwin"
|
||||||
|
"x86_64-apple-darwin"
|
||||||
|
"x86_64-unknown-linux-gnu"
|
||||||
|
"aarch64-unknown-linux-gnu"
|
||||||
|
"x86_64-pc-windows-gnu"
|
||||||
|
)
|
||||||
|
|
||||||
|
PLATFORM_LABELS=(
|
||||||
|
"macOS aarch64 (native)"
|
||||||
|
"macOS x86_64 (native)"
|
||||||
|
"Linux x86_64 (cross)"
|
||||||
|
"Linux aarch64 (cross)"
|
||||||
|
"Windows x86_64 (cross)"
|
||||||
|
)
|
||||||
|
|
||||||
|
PLATFORM_ALIASES=(
|
||||||
|
"macos-arm64"
|
||||||
|
"macos-x86_64"
|
||||||
|
"linux-x86_64"
|
||||||
|
"linux-aarch64"
|
||||||
|
"windows-x86_64"
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- CLI argument parsing ---
|
||||||
|
|
||||||
|
cli_platforms=""
|
||||||
|
cli_targets=""
|
||||||
|
cli_yes=false
|
||||||
|
cli_all=false
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--platforms) cli_platforms="$2"; shift 2 ;;
|
||||||
|
--targets) cli_targets="$2"; shift 2 ;;
|
||||||
|
--yes) cli_yes=true; shift ;;
|
||||||
|
--all) cli_all=true; shift ;;
|
||||||
|
-h|--help)
|
||||||
|
echo "Usage: $0 [OPTIONS]"
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo " --platforms <list> Comma-separated: macos-arm64,macos-x86_64,linux-x86_64,linux-aarch64,windows-x86_64"
|
||||||
|
echo " --targets <list> Comma-separated: cli,desktop,plugins"
|
||||||
|
echo " --all Build all platforms and targets"
|
||||||
|
echo " --yes Skip confirmation prompt"
|
||||||
|
echo ""
|
||||||
|
echo "Without options, runs interactively."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*) echo "Unknown option: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
resolve_platform_alias() {
|
||||||
|
local alias="$1"
|
||||||
|
for i in "${!PLATFORM_ALIASES[@]}"; do
|
||||||
|
if [[ "${PLATFORM_ALIASES[$i]}" == "$alias" ]]; then
|
||||||
|
echo "$i"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "Unknown platform: $alias" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Helpers ---
|
||||||
|
|
||||||
|
prompt_platforms() {
|
||||||
|
echo "Select platform (0=all, comma-separated):"
|
||||||
|
echo " 0) All"
|
||||||
|
for i in "${!PLATFORMS[@]}"; do
|
||||||
|
echo " $((i+1))) ${PLATFORM_LABELS[$i]}"
|
||||||
|
done
|
||||||
|
read -rp "> " choice
|
||||||
|
|
||||||
|
if [[ "$choice" == "0" || -z "$choice" ]]; then
|
||||||
|
selected_platforms=("${PLATFORMS[@]}")
|
||||||
|
selected_labels=("${PLATFORM_LABELS[@]}")
|
||||||
|
else
|
||||||
|
IFS=',' read -ra indices <<< "$choice"
|
||||||
|
selected_platforms=()
|
||||||
|
selected_labels=()
|
||||||
|
for idx in "${indices[@]}"; do
|
||||||
|
idx="${idx// /}"
|
||||||
|
idx=$((idx - 1))
|
||||||
|
if (( idx < 0 || idx >= ${#PLATFORMS[@]} )); then
|
||||||
|
echo "Invalid platform index: $((idx+1))"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
selected_platforms+=("${PLATFORMS[$idx]}")
|
||||||
|
selected_labels+=("${PLATFORM_LABELS[$idx]}")
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt_targets() {
|
||||||
|
echo ""
|
||||||
|
echo "Select targets (0=all, comma-separated):"
|
||||||
|
echo " 0) All"
|
||||||
|
echo " 1) cagire"
|
||||||
|
echo " 2) cagire-desktop"
|
||||||
|
echo " 3) cagire-plugins (CLAP/VST3)"
|
||||||
|
read -rp "> " choice
|
||||||
|
|
||||||
|
build_cagire=false
|
||||||
|
build_desktop=false
|
||||||
|
build_plugins=false
|
||||||
|
|
||||||
|
if [[ "$choice" == "0" || -z "$choice" ]]; then
|
||||||
|
build_cagire=true
|
||||||
|
build_desktop=true
|
||||||
|
build_plugins=true
|
||||||
|
else
|
||||||
|
IFS=',' read -ra targets <<< "$choice"
|
||||||
|
for t in "${targets[@]}"; do
|
||||||
|
t="${t// /}"
|
||||||
|
case "$t" in
|
||||||
|
1) build_cagire=true ;;
|
||||||
|
2) build_desktop=true ;;
|
||||||
|
3) build_plugins=true ;;
|
||||||
|
*) echo "Invalid target: $t"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm_summary() {
|
||||||
|
echo ""
|
||||||
|
echo "=== Build Summary ==="
|
||||||
|
echo ""
|
||||||
|
echo "Platforms:"
|
||||||
|
for label in "${selected_labels[@]}"; do
|
||||||
|
echo " - $label"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
echo "Targets:"
|
||||||
|
$build_cagire && echo " - cagire"
|
||||||
|
$build_desktop && echo " - cagire-desktop"
|
||||||
|
$build_plugins && echo " - cagire-plugins (CLAP/VST3)"
|
||||||
|
echo ""
|
||||||
|
read -rp "Proceed? [Y/n] " yn
|
||||||
|
case "${yn,,}" in
|
||||||
|
n|no) echo "Aborted."; exit 0 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
platform_os() {
|
||||||
|
case "$1" in
|
||||||
|
*windows*) echo "windows" ;;
|
||||||
|
*linux*) echo "linux" ;;
|
||||||
|
*apple*) echo "macos" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
platform_arch() {
|
||||||
|
case "$1" in
|
||||||
|
aarch64*) echo "aarch64" ;;
|
||||||
|
x86_64*) echo "x86_64" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
platform_suffix() {
|
||||||
|
case "$1" in
|
||||||
|
*windows*) echo ".exe" ;;
|
||||||
|
*) echo "" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
is_cross_target() {
|
||||||
|
case "$1" in
|
||||||
|
*linux*|*windows*) return 0 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
native_target() {
|
||||||
|
[[ "$1" == "aarch64-apple-darwin" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
release_dir() {
|
||||||
|
if native_target "$1"; then
|
||||||
|
echo "target/release"
|
||||||
|
else
|
||||||
|
echo "target/$1/release"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
target_flag() {
|
||||||
|
if native_target "$1"; then
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
echo "--target $1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
builder_for() {
|
||||||
|
if is_cross_target "$1"; then
|
||||||
|
echo "cross"
|
||||||
|
else
|
||||||
|
echo "cargo"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
build_binary() {
|
||||||
|
local platform="$1"
|
||||||
|
shift
|
||||||
|
local builder
|
||||||
|
builder=$(builder_for "$platform")
|
||||||
|
local tf
|
||||||
|
tf=$(target_flag "$platform")
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
$builder build --release $tf "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
bundle_plugins_native() {
|
||||||
|
local platform="$1"
|
||||||
|
local tf
|
||||||
|
tf=$(target_flag "$platform")
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
cargo xtask bundle "$PLUGIN_NAME" --release $tf
|
||||||
|
}
|
||||||
|
|
||||||
|
bundle_plugins_cross() {
|
||||||
|
local platform="$1"
|
||||||
|
local rd
|
||||||
|
rd=$(release_dir "$platform")
|
||||||
|
local os
|
||||||
|
os=$(platform_os "$platform")
|
||||||
|
local arch
|
||||||
|
arch=$(platform_arch "$platform")
|
||||||
|
|
||||||
|
# Build the cdylib with cross
|
||||||
|
# shellcheck disable=SC2046
|
||||||
|
build_binary "$platform" -p "$PLUGIN_NAME"
|
||||||
|
|
||||||
|
# Determine source library file
|
||||||
|
local src_lib
|
||||||
|
case "$os" in
|
||||||
|
linux) src_lib="$rd/lib${LIB_NAME}.so" ;;
|
||||||
|
windows) src_lib="$rd/${LIB_NAME}.dll" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ ! -f "$src_lib" ]]; then
|
||||||
|
echo " ERROR: Expected library not found: $src_lib"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Assemble CLAP bundle (flat file)
|
||||||
|
local clap_out="$OUT/${PLUGIN_NAME}-${os}-${arch}.clap"
|
||||||
|
cp "$src_lib" "$clap_out"
|
||||||
|
echo " CLAP -> $clap_out"
|
||||||
|
|
||||||
|
# Assemble VST3 bundle (directory tree)
|
||||||
|
local vst3_dir="$OUT/${PLUGIN_NAME}-${os}-${arch}.vst3"
|
||||||
|
local vst3_contents
|
||||||
|
case "$os" in
|
||||||
|
linux)
|
||||||
|
vst3_contents="$vst3_dir/Contents/${arch}-linux"
|
||||||
|
mkdir -p "$vst3_contents"
|
||||||
|
cp "$src_lib" "$vst3_contents/${PLUGIN_NAME}.so"
|
||||||
|
;;
|
||||||
|
windows)
|
||||||
|
vst3_contents="$vst3_dir/Contents/${arch}-win"
|
||||||
|
mkdir -p "$vst3_contents"
|
||||||
|
cp "$src_lib" "$vst3_contents/${PLUGIN_NAME}.vst3"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
echo " VST3 -> $vst3_dir/"
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_artifacts() {
|
||||||
|
local platform="$1"
|
||||||
|
local rd
|
||||||
|
rd=$(release_dir "$platform")
|
||||||
|
local os
|
||||||
|
os=$(platform_os "$platform")
|
||||||
|
local arch
|
||||||
|
arch=$(platform_arch "$platform")
|
||||||
|
local suffix
|
||||||
|
suffix=$(platform_suffix "$platform")
|
||||||
|
|
||||||
|
if $build_cagire; then
|
||||||
|
local src="$rd/cagire${suffix}"
|
||||||
|
local dst="$OUT/cagire-${os}-${arch}${suffix}"
|
||||||
|
cp "$src" "$dst"
|
||||||
|
echo " cagire -> $dst"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $build_desktop; then
|
||||||
|
local src="$rd/cagire-desktop${suffix}"
|
||||||
|
local dst="$OUT/cagire-desktop-${os}-${arch}${suffix}"
|
||||||
|
cp "$src" "$dst"
|
||||||
|
echo " cagire-desktop -> $dst"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# AppImage for Linux targets
|
||||||
|
if [[ "$os" == "linux" ]]; then
|
||||||
|
if $build_cagire; then
|
||||||
|
scripts/make-appimage.sh "$rd/cagire" "$arch" "$OUT"
|
||||||
|
fi
|
||||||
|
if $build_desktop; then
|
||||||
|
scripts/make-appimage.sh "$rd/cagire-desktop" "$arch" "$OUT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Plugin artifacts for native targets (cross handled in bundle_plugins_cross)
|
||||||
|
if $build_plugins && ! is_cross_target "$platform"; then
|
||||||
|
local bundle_dir="target/bundled"
|
||||||
|
|
||||||
|
# CLAP
|
||||||
|
local clap_src="$bundle_dir/${PLUGIN_NAME}.clap"
|
||||||
|
if [[ -e "$clap_src" ]]; then
|
||||||
|
local clap_dst="$OUT/${PLUGIN_NAME}-${os}-${arch}.clap"
|
||||||
|
cp -r "$clap_src" "$clap_dst"
|
||||||
|
echo " CLAP -> $clap_dst"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# VST3
|
||||||
|
local vst3_src="$bundle_dir/${PLUGIN_NAME}.vst3"
|
||||||
|
if [[ -d "$vst3_src" ]]; then
|
||||||
|
local vst3_dst="$OUT/${PLUGIN_NAME}-${os}-${arch}.vst3"
|
||||||
|
rm -rf "$vst3_dst"
|
||||||
|
cp -r "$vst3_src" "$vst3_dst"
|
||||||
|
echo " VST3 -> $vst3_dst/"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Main ---
|
||||||
|
|
||||||
|
if $cli_all; then
|
||||||
|
selected_platforms=("${PLATFORMS[@]}")
|
||||||
|
selected_labels=("${PLATFORM_LABELS[@]}")
|
||||||
|
build_cagire=true
|
||||||
|
build_desktop=true
|
||||||
|
build_plugins=true
|
||||||
|
elif [[ -n "$cli_platforms" || -n "$cli_targets" ]]; then
|
||||||
|
# Resolve platforms from CLI
|
||||||
|
if [[ -n "$cli_platforms" ]]; then
|
||||||
|
selected_platforms=()
|
||||||
|
selected_labels=()
|
||||||
|
IFS=',' read -ra aliases <<< "$cli_platforms"
|
||||||
|
for alias in "${aliases[@]}"; do
|
||||||
|
alias="${alias// /}"
|
||||||
|
idx=$(resolve_platform_alias "$alias")
|
||||||
|
selected_platforms+=("${PLATFORMS[$idx]}")
|
||||||
|
selected_labels+=("${PLATFORM_LABELS[$idx]}")
|
||||||
|
done
|
||||||
|
else
|
||||||
|
selected_platforms=("${PLATFORMS[@]}")
|
||||||
|
selected_labels=("${PLATFORM_LABELS[@]}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Resolve targets from CLI
|
||||||
|
build_cagire=false
|
||||||
|
build_desktop=false
|
||||||
|
build_plugins=false
|
||||||
|
if [[ -n "$cli_targets" ]]; then
|
||||||
|
IFS=',' read -ra tgts <<< "$cli_targets"
|
||||||
|
for t in "${tgts[@]}"; do
|
||||||
|
t="${t// /}"
|
||||||
|
case "$t" in
|
||||||
|
cli) build_cagire=true ;;
|
||||||
|
desktop) build_desktop=true ;;
|
||||||
|
plugins) build_plugins=true ;;
|
||||||
|
*) echo "Unknown target: $t (expected: cli, desktop, plugins)"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
else
|
||||||
|
build_cagire=true
|
||||||
|
build_desktop=true
|
||||||
|
build_plugins=true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
prompt_platforms
|
||||||
|
prompt_targets
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! $cli_yes && [[ -z "$cli_platforms" ]] && ! $cli_all; then
|
||||||
|
confirm_summary
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$OUT"
|
||||||
|
|
||||||
|
step=0
|
||||||
|
total=${#selected_platforms[@]}
|
||||||
|
|
||||||
|
for platform in "${selected_platforms[@]}"; do
|
||||||
|
step=$((step + 1))
|
||||||
|
echo ""
|
||||||
|
echo "=== [$step/$total] $platform ==="
|
||||||
|
|
||||||
|
if $build_cagire; then
|
||||||
|
echo " -> cagire"
|
||||||
|
build_binary "$platform"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $build_desktop; then
|
||||||
|
echo " -> cagire-desktop"
|
||||||
|
build_binary "$platform" --features desktop --bin cagire-desktop
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $build_plugins; then
|
||||||
|
echo " -> cagire-plugins"
|
||||||
|
if is_cross_target "$platform"; then
|
||||||
|
bundle_plugins_cross "$platform"
|
||||||
|
else
|
||||||
|
bundle_plugins_native "$platform"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " Copying artifacts..."
|
||||||
|
copy_artifacts "$platform"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Done ==="
|
||||||
|
echo ""
|
||||||
|
ls -lhR "$OUT/"
|
||||||
141
scripts/make-appimage.sh
Executable file
141
scripts/make-appimage.sh
Executable file
@@ -0,0 +1,141 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Usage: scripts/make-appimage.sh <binary-path> <arch> <output-dir>
|
||||||
|
# Produces an AppImage from a Linux binary.
|
||||||
|
# On native Linux with matching arch: uses linuxdeploy.
|
||||||
|
# Otherwise (cross-compilation): builds AppImage via mksquashfs in Docker.
|
||||||
|
|
||||||
|
if [[ $# -ne 3 ]]; then
|
||||||
|
echo "Usage: $0 <binary-path> <arch> <output-dir>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
BINARY="$1"
|
||||||
|
ARCH="$2"
|
||||||
|
OUTDIR="$3"
|
||||||
|
|
||||||
|
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||||
|
CACHE_DIR="$REPO_ROOT/.cache"
|
||||||
|
APP_NAME="$(basename "$BINARY")"
|
||||||
|
|
||||||
|
RUNTIME_URL="https://github.com/AppImage/type2-runtime/releases/download/continuous/runtime-${ARCH}"
|
||||||
|
RUNTIME="$CACHE_DIR/runtime-${ARCH}"
|
||||||
|
|
||||||
|
build_appdir() {
|
||||||
|
local appdir="$1"
|
||||||
|
mkdir -p "$appdir/usr/bin"
|
||||||
|
cp "$BINARY" "$appdir/usr/bin/cagire"
|
||||||
|
chmod +x "$appdir/usr/bin/cagire"
|
||||||
|
|
||||||
|
mkdir -p "$appdir/usr/share/icons/hicolor/512x512/apps"
|
||||||
|
cp "$REPO_ROOT/assets/Cagire.png" "$appdir/usr/share/icons/hicolor/512x512/apps/cagire.png"
|
||||||
|
|
||||||
|
cp "$REPO_ROOT/assets/cagire.desktop" "$appdir/cagire.desktop"
|
||||||
|
|
||||||
|
# AppRun entry point
|
||||||
|
cat > "$appdir/AppRun" <<'APPRUN'
|
||||||
|
#!/bin/sh
|
||||||
|
SELF="$(readlink -f "$0")"
|
||||||
|
HERE="$(dirname "$SELF")"
|
||||||
|
exec "$HERE/usr/bin/cagire" "$@"
|
||||||
|
APPRUN
|
||||||
|
chmod +x "$appdir/AppRun"
|
||||||
|
|
||||||
|
# Symlink icon at root for AppImage spec
|
||||||
|
ln -sf usr/share/icons/hicolor/512x512/apps/cagire.png "$appdir/cagire.png"
|
||||||
|
ln -sf cagire.desktop "$appdir/.DirIcon" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
download_runtime() {
|
||||||
|
mkdir -p "$CACHE_DIR"
|
||||||
|
if [[ ! -f "$RUNTIME" ]]; then
|
||||||
|
echo " Downloading AppImage runtime for $ARCH..."
|
||||||
|
curl -fSL "$RUNTIME_URL" -o "$RUNTIME"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_native() {
|
||||||
|
local linuxdeploy_url="https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-${ARCH}.AppImage"
|
||||||
|
local linuxdeploy="$CACHE_DIR/linuxdeploy-${ARCH}.AppImage"
|
||||||
|
|
||||||
|
mkdir -p "$CACHE_DIR"
|
||||||
|
if [[ ! -f "$linuxdeploy" ]]; then
|
||||||
|
echo " Downloading linuxdeploy for $ARCH..."
|
||||||
|
curl -fSL "$linuxdeploy_url" -o "$linuxdeploy"
|
||||||
|
chmod +x "$linuxdeploy"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local appdir
|
||||||
|
appdir="$(mktemp -d)/AppDir"
|
||||||
|
build_appdir "$appdir"
|
||||||
|
|
||||||
|
export ARCH
|
||||||
|
export LDAI_RUNTIME_FILE="$RUNTIME"
|
||||||
|
"$linuxdeploy" \
|
||||||
|
--appimage-extract-and-run \
|
||||||
|
--appdir "$appdir" \
|
||||||
|
--desktop-file "$appdir/cagire.desktop" \
|
||||||
|
--icon-file "$appdir/usr/share/icons/hicolor/512x512/apps/cagire.png" \
|
||||||
|
--output appimage
|
||||||
|
|
||||||
|
local appimage
|
||||||
|
appimage=$(ls -1t ./*.AppImage 2>/dev/null | head -1 || true)
|
||||||
|
if [[ -z "$appimage" ]]; then
|
||||||
|
echo " ERROR: No AppImage produced"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mkdir -p "$OUTDIR"
|
||||||
|
mv "$appimage" "$OUTDIR/${APP_NAME}-linux-${ARCH}.AppImage"
|
||||||
|
echo " AppImage -> $OUTDIR/${APP_NAME}-linux-${ARCH}.AppImage"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_docker() {
|
||||||
|
local platform
|
||||||
|
case "$ARCH" in
|
||||||
|
x86_64) platform="linux/amd64" ;;
|
||||||
|
aarch64) platform="linux/arm64" ;;
|
||||||
|
*) echo "Unsupported arch: $ARCH"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
local appdir
|
||||||
|
appdir="$(mktemp -d)/AppDir"
|
||||||
|
build_appdir "$appdir"
|
||||||
|
|
||||||
|
local image_tag="cagire-appimage-${ARCH}"
|
||||||
|
|
||||||
|
echo " Building Docker image $image_tag ($platform)..."
|
||||||
|
docker build --platform "$platform" -q -t "$image_tag" - <<'DOCKERFILE'
|
||||||
|
FROM ubuntu:22.04
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
squashfs-tools \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
DOCKERFILE
|
||||||
|
|
||||||
|
echo " Creating squashfs via Docker ($image_tag)..."
|
||||||
|
docker run --rm --platform "$platform" \
|
||||||
|
-v "$appdir:/appdir:ro" \
|
||||||
|
-v "$CACHE_DIR:/cache" \
|
||||||
|
"$image_tag" \
|
||||||
|
mksquashfs /appdir /cache/appimage-${ARCH}.squashfs \
|
||||||
|
-root-owned -noappend -comp gzip -no-progress
|
||||||
|
|
||||||
|
mkdir -p "$OUTDIR"
|
||||||
|
local final="$OUTDIR/${APP_NAME}-linux-${ARCH}.AppImage"
|
||||||
|
cat "$RUNTIME" "$CACHE_DIR/appimage-${ARCH}.squashfs" > "$final"
|
||||||
|
chmod +x "$final"
|
||||||
|
rm -f "$CACHE_DIR/appimage-${ARCH}.squashfs"
|
||||||
|
echo " AppImage -> $final"
|
||||||
|
}
|
||||||
|
|
||||||
|
HOST_ARCH="$(uname -m)"
|
||||||
|
|
||||||
|
download_runtime
|
||||||
|
|
||||||
|
echo " Building AppImage for ${APP_NAME} ($ARCH)..."
|
||||||
|
|
||||||
|
if [[ "$HOST_ARCH" == "$ARCH" ]] && [[ "$(uname -s)" == "Linux" ]]; then
|
||||||
|
run_native
|
||||||
|
else
|
||||||
|
run_docker
|
||||||
|
fi
|
||||||
@@ -175,7 +175,7 @@ impl App {
|
|||||||
match model::share::export(pattern_data) {
|
match model::share::export(pattern_data) {
|
||||||
Ok(encoded) => {
|
Ok(encoded) => {
|
||||||
let len = encoded.len();
|
let len = encoded.len();
|
||||||
if let Some(clip) = &mut self.clipboard {
|
if let Ok(mut clip) = arboard::Clipboard::new() {
|
||||||
let _ = clip.set_text(encoded);
|
let _ = clip.set_text(encoded);
|
||||||
}
|
}
|
||||||
if len > 2000 {
|
if len > 2000 {
|
||||||
@@ -201,7 +201,7 @@ impl App {
|
|||||||
match model::share::export_bank(bank_data) {
|
match model::share::export_bank(bank_data) {
|
||||||
Ok(encoded) => {
|
Ok(encoded) => {
|
||||||
let len = encoded.len();
|
let len = encoded.len();
|
||||||
if let Some(clip) = &mut self.clipboard {
|
if let Ok(mut clip) = arboard::Clipboard::new() {
|
||||||
let _ = clip.set_text(encoded);
|
let _ = clip.set_text(encoded);
|
||||||
}
|
}
|
||||||
if len > 2000 {
|
if len > 2000 {
|
||||||
@@ -223,7 +223,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn import_bank(&mut self, bank: usize) {
|
pub fn import_bank(&mut self, bank: usize) {
|
||||||
let text = match self.clipboard.as_mut().and_then(|c| c.get_text().ok()) {
|
let text = match arboard::Clipboard::new().ok().and_then(|mut c| c.get_text().ok()) {
|
||||||
Some(t) => t,
|
Some(t) => t,
|
||||||
None => {
|
None => {
|
||||||
self.ui.flash("Clipboard empty", 150, FlashKind::Error);
|
self.ui.flash("Clipboard empty", 150, FlashKind::Error);
|
||||||
@@ -250,7 +250,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn import_pattern(&mut self, bank: usize, pattern: usize) {
|
pub fn import_pattern(&mut self, bank: usize, pattern: usize) {
|
||||||
let text = match self.clipboard.as_mut().and_then(|c| c.get_text().ok()) {
|
let text = match arboard::Clipboard::new().ok().and_then(|mut c| c.get_text().ok()) {
|
||||||
Some(t) => t,
|
Some(t) => t,
|
||||||
None => {
|
None => {
|
||||||
self.ui
|
self.ui
|
||||||
@@ -305,7 +305,7 @@ impl App {
|
|||||||
&indices,
|
&indices,
|
||||||
);
|
);
|
||||||
let count = copied.steps.len();
|
let count = copied.steps.len();
|
||||||
if let Some(clip) = &mut self.clipboard {
|
if let Ok(mut clip) = arboard::Clipboard::new() {
|
||||||
let text: String = copied.steps.iter().map(|s| s.script.as_str()).collect::<Vec<_>>().join("\n");
|
let text: String = copied.steps.iter().map(|s| s.script.as_str()).collect::<Vec<_>>().join("\n");
|
||||||
let _ = clip.set_text(text);
|
let _ = clip.set_text(text);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ impl App {
|
|||||||
AppCommand::PrevStep => self.prev_step(),
|
AppCommand::PrevStep => self.prev_step(),
|
||||||
AppCommand::StepUp => self.step_up(),
|
AppCommand::StepUp => self.step_up(),
|
||||||
AppCommand::StepDown => self.step_down(),
|
AppCommand::StepDown => self.step_down(),
|
||||||
|
AppCommand::NextPattern => self.navigate_pattern(1),
|
||||||
|
AppCommand::PrevPattern => self.navigate_pattern(-1),
|
||||||
|
AppCommand::NextBank => self.navigate_bank(1),
|
||||||
|
AppCommand::PrevBank => self.navigate_bank(-1),
|
||||||
|
|
||||||
// Pattern editing
|
// Pattern editing
|
||||||
AppCommand::ToggleSteps => self.toggle_steps(),
|
AppCommand::ToggleSteps => self.toggle_steps(),
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ pub struct App {
|
|||||||
// Held to keep the Arc alive (shared with ScriptEngine).
|
// Held to keep the Arc alive (shared with ScriptEngine).
|
||||||
pub _rng: Rng,
|
pub _rng: Rng,
|
||||||
pub live_keys: Arc<LiveKeyState>,
|
pub live_keys: Arc<LiveKeyState>,
|
||||||
pub clipboard: Option<arboard::Clipboard>,
|
|
||||||
pub copied_patterns: Option<Vec<Pattern>>,
|
pub copied_patterns: Option<Vec<Pattern>>,
|
||||||
pub copied_banks: Option<Vec<Bank>>,
|
pub copied_banks: Option<Vec<Bank>>,
|
||||||
|
|
||||||
@@ -115,7 +114,6 @@ impl App {
|
|||||||
_rng: rng,
|
_rng: rng,
|
||||||
live_keys,
|
live_keys,
|
||||||
script_engine,
|
script_engine,
|
||||||
clipboard: arboard::Clipboard::new().ok(),
|
|
||||||
copied_patterns: None,
|
copied_patterns: None,
|
||||||
copied_banks: None,
|
copied_banks: None,
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
//! Step and bank/pattern cursor navigation.
|
//! Step and bank/pattern cursor navigation.
|
||||||
|
|
||||||
|
use cagire_project::{MAX_BANKS, MAX_PATTERNS};
|
||||||
|
use tachyonfx::Motion;
|
||||||
|
|
||||||
use super::App;
|
use super::App;
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
@@ -55,4 +58,22 @@ impl App {
|
|||||||
self.editor_ctx.step = 0;
|
self.editor_ctx.step = 0;
|
||||||
self.load_step_to_editor();
|
self.load_step_to_editor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn navigate_pattern(&mut self, delta: i32) {
|
||||||
|
let cur = self.editor_ctx.pattern as i32;
|
||||||
|
self.editor_ctx.pattern = (cur + delta).rem_euclid(MAX_PATTERNS as i32) as usize;
|
||||||
|
self.editor_ctx.step = 0;
|
||||||
|
self.load_step_to_editor();
|
||||||
|
let direction = if delta > 0 { Motion::UpToDown } else { Motion::DownToUp };
|
||||||
|
self.ui.show_nav_indicator(500, direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn navigate_bank(&mut self, delta: i32) {
|
||||||
|
let cur = self.editor_ctx.bank as i32;
|
||||||
|
self.editor_ctx.bank = (cur + delta).rem_euclid(MAX_BANKS as i32) as usize;
|
||||||
|
self.editor_ctx.step = 0;
|
||||||
|
self.load_step_to_editor();
|
||||||
|
let direction = if delta > 0 { Motion::LeftToRight } else { Motion::RightToLeft };
|
||||||
|
self.ui.show_nav_indicator(500, direction);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ pub enum AppCommand {
|
|||||||
PrevStep,
|
PrevStep,
|
||||||
StepUp,
|
StepUp,
|
||||||
StepDown,
|
StepDown,
|
||||||
|
NextPattern,
|
||||||
|
PrevPattern,
|
||||||
|
NextBank,
|
||||||
|
PrevBank,
|
||||||
|
|
||||||
// Pattern editing
|
// Pattern editing
|
||||||
ToggleSteps,
|
ToggleSteps,
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ pub enum SeqCommand {
|
|||||||
length: usize,
|
length: usize,
|
||||||
},
|
},
|
||||||
StopAll,
|
StopAll,
|
||||||
|
RestartAll,
|
||||||
ResetScriptState,
|
ResetScriptState,
|
||||||
Shutdown,
|
Shutdown,
|
||||||
}
|
}
|
||||||
@@ -712,6 +713,23 @@ impl SequencerState {
|
|||||||
self.runs_counter.counts.clear();
|
self.runs_counter.counts.clear();
|
||||||
self.audio_state.flush_midi_notes = true;
|
self.audio_state.flush_midi_notes = true;
|
||||||
}
|
}
|
||||||
|
SeqCommand::RestartAll => {
|
||||||
|
for active in self.audio_state.active_patterns.values_mut() {
|
||||||
|
active.step_index = 0;
|
||||||
|
active.iter = 0;
|
||||||
|
}
|
||||||
|
self.audio_state.prev_beat = -1.0;
|
||||||
|
self.script_frontier = -1.0;
|
||||||
|
self.script_step = 0;
|
||||||
|
self.script_trace = None;
|
||||||
|
self.variables.store(Arc::new(HashMap::new()));
|
||||||
|
self.dict.lock().clear();
|
||||||
|
self.speed_overrides.clear();
|
||||||
|
self.script_engine.clear_global_params();
|
||||||
|
self.runs_counter.counts.clear();
|
||||||
|
Arc::make_mut(&mut self.step_traces).clear();
|
||||||
|
self.audio_state.flush_midi_notes = true;
|
||||||
|
}
|
||||||
SeqCommand::ResetScriptState => {
|
SeqCommand::ResetScriptState => {
|
||||||
// Clear shared state instead of replacing - preserves sharing with app
|
// Clear shared state instead of replacing - preserves sharing with app
|
||||||
self.variables.store(Arc::new(HashMap::new()));
|
self.variables.store(Arc::new(HashMap::new()));
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
|
|||||||
KeyCode::Char(']') => ctx.dispatch(AppCommand::SpeedIncrease),
|
KeyCode::Char(']') => ctx.dispatch(AppCommand::SpeedIncrease),
|
||||||
KeyCode::Char('L') => ctx.dispatch(AppCommand::OpenPatternModal(PatternField::Length)),
|
KeyCode::Char('L') => ctx.dispatch(AppCommand::OpenPatternModal(PatternField::Length)),
|
||||||
KeyCode::Char('S') => ctx.dispatch(AppCommand::OpenPatternModal(PatternField::Speed)),
|
KeyCode::Char('S') => ctx.dispatch(AppCommand::OpenPatternModal(PatternField::Speed)),
|
||||||
KeyCode::Char('p') => ctx.dispatch(AppCommand::OpenModal(Modal::Preview)),
|
KeyCode::Char('p') => ctx.dispatch(AppCommand::OpenPreludeEditor),
|
||||||
KeyCode::Delete | KeyCode::Backspace => {
|
KeyCode::Delete | KeyCode::Backspace => {
|
||||||
let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern);
|
let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern);
|
||||||
if let Some(range) = ctx.app.editor_ctx.selection_range() {
|
if let Some(range) = ctx.app.editor_ctx.selection_range() {
|
||||||
@@ -231,9 +231,6 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
|
|||||||
ctx.app.send_mute_state(ctx.seq_cmd_tx);
|
ctx.app.send_mute_state(ctx.seq_cmd_tx);
|
||||||
}
|
}
|
||||||
KeyCode::Char('d') => {
|
KeyCode::Char('d') => {
|
||||||
ctx.dispatch(AppCommand::OpenPreludeEditor);
|
|
||||||
}
|
|
||||||
KeyCode::Char('D') => {
|
|
||||||
ctx.dispatch(AppCommand::EvaluatePrelude);
|
ctx.dispatch(AppCommand::EvaluatePrelude);
|
||||||
}
|
}
|
||||||
KeyCode::Char('g') => {
|
KeyCode::Char('g') => {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use arc_swap::ArcSwap;
|
|||||||
use crossbeam_channel::Sender;
|
use crossbeam_channel::Sender;
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEvent};
|
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEvent};
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use std::sync::atomic::{AtomicBool, AtomicI64};
|
use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ fn handle_live_keys(ctx: &mut InputContext, key: &KeyEvent) -> bool {
|
|||||||
match (key.code, key.kind) {
|
match (key.code, key.kind) {
|
||||||
_ if !matches!(ctx.app.ui.modal, Modal::None) => false,
|
_ if !matches!(ctx.app.ui.modal, Modal::None) => false,
|
||||||
_ if ctx.app.page == Page::Script && ctx.app.script_editor.focused => false,
|
_ if ctx.app.page == Page::Script && ctx.app.script_editor.focused => false,
|
||||||
(KeyCode::Char('f'), KeyEventKind::Press) => {
|
(KeyCode::Char('f'), KeyEventKind::Press) if !key.modifiers.contains(KeyModifiers::ALT) => {
|
||||||
ctx.dispatch(AppCommand::ToggleLiveKeysFill);
|
ctx.dispatch(AppCommand::ToggleLiveKeysFill);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -97,11 +97,42 @@ fn handle_live_keys(ctx: &mut InputContext, key: &KeyEvent) -> bool {
|
|||||||
|
|
||||||
fn handle_normal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
fn handle_normal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||||
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||||
|
let alt = key.modifiers.contains(KeyModifiers::ALT);
|
||||||
|
if key.code == KeyCode::F(12) && !ctx.app.plugin_mode {
|
||||||
|
if !ctx.app.playback.playing {
|
||||||
|
ctx.dispatch(AppCommand::TogglePlaying);
|
||||||
|
ctx.playing.store(true, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
let _ = ctx.seq_cmd_tx.send(SeqCommand::RestartAll);
|
||||||
|
return InputResult::Continue;
|
||||||
|
}
|
||||||
|
|
||||||
if ctx.app.panel.visible && ctx.app.panel.focus == PanelFocus::Side {
|
if ctx.app.panel.visible && ctx.app.panel.focus == PanelFocus::Side {
|
||||||
return panel::handle_panel_input(ctx, key);
|
return panel::handle_panel_input(ctx, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if alt {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Up => {
|
||||||
|
ctx.dispatch(AppCommand::PrevPattern);
|
||||||
|
return InputResult::Continue;
|
||||||
|
}
|
||||||
|
KeyCode::Down => {
|
||||||
|
ctx.dispatch(AppCommand::NextPattern);
|
||||||
|
return InputResult::Continue;
|
||||||
|
}
|
||||||
|
KeyCode::Left | KeyCode::Char('b') => {
|
||||||
|
ctx.dispatch(AppCommand::PrevBank);
|
||||||
|
return InputResult::Continue;
|
||||||
|
}
|
||||||
|
KeyCode::Right | KeyCode::Char('f') => {
|
||||||
|
ctx.dispatch(AppCommand::NextBank);
|
||||||
|
return InputResult::Continue;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ctrl {
|
if ctrl {
|
||||||
let minimap_timed = MinimapMode::Timed(Instant::now() + Duration::from_millis(250));
|
let minimap_timed = MinimapMode::Timed(Instant::now() + Duration::from_millis(250));
|
||||||
match key.code {
|
match key.code {
|
||||||
|
|||||||
@@ -383,19 +383,19 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
|||||||
KeyCode::Char('c') if ctrl => {
|
KeyCode::Char('c') if ctrl => {
|
||||||
ctx.app.editor_ctx.editor.copy();
|
ctx.app.editor_ctx.editor.copy();
|
||||||
let text = ctx.app.editor_ctx.editor.yank_text();
|
let text = ctx.app.editor_ctx.editor.yank_text();
|
||||||
if let Some(clip) = &mut ctx.app.clipboard {
|
if let Ok(mut clip) = arboard::Clipboard::new() {
|
||||||
let _ = clip.set_text(text);
|
let _ = clip.set_text(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Char('x') if ctrl => {
|
KeyCode::Char('x') if ctrl => {
|
||||||
ctx.app.editor_ctx.editor.cut();
|
ctx.app.editor_ctx.editor.cut();
|
||||||
let text = ctx.app.editor_ctx.editor.yank_text();
|
let text = ctx.app.editor_ctx.editor.yank_text();
|
||||||
if let Some(clip) = &mut ctx.app.clipboard {
|
if let Ok(mut clip) = arboard::Clipboard::new() {
|
||||||
let _ = clip.set_text(text);
|
let _ = clip.set_text(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Char('v') if ctrl => {
|
KeyCode::Char('v') if ctrl => {
|
||||||
if let Some(clip) = &mut ctx.app.clipboard {
|
if let Ok(mut clip) = arboard::Clipboard::new() {
|
||||||
if let Ok(text) = clip.get_text() {
|
if let Ok(text) = clip.get_text() {
|
||||||
ctx.app.editor_ctx.editor.set_yank_text(text);
|
ctx.app.editor_ctx.editor.set_yank_text(text);
|
||||||
}
|
}
|
||||||
@@ -417,14 +417,6 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
|||||||
crate::services::stack_preview::update_cache(&ctx.app.editor_ctx);
|
crate::services::stack_preview::update_cache(&ctx.app.editor_ctx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Modal::Preview => match key.code {
|
|
||||||
KeyCode::Esc | KeyCode::Char('p') => ctx.dispatch(AppCommand::CloseModal),
|
|
||||||
KeyCode::Left => ctx.dispatch(AppCommand::PrevStep),
|
|
||||||
KeyCode::Right => ctx.dispatch(AppCommand::NextStep),
|
|
||||||
KeyCode::Up => ctx.dispatch(AppCommand::StepUp),
|
|
||||||
KeyCode::Down => ctx.dispatch(AppCommand::StepDown),
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
Modal::PatternProps {
|
Modal::PatternProps {
|
||||||
bank,
|
bank,
|
||||||
pattern,
|
pattern,
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ fn handle_click(ctx: &mut InputContext, col: u16, row: u16, term: Rect) {
|
|||||||
|
|
||||||
ctx.dispatch(AppCommand::ClearStatus);
|
ctx.dispatch(AppCommand::ClearStatus);
|
||||||
|
|
||||||
// If a modal is active, clicks outside dismiss it (except Editor/Preview)
|
// If a modal is active, clicks outside dismiss it (except Editor)
|
||||||
if !matches!(ctx.app.ui.modal, Modal::None) {
|
if !matches!(ctx.app.ui.modal, Modal::None) {
|
||||||
handle_modal_click(ctx, col, row, term);
|
handle_modal_click(ctx, col, row, term);
|
||||||
return;
|
return;
|
||||||
@@ -893,9 +893,6 @@ fn handle_modal_click(ctx: &mut InputContext, col: u16, row: u16, term: Rect) {
|
|||||||
Modal::Editor => {
|
Modal::Editor => {
|
||||||
handle_editor_mouse(ctx, col, row, term, false);
|
handle_editor_mouse(ctx, col, row, term, false);
|
||||||
}
|
}
|
||||||
Modal::Preview => {
|
|
||||||
// Don't dismiss preview on click
|
|
||||||
}
|
|
||||||
Modal::Confirm { .. } => {
|
Modal::Confirm { .. } => {
|
||||||
handle_confirm_click(ctx, col, row, term);
|
handle_confirm_click(ctx, col, row, term);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -338,7 +338,8 @@ fn main() -> io::Result<()> {
|
|||||||
|
|
||||||
let effects_active = app.ui.effects.borrow().is_running()
|
let effects_active = app.ui.effects.borrow().is_running()
|
||||||
|| app.ui.modal_fx.borrow().is_some()
|
|| app.ui.modal_fx.borrow().is_some()
|
||||||
|| app.ui.title_fx.borrow().is_some();
|
|| app.ui.title_fx.borrow().is_some()
|
||||||
|
|| app.ui.nav_fx.borrow().is_some();
|
||||||
if app.playback.playing || had_event || app.ui.show_title || effects_active {
|
if app.playback.playing || had_event || app.ui.show_title || effects_active {
|
||||||
if app.ui.show_title {
|
if app.ui.show_title {
|
||||||
app.ui.sparkles.tick(terminal.get_frame().area());
|
app.ui.sparkles.tick(terminal.get_frame().area());
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use crate::theme;
|
|||||||
pub enum FxId {
|
pub enum FxId {
|
||||||
#[default]
|
#[default]
|
||||||
PageTransition,
|
PageTransition,
|
||||||
|
NavSwitch,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tick_effects(ui: &mut UiState, page: Page) {
|
pub fn tick_effects(ui: &mut UiState, page: Page) {
|
||||||
@@ -39,6 +40,14 @@ pub fn tick_effects(ui: &mut UiState, page: Page) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn nav_sweep(ui: &UiState, direction: Motion) {
|
||||||
|
let bg = theme::get().ui.bg;
|
||||||
|
ui.effects.borrow_mut().add_unique_effect(
|
||||||
|
FxId::NavSwitch,
|
||||||
|
fx::sweep_in(direction, 10, 0, bg, (300, Interpolation::QuadOut)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn page_direction(from: Page, to: Page) -> Motion {
|
fn page_direction(from: Page, to: Page) -> Motion {
|
||||||
let (fc, fr) = from.grid_pos();
|
let (fc, fr) = from.grid_pos();
|
||||||
let (tc, tr) = to.grid_pos();
|
let (tc, tr) = to.grid_pos();
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ pub enum Modal {
|
|||||||
JumpToStep(String),
|
JumpToStep(String),
|
||||||
AddSamplePath(Box<FileBrowserState>),
|
AddSamplePath(Box<FileBrowserState>),
|
||||||
Editor,
|
Editor,
|
||||||
Preview,
|
|
||||||
PatternProps {
|
PatternProps {
|
||||||
bank: usize,
|
bank: usize,
|
||||||
pattern: usize,
|
pattern: usize,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use std::time::{Duration, Instant};
|
|||||||
|
|
||||||
use cagire_markdown::ParsedMarkdown;
|
use cagire_markdown::ParsedMarkdown;
|
||||||
use cagire_ratatui::Sparkles;
|
use cagire_ratatui::Sparkles;
|
||||||
use tachyonfx::{fx, Effect, EffectManager, Interpolation};
|
use tachyonfx::{fx, Effect, EffectManager, Interpolation, Motion};
|
||||||
|
|
||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
use crate::state::effects::FxId;
|
use crate::state::effects::FxId;
|
||||||
@@ -83,6 +83,8 @@ pub struct UiState {
|
|||||||
pub window_height: u32,
|
pub window_height: u32,
|
||||||
pub load_demo_on_startup: bool,
|
pub load_demo_on_startup: bool,
|
||||||
pub demo_index: usize,
|
pub demo_index: usize,
|
||||||
|
pub nav_indicator_until: Option<Instant>,
|
||||||
|
pub nav_fx: RefCell<Option<Effect>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for UiState {
|
impl Default for UiState {
|
||||||
@@ -135,6 +137,8 @@ impl Default for UiState {
|
|||||||
window_height: 800,
|
window_height: 800,
|
||||||
load_demo_on_startup: true,
|
load_demo_on_startup: true,
|
||||||
demo_index: 0,
|
demo_index: 0,
|
||||||
|
nav_indicator_until: None,
|
||||||
|
nav_fx: RefCell::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,6 +200,19 @@ impl UiState {
|
|||||||
self.minimap = MinimapMode::Hidden;
|
self.minimap = MinimapMode::Hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn show_nav_indicator(&mut self, duration_ms: u64, direction: Motion) {
|
||||||
|
self.nav_indicator_until = Some(Instant::now() + Duration::from_millis(duration_ms));
|
||||||
|
let bg = crate::theme::get().ui.bg;
|
||||||
|
*self.nav_fx.borrow_mut() = Some(fx::fade_from_fg(bg, (150, Interpolation::QuadOut)));
|
||||||
|
crate::state::effects::nav_sweep(self, direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nav_indicator_visible(&self) -> bool {
|
||||||
|
self.nav_indicator_until
|
||||||
|
.map(|t| Instant::now() < t)
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn invalidate_help_cache(&self) {
|
pub fn invalidate_help_cache(&self) {
|
||||||
self.help_parsed
|
self.help_parsed
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ pub fn bindings_for(page: Page, plugin_mode: bool) -> Vec<(&'static str, &'stati
|
|||||||
("s", "Save", "Save project"),
|
("s", "Save", "Save project"),
|
||||||
("l", "Load", "Load project"),
|
("l", "Load", "Load project"),
|
||||||
("?", "Keybindings", "Show this help"),
|
("?", "Keybindings", "Show this help"),
|
||||||
|
("F12", "Restart", "Full restart from step 0"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Page-specific bindings
|
// Page-specific bindings
|
||||||
@@ -20,12 +21,14 @@ pub fn bindings_for(page: Page, plugin_mode: bool) -> Vec<(&'static str, &'stati
|
|||||||
if !plugin_mode {
|
if !plugin_mode {
|
||||||
bindings.push(("Space", "Play/Stop", "Toggle playback"));
|
bindings.push(("Space", "Play/Stop", "Toggle playback"));
|
||||||
}
|
}
|
||||||
|
bindings.push(("Alt+↑↓", "Pattern", "Previous/next pattern"));
|
||||||
|
bindings.push(("Alt+←→", "Bank", "Previous/next bank"));
|
||||||
bindings.push(("←→↑↓", "Navigate", "Move cursor between steps"));
|
bindings.push(("←→↑↓", "Navigate", "Move cursor between steps"));
|
||||||
bindings.push(("Shift+←→↑↓", "Select", "Extend selection"));
|
bindings.push(("Shift+←→↑↓", "Select", "Extend selection"));
|
||||||
bindings.push(("Esc", "Clear", "Clear selection"));
|
bindings.push(("Esc", "Clear", "Clear selection"));
|
||||||
bindings.push(("Enter", "Edit", "Open step editor"));
|
bindings.push(("Enter", "Edit", "Open step editor"));
|
||||||
bindings.push(("t", "Toggle", "Toggle selected steps"));
|
bindings.push(("t", "Toggle", "Toggle selected steps"));
|
||||||
bindings.push(("p", "Preview", "Preview step script"));
|
bindings.push(("p", "Prelude", "Edit prelude script"));
|
||||||
bindings.push(("Tab", "Samples", "Toggle sample browser"));
|
bindings.push(("Tab", "Samples", "Toggle sample browser"));
|
||||||
bindings.push(("Ctrl+C", "Copy", "Copy selected steps"));
|
bindings.push(("Ctrl+C", "Copy", "Copy selected steps"));
|
||||||
bindings.push(("Ctrl+V", "Paste", "Paste steps"));
|
bindings.push(("Ctrl+V", "Paste", "Paste steps"));
|
||||||
@@ -50,8 +53,7 @@ pub fn bindings_for(page: Page, plugin_mode: bool) -> Vec<(&'static str, &'stati
|
|||||||
bindings.push(("x", "Solo", "Stage solo for current pattern"));
|
bindings.push(("x", "Solo", "Stage solo for current pattern"));
|
||||||
bindings.push(("M", "Clear mutes", "Clear all mutes"));
|
bindings.push(("M", "Clear mutes", "Clear all mutes"));
|
||||||
bindings.push(("X", "Clear solos", "Clear all solos"));
|
bindings.push(("X", "Clear solos", "Clear all solos"));
|
||||||
bindings.push(("d", "Prelude", "Edit prelude script"));
|
bindings.push(("d", "Eval prelude", "Re-evaluate prelude without editing"));
|
||||||
bindings.push(("D", "Eval prelude", "Re-evaluate prelude without editing"));
|
|
||||||
bindings.push(("g", "Share", "Export pattern to clipboard"));
|
bindings.push(("g", "Share", "Export pattern to clipboard"));
|
||||||
bindings.push(("G", "Import", "Import pattern from clipboard"));
|
bindings.push(("G", "Import", "Import pattern from clipboard"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,6 +201,17 @@ pub fn render(
|
|||||||
}
|
}
|
||||||
let modal_area = render_modal(frame, app, snapshot, term);
|
let modal_area = render_modal(frame, app, snapshot, term);
|
||||||
|
|
||||||
|
if app.ui.nav_indicator_visible() {
|
||||||
|
let nav_area = render_nav_indicator(frame, app, term);
|
||||||
|
let mut fx = app.ui.nav_fx.borrow_mut();
|
||||||
|
if let Some(effect) = fx.as_mut() {
|
||||||
|
effect.process(elapsed, frame.buffer_mut(), nav_area);
|
||||||
|
if !effect.running() {
|
||||||
|
*fx = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if app.ui.show_minimap() {
|
if app.ui.show_minimap() {
|
||||||
let tiles: Vec<NavTile> = Page::ALL
|
let tiles: Vec<NavTile> = Page::ALL
|
||||||
.iter()
|
.iter()
|
||||||
@@ -234,6 +245,57 @@ pub fn render(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_nav_indicator(frame: &mut Frame, app: &App, term: Rect) -> Rect {
|
||||||
|
let theme = theme::get();
|
||||||
|
let bank = &app.project_state.project.banks[app.editor_ctx.bank];
|
||||||
|
let pattern = &bank.patterns[app.editor_ctx.pattern];
|
||||||
|
|
||||||
|
let bank_num = format!("{:02}", app.editor_ctx.bank + 1);
|
||||||
|
let pattern_num = format!("{:02}", app.editor_ctx.pattern + 1);
|
||||||
|
let bank_name = bank.name.as_deref().unwrap_or("");
|
||||||
|
let pattern_name = pattern.name.as_deref().unwrap_or("");
|
||||||
|
|
||||||
|
let inner = ModalFrame::new("")
|
||||||
|
.width(34)
|
||||||
|
.height(5)
|
||||||
|
.border_color(theme.modal.border_accent)
|
||||||
|
.render_centered(frame, term);
|
||||||
|
|
||||||
|
let bank_style = Style::new().fg(theme.header.bank_fg).bold();
|
||||||
|
let pattern_style = Style::new().fg(theme.header.pattern_fg).bold();
|
||||||
|
let dim = Style::new().fg(theme.ui.text_dim);
|
||||||
|
let divider = Style::new().fg(theme.ui.border);
|
||||||
|
|
||||||
|
let line1 = Line::from(vec![
|
||||||
|
Span::styled(" BANK ", bank_style),
|
||||||
|
Span::styled("│", divider),
|
||||||
|
Span::styled(" PATTERN ", pattern_style),
|
||||||
|
]);
|
||||||
|
let line2 = Line::from(vec![
|
||||||
|
Span::styled(format!(" {bank_num} "), bank_style),
|
||||||
|
Span::styled(format!("{bank_name:<10}"), dim),
|
||||||
|
Span::styled("│", divider),
|
||||||
|
Span::styled(format!(" {pattern_num} "), pattern_style),
|
||||||
|
Span::styled(format!("{pattern_name:<11}"), dim),
|
||||||
|
]);
|
||||||
|
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(line1),
|
||||||
|
Rect::new(inner.x, inner.y, inner.width, 1),
|
||||||
|
);
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(line2),
|
||||||
|
Rect::new(inner.x, inner.y + 1, inner.width, 1),
|
||||||
|
);
|
||||||
|
|
||||||
|
Rect::new(
|
||||||
|
inner.x.saturating_sub(1),
|
||||||
|
inner.y.saturating_sub(1),
|
||||||
|
inner.width + 2,
|
||||||
|
inner.height + 2,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn header_height(_width: u16) -> u16 {
|
fn header_height(_width: u16) -> u16 {
|
||||||
3
|
3
|
||||||
}
|
}
|
||||||
@@ -660,10 +722,6 @@ fn render_modal(
|
|||||||
.height(18)
|
.height(18)
|
||||||
.render_centered(frame, term)
|
.render_centered(frame, term)
|
||||||
}
|
}
|
||||||
Modal::Preview => {
|
|
||||||
let user_words: HashSet<String> = app.dict.lock().keys().cloned().collect();
|
|
||||||
render_modal_preview(frame, app, snapshot, &user_words, term)
|
|
||||||
}
|
|
||||||
Modal::Editor => {
|
Modal::Editor => {
|
||||||
let user_words: HashSet<String> = app.dict.lock().keys().cloned().collect();
|
let user_words: HashSet<String> = app.dict.lock().keys().cloned().collect();
|
||||||
render_modal_editor(frame, app, snapshot, &user_words, term)
|
render_modal_editor(frame, app, snapshot, &user_words, term)
|
||||||
@@ -878,64 +936,6 @@ fn render_modal(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_modal_preview(
|
|
||||||
frame: &mut Frame,
|
|
||||||
app: &App,
|
|
||||||
snapshot: &SequencerSnapshot,
|
|
||||||
user_words: &HashSet<String>,
|
|
||||||
term: Rect,
|
|
||||||
) -> Rect {
|
|
||||||
let theme = theme::get();
|
|
||||||
let width = (term.width * 80 / 100).max(40);
|
|
||||||
let height = (term.height * 80 / 100).max(10);
|
|
||||||
|
|
||||||
let pattern = app.current_edit_pattern();
|
|
||||||
let step_idx = app.editor_ctx.step;
|
|
||||||
let step = pattern.step(step_idx);
|
|
||||||
let source_idx = step.and_then(|s| s.source);
|
|
||||||
let step_name = step.and_then(|s| s.name.as_ref());
|
|
||||||
|
|
||||||
let title = match (source_idx, step_name) {
|
|
||||||
(Some(src), Some(name)) => {
|
|
||||||
format!("Step {:02}: {} → {:02}", step_idx + 1, name, src + 1)
|
|
||||||
}
|
|
||||||
(None, Some(name)) => format!("Step {:02}: {}", step_idx + 1, name),
|
|
||||||
(Some(src), None) => format!("Step {:02} → {:02}", step_idx + 1, src + 1),
|
|
||||||
(None, None) => format!("Step {:02}", step_idx + 1),
|
|
||||||
};
|
|
||||||
|
|
||||||
let inner = ModalFrame::new(&title)
|
|
||||||
.width(width)
|
|
||||||
.height(height)
|
|
||||||
.border_color(theme.modal.preview)
|
|
||||||
.render_centered(frame, term);
|
|
||||||
|
|
||||||
let script = pattern.resolve_script(step_idx).unwrap_or("");
|
|
||||||
if script.is_empty() {
|
|
||||||
let empty = Paragraph::new("(empty)")
|
|
||||||
.alignment(Alignment::Center)
|
|
||||||
.style(Style::new().fg(theme.ui.text_dim));
|
|
||||||
let centered_area = Rect {
|
|
||||||
y: inner.y + inner.height / 2,
|
|
||||||
height: 1,
|
|
||||||
..inner
|
|
||||||
};
|
|
||||||
frame.render_widget(empty, centered_area);
|
|
||||||
} else {
|
|
||||||
let trace = if app.ui.runtime_highlight && app.playback.playing {
|
|
||||||
let source = pattern.resolve_source(step_idx);
|
|
||||||
snapshot.get_trace(app.editor_ctx.bank, app.editor_ctx.pattern, source)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let lines = highlight_script_lines(script, trace, user_words, usize::MAX);
|
|
||||||
frame.render_widget(Paragraph::new(lines), inner);
|
|
||||||
}
|
|
||||||
|
|
||||||
inner
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_modal_editor(
|
fn render_modal_editor(
|
||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
app: &App,
|
app: &App,
|
||||||
|
|||||||
Reference in New Issue
Block a user