Compare commits
256 Commits
985ab687d7
...
v0.1.3
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b60b3761b | |||
| 63fd2419d3 | |||
| da92fa6622 | |||
| 8e43e1bb3c | |||
| 3104a61490 | |||
| 20d72c9b21 | |||
| 09cfa82809 | |||
| bc1396d61d | |||
| 82d51a9add | |||
| fed7781bae | |||
| d055d2bfc6 | |||
| f273470eaf | |||
| b2a089fb0c | |||
| 04b68850d0 | |||
| 77364dddae | |||
| 5a72e4cef4 | |||
| 0097777449 | |||
| 4743c33916 | |||
| 2c8a6794a3 | |||
| 60fb62829f | |||
| 35370a6f2c | |||
| 4e1c04f9c7 | |||
| 80a3d91f76 | |||
| f130c9b54a | |||
| bdd2f9210e | |||
| 1fb599f574 | |||
| e8cf8c506b | |||
| 16d6d76422 | |||
| cf1d2be140 | |||
| cc89021cc0 | |||
| 470f62df89 | |||
| 88cb43a760 | |||
| eeefb7d54d | |||
| cfd7d31d3d | |||
| e9f5d8bb6d | |||
| 17643b3332 | |||
| 95879c852d | |||
| 3ad82a1954 | |||
| 4718248ee6 | |||
| 6fd844cdf6 | |||
| 2d3094464f | |||
| db44f9b98e | |||
| ecb559e556 | |||
| 5a59937cc7 | |||
| 11cc925faf | |||
| b72c782b2b | |||
| 6cd20732ed | |||
| d30ef8bb5b | |||
| e73ee1eb1e | |||
| 19bb3e0820 | |||
| cb7fcdb74a | |||
| 651ed1219d | |||
| ec98274dfe | |||
| 66abc4f961 | |||
| ca08074686 | |||
| 81fb174d7e | |||
| 7ae3f255b0 | |||
| 511726b65b | |||
| 052a6caa1a | |||
| fb62b121c1 | |||
| 0ecc4dae11 | |||
| 6b56655661 | |||
| f618f47811 | |||
| 47099a6eef | |||
| 70032acc75 | |||
| e1cf57918e | |||
| 71bd09d5ea | |||
| 6dd265067f | |||
| aa607a78d8 | |||
| 03c8187359 | |||
| c219b4efab | |||
| 0119988d7c | |||
| a6ff19bb08 | |||
| 2de49bdeba | |||
| 848d0e773f | |||
| 8f131b46cc | |||
| 8b745a77a6 | |||
| 502f7afe8f | |||
| e7137cc7ed | |||
| d9e6505e07 | |||
| 009d68087d | |||
| f47285385c | |||
| 81f475a75b | |||
| 3d552ec072 | |||
| 3093b40dbc | |||
| e2f3bcd4a9 | |||
| d3b27e8245 | |||
| c9c8fe4117 | |||
| a7a1f9e759 | |||
| 2ba957f2d4 | |||
| 7207a5fefe | |||
| 4526156c37 | |||
| 7a95207c58 | |||
| ab353edc0b | |||
| 77d5235d92 | |||
| e9bca2548c | |||
| 5ef988382b | |||
| 2d734c471f | |||
| 6216b9341b | |||
| bf361d3ab9 | |||
| 8fcc0f4e54 | |||
| 524e686b3a | |||
| 540f59dcf5 | |||
| 773c7bbd1c | |||
| b60703aa16 | |||
| c749ed6f85 | |||
| af6732db1c | |||
| b23dd85d0f | |||
| 160546d64d | |||
| cfaadd9d33 | |||
| 5e7fd8b79c | |||
| d56fa58157 | |||
| c803591ebb | |||
| d2e28b0415 | |||
| 38fad92f2e | |||
| d010392a3c | |||
| 80c392c24b | |||
| 60bc7618d3 | |||
| 55878707f2 | |||
| f6132bdd70 | |||
| 2c1765effa | |||
| f6e7330ad6 | |||
| af6016b9a9 | |||
| c7fabf3424 | |||
| 152536901b | |||
| dbd17a7946 | |||
| 83c756618f | |||
| e0d338a030 | |||
| 9a769518f9 | |||
| f1af4d2cdb | |||
| 3c518e4c5a | |||
| 53167e35b6 | |||
| 5a83c4c1d1 | |||
| 3fe837653b | |||
| 636126e7c6 | |||
| b46b65ed2a | |||
| 122d88c48d | |||
| 07523a49e7 | |||
| fb751c8691 | |||
| 5af536dea2 | |||
| b342595a09 | |||
| c92a29ab85 | |||
| 53fb3eb759 | |||
| b75b9562af | |||
| 8d249cf89b | |||
| a943d9622e | |||
| 467c504071 | |||
| 3bb1fa6e51 | |||
| ed70b47c81 | |||
| c95c82169f | |||
| bbbd8ff64a | |||
| 65736ccf84 | |||
| 75336656c2 | |||
| 96489c8f72 | |||
| 9b5759d794 | |||
| 3284354f40 | |||
| 266a625cf3 | |||
| 243f76ce05 | |||
| e01014a89a | |||
| 9d9dd5be38 | |||
| 9ff024cf9b | |||
| e337eb35e7 | |||
| a07a87a35f | |||
| 5c805c60d7 | |||
| b305df3d79 | |||
| 33ee1822a5 | |||
| 2cee1ba686 | |||
| c283887ada | |||
| 4235862d86 | |||
| 74fe999496 | |||
| cd8182425a | |||
| 7626f97695 | |||
| 19555be975 | |||
| 0aaa3efbb0 | |||
| f1902e18d3 | |||
| 39ca7de169 | |||
| 7c14ce7634 | |||
| d382c9e83a | |||
| d54d9218c1 | |||
| 7348bd38b1 | |||
| 2af0b67714 | |||
| 3e8076e416 | |||
| ceee3228c3 | |||
| 255cd34380 | |||
| 83fd4d028e | |||
| efacda2976 | |||
| ccce0df79d | |||
| 8452033473 | |||
| bc66f0a34c | |||
| cda987c2cb | |||
| ea202a2ab0 | |||
| dd77f6d92d | |||
| c356aebfde | |||
| 5b4a6ddd14 | |||
| 96e7fb6bc4 | |||
| dfd024cab7 | |||
| 03c0baf5b5 | |||
| b5fe6a1437 | |||
| 2e94bd90b0 | |||
| 92d80d1dfe | |||
| 971f40813f | |||
| 55383a2aa4 | |||
| 07287d2939 | |||
| c3f8ab5fb4 | |||
| 1903d77ac1 | |||
| 029b228025 | |||
| 9b730c310e | |||
| 8cd0ec92c0 | |||
| e1c4987db5 | |||
| bdba58312c | |||
| 6c9ec9a05f | |||
| f6679c5d66 | |||
| 2aa58670e3 | |||
| eb3969b952 | |||
| 44d1e9af24 | |||
| c2e6dfe88b | |||
| 17027b3968 | |||
| f841d8ba06 | |||
| aac9524316 | |||
| aee7433641 | |||
| 7729868939 | |||
| 89e4795e86 | |||
| 00a90f1c15 | |||
| 845c1134fe | |||
| 4d0d837e14 | |||
| f1f1b28b31 | |||
| 7e4f8d0e46 | |||
| db5237480a | |||
| 4c633a895f | |||
| 0520ef872e | |||
| 556058bfe9 | |||
| c7a9f7bc5a | |||
| 322885b908 | |||
| a9ce70d292 | |||
| 4dfb81af89 | |||
| 5fa2c5b6b0 | |||
| 324d1feda1 | |||
| 5456c9414a | |||
| 66933433d1 | |||
| 1b32a91b0d | |||
| bde64e7dc5 | |||
| 4ae8e28b2f | |||
| 87fd59549d | |||
| 016d050678 | |||
| 2d609f6b7a | |||
| 73470ded79 | |||
| ac83ceb2cb | |||
| b1a982aaa0 | |||
| 6f5fa762a4 | |||
| 04f5e19ab2 | |||
| f75ea4bb97 | |||
| a1ddb4a170 | |||
| 1433e07066 | |||
| 74f178f271 | |||
| a88904ed0f | |||
| 1bb5ba0061 |
10
.cargo/config.toml
Normal file
10
.cargo/config.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[env]
|
||||
MACOSX_DEPLOYMENT_TARGET = "12.0"
|
||||
|
||||
[alias]
|
||||
xtask = "run --package xtask --release --"
|
||||
|
||||
[target.x86_64-pc-windows-gnu]
|
||||
rustflags = [
|
||||
"-C", "link-args=-Wl,-Bstatic -lstdc++ -lgcc -lgcc_eh -lpthread -Wl,-Bdynamic -lmingwex -lmsvcrt -lws2_32 -liphlpapi -lwinmm -lole32 -loleaut32 -luuid -lkernel32",
|
||||
]
|
||||
135
.github/workflows/assemble-macos.yml
vendored
Normal file
135
.github/workflows/assemble-macos.yml
vendored
Normal file
@@ -0,0 +1,135 @@
|
||||
name: Assemble macOS Universal
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
assemble:
|
||||
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
|
||||
|
||||
- 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
|
||||
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: Prepare universal plugin staging
|
||||
run: |
|
||||
mkdir -p staging/clap staging/vst3
|
||||
cp -R cagire-plugins.clap staging/clap/
|
||||
cp -R cagire-plugins.vst3 staging/vst3/
|
||||
|
||||
- name: Upload universal CLAP plugin
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-macos-universal-clap
|
||||
path: staging/clap/
|
||||
|
||||
- name: Upload universal VST3 plugin
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-macos-universal-vst3
|
||||
path: staging/vst3/
|
||||
|
||||
- name: Upload DMG
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-macos-universal-dmg
|
||||
path: Cagire-*.dmg
|
||||
|
||||
- name: Upload .pkg installer
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-macos-universal-pkg
|
||||
path: Cagire-*-universal.pkg
|
||||
49
.github/workflows/build-cross.yml
vendored
Normal file
49
.github/workflows/build-cross.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: Build Cross (Linux ARM64)
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: aarch64-unknown-linux-gnu
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: aarch64-unknown-linux-gnu
|
||||
|
||||
- name: Install cross
|
||||
run: cargo install cross --git https://github.com/cross-rs/cross
|
||||
|
||||
- name: Build
|
||||
run: cross build --release --target aarch64-unknown-linux-gnu
|
||||
|
||||
- name: Build desktop
|
||||
run: cross build --release --features desktop --bin cagire-desktop --target aarch64-unknown-linux-gnu
|
||||
|
||||
- name: Upload CLI artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-linux-aarch64
|
||||
path: target/aarch64-unknown-linux-gnu/release/cagire
|
||||
|
||||
- name: Upload desktop artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-linux-aarch64-desktop
|
||||
path: target/aarch64-unknown-linux-gnu/release/cagire-desktop
|
||||
131
.github/workflows/build-linux.yml
vendored
Normal file
131
.github/workflows/build-linux.yml
vendored
Normal file
@@ -0,0 +1,131 @@
|
||||
name: Build Linux
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
run-tests:
|
||||
type: boolean
|
||||
default: false
|
||||
run-clippy:
|
||||
type: boolean
|
||||
default: false
|
||||
build-packages:
|
||||
type: boolean
|
||||
default: false
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
run-tests:
|
||||
type: boolean
|
||||
default: true
|
||||
run-clippy:
|
||||
type: boolean
|
||||
default: true
|
||||
build-packages:
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: x86_64-unknown-linux-gnu
|
||||
components: clippy
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: x86_64-unknown-linux-gnu
|
||||
|
||||
- name: Install dependencies
|
||||
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 \
|
||||
libx11-dev libx11-xcb-dev libxcursor-dev libxrandr-dev libxi-dev libwayland-dev
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release --target x86_64-unknown-linux-gnu
|
||||
|
||||
- name: Build desktop
|
||||
run: cargo build --release --features desktop --bin cagire-desktop --target x86_64-unknown-linux-gnu
|
||||
|
||||
- name: Test
|
||||
if: inputs.run-tests
|
||||
run: cargo test --target x86_64-unknown-linux-gnu
|
||||
|
||||
- name: Clippy
|
||||
if: inputs.run-clippy
|
||||
run: cargo clippy --target x86_64-unknown-linux-gnu -- -D warnings
|
||||
|
||||
- name: Install cargo-bundle
|
||||
if: inputs.build-packages
|
||||
run: cargo install cargo-bundle
|
||||
|
||||
- name: Bundle desktop app
|
||||
if: inputs.build-packages
|
||||
run: cargo bundle --release --features desktop --bin cagire-desktop --target x86_64-unknown-linux-gnu
|
||||
|
||||
- name: Build AppImages
|
||||
if: inputs.build-packages
|
||||
run: |
|
||||
mkdir -p target/releases
|
||||
scripts/make-appimage.sh target/x86_64-unknown-linux-gnu/release/cagire x86_64 target/releases
|
||||
scripts/make-appimage.sh target/x86_64-unknown-linux-gnu/release/cagire-desktop x86_64 target/releases
|
||||
|
||||
- name: Bundle CLAP plugin
|
||||
if: inputs.build-packages
|
||||
run: cargo xtask bundle cagire-plugins --release --target x86_64-unknown-linux-gnu
|
||||
|
||||
- name: Upload CLI artifact
|
||||
if: inputs.build-packages
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-linux-x86_64
|
||||
path: target/x86_64-unknown-linux-gnu/release/cagire
|
||||
|
||||
- name: Upload desktop artifact
|
||||
if: inputs.build-packages
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-linux-x86_64-desktop
|
||||
path: target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
|
||||
|
||||
- name: Upload AppImage artifacts
|
||||
if: inputs.build-packages
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-linux-x86_64-appimage
|
||||
path: target/releases/*.AppImage
|
||||
|
||||
- name: Prepare plugin artifacts
|
||||
if: inputs.build-packages
|
||||
run: |
|
||||
mkdir -p staging/clap staging/vst3
|
||||
cp -R target/bundled/cagire-plugins.clap staging/clap/
|
||||
cp -R target/bundled/cagire-plugins.vst3 staging/vst3/
|
||||
|
||||
- name: Upload CLAP artifact
|
||||
if: inputs.build-packages
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-linux-x86_64-clap
|
||||
path: staging/clap/
|
||||
|
||||
- name: Upload VST3 artifact
|
||||
if: inputs.build-packages
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-linux-x86_64-vst3
|
||||
path: staging/vst3/
|
||||
127
.github/workflows/build-macos.yml
vendored
Normal file
127
.github/workflows/build-macos.yml
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
name: Build macOS
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
run-tests:
|
||||
type: boolean
|
||||
default: false
|
||||
run-clippy:
|
||||
type: boolean
|
||||
default: false
|
||||
build-packages:
|
||||
type: boolean
|
||||
default: false
|
||||
matrix:
|
||||
type: string
|
||||
default: '[{"os":"macos-14","target":"aarch64-apple-darwin","artifact":"cagire-macos-aarch64"}]'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
run-tests:
|
||||
type: boolean
|
||||
default: true
|
||||
run-clippy:
|
||||
type: boolean
|
||||
default: true
|
||||
build-packages:
|
||||
type: boolean
|
||||
default: false
|
||||
matrix:
|
||||
type: string
|
||||
default: '[{"os":"macos-14","target":"aarch64-apple-darwin","artifact":"cagire-macos-aarch64"}]'
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
MACOSX_DEPLOYMENT_TARGET: "12.0"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include: ${{ fromJSON(inputs.matrix) }}
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
components: clippy
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: ${{ matrix.target }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: brew list cmake &>/dev/null || brew install cmake
|
||||
|
||||
- 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: Test
|
||||
if: inputs.run-tests
|
||||
run: cargo test --target ${{ matrix.target }}
|
||||
|
||||
- name: Clippy
|
||||
if: inputs.run-clippy
|
||||
run: cargo clippy --target ${{ matrix.target }} -- -D warnings
|
||||
|
||||
- name: Bundle desktop app
|
||||
if: inputs.build-packages
|
||||
run: scripts/make-app-bundle.sh ${{ matrix.target }}
|
||||
|
||||
- name: Bundle CLAP plugin
|
||||
if: inputs.build-packages
|
||||
run: cargo xtask bundle cagire-plugins --release --target ${{ matrix.target }}
|
||||
|
||||
- name: Zip macOS app bundle
|
||||
if: inputs.build-packages
|
||||
run: |
|
||||
cd target/${{ matrix.target }}/release/bundle/osx
|
||||
zip -r Cagire.app.zip Cagire.app
|
||||
|
||||
- name: Upload CLI artifact
|
||||
if: inputs.build-packages
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact }}
|
||||
path: target/${{ matrix.target }}/release/cagire
|
||||
|
||||
- name: Upload desktop artifact
|
||||
if: inputs.build-packages
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact }}-desktop
|
||||
path: target/${{ matrix.target }}/release/bundle/osx/Cagire.app.zip
|
||||
|
||||
- name: Prepare plugin artifacts
|
||||
if: inputs.build-packages
|
||||
run: |
|
||||
mkdir -p staging/clap staging/vst3
|
||||
cp -R target/bundled/cagire-plugins.clap staging/clap/
|
||||
cp -R target/bundled/cagire-plugins.vst3 staging/vst3/
|
||||
|
||||
- name: Upload CLAP artifact
|
||||
if: inputs.build-packages
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact }}-clap
|
||||
path: staging/clap/
|
||||
|
||||
- name: Upload VST3 artifact
|
||||
if: inputs.build-packages
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact }}-vst3
|
||||
path: staging/vst3/
|
||||
56
.github/workflows/build-plugins-linux.yml
vendored
Normal file
56
.github/workflows/build-plugins-linux.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: Build Plugins Linux
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: x86_64-unknown-linux-gnu
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: x86_64-unknown-linux-gnu-plugins
|
||||
|
||||
- name: Install dependencies
|
||||
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 \
|
||||
libx11-dev libx11-xcb-dev libxcursor-dev libxrandr-dev libxi-dev libwayland-dev
|
||||
|
||||
- name: Build plugins
|
||||
run: cargo xtask bundle cagire-plugins --release --target x86_64-unknown-linux-gnu
|
||||
|
||||
- name: Prepare plugin artifacts
|
||||
run: |
|
||||
mkdir -p staging/clap staging/vst3
|
||||
cp -R target/bundled/cagire-plugins.clap staging/clap/
|
||||
cp -R target/bundled/cagire-plugins.vst3 staging/vst3/
|
||||
|
||||
- name: Upload CLAP artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: plugins-linux-x86_64-clap
|
||||
path: staging/clap/
|
||||
|
||||
- name: Upload VST3 artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: plugins-linux-x86_64-vst3
|
||||
path: staging/vst3/
|
||||
66
.github/workflows/build-plugins-macos.yml
vendored
Normal file
66
.github/workflows/build-plugins-macos.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
name: Build Plugins macOS
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
matrix:
|
||||
type: string
|
||||
default: '[{"os":"macos-14","target":"aarch64-apple-darwin","artifact":"plugins-macos-aarch64"},{"os":"macos-15-intel","target":"x86_64-apple-darwin","artifact":"plugins-macos-x86_64"}]'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
matrix:
|
||||
type: string
|
||||
default: '[{"os":"macos-14","target":"aarch64-apple-darwin","artifact":"plugins-macos-aarch64"}]'
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
MACOSX_DEPLOYMENT_TARGET: "12.0"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include: ${{ fromJSON(inputs.matrix) }}
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 60
|
||||
|
||||
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 }}-plugins
|
||||
|
||||
- name: Install dependencies
|
||||
run: brew list cmake &>/dev/null || brew install cmake
|
||||
|
||||
- name: Build plugins
|
||||
run: cargo xtask bundle cagire-plugins --release --target ${{ matrix.target }}
|
||||
|
||||
- name: Prepare plugin artifacts
|
||||
run: |
|
||||
mkdir -p staging/clap staging/vst3
|
||||
cp -R target/bundled/cagire-plugins.clap staging/clap/
|
||||
cp -R target/bundled/cagire-plugins.vst3 staging/vst3/
|
||||
|
||||
- name: Upload CLAP artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact }}-clap
|
||||
path: staging/clap/
|
||||
|
||||
- name: Upload VST3 artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact }}-vst3
|
||||
path: staging/vst3/
|
||||
59
.github/workflows/build-plugins-rpi.yml
vendored
Normal file
59
.github/workflows/build-plugins-rpi.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
name: Build Plugins RPi
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: aarch64-unknown-linux-gnu
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: aarch64-unknown-linux-gnu-plugins
|
||||
|
||||
- name: Install cross
|
||||
run: cargo install cross --git https://github.com/cross-rs/cross
|
||||
|
||||
- name: Build plugins
|
||||
run: cross build --release -p cagire-plugins --target aarch64-unknown-linux-gnu
|
||||
|
||||
- name: Prepare plugin artifacts
|
||||
run: |
|
||||
mkdir -p target/bundled
|
||||
# CLAP: single .so renamed to .clap
|
||||
cp target/aarch64-unknown-linux-gnu/release/libcagire_plugins.so target/bundled/cagire-plugins.clap
|
||||
# VST3: correct directory structure
|
||||
mkdir -p "target/bundled/cagire-plugins.vst3/Contents/aarch64-linux"
|
||||
cp target/aarch64-unknown-linux-gnu/release/libcagire_plugins.so "target/bundled/cagire-plugins.vst3/Contents/aarch64-linux/cagire-plugins.so"
|
||||
|
||||
mkdir -p staging/clap staging/vst3
|
||||
cp -R target/bundled/cagire-plugins.clap staging/clap/
|
||||
cp -R target/bundled/cagire-plugins.vst3 staging/vst3/
|
||||
|
||||
- name: Upload CLAP artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: plugins-linux-aarch64-clap
|
||||
path: staging/clap/
|
||||
|
||||
- name: Upload VST3 artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: plugins-linux-aarch64-vst3
|
||||
path: staging/vst3/
|
||||
59
.github/workflows/build-plugins-windows.yml
vendored
Normal file
59
.github/workflows/build-plugins-windows.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
name: Build Plugins Windows
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: x86_64-pc-windows-msvc
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: x86_64-pc-windows-msvc-plugins
|
||||
|
||||
- name: Install dependencies
|
||||
shell: pwsh
|
||||
run: |
|
||||
choco install cmake --installargs 'ADD_CMAKE_TO_PATH=System'
|
||||
echo "C:\Program Files\CMake\bin" >> $env:GITHUB_PATH
|
||||
|
||||
- name: Build plugins
|
||||
run: cargo xtask bundle cagire-plugins --release --target x86_64-pc-windows-msvc
|
||||
|
||||
- name: Prepare plugin artifacts
|
||||
run: |
|
||||
mkdir -p staging/clap staging/vst3
|
||||
cp -R target/bundled/cagire-plugins.clap staging/clap/
|
||||
cp -R target/bundled/cagire-plugins.vst3 staging/vst3/
|
||||
|
||||
- name: Upload CLAP artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: plugins-windows-x86_64-clap
|
||||
path: staging/clap/
|
||||
|
||||
- name: Upload VST3 artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: plugins-windows-x86_64-vst3
|
||||
path: staging/vst3/
|
||||
18
.github/workflows/build-plugins.yml
vendored
Normal file
18
.github/workflows/build-plugins.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: Build Plugins
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
uses: ./.github/workflows/build-plugins-linux.yml
|
||||
|
||||
macos:
|
||||
uses: ./.github/workflows/build-plugins-macos.yml
|
||||
|
||||
windows:
|
||||
uses: ./.github/workflows/build-plugins-windows.yml
|
||||
|
||||
rpi:
|
||||
uses: ./.github/workflows/build-plugins-rpi.yml
|
||||
134
.github/workflows/build-windows.yml
vendored
Normal file
134
.github/workflows/build-windows.yml
vendored
Normal file
@@ -0,0 +1,134 @@
|
||||
name: Build Windows
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
run-tests:
|
||||
type: boolean
|
||||
default: false
|
||||
run-clippy:
|
||||
type: boolean
|
||||
default: false
|
||||
build-packages:
|
||||
type: boolean
|
||||
default: false
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
run-tests:
|
||||
type: boolean
|
||||
default: true
|
||||
run-clippy:
|
||||
type: boolean
|
||||
default: true
|
||||
build-packages:
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: x86_64-pc-windows-msvc
|
||||
components: clippy
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: x86_64-pc-windows-msvc
|
||||
|
||||
- name: Install dependencies
|
||||
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 x86_64-pc-windows-msvc
|
||||
|
||||
- name: Build desktop
|
||||
run: cargo build --release --features desktop --bin cagire-desktop --target x86_64-pc-windows-msvc
|
||||
|
||||
- name: Test
|
||||
if: inputs.run-tests
|
||||
run: cargo test --target x86_64-pc-windows-msvc
|
||||
|
||||
- name: Clippy
|
||||
if: inputs.run-clippy
|
||||
run: cargo clippy --target x86_64-pc-windows-msvc -- -D warnings
|
||||
|
||||
- name: Bundle CLAP plugin
|
||||
if: inputs.build-packages
|
||||
run: cargo xtask bundle cagire-plugins --release --target x86_64-pc-windows-msvc
|
||||
|
||||
- name: Install NSIS
|
||||
if: inputs.build-packages
|
||||
run: choco install nsis
|
||||
|
||||
- name: Build NSIS installer
|
||||
if: inputs.build-packages
|
||||
shell: pwsh
|
||||
run: |
|
||||
$version = (Select-String -Path Cargo.toml -Pattern '^version\s*=\s*"(.+)"' | Select-Object -First 1).Matches.Groups[1].Value
|
||||
$root = (Get-Location).Path
|
||||
$target = "x86_64-pc-windows-msvc"
|
||||
& "C:\Program Files (x86)\NSIS\makensis.exe" `
|
||||
"-DVERSION=$version" `
|
||||
"-DCLI_EXE=$root\target\$target\release\cagire.exe" `
|
||||
"-DDESKTOP_EXE=$root\target\$target\release\cagire-desktop.exe" `
|
||||
"-DICON=$root\assets\Cagire.ico" `
|
||||
"-DOUTDIR=$root\target" `
|
||||
nsis/cagire.nsi
|
||||
|
||||
- name: Upload CLI artifact
|
||||
if: inputs.build-packages
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-windows-x86_64
|
||||
path: target/x86_64-pc-windows-msvc/release/cagire.exe
|
||||
|
||||
- name: Upload desktop artifact
|
||||
if: inputs.build-packages
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-windows-x86_64-desktop
|
||||
path: target/x86_64-pc-windows-msvc/release/cagire-desktop.exe
|
||||
|
||||
- name: Upload installer artifact
|
||||
if: inputs.build-packages
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-windows-x86_64-installer
|
||||
path: target/cagire-*-setup.exe
|
||||
|
||||
- name: Prepare plugin artifacts
|
||||
if: inputs.build-packages
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p staging/clap staging/vst3
|
||||
cp -R target/bundled/cagire-plugins.clap staging/clap/
|
||||
cp -R target/bundled/cagire-plugins.vst3 staging/vst3/
|
||||
|
||||
- name: Upload CLAP artifact
|
||||
if: inputs.build-packages
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-windows-x86_64-clap
|
||||
path: staging/clap/
|
||||
|
||||
- name: Upload VST3 artifact
|
||||
if: inputs.build-packages
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-windows-x86_64-vst3
|
||||
path: staging/vst3/
|
||||
162
.github/workflows/ci.yml
vendored
162
.github/workflows/ci.yml
vendored
@@ -1,166 +1,28 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
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
|
||||
linux:
|
||||
uses: ./.github/workflows/build-linux.yml
|
||||
with:
|
||||
submodules: recursive
|
||||
run-tests: true
|
||||
run-clippy: true
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
macos:
|
||||
uses: ./.github/workflows/build-macos.yml
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
run-tests: true
|
||||
run-clippy: true
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
windows:
|
||||
uses: ./.github/workflows/build-windows.yml
|
||||
with:
|
||||
key: ${{ matrix.target }}
|
||||
|
||||
- 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 install cargo-bundle
|
||||
|
||||
- name: Install dependencies (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
brew list cmake &>/dev/null || brew install cmake
|
||||
cargo install 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: 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
|
||||
|
||||
release:
|
||||
needs: build
|
||||
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" == *-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
|
||||
run-tests: true
|
||||
run-clippy: true
|
||||
|
||||
1
.github/workflows/pages.yml
vendored
1
.github/workflows/pages.yml
vendored
@@ -16,6 +16,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.server_url == 'https://github.com'
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
107
.github/workflows/release.yml
vendored
Normal file
107
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags: ['v*']
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
if: github.server_url == 'https://github.com'
|
||||
uses: ./.github/workflows/build-linux.yml
|
||||
with:
|
||||
build-packages: true
|
||||
|
||||
macos:
|
||||
if: github.server_url == 'https://github.com'
|
||||
uses: ./.github/workflows/build-macos.yml
|
||||
with:
|
||||
build-packages: true
|
||||
matrix: >-
|
||||
[
|
||||
{"os":"macos-14","target":"aarch64-apple-darwin","artifact":"cagire-macos-aarch64"},
|
||||
{"os":"macos-15-intel","target":"x86_64-apple-darwin","artifact":"cagire-macos-x86_64"}
|
||||
]
|
||||
|
||||
windows:
|
||||
if: github.server_url == 'https://github.com'
|
||||
uses: ./.github/workflows/build-windows.yml
|
||||
with:
|
||||
build-packages: true
|
||||
|
||||
cross:
|
||||
if: github.server_url == 'https://github.com'
|
||||
uses: ./.github/workflows/build-cross.yml
|
||||
|
||||
assemble-macos:
|
||||
needs: macos
|
||||
uses: ./.github/workflows/assemble-macos.yml
|
||||
|
||||
release:
|
||||
needs: [linux, macos, windows, cross, assemble-macos]
|
||||
if: startsWith(github.ref, 'refs/tags/v') && github.server_url == 'https://github.com'
|
||||
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-dmg" ]]; then
|
||||
cp "$dir"/*.dmg release/
|
||||
elif [[ "$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" == *-installer ]]; then
|
||||
cp "$dir"/*-setup.exe release/
|
||||
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
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,7 +1,11 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
/.cache
|
||||
*.prof
|
||||
.DS_Store
|
||||
releases/
|
||||
|
||||
# Local cargo overrides (doux path patch)
|
||||
.cargo/config.local.toml
|
||||
|
||||
# Claude
|
||||
.claude/
|
||||
|
||||
187
BUILDING.md
Normal file
187
BUILDING.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# Building Cagire
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
git clone 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
|
||||
|
||||
**Rust** (stable toolchain): https://rustup.rs
|
||||
|
||||
## System Dependencies
|
||||
|
||||
### macOS
|
||||
|
||||
```bash
|
||||
brew install cmake
|
||||
```
|
||||
|
||||
cmake is required by `rusty_link` (Ableton Link C++ bindings). Xcode Command Line Tools provide the C++ compiler. CoreAudio and CoreMIDI are built-in. The desktop build needs no additional dependencies on macOS (Cocoa/Metal are provided by the system).
|
||||
|
||||
### Linux (Debian/Ubuntu)
|
||||
|
||||
```bash
|
||||
sudo apt install cmake g++ pkg-config libasound2-dev libjack-jackd2-dev
|
||||
```
|
||||
|
||||
For the desktop build (egui/eframe), also install:
|
||||
|
||||
```bash
|
||||
sudo apt install libgl-dev libxkbcommon-dev libx11-dev libxcursor-dev libxrandr-dev libxi-dev libwayland-dev
|
||||
```
|
||||
|
||||
### Linux (Arch)
|
||||
|
||||
```bash
|
||||
sudo pacman -S cmake gcc pkgconf alsa-lib jack2
|
||||
```
|
||||
|
||||
For the desktop build:
|
||||
|
||||
```bash
|
||||
sudo pacman -S libxkbcommon libx11 libxcursor libxrandr libxi wayland mesa
|
||||
```
|
||||
|
||||
### Linux (Fedora)
|
||||
|
||||
```bash
|
||||
sudo dnf install cmake gcc-c++ pkgconf-pkg-config alsa-lib-devel jack-audio-connection-kit-devel
|
||||
```
|
||||
|
||||
For the desktop build:
|
||||
|
||||
```bash
|
||||
sudo dnf install libxkbcommon-devel libX11-devel libXcursor-devel libXrandr-devel libXi-devel wayland-devel mesa-libGL-devel
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
Install Visual Studio Build Tools (MSVC) and CMake. Everything else is provided by the Windows SDK.
|
||||
|
||||
## Build
|
||||
|
||||
Terminal (default):
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
Desktop (egui window):
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
Terminal (default):
|
||||
|
||||
```bash
|
||||
cargo run --release -- [OPTIONS]
|
||||
```
|
||||
|
||||
Desktop (egui window):
|
||||
|
||||
```bash
|
||||
cargo run --release --features desktop --bin cagire-desktop
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-s, --samples <path>` | Sample directory (repeatable) |
|
||||
| `-o, --output <device>` | Output audio device |
|
||||
| `-i, --input <device>` | Input audio device |
|
||||
| `-c, --channels <n>` | Output channel count |
|
||||
| `-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 `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 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.
|
||||
303
CHANGELOG.md
303
CHANGELOG.md
@@ -2,7 +2,308 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased]
|
||||
## [0.1.2]
|
||||
|
||||
### Forth Language
|
||||
- Single-letter envelope aliases: `a` (attack), `d` (decay), `s` (sustain), `r` (release).
|
||||
- `sound` alias changed from `s` to `snd` (frees `s` for sustain).
|
||||
- New `partials` word: set number of active harmonics for additive oscillator.
|
||||
- Velocity parameter normalized to 0–1 float range (was 0–127 integer).
|
||||
|
||||
### UI / UX
|
||||
- **Sample Explorer as dedicated page**: the side panel is now a full page (Tab key), with keyboard navigation (j/k, search with `/`, preview with Enter), replacing the old collapsible side panel.
|
||||
- **Pulsing armed-changes bar** on patterns page: staged play/stop/mute/solo changes shown in a launch bar with animated feedback ("c to launch").
|
||||
- Pulsing highlight on banks and patterns with staged changes.
|
||||
- Sample browser shows child count on collapsed folders and uses `+`/`-` tree icons.
|
||||
- File browser modal: shows audio file counts per directory, colored path segments, and hint bar.
|
||||
- Audio devices refreshed automatically when entering the Engine page.
|
||||
- Bank prelude field added to data model (foundation for bank-level Forth scripts).
|
||||
|
||||
### Engine
|
||||
- Audio timing switched from float seconds to integer tick-based scheduling, improving timing precision.
|
||||
- Stream error handling refined: only `DeviceNotAvailable` and `StreamInvalidated` trigger device-lost recovery (non-fatal errors no longer restart the stream).
|
||||
- Step traces use `Arc` for cheaper cloning between threads.
|
||||
|
||||
### Packaging
|
||||
- **Windows: NSIS installer** replaces cargo-wix MSI. Includes optional PATH registration, Start Menu shortcut, and proper Add/Remove Programs entry with uninstaller.
|
||||
- Improved Windows cross-compilation from Unix hosts (MinGW toolchain detection).
|
||||
- CI build timeouts increased to 60 minutes across all platforms.
|
||||
- Website download matrix updated.
|
||||
|
||||
## [0.1.1]
|
||||
|
||||
### Forth Language
|
||||
- `map` word: apply a quotation to each stack element (`1 2 3 ( 10 * ) map => 10 20 30`).
|
||||
- `loop` fix: now operates in steps instead of beats, uses `step_duration()` for correct timing.
|
||||
|
||||
### Fixed
|
||||
- Crash on missing sample directories: sample path scanning now validates directories exist before scanning.
|
||||
- Audio channel minimum enforced to 2, preventing crash on devices reporting fewer channels.
|
||||
- Audio device disconnect: automatic stream restart when device is lost (terminal and desktop).
|
||||
- Live keys (e.g. `f` for fill) no longer trigger while searching in dictionary or help views.
|
||||
- Side panel always uses horizontal layout (removed broken vertical fallback for narrow terminals).
|
||||
|
||||
### Changed
|
||||
- Runtime highlight enabled by default.
|
||||
|
||||
### Packaging
|
||||
- Modular CI: split monolithic release workflow into per-platform builds (Linux, macOS, Windows, cross-compilation).
|
||||
- Separate CI workflows for CLAP/VST plugin builds (Linux, macOS, Windows, Raspberry Pi).
|
||||
- Windows MSI installer workflow fixes.
|
||||
- Website download matrix updated.
|
||||
|
||||
## [0.1.0]
|
||||
|
||||
### Breaking
|
||||
- **Quotation syntax changed from `{ }` to `( )`** — all deferred code blocks now use parentheses.
|
||||
|
||||
### Forth Language
|
||||
|
||||
**Syntax:**
|
||||
- `[ v1 v2 v3 ]` bracket lists with implicit count.
|
||||
- `( ... )` quotation syntax (replaces `{ }`).
|
||||
- `,varname` assignment syntax (SetKeep): assign without consuming.
|
||||
- `case/of/endof/endcase` control flow.
|
||||
- `print` — debug word, outputs top-of-stack as text.
|
||||
- Arithmetic and unary ops now lift over ArpList and CycleList element-wise.
|
||||
|
||||
**New words:**
|
||||
- `index` — select item at explicit index (wraps with modulo).
|
||||
- `slice` / `pick` — sample slicing: divide a sample into N equal parts and select which slice to play.
|
||||
- `wave` / `waveform` — set drum synthesis waveform (0=sine, 0.5=triangle, 1=saw).
|
||||
- `pbounce` — ping-pong cycle keyed by pattern iteration.
|
||||
- `except` — inverse of `every`.
|
||||
- `every+` / `except+` — phase-offset variants.
|
||||
- `bjork` / `pbjork` — euclidean rhythm gates using quotations.
|
||||
- `arp` — arpeggio list type (spreads notes across time).
|
||||
- `all` / `noall` — apply params globally to all emitted sounds.
|
||||
- `linmap` / `expmap` — linear and exponential range mapping.
|
||||
- `rec` / `overdub` (`dub`) — record/overdub master audio to a named sample.
|
||||
- `orec` / `odub` — record/overdub a single orbit.
|
||||
|
||||
**Harmony and voicing:**
|
||||
- `key!` — set tonal center.
|
||||
- `triad` / `seventh` — diatonic chord from scale degree.
|
||||
- `inv` / `dinv` — chord inversion / down inversion.
|
||||
- `drop2` / `drop3` — drop voicings.
|
||||
- `tp` — transpose all ints on stack by N semitones.
|
||||
|
||||
**New chord types:**
|
||||
- `pwr`, `augmaj7`, `7sus4`, `9sus4`, `maj69`, `min69`, `maj11`, `maj13`, `min13`, `dom7s11`.
|
||||
|
||||
**Effect parameters:**
|
||||
- Ducking compressor: `comp`, `compattack`/`cattack`, `comprelease`/`crelease`, `comporbit`/`corbit`.
|
||||
- Smear effect: `smear`, `smearfreq`, `smearfb`.
|
||||
- Reverb: `verbtype`, `verbchorus`, `verbchorusfreq`, `verbprelow`, `verbprehigh`, `verblowcut`, `verbhighcut`, `verblowgain`.
|
||||
|
||||
**Behavior changes:**
|
||||
- All parameter words now accept varargs (100+ words updated to consume the full stack).
|
||||
- `every` reworked to accept quotations.
|
||||
- Removed `chain` word (replaced by pattern-level Follow Up setting).
|
||||
|
||||
### Engine
|
||||
- SF2 soundfont support: auto-scans sample directories for `.sf2` files.
|
||||
- Follow-up actions: patterns have configurable follow-up (Loop, Stop, Chain). Replaces the `chain` word with a declarative UI setting (`e` key).
|
||||
- Delta-time MIDI scheduling for tighter timing.
|
||||
- Audio stream errors surfaced as flash messages.
|
||||
- Prelude script evaluated on application startup (not only on play).
|
||||
- Global periodic script: a hidden script page runs alongside all patterns at its own speed/length.
|
||||
- RestartAll command: reset all active patterns to step 0 and clear state.
|
||||
- Tempo and current beat exposed in sequencer snapshot.
|
||||
- Spectrum analyzer rescaling.
|
||||
|
||||
### UI / UX
|
||||
- **Engine page redesign**: responsive narrow/wide layout, Link/MIDI/device settings moved here from Options.
|
||||
- **Patterns view redesign**: banks column with pattern counts, expandable detail rows, bottom preview strip with mini step grid.
|
||||
- **Mouse support**: click navigation on header/grid/panels/modals, text selection in code editor (click+drag), double-click on scope/spectrum/lissajous to cycle display modes.
|
||||
- Smooth playback progress bar interpolated between steps.
|
||||
- Dynamic step grid sizing adapts to terminal height.
|
||||
- Lissajous XY scope with Braille rendering and thermal trail mode.
|
||||
- Gain boost (1x–16x) and normalize toggle for scope/lissajous/spectrum.
|
||||
- Pattern description field: editable via `d`, shown in pattern list and properties.
|
||||
- Bank/pattern import and export via clipboard (base64 serialization for sharing).
|
||||
- Mute/solo on main page now apply immediately (no staging).
|
||||
- Step name automatically cleared when deleting a step.
|
||||
- F1–F6 page navigation across the 3×2 page grid.
|
||||
- Collapsible help sections with code block copy.
|
||||
- Onboarding system for first-time users.
|
||||
- Show/hide preview pane toggle and zoom factor setting.
|
||||
- Reduced UI lag: sequencer snapshot moved after render call.
|
||||
- 10 bundled demo projects loaded on fresh startup (togglable in Options).
|
||||
- Options page: each option shows a description line below when focused.
|
||||
- Dictionary page: word list uses full page height (removed description box).
|
||||
|
||||
### Themes
|
||||
- 5 new themes: Iceberg, Everforest, Fauve, Tropicalia, Jaipur.
|
||||
- Palette-based generation: all 18 themes derived from a 14-field Palette via Oklab color space (definitions reduced from ~300 to ~20 lines each).
|
||||
|
||||
### Desktop (egui)
|
||||
- Fixed Alt/Option key on macOS (dead-key composition now works).
|
||||
- Fixed multi-character text paste.
|
||||
- Extended function key support (F13–F20).
|
||||
- No console window on Windows desktop build.
|
||||
|
||||
### Packaging
|
||||
- macOS: `.dmg` disk image with `.app` bundle (Intel + Apple Silicon fat binaries via `lipo`).
|
||||
- Windows: `.msi` installer via WiX.
|
||||
- Linux: improved AppImage build scripts and Docker cross-compilation.
|
||||
|
||||
### CLAP Plugin (experimental)
|
||||
- Early CLAP plugin support via nih-plug, baseview, and egui. Feature-gated builds separate CLI from plugin targets.
|
||||
|
||||
### Documentation
|
||||
- Complete reorganization into `docs/` subdirectories.
|
||||
- 10 getting-started guides, 5 interactive tutorials.
|
||||
- New tutorials: Recording, Soundfonts, Sharing (import/export).
|
||||
- New topics: control flow, generators, harmony, randomness, variables, timing, bracket syntax.
|
||||
- Crate-level READMEs for forth, markdown, project, ratatui.
|
||||
|
||||
### Fixed
|
||||
- CycleList + ArpList index collision: arp uses timing index, cycle uses polyphony slot.
|
||||
- Scope widget not drawing completely in some terminal sizes.
|
||||
|
||||
### Codebase
|
||||
- `src/app.rs` split into 10 focused modules.
|
||||
- `src/input.rs` split into 8 page-specific handlers.
|
||||
- Undo/redo system with scope-based tracking.
|
||||
- Feature-gated CLI vs plugin builds.
|
||||
- New reusable widgets: CategoryList, HintBar, PropsForm, ScrollIndicators, SearchBar, SectionHeader.
|
||||
|
||||
## [0.0.9]
|
||||
|
||||
### Website
|
||||
- Compressed screenshot images: resized to 1600px and converted PNG to WebP (8MB → 538KB).
|
||||
- Version number displayed in subtitle, read automatically from `Cargo.toml` at build time.
|
||||
|
||||
### Added
|
||||
- `arp` word for arpeggios: wraps stack values into an arpeggio list that spreads notes across time positions instead of playing them all simultaneously. With explicit `at` deltas, arp items zip with deltas (cycling the shorter list); without `at`, the step is auto-subdivided evenly. Example: `sine s c4 e4 g4 b4 arp note .` plays a 4-note arpeggio across the step.
|
||||
- Resolved value annotations: nondeterministic words (`rand`, `choose`, `cycle`, `bounce`, `wchoose`, `coin`, `chance`, `prob`, `exprand`, `logrand`) now display their resolved value inline (e.g., `choose [sine]`, `rand [7]`, `chance [yes]`) during playback in both Preview and Editor modals.
|
||||
- Inline sample finder in the editor: press `Ctrl+B` to open a fuzzy-search popup of all sample folder names. Type to filter, `Ctrl+N`/`Ctrl+P` to navigate, `Tab`/`Enter` to insert the folder name at cursor, `Esc` to dismiss. Mutually exclusive with word completion.
|
||||
- Sample browser now displays the 0-based file index next to each sample name, making it easy to reference samples by index in Forth scripts (e.g., `"drums" bank 0 n`).
|
||||
|
||||
### Improved
|
||||
- Header bar stats block (CPU/voices/Link peers) is now centered like all other header sections.
|
||||
- CPU percentage changes color when load is high: accent color at 50%+, error color at 80%+.
|
||||
- Extracted 6 reusable TUI components into `cagire-ratatui`: `CategoryList`, `render_scroll_indicators`, `render_search_bar`, `render_section_header`, `render_props_form`, `hint_line`. Reduces duplication across views.
|
||||
|
||||
### Fixed
|
||||
- Soundless emits (e.g., `1 gain .`) no longer stack infinite voices. All emitted commands now receive a default duration of one beat unless the user explicitly sets `dur`. Use `0 dur` for intentionally infinite voices.
|
||||
|
||||
## [0.0.8] - 2026-02-07
|
||||
|
||||
### Fixed
|
||||
- macOS `.pkg` installer bundle relocation: disabled `BundleIsRelocatable` so `Cagire.app` always installs to `/Applications/` instead of being redirected to an existing bundle location.
|
||||
|
||||
### Added
|
||||
- Syntax highlighting for user-defined Forth words: words created with `: name ... ;` now render with a distinct color in both the editor and step preview, instead of dimmed gray.
|
||||
- Multi-selection in Patterns view: Shift+Up/Down selects adjacent ranges of banks or patterns using anchor-based selection. Works with copy/paste (Ctrl+C/V), reset (Delete), toggle play (`p`), mute (`m`), and solo (`x`). Selection is column-scoped and clears on plain arrows, column switch, or Esc. Single-only actions (rename, pattern props, enter) are disabled during multi-selection.
|
||||
- Audio-rate modulation DSL: LFO words (`lfo`, `tlfo`, `wlfo`, `qlfo` for sine/triangle/sawtooth/square), transition envelopes (`slide`, `expslide`, `sslide` for linear/exponential/smooth), random modulation (`jit`, `sjit`, `drunk` for random hold/smooth random/drunk walk), and multi-segment envelope (`env`). These produce modulation strings consumed by parameter words for continuous audio-rate control.
|
||||
- Feedback delay FX words: `feedback`/`fb` (level), `fbtime`/`fbt` (delay time), `fbdamp`/`fbd` (damping), `fblfo` (LFO rate), `fblfodepth` (LFO depth), `fblfoshape` (LFO shape).
|
||||
- `bounce` word: ping-pong cycle through n items by step runs (e.g., `60 64 67 72 4 bounce` → 60 64 67 72 67 64 60 64...).
|
||||
- `wchoose` word: weighted random selection from n value/weight pairs (e.g., `60 0.6 64 0.3 67 0.1 3 wchoose`). Supports quotations.
|
||||
- New themes: **Eden** (dark forest — green-only palette on black), **Georges** (Commodore 64 palette on black), **Ember** (warm dark tones), **Letz Light** (light theme).
|
||||
- Proper desktop app icon and metadata across all platforms: moved icon to `assets/Cagire.png`, added Windows `.exe` icon and file properties embedding via `winres` build script, added PNG to cargo-bundle icon list for Linux `.deb` packaging.
|
||||
- Universal macOS `.pkg` installer in CI: combines Intel and Apple Silicon builds into fat binaries via `lipo`, then packages `Cagire.app` and CLI into a single `.pkg` installer.
|
||||
- Waveform rendering widget in the TUI.
|
||||
|
||||
### Improved
|
||||
- Sample library browser: search now shows folder names only (no files) while typing, sorted by fuzzy match score. After confirming search with Enter, folders can be expanded and collapsed normally. Esc clears the search filter before closing the panel. Left arrow on a file collapses the parent folder. Cursor and scroll position stay valid after expand/collapse operations.
|
||||
- RAM optimizations saving ~5 MB at startup plus smaller enums and fewer hot-path allocations:
|
||||
- Removed dead `Step::command` field (~3.1 MB)
|
||||
- Narrowed `Step::source` from `Option<usize>` to `Option<u8>` (~1.8 MB)
|
||||
- `Op::SetParam` and `Op::GetContext` now use `&'static str` instead of `String`
|
||||
- `SourceSpan` fields narrowed from `usize` to `u32`
|
||||
- Dirty pattern tracking uses fixed `[[bool; 32]; 32]` array instead of `HashSet`
|
||||
- Boxed `FileBrowserState` in `Modal` enum to shrink all variants
|
||||
- `StepContext::cc_access` borrows instead of cloning `Arc<dyn CcAccess>`
|
||||
- Removed unnecessary `Arc` wrapper from `Stack` type
|
||||
- Variable key cache computes on-demand with reusable buffers instead of pre-allocating 2048 Strings
|
||||
- Render pipeline: background fill uses `Clear` widget instead of generating blank paragraph lines.
|
||||
|
||||
### Fixed
|
||||
- Sequencer sync: auto-loaded patterns now use PhaseLock instead of Reset, so they align to the global beat grid and stay in sync with manually-started patterns.
|
||||
- PhaseLock off-by-one: start step calculation now uses the frontier beat instead of the lookahead end, eliminating a systematic 1-step offset.
|
||||
- Stale pattern cache on load: dirty patterns are now flushed before queued start/stop changes, ensuring pattern data arrives before activation.
|
||||
- Loading while paused no longer drops auto-started patterns; pending starts are preserved and activate on resume.
|
||||
|
||||
### Changed
|
||||
- Header bar is now always 3 lines tall with vertically centered content and full-height background colors, replacing the previous 1-or-2-line width-dependent layout.
|
||||
- Help view Welcome page: BigText title is now gated behind `cfg(not(feature = "desktop"))`, falling back to a plain text title in the desktop build (same strategy as the splash screen).
|
||||
- Space now toggles play/pause on all views, including the Patterns page where it previously toggled pattern play. Pattern play on the Patterns page is now bound to `p`.
|
||||
|
||||
## [0.0.7] - 2026-05-02
|
||||
|
||||
### Added
|
||||
- 3-operator FM synthesis words: `fm2` (operator 2 depth), `fm2h` (operator 2 harmonic ratio), `fmalgo` (algorithm: 0=cascade, 1=parallel, 2=branch), `fmfb` (feedback amount). Extends the existing 2-OP FM engine to a full 3-OP architecture with configurable routing and operator feedback.
|
||||
- Background head-preload for sample libraries. At startup, a background thread decodes the first 4096 frames (~93ms) of every sample into RAM. Short samples (most percussion/drums) are fully captured and play instantly on first trigger. Eliminates first-hit misses for live performance.
|
||||
- Most changes are on doux side. It makes sense to recompile and release now to ship a version that comes with these improvements.
|
||||
|
||||
### Fixed
|
||||
- Code editor now scrolls vertically to keep the cursor visible. Previously, lines beyond the visible area were clipped and the cursor could move off-screen.
|
||||
|
||||
## [0.0.6] - 2026-05-02
|
||||
|
||||
### Added
|
||||
- TachyonFX based animations
|
||||
- Prelude: project-level Forth script for persistent word definitions. Press `d` to edit, `D` to re-evaluate. Runs automatically on playback start and project load.
|
||||
- Varargs stack words: `rev`, `shuffle`, `sort` (ascending), `rsort` (descending), `sum`, `prod`. All take a count and operate on the top n items.
|
||||
- Euclidean rhythm words: `euclid` (k n -- hits) distributes k hits across n steps, `euclidrot` (k n r -- hits) adds rotation offset.
|
||||
- Shorthand float syntax: `.25` parses as `0.25`, `-.5` parses as `-0.5`.
|
||||
|
||||
### Changed
|
||||
- Split `words.rs` (3,078 lines) into a `words/` directory module with category-based files: `core.rs`, `sound.rs`, `effects.rs`, `sequencing.rs`, `music.rs`, `midi.rs`, plus `compile.rs` and `mod.rs`.
|
||||
- Renamed `tri` Forth word to `triangle`.
|
||||
- Sequencer rewritten with prospective lookahead scheduling. Instead of sleeping until a substep, waking late, and detecting past events, the sequencer now pre-computes all events within a ~20ms forward window. Events arrive at doux with positive time deltas, scheduled before they need to fire. Sleep+spin-wait replaced by `recv_timeout(3ms)` on the command channel. Timing no longer depends on OS sleep precision.
|
||||
- `audio_sample_pos` updated at buffer start instead of end, so `engine_time` reflects current playback position.
|
||||
- Doux grace period increased from 1ms to 50ms as a safety net (events should never be late with lookahead).
|
||||
- Flattened model re-export indirection; `script.rs` now exports only `ScriptEngine`.
|
||||
- Hue rotation step size increased from 1° to 5° for faster adjustment.
|
||||
- Moved catalog data (DOCS, CATEGORIES) from views to `src/model/`, eliminating state-to-view layer inversion.
|
||||
- Extracted shared initialization into `src/init.rs`, deduplicating ~140 lines between terminal and desktop binaries.
|
||||
- Split App dispatch into focused service modules (`help_nav`, `dict_nav`, `euclidean`, `clipboard`, extended `pattern_editor`), reducing `app.rs` by ~310 lines.
|
||||
- Moved stack preview computation from render path to input time, making editor rendering pure.
|
||||
- Decoupled script runtime state between UI and sequencer threads, eliminating shared mutexes on the RT path.
|
||||
|
||||
### Fixed
|
||||
- Prelude content no longer leaks into step editor. Closing the prelude editor now restores the current step's content to the buffer.
|
||||
- Desktop binary now loads color theme and connects MIDI devices on startup (was missing).
|
||||
- Audio commands no longer silently dropped when channel is full; switched to unbounded channel matching MIDI dispatch pattern.
|
||||
- PatternProps and EuclideanDistribution modals now use the global theme background instead of the terminal default.
|
||||
- Changing pattern properties is now a stage/commit operation.
|
||||
- Changing pattern speed only happens at pattern boundaries.
|
||||
- `mlockall` warning no longer appears on macOS; memory locking is now Linux-only.
|
||||
- `clear` now resets `at` deltas, so subsequent emits default to a single emit at position 0.
|
||||
|
||||
## [0.0.5] - 2026-04-02
|
||||
|
||||
### Added
|
||||
- Mute/solo for patterns: stage with `m`/`x`, commit with `c`. Solo mutes all other patterns. Clear with `M`/`X`.
|
||||
- Lookahead scheduling: scripts are pre-evaluated ahead of time and audio commands are scheduled at precise beat positions, improving timing accuracy under CPU load.
|
||||
- Realtime thread scheduling (`SCHED_FIFO`) for sequencer thread on Unix systems, improving timing reliability.
|
||||
- Deep into the Linux hellscape: trying to get reliable performance, better stability, etc.
|
||||
|
||||
### Fixed
|
||||
- Editor completion popup no longer steals arrow keys. Arrow keys always move the cursor; use Ctrl+N/Ctrl+P to navigate the completion list.
|
||||
|
||||
## [0.0.4] - 2026-02-02
|
||||
|
||||
### Added
|
||||
- Double-stack words: `2dup`, `2drop`, `2swap`, `2over`.
|
||||
- `forget` word to remove user-defined words from the dictionary.
|
||||
- Active patterns panel showing playing patterns with bank, pattern, iteration count, and step position.
|
||||
- Configurable visualization layout (Top/Bottom/Left/Right) for scope and spectrum placement.
|
||||
- Euclidean distribution modal to spread a step's script across the pattern using Euclidean rhythms.
|
||||
- Fairyfloss theme (pastel candy colors by sailorhg).
|
||||
- Hot Dog Stand theme (classic Windows 3.1 red/yellow).
|
||||
- Hue rotation option in Options menu to shift all theme colors (0-360°).
|
||||
|
||||
### Changed
|
||||
- Title view now adapts to smaller terminal sizes gracefully.
|
||||
|
||||
### Fixed
|
||||
- Scope/spectrum ratio asymmetry in Left/Right layout modes.
|
||||
- Updated `cpal` dependency from 0.15 to 0.17 to fix type mismatch with `doux` audio backend.
|
||||
- Copy/paste (Ctrl+C/V/X) not working in desktop version due to egui intercepting clipboard shortcuts.
|
||||
|
||||
## [0.0.3] - 2026-02-02
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ Contributions are welcome. There are many ways to contribute beyond code:
|
||||
## Prerequisites
|
||||
|
||||
- **Rust** (stable toolchain) - [rustup.rs](https://rustup.rs/)
|
||||
- **System libraries** - See [BUILDING.md](BUILDING.md) for platform-specific packages (cmake, ALSA, etc.)
|
||||
|
||||
## Quick start
|
||||
|
||||
|
||||
7618
Cargo.lock
generated
Normal file
7618
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
55
Cargo.toml
55
Cargo.toml
@@ -1,8 +1,8 @@
|
||||
[workspace]
|
||||
members = ["crates/forth", "crates/markdown", "crates/project", "crates/ratatui"]
|
||||
members = ["crates/forth", "crates/markdown", "crates/project", "crates/ratatui", "plugins/cagire-plugins", "plugins/baseview", "plugins/egui-baseview", "plugins/nih-plug-egui", "xtask"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.0.3"
|
||||
version = "0.1.3"
|
||||
edition = "2021"
|
||||
authors = ["Raphaël Forment <raphael.forment@gmail.com>"]
|
||||
license = "AGPL-3.0"
|
||||
@@ -30,18 +30,20 @@ path = "src/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "cagire-desktop"
|
||||
path = "src/bin/desktop.rs"
|
||||
path = "src/bin/desktop/main.rs"
|
||||
required-features = ["desktop"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["cli"]
|
||||
cli = ["dep:cpal", "dep:midir", "dep:confy", "dep:clap", "dep:thread-priority"]
|
||||
block-renderer = ["dep:soft_ratatui", "dep:rustc-hash", "dep:egui"]
|
||||
desktop = [
|
||||
"cli",
|
||||
"block-renderer",
|
||||
"cagire-forth/desktop",
|
||||
"egui",
|
||||
"eframe",
|
||||
"egui_ratatui",
|
||||
"soft_ratatui",
|
||||
"image",
|
||||
"dep:eframe",
|
||||
"dep:egui_ratatui",
|
||||
"dep:image",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
@@ -49,33 +51,44 @@ cagire-forth = { path = "crates/forth" }
|
||||
cagire-markdown = { path = "crates/markdown" }
|
||||
cagire-project = { path = "crates/project" }
|
||||
cagire-ratatui = { path = "crates/ratatui" }
|
||||
doux = { git = "https://github.com/sova-org/doux", features = ["native"] }
|
||||
doux = { git = "https://github.com/sova-org/doux", features = ["native", "soundfont"] }
|
||||
rusty_link = "0.4"
|
||||
ratatui = "0.30"
|
||||
crossterm = "0.29"
|
||||
cpal = "0.15"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
cpal = { version = "0.17", optional = true }
|
||||
clap = { version = "4", features = ["derive"], optional = true }
|
||||
rand = "0.8"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tachyonfx = { version = "0.22", features = ["std-duration"] }
|
||||
tui-big-text = "0.8"
|
||||
arboard = "3"
|
||||
minimad = "0.13"
|
||||
crossbeam-channel = "0.5"
|
||||
confy = "2"
|
||||
confy = { version = "2", optional = true }
|
||||
rustfft = "6"
|
||||
thread-priority = "1"
|
||||
thread-priority = { version = "1", optional = true }
|
||||
ringbuf = "0.4"
|
||||
arc-swap = "1"
|
||||
midir = "0.10"
|
||||
midir = { version = "0.10", optional = true }
|
||||
parking_lot = "0.12"
|
||||
libc = "0.2"
|
||||
|
||||
# Desktop-only dependencies (behind feature flag)
|
||||
egui = { version = "0.33", optional = true }
|
||||
eframe = { version = "0.33", optional = true }
|
||||
egui_ratatui = { version = "2.1", optional = true }
|
||||
soft_ratatui = { version = "0.1.3", features = ["unicodefonts"], optional = true }
|
||||
rustc-hash = { version = "2", optional = true }
|
||||
image = { version = "0.25", default-features = false, features = ["png"], optional = true }
|
||||
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
cpal = { version = "0.17", optional = true, features = ["jack"] }
|
||||
|
||||
[build-dependencies]
|
||||
winres = "0.1"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = "fat"
|
||||
@@ -83,10 +96,20 @@ codegen-units = 1
|
||||
panic = "abort"
|
||||
strip = true
|
||||
|
||||
[patch."https://github.com/robbert-vdh/nih-plug"]
|
||||
nih_plug_egui = { path = "plugins/nih-plug-egui" }
|
||||
|
||||
[patch."https://github.com/BillyDM/egui-baseview.git"]
|
||||
egui-baseview = { path = "plugins/egui-baseview" }
|
||||
|
||||
[patch."https://github.com/RustAudio/baseview.git"]
|
||||
baseview = { path = "plugins/baseview" }
|
||||
|
||||
[package.metadata.bundle.bin.cagire-desktop]
|
||||
name = "Cagire"
|
||||
identifier = "com.sova.cagire"
|
||||
icon = ["assets/Cagire.icns", "assets/Cagire.ico"]
|
||||
icon = ["assets/Cagire.icns", "assets/Cagire.ico", "assets/Cagire.png"]
|
||||
copyright = "Copyright (c) 2025 Raphaël Forment"
|
||||
category = "Music"
|
||||
short_description = "Forth-based music sequencer"
|
||||
minimum_system_version = "12.0"
|
||||
|
||||
8
Cross.toml
Normal file
8
Cross.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
dockerfile = "./scripts/cross/aarch64-linux.Dockerfile"
|
||||
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
dockerfile = "./scripts/cross/x86_64-linux.Dockerfile"
|
||||
|
||||
[target.x86_64-pc-windows-gnu]
|
||||
dockerfile = "./scripts/cross/x86_64-windows.Dockerfile"
|
||||
107
README.md
107
README.md
@@ -1,37 +1,84 @@
|
||||
<h1 align="center">Cagire</h1>
|
||||
|
||||
<p align="center"><em>A Forth Music Sequencer</em></p>
|
||||
<p align="center"><em>A Forth-based live coding sequencer</em></p>
|
||||
|
||||
<p align="center">
|
||||
<img src="cagire_pixel.png" alt="Cagire" width="256">
|
||||
<img src="assets/Cagire.png" alt="Cagire" width="256">
|
||||
</p>
|
||||
|
||||
Cagire is a terminal-based step sequencer for live coding music. Each step in a pattern contains a **Forth** script that produces sound and create events. It is made by BuboBubo (Raphaël Maurice Forment): [https://raphaelforment.fr](https://raphaelforment.fr). Cagire is open-source (AGPL-3.0 licensed) and available on GitHub : [https://github.com/BuboBubo/cagire](https://github.com/BuboBubo/cagire).
|
||||
|
||||
## Build
|
||||
|
||||
Terminal version:
|
||||
```
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
Desktop version (with egui window):
|
||||
```
|
||||
cargo build --release --features desktop --bin cagire-desktop
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
Terminal version:
|
||||
```
|
||||
cargo run --release
|
||||
```
|
||||
|
||||
Desktop version:
|
||||
```
|
||||
cargo run --release --features desktop --bin cagire-desktop
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
<p align="center">
|
||||
<a href="https://cagire.raphaelforment.fr">Website</a> ·
|
||||
<a href="https://github.com/Bubobubobubobubo/cagire">GitHub</a> ·
|
||||
AGPL-3.0
|
||||
</p>
|
||||
|
||||
Cagire is a terminal based step sequencer and live coding platform. Each step in a sequence is represented by a **Forth** script. It ships with a self-contained audio engine. No external software is needed, Cagire is a fully autonomous musical instrument that provides everything you need to perform.
|
||||
|
||||
### Examples
|
||||
|
||||
A filtered sawtooth with reverb:
|
||||
|
||||
```forth
|
||||
saw sound
|
||||
200 199 freq
|
||||
400 lpf
|
||||
.8 lpq .3 verb
|
||||
.
|
||||
```
|
||||
|
||||
A generative pattern using randomness, scales, and effects:
|
||||
|
||||
```forth
|
||||
sine sound 2 fm 0.5 fmh
|
||||
0 7 rand minor 50 + note
|
||||
.1 .8 rrand cutoff
|
||||
1 4 irand 10 * delay .5 delayfb
|
||||
.
|
||||
```
|
||||
|
||||
### Features
|
||||
|
||||
- **Cagire's Forth**: a stack-based language made for live coding
|
||||
- Forth has almost no syntax, only words, numbers and spaces. Very easy to learn for beginners, quite deep for experienced programmers.
|
||||
- Nondeterminism and generative: randomness, probabilities, patterns thought as first-class features.
|
||||
- Quotations: code blocks `( ... )` that compose with probability, cycling, euclidean, and conditional words.
|
||||
- User-defined words: extend (or redefine) the language on the fly with `:name ... ;` definitions.
|
||||
- Interactive documentation: built-in tutorials with runnable examples.
|
||||
- **Audio engine** (powered by [Doux](https://doux.livecoding.fr)):
|
||||
- Synthesis: classic waveforms (saw, pulse, tri, sine), additive, FM (2-op, 3 algorithms), additive synthesis, wavetables, 7-voice spread, Mutable Instruments Plaits models: modal, granular, waveshaping, chord, swarm, etc.
|
||||
- Drum models: seven drum models with timbral morphing.
|
||||
- Sampling: disk-loaded samples with slicing, looping, pitch tracking, wavetable mode, and live recording from engine output or line input.
|
||||
- Filters: biquad LP/HP/BP and ladder filters, each with independent envelope. Filters can be modulated, stacked, etc.
|
||||
- Effects: phaser, flanger, chorus, smear, distortion, wavefolder, wavewrapper, bitcrusher, sample-rate reduction, 3-band EQ, tilt EQ, Haas stereo.
|
||||
- Bus effects: delay (standard, ping-pong, tape, multitap), two reverb engines (Dattorro plate, Vital Space), comb filter, feedback delay with LFO, sidechain compressor.
|
||||
- Modulation: vibrato, AM, ring mod, pitch envelope, FM envelope, glide — all with selectable LFO shapes (sine, tri, saw, square, sample & hold).
|
||||
- **Sequencing**: probabilities, patterns, euclidean structures, sub-step timing, pattern chaining and a lot more.
|
||||
- **MIDI**: receive or send MIDI messages across up to 4 inputs and 4 outputs.
|
||||
- **Ableton Link**: tempo and phase sync with any Link-enabled software or hardware.
|
||||
- **Cross-platform**: terminal and desktop interfaces on macOS, Linux, and Windows.
|
||||
- **Plugins**: run Cagire as a CLAP or VST3 plugin inside your DAW (separate version).
|
||||
|
||||
### Getting started
|
||||
|
||||
Download the latest release for your platform from the [website](https://cagire.raphaelforment.fr).
|
||||
|
||||
To build from source instead, see [BUILDING.md](BUILDING.md).
|
||||
|
||||
### Documentation
|
||||
|
||||
Cagire includes interactive documentation with runnable code examples. Press **F1** in the application to open it.
|
||||
|
||||
- [Website](https://cagire.raphaelforment.fr)
|
||||
- [BUILDING.md](BUILDING.md) — build instructions and CLI flags
|
||||
- [CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
### Credits
|
||||
|
||||
Cagire is developed by [BuboBubo](https://raphaelforment.fr) (Raphael Forment).
|
||||
|
||||
- **[Doux](https://doux.livecoding.fr)** (audio engine) — Rust port of Dough, originally written in C by Felix Roos
|
||||
- **mi-plaits-dsp-rs** — Rust port of Mutable Instruments Plaits DSP by Oliver Rockstedt, original code by Emilie Gillet
|
||||
|
||||
### License
|
||||
|
||||
[AGPL-3.0](LICENSE)
|
||||
|
||||
BIN
assets/Cagire.png
Normal file
BIN
assets/Cagire.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
18
assets/DMG-README.txt
Normal file
18
assets/DMG-README.txt
Normal file
@@ -0,0 +1,18 @@
|
||||
# Cagire - A Forth-based music sequencer
|
||||
|
||||
## 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. Thanks Apple! To fix this, open
|
||||
Terminal and run:
|
||||
|
||||
xattr -cr /Applications/Cagire.app
|
||||
|
||||
## Support
|
||||
|
||||
If you enjoy this software, consider supporting development:
|
||||
https://ko-fi.com/raphaelbubo
|
||||
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;
|
||||
38
build.rs
Normal file
38
build.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
//! Build script — embeds Windows application resources (icon, metadata).
|
||||
|
||||
fn main() {
|
||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||
|
||||
if target_os == "windows" {
|
||||
// C++ runtime (stdc++, gcc, gcc_eh, pthread) linked statically via .cargo/config.toml
|
||||
// using -Wl,-Bstatic. Only Windows system DLLs go here.
|
||||
println!("cargo:rustc-link-lib=ws2_32");
|
||||
println!("cargo:rustc-link-lib=iphlpapi");
|
||||
println!("cargo:rustc-link-lib=winmm");
|
||||
println!("cargo:rustc-link-lib=ole32");
|
||||
println!("cargo:rustc-link-lib=oleaut32");
|
||||
}
|
||||
|
||||
if target_os == "windows" {
|
||||
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
let icon = format!("{manifest_dir}/assets/Cagire.ico");
|
||||
let mut res = winres::WindowsResource::new();
|
||||
// Cross-compiling from Unix: use prefixed MinGW tools
|
||||
if cfg!(unix) {
|
||||
res.set_windres_path("x86_64-w64-mingw32-windres");
|
||||
res.set_ar_path("x86_64-w64-mingw32-ar");
|
||||
res.set_toolkit_path("/");
|
||||
}
|
||||
res.set_icon(&icon)
|
||||
.set("ProductName", "Cagire")
|
||||
.set("FileDescription", "Forth-based music sequencer")
|
||||
.set("LegalCopyright", "Copyright (c) 2025 Raphaël Forment");
|
||||
res.compile().expect("Failed to compile Windows resources");
|
||||
// GNU ld discards unreferenced sections from static archives,
|
||||
// so link the resource object directly to ensure .rsrc is kept.
|
||||
if cfg!(unix) {
|
||||
let out_dir = std::env::var("OUT_DIR").unwrap();
|
||||
println!("cargo:rustc-link-arg-bins={out_dir}/resource.o");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,3 +13,5 @@ desktop = []
|
||||
|
||||
[dependencies]
|
||||
rand = "0.8"
|
||||
parking_lot = "0.12"
|
||||
arc-swap = "1"
|
||||
|
||||
22
crates/forth/README.md
Normal file
22
crates/forth/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# cagire-forth
|
||||
|
||||
Stack-based Forth VM for the Cagire sequencer. Tokenizes, compiles, and executes step scripts to produce audio and MIDI commands.
|
||||
|
||||
## Modules
|
||||
|
||||
| Module | Description |
|
||||
|--------|-------------|
|
||||
| `vm` | Interpreter loop, `Forth::evaluate()` entry point |
|
||||
| `compiler` | Tokenization (with source spans) and single-pass compilation to ops |
|
||||
| `ops` | `Op` enum (~90 variants) |
|
||||
| `types` | `Value`, `StepContext`, shared state types |
|
||||
| `words/` | Built-in word definitions: `core`, `sound`, `music`, `midi`, `effects`, `sequencing`, `compile` |
|
||||
| `theory/` | Music theory lookups: `scales` (~200 patterns), `chords` (interval arrays) |
|
||||
|
||||
## Key Types
|
||||
|
||||
- **`Forth`** — VM instance, holds stacks and compilation state
|
||||
- **`Value`** — Stack value (int, float, string, list, quotation, ...)
|
||||
- **`StepContext`** — Per-step evaluation context (step index, tempo, variables, ...)
|
||||
- **`Op`** — Compiled operation; nondeterministic variants carry `Option<SourceSpan>` for tracing
|
||||
- **`ExecutionTrace`** — Records executed/selected spans and resolved values during evaluation
|
||||
@@ -1,3 +1,8 @@
|
||||
//! Single-pass compiler from Forth source text to Op sequences.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::ops::Op;
|
||||
use super::types::{Dictionary, SourceSpan};
|
||||
use super::words::compile_word;
|
||||
@@ -10,6 +15,7 @@ enum Token {
|
||||
Word(String, SourceSpan),
|
||||
}
|
||||
|
||||
/// Compile Forth source text into an executable Op sequence.
|
||||
pub(super) fn compile_script(input: &str, dict: &Dictionary) -> Result<Vec<Op>, String> {
|
||||
let tokens = tokenize(input);
|
||||
compile(&tokens, dict)
|
||||
@@ -25,7 +31,7 @@ fn tokenize(input: &str) -> Vec<Token> {
|
||||
continue;
|
||||
}
|
||||
|
||||
if c == '(' || c == ')' {
|
||||
if c == '{' || c == '}' {
|
||||
chars.next();
|
||||
continue;
|
||||
}
|
||||
@@ -43,7 +49,7 @@ fn tokenize(input: &str) -> Vec<Token> {
|
||||
}
|
||||
s.push(ch);
|
||||
}
|
||||
tokens.push(Token::Str(s, SourceSpan { start, end }));
|
||||
tokens.push(Token::Str(s, SourceSpan { start: start as u32, end: end as u32 }));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -64,8 +70,8 @@ fn tokenize(input: &str) -> Vec<Token> {
|
||||
tokens.push(Token::Word(
|
||||
";".to_string(),
|
||||
SourceSpan {
|
||||
start: pos,
|
||||
end: pos + 1,
|
||||
start: pos as u32,
|
||||
end: (pos + 1) as u32,
|
||||
},
|
||||
));
|
||||
continue;
|
||||
@@ -83,10 +89,26 @@ fn tokenize(input: &str) -> Vec<Token> {
|
||||
chars.next();
|
||||
}
|
||||
|
||||
let span = SourceSpan { start, end };
|
||||
if let Ok(i) = word.parse::<i64>() {
|
||||
let span = SourceSpan { start: start as u32, end: end as u32 };
|
||||
|
||||
// Normalize shorthand float syntax: .25 -> 0.25, -.5 -> -0.5
|
||||
let word_to_parse: Cow<str> = if word.starts_with('.')
|
||||
&& word.len() > 1
|
||||
&& word.as_bytes()[1].is_ascii_digit()
|
||||
{
|
||||
Cow::Owned(format!("0{word}"))
|
||||
} else if word.starts_with("-.")
|
||||
&& word.len() > 2
|
||||
&& word.as_bytes()[2].is_ascii_digit()
|
||||
{
|
||||
Cow::Owned(format!("-0{}", &word[1..]))
|
||||
} else {
|
||||
Cow::Borrowed(&word)
|
||||
};
|
||||
|
||||
if let Ok(i) = word_to_parse.parse::<i64>() {
|
||||
tokens.push(Token::Int(i, span));
|
||||
} else if let Ok(f) = word.parse::<f64>() {
|
||||
} else if let Ok(f) = word_to_parse.parse::<f64>() {
|
||||
tokens.push(Token::Float(f, span));
|
||||
} else {
|
||||
tokens.push(Token::Word(word, span));
|
||||
@@ -103,25 +125,15 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
|
||||
while i < tokens.len() {
|
||||
match &tokens[i] {
|
||||
Token::Int(n, span) => {
|
||||
let key = n.to_string();
|
||||
if let Some(body) = dict.lock().unwrap().get(&key).cloned() {
|
||||
ops.extend(body);
|
||||
} else {
|
||||
ops.push(Op::PushInt(*n, Some(*span)));
|
||||
}
|
||||
}
|
||||
Token::Float(f, span) => {
|
||||
let key = f.to_string();
|
||||
if let Some(body) = dict.lock().unwrap().get(&key).cloned() {
|
||||
ops.extend(body);
|
||||
} else {
|
||||
ops.push(Op::PushFloat(*f, Some(*span)));
|
||||
}
|
||||
}
|
||||
Token::Str(s, span) => ops.push(Op::PushStr(s.clone(), Some(*span))),
|
||||
Token::Str(s, span) => ops.push(Op::PushStr(Arc::from(s.as_str()), Some(*span))),
|
||||
Token::Word(w, span) => {
|
||||
let word = w.as_str();
|
||||
if word == "{" {
|
||||
if word == "(" {
|
||||
let (quote_ops, consumed, end_span) =
|
||||
compile_quotation(&tokens[i + 1..], dict)?;
|
||||
i += consumed;
|
||||
@@ -129,13 +141,26 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
|
||||
start: span.start,
|
||||
end: end_span.end,
|
||||
};
|
||||
ops.push(Op::Quotation(quote_ops, Some(body_span)));
|
||||
} else if word == "}" {
|
||||
return Err("unexpected }".into());
|
||||
ops.push(Op::Quotation(Arc::from(quote_ops), Some(body_span)));
|
||||
} else if word == ")" {
|
||||
return Err("unexpected )".into());
|
||||
} else if word == "[" {
|
||||
let (bracket_ops, consumed, end_span) =
|
||||
compile_bracket(&tokens[i + 1..], dict)?;
|
||||
i += consumed;
|
||||
ops.push(Op::Mark);
|
||||
ops.extend(bracket_ops);
|
||||
let count_span = SourceSpan {
|
||||
start: span.start,
|
||||
end: end_span.end,
|
||||
};
|
||||
ops.push(Op::Count(Some(count_span)));
|
||||
} else if word == "]" {
|
||||
return Err("unexpected ]".into());
|
||||
} else if word == ":" {
|
||||
let (consumed, name, body) = compile_colon_def(&tokens[i + 1..], dict)?;
|
||||
i += consumed;
|
||||
dict.lock().unwrap().insert(name, body);
|
||||
dict.lock().insert(name, body);
|
||||
} else if word == ";" {
|
||||
return Err("unexpected ;".into());
|
||||
} else if word == "if" {
|
||||
@@ -151,6 +176,12 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
|
||||
ops.push(Op::Branch(else_ops.len()));
|
||||
ops.extend(else_ops);
|
||||
}
|
||||
} else if word == "case" {
|
||||
let (case_ops, consumed) = compile_case(&tokens[i + 1..], dict)?;
|
||||
i += consumed;
|
||||
ops.extend(case_ops);
|
||||
} else if word == "of" || word == "endof" || word == "endcase" {
|
||||
return Err(format!("unexpected '{word}'"));
|
||||
} else if !compile_word(word, Some(*span), &mut ops, dict) {
|
||||
return Err(format!("unknown word: {word}"));
|
||||
}
|
||||
@@ -172,8 +203,8 @@ fn compile_quotation(
|
||||
for (i, tok) in tokens.iter().enumerate() {
|
||||
if let Token::Word(w, _) = tok {
|
||||
match w.as_str() {
|
||||
"{" => depth += 1,
|
||||
"}" => {
|
||||
"(" => depth += 1,
|
||||
")" => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
end_idx = Some(i);
|
||||
@@ -185,7 +216,7 @@ fn compile_quotation(
|
||||
}
|
||||
}
|
||||
|
||||
let end_idx = end_idx.ok_or("missing }")?;
|
||||
let end_idx = end_idx.ok_or("missing )")?;
|
||||
let end_span = match &tokens[end_idx] {
|
||||
Token::Word(_, span) => *span,
|
||||
_ => unreachable!(),
|
||||
@@ -194,6 +225,38 @@ fn compile_quotation(
|
||||
Ok((quote_ops, end_idx + 1, end_span))
|
||||
}
|
||||
|
||||
fn compile_bracket(
|
||||
tokens: &[Token],
|
||||
dict: &Dictionary,
|
||||
) -> Result<(Vec<Op>, usize, SourceSpan), String> {
|
||||
let mut depth = 1;
|
||||
let mut end_idx = None;
|
||||
|
||||
for (i, tok) in tokens.iter().enumerate() {
|
||||
if let Token::Word(w, _) = tok {
|
||||
match w.as_str() {
|
||||
"[" => depth += 1,
|
||||
"]" => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
end_idx = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let end_idx = end_idx.ok_or("missing ]")?;
|
||||
let end_span = match &tokens[end_idx] {
|
||||
Token::Word(_, span) => *span,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let body_ops = compile(&tokens[..end_idx], dict)?;
|
||||
Ok((body_ops, end_idx + 1, end_span))
|
||||
}
|
||||
|
||||
fn token_span(tok: &Token) -> Option<SourceSpan> {
|
||||
match tok {
|
||||
Token::Int(_, s) | Token::Float(_, s) | Token::Str(_, s) | Token::Word(_, s) => Some(*s),
|
||||
@@ -291,3 +354,69 @@ fn compile_if(
|
||||
|
||||
Ok((then_ops, else_ops, then_pos + 1, then_span, else_span))
|
||||
}
|
||||
|
||||
fn compile_case(tokens: &[Token], dict: &Dictionary) -> Result<(Vec<Op>, usize), String> {
|
||||
let mut depth = 1;
|
||||
let mut endcase_pos = None;
|
||||
let mut clauses: Vec<(usize, usize)> = Vec::new();
|
||||
let mut last_of = None;
|
||||
|
||||
for (i, tok) in tokens.iter().enumerate() {
|
||||
if let Token::Word(w, _) = tok {
|
||||
match w.as_str() {
|
||||
"case" => depth += 1,
|
||||
"endcase" => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
endcase_pos = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
"of" if depth == 1 => last_of = Some(i),
|
||||
"endof" if depth == 1 => {
|
||||
let of_pos = last_of.ok_or("'endof' without matching 'of'")?;
|
||||
clauses.push((of_pos, i));
|
||||
last_of = None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let endcase_pos = endcase_pos.ok_or("missing 'endcase'")?;
|
||||
|
||||
let mut ops = Vec::new();
|
||||
let mut branch_fixups: Vec<usize> = Vec::new();
|
||||
let mut clause_start = 0;
|
||||
|
||||
for &(of_pos, endof_pos) in &clauses {
|
||||
let test_ops = compile(&tokens[clause_start..of_pos], dict)?;
|
||||
let body_ops = compile(&tokens[of_pos + 1..endof_pos], dict)?;
|
||||
|
||||
ops.extend(test_ops);
|
||||
ops.push(Op::Over);
|
||||
ops.push(Op::Eq);
|
||||
ops.push(Op::BranchIfZero(body_ops.len() + 2, None, None));
|
||||
ops.push(Op::Drop);
|
||||
ops.extend(body_ops);
|
||||
branch_fixups.push(ops.len());
|
||||
ops.push(Op::Branch(0));
|
||||
|
||||
clause_start = endof_pos + 1;
|
||||
}
|
||||
|
||||
let default_tokens = &tokens[clause_start..endcase_pos];
|
||||
if !default_tokens.is_empty() {
|
||||
let default_ops = compile(default_tokens, dict)?;
|
||||
ops.extend(default_ops);
|
||||
}
|
||||
|
||||
ops.push(Op::Drop);
|
||||
|
||||
let end = ops.len();
|
||||
for pos in branch_fixups {
|
||||
ops[pos] = Op::Branch(end - pos - 1);
|
||||
}
|
||||
|
||||
Ok((ops, endcase_pos + 1))
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Forth virtual machine for the Cagire music sequencer.
|
||||
|
||||
mod compiler;
|
||||
mod ops;
|
||||
mod theory;
|
||||
@@ -6,7 +8,8 @@ mod vm;
|
||||
mod words;
|
||||
|
||||
pub use types::{
|
||||
CcAccess, Dictionary, ExecutionTrace, Rng, SourceSpan, StepContext, Value, Variables,
|
||||
CcAccess, Dictionary, ExecutionTrace, ResolvedValue, Rng, SourceSpan, StepContext, Value,
|
||||
Variables, VariablesMap,
|
||||
};
|
||||
pub use vm::Forth;
|
||||
pub use words::{Word, WordCompile, WORDS};
|
||||
pub use words::{lookup_word, Word, WordCompile, WORDS};
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
//! Compiled operation variants for the Forth VM instruction set.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::types::SourceSpan;
|
||||
|
||||
/// Single VM instruction produced by the compiler.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Op {
|
||||
PushInt(i64, Option<SourceSpan>),
|
||||
PushFloat(f64, Option<SourceSpan>),
|
||||
PushStr(String, Option<SourceSpan>),
|
||||
PushStr(Arc<str>, Option<SourceSpan>),
|
||||
Dup,
|
||||
Dupn,
|
||||
Drop,
|
||||
@@ -13,6 +18,17 @@ pub enum Op {
|
||||
Rot,
|
||||
Nip,
|
||||
Tuck,
|
||||
Dup2,
|
||||
Drop2,
|
||||
Swap2,
|
||||
Over2,
|
||||
Rev,
|
||||
Shuffle,
|
||||
Sort,
|
||||
RSort,
|
||||
Sum,
|
||||
Prod,
|
||||
Forget,
|
||||
Add,
|
||||
Sub,
|
||||
Mul,
|
||||
@@ -47,47 +63,79 @@ pub enum Op {
|
||||
BranchIfZero(usize, Option<SourceSpan>, Option<SourceSpan>),
|
||||
Branch(usize),
|
||||
NewCmd,
|
||||
SetParam(String),
|
||||
SetParam(&'static str),
|
||||
Emit,
|
||||
Print,
|
||||
Get,
|
||||
Set,
|
||||
GetContext(String),
|
||||
Rand,
|
||||
ExpRand,
|
||||
LogRand,
|
||||
SetKeep,
|
||||
GetContext(&'static str),
|
||||
Rand(Option<SourceSpan>),
|
||||
ExpRand(Option<SourceSpan>),
|
||||
LogRand(Option<SourceSpan>),
|
||||
Seed,
|
||||
Cycle,
|
||||
PCycle,
|
||||
Choose,
|
||||
ChanceExec,
|
||||
ProbExec,
|
||||
Coin,
|
||||
Cycle(Option<SourceSpan>),
|
||||
PCycle(Option<SourceSpan>),
|
||||
Choose(Option<SourceSpan>),
|
||||
Bounce(Option<SourceSpan>),
|
||||
PBounce(Option<SourceSpan>),
|
||||
WChoose(Option<SourceSpan>),
|
||||
ChanceExec(Option<SourceSpan>),
|
||||
ProbExec(Option<SourceSpan>),
|
||||
Coin(Option<SourceSpan>),
|
||||
Mtof,
|
||||
Ftom,
|
||||
SetTempo,
|
||||
Every,
|
||||
Quotation(Vec<Op>, Option<SourceSpan>),
|
||||
Every(Option<SourceSpan>),
|
||||
Except(Option<SourceSpan>),
|
||||
EveryOffset(Option<SourceSpan>),
|
||||
ExceptOffset(Option<SourceSpan>),
|
||||
Bjork(Option<SourceSpan>),
|
||||
PBjork(Option<SourceSpan>),
|
||||
Quotation(Arc<[Op]>, Option<SourceSpan>),
|
||||
When,
|
||||
Unless,
|
||||
Adsr,
|
||||
Ad,
|
||||
Apply,
|
||||
Ramp,
|
||||
Tri,
|
||||
Triangle,
|
||||
Range,
|
||||
LinMap,
|
||||
ExpMap,
|
||||
Perlin,
|
||||
Chain,
|
||||
Loop,
|
||||
Degree(&'static [i64]),
|
||||
Oct,
|
||||
ClearCmd,
|
||||
SetSpeed,
|
||||
At,
|
||||
Arp,
|
||||
IntRange,
|
||||
StepRange,
|
||||
Generate,
|
||||
GeomRange,
|
||||
Euclid,
|
||||
EuclidRot,
|
||||
Times,
|
||||
Map,
|
||||
Chord(&'static [i64]),
|
||||
Transpose,
|
||||
Invert,
|
||||
DownInvert,
|
||||
VoiceDrop2,
|
||||
VoiceDrop3,
|
||||
SetKey,
|
||||
DiatonicTriad(&'static [i64]),
|
||||
DiatonicSeventh(&'static [i64]),
|
||||
// Audio-rate modulation DSL
|
||||
ModLfo(u8),
|
||||
ModSlide(u8),
|
||||
ModRnd(u8),
|
||||
ModEnv,
|
||||
// Global params
|
||||
EmitAll,
|
||||
ClearGlobal,
|
||||
// MIDI
|
||||
MidiEmit,
|
||||
GetMidiCC,
|
||||
@@ -95,4 +143,13 @@ pub enum Op {
|
||||
MidiStart,
|
||||
MidiStop,
|
||||
MidiContinue,
|
||||
// Recording
|
||||
Rec,
|
||||
Overdub,
|
||||
Orec,
|
||||
Odub,
|
||||
// Bracket syntax (mark/count for auto-counting)
|
||||
Mark,
|
||||
Count(Option<SourceSpan>),
|
||||
Index(Option<SourceSpan>),
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
//! Chord definitions as semitone interval arrays.
|
||||
|
||||
/// Named chord with its interval pattern.
|
||||
pub struct Chord {
|
||||
pub name: &'static str,
|
||||
pub intervals: &'static [i64],
|
||||
}
|
||||
|
||||
/// All built-in chord types.
|
||||
pub static CHORDS: &[Chord] = &[
|
||||
// Triads
|
||||
Chord {
|
||||
@@ -105,6 +109,47 @@ pub static CHORDS: &[Chord] = &[
|
||||
name: "madd9",
|
||||
intervals: &[0, 3, 7, 14],
|
||||
},
|
||||
// Power chord
|
||||
Chord {
|
||||
name: "pwr",
|
||||
intervals: &[0, 7],
|
||||
},
|
||||
// Suspended seventh
|
||||
Chord {
|
||||
name: "7sus4",
|
||||
intervals: &[0, 5, 7, 10],
|
||||
},
|
||||
Chord {
|
||||
name: "9sus4",
|
||||
intervals: &[0, 5, 7, 10, 14],
|
||||
},
|
||||
// Augmented major
|
||||
Chord {
|
||||
name: "augmaj7",
|
||||
intervals: &[0, 4, 8, 11],
|
||||
},
|
||||
// 6/9 chords
|
||||
Chord {
|
||||
name: "maj69",
|
||||
intervals: &[0, 4, 7, 9, 14],
|
||||
},
|
||||
Chord {
|
||||
name: "min69",
|
||||
intervals: &[0, 3, 7, 9, 14],
|
||||
},
|
||||
// Extended
|
||||
Chord {
|
||||
name: "maj11",
|
||||
intervals: &[0, 4, 7, 11, 14, 17],
|
||||
},
|
||||
Chord {
|
||||
name: "maj13",
|
||||
intervals: &[0, 4, 7, 11, 14, 21],
|
||||
},
|
||||
Chord {
|
||||
name: "min13",
|
||||
intervals: &[0, 3, 7, 10, 14, 21],
|
||||
},
|
||||
// Altered dominants
|
||||
Chord {
|
||||
name: "dom7b9",
|
||||
@@ -122,8 +167,13 @@ pub static CHORDS: &[Chord] = &[
|
||||
name: "dom7s5",
|
||||
intervals: &[0, 4, 8, 10],
|
||||
},
|
||||
Chord {
|
||||
name: "dom7s11",
|
||||
intervals: &[0, 4, 7, 10, 18],
|
||||
},
|
||||
];
|
||||
|
||||
/// Find a chord's intervals by name.
|
||||
pub fn lookup(name: &str) -> Option<&'static [i64]> {
|
||||
CHORDS.iter().find(|c| c.name == name).map(|c| c.intervals)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Music theory data — chord and scale lookup tables.
|
||||
|
||||
pub mod chords;
|
||||
mod scales;
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
//! Scale definitions as semitone offset arrays.
|
||||
|
||||
/// Named scale with its semitone pattern.
|
||||
pub struct Scale {
|
||||
pub name: &'static str,
|
||||
pub pattern: &'static [i64],
|
||||
}
|
||||
|
||||
/// All built-in scale types.
|
||||
pub static SCALES: &[Scale] = &[
|
||||
Scale {
|
||||
name: "major",
|
||||
@@ -125,6 +129,7 @@ pub static SCALES: &[Scale] = &[
|
||||
},
|
||||
];
|
||||
|
||||
/// Find a scale's pattern by name.
|
||||
pub fn lookup(name: &str) -> Option<&'static [i64]> {
|
||||
SCALES.iter().find(|s| s.name == name).map(|s| s.pattern)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
//! Core types for the Forth VM: values, execution context, and shared state.
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use parking_lot::Mutex;
|
||||
use rand::rngs::StdRng;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::ops::Op;
|
||||
|
||||
@@ -10,19 +14,43 @@ pub trait CcAccess: Send + Sync {
|
||||
fn get_cc(&self, device: usize, channel: usize, cc: usize) -> u8;
|
||||
}
|
||||
|
||||
/// Byte range in source text.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct SourceSpan {
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
pub start: u32,
|
||||
pub end: u32,
|
||||
}
|
||||
|
||||
/// Concrete value resolved from a nondeterministic op, used for trace annotations.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ResolvedValue {
|
||||
Int(i64),
|
||||
Float(f64),
|
||||
Bool(bool),
|
||||
Str(Arc<str>),
|
||||
}
|
||||
|
||||
impl ResolvedValue {
|
||||
pub fn display(&self) -> String {
|
||||
match self {
|
||||
ResolvedValue::Int(i) => i.to_string(),
|
||||
ResolvedValue::Float(f) => format!("{f:.2}"),
|
||||
ResolvedValue::Bool(b) => if *b { "yes" } else { "no" }.into(),
|
||||
ResolvedValue::Str(s) => s.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spans and resolved values collected during a single evaluation, used for UI highlighting.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ExecutionTrace {
|
||||
pub executed_spans: Vec<SourceSpan>,
|
||||
pub selected_spans: Vec<SourceSpan>,
|
||||
pub resolved: Vec<(SourceSpan, ResolvedValue)>,
|
||||
}
|
||||
|
||||
pub struct StepContext {
|
||||
/// Per-step sequencer state passed into the VM.
|
||||
pub struct StepContext<'a> {
|
||||
pub step: usize,
|
||||
pub beat: f64,
|
||||
pub bank: usize,
|
||||
@@ -35,34 +63,40 @@ pub struct StepContext {
|
||||
pub speed: f64,
|
||||
pub fill: bool,
|
||||
pub nudge_secs: f64,
|
||||
pub cc_access: Option<Arc<dyn CcAccess>>,
|
||||
#[cfg(feature = "desktop")]
|
||||
pub sr: f64,
|
||||
pub cc_access: Option<&'a dyn CcAccess>,
|
||||
pub speed_key: &'a str,
|
||||
pub mouse_x: f64,
|
||||
#[cfg(feature = "desktop")]
|
||||
pub mouse_y: f64,
|
||||
#[cfg(feature = "desktop")]
|
||||
pub mouse_down: f64,
|
||||
}
|
||||
|
||||
impl StepContext {
|
||||
impl StepContext<'_> {
|
||||
pub fn step_duration(&self) -> f64 {
|
||||
60.0 / self.tempo / 4.0 / self.speed
|
||||
}
|
||||
}
|
||||
|
||||
pub type Variables = Arc<Mutex<HashMap<String, Value>>>;
|
||||
/// Underlying map for user-defined variables.
|
||||
pub type VariablesMap = HashMap<String, Value>;
|
||||
/// Shared variable store, swapped atomically after each step.
|
||||
pub type Variables = Arc<ArcSwap<VariablesMap>>;
|
||||
/// Shared user-defined word dictionary.
|
||||
pub type Dictionary = Arc<Mutex<HashMap<String, Vec<Op>>>>;
|
||||
/// Shared random number generator.
|
||||
pub type Rng = Arc<Mutex<StdRng>>;
|
||||
pub type Stack = Arc<Mutex<Vec<Value>>>;
|
||||
pub(super) type CmdSnapshot<'a> = (Option<&'a Value>, &'a [(String, Value)]);
|
||||
pub type Stack = Mutex<Vec<Value>>;
|
||||
pub(super) type CmdSnapshot<'a> = (Option<&'a Value>, &'a [(&'static str, Value)]);
|
||||
|
||||
/// Stack value in the Forth VM.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Value {
|
||||
Int(i64, Option<SourceSpan>),
|
||||
Float(f64, Option<SourceSpan>),
|
||||
Str(String, Option<SourceSpan>),
|
||||
Quotation(Vec<Op>, Option<SourceSpan>),
|
||||
CycleList(Vec<Value>),
|
||||
Str(Arc<str>, Option<SourceSpan>),
|
||||
Quotation(Arc<[Op]>, Option<SourceSpan>),
|
||||
CycleList(Arc<[Value]>),
|
||||
ArpList(Arc<[Value]>),
|
||||
}
|
||||
|
||||
impl PartialEq for Value {
|
||||
@@ -73,6 +107,7 @@ impl PartialEq for Value {
|
||||
(Value::Str(a, _), Value::Str(b, _)) => a == b,
|
||||
(Value::Quotation(a, _), Value::Quotation(b, _)) => a == b,
|
||||
(Value::CycleList(a), Value::CycleList(b)) => a == b,
|
||||
(Value::ArpList(a), Value::ArpList(b)) => a == b,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -108,7 +143,7 @@ impl Value {
|
||||
Value::Float(f, _) => *f != 0.0,
|
||||
Value::Str(s, _) => !s.is_empty(),
|
||||
Value::Quotation(..) => true,
|
||||
Value::CycleList(items) => !items.is_empty(),
|
||||
Value::CycleList(items) | Value::ArpList(items) => !items.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,16 +151,16 @@ impl Value {
|
||||
match self {
|
||||
Value::Int(i, _) => i.to_string(),
|
||||
Value::Float(f, _) => f.to_string(),
|
||||
Value::Str(s, _) => s.clone(),
|
||||
Value::Str(s, _) => s.to_string(),
|
||||
Value::Quotation(..) => String::new(),
|
||||
Value::CycleList(_) => String::new(),
|
||||
Value::CycleList(_) | Value::ArpList(_) => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn span(&self) -> Option<SourceSpan> {
|
||||
match self {
|
||||
Value::Int(_, s) | Value::Float(_, s) | Value::Str(_, s) | Value::Quotation(_, s) => *s,
|
||||
Value::CycleList(_) => None,
|
||||
Value::CycleList(_) | Value::ArpList(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,16 +168,26 @@ impl Value {
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(super) struct CmdRegister {
|
||||
sound: Option<Value>,
|
||||
params: Vec<(String, Value)>,
|
||||
params: Vec<(&'static str, Value)>,
|
||||
deltas: Vec<Value>,
|
||||
global_params: Vec<(&'static str, Value)>,
|
||||
}
|
||||
|
||||
impl CmdRegister {
|
||||
pub(super) fn new() -> Self {
|
||||
Self {
|
||||
sound: None,
|
||||
params: Vec::with_capacity(16),
|
||||
deltas: Vec::with_capacity(4),
|
||||
global_params: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn set_sound(&mut self, val: Value) {
|
||||
self.sound = Some(val);
|
||||
}
|
||||
|
||||
pub(super) fn set_param(&mut self, key: String, val: Value) {
|
||||
pub(super) fn set_param(&mut self, key: &'static str, val: Value) {
|
||||
self.params.push((key, val));
|
||||
}
|
||||
|
||||
@@ -158,7 +203,7 @@ impl CmdRegister {
|
||||
self.sound.as_ref()
|
||||
}
|
||||
|
||||
pub(super) fn params(&self) -> &[(String, Value)] {
|
||||
pub(super) fn params(&self) -> &[(&'static str, Value)] {
|
||||
&self.params
|
||||
}
|
||||
|
||||
@@ -170,8 +215,31 @@ impl CmdRegister {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn global_params(&self) -> &[(&'static str, Value)] {
|
||||
&self.global_params
|
||||
}
|
||||
|
||||
pub(super) fn commit_global(&mut self) {
|
||||
self.global_params.append(&mut self.params);
|
||||
self.sound = None;
|
||||
self.deltas.clear();
|
||||
}
|
||||
|
||||
pub(super) fn clear_global(&mut self) {
|
||||
self.global_params.clear();
|
||||
}
|
||||
|
||||
pub fn set_global(&mut self, params: Vec<(&'static str, Value)>) {
|
||||
self.global_params = params;
|
||||
}
|
||||
|
||||
pub fn take_global(&mut self) -> Vec<(&'static str, Value)> {
|
||||
std::mem::take(&mut self.global_params)
|
||||
}
|
||||
|
||||
pub(super) fn clear(&mut self) {
|
||||
self.sound = None;
|
||||
self.params.clear();
|
||||
self.deltas.clear();
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
344
crates/forth/src/words/compile.rs
Normal file
344
crates/forth/src/words/compile.rs
Normal file
@@ -0,0 +1,344 @@
|
||||
//! Word-to-Op translation: maps Forth word names to compiled instructions.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::ops::Op;
|
||||
use crate::theory;
|
||||
use crate::types::{Dictionary, SourceSpan};
|
||||
|
||||
use super::{lookup_word, WordCompile::*};
|
||||
|
||||
pub(super) fn simple_op(name: &str) -> Option<Op> {
|
||||
Some(match name {
|
||||
"dup" => Op::Dup,
|
||||
"dupn" => Op::Dupn,
|
||||
"drop" => Op::Drop,
|
||||
"print" => Op::Print,
|
||||
"swap" => Op::Swap,
|
||||
"over" => Op::Over,
|
||||
"rot" => Op::Rot,
|
||||
"nip" => Op::Nip,
|
||||
"tuck" => Op::Tuck,
|
||||
"2dup" => Op::Dup2,
|
||||
"2drop" => Op::Drop2,
|
||||
"2swap" => Op::Swap2,
|
||||
"2over" => Op::Over2,
|
||||
"rev" => Op::Rev,
|
||||
"shuffle" => Op::Shuffle,
|
||||
"sort" => Op::Sort,
|
||||
"rsort" => Op::RSort,
|
||||
"sum" => Op::Sum,
|
||||
"prod" => Op::Prod,
|
||||
"+" => Op::Add,
|
||||
"-" => Op::Sub,
|
||||
"*" => Op::Mul,
|
||||
"/" => Op::Div,
|
||||
"mod" => Op::Mod,
|
||||
"neg" => Op::Neg,
|
||||
"abs" => Op::Abs,
|
||||
"floor" => Op::Floor,
|
||||
"ceil" => Op::Ceil,
|
||||
"round" => Op::Round,
|
||||
"min" => Op::Min,
|
||||
"max" => Op::Max,
|
||||
"pow" => Op::Pow,
|
||||
"sqrt" => Op::Sqrt,
|
||||
"sin" => Op::Sin,
|
||||
"cos" => Op::Cos,
|
||||
"log" => Op::Log,
|
||||
"=" => Op::Eq,
|
||||
"!=" => Op::Ne,
|
||||
"lt" => Op::Lt,
|
||||
"gt" => Op::Gt,
|
||||
"<=" => Op::Le,
|
||||
">=" => Op::Ge,
|
||||
"and" => Op::And,
|
||||
"or" => Op::Or,
|
||||
"not" => Op::Not,
|
||||
"xor" => Op::Xor,
|
||||
"nand" => Op::Nand,
|
||||
"nor" => Op::Nor,
|
||||
"ifelse" => Op::IfElse,
|
||||
"select" => Op::Pick,
|
||||
"sound" => Op::NewCmd,
|
||||
"." => Op::Emit,
|
||||
"rand" => Op::Rand(None),
|
||||
"exprand" => Op::ExpRand(None),
|
||||
"logrand" => Op::LogRand(None),
|
||||
"seed" => Op::Seed,
|
||||
"cycle" => Op::Cycle(None),
|
||||
"pcycle" => Op::PCycle(None),
|
||||
"choose" => Op::Choose(None),
|
||||
"bounce" => Op::Bounce(None),
|
||||
"pbounce" => Op::PBounce(None),
|
||||
"wchoose" => Op::WChoose(None),
|
||||
"every" => Op::Every(None),
|
||||
"except" => Op::Except(None),
|
||||
"every+" => Op::EveryOffset(None),
|
||||
"except+" => Op::ExceptOffset(None),
|
||||
"bjork" => Op::Bjork(None),
|
||||
"pbjork" => Op::PBjork(None),
|
||||
"chance" => Op::ChanceExec(None),
|
||||
"prob" => Op::ProbExec(None),
|
||||
"coin" => Op::Coin(None),
|
||||
"mtof" => Op::Mtof,
|
||||
"ftom" => Op::Ftom,
|
||||
"?" => Op::When,
|
||||
"!?" => Op::Unless,
|
||||
"tempo!" => Op::SetTempo,
|
||||
"speed!" => Op::SetSpeed,
|
||||
"at" => Op::At,
|
||||
"arp" => Op::Arp,
|
||||
"adsr" => Op::Adsr,
|
||||
"ad" => Op::Ad,
|
||||
"apply" => Op::Apply,
|
||||
"ramp" => Op::Ramp,
|
||||
"triangle" => Op::Triangle,
|
||||
"range" => Op::Range,
|
||||
"linmap" => Op::LinMap,
|
||||
"expmap" => Op::ExpMap,
|
||||
"perlin" => Op::Perlin,
|
||||
"loop" => Op::Loop,
|
||||
"oct" => Op::Oct,
|
||||
"clear" => Op::ClearCmd,
|
||||
"all" => Op::EmitAll,
|
||||
"noall" => Op::ClearGlobal,
|
||||
".." => Op::IntRange,
|
||||
".," => Op::StepRange,
|
||||
"gen" => Op::Generate,
|
||||
"geom.." => Op::GeomRange,
|
||||
"euclid" => Op::Euclid,
|
||||
"euclidrot" => Op::EuclidRot,
|
||||
"times" => Op::Times,
|
||||
"map" => Op::Map,
|
||||
"m." => Op::MidiEmit,
|
||||
"ccval" => Op::GetMidiCC,
|
||||
"mclock" => Op::MidiClock,
|
||||
"mstart" => Op::MidiStart,
|
||||
"mstop" => Op::MidiStop,
|
||||
"mcont" => Op::MidiContinue,
|
||||
"rec" => Op::Rec,
|
||||
"overdub" | "dub" => Op::Overdub,
|
||||
"orec" => Op::Orec,
|
||||
"odub" => Op::Odub,
|
||||
"forget" => Op::Forget,
|
||||
"index" => Op::Index(None),
|
||||
"key!" => Op::SetKey,
|
||||
"tp" => Op::Transpose,
|
||||
"inv" => Op::Invert,
|
||||
"dinv" => Op::DownInvert,
|
||||
"drop2" => Op::VoiceDrop2,
|
||||
"drop3" => Op::VoiceDrop3,
|
||||
"lfo" => Op::ModLfo(0),
|
||||
"tlfo" => Op::ModLfo(1),
|
||||
"wlfo" => Op::ModLfo(2),
|
||||
"qlfo" => Op::ModLfo(3),
|
||||
"slide" => Op::ModSlide(0),
|
||||
"expslide" => Op::ModSlide(1),
|
||||
"sslide" => Op::ModSlide(2),
|
||||
"jit" => Op::ModRnd(0),
|
||||
"sjit" => Op::ModRnd(1),
|
||||
"drunk" => Op::ModRnd(2),
|
||||
"env" => Op::ModEnv,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_note_name(name: &str) -> Option<i64> {
|
||||
let name = name.to_lowercase();
|
||||
let bytes = name.as_bytes();
|
||||
|
||||
if bytes.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let base = match bytes[0] {
|
||||
b'c' => 0,
|
||||
b'd' => 2,
|
||||
b'e' => 4,
|
||||
b'f' => 5,
|
||||
b'g' => 7,
|
||||
b'a' => 9,
|
||||
b'b' => 11,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let (modifier, octave_start) = match bytes[1] {
|
||||
b'#' | b's' => (1, 2),
|
||||
b'b' if bytes.len() > 2 && bytes[2].is_ascii_digit() => (-1, 2),
|
||||
b'0'..=b'9' => (0, 1),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let octave_str = &name[octave_start..];
|
||||
let octave: i64 = octave_str.parse().ok()?;
|
||||
|
||||
if !(-1..=9).contains(&octave) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((octave + 1) * 12 + base + modifier)
|
||||
}
|
||||
|
||||
fn parse_interval(name: &str) -> Option<i64> {
|
||||
let simple = match name {
|
||||
"P1" | "unison" => 0,
|
||||
"m2" => 1,
|
||||
"M2" => 2,
|
||||
"m3" => 3,
|
||||
"M3" => 4,
|
||||
"P4" => 5,
|
||||
"aug4" | "dim5" | "tritone" => 6,
|
||||
"P5" => 7,
|
||||
"m6" => 8,
|
||||
"M6" => 9,
|
||||
"m7" => 10,
|
||||
"M7" => 11,
|
||||
"P8" => 12,
|
||||
"m9" => 13,
|
||||
"M9" => 14,
|
||||
"m10" => 15,
|
||||
"M10" => 16,
|
||||
"P11" => 17,
|
||||
"aug11" => 18,
|
||||
"P12" => 19,
|
||||
"m13" => 20,
|
||||
"M13" => 21,
|
||||
"m14" => 22,
|
||||
"M14" => 23,
|
||||
"P15" => 24,
|
||||
_ => return None,
|
||||
};
|
||||
Some(simple)
|
||||
}
|
||||
|
||||
fn attach_span(op: &mut Op, span: SourceSpan) {
|
||||
match op {
|
||||
Op::Rand(s) | Op::ExpRand(s) | Op::LogRand(s) | Op::Coin(s)
|
||||
| Op::Choose(s) | Op::WChoose(s) | Op::Cycle(s) | Op::PCycle(s)
|
||||
| Op::Bounce(s) | Op::PBounce(s) | Op::ChanceExec(s) | Op::ProbExec(s)
|
||||
| Op::Every(s) | Op::Except(s) | Op::EveryOffset(s) | Op::ExceptOffset(s)
|
||||
| Op::Bjork(s) | Op::PBjork(s)
|
||||
| Op::Count(s) | Op::Index(s) => *s = Some(span),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn compile_word(
|
||||
name: &str,
|
||||
span: Option<SourceSpan>,
|
||||
ops: &mut Vec<Op>,
|
||||
dict: &Dictionary,
|
||||
) -> bool {
|
||||
match name {
|
||||
"linramp" => {
|
||||
ops.push(Op::PushFloat(1.0, span));
|
||||
ops.push(Op::Ramp);
|
||||
return true;
|
||||
}
|
||||
"expramp" => {
|
||||
ops.push(Op::PushFloat(3.0, span));
|
||||
ops.push(Op::Ramp);
|
||||
return true;
|
||||
}
|
||||
"logramp" => {
|
||||
ops.push(Op::PushFloat(0.3, span));
|
||||
ops.push(Op::Ramp);
|
||||
return true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if name == "triad" || name == "seventh" {
|
||||
if let Some(Op::Degree(pattern)) = ops.last() {
|
||||
let pattern = *pattern;
|
||||
ops.pop();
|
||||
ops.push(if name == "triad" {
|
||||
Op::DiatonicTriad(pattern)
|
||||
} else {
|
||||
Op::DiatonicSeventh(pattern)
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(pattern) = theory::lookup(name) {
|
||||
ops.push(Op::Degree(pattern));
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(intervals) = theory::chords::lookup(name) {
|
||||
ops.push(Op::Chord(intervals));
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(word) = lookup_word(name) {
|
||||
match &word.compile {
|
||||
Simple => {
|
||||
if let Some(mut op) = simple_op(word.name) {
|
||||
if let Some(sp) = span {
|
||||
attach_span(&mut op, sp);
|
||||
}
|
||||
ops.push(op);
|
||||
}
|
||||
}
|
||||
Context(ctx) => ops.push(Op::GetContext(ctx)),
|
||||
Param => ops.push(Op::SetParam(word.name)),
|
||||
Probability(p) => {
|
||||
ops.push(Op::PushFloat(*p, None));
|
||||
ops.push(Op::ChanceExec(span));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(var_name) = name.strip_prefix('@') {
|
||||
if !var_name.is_empty() {
|
||||
ops.push(Op::PushStr(Arc::from(var_name), span));
|
||||
ops.push(Op::Get);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(var_name) = name.strip_prefix('!') {
|
||||
if !var_name.is_empty() {
|
||||
ops.push(Op::PushStr(Arc::from(var_name), span));
|
||||
ops.push(Op::Set);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(var_name) = name.strip_prefix(',') {
|
||||
if !var_name.is_empty() {
|
||||
ops.push(Op::PushStr(Arc::from(var_name), span));
|
||||
ops.push(Op::SetKeep);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(midi) = parse_note_name(name) {
|
||||
ops.push(Op::PushInt(midi, span));
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(semitones) = parse_interval(name) {
|
||||
ops.push(Op::Dup);
|
||||
ops.push(Op::PushInt(semitones, span));
|
||||
ops.push(Op::Add);
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(op) = simple_op(name) {
|
||||
ops.push(op);
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(body) = dict.lock().get(name) {
|
||||
ops.extend(body.iter().cloned());
|
||||
return true;
|
||||
}
|
||||
|
||||
ops.push(Op::PushStr(Arc::from(name), span));
|
||||
true
|
||||
}
|
||||
642
crates/forth/src/words/core.rs
Normal file
642
crates/forth/src/words/core.rs
Normal file
@@ -0,0 +1,642 @@
|
||||
//! Word metadata for core language primitives (stack, arithmetic, logic, variables, definitions).
|
||||
|
||||
use super::{Word, WordCompile::*};
|
||||
pub(super) const WORDS: &[Word] = &[
|
||||
// Stack manipulation
|
||||
Word {
|
||||
name: "dup",
|
||||
aliases: &[],
|
||||
category: "Stack",
|
||||
stack: "(a -- a a)",
|
||||
desc: "Duplicate top of stack",
|
||||
example: "3 dup => 3 3",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "dupn",
|
||||
aliases: &["!"],
|
||||
category: "Stack",
|
||||
stack: "(a n -- a a ... a)",
|
||||
desc: "Duplicate a onto stack n times",
|
||||
example: "2 4 dupn => 2 2 2 2",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "drop",
|
||||
aliases: &[],
|
||||
category: "Stack",
|
||||
stack: "(a --)",
|
||||
desc: "Remove top of stack",
|
||||
example: "1 2 drop => 1",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "print",
|
||||
aliases: &[],
|
||||
category: "Stack",
|
||||
stack: "(x --)",
|
||||
desc: "Print top of stack to footer bar",
|
||||
example: "42 print",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "swap",
|
||||
aliases: &[],
|
||||
category: "Stack",
|
||||
stack: "(a b -- b a)",
|
||||
desc: "Exchange top two items",
|
||||
example: "1 2 swap => 2 1",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "over",
|
||||
aliases: &[],
|
||||
category: "Stack",
|
||||
stack: "(a b -- a b a)",
|
||||
desc: "Copy second to top",
|
||||
example: "1 2 over => 1 2 1",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "rot",
|
||||
aliases: &[],
|
||||
category: "Stack",
|
||||
stack: "(a b c -- b c a)",
|
||||
desc: "Rotate top three",
|
||||
example: "1 2 3 rot => 2 3 1",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "nip",
|
||||
aliases: &[],
|
||||
category: "Stack",
|
||||
stack: "(a b -- b)",
|
||||
desc: "Remove second item",
|
||||
example: "1 2 nip => 2",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "tuck",
|
||||
aliases: &[],
|
||||
category: "Stack",
|
||||
stack: "(a b -- b a b)",
|
||||
desc: "Copy top under second",
|
||||
example: "1 2 tuck => 2 1 2",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "2dup",
|
||||
aliases: &[],
|
||||
category: "Stack",
|
||||
stack: "(a b -- a b a b)",
|
||||
desc: "Duplicate top two values",
|
||||
example: "1 2 2dup => 1 2 1 2",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "2drop",
|
||||
aliases: &[],
|
||||
category: "Stack",
|
||||
stack: "(a b --)",
|
||||
desc: "Drop top two values",
|
||||
example: "1 2 3 2drop => 1",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "2swap",
|
||||
aliases: &[],
|
||||
category: "Stack",
|
||||
stack: "(a b c d -- c d a b)",
|
||||
desc: "Swap top two pairs",
|
||||
example: "1 2 3 4 2swap => 3 4 1 2",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "2over",
|
||||
aliases: &[],
|
||||
category: "Stack",
|
||||
stack: "(a b c d -- a b c d a b)",
|
||||
desc: "Copy second pair to top",
|
||||
example: "1 2 3 4 2over => 1 2 3 4 1 2",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "rev",
|
||||
aliases: &[],
|
||||
category: "Stack",
|
||||
stack: "(..n n -- ..n)",
|
||||
desc: "Reverse top n items",
|
||||
example: "1 2 3 3 rev => 3 2 1",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "shuffle",
|
||||
aliases: &[],
|
||||
category: "Stack",
|
||||
stack: "(..n n -- ..n)",
|
||||
desc: "Randomly shuffle top n items",
|
||||
example: "1 2 3 3 shuffle",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "sort",
|
||||
aliases: &[],
|
||||
category: "Stack",
|
||||
stack: "(..n n -- ..n)",
|
||||
desc: "Sort top n items ascending",
|
||||
example: "3 1 2 3 sort => 1 2 3",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "rsort",
|
||||
aliases: &[],
|
||||
category: "Stack",
|
||||
stack: "(..n n -- ..n)",
|
||||
desc: "Sort top n items descending",
|
||||
example: "1 2 3 3 rsort => 3 2 1",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "sum",
|
||||
aliases: &[],
|
||||
category: "Stack",
|
||||
stack: "(..n n -- total)",
|
||||
desc: "Sum top n items",
|
||||
example: "1 2 3 3 sum => 6",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "prod",
|
||||
aliases: &[],
|
||||
category: "Stack",
|
||||
stack: "(..n n -- product)",
|
||||
desc: "Multiply top n items",
|
||||
example: "2 3 4 3 prod => 24",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
// Arithmetic
|
||||
Word {
|
||||
name: "+",
|
||||
aliases: &[],
|
||||
category: "Arithmetic",
|
||||
stack: "(a b -- a+b)",
|
||||
desc: "Add",
|
||||
example: "2 3 + => 5",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "-",
|
||||
aliases: &[],
|
||||
category: "Arithmetic",
|
||||
stack: "(a b -- a-b)",
|
||||
desc: "Subtract",
|
||||
example: "5 3 - => 2",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "*",
|
||||
aliases: &[],
|
||||
category: "Arithmetic",
|
||||
stack: "(a b -- a*b)",
|
||||
desc: "Multiply",
|
||||
example: "3 4 * => 12",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "/",
|
||||
aliases: &[],
|
||||
category: "Arithmetic",
|
||||
stack: "(a b -- a/b)",
|
||||
desc: "Divide",
|
||||
example: "10 2 / => 5",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "mod",
|
||||
aliases: &[],
|
||||
category: "Arithmetic",
|
||||
stack: "(a b -- a%b)",
|
||||
desc: "Modulo",
|
||||
example: "7 3 mod => 1",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "neg",
|
||||
aliases: &[],
|
||||
category: "Arithmetic",
|
||||
stack: "(a -- -a)",
|
||||
desc: "Negate",
|
||||
example: "5 neg => -5",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "abs",
|
||||
aliases: &[],
|
||||
category: "Arithmetic",
|
||||
stack: "(a -- |a|)",
|
||||
desc: "Absolute value",
|
||||
example: "-5 abs => 5",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "floor",
|
||||
aliases: &[],
|
||||
category: "Arithmetic",
|
||||
stack: "(f -- n)",
|
||||
desc: "Round down to integer",
|
||||
example: "3.7 floor => 3",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "ceil",
|
||||
aliases: &[],
|
||||
category: "Arithmetic",
|
||||
stack: "(f -- n)",
|
||||
desc: "Round up to integer",
|
||||
example: "3.2 ceil => 4",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "round",
|
||||
aliases: &[],
|
||||
category: "Arithmetic",
|
||||
stack: "(f -- n)",
|
||||
desc: "Round to nearest integer",
|
||||
example: "3.5 round => 4",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "min",
|
||||
aliases: &[],
|
||||
category: "Arithmetic",
|
||||
stack: "(a b -- min)",
|
||||
desc: "Minimum of two values",
|
||||
example: "3 5 min => 3",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "max",
|
||||
aliases: &[],
|
||||
category: "Arithmetic",
|
||||
stack: "(a b -- max)",
|
||||
desc: "Maximum of two values",
|
||||
example: "3 5 max => 5",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "pow",
|
||||
aliases: &[],
|
||||
category: "Arithmetic",
|
||||
stack: "(a b -- a^b)",
|
||||
desc: "Exponentiation",
|
||||
example: "2 3 pow => 8",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "sqrt",
|
||||
aliases: &[],
|
||||
category: "Arithmetic",
|
||||
stack: "(a -- √a)",
|
||||
desc: "Square root",
|
||||
example: "16 sqrt => 4",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "sin",
|
||||
aliases: &[],
|
||||
category: "Arithmetic",
|
||||
stack: "(a -- sin(a))",
|
||||
desc: "Sine (radians)",
|
||||
example: "3.14159 2 / sin => 1.0",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "cos",
|
||||
aliases: &[],
|
||||
category: "Arithmetic",
|
||||
stack: "(a -- cos(a))",
|
||||
desc: "Cosine (radians)",
|
||||
example: "0 cos => 1.0",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "log",
|
||||
aliases: &[],
|
||||
category: "Arithmetic",
|
||||
stack: "(a -- ln(a))",
|
||||
desc: "Natural logarithm",
|
||||
example: "2.718 log => 1.0",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "linmap",
|
||||
aliases: &[],
|
||||
category: "Arithmetic",
|
||||
stack: "(val inlo inhi outlo outhi -- mapped)",
|
||||
desc: "Linear map from [inlo,inhi] to [outlo,outhi]",
|
||||
example: "64 0 127 200 2000 linmap => 1007.87",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "expmap",
|
||||
aliases: &[],
|
||||
category: "Arithmetic",
|
||||
stack: "(val lo hi -- mapped)",
|
||||
desc: "Exponential map from [0,1] to [lo,hi]",
|
||||
example: "0.5 200 8000 expmap => 1264.91",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
// Comparison
|
||||
Word {
|
||||
name: "=",
|
||||
aliases: &[],
|
||||
category: "Comparison",
|
||||
stack: "(a b -- bool)",
|
||||
desc: "Equal",
|
||||
example: "3 3 = => 1",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "!=",
|
||||
aliases: &["<>"],
|
||||
category: "Comparison",
|
||||
stack: "(a b -- bool)",
|
||||
desc: "Not equal",
|
||||
example: "3 4 != => 1",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "lt",
|
||||
aliases: &[],
|
||||
category: "Comparison",
|
||||
stack: "(a b -- bool)",
|
||||
desc: "Less than",
|
||||
example: "2 3 lt => 1",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "gt",
|
||||
aliases: &[],
|
||||
category: "Comparison",
|
||||
stack: "(a b -- bool)",
|
||||
desc: "Greater than",
|
||||
example: "3 2 gt => 1",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "<=",
|
||||
aliases: &[],
|
||||
category: "Comparison",
|
||||
stack: "(a b -- bool)",
|
||||
desc: "Less or equal",
|
||||
example: "3 3 <= => 1",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: ">=",
|
||||
aliases: &[],
|
||||
category: "Comparison",
|
||||
stack: "(a b -- bool)",
|
||||
desc: "Greater or equal",
|
||||
example: "3 3 >= => 1",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
// Logic
|
||||
Word {
|
||||
name: "and",
|
||||
aliases: &[],
|
||||
category: "Logic",
|
||||
stack: "(a b -- bool)",
|
||||
desc: "Logical and",
|
||||
example: "1 1 and => 1",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "or",
|
||||
aliases: &[],
|
||||
category: "Logic",
|
||||
stack: "(a b -- bool)",
|
||||
desc: "Logical or",
|
||||
example: "0 1 or => 1",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "not",
|
||||
aliases: &[],
|
||||
category: "Logic",
|
||||
stack: "(a -- bool)",
|
||||
desc: "Logical not",
|
||||
example: "0 not => 1",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "xor",
|
||||
aliases: &[],
|
||||
category: "Logic",
|
||||
stack: "(a b -- bool)",
|
||||
desc: "Exclusive or",
|
||||
example: "1 0 xor => 1",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "nand",
|
||||
aliases: &[],
|
||||
category: "Logic",
|
||||
stack: "(a b -- bool)",
|
||||
desc: "Not and",
|
||||
example: "1 1 nand => 0",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "nor",
|
||||
aliases: &[],
|
||||
category: "Logic",
|
||||
stack: "(a b -- bool)",
|
||||
desc: "Not or",
|
||||
example: "0 0 nor => 1",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "ifelse",
|
||||
aliases: &[],
|
||||
category: "Logic",
|
||||
stack: "(true-quot false-quot bool --)",
|
||||
desc: "Execute true-quot if true, else false-quot",
|
||||
example: "( 1 ) ( 2 ) coin ifelse",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "select",
|
||||
aliases: &[],
|
||||
category: "Logic",
|
||||
stack: "(..quots n --)",
|
||||
desc: "Execute nth quotation (0-indexed)",
|
||||
example: "( 1 ) ( 2 ) ( 3 ) 2 select => 3",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "?",
|
||||
aliases: &[],
|
||||
category: "Logic",
|
||||
stack: "(quot bool --)",
|
||||
desc: "Execute quotation if true",
|
||||
example: "( 2 distort ) 0.5 chance ?",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "!?",
|
||||
aliases: &[],
|
||||
category: "Logic",
|
||||
stack: "(quot bool --)",
|
||||
desc: "Execute quotation if false",
|
||||
example: "( 1 distort ) 0.5 chance !?",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "apply",
|
||||
aliases: &[],
|
||||
category: "Logic",
|
||||
stack: "(quot --)",
|
||||
desc: "Execute quotation unconditionally",
|
||||
example: "( 2 * ) apply",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
// Control
|
||||
Word {
|
||||
name: "times",
|
||||
aliases: &[],
|
||||
category: "Control",
|
||||
stack: "(n quot --)",
|
||||
desc: "Execute quotation n times, @i holds current index",
|
||||
example: "4 ( @i . ) times => 0 1 2 3",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "map",
|
||||
aliases: &[],
|
||||
category: "Control",
|
||||
stack: "(..vals quot -- ..results)",
|
||||
desc: "Apply quotation to each stack element",
|
||||
example: "1 2 3 ( 10 * ) map => 10 20 30",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
// Variables
|
||||
Word {
|
||||
name: "@<var>",
|
||||
aliases: &[],
|
||||
category: "Variables",
|
||||
stack: "( -- val)",
|
||||
desc: "Fetch variable value",
|
||||
example: "@freq => 440",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "!<var>",
|
||||
aliases: &[],
|
||||
category: "Variables",
|
||||
stack: "(val --)",
|
||||
desc: "Store value in variable",
|
||||
example: "440 !freq",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: ",<var>",
|
||||
aliases: &[],
|
||||
category: "Variables",
|
||||
stack: "(val -- val)",
|
||||
desc: "Store value in variable, keep on stack",
|
||||
example: "440 ,freq => 440",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
// Definitions
|
||||
Word {
|
||||
name: ":",
|
||||
aliases: &[],
|
||||
category: "Definitions",
|
||||
stack: "( -- )",
|
||||
desc: "Begin word definition",
|
||||
example: ": kick \"kick\" s emit ;",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: ";",
|
||||
aliases: &[],
|
||||
category: "Definitions",
|
||||
stack: "( -- )",
|
||||
desc: "End word definition",
|
||||
example: ": kick \"kick\" s emit ;",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "forget",
|
||||
aliases: &[],
|
||||
category: "Definitions",
|
||||
stack: "(name --)",
|
||||
desc: "Remove user-defined word from dictionary",
|
||||
example: "\"double\" forget",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
];
|
||||
1003
crates/forth/src/words/effects.rs
Normal file
1003
crates/forth/src/words/effects.rs
Normal file
File diff suppressed because it is too large
Load Diff
136
crates/forth/src/words/midi.rs
Normal file
136
crates/forth/src/words/midi.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
//! MIDI word definitions: channel, CC, pitch bend, transport, and device routing.
|
||||
|
||||
use super::{Word, WordCompile::*};
|
||||
|
||||
pub(super) const WORDS: &[Word] = &[
|
||||
Word {
|
||||
name: "chan",
|
||||
aliases: &[],
|
||||
category: "MIDI",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set MIDI channel 1-16",
|
||||
example: "1 chan",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "ccnum",
|
||||
aliases: &[],
|
||||
category: "MIDI",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set MIDI CC number 0-127",
|
||||
example: "1 ccnum",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "ccout",
|
||||
aliases: &[],
|
||||
category: "MIDI",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set MIDI CC output value 0-127",
|
||||
example: "64 ccout",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "bend",
|
||||
aliases: &[],
|
||||
category: "MIDI",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set pitch bend -1.0 to 1.0 (0 = center)",
|
||||
example: "0.5 bend",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "pressure",
|
||||
aliases: &[],
|
||||
category: "MIDI",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set channel pressure (aftertouch) 0-127",
|
||||
example: "64 pressure",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "program",
|
||||
aliases: &[],
|
||||
category: "MIDI",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set program change number 0-127",
|
||||
example: "0 program",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "m.",
|
||||
aliases: &[],
|
||||
category: "MIDI",
|
||||
stack: "(--)",
|
||||
desc: "Emit MIDI message from params (note/cc/bend/pressure/program)",
|
||||
example: "60 note 100 velocity 1 chan m.",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "mclock",
|
||||
aliases: &[],
|
||||
category: "MIDI",
|
||||
stack: "(--)",
|
||||
desc: "Send MIDI clock pulse (24 per quarter note)",
|
||||
example: "mclock",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "mstart",
|
||||
aliases: &[],
|
||||
category: "MIDI",
|
||||
stack: "(--)",
|
||||
desc: "Send MIDI start message",
|
||||
example: "mstart",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "mstop",
|
||||
aliases: &[],
|
||||
category: "MIDI",
|
||||
stack: "(--)",
|
||||
desc: "Send MIDI stop message",
|
||||
example: "mstop",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "mcont",
|
||||
aliases: &[],
|
||||
category: "MIDI",
|
||||
stack: "(--)",
|
||||
desc: "Send MIDI continue message",
|
||||
example: "mcont",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "ccval",
|
||||
aliases: &[],
|
||||
category: "MIDI",
|
||||
stack: "(cc chan -- val)",
|
||||
desc: "Read CC value 0-127 from MIDI input (uses dev param for device)",
|
||||
example: "1 1 ccval",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "dev",
|
||||
aliases: &[],
|
||||
category: "MIDI",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set MIDI device slot 0-3 for output/input",
|
||||
example: "1 dev 60 note m.",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
];
|
||||
65
crates/forth/src/words/mod.rs
Normal file
65
crates/forth/src/words/mod.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
//! Built-in word definitions and lookup for the Forth VM.
|
||||
|
||||
mod compile;
|
||||
mod core;
|
||||
mod effects;
|
||||
mod midi;
|
||||
mod music;
|
||||
mod sequencing;
|
||||
mod sound;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
pub(crate) use compile::compile_word;
|
||||
|
||||
/// How a word is compiled into ops.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum WordCompile {
|
||||
Simple,
|
||||
Context(&'static str),
|
||||
Param,
|
||||
Probability(f64),
|
||||
}
|
||||
|
||||
/// Metadata for a built-in Forth word.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Word {
|
||||
pub name: &'static str,
|
||||
pub aliases: &'static [&'static str],
|
||||
pub category: &'static str,
|
||||
pub stack: &'static str,
|
||||
pub desc: &'static str,
|
||||
pub example: &'static str,
|
||||
pub compile: WordCompile,
|
||||
pub varargs: bool,
|
||||
}
|
||||
|
||||
/// All built-in words, aggregated from every category module.
|
||||
pub static WORDS: LazyLock<Vec<Word>> = LazyLock::new(|| {
|
||||
let mut words = Vec::new();
|
||||
words.extend_from_slice(self::core::WORDS);
|
||||
words.extend_from_slice(sound::WORDS);
|
||||
words.extend_from_slice(effects::WORDS);
|
||||
words.extend_from_slice(sequencing::WORDS);
|
||||
words.extend_from_slice(music::WORDS);
|
||||
words.extend_from_slice(midi::WORDS);
|
||||
words
|
||||
});
|
||||
|
||||
/// Index mapping word names and aliases to their definitions.
|
||||
static WORD_MAP: LazyLock<HashMap<&'static str, &'static Word>> = LazyLock::new(|| {
|
||||
let mut map = HashMap::with_capacity(WORDS.len() * 2);
|
||||
for word in WORDS.iter() {
|
||||
map.insert(word.name, word);
|
||||
for alias in word.aliases {
|
||||
map.insert(alias, word);
|
||||
}
|
||||
}
|
||||
map
|
||||
});
|
||||
|
||||
/// Find a word by name or alias.
|
||||
pub fn lookup_word(name: &str) -> Option<&'static Word> {
|
||||
WORD_MAP.get(name).copied()
|
||||
}
|
||||
496
crates/forth/src/words/music.rs
Normal file
496
crates/forth/src/words/music.rs
Normal file
@@ -0,0 +1,496 @@
|
||||
//! Word definitions for music theory, harmony, and chord construction.
|
||||
|
||||
use super::{Word, WordCompile::*};
|
||||
|
||||
pub(super) const WORDS: &[Word] = &[
|
||||
// Music
|
||||
Word {
|
||||
name: "mtof",
|
||||
aliases: &[],
|
||||
category: "Music",
|
||||
stack: "(midi -- hz)",
|
||||
desc: "MIDI note to frequency",
|
||||
example: "69 mtof => 440.0",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "ftom",
|
||||
aliases: &[],
|
||||
category: "Music",
|
||||
stack: "(hz -- midi)",
|
||||
desc: "Frequency to MIDI note",
|
||||
example: "440 ftom => 69.0",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
// Harmony
|
||||
Word {
|
||||
name: "key!",
|
||||
aliases: &[],
|
||||
category: "Harmony",
|
||||
stack: "(root --)",
|
||||
desc: "Set tonal center for scale operations",
|
||||
example: "g3 key! 0 major => 55",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "triad",
|
||||
aliases: &[],
|
||||
category: "Harmony",
|
||||
stack: "(degree -- n1 n2 n3)",
|
||||
desc: "Diatonic triad from scale degree (follows a scale word)",
|
||||
example: "0 major triad => 60 64 67",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "seventh",
|
||||
aliases: &[],
|
||||
category: "Harmony",
|
||||
stack: "(degree -- n1 n2 n3 n4)",
|
||||
desc: "Diatonic seventh from scale degree (follows a scale word)",
|
||||
example: "0 major seventh => 60 64 67 71",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
// Chord voicings
|
||||
Word {
|
||||
name: "inv",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(a b c.. -- b c.. a+12)",
|
||||
desc: "Inversion: bottom note moves up an octave",
|
||||
example: "c4 maj inv => 64 67 72",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "dinv",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(a b.. z -- z-12 a b..)",
|
||||
desc: "Down inversion: top note moves down an octave",
|
||||
example: "c4 maj dinv => 55 60 64",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "drop2",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(a b c d -- b-12 a c d)",
|
||||
desc: "Drop-2 voicing: 2nd from top moves down an octave",
|
||||
example: "c4 maj7 drop2 => 55 60 64 71",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "drop3",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(a b c d -- c-12 a b d)",
|
||||
desc: "Drop-3 voicing: 3rd from top moves down an octave",
|
||||
example: "c4 maj7 drop3 => 52 60 67 71",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
// Transpose
|
||||
Word {
|
||||
name: "tp",
|
||||
aliases: &[],
|
||||
category: "Harmony",
|
||||
stack: "(n --)",
|
||||
desc: "Transpose all ints on stack by N semitones",
|
||||
example: "c4 maj 3 tp => 63 67 70",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
// Chords - Triads
|
||||
Word {
|
||||
name: "pwr",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root fifth)",
|
||||
desc: "Power chord",
|
||||
example: "c4 pwr => 60 67",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "maj",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth)",
|
||||
desc: "Major triad",
|
||||
example: "c4 maj => 60 64 67",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "m",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth)",
|
||||
desc: "Minor triad",
|
||||
example: "c4 m => 60 63 67",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "dim",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth)",
|
||||
desc: "Diminished triad",
|
||||
example: "c4 dim => 60 63 66",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "aug",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth)",
|
||||
desc: "Augmented triad",
|
||||
example: "c4 aug => 60 64 68",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "sus2",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root second fifth)",
|
||||
desc: "Suspended 2nd",
|
||||
example: "c4 sus2 => 60 62 67",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "sus4",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root fourth fifth)",
|
||||
desc: "Suspended 4th",
|
||||
example: "c4 sus4 => 60 65 67",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
// Chords - Seventh
|
||||
Word {
|
||||
name: "maj7",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth seventh)",
|
||||
desc: "Major 7th",
|
||||
example: "c4 maj7 => 60 64 67 71",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "min7",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth seventh)",
|
||||
desc: "Minor 7th",
|
||||
example: "c4 min7 => 60 63 67 70",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "dom7",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth seventh)",
|
||||
desc: "Dominant 7th",
|
||||
example: "c4 dom7 => 60 64 67 70",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "dim7",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth seventh)",
|
||||
desc: "Diminished 7th",
|
||||
example: "c4 dim7 => 60 63 66 69",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "m7b5",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth seventh)",
|
||||
desc: "Half-diminished (min7b5)",
|
||||
example: "c4 m7b5 => 60 63 66 70",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "minmaj7",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth seventh)",
|
||||
desc: "Minor-major 7th",
|
||||
example: "c4 minmaj7 => 60 63 67 71",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "aug7",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth seventh)",
|
||||
desc: "Augmented 7th",
|
||||
example: "c4 aug7 => 60 64 68 70",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "augmaj7",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth seventh)",
|
||||
desc: "Augmented major 7th",
|
||||
example: "c4 augmaj7 => 60 64 68 71",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "7sus4",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root fourth fifth seventh)",
|
||||
desc: "Dominant 7 sus4",
|
||||
example: "c4 7sus4 => 60 65 67 70",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "9sus4",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root fourth fifth seventh ninth)",
|
||||
desc: "9 sus4",
|
||||
example: "c4 9sus4 => 60 65 67 70 74",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
// Chords - Sixth
|
||||
Word {
|
||||
name: "maj6",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth sixth)",
|
||||
desc: "Major 6th",
|
||||
example: "c4 maj6 => 60 64 67 69",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "min6",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth sixth)",
|
||||
desc: "Minor 6th",
|
||||
example: "c4 min6 => 60 63 67 69",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "maj69",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth sixth ninth)",
|
||||
desc: "Major 6/9",
|
||||
example: "c4 maj69 => 60 64 67 69 74",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "min69",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth sixth ninth)",
|
||||
desc: "Minor 6/9",
|
||||
example: "c4 min69 => 60 63 67 69 74",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
// Chords - Extended
|
||||
Word {
|
||||
name: "dom9",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth seventh ninth)",
|
||||
desc: "Dominant 9th",
|
||||
example: "c4 dom9 => 60 64 67 70 74",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "maj9",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth seventh ninth)",
|
||||
desc: "Major 9th",
|
||||
example: "c4 maj9 => 60 64 67 71 74",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "min9",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth seventh ninth)",
|
||||
desc: "Minor 9th",
|
||||
example: "c4 min9 => 60 63 67 70 74",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "dom11",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth seventh ninth eleventh)",
|
||||
desc: "Dominant 11th",
|
||||
example: "c4 dom11 => 60 64 67 70 74 77",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "maj11",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth seventh ninth eleventh)",
|
||||
desc: "Major 11th",
|
||||
example: "c4 maj11 => 60 64 67 71 74 77",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "min11",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth seventh ninth eleventh)",
|
||||
desc: "Minor 11th",
|
||||
example: "c4 min11 => 60 63 67 70 74 77",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "dom13",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth seventh ninth thirteenth)",
|
||||
desc: "Dominant 13th",
|
||||
example: "c4 dom13 => 60 64 67 70 74 81",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "maj13",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth seventh ninth thirteenth)",
|
||||
desc: "Major 13th",
|
||||
example: "c4 maj13 => 60 64 67 71 74 81",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "min13",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth seventh ninth thirteenth)",
|
||||
desc: "Minor 13th",
|
||||
example: "c4 min13 => 60 63 67 70 74 81",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
// Chords - Add
|
||||
Word {
|
||||
name: "add9",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth ninth)",
|
||||
desc: "Major add 9",
|
||||
example: "c4 add9 => 60 64 67 74",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "add11",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth eleventh)",
|
||||
desc: "Major add 11",
|
||||
example: "c4 add11 => 60 64 67 77",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "madd9",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth ninth)",
|
||||
desc: "Minor add 9",
|
||||
example: "c4 madd9 => 60 63 67 74",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
// Chords - Altered dominants
|
||||
Word {
|
||||
name: "dom7b9",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth seventh flatninth)",
|
||||
desc: "7th flat 9",
|
||||
example: "c4 dom7b9 => 60 64 67 70 73",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "dom7s9",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth seventh sharpninth)",
|
||||
desc: "7th sharp 9 (Hendrix chord)",
|
||||
example: "c4 dom7s9 => 60 64 67 70 75",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "dom7b5",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third flatfifth seventh)",
|
||||
desc: "7th flat 5",
|
||||
example: "c4 dom7b5 => 60 64 66 70",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "dom7s5",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third sharpfifth seventh)",
|
||||
desc: "7th sharp 5",
|
||||
example: "c4 dom7s5 => 60 64 68 70",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "dom7s11",
|
||||
aliases: &[],
|
||||
category: "Chord",
|
||||
stack: "(root -- root third fifth seventh sharpelev)",
|
||||
desc: "7th sharp 11 (lydian dominant)",
|
||||
example: "c4 dom7s11 => 60 64 67 70 78",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
];
|
||||
524
crates/forth/src/words/sequencing.rs
Normal file
524
crates/forth/src/words/sequencing.rs
Normal file
@@ -0,0 +1,524 @@
|
||||
//! Word metadata for sequencing: probability, timing, context queries, generators.
|
||||
|
||||
use super::{Word, WordCompile::*};
|
||||
|
||||
pub(super) const WORDS: &[Word] = &[
|
||||
// Probability
|
||||
Word {
|
||||
name: "rand",
|
||||
aliases: &[],
|
||||
category: "Probability",
|
||||
stack: "(min max -- n|f)",
|
||||
desc: "Random in range. Int if both args are int, float otherwise",
|
||||
example: "1 6 rand => 4 | 0.0 1.0 rand => 0.42",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "exprand",
|
||||
aliases: &[],
|
||||
category: "Probability",
|
||||
stack: "(lo hi -- f)",
|
||||
desc: "Exponential random biased toward lo. Both args must be positive",
|
||||
example: "1.0 100.0 exprand => 3.7",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "logrand",
|
||||
aliases: &[],
|
||||
category: "Probability",
|
||||
stack: "(lo hi -- f)",
|
||||
desc: "Exponential random biased toward hi. Both args must be positive",
|
||||
example: "1.0 100.0 logrand => 87.2",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "seed",
|
||||
aliases: &[],
|
||||
category: "Probability",
|
||||
stack: "(n --)",
|
||||
desc: "Set random seed",
|
||||
example: "12345 seed",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "coin",
|
||||
aliases: &[],
|
||||
category: "Probability",
|
||||
stack: "(-- bool)",
|
||||
desc: "50/50 random boolean",
|
||||
example: "coin => 0 or 1",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "chance",
|
||||
aliases: &[],
|
||||
category: "Probability",
|
||||
stack: "(quot prob --)",
|
||||
desc: "Execute quotation with probability (0.0-1.0)",
|
||||
example: "( 2 distort ) 0.75 chance",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "prob",
|
||||
aliases: &[],
|
||||
category: "Probability",
|
||||
stack: "(quot pct --)",
|
||||
desc: "Execute quotation with probability (0-100)",
|
||||
example: "( 2 distort ) 75 prob",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "choose",
|
||||
aliases: &[],
|
||||
category: "Probability",
|
||||
stack: "(..n n -- val)",
|
||||
desc: "Random pick from n items",
|
||||
example: "1 2 3 3 choose",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "cycle",
|
||||
aliases: &[],
|
||||
category: "Probability",
|
||||
stack: "(v1..vn n -- selected)",
|
||||
desc: "Cycle through n items by step runs",
|
||||
example: "60 64 67 3 cycle",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "pcycle",
|
||||
aliases: &[],
|
||||
category: "Probability",
|
||||
stack: "(v1..vn n -- selected)",
|
||||
desc: "Cycle through n items by pattern iteration",
|
||||
example: "60 64 67 3 pcycle",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "bounce",
|
||||
aliases: &[],
|
||||
category: "Probability",
|
||||
stack: "(v1..vn n -- selected)",
|
||||
desc: "Ping-pong cycle through n items by step runs",
|
||||
example: "60 64 67 72 4 bounce",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "pbounce",
|
||||
aliases: &[],
|
||||
category: "Probability",
|
||||
stack: "(v1..vn n -- selected)",
|
||||
desc: "Ping-pong cycle through n items by pattern iteration",
|
||||
example: "60 64 67 72 4 pbounce",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "index",
|
||||
aliases: &[],
|
||||
category: "Probability",
|
||||
stack: "(v1..vn n idx -- selected)",
|
||||
desc: "Select item at explicit index",
|
||||
example: "[ c4 e4 g4 ] step index",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "wchoose",
|
||||
aliases: &[],
|
||||
category: "Probability",
|
||||
stack: "(v1 w1 v2 w2 ... n -- selected)",
|
||||
desc: "Weighted random pick from n value/weight pairs",
|
||||
example: "60 0.6 64 0.3 67 0.1 3 wchoose",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "always",
|
||||
aliases: &[],
|
||||
category: "Probability",
|
||||
stack: "(quot --)",
|
||||
desc: "Always execute quotation",
|
||||
example: "( 2 distort ) always",
|
||||
compile: Probability(1.0),
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "never",
|
||||
aliases: &[],
|
||||
category: "Probability",
|
||||
stack: "(quot --)",
|
||||
desc: "Never execute quotation",
|
||||
example: "( 2 distort ) never",
|
||||
compile: Probability(0.0),
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "often",
|
||||
aliases: &[],
|
||||
category: "Probability",
|
||||
stack: "(quot --)",
|
||||
desc: "Execute quotation 75% of the time",
|
||||
example: "( 2 distort ) often",
|
||||
compile: Probability(0.75),
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "sometimes",
|
||||
aliases: &[],
|
||||
category: "Probability",
|
||||
stack: "(quot --)",
|
||||
desc: "Execute quotation 50% of the time",
|
||||
example: "( 2 distort ) sometimes",
|
||||
compile: Probability(0.5),
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "rarely",
|
||||
aliases: &[],
|
||||
category: "Probability",
|
||||
stack: "(quot --)",
|
||||
desc: "Execute quotation 25% of the time",
|
||||
example: "( 2 distort ) rarely",
|
||||
compile: Probability(0.25),
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "almostNever",
|
||||
aliases: &[],
|
||||
category: "Probability",
|
||||
stack: "(quot --)",
|
||||
desc: "Execute quotation 10% of the time",
|
||||
example: "( 2 distort ) almostNever",
|
||||
compile: Probability(0.1),
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "almostAlways",
|
||||
aliases: &[],
|
||||
category: "Probability",
|
||||
stack: "(quot --)",
|
||||
desc: "Execute quotation 90% of the time",
|
||||
example: "( 2 distort ) almostAlways",
|
||||
compile: Probability(0.9),
|
||||
varargs: false,
|
||||
},
|
||||
// Time
|
||||
Word {
|
||||
name: "every",
|
||||
aliases: &[],
|
||||
category: "Time",
|
||||
stack: "(quot n --)",
|
||||
desc: "Execute quotation every nth iteration",
|
||||
example: "( 2 distort ) 4 every",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "except",
|
||||
aliases: &[],
|
||||
category: "Time",
|
||||
stack: "(quot n --)",
|
||||
desc: "Execute quotation on all iterations except every nth",
|
||||
example: "( 2 distort ) 4 except",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "every+",
|
||||
aliases: &[],
|
||||
category: "Time",
|
||||
stack: "(quot n offset --)",
|
||||
desc: "Execute quotation every nth iteration with phase offset",
|
||||
example: "( snare ) 4 2 every+ => fires at iter 2, 6, 10...",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "except+",
|
||||
aliases: &[],
|
||||
category: "Time",
|
||||
stack: "(quot n offset --)",
|
||||
desc: "Skip quotation every nth iteration with phase offset",
|
||||
example: "( snare ) 4 2 except+ => skips at iter 2, 6, 10...",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "bjork",
|
||||
aliases: &[],
|
||||
category: "Time",
|
||||
stack: "(quot k n --)",
|
||||
desc: "Execute quotation using Euclidean distribution over step runs",
|
||||
example: "( 2 distort ) 3 8 bjork",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "pbjork",
|
||||
aliases: &[],
|
||||
category: "Time",
|
||||
stack: "(quot k n --)",
|
||||
desc: "Execute quotation using Euclidean distribution over pattern iterations",
|
||||
example: "( 2 distort ) 3 8 pbjork",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "loop",
|
||||
aliases: &[],
|
||||
category: "Time",
|
||||
stack: "(n --)",
|
||||
desc: "Fit sample to n steps",
|
||||
example: "\"break\" s 16 loop @",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "tempo!",
|
||||
aliases: &[],
|
||||
category: "Time",
|
||||
stack: "(bpm --)",
|
||||
desc: "Set global tempo",
|
||||
example: "140 tempo!",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "speed!",
|
||||
aliases: &[],
|
||||
category: "Time",
|
||||
stack: "(multiplier --)",
|
||||
desc: "Set pattern speed multiplier",
|
||||
example: "2.0 speed!",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "at",
|
||||
aliases: &[],
|
||||
category: "Time",
|
||||
stack: "(v1..vn --)",
|
||||
desc: "Set delta context for emit timing",
|
||||
example: "0 0.5 at kick s . => emits at 0 and 0.5 of step",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
// Context
|
||||
Word {
|
||||
name: "step",
|
||||
aliases: &[],
|
||||
category: "Context",
|
||||
stack: "(-- n)",
|
||||
desc: "Current step index",
|
||||
example: "step => 0",
|
||||
compile: Context("step"),
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "beat",
|
||||
aliases: &[],
|
||||
category: "Context",
|
||||
stack: "(-- f)",
|
||||
desc: "Current beat position",
|
||||
example: "beat => 4.5",
|
||||
compile: Context("beat"),
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "pattern",
|
||||
aliases: &[],
|
||||
category: "Context",
|
||||
stack: "(-- n)",
|
||||
desc: "Current pattern index",
|
||||
example: "pattern => 0",
|
||||
compile: Context("pattern"),
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "pbank",
|
||||
aliases: &[],
|
||||
category: "Context",
|
||||
stack: "(-- n)",
|
||||
desc: "Current pattern's bank index",
|
||||
example: "pbank => 0",
|
||||
compile: Context("bank"),
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "tempo",
|
||||
aliases: &[],
|
||||
category: "Context",
|
||||
stack: "(-- f)",
|
||||
desc: "Current BPM",
|
||||
example: "tempo => 120.0",
|
||||
compile: Context("tempo"),
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "phase",
|
||||
aliases: &[],
|
||||
category: "Context",
|
||||
stack: "(-- f)",
|
||||
desc: "Phase in bar (0-1)",
|
||||
example: "phase => 0.25",
|
||||
compile: Context("phase"),
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "slot",
|
||||
aliases: &[],
|
||||
category: "Context",
|
||||
stack: "(-- n)",
|
||||
desc: "Current slot number",
|
||||
example: "slot => 0",
|
||||
compile: Context("slot"),
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "runs",
|
||||
aliases: &[],
|
||||
category: "Context",
|
||||
stack: "(-- n)",
|
||||
desc: "Times this step ran",
|
||||
example: "runs => 3",
|
||||
compile: Context("runs"),
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "iter",
|
||||
aliases: &[],
|
||||
category: "Context",
|
||||
stack: "(-- n)",
|
||||
desc: "Pattern iteration count",
|
||||
example: "iter => 2",
|
||||
compile: Context("iter"),
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "stepdur",
|
||||
aliases: &[],
|
||||
category: "Context",
|
||||
stack: "(-- f)",
|
||||
desc: "Step duration in seconds",
|
||||
example: "stepdur => 0.125",
|
||||
compile: Context("stepdur"),
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "fill",
|
||||
aliases: &[],
|
||||
category: "Context",
|
||||
stack: "(-- bool)",
|
||||
desc: "True when fill is on (f key)",
|
||||
example: "\"snare\" s . fill ?",
|
||||
compile: Context("fill"),
|
||||
varargs: false,
|
||||
},
|
||||
// Desktop
|
||||
#[cfg(feature = "desktop")]
|
||||
Word {
|
||||
name: "mx",
|
||||
aliases: &[],
|
||||
category: "Desktop",
|
||||
stack: "(-- x)",
|
||||
desc: "Normalized mouse X position (0-1)",
|
||||
example: "mx 440 880 range freq",
|
||||
compile: Context("mx"),
|
||||
varargs: false,
|
||||
},
|
||||
#[cfg(feature = "desktop")]
|
||||
Word {
|
||||
name: "my",
|
||||
aliases: &[],
|
||||
category: "Desktop",
|
||||
stack: "(-- y)",
|
||||
desc: "Normalized mouse Y position (0-1)",
|
||||
example: "my 0.1 0.9 range gain",
|
||||
compile: Context("my"),
|
||||
varargs: false,
|
||||
},
|
||||
#[cfg(feature = "desktop")]
|
||||
Word {
|
||||
name: "mdown",
|
||||
aliases: &[],
|
||||
category: "Desktop",
|
||||
stack: "(-- bool)",
|
||||
desc: "1 when mouse button held, 0 otherwise",
|
||||
example: "mdown ( \"crash\" s . ) ?",
|
||||
compile: Context("mdown"),
|
||||
varargs: false,
|
||||
},
|
||||
// Generator
|
||||
Word {
|
||||
name: "..",
|
||||
aliases: &[],
|
||||
category: "Generator",
|
||||
stack: "(start end -- start start+1 ... end)",
|
||||
desc: "Push arithmetic sequence from start to end",
|
||||
example: "1 4 .. => 1 2 3 4",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: ".,",
|
||||
aliases: &[],
|
||||
category: "Generator",
|
||||
stack: "(start end step -- start start+step ...)",
|
||||
desc: "Push arithmetic sequence with custom step",
|
||||
example: "0 1 0.25 ., => 0 0.25 0.5 0.75 1",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "gen",
|
||||
aliases: &[],
|
||||
category: "Generator",
|
||||
stack: "(quot n -- results...)",
|
||||
desc: "Execute quotation n times, push all results",
|
||||
example: "( 1 6 rand ) 4 gen => 4 random values",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "geom..",
|
||||
aliases: &[],
|
||||
category: "Generator",
|
||||
stack: "(start ratio count -- start start*r start*r^2 ...)",
|
||||
desc: "Push geometric sequence",
|
||||
example: "1 2 4 geom.. => 1 2 4 8",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "euclid",
|
||||
aliases: &[],
|
||||
category: "Generator",
|
||||
stack: "(k n -- i1 i2 ... ik)",
|
||||
desc: "Push indices for k hits evenly distributed over n steps",
|
||||
example: "4 8 euclid => 0 2 4 6",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "euclidrot",
|
||||
aliases: &[],
|
||||
category: "Generator",
|
||||
stack: "(k n r -- i1 i2 ... ik)",
|
||||
desc: "Push Euclidean indices with rotation r",
|
||||
example: "3 8 2 euclidrot => 1 4 6",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
];
|
||||
895
crates/forth/src/words/sound.rs
Normal file
895
crates/forth/src/words/sound.rs
Normal file
@@ -0,0 +1,895 @@
|
||||
//! Word metadata for sound commands, sample/oscillator params, FM, modulation, and LFO.
|
||||
|
||||
use super::{Word, WordCompile::*};
|
||||
|
||||
pub(super) const WORDS: &[Word] = &[
|
||||
// Sound
|
||||
Word {
|
||||
name: "sound",
|
||||
aliases: &["snd"],
|
||||
category: "Sound",
|
||||
stack: "(name --)",
|
||||
desc: "Begin sound command",
|
||||
example: "\"kick\" sound",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: ".",
|
||||
aliases: &[],
|
||||
category: "Sound",
|
||||
stack: "(--)",
|
||||
desc: "Emit current sound",
|
||||
example: "\"kick\" s . . . .",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "arp",
|
||||
aliases: &[],
|
||||
category: "Sound",
|
||||
stack: "(v1..vn -- arplist)",
|
||||
desc: "Wrap stack values as arpeggio list for spreading across deltas",
|
||||
example: "c4 e4 g4 b4 arp note => arpeggio",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "clear",
|
||||
aliases: &[],
|
||||
category: "Sound",
|
||||
stack: "(--)",
|
||||
desc: "Clear sound register (sound and all params)",
|
||||
example: "\"kick\" s 0.5 gain . clear \"hat\" s .",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "all",
|
||||
aliases: &[],
|
||||
category: "Sound",
|
||||
stack: "(--)",
|
||||
desc: "Apply current params to all sounds",
|
||||
example: "500 lpf 0.5 verb all",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "noall",
|
||||
aliases: &[],
|
||||
category: "Sound",
|
||||
stack: "(--)",
|
||||
desc: "Clear global params",
|
||||
example: "noall",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
// Recording
|
||||
Word {
|
||||
name: "rec",
|
||||
aliases: &[],
|
||||
category: "Sound",
|
||||
stack: "(name --)",
|
||||
desc: "Toggle recording audio output to named sample",
|
||||
example: "\"loop1\" rec",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "overdub",
|
||||
aliases: &["dub"],
|
||||
category: "Sound",
|
||||
stack: "(name --)",
|
||||
desc: "Toggle overdub recording onto existing named sample",
|
||||
example: "\"loop1\" overdub",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "orec",
|
||||
aliases: &[],
|
||||
category: "Sound",
|
||||
stack: "(name orbit --)",
|
||||
desc: "Toggle recording a single orbit into named sample",
|
||||
example: "\"drums\" 0 orec",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "odub",
|
||||
aliases: &[],
|
||||
category: "Sound",
|
||||
stack: "(name orbit --)",
|
||||
desc: "Toggle overdub recording a single orbit onto named sample",
|
||||
example: "\"drums\" 0 odub",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
// Sample
|
||||
Word {
|
||||
name: "bank",
|
||||
aliases: &[],
|
||||
category: "Sample",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set sample bank suffix",
|
||||
example: "\"a\" bank",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "time",
|
||||
aliases: &[],
|
||||
category: "Sample",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set time offset",
|
||||
example: "0.1 time",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "repeat",
|
||||
aliases: &[],
|
||||
category: "Sample",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set repeat count",
|
||||
example: "4 repeat",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "dur",
|
||||
aliases: &[],
|
||||
category: "Sample",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set duration",
|
||||
example: "0.5 dur",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "gate",
|
||||
aliases: &[],
|
||||
category: "Sample",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set gate time",
|
||||
example: "0.8 gate",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "speed",
|
||||
aliases: &[],
|
||||
category: "Sample",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set playback speed",
|
||||
example: "1.5 speed",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "stretch",
|
||||
aliases: &[],
|
||||
category: "Sample",
|
||||
stack: "(v.. --)",
|
||||
desc: "Time stretch factor (pitch-independent)",
|
||||
example: "2 stretch",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "begin",
|
||||
aliases: &[],
|
||||
category: "Sample",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set sample start (0-1)",
|
||||
example: "0.25 begin",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "end",
|
||||
aliases: &[],
|
||||
category: "Sample",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set sample end (0-1)",
|
||||
example: "0.75 end",
|
||||
compile: Param,
|
||||
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 {
|
||||
name: "voice",
|
||||
aliases: &[],
|
||||
category: "Sample",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set voice number",
|
||||
example: "1 voice",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "orbit",
|
||||
aliases: &[],
|
||||
category: "Sample",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set orbit/bus",
|
||||
example: "0 orbit",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "n",
|
||||
aliases: &[],
|
||||
category: "Sample",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set sample number",
|
||||
example: "0 n",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "cut",
|
||||
aliases: &[],
|
||||
category: "Sample",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set cut group",
|
||||
example: "1 cut",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "reset",
|
||||
aliases: &[],
|
||||
category: "Sample",
|
||||
stack: "(v.. --)",
|
||||
desc: "Reset parameter",
|
||||
example: "1 reset",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
// Oscillator
|
||||
Word {
|
||||
name: "freq",
|
||||
aliases: &[],
|
||||
category: "Oscillator",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set frequency (Hz)",
|
||||
example: "440 freq",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "detune",
|
||||
aliases: &[],
|
||||
category: "Oscillator",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set detune amount",
|
||||
example: "0.01 detune",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "glide",
|
||||
aliases: &[],
|
||||
category: "Oscillator",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set glide/portamento",
|
||||
example: "0.1 glide",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "pw",
|
||||
aliases: &[],
|
||||
category: "Oscillator",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set pulse width",
|
||||
example: "0.5 pw",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "wave",
|
||||
aliases: &["waveform"],
|
||||
category: "Oscillator",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set drum waveform [0,1]: 0=sine, 0.5=tri, 1=saw",
|
||||
example: "0.5 wave",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "spread",
|
||||
aliases: &[],
|
||||
category: "Oscillator",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set stereo spread",
|
||||
example: "0.5 spread",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "mult",
|
||||
aliases: &[],
|
||||
category: "Oscillator",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set multiplier",
|
||||
example: "2 mult",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "warp",
|
||||
aliases: &[],
|
||||
category: "Oscillator",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set warp amount",
|
||||
example: "0.5 warp",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "mirror",
|
||||
aliases: &[],
|
||||
category: "Oscillator",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set mirror",
|
||||
example: "1 mirror",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "harmonics",
|
||||
aliases: &[],
|
||||
category: "Oscillator",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set harmonics (mutable only)",
|
||||
example: "4 harmonics",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "timbre",
|
||||
aliases: &[],
|
||||
category: "Oscillator",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set timbre (mutable only)",
|
||||
example: "0.5 timbre",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "morph",
|
||||
aliases: &[],
|
||||
category: "Oscillator",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set morph (mutable only)",
|
||||
example: "0.5 morph",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "partials",
|
||||
aliases: &[],
|
||||
category: "Oscillator",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set number of active harmonics (add source only)",
|
||||
example: "16 partials",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "coarse",
|
||||
aliases: &[],
|
||||
category: "Oscillator",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set coarse tune",
|
||||
example: "12 coarse",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "sub",
|
||||
aliases: &[],
|
||||
category: "Oscillator",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set sub oscillator level",
|
||||
example: "0.5 sub",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "suboct",
|
||||
aliases: &[],
|
||||
category: "Oscillator",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set sub oscillator octave",
|
||||
example: "2 suboct",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "subwave",
|
||||
aliases: &[],
|
||||
category: "Oscillator",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set sub oscillator waveform",
|
||||
example: "1 subwave",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "note",
|
||||
aliases: &[],
|
||||
category: "Oscillator",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set MIDI note",
|
||||
example: "60 note",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
// Wavetable
|
||||
Word {
|
||||
name: "scan",
|
||||
aliases: &[],
|
||||
category: "Wavetable",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set wavetable scan position (0-1)",
|
||||
example: "0.5 scan",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "wtlen",
|
||||
aliases: &[],
|
||||
category: "Wavetable",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set wavetable cycle length in samples",
|
||||
example: "2048 wtlen",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "scanlfo",
|
||||
aliases: &[],
|
||||
category: "Wavetable",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set scan LFO rate (Hz)",
|
||||
example: "0.2 scanlfo",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "scandepth",
|
||||
aliases: &[],
|
||||
category: "Wavetable",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set scan LFO depth (0-1)",
|
||||
example: "0.4 scandepth",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "scanshape",
|
||||
aliases: &[],
|
||||
category: "Wavetable",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set scan LFO shape (sine/tri/saw/square/sh)",
|
||||
example: "\"tri\" scanshape",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
// FM
|
||||
Word {
|
||||
name: "fm",
|
||||
aliases: &[],
|
||||
category: "FM",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set FM frequency",
|
||||
example: "200 fm",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "fmh",
|
||||
aliases: &[],
|
||||
category: "FM",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set FM harmonic ratio",
|
||||
example: "2 fmh",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "fmshape",
|
||||
aliases: &[],
|
||||
category: "FM",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set FM shape",
|
||||
example: "0 fmshape",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "fme",
|
||||
aliases: &[],
|
||||
category: "FM",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set FM envelope",
|
||||
example: "0.5 fme",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "fma",
|
||||
aliases: &[],
|
||||
category: "FM",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set FM attack",
|
||||
example: "0.01 fma",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "fmd",
|
||||
aliases: &[],
|
||||
category: "FM",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set FM decay",
|
||||
example: "0.1 fmd",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "fms",
|
||||
aliases: &[],
|
||||
category: "FM",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set FM sustain",
|
||||
example: "0.5 fms",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "fmr",
|
||||
aliases: &[],
|
||||
category: "FM",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set FM release",
|
||||
example: "0.1 fmr",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "fm2",
|
||||
aliases: &[],
|
||||
category: "FM",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set FM operator 2 depth",
|
||||
example: "1.5 fm2",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "fm2h",
|
||||
aliases: &[],
|
||||
category: "FM",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set FM operator 2 harmonic ratio",
|
||||
example: "3 fm2h",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "fmalgo",
|
||||
aliases: &[],
|
||||
category: "FM",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set FM algorithm (0=cascade 1=parallel 2=branch)",
|
||||
example: "0 fmalgo",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "fmfb",
|
||||
aliases: &[],
|
||||
category: "FM",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set FM feedback amount",
|
||||
example: "0.5 fmfb",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
// Modulation
|
||||
Word {
|
||||
name: "vib",
|
||||
aliases: &[],
|
||||
category: "Modulation",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set vibrato rate",
|
||||
example: "5 vib",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "vibmod",
|
||||
aliases: &[],
|
||||
category: "Modulation",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set vibrato depth",
|
||||
example: "0.5 vibmod",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "vibshape",
|
||||
aliases: &[],
|
||||
category: "Modulation",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set vibrato shape",
|
||||
example: "0 vibshape",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "am",
|
||||
aliases: &[],
|
||||
category: "Modulation",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set AM frequency",
|
||||
example: "10 am",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "amdepth",
|
||||
aliases: &[],
|
||||
category: "Modulation",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set AM depth",
|
||||
example: "0.5 amdepth",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "amshape",
|
||||
aliases: &[],
|
||||
category: "Modulation",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set AM shape",
|
||||
example: "0 amshape",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "rm",
|
||||
aliases: &[],
|
||||
category: "Modulation",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set RM frequency",
|
||||
example: "100 rm",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "rmdepth",
|
||||
aliases: &[],
|
||||
category: "Modulation",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set RM depth",
|
||||
example: "0.5 rmdepth",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "rmshape",
|
||||
aliases: &[],
|
||||
category: "Modulation",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set RM shape",
|
||||
example: "0 rmshape",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
// LFO
|
||||
Word {
|
||||
name: "ramp",
|
||||
aliases: &[],
|
||||
category: "LFO",
|
||||
stack: "(freq curve -- val)",
|
||||
desc: "Ramp [0,1]: fract(freq*beat)^curve",
|
||||
example: "0.25 2.0 ramp",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "range",
|
||||
aliases: &[],
|
||||
category: "LFO",
|
||||
stack: "(val min max -- scaled)",
|
||||
desc: "Scale [0,1] to [min,max]",
|
||||
example: "0.5 200 800 range => 500",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "linramp",
|
||||
aliases: &[],
|
||||
category: "LFO",
|
||||
stack: "(freq -- val)",
|
||||
desc: "Linear ramp (curve=1)",
|
||||
example: "1.0 linramp",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "expramp",
|
||||
aliases: &[],
|
||||
category: "LFO",
|
||||
stack: "(freq -- val)",
|
||||
desc: "Exponential ramp (curve=3)",
|
||||
example: "0.25 expramp",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "logramp",
|
||||
aliases: &[],
|
||||
category: "LFO",
|
||||
stack: "(freq -- val)",
|
||||
desc: "Logarithmic ramp (curve=0.3)",
|
||||
example: "2.0 logramp",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "triangle",
|
||||
aliases: &[],
|
||||
category: "LFO",
|
||||
stack: "(freq -- val)",
|
||||
desc: "Triangle wave [0,1]: 0→1→0",
|
||||
example: "0.5 triangle",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "perlin",
|
||||
aliases: &[],
|
||||
category: "LFO",
|
||||
stack: "(freq -- val)",
|
||||
desc: "Perlin noise [0,1] sampled at freq*beat",
|
||||
example: "0.25 perlin",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
// Audio-rate Modulation DSL
|
||||
Word {
|
||||
name: "lfo",
|
||||
aliases: &[],
|
||||
category: "Audio Modulation",
|
||||
stack: "(min max period -- str)",
|
||||
desc: "Sine oscillation: min~max:period",
|
||||
example: "200 4000 2 lfo lpf",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "tlfo",
|
||||
aliases: &[],
|
||||
category: "Audio Modulation",
|
||||
stack: "(min max period -- str)",
|
||||
desc: "Triangle oscillation: min~max:periodt",
|
||||
example: "0.3 0.7 0.5 tlfo pan",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "wlfo",
|
||||
aliases: &[],
|
||||
category: "Audio Modulation",
|
||||
stack: "(min max period -- str)",
|
||||
desc: "Sawtooth oscillation: min~max:periodw",
|
||||
example: "200 4000 1 wlfo lpf",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "qlfo",
|
||||
aliases: &[],
|
||||
category: "Audio Modulation",
|
||||
stack: "(min max period -- str)",
|
||||
desc: "Square oscillation: min~max:periodq",
|
||||
example: "0.0 1.0 0.25 qlfo gain",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "slide",
|
||||
aliases: &[],
|
||||
category: "Audio Modulation",
|
||||
stack: "(start end dur -- str)",
|
||||
desc: "Linear transition: start>end:dur",
|
||||
example: "0 1 0.01 slide gain",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "expslide",
|
||||
aliases: &[],
|
||||
category: "Audio Modulation",
|
||||
stack: "(start end dur -- str)",
|
||||
desc: "Exponential transition: start>end:dure",
|
||||
example: "0 1 0.5 expslide gain",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "sslide",
|
||||
aliases: &[],
|
||||
category: "Audio Modulation",
|
||||
stack: "(start end dur -- str)",
|
||||
desc: "Smooth transition: start>end:durs",
|
||||
example: "200 800 1 sslide lpf",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "jit",
|
||||
aliases: &[],
|
||||
category: "Audio Modulation",
|
||||
stack: "(min max period -- str)",
|
||||
desc: "Random hold: min?max:period",
|
||||
example: "200 4000 0.5 jit lpf",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "sjit",
|
||||
aliases: &[],
|
||||
category: "Audio Modulation",
|
||||
stack: "(min max period -- str)",
|
||||
desc: "Smooth random: min?max:periods",
|
||||
example: "200 4000 0.5 sjit lpf",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "drunk",
|
||||
aliases: &[],
|
||||
category: "Audio Modulation",
|
||||
stack: "(min max period -- str)",
|
||||
desc: "Drunk walk: min?max:periodd",
|
||||
example: "200 4000 0.5 drunk lpf",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "env",
|
||||
aliases: &[],
|
||||
category: "Audio Modulation",
|
||||
stack: "(start t1 d1 ... -- str)",
|
||||
desc: "Multi-segment envelope: start>t1:d1>...",
|
||||
example: "0 1 0.01 0.7 0.1 0 2 env gain",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
];
|
||||
15
crates/markdown/README.md
Normal file
15
crates/markdown/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# cagire-markdown
|
||||
|
||||
Markdown parser and renderer that produces ratatui-styled lines. Used for the built-in help/documentation views.
|
||||
|
||||
## Modules
|
||||
|
||||
| Module | Description |
|
||||
|--------|-------------|
|
||||
| `parser` | Markdown-to-styled-lines conversion |
|
||||
| `highlighter` | `CodeHighlighter` trait for syntax highlighting in fenced code blocks |
|
||||
| `theme` | Color mappings for markdown elements |
|
||||
|
||||
## Key Trait
|
||||
|
||||
- **`CodeHighlighter`** — Implement to provide language-specific syntax highlighting. Returns `Vec<(Style, String)>` per line.
|
||||
@@ -1,9 +1,13 @@
|
||||
//! Syntax highlighting trait for fenced code blocks in markdown.
|
||||
|
||||
use ratatui::style::Style;
|
||||
|
||||
/// Produce styled spans from a single line of source code.
|
||||
pub trait CodeHighlighter {
|
||||
fn highlight(&self, line: &str) -> Vec<(Style, String)>;
|
||||
}
|
||||
|
||||
/// Pass-through highlighter that applies no styling.
|
||||
pub struct NoHighlight;
|
||||
|
||||
impl CodeHighlighter for NoHighlight {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
//! Parse markdown into styled ratatui lines with pluggable syntax highlighting.
|
||||
|
||||
mod highlighter;
|
||||
mod parser;
|
||||
mod theme;
|
||||
|
||||
pub use highlighter::{CodeHighlighter, NoHighlight};
|
||||
pub use parser::parse;
|
||||
pub use parser::{parse, CodeBlock, ParsedMarkdown};
|
||||
pub use theme::{DefaultTheme, MarkdownTheme};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Parse markdown text into styled ratatui lines with syntax-highlighted code blocks.
|
||||
|
||||
use minimad::{Composite, CompositeStyle, Compound, Line, TableRow};
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::text::{Line as RLine, Span};
|
||||
@@ -5,17 +7,34 @@ use ratatui::text::{Line as RLine, Span};
|
||||
use crate::highlighter::CodeHighlighter;
|
||||
use crate::theme::MarkdownTheme;
|
||||
|
||||
/// Span of lines within a parsed document that form a fenced code block.
|
||||
pub struct CodeBlock {
|
||||
pub start_line: usize,
|
||||
pub end_line: usize,
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
/// Result of parsing a markdown string: styled lines and extracted code blocks.
|
||||
pub struct ParsedMarkdown {
|
||||
pub lines: Vec<RLine<'static>>,
|
||||
pub code_blocks: Vec<CodeBlock>,
|
||||
}
|
||||
|
||||
/// Parse markdown text into themed, syntax-highlighted ratatui lines.
|
||||
pub fn parse<T: MarkdownTheme, H: CodeHighlighter>(
|
||||
md: &str,
|
||||
theme: &T,
|
||||
highlighter: &H,
|
||||
) -> Vec<RLine<'static>> {
|
||||
) -> ParsedMarkdown {
|
||||
let processed = preprocess_markdown(md);
|
||||
let text = minimad::Text::from(processed.as_str());
|
||||
let mut lines = Vec::new();
|
||||
|
||||
let mut code_line_nr: usize = 0;
|
||||
let mut table_buffer: Vec<TableRow> = Vec::new();
|
||||
let mut code_blocks: Vec<CodeBlock> = Vec::new();
|
||||
let mut current_block_start: Option<usize> = None;
|
||||
let mut current_block_source: Vec<String> = Vec::new();
|
||||
|
||||
let flush_table = |buf: &mut Vec<TableRow>, out: &mut Vec<RLine<'static>>, theme: &T| {
|
||||
if buf.is_empty() {
|
||||
@@ -27,16 +46,43 @@ pub fn parse<T: MarkdownTheme, H: CodeHighlighter>(
|
||||
}
|
||||
};
|
||||
|
||||
let close_block = |start: Option<usize>,
|
||||
source: &mut Vec<String>,
|
||||
blocks: &mut Vec<CodeBlock>,
|
||||
lines: &[RLine<'static>]| {
|
||||
if let Some(start) = start {
|
||||
blocks.push(CodeBlock {
|
||||
start_line: start,
|
||||
end_line: lines.len(),
|
||||
source: std::mem::take(source).join("\n"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
for line in text.lines {
|
||||
let is_code = matches!(&line, Line::Normal(c) if c.style == CompositeStyle::Code);
|
||||
if !is_code {
|
||||
close_block(
|
||||
current_block_start.take(),
|
||||
&mut current_block_source,
|
||||
&mut code_blocks,
|
||||
&lines,
|
||||
);
|
||||
}
|
||||
|
||||
match line {
|
||||
Line::Normal(composite) if composite.style == CompositeStyle::Code => {
|
||||
flush_table(&mut table_buffer, &mut lines, theme);
|
||||
code_line_nr += 1;
|
||||
if current_block_start.is_none() {
|
||||
current_block_start = Some(lines.len());
|
||||
}
|
||||
let raw: String = composite
|
||||
.compounds
|
||||
.iter()
|
||||
.map(|c: &minimad::Compound| c.src)
|
||||
.collect();
|
||||
current_block_source.push(raw.clone());
|
||||
let mut spans = vec![
|
||||
Span::styled(format!(" {code_line_nr:>2} "), theme.code_border()),
|
||||
Span::styled("│ ", theme.code_border()),
|
||||
@@ -66,12 +112,18 @@ pub fn parse<T: MarkdownTheme, H: CodeHighlighter>(
|
||||
}
|
||||
}
|
||||
}
|
||||
close_block(
|
||||
current_block_start.take(),
|
||||
&mut current_block_source,
|
||||
&mut code_blocks,
|
||||
&lines,
|
||||
);
|
||||
flush_table(&mut table_buffer, &mut lines, theme);
|
||||
|
||||
lines
|
||||
ParsedMarkdown { lines, code_blocks }
|
||||
}
|
||||
|
||||
pub fn preprocess_markdown(md: &str) -> String {
|
||||
fn preprocess_markdown(md: &str) -> String {
|
||||
let mut out = String::with_capacity(md.len());
|
||||
for line in md.lines() {
|
||||
let line = convert_dash_lists(line);
|
||||
@@ -115,7 +167,7 @@ pub fn preprocess_markdown(md: &str) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
pub fn convert_dash_lists(line: &str) -> String {
|
||||
fn convert_dash_lists(line: &str) -> String {
|
||||
let trimmed = line.trim_start();
|
||||
if let Some(rest) = trimmed.strip_prefix("- ") {
|
||||
let indent = line.len() - trimmed.len();
|
||||
@@ -300,28 +352,39 @@ mod tests {
|
||||
#[test]
|
||||
fn test_parse_headings() {
|
||||
let md = "# H1\n## H2\n### H3";
|
||||
let lines = parse(md, &DefaultTheme, &NoHighlight);
|
||||
assert_eq!(lines.len(), 3);
|
||||
let parsed = parse(md, &DefaultTheme, &NoHighlight);
|
||||
assert_eq!(parsed.lines.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_code_block() {
|
||||
let md = "```\ncode line\n```";
|
||||
let lines = parse(md, &DefaultTheme, &NoHighlight);
|
||||
assert!(!lines.is_empty());
|
||||
let parsed = parse(md, &DefaultTheme, &NoHighlight);
|
||||
assert!(!parsed.lines.is_empty());
|
||||
assert_eq!(parsed.code_blocks.len(), 1);
|
||||
assert_eq!(parsed.code_blocks[0].source, "code line");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_table() {
|
||||
let md = "| A | B |\n|---|---|\n| 1 | 2 |";
|
||||
let lines = parse(md, &DefaultTheme, &NoHighlight);
|
||||
assert_eq!(lines.len(), 2);
|
||||
let parsed = parse(md, &DefaultTheme, &NoHighlight);
|
||||
assert_eq!(parsed.lines.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_theme_works() {
|
||||
let md = "Hello **world**";
|
||||
let lines = parse(md, &DefaultTheme, &NoHighlight);
|
||||
assert_eq!(lines.len(), 1);
|
||||
let parsed = parse(md, &DefaultTheme, &NoHighlight);
|
||||
assert_eq!(parsed.lines.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_code_blocks() {
|
||||
let md = "text\n```\nfirst\n```\nmore text\n```\nsecond line 1\nsecond line 2\n```";
|
||||
let parsed = parse(md, &DefaultTheme, &NoHighlight);
|
||||
assert_eq!(parsed.code_blocks.len(), 2);
|
||||
assert_eq!(parsed.code_blocks[0].source, "first");
|
||||
assert_eq!(parsed.code_blocks[1].source, "second line 1\nsecond line 2");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
//! Style provider trait for markdown rendering.
|
||||
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
|
||||
/// Style provider for each markdown element type.
|
||||
pub trait MarkdownTheme {
|
||||
fn h1(&self) -> Style;
|
||||
fn h2(&self) -> Style;
|
||||
@@ -16,6 +19,7 @@ pub trait MarkdownTheme {
|
||||
fn table_row_odd(&self) -> Color;
|
||||
}
|
||||
|
||||
/// Fallback theme with hardcoded terminal colors, used in tests.
|
||||
pub struct DefaultTheme;
|
||||
|
||||
impl MarkdownTheme for DefaultTheme {
|
||||
|
||||
@@ -10,3 +10,9 @@ description = "Project data structures for cagire sequencer"
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
rmp-serde = "1"
|
||||
brotli = "7"
|
||||
base64 = "0.22"
|
||||
|
||||
[dev-dependencies]
|
||||
flate2 = "1"
|
||||
|
||||
22
crates/project/README.md
Normal file
22
crates/project/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# cagire-project
|
||||
|
||||
Project data model and persistence for Cagire.
|
||||
|
||||
## Modules
|
||||
|
||||
| Module | Description |
|
||||
|--------|-------------|
|
||||
| `project` | `Project`, `Bank`, `Pattern`, `Step` structs and constants |
|
||||
| `file` | File I/O (save/load) |
|
||||
| `share` | Project sharing/export |
|
||||
|
||||
## Key Types
|
||||
|
||||
- **`Project`** — Top-level container: banks of patterns
|
||||
- **`Bank`** — Collection of patterns
|
||||
- **`Pattern`** — Sequence of steps with metadata
|
||||
- **`Step`** — Single step holding a Forth script
|
||||
|
||||
## Constants
|
||||
|
||||
`MAX_BANKS=32`, `MAX_PATTERNS=32`, `MAX_STEPS=1024`
|
||||
@@ -1,15 +1,17 @@
|
||||
//! JSON-based project file persistence with versioned format.
|
||||
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::project::{Bank, Project};
|
||||
use crate::project::{Bank, PatternSpeed, Project};
|
||||
|
||||
const VERSION: u8 = 1;
|
||||
pub const EXTENSION: &str = "cagire";
|
||||
const EXTENSION: &str = "cagire";
|
||||
|
||||
pub fn ensure_extension(path: &Path) -> PathBuf {
|
||||
fn ensure_extension(path: &Path) -> PathBuf {
|
||||
if path.extension().map(|e| e == EXTENSION).unwrap_or(false) {
|
||||
path.to_path_buf()
|
||||
} else {
|
||||
@@ -27,6 +29,26 @@ struct ProjectFile {
|
||||
tempo: f64,
|
||||
#[serde(default)]
|
||||
playing_patterns: Vec<(usize, usize)>,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
prelude: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
script: String,
|
||||
#[serde(default, skip_serializing_if = "is_default_speed")]
|
||||
script_speed: PatternSpeed,
|
||||
#[serde(default = "default_script_length", skip_serializing_if = "is_default_script_length")]
|
||||
script_length: usize,
|
||||
}
|
||||
|
||||
fn is_default_speed(s: &PatternSpeed) -> bool {
|
||||
*s == PatternSpeed::default()
|
||||
}
|
||||
|
||||
fn default_script_length() -> usize {
|
||||
16
|
||||
}
|
||||
|
||||
fn is_default_script_length(n: &usize) -> bool {
|
||||
*n == default_script_length()
|
||||
}
|
||||
|
||||
fn default_tempo() -> f64 {
|
||||
@@ -41,6 +63,10 @@ impl From<&Project> for ProjectFile {
|
||||
sample_paths: project.sample_paths.clone(),
|
||||
tempo: project.tempo,
|
||||
playing_patterns: project.playing_patterns.clone(),
|
||||
prelude: project.prelude.clone(),
|
||||
script: project.script.clone(),
|
||||
script_speed: project.script_speed,
|
||||
script_length: project.script_length,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,12 +78,17 @@ impl From<ProjectFile> for Project {
|
||||
sample_paths: file.sample_paths,
|
||||
tempo: file.tempo,
|
||||
playing_patterns: file.playing_patterns,
|
||||
prelude: file.prelude,
|
||||
script: file.script,
|
||||
script_speed: file.script_speed,
|
||||
script_length: file.script_length,
|
||||
};
|
||||
project.normalize();
|
||||
project
|
||||
}
|
||||
}
|
||||
|
||||
/// Error returned by project save/load operations.
|
||||
#[derive(Debug)]
|
||||
pub enum FileError {
|
||||
Io(io::Error),
|
||||
@@ -87,6 +118,7 @@ impl From<serde_json::Error> for FileError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a project to disk as pretty-printed JSON, returning the final path.
|
||||
pub fn save(project: &Project, path: &Path) -> Result<PathBuf, FileError> {
|
||||
let path = ensure_extension(path);
|
||||
let file = ProjectFile::from(project);
|
||||
@@ -95,9 +127,15 @@ pub fn save(project: &Project, path: &Path) -> Result<PathBuf, FileError> {
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Read a project from a `.cagire` file on disk.
|
||||
pub fn load(path: &Path) -> Result<Project, FileError> {
|
||||
let json = fs::read_to_string(path)?;
|
||||
let file: ProjectFile = serde_json::from_str(&json)?;
|
||||
load_str(&json)
|
||||
}
|
||||
|
||||
/// Parse a project from a JSON string.
|
||||
pub fn load_str(json: &str) -> Result<Project, FileError> {
|
||||
let file: ProjectFile = serde_json::from_str(json)?;
|
||||
if file.version > VERSION {
|
||||
return Err(FileError::Version(file.version));
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
//! Project data model: banks, patterns, and steps for the Cagire sequencer.
|
||||
|
||||
mod file;
|
||||
mod project;
|
||||
pub mod share;
|
||||
|
||||
/// Maximum number of banks in a project.
|
||||
pub const MAX_BANKS: usize = 32;
|
||||
/// Maximum number of patterns per bank.
|
||||
pub const MAX_PATTERNS: usize = 32;
|
||||
pub const MAX_STEPS: usize = 128;
|
||||
/// Maximum number of steps per pattern.
|
||||
pub const MAX_STEPS: usize = 1024;
|
||||
/// Default pattern length in steps.
|
||||
pub const DEFAULT_LENGTH: usize = 16;
|
||||
|
||||
pub use file::{load, save, FileError};
|
||||
pub use project::{Bank, LaunchQuantization, Pattern, PatternSpeed, Project, Step, SyncMode};
|
||||
pub use file::{load, load_str, save, FileError};
|
||||
pub use project::{Bank, FollowUp, LaunchQuantization, Pattern, PatternSpeed, Project, Step, SyncMode};
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
//! Project, Bank, Pattern, and Step structs with serialization.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
use crate::{DEFAULT_LENGTH, MAX_BANKS, MAX_PATTERNS, MAX_STEPS};
|
||||
|
||||
/// Speed multiplier for a pattern, expressed as a rational fraction.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub struct PatternSpeed {
|
||||
pub num: u8,
|
||||
@@ -35,10 +38,12 @@ impl PatternSpeed {
|
||||
Self::OCTO,
|
||||
];
|
||||
|
||||
/// Return the speed as a floating-point multiplier.
|
||||
pub fn multiplier(&self) -> f64 {
|
||||
self.num as f64 / self.denom as f64
|
||||
}
|
||||
|
||||
/// Format as a human-readable label (e.g. "2x", "1/4x").
|
||||
pub fn label(&self) -> String {
|
||||
if self.denom == 1 {
|
||||
format!("{}x", self.num)
|
||||
@@ -47,6 +52,7 @@ impl PatternSpeed {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the next faster preset, or self if already at maximum.
|
||||
pub fn next(&self) -> Self {
|
||||
let current = self.multiplier();
|
||||
Self::PRESETS
|
||||
@@ -56,6 +62,7 @@ impl PatternSpeed {
|
||||
.unwrap_or(*self)
|
||||
}
|
||||
|
||||
/// Return the next slower preset, or self if already at minimum.
|
||||
pub fn prev(&self) -> Self {
|
||||
let current = self.multiplier();
|
||||
Self::PRESETS
|
||||
@@ -66,6 +73,7 @@ impl PatternSpeed {
|
||||
.unwrap_or(*self)
|
||||
}
|
||||
|
||||
/// Parse a speed label like "2x" or "1/4x" into a `PatternSpeed`.
|
||||
pub fn from_label(s: &str) -> Option<Self> {
|
||||
let s = s.trim().trim_end_matches('x');
|
||||
if let Some((num, denom)) = s.split_once('/') {
|
||||
@@ -137,6 +145,7 @@ impl<'de> Deserialize<'de> for PatternSpeed {
|
||||
}
|
||||
}
|
||||
|
||||
/// Quantization grid for launching patterns.
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||
pub enum LaunchQuantization {
|
||||
Immediate,
|
||||
@@ -149,6 +158,7 @@ pub enum LaunchQuantization {
|
||||
}
|
||||
|
||||
impl LaunchQuantization {
|
||||
/// Human-readable label for display.
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Immediate => "Immediate",
|
||||
@@ -160,6 +170,18 @@ impl LaunchQuantization {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn short_label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Immediate => "Imm",
|
||||
Self::Beat => "Bt",
|
||||
Self::Bar => "1B",
|
||||
Self::Bars2 => "2B",
|
||||
Self::Bars4 => "4B",
|
||||
Self::Bars8 => "8B",
|
||||
}
|
||||
}
|
||||
|
||||
/// Cycle to the next longer quantization, clamped at `Bars8`.
|
||||
pub fn next(&self) -> Self {
|
||||
match self {
|
||||
Self::Immediate => Self::Beat,
|
||||
@@ -171,6 +193,7 @@ impl LaunchQuantization {
|
||||
}
|
||||
}
|
||||
|
||||
/// Cycle to the next shorter quantization, clamped at `Immediate`.
|
||||
pub fn prev(&self) -> Self {
|
||||
match self {
|
||||
Self::Immediate => Self::Immediate,
|
||||
@@ -183,6 +206,7 @@ impl LaunchQuantization {
|
||||
}
|
||||
}
|
||||
|
||||
/// How a pattern synchronizes when launched: restart or phase-lock.
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||
pub enum SyncMode {
|
||||
#[default]
|
||||
@@ -191,6 +215,7 @@ pub enum SyncMode {
|
||||
}
|
||||
|
||||
impl SyncMode {
|
||||
/// Human-readable label for display.
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Reset => "Reset",
|
||||
@@ -198,6 +223,14 @@ impl SyncMode {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn short_label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Reset => "Rst",
|
||||
Self::PhaseLock => "Plk",
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle between Reset and PhaseLock.
|
||||
pub fn toggle(&self) -> Self {
|
||||
match self {
|
||||
Self::Reset => Self::PhaseLock,
|
||||
@@ -206,22 +239,69 @@ impl SyncMode {
|
||||
}
|
||||
}
|
||||
|
||||
/// What happens when a pattern finishes: loop, stop, or chain to another.
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||
pub enum FollowUp {
|
||||
#[default]
|
||||
Loop,
|
||||
Stop,
|
||||
Chain { bank: usize, pattern: usize },
|
||||
}
|
||||
|
||||
impl FollowUp {
|
||||
/// Human-readable label for display.
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Loop => "Loop",
|
||||
Self::Stop => "Stop",
|
||||
Self::Chain { .. } => "Chain",
|
||||
}
|
||||
}
|
||||
|
||||
/// Cycle forward through follow-up modes.
|
||||
pub fn next_mode(&self) -> Self {
|
||||
match self {
|
||||
Self::Loop => Self::Stop,
|
||||
Self::Stop => Self::Chain { bank: 0, pattern: 0 },
|
||||
Self::Chain { .. } => Self::Loop,
|
||||
}
|
||||
}
|
||||
|
||||
/// Cycle backward through follow-up modes.
|
||||
pub fn prev_mode(&self) -> Self {
|
||||
match self {
|
||||
Self::Loop => Self::Chain { bank: 0, pattern: 0 },
|
||||
Self::Stop => Self::Loop,
|
||||
Self::Chain { .. } => Self::Stop,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_default_follow_up(f: &FollowUp) -> bool {
|
||||
*f == FollowUp::default()
|
||||
}
|
||||
|
||||
/// Single step in a pattern, holding a Forth script and optional metadata.
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Step {
|
||||
pub active: bool,
|
||||
pub script: String,
|
||||
#[serde(skip)]
|
||||
pub command: Option<String>,
|
||||
#[serde(default)]
|
||||
pub source: Option<usize>,
|
||||
pub source: Option<u8>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
impl Step {
|
||||
/// True if all fields are at their default values.
|
||||
pub fn is_default(&self) -> bool {
|
||||
self.active && self.script.is_empty() && self.source.is_none() && self.name.is_none()
|
||||
}
|
||||
|
||||
/// True if the script is non-empty.
|
||||
pub fn has_content(&self) -> bool {
|
||||
!self.script.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Step {
|
||||
@@ -229,21 +309,23 @@ impl Default for Step {
|
||||
Self {
|
||||
active: true,
|
||||
script: String::new(),
|
||||
command: None,
|
||||
source: None,
|
||||
name: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sequence of steps with playback settings (speed, quantization, sync, follow-up).
|
||||
#[derive(Clone)]
|
||||
pub struct Pattern {
|
||||
pub steps: Vec<Step>,
|
||||
pub length: usize,
|
||||
pub speed: PatternSpeed,
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub quantization: LaunchQuantization,
|
||||
pub sync_mode: SyncMode,
|
||||
pub follow_up: FollowUp,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -254,7 +336,7 @@ struct SparseStep {
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
script: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
source: Option<usize>,
|
||||
source: Option<u8>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
name: Option<String>,
|
||||
}
|
||||
@@ -275,10 +357,14 @@ struct SparsePattern {
|
||||
speed: PatternSpeed,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
name: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
description: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "is_default_quantization")]
|
||||
quantization: LaunchQuantization,
|
||||
#[serde(default, skip_serializing_if = "is_default_sync_mode")]
|
||||
sync_mode: SyncMode,
|
||||
#[serde(default, skip_serializing_if = "is_default_follow_up")]
|
||||
follow_up: FollowUp,
|
||||
}
|
||||
|
||||
fn is_default_quantization(q: &LaunchQuantization) -> bool {
|
||||
@@ -298,9 +384,13 @@ struct LegacyPattern {
|
||||
#[serde(default)]
|
||||
name: Option<String>,
|
||||
#[serde(default)]
|
||||
description: Option<String>,
|
||||
#[serde(default)]
|
||||
quantization: LaunchQuantization,
|
||||
#[serde(default)]
|
||||
sync_mode: SyncMode,
|
||||
#[serde(default)]
|
||||
follow_up: FollowUp,
|
||||
}
|
||||
|
||||
impl Serialize for Pattern {
|
||||
@@ -324,8 +414,10 @@ impl Serialize for Pattern {
|
||||
length: self.length,
|
||||
speed: self.speed,
|
||||
name: self.name.clone(),
|
||||
description: self.description.clone(),
|
||||
quantization: self.quantization,
|
||||
sync_mode: self.sync_mode,
|
||||
follow_up: self.follow_up,
|
||||
};
|
||||
sparse.serialize(serializer)
|
||||
}
|
||||
@@ -348,7 +440,6 @@ impl<'de> Deserialize<'de> for Pattern {
|
||||
steps[ss.i] = Step {
|
||||
active: ss.active,
|
||||
script: ss.script,
|
||||
command: None,
|
||||
source: ss.source,
|
||||
name: ss.name,
|
||||
};
|
||||
@@ -359,8 +450,10 @@ impl<'de> Deserialize<'de> for Pattern {
|
||||
length: sparse.length,
|
||||
speed: sparse.speed,
|
||||
name: sparse.name,
|
||||
description: sparse.description,
|
||||
quantization: sparse.quantization,
|
||||
sync_mode: sparse.sync_mode,
|
||||
follow_up: sparse.follow_up,
|
||||
})
|
||||
}
|
||||
PatternFormat::Legacy(legacy) => Ok(Pattern {
|
||||
@@ -368,8 +461,10 @@ impl<'de> Deserialize<'de> for Pattern {
|
||||
length: legacy.length,
|
||||
speed: legacy.speed,
|
||||
name: legacy.name,
|
||||
description: legacy.description,
|
||||
quantization: legacy.quantization,
|
||||
sync_mode: legacy.sync_mode,
|
||||
follow_up: legacy.follow_up,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -382,21 +477,26 @@ impl Default for Pattern {
|
||||
length: DEFAULT_LENGTH,
|
||||
speed: PatternSpeed::default(),
|
||||
name: None,
|
||||
description: None,
|
||||
quantization: LaunchQuantization::default(),
|
||||
sync_mode: SyncMode::default(),
|
||||
follow_up: FollowUp::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Pattern {
|
||||
/// Borrow a step by index.
|
||||
pub fn step(&self, index: usize) -> Option<&Step> {
|
||||
self.steps.get(index)
|
||||
}
|
||||
|
||||
/// Mutably borrow a step by index.
|
||||
pub fn step_mut(&mut self, index: usize) -> Option<&mut Step> {
|
||||
self.steps.get_mut(index)
|
||||
}
|
||||
|
||||
/// Set the active length, clamped to `[1, MAX_STEPS]`.
|
||||
pub fn set_length(&mut self, length: usize) {
|
||||
let length = length.clamp(1, MAX_STEPS);
|
||||
while self.steps.len() < length {
|
||||
@@ -405,12 +505,13 @@ impl Pattern {
|
||||
self.length = length;
|
||||
}
|
||||
|
||||
/// Follow the source chain from `index` to find the originating step.
|
||||
pub fn resolve_source(&self, index: usize) -> usize {
|
||||
let mut current = index;
|
||||
for _ in 0..self.steps.len() {
|
||||
if let Some(step) = self.steps.get(current) {
|
||||
if let Some(source) = step.source {
|
||||
current = source;
|
||||
current = source as usize;
|
||||
} else {
|
||||
return current;
|
||||
}
|
||||
@@ -421,17 +522,39 @@ impl Pattern {
|
||||
index
|
||||
}
|
||||
|
||||
/// Return the script at the resolved source of `index`.
|
||||
pub fn resolve_script(&self, index: usize) -> Option<&str> {
|
||||
let source_idx = self.resolve_source(index);
|
||||
self.steps.get(source_idx).map(|s| s.script.as_str())
|
||||
}
|
||||
|
||||
/// Count active-length steps that have a script or a source reference.
|
||||
pub fn content_step_count(&self) -> usize {
|
||||
self.steps[..self.length]
|
||||
.iter()
|
||||
.filter(|s| s.has_content() || s.source.is_some())
|
||||
.count()
|
||||
}
|
||||
}
|
||||
|
||||
/// Collection of patterns forming a bank.
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Bank {
|
||||
pub patterns: Vec<Pattern>,
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub prelude: String,
|
||||
}
|
||||
|
||||
impl Bank {
|
||||
/// Count patterns that contain at least one non-empty step.
|
||||
pub fn content_pattern_count(&self) -> usize {
|
||||
self.patterns
|
||||
.iter()
|
||||
.filter(|p| p.content_step_count() > 0)
|
||||
.count()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Bank {
|
||||
@@ -439,10 +562,12 @@ impl Default for Bank {
|
||||
Self {
|
||||
patterns: (0..MAX_PATTERNS).map(|_| Pattern::default()).collect(),
|
||||
name: None,
|
||||
prelude: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Top-level project: banks, tempo, sample paths, and prelude script.
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Project {
|
||||
pub banks: Vec<Bank>,
|
||||
@@ -452,12 +577,24 @@ pub struct Project {
|
||||
pub tempo: f64,
|
||||
#[serde(default)]
|
||||
pub playing_patterns: Vec<(usize, usize)>,
|
||||
#[serde(default)]
|
||||
pub prelude: String,
|
||||
#[serde(default)]
|
||||
pub script: String,
|
||||
#[serde(default)]
|
||||
pub script_speed: PatternSpeed,
|
||||
#[serde(default = "default_script_length")]
|
||||
pub script_length: usize,
|
||||
}
|
||||
|
||||
fn default_tempo() -> f64 {
|
||||
120.0
|
||||
}
|
||||
|
||||
fn default_script_length() -> usize {
|
||||
16
|
||||
}
|
||||
|
||||
impl Default for Project {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -465,19 +602,26 @@ impl Default for Project {
|
||||
sample_paths: Vec::new(),
|
||||
tempo: default_tempo(),
|
||||
playing_patterns: Vec::new(),
|
||||
prelude: String::new(),
|
||||
script: String::new(),
|
||||
script_speed: PatternSpeed::default(),
|
||||
script_length: default_script_length(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Project {
|
||||
/// Borrow a pattern by bank and pattern index.
|
||||
pub fn pattern_at(&self, bank: usize, pattern: usize) -> &Pattern {
|
||||
&self.banks[bank].patterns[pattern]
|
||||
}
|
||||
|
||||
/// Mutably borrow a pattern by bank and pattern index.
|
||||
pub fn pattern_at_mut(&mut self, bank: usize, pattern: usize) -> &mut Pattern {
|
||||
&mut self.banks[bank].patterns[pattern]
|
||||
}
|
||||
|
||||
/// Pad banks, patterns, and steps to their maximum sizes after deserialization.
|
||||
pub fn normalize(&mut self) {
|
||||
self.banks.resize_with(MAX_BANKS, Bank::default);
|
||||
for bank in &mut self.banks {
|
||||
|
||||
214
crates/project/src/share.rs
Normal file
214
crates/project/src/share.rs
Normal file
@@ -0,0 +1,214 @@
|
||||
//! Pattern and project sharing via compact text strings.
|
||||
//!
|
||||
//! Export: data → MessagePack → Brotli → base64 URL-safe → prefix
|
||||
//! Import: strip prefix → base64 decode → Brotli decompress → MessagePack → data
|
||||
|
||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
use base64::Engine;
|
||||
|
||||
use crate::{Bank, Pattern};
|
||||
|
||||
const PATTERN_PREFIX: &str = "cgr:";
|
||||
const BANK_PREFIX: &str = "cgrb:";
|
||||
|
||||
/// Error during pattern or bank import/export.
|
||||
#[derive(Debug)]
|
||||
pub enum ShareError {
|
||||
InvalidPrefix,
|
||||
Base64(base64::DecodeError),
|
||||
Decompress(std::io::Error),
|
||||
Deserialize(rmp_serde::decode::Error),
|
||||
Serialize(rmp_serde::encode::Error),
|
||||
Compress(std::io::Error),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ShareError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::InvalidPrefix => write!(f, "missing cgr:/cgrb: prefix"),
|
||||
Self::Base64(e) => write!(f, "base64: {e}"),
|
||||
Self::Decompress(e) => write!(f, "decompress: {e}"),
|
||||
Self::Deserialize(e) => write!(f, "deserialize: {e}"),
|
||||
Self::Serialize(e) => write!(f, "serialize: {e}"),
|
||||
Self::Compress(e) => write!(f, "compress: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn compress(data: &[u8]) -> Result<Vec<u8>, ShareError> {
|
||||
let mut output = Vec::new();
|
||||
let params = brotli::enc::BrotliEncoderParams {
|
||||
quality: 11,
|
||||
lgwin: 22,
|
||||
lgblock: 0,
|
||||
..Default::default()
|
||||
};
|
||||
brotli::BrotliCompress(&mut &data[..], &mut output, ¶ms).map_err(ShareError::Compress)?;
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn decompress(data: &[u8]) -> Result<Vec<u8>, ShareError> {
|
||||
let mut output = Vec::new();
|
||||
brotli::BrotliDecompress(&mut &data[..], &mut output).map_err(ShareError::Decompress)?;
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn encode<T: serde::Serialize>(value: &T, prefix: &str) -> Result<String, ShareError> {
|
||||
let packed = rmp_serde::to_vec_named(value).map_err(ShareError::Serialize)?;
|
||||
let compressed = compress(&packed)?;
|
||||
let encoded = URL_SAFE_NO_PAD.encode(&compressed);
|
||||
Ok(format!("{prefix}{encoded}"))
|
||||
}
|
||||
|
||||
fn decode<T: serde::de::DeserializeOwned>(text: &str, prefix: &str) -> Result<T, ShareError> {
|
||||
let text = text.trim();
|
||||
let payload = text.strip_prefix(prefix).ok_or(ShareError::InvalidPrefix)?;
|
||||
let compressed = URL_SAFE_NO_PAD.decode(payload).map_err(ShareError::Base64)?;
|
||||
let packed = decompress(&compressed)?;
|
||||
rmp_serde::from_slice(&packed).map_err(ShareError::Deserialize)
|
||||
}
|
||||
|
||||
/// Encode a pattern as a shareable `cgr:` string.
|
||||
pub fn export(pattern: &Pattern) -> Result<String, ShareError> {
|
||||
encode(pattern, PATTERN_PREFIX)
|
||||
}
|
||||
|
||||
/// Decode a `cgr:` string back into a pattern.
|
||||
pub fn import(text: &str) -> Result<Pattern, ShareError> {
|
||||
decode(text, PATTERN_PREFIX)
|
||||
}
|
||||
|
||||
/// Encode a bank as a shareable `cgrb:` string.
|
||||
pub fn export_bank(bank: &Bank) -> Result<String, ShareError> {
|
||||
encode(bank, BANK_PREFIX)
|
||||
}
|
||||
|
||||
/// Decode a `cgrb:` string back into a bank.
|
||||
pub fn import_bank(text: &str) -> Result<Bank, ShareError> {
|
||||
decode(text, BANK_PREFIX)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Step;
|
||||
|
||||
#[test]
|
||||
fn roundtrip_empty() {
|
||||
let pattern = Pattern::default();
|
||||
let encoded = export(&pattern).expect("export pattern");
|
||||
assert!(encoded.starts_with("cgr:"));
|
||||
let decoded = import(&encoded).expect("import pattern");
|
||||
assert_eq!(decoded.length, pattern.length);
|
||||
assert_eq!(decoded.steps.len(), pattern.steps.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_with_steps() {
|
||||
let mut pattern = Pattern::default();
|
||||
pattern.steps[0] = Step {
|
||||
active: true,
|
||||
script: "kick 60 note".to_string(),
|
||||
source: None,
|
||||
name: Some("kick".to_string()),
|
||||
};
|
||||
pattern.steps[1] = Step {
|
||||
active: false,
|
||||
script: "snare".to_string(),
|
||||
source: None,
|
||||
name: None,
|
||||
};
|
||||
pattern.steps[3] = Step {
|
||||
active: true,
|
||||
script: String::new(),
|
||||
source: Some(0),
|
||||
name: None,
|
||||
};
|
||||
pattern.length = 8;
|
||||
pattern.name = Some("Test".to_string());
|
||||
|
||||
let encoded = export(&pattern).expect("export pattern");
|
||||
let decoded = import(&encoded).expect("import pattern");
|
||||
|
||||
assert_eq!(decoded.length, 8);
|
||||
assert_eq!(decoded.name.as_deref(), Some("Test"));
|
||||
assert_eq!(decoded.steps[0].script, "kick 60 note");
|
||||
assert_eq!(decoded.steps[0].name.as_deref(), Some("kick"));
|
||||
assert!(!decoded.steps[1].active);
|
||||
assert_eq!(decoded.steps[1].script, "snare");
|
||||
assert_eq!(decoded.steps[3].source, Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_prefix() {
|
||||
assert!(matches!(import("xxx:abc"), Err(ShareError::InvalidPrefix)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_base64() {
|
||||
assert!(matches!(import("cgr:!!!"), Err(ShareError::Base64(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whitespace_trimming() {
|
||||
let pattern = Pattern::default();
|
||||
let encoded = export(&pattern).expect("export pattern");
|
||||
let padded = format!(" {encoded} \n");
|
||||
let decoded = import(&padded).expect("import padded pattern");
|
||||
assert_eq!(decoded.length, pattern.length);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn msgpack_brotli_smaller_than_json_deflate() {
|
||||
let mut pattern = Pattern::default();
|
||||
for i in 0..16 {
|
||||
pattern.steps[i] = Step {
|
||||
active: true,
|
||||
script: format!("kick {i} note 0.5 dur"),
|
||||
source: None,
|
||||
name: Some(format!("step_{i}")),
|
||||
};
|
||||
}
|
||||
pattern.length = 16;
|
||||
|
||||
// Current (msgpack+brotli)
|
||||
let new_encoded = export(&pattern).expect("export pattern");
|
||||
|
||||
// Old pipeline (json+deflate) for comparison
|
||||
use std::io::Write;
|
||||
let json = serde_json::to_vec(&pattern).expect("serialize json");
|
||||
let mut encoder =
|
||||
flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::best());
|
||||
encoder.write_all(&json).expect("write to encoder");
|
||||
let old_compressed = encoder.finish().expect("finish encoder");
|
||||
let old_encoded = format!("cgr:{}", URL_SAFE_NO_PAD.encode(&old_compressed));
|
||||
|
||||
assert!(
|
||||
new_encoded.len() < old_encoded.len(),
|
||||
"msgpack+brotli ({}) should be smaller than json+deflate ({})",
|
||||
new_encoded.len(),
|
||||
old_encoded.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_bank() {
|
||||
let mut bank = Bank::default();
|
||||
bank.patterns[0].steps[0] = Step {
|
||||
active: true,
|
||||
script: "kick 60 note".to_string(),
|
||||
source: None,
|
||||
name: Some("kick".to_string()),
|
||||
};
|
||||
bank.patterns[0].length = 8;
|
||||
bank.name = Some("Drums".to_string());
|
||||
|
||||
let encoded = export_bank(&bank).expect("export bank");
|
||||
assert!(encoded.starts_with("cgrb:"));
|
||||
let decoded = import_bank(&encoded).expect("import bank");
|
||||
|
||||
assert_eq!(decoded.name.as_deref(), Some("Drums"));
|
||||
assert_eq!(decoded.patterns[0].length, 8);
|
||||
assert_eq!(decoded.patterns[0].steps[0].script, "kick 60 note");
|
||||
}
|
||||
}
|
||||
@@ -11,4 +11,4 @@ description = "TUI components for cagire sequencer"
|
||||
rand = "0.8"
|
||||
ratatui = "0.30"
|
||||
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"] }
|
||||
|
||||
25
crates/ratatui/README.md
Normal file
25
crates/ratatui/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# cagire-ratatui
|
||||
|
||||
TUI widget library and theme system for Cagire.
|
||||
|
||||
## Widgets
|
||||
|
||||
`category_list`, `confirm`, `editor`, `file_browser`, `hint_bar`, `lissajous`, `list_select`, `modal`, `nav_minimap`, `props_form`, `sample_browser`, `scope`, `scroll_indicators`, `search_bar`, `section_header`, `sparkles`, `spectrum`, `text_input`, `vu_meter`, `waveform`
|
||||
|
||||
## Theme System
|
||||
|
||||
The `theme/` module provides a palette-based theming system using Oklab color space.
|
||||
|
||||
| Module | Description |
|
||||
|--------|-------------|
|
||||
| `mod` | `THEMES` array, `CURRENT_THEME` thread-local, `get()`/`set()` |
|
||||
| `palette` | `Palette` (14 fields), color manipulation helpers (`shift`, `mix`, `tint_bg`, ...) |
|
||||
| `build` | Derives ~190 `ThemeColors` fields from a `Palette` |
|
||||
| `transform` | HSV-based hue rotation for generated palettes |
|
||||
|
||||
25 built-in themes.
|
||||
|
||||
## Key Types
|
||||
|
||||
- **`Palette`** — 14-field color definition, input to theme generation
|
||||
- **`ThemeColors`** — ~190 derived semantic colors used throughout the UI
|
||||
189
crates/ratatui/src/category_list.rs
Normal file
189
crates/ratatui/src/category_list.rs
Normal file
@@ -0,0 +1,189 @@
|
||||
//! Collapsible categorized list widget with section headers.
|
||||
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::widgets::{Block, Borders, List, ListItem};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::theme;
|
||||
|
||||
/// Entry in a category list: either a section header or a leaf item.
|
||||
pub struct CategoryItem<'a> {
|
||||
pub label: &'a str,
|
||||
pub is_section: bool,
|
||||
pub collapsed: bool,
|
||||
}
|
||||
|
||||
/// What is currently selected: a leaf item or a section header.
|
||||
pub enum Selection {
|
||||
Item(usize),
|
||||
Section(usize),
|
||||
}
|
||||
|
||||
/// Scrollable list with collapsible section headers.
|
||||
pub struct CategoryList<'a> {
|
||||
items: &'a [CategoryItem<'a>],
|
||||
selection: Selection,
|
||||
focused: bool,
|
||||
title: &'a str,
|
||||
section_color: Color,
|
||||
focused_color: Color,
|
||||
selected_color: Color,
|
||||
normal_color: Color,
|
||||
dimmed_color: Option<Color>,
|
||||
}
|
||||
|
||||
impl<'a> CategoryList<'a> {
|
||||
pub fn new(items: &'a [CategoryItem<'a>], selection: Selection) -> Self {
|
||||
let theme = theme::get();
|
||||
Self {
|
||||
items,
|
||||
selection,
|
||||
focused: false,
|
||||
title: "",
|
||||
section_color: theme.ui.text_dim,
|
||||
focused_color: theme.dict.category_focused,
|
||||
selected_color: theme.dict.category_selected,
|
||||
normal_color: theme.dict.category_normal,
|
||||
dimmed_color: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focused(mut self, focused: bool) -> Self {
|
||||
self.focused = focused;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn title(mut self, title: &'a str) -> Self {
|
||||
self.title = title;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn selected_color(mut self, color: Color) -> Self {
|
||||
self.selected_color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn normal_color(mut self, color: Color) -> Self {
|
||||
self.normal_color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn dimmed(mut self, color: Color) -> Self {
|
||||
self.dimmed_color = Some(color);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the visible items list, filtering out children of collapsed sections.
|
||||
/// Returns (item, section_index_if_section, item_index_if_item).
|
||||
fn visible_items(&self) -> Vec<(&CategoryItem<'a>, Option<usize>, Option<usize>)> {
|
||||
let mut result = Vec::new();
|
||||
let mut skipping = false;
|
||||
let mut section_idx = 0usize;
|
||||
let mut item_idx = 0usize;
|
||||
for item in self.items.iter() {
|
||||
if item.is_section {
|
||||
skipping = item.collapsed;
|
||||
result.push((item, Some(section_idx), None));
|
||||
section_idx += 1;
|
||||
} else if !skipping {
|
||||
result.push((item, None, Some(item_idx)));
|
||||
item_idx += 1;
|
||||
} else {
|
||||
item_idx += 1;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn render(self, frame: &mut Frame, area: Rect) {
|
||||
let theme = theme::get();
|
||||
let visible = self.visible_items();
|
||||
|
||||
let visible_height = area.height.saturating_sub(2) as usize;
|
||||
let total_items = visible.len();
|
||||
|
||||
let selected_visual_idx = match &self.selection {
|
||||
Selection::Item(sel) => visible
|
||||
.iter()
|
||||
.position(|(_, _, item_idx)| *item_idx == Some(*sel))
|
||||
.unwrap_or(0),
|
||||
Selection::Section(sel) => visible
|
||||
.iter()
|
||||
.position(|(_, sec_idx, _)| *sec_idx == Some(*sel))
|
||||
.unwrap_or(0),
|
||||
};
|
||||
|
||||
let scroll = if selected_visual_idx < visible_height / 2 {
|
||||
0
|
||||
} else if selected_visual_idx > total_items.saturating_sub(visible_height / 2) {
|
||||
total_items.saturating_sub(visible_height)
|
||||
} else {
|
||||
selected_visual_idx.saturating_sub(visible_height / 2)
|
||||
};
|
||||
|
||||
let is_dimmed = self.dimmed_color.is_some();
|
||||
|
||||
let items: Vec<ListItem> = visible
|
||||
.iter()
|
||||
.skip(scroll)
|
||||
.take(visible_height)
|
||||
.enumerate()
|
||||
.map(|(vis_offset, (item, sec_idx, _itm_idx))| {
|
||||
let visual_pos = scroll + vis_offset;
|
||||
if item.is_section {
|
||||
let is_selected =
|
||||
matches!(&self.selection, Selection::Section(s) if Some(*s) == *sec_idx);
|
||||
let arrow = if item.collapsed { "▸" } else { "▾" };
|
||||
let style = if is_selected && self.focused {
|
||||
Style::new()
|
||||
.fg(self.focused_color)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else if is_selected {
|
||||
Style::new()
|
||||
.fg(self.selected_color)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::new().fg(self.section_color)
|
||||
};
|
||||
let prefix = if is_selected && !is_dimmed { "> " } else { "" };
|
||||
ListItem::new(format!("{prefix}{arrow} {}", item.label)).style(style)
|
||||
} else {
|
||||
let is_selected = visual_pos == selected_visual_idx
|
||||
&& matches!(&self.selection, Selection::Item(_));
|
||||
let style = if let Some(dim_color) = self.dimmed_color {
|
||||
Style::new().fg(dim_color)
|
||||
} else if is_selected && self.focused {
|
||||
Style::new()
|
||||
.fg(self.focused_color)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else if is_selected {
|
||||
Style::new()
|
||||
.fg(self.selected_color)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::new().fg(self.normal_color)
|
||||
};
|
||||
let prefix = if is_selected && !is_dimmed {
|
||||
"> "
|
||||
} else {
|
||||
" "
|
||||
};
|
||||
ListItem::new(format!("{prefix}{}", item.label)).style(style)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let border_color = if self.focused {
|
||||
theme.dict.border_focused
|
||||
} else {
|
||||
theme.dict.border_normal
|
||||
};
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::new().fg(border_color))
|
||||
.title(self.title);
|
||||
let list = List::new(items).block(block);
|
||||
frame.render_widget(list, area);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Yes/No confirmation dialog widget.
|
||||
|
||||
use crate::theme;
|
||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||
use ratatui::style::Style;
|
||||
@@ -7,6 +9,7 @@ use ratatui::Frame;
|
||||
|
||||
use super::ModalFrame;
|
||||
|
||||
/// Modal dialog with Yes/No buttons.
|
||||
pub struct ConfirmModal<'a> {
|
||||
title: &'a str,
|
||||
message: &'a str,
|
||||
@@ -22,7 +25,7 @@ impl<'a> ConfirmModal<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_centered(self, frame: &mut Frame, term: Rect) {
|
||||
pub fn render_centered(self, frame: &mut Frame, term: Rect) -> Rect {
|
||||
let t = theme::get();
|
||||
let inner = ModalFrame::new(self.title)
|
||||
.width(30)
|
||||
@@ -58,5 +61,7 @@ impl<'a> ConfirmModal<'a> {
|
||||
Paragraph::new(buttons).alignment(Alignment::Center),
|
||||
rows[1],
|
||||
);
|
||||
|
||||
inner
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
//! Script editor widget with completion, search, and sample finder popups.
|
||||
|
||||
use std::cell::Cell;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::theme;
|
||||
use ratatui::{
|
||||
layout::Rect,
|
||||
@@ -8,8 +13,11 @@ use ratatui::{
|
||||
};
|
||||
use tui_textarea::TextArea;
|
||||
|
||||
pub type Highlighter<'a> = &'a dyn Fn(usize, &str) -> Vec<(Style, String)>;
|
||||
/// Callback that syntax-highlights a single line, returning styled spans (bool = annotation).
|
||||
pub type Highlighter<'a> = &'a dyn Fn(usize, &str) -> Vec<(Style, String, bool)>;
|
||||
|
||||
/// Metadata for a single autocomplete entry.
|
||||
#[derive(Clone)]
|
||||
pub struct CompletionCandidate {
|
||||
pub name: String,
|
||||
pub signature: String,
|
||||
@@ -18,7 +26,7 @@ pub struct CompletionCandidate {
|
||||
}
|
||||
|
||||
struct CompletionState {
|
||||
candidates: Vec<CompletionCandidate>,
|
||||
candidates: Arc<[CompletionCandidate]>,
|
||||
matches: Vec<usize>,
|
||||
cursor: usize,
|
||||
prefix: String,
|
||||
@@ -30,7 +38,7 @@ struct CompletionState {
|
||||
impl CompletionState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
candidates: Vec::new(),
|
||||
candidates: Arc::from([]),
|
||||
matches: Vec::new(),
|
||||
cursor: 0,
|
||||
prefix: String::new(),
|
||||
@@ -41,6 +49,26 @@ impl CompletionState {
|
||||
}
|
||||
}
|
||||
|
||||
struct SampleFinderState {
|
||||
query: String,
|
||||
folders: Vec<String>,
|
||||
matches: Vec<usize>,
|
||||
cursor: usize,
|
||||
active: bool,
|
||||
}
|
||||
|
||||
impl SampleFinderState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
query: String::new(),
|
||||
folders: Vec::new(),
|
||||
matches: Vec::new(),
|
||||
cursor: 0,
|
||||
active: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SearchState {
|
||||
query: String,
|
||||
active: bool,
|
||||
@@ -55,10 +83,13 @@ impl SearchState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Multi-line text editor backed by tui_textarea.
|
||||
pub struct Editor {
|
||||
text: TextArea<'static>,
|
||||
completion: CompletionState,
|
||||
sample_finder: SampleFinderState,
|
||||
search: SearchState,
|
||||
scroll_offset: Cell<u16>,
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
@@ -74,6 +105,14 @@ impl Editor {
|
||||
self.text.is_selecting()
|
||||
}
|
||||
|
||||
pub fn move_cursor_to(&mut self, row: u16, col: u16) {
|
||||
self.text.move_cursor(tui_textarea::CursorMove::Jump(row, col));
|
||||
}
|
||||
|
||||
pub fn scroll_offset(&self) -> u16 {
|
||||
self.scroll_offset.get()
|
||||
}
|
||||
|
||||
pub fn copy(&mut self) {
|
||||
self.text.copy();
|
||||
}
|
||||
@@ -86,6 +125,14 @@ impl Editor {
|
||||
self.text.paste()
|
||||
}
|
||||
|
||||
pub fn yank_text(&self) -> String {
|
||||
self.text.yank_text()
|
||||
}
|
||||
|
||||
pub fn set_yank_text(&mut self, text: impl Into<String>) {
|
||||
self.text.set_yank_text(text);
|
||||
}
|
||||
|
||||
pub fn select_all(&mut self) {
|
||||
self.text.select_all();
|
||||
}
|
||||
@@ -106,18 +153,26 @@ impl Editor {
|
||||
Self {
|
||||
text: TextArea::default(),
|
||||
completion: CompletionState::new(),
|
||||
sample_finder: SampleFinderState::new(),
|
||||
search: SearchState::new(),
|
||||
scroll_offset: Cell::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_content(&mut self, lines: Vec<String>) {
|
||||
let yank = self.text.yank_text();
|
||||
self.text = TextArea::new(lines);
|
||||
if !yank.is_empty() {
|
||||
self.text.set_yank_text(yank);
|
||||
}
|
||||
self.completion.active = false;
|
||||
self.sample_finder.active = false;
|
||||
self.search.query.clear();
|
||||
self.search.active = false;
|
||||
self.scroll_offset.set(0);
|
||||
}
|
||||
|
||||
pub fn set_candidates(&mut self, candidates: Vec<CompletionCandidate>) {
|
||||
pub fn set_candidates(&mut self, candidates: Arc<[CompletionCandidate]>) {
|
||||
self.completion.candidates = candidates;
|
||||
}
|
||||
|
||||
@@ -145,6 +200,18 @@ impl Editor {
|
||||
self.completion.active = false;
|
||||
}
|
||||
|
||||
pub fn completion_next(&mut self) {
|
||||
if self.completion.cursor + 1 < self.completion.matches.len() {
|
||||
self.completion.cursor += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn completion_prev(&mut self) {
|
||||
if self.completion.cursor > 0 {
|
||||
self.completion.cursor -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_completion_enabled(&mut self, enabled: bool) {
|
||||
self.completion.enabled = enabled;
|
||||
if !enabled {
|
||||
@@ -208,24 +275,85 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_sample_folders(&mut self, folders: Vec<String>) {
|
||||
self.sample_finder.folders = folders;
|
||||
}
|
||||
|
||||
pub fn activate_sample_finder(&mut self) {
|
||||
self.completion.active = false;
|
||||
self.sample_finder.query.clear();
|
||||
self.sample_finder.cursor = 0;
|
||||
self.sample_finder.matches = (0..self.sample_finder.folders.len()).collect();
|
||||
self.sample_finder.active = true;
|
||||
}
|
||||
|
||||
pub fn dismiss_sample_finder(&mut self) {
|
||||
self.sample_finder.active = false;
|
||||
}
|
||||
|
||||
pub fn sample_finder_active(&self) -> bool {
|
||||
self.sample_finder.active
|
||||
}
|
||||
|
||||
pub fn sample_finder_input(&mut self, c: char) {
|
||||
self.sample_finder.query.push(c);
|
||||
self.update_sample_finder_matches();
|
||||
}
|
||||
|
||||
pub fn sample_finder_backspace(&mut self) {
|
||||
self.sample_finder.query.pop();
|
||||
self.update_sample_finder_matches();
|
||||
}
|
||||
|
||||
pub fn sample_finder_next(&mut self) {
|
||||
if self.sample_finder.cursor + 1 < self.sample_finder.matches.len() {
|
||||
self.sample_finder.cursor += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sample_finder_prev(&mut self) {
|
||||
if self.sample_finder.cursor > 0 {
|
||||
self.sample_finder.cursor -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn accept_sample_finder(&mut self) {
|
||||
if self.sample_finder.matches.is_empty() {
|
||||
self.sample_finder.active = false;
|
||||
return;
|
||||
}
|
||||
let idx = self.sample_finder.matches[self.sample_finder.cursor];
|
||||
let name = self.sample_finder.folders[idx].clone();
|
||||
self.text.insert_str(&name);
|
||||
self.sample_finder.active = false;
|
||||
}
|
||||
|
||||
fn update_sample_finder_matches(&mut self) {
|
||||
if self.sample_finder.query.is_empty() {
|
||||
self.sample_finder.matches = (0..self.sample_finder.folders.len()).collect();
|
||||
} else {
|
||||
let mut scored: Vec<(usize, usize)> = self
|
||||
.sample_finder
|
||||
.folders
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, name)| fuzzy_match(&self.sample_finder.query, name).map(|s| (s, i)))
|
||||
.collect();
|
||||
scored.sort_by_key(|(score, _)| *score);
|
||||
self.sample_finder.matches = scored.into_iter().map(|(_, i)| i).collect();
|
||||
}
|
||||
self.sample_finder.cursor = self
|
||||
.sample_finder
|
||||
.cursor
|
||||
.min(self.sample_finder.matches.len().saturating_sub(1));
|
||||
}
|
||||
|
||||
pub fn input(&mut self, input: impl Into<tui_textarea::Input>) {
|
||||
let input: tui_textarea::Input = input.into();
|
||||
let has_modifier = input.ctrl || input.alt;
|
||||
|
||||
if self.completion.active && !has_modifier {
|
||||
match &input {
|
||||
tui_textarea::Input { key: tui_textarea::Key::Up, .. } => {
|
||||
if self.completion.cursor > 0 {
|
||||
self.completion.cursor -= 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
tui_textarea::Input { key: tui_textarea::Key::Down, .. } => {
|
||||
if self.completion.cursor + 1 < self.completion.matches.len() {
|
||||
self.completion.cursor += 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
tui_textarea::Input { key: tui_textarea::Key::Tab, .. } => {
|
||||
self.accept_completion();
|
||||
return;
|
||||
@@ -255,7 +383,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
fn update_completion(&mut self) {
|
||||
if !self.completion.enabled || self.completion.candidates.is_empty() {
|
||||
if !self.completion.enabled || self.completion.candidates.is_empty() || self.sample_finder.active {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -350,23 +478,27 @@ impl Editor {
|
||||
let mut spans: Vec<Span> = Vec::new();
|
||||
let mut col = 0;
|
||||
|
||||
for (base_style, text) in tokens {
|
||||
for (base_style, text, is_annotation) in tokens {
|
||||
for ch in text.chars() {
|
||||
let style = if is_annotation {
|
||||
base_style
|
||||
} else {
|
||||
let is_cursor = row == cursor_row && col == cursor_col;
|
||||
let is_selected = is_in_selection(row, col, selection);
|
||||
|
||||
let style = if is_cursor {
|
||||
if is_cursor {
|
||||
cursor_style
|
||||
} else if is_selected {
|
||||
base_style.bg(selection_style.bg.unwrap())
|
||||
base_style.bg(selection_style.bg.expect("selection style has bg"))
|
||||
} else {
|
||||
base_style
|
||||
}
|
||||
};
|
||||
|
||||
spans.push(Span::styled(ch.to_string(), style));
|
||||
if !is_annotation {
|
||||
col += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if row == cursor_row && cursor_col >= col {
|
||||
spans.push(Span::styled(" ", cursor_style));
|
||||
@@ -376,10 +508,23 @@ impl Editor {
|
||||
})
|
||||
.collect();
|
||||
|
||||
frame.render_widget(Paragraph::new(lines), area);
|
||||
let viewport_height = area.height as usize;
|
||||
let offset = self.scroll_offset.get() as usize;
|
||||
let offset = if cursor_row < offset {
|
||||
cursor_row
|
||||
} else if cursor_row >= offset + viewport_height {
|
||||
cursor_row - viewport_height + 1
|
||||
} else {
|
||||
offset
|
||||
};
|
||||
self.scroll_offset.set(offset as u16);
|
||||
|
||||
if self.completion.active && !self.completion.matches.is_empty() {
|
||||
self.render_completion(frame, area, cursor_row);
|
||||
frame.render_widget(Paragraph::new(lines).scroll((offset as u16, 0)), area);
|
||||
|
||||
if self.sample_finder.active && !self.sample_finder.matches.is_empty() {
|
||||
self.render_sample_finder(frame, area, cursor_row - offset);
|
||||
} else if self.completion.active && !self.completion.matches.is_empty() {
|
||||
self.render_completion(frame, area, cursor_row - offset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,6 +639,99 @@ impl Editor {
|
||||
|
||||
frame.render_widget(Paragraph::new(doc_lines), doc_area);
|
||||
}
|
||||
|
||||
fn render_sample_finder(&self, frame: &mut Frame, editor_area: Rect, cursor_row: usize) {
|
||||
let t = theme::get();
|
||||
let max_visible: usize = 8;
|
||||
let width: u16 = 24;
|
||||
|
||||
let visible_count = self.sample_finder.matches.len().min(max_visible);
|
||||
let total_height = visible_count as u16 + 1; // +1 for query line
|
||||
|
||||
let (_, cursor_col) = self.text.cursor();
|
||||
let popup_x = (editor_area.x + cursor_col as u16)
|
||||
.min(editor_area.x + editor_area.width.saturating_sub(width));
|
||||
|
||||
let below_y = editor_area.y + cursor_row as u16 + 1;
|
||||
let popup_y = if below_y + total_height > editor_area.y + editor_area.height {
|
||||
(editor_area.y + cursor_row as u16).saturating_sub(total_height)
|
||||
} else {
|
||||
below_y
|
||||
};
|
||||
|
||||
let area = Rect::new(popup_x, popup_y, width, total_height);
|
||||
frame.render_widget(Clear, area);
|
||||
|
||||
let bg_style = Style::default().bg(t.editor_widget.completion_bg);
|
||||
let highlight_style = Style::default()
|
||||
.fg(t.editor_widget.completion_selected)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
let normal_style = Style::default().fg(t.editor_widget.completion_fg);
|
||||
|
||||
let w = width as usize;
|
||||
let mut lines: Vec<Line> = Vec::new();
|
||||
|
||||
let query_display = format!("/{}", self.sample_finder.query);
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!("{query_display:<w$}"),
|
||||
highlight_style.bg(t.editor_widget.completion_bg),
|
||||
)));
|
||||
|
||||
let scroll_offset = if self.sample_finder.cursor >= max_visible {
|
||||
self.sample_finder.cursor - max_visible + 1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
for i in scroll_offset..scroll_offset + visible_count {
|
||||
let idx = self.sample_finder.matches[i];
|
||||
let name = &self.sample_finder.folders[idx];
|
||||
let style = if i == self.sample_finder.cursor {
|
||||
highlight_style
|
||||
} else {
|
||||
normal_style
|
||||
};
|
||||
let prefix = if i == self.sample_finder.cursor { "> " } else { " " };
|
||||
let display = format!("{prefix}{name:<width$}", width = w - 2);
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!("{display:<w$}"),
|
||||
style.bg(t.editor_widget.completion_bg),
|
||||
)));
|
||||
}
|
||||
|
||||
// Fill rest with bg
|
||||
for _ in lines.len() as u16..total_height {
|
||||
lines.push(Line::from(Span::styled(" ".repeat(w), bg_style)));
|
||||
}
|
||||
|
||||
frame.render_widget(Paragraph::new(lines), area);
|
||||
}
|
||||
}
|
||||
|
||||
/// Score a fuzzy match of `query` against `target`. Lower is better; `None` if no match.
|
||||
pub fn fuzzy_match(query: &str, target: &str) -> Option<usize> {
|
||||
let target_lower: Vec<char> = target.to_lowercase().chars().collect();
|
||||
let query_lower: Vec<char> = query.to_lowercase().chars().collect();
|
||||
let mut ti = 0;
|
||||
let mut score = 0;
|
||||
let mut prev_pos = 0;
|
||||
for (qi, &qc) in query_lower.iter().enumerate() {
|
||||
loop {
|
||||
if ti >= target_lower.len() {
|
||||
return None;
|
||||
}
|
||||
if target_lower[ti] == qc {
|
||||
if qi > 0 {
|
||||
score += ti - prev_pos;
|
||||
}
|
||||
prev_pos = ti;
|
||||
ti += 1;
|
||||
break;
|
||||
}
|
||||
ti += 1;
|
||||
}
|
||||
}
|
||||
Some(score)
|
||||
}
|
||||
|
||||
fn is_word_char(c: char) -> bool {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! File/directory browser modal widget.
|
||||
|
||||
use crate::theme;
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::{Color, Style};
|
||||
@@ -7,15 +9,19 @@ use ratatui::Frame;
|
||||
|
||||
use super::ModalFrame;
|
||||
|
||||
/// Modal listing files and directories with a filter input line.
|
||||
pub struct FileBrowserModal<'a> {
|
||||
title: &'a str,
|
||||
input: &'a str,
|
||||
entries: &'a [(String, bool, bool)],
|
||||
audio_counts: &'a [Option<usize>],
|
||||
selected: usize,
|
||||
scroll_offset: usize,
|
||||
border_color: Option<Color>,
|
||||
width: u16,
|
||||
height: u16,
|
||||
hints: Option<Line<'a>>,
|
||||
color_path: bool,
|
||||
}
|
||||
|
||||
impl<'a> FileBrowserModal<'a> {
|
||||
@@ -24,11 +30,14 @@ impl<'a> FileBrowserModal<'a> {
|
||||
title,
|
||||
input,
|
||||
entries,
|
||||
audio_counts: &[],
|
||||
selected: 0,
|
||||
scroll_offset: 0,
|
||||
border_color: None,
|
||||
width: 60,
|
||||
height: 16,
|
||||
hints: None,
|
||||
color_path: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +66,22 @@ impl<'a> FileBrowserModal<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn render_centered(self, frame: &mut Frame, term: Rect) {
|
||||
pub fn hints(mut self, hints: Line<'a>) -> Self {
|
||||
self.hints = Some(hints);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn audio_counts(mut self, counts: &'a [Option<usize>]) -> Self {
|
||||
self.audio_counts = counts;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn color_path(mut self) -> Self {
|
||||
self.color_path = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn render_centered(self, frame: &mut Frame, term: Rect) -> Rect {
|
||||
let colors = theme::get();
|
||||
let border_color = self.border_color.unwrap_or(colors.ui.text_primary);
|
||||
|
||||
@@ -67,37 +91,61 @@ impl<'a> FileBrowserModal<'a> {
|
||||
.border_color(border_color)
|
||||
.render_centered(frame, term);
|
||||
|
||||
let rows = Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).split(inner);
|
||||
let has_hints = self.hints.is_some();
|
||||
let constraints = if has_hints {
|
||||
vec![
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(1),
|
||||
Constraint::Length(1),
|
||||
]
|
||||
} else {
|
||||
vec![Constraint::Length(1), Constraint::Min(1)]
|
||||
};
|
||||
let rows = Layout::vertical(constraints).split(inner);
|
||||
|
||||
// Input line
|
||||
frame.render_widget(
|
||||
Paragraph::new(Line::from(vec![
|
||||
let input_spans = if self.color_path {
|
||||
let (path_part, filter_part) = match self.input.rfind('/') {
|
||||
Some(pos) => (&self.input[..=pos], &self.input[pos + 1..]),
|
||||
None => ("", self.input),
|
||||
};
|
||||
vec![
|
||||
Span::raw("> "),
|
||||
Span::styled(path_part.to_string(), Style::new().fg(colors.browser.directory)),
|
||||
Span::styled(filter_part.to_string(), Style::new().fg(colors.input.text)),
|
||||
Span::styled("█", Style::new().fg(colors.input.cursor)),
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
Span::raw("> "),
|
||||
Span::styled(self.input, Style::new().fg(colors.input.text)),
|
||||
Span::styled("█", Style::new().fg(colors.input.cursor)),
|
||||
])),
|
||||
rows[0],
|
||||
]
|
||||
};
|
||||
frame.render_widget(Paragraph::new(Line::from(input_spans)), rows[0]);
|
||||
|
||||
// Hints bar
|
||||
if let Some(hints) = self.hints {
|
||||
let hint_row = rows[2];
|
||||
frame.render_widget(
|
||||
Paragraph::new(hints).alignment(ratatui::layout::Alignment::Right),
|
||||
hint_row,
|
||||
);
|
||||
}
|
||||
|
||||
// Entries list
|
||||
let visible_height = rows[1].height as usize;
|
||||
let visible_entries = self
|
||||
.entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.skip(self.scroll_offset)
|
||||
.take(visible_height);
|
||||
|
||||
let lines: Vec<Line> = visible_entries
|
||||
.enumerate()
|
||||
.map(|(i, (name, is_dir, is_cagire))| {
|
||||
let abs_idx = i + self.scroll_offset;
|
||||
.map(|(abs_idx, (name, is_dir, is_cagire))| {
|
||||
let is_selected = abs_idx == self.selected;
|
||||
let prefix = if is_selected { "> " } else { " " };
|
||||
let display = if *is_dir {
|
||||
format!("{prefix}{name}/")
|
||||
} else {
|
||||
format!("{prefix}{name}")
|
||||
};
|
||||
let color = if is_selected {
|
||||
colors.browser.selected
|
||||
} else if *is_dir {
|
||||
@@ -107,10 +155,26 @@ impl<'a> FileBrowserModal<'a> {
|
||||
} else {
|
||||
colors.browser.file
|
||||
};
|
||||
Line::from(Span::styled(display, Style::new().fg(color)))
|
||||
let display = if *is_dir {
|
||||
format!("{prefix}{name}/")
|
||||
} else {
|
||||
format!("{prefix}{name}")
|
||||
};
|
||||
let mut spans = vec![Span::styled(display, Style::new().fg(color))];
|
||||
if *is_dir && name != ".." {
|
||||
if let Some(Some(count)) = self.audio_counts.get(abs_idx) {
|
||||
spans.push(Span::styled(
|
||||
format!(" ({count})"),
|
||||
Style::new().fg(colors.browser.file),
|
||||
));
|
||||
}
|
||||
}
|
||||
Line::from(spans)
|
||||
})
|
||||
.collect();
|
||||
|
||||
frame.render_widget(Paragraph::new(lines), rows[1]);
|
||||
|
||||
inner
|
||||
}
|
||||
}
|
||||
|
||||
30
crates/ratatui/src/hint_bar.rs
Normal file
30
crates/ratatui/src/hint_bar.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
//! Bottom-bar keyboard hint renderer.
|
||||
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::style::Style;
|
||||
|
||||
use crate::theme;
|
||||
|
||||
/// Build a styled line of key/action pairs for the hint bar.
|
||||
pub fn hint_line(pairs: &[(&str, &str)]) -> Line<'static> {
|
||||
let theme = theme::get();
|
||||
let key_style = Style::default().fg(theme.hint.key);
|
||||
let text_style = Style::default().fg(theme.hint.text);
|
||||
|
||||
let spans: Vec<Span> = pairs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(i, (key, action))| {
|
||||
let mut s = vec![
|
||||
Span::styled(key.to_string(), key_style),
|
||||
Span::styled(format!(" {action}"), text_style),
|
||||
];
|
||||
if i + 1 < pairs.len() {
|
||||
s.push(Span::styled(" ", text_style));
|
||||
}
|
||||
s
|
||||
})
|
||||
.collect();
|
||||
|
||||
Line::from(spans)
|
||||
}
|
||||
@@ -1,26 +1,44 @@
|
||||
//! Reusable TUI widgets for the Cagire sequencer interface.
|
||||
|
||||
mod category_list;
|
||||
mod confirm;
|
||||
mod editor;
|
||||
mod file_browser;
|
||||
mod hint_bar;
|
||||
mod lissajous;
|
||||
mod list_select;
|
||||
mod modal;
|
||||
mod nav_minimap;
|
||||
mod props_form;
|
||||
mod sample_browser;
|
||||
mod scope;
|
||||
mod scroll_indicators;
|
||||
mod search_bar;
|
||||
mod section_header;
|
||||
mod sparkles;
|
||||
mod spectrum;
|
||||
mod text_input;
|
||||
pub mod theme;
|
||||
mod vu_meter;
|
||||
mod waveform;
|
||||
|
||||
pub use category_list::{CategoryItem, CategoryList, Selection};
|
||||
pub use confirm::ConfirmModal;
|
||||
pub use editor::{CompletionCandidate, Editor};
|
||||
pub use editor::{fuzzy_match, CompletionCandidate, Editor};
|
||||
pub use file_browser::FileBrowserModal;
|
||||
pub use hint_bar::hint_line;
|
||||
pub use lissajous::Lissajous;
|
||||
pub use list_select::ListSelect;
|
||||
pub use modal::ModalFrame;
|
||||
pub use nav_minimap::{NavMinimap, NavTile};
|
||||
pub use nav_minimap::{hit_test_tile, minimap_area, NavMinimap, NavTile};
|
||||
pub use props_form::render_props_form;
|
||||
pub use sample_browser::{SampleBrowser, TreeLine, TreeLineKind};
|
||||
pub use scope::{Orientation, Scope};
|
||||
pub use scroll_indicators::{render_scroll_indicators, IndicatorAlign};
|
||||
pub use search_bar::render_search_bar;
|
||||
pub use section_header::render_section_header;
|
||||
pub use sparkles::Sparkles;
|
||||
pub use spectrum::Spectrum;
|
||||
pub use spectrum::{Spectrum, SpectrumStyle};
|
||||
pub use text_input::TextInputModal;
|
||||
pub use vu_meter::VuMeter;
|
||||
pub use waveform::Waveform;
|
||||
|
||||
234
crates/ratatui/src/lissajous.rs
Normal file
234
crates/ratatui/src/lissajous.rs
Normal file
@@ -0,0 +1,234 @@
|
||||
//! Lissajous XY oscilloscope widget using braille characters.
|
||||
|
||||
use crate::theme;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Color;
|
||||
use ratatui::widgets::Widget;
|
||||
use std::cell::RefCell;
|
||||
|
||||
thread_local! {
|
||||
static PATTERNS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
|
||||
static TRAIL: RefCell<TrailState> = const { RefCell::new(TrailState { fine_w: 0, fine_h: 0, heat: Vec::new() }) };
|
||||
}
|
||||
|
||||
struct TrailState {
|
||||
fine_w: usize,
|
||||
fine_h: usize,
|
||||
heat: Vec<f32>,
|
||||
}
|
||||
|
||||
/// XY oscilloscope plotting left vs right channels as a Lissajous curve.
|
||||
pub struct Lissajous<'a> {
|
||||
left: &'a [f32],
|
||||
right: &'a [f32],
|
||||
color: Option<Color>,
|
||||
gain: f32,
|
||||
trails: bool,
|
||||
}
|
||||
|
||||
impl<'a> Lissajous<'a> {
|
||||
pub fn new(left: &'a [f32], right: &'a [f32]) -> Self {
|
||||
Self {
|
||||
left,
|
||||
right,
|
||||
color: None,
|
||||
gain: 1.0,
|
||||
trails: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn trails(mut self, enabled: bool) -> Self {
|
||||
self.trails = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn color(mut self, c: Color) -> Self {
|
||||
self.color = Some(c);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn gain(mut self, g: f32) -> Self {
|
||||
self.gain = g;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Lissajous<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
if area.width == 0 || area.height == 0 || self.left.is_empty() || self.right.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.trails {
|
||||
self.render_trails(area, buf);
|
||||
} else {
|
||||
self.render_normal(area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Lissajous<'_> {
|
||||
fn render_normal(self, area: Rect, buf: &mut Buffer) {
|
||||
let color = self.color.unwrap_or_else(|| theme::get().meter.low);
|
||||
let width = area.width as usize;
|
||||
let height = area.height as usize;
|
||||
let fine_width = width * 2;
|
||||
let fine_height = height * 4;
|
||||
let len = self.left.len().min(self.right.len());
|
||||
|
||||
PATTERNS.with(|p| {
|
||||
let mut patterns = p.borrow_mut();
|
||||
let size = width * height;
|
||||
patterns.clear();
|
||||
patterns.resize(size, 0);
|
||||
|
||||
for i in 0..len {
|
||||
let l = (self.left[i] * self.gain).clamp(-1.0, 1.0);
|
||||
let r = (self.right[i] * self.gain).clamp(-1.0, 1.0);
|
||||
|
||||
let fine_x = ((r + 1.0) * 0.5 * (fine_width - 1) as f32).round() as usize;
|
||||
let fine_y = ((1.0 - l) * 0.5 * (fine_height - 1) as f32).round() as usize;
|
||||
let fine_x = fine_x.min(fine_width - 1);
|
||||
let fine_y = fine_y.min(fine_height - 1);
|
||||
|
||||
let char_x = fine_x / 2;
|
||||
let char_y = fine_y / 4;
|
||||
let dot_x = fine_x % 2;
|
||||
let dot_y = fine_y % 4;
|
||||
|
||||
patterns[char_y * width + char_x] |= braille_bit(dot_x, dot_y);
|
||||
}
|
||||
|
||||
for cy in 0..height {
|
||||
for cx in 0..width {
|
||||
let pattern = patterns[cy * width + cx];
|
||||
if pattern != 0 {
|
||||
let ch = char::from_u32(0x2800 + pattern as u32).unwrap_or(' ');
|
||||
buf[(area.x + cx as u16, area.y + cy as u16)]
|
||||
.set_char(ch)
|
||||
.set_fg(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn render_trails(self, area: Rect, buf: &mut Buffer) {
|
||||
let theme = theme::get();
|
||||
let width = area.width as usize;
|
||||
let height = area.height as usize;
|
||||
let fine_w = width * 2;
|
||||
let fine_h = height * 4;
|
||||
let len = self.left.len().min(self.right.len());
|
||||
|
||||
TRAIL.with(|t| {
|
||||
let mut trail = t.borrow_mut();
|
||||
|
||||
// Reset if dimensions changed
|
||||
if trail.fine_w != fine_w || trail.fine_h != fine_h {
|
||||
trail.fine_w = fine_w;
|
||||
trail.fine_h = fine_h;
|
||||
trail.heat.clear();
|
||||
trail.heat.resize(fine_w * fine_h, 0.0);
|
||||
}
|
||||
|
||||
// Decay existing heat
|
||||
for h in trail.heat.iter_mut() {
|
||||
*h *= 0.85;
|
||||
}
|
||||
|
||||
// Plot new sample points
|
||||
for i in 0..len {
|
||||
let l = (self.left[i] * self.gain).clamp(-1.0, 1.0);
|
||||
let r = (self.right[i] * self.gain).clamp(-1.0, 1.0);
|
||||
|
||||
let fx = ((r + 1.0) * 0.5 * (fine_w - 1) as f32).round() as usize;
|
||||
let fy = ((1.0 - l) * 0.5 * (fine_h - 1) as f32).round() as usize;
|
||||
let fx = fx.min(fine_w - 1);
|
||||
let fy = fy.min(fine_h - 1);
|
||||
|
||||
trail.heat[fy * fine_w + fx] = 1.0;
|
||||
}
|
||||
|
||||
// Convert heat map to braille
|
||||
PATTERNS.with(|p| {
|
||||
let mut patterns = p.borrow_mut();
|
||||
patterns.clear();
|
||||
patterns.resize(width * height, 0);
|
||||
|
||||
// Track brightest color per cell
|
||||
let mut colors: Vec<Option<Color>> = vec![None; width * height];
|
||||
|
||||
for fy in 0..fine_h {
|
||||
for fx in 0..fine_w {
|
||||
let h = trail.heat[fy * fine_w + fx];
|
||||
if h < 0.05 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cx = fx / 2;
|
||||
let cy = fy / 4;
|
||||
let dx = fx % 2;
|
||||
let dy = fy % 4;
|
||||
|
||||
let idx = cy * width + cx;
|
||||
patterns[idx] |= braille_bit(dx, dy);
|
||||
|
||||
let dot_color = if h > 0.7 {
|
||||
theme.meter.high
|
||||
} else if h > 0.25 {
|
||||
theme.meter.mid
|
||||
} else {
|
||||
theme.meter.low
|
||||
};
|
||||
|
||||
let replace = match colors[idx] {
|
||||
None => true,
|
||||
Some(cur) => {
|
||||
rank_color(dot_color, &theme) > rank_color(cur, &theme)
|
||||
}
|
||||
};
|
||||
if replace {
|
||||
colors[idx] = Some(dot_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for cy in 0..height {
|
||||
for cx in 0..width {
|
||||
let idx = cy * width + cx;
|
||||
let pattern = patterns[idx];
|
||||
if pattern != 0 {
|
||||
let ch = char::from_u32(0x2800 + pattern as u32).unwrap_or(' ');
|
||||
let color = colors[idx].unwrap_or(theme.meter.low);
|
||||
buf[(area.x + cx as u16, area.y + cy as u16)]
|
||||
.set_char(ch)
|
||||
.set_fg(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn braille_bit(dot_x: usize, dot_y: usize) -> u8 {
|
||||
match (dot_x, dot_y) {
|
||||
(0, 0) => 0x01,
|
||||
(0, 1) => 0x02,
|
||||
(0, 2) => 0x04,
|
||||
(0, 3) => 0x40,
|
||||
(1, 0) => 0x08,
|
||||
(1, 1) => 0x10,
|
||||
(1, 2) => 0x20,
|
||||
(1, 3) => 0x80,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn rank_color(c: Color, theme: &crate::theme::ThemeColors) -> u8 {
|
||||
if c == theme.meter.high { 2 }
|
||||
else if c == theme.meter.mid { 1 }
|
||||
else { 0 }
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Scrollable single-select list widget with cursor highlight.
|
||||
|
||||
use crate::theme;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Modifier, Style};
|
||||
@@ -5,6 +7,7 @@ use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::Frame;
|
||||
|
||||
/// Scrollable list with a highlighted cursor and selected-item marker.
|
||||
pub struct ListSelect<'a> {
|
||||
items: &'a [String],
|
||||
selected: usize,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
//! Centered modal frame with border and title.
|
||||
|
||||
use crate::theme;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Color, Style};
|
||||
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
|
||||
use ratatui::Frame;
|
||||
|
||||
/// Centered modal overlay with titled border.
|
||||
pub struct ModalFrame<'a> {
|
||||
title: &'a str,
|
||||
width: u16,
|
||||
|
||||
@@ -1,9 +1,47 @@
|
||||
//! Page navigation minimap showing a 3x2 grid of tiles.
|
||||
|
||||
use crate::theme;
|
||||
use ratatui::layout::{Alignment, Rect};
|
||||
use ratatui::style::Style;
|
||||
use ratatui::widgets::{Clear, Paragraph};
|
||||
use ratatui::Frame;
|
||||
|
||||
const TILE_W: u16 = 12;
|
||||
const TILE_H: u16 = 3;
|
||||
const GAP: u16 = 1;
|
||||
const PAD: u16 = 2;
|
||||
const GRID_COLS: u16 = 3;
|
||||
const GRID_ROWS: u16 = 2;
|
||||
|
||||
/// Compute the centered minimap area for a 3x2 grid.
|
||||
pub fn minimap_area(term: Rect) -> Rect {
|
||||
let content_w = TILE_W * GRID_COLS + GAP * (GRID_COLS - 1);
|
||||
let content_h = TILE_H * GRID_ROWS + GAP * (GRID_ROWS - 1);
|
||||
let modal_w = content_w + PAD * 2;
|
||||
let modal_h = content_h + PAD * 2;
|
||||
let x = term.x + (term.width.saturating_sub(modal_w)) / 2;
|
||||
let y = term.y + (term.height.saturating_sub(modal_h)) / 2;
|
||||
Rect::new(x, y, modal_w, modal_h)
|
||||
}
|
||||
|
||||
/// Hit-test: returns `(grid_col, grid_row)` if the click lands on a tile.
|
||||
pub fn hit_test_tile(col: u16, row: u16, term: Rect) -> Option<(i8, i8)> {
|
||||
let area = minimap_area(term);
|
||||
let inner_x = area.x + PAD;
|
||||
let inner_y = area.y + PAD;
|
||||
|
||||
for grid_row in 0..GRID_ROWS {
|
||||
for grid_col in 0..GRID_COLS {
|
||||
let tx = inner_x + grid_col * (TILE_W + GAP);
|
||||
let ty = inner_y + grid_row * (TILE_H + GAP);
|
||||
if col >= tx && col < tx + TILE_W && row >= ty && row < ty + TILE_H {
|
||||
return Some((grid_col as i8, grid_row as i8));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// A tile in the navigation grid
|
||||
pub struct NavTile {
|
||||
pub col: i8,
|
||||
@@ -27,25 +65,7 @@ impl<'a> NavMinimap<'a> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute grid bounds from tiles
|
||||
let max_col = self.tiles.iter().map(|t| t.col).max().unwrap_or(0);
|
||||
let max_row = self.tiles.iter().map(|t| t.row).max().unwrap_or(0);
|
||||
let cols = (max_col + 1) as u16;
|
||||
let rows = (max_row + 1) as u16;
|
||||
|
||||
let tile_w: u16 = 12;
|
||||
let tile_h: u16 = 3;
|
||||
let gap: u16 = 1;
|
||||
let pad: u16 = 2;
|
||||
|
||||
let content_w = tile_w * cols + gap * (cols.saturating_sub(1));
|
||||
let content_h = tile_h * rows + gap * (rows.saturating_sub(1));
|
||||
let modal_w = content_w + pad * 2;
|
||||
let modal_h = content_h + pad * 2;
|
||||
|
||||
let x = term.x + (term.width.saturating_sub(modal_w)) / 2;
|
||||
let y = term.y + (term.height.saturating_sub(modal_h)) / 2;
|
||||
let area = Rect::new(x, y, modal_w, modal_h);
|
||||
let area = minimap_area(term);
|
||||
|
||||
frame.render_widget(Clear, area);
|
||||
|
||||
@@ -60,13 +80,13 @@ impl<'a> NavMinimap<'a> {
|
||||
);
|
||||
}
|
||||
|
||||
let inner_x = area.x + pad;
|
||||
let inner_y = area.y + pad;
|
||||
let inner_x = area.x + PAD;
|
||||
let inner_y = area.y + PAD;
|
||||
|
||||
for tile in self.tiles {
|
||||
let tile_x = inner_x + (tile.col as u16) * (tile_w + gap);
|
||||
let tile_y = inner_y + (tile.row as u16) * (tile_h + gap);
|
||||
let tile_area = Rect::new(tile_x, tile_y, tile_w, tile_h);
|
||||
let tile_x = inner_x + (tile.col as u16) * (TILE_W + GAP);
|
||||
let tile_y = inner_y + (tile.row as u16) * (TILE_H + GAP);
|
||||
let tile_area = Rect::new(tile_x, tile_y, TILE_W, TILE_H);
|
||||
let is_selected = (tile.col, tile.row) == self.selected;
|
||||
self.render_tile(frame, tile_area, tile.name, is_selected);
|
||||
}
|
||||
|
||||
45
crates/ratatui/src/props_form.rs
Normal file
45
crates/ratatui/src/props_form.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
//! Vertical label/value property form renderer.
|
||||
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::theme;
|
||||
|
||||
/// Render a vertical list of label/value pairs with selection highlight.
|
||||
pub fn render_props_form(frame: &mut Frame, area: Rect, fields: &[(&str, &str, bool)]) {
|
||||
let theme = theme::get();
|
||||
|
||||
for (i, (label, value, selected)) in fields.iter().enumerate() {
|
||||
let y = area.y + i as u16;
|
||||
if y >= area.y + area.height {
|
||||
break;
|
||||
}
|
||||
|
||||
let (label_style, value_style) = if *selected {
|
||||
(
|
||||
Style::default()
|
||||
.fg(theme.hint.key)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
Style::default()
|
||||
.fg(theme.ui.text_primary)
|
||||
.bg(theme.ui.surface),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
Style::default().fg(theme.ui.text_muted),
|
||||
Style::default().fg(theme.ui.text_primary),
|
||||
)
|
||||
};
|
||||
|
||||
let label_area = Rect::new(area.x + 1, y, 14, 1);
|
||||
let value_area = Rect::new(area.x + 16, y, area.width.saturating_sub(18), 1);
|
||||
|
||||
frame.render_widget(
|
||||
Paragraph::new(format!("{label}:")).style(label_style),
|
||||
label_area,
|
||||
);
|
||||
frame.render_widget(Paragraph::new(*value).style(value_style), value_area);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
//! Tree-view sample browser with search filtering.
|
||||
|
||||
use crate::theme;
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Borders, Paragraph};
|
||||
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
|
||||
use ratatui::Frame;
|
||||
|
||||
/// Node type in the sample tree.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum TreeLineKind {
|
||||
Root { expanded: bool },
|
||||
@@ -12,6 +15,7 @@ pub enum TreeLineKind {
|
||||
File,
|
||||
}
|
||||
|
||||
/// A single row in the sample browser tree.
|
||||
#[derive(Clone)]
|
||||
pub struct TreeLine {
|
||||
pub depth: u8,
|
||||
@@ -19,8 +23,10 @@ pub struct TreeLine {
|
||||
pub label: String,
|
||||
pub folder: String,
|
||||
pub index: usize,
|
||||
pub child_count: usize,
|
||||
}
|
||||
|
||||
/// Tree-view browser for navigating sample folders.
|
||||
pub struct SampleBrowser<'a> {
|
||||
entries: &'a [TreeLine],
|
||||
cursor: usize,
|
||||
@@ -111,13 +117,13 @@ impl<'a> SampleBrowser<'a> {
|
||||
fn render_tree(&self, frame: &mut Frame, area: Rect, colors: &theme::ThemeColors) {
|
||||
let height = area.height as usize;
|
||||
if self.entries.is_empty() {
|
||||
let msg = if self.search_query.is_empty() {
|
||||
"No samples loaded"
|
||||
if self.search_query.is_empty() {
|
||||
self.render_empty_guide(frame, area, colors);
|
||||
} else {
|
||||
"No matches"
|
||||
};
|
||||
let line = Line::from(Span::styled(msg, Style::new().fg(colors.browser.empty_text)));
|
||||
let line =
|
||||
Line::from(Span::styled("No matches", Style::new().fg(colors.browser.empty_text)));
|
||||
frame.render_widget(Paragraph::new(vec![line]), area);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -131,10 +137,10 @@ impl<'a> SampleBrowser<'a> {
|
||||
|
||||
let (icon, icon_color) = match entry.kind {
|
||||
TreeLineKind::Root { expanded: true } | TreeLineKind::Folder { expanded: true } => {
|
||||
("\u{25BC} ", colors.browser.folder_icon)
|
||||
("\u{2212} ", colors.browser.folder_icon)
|
||||
}
|
||||
TreeLineKind::Root { expanded: false }
|
||||
| TreeLineKind::Folder { expanded: false } => ("\u{25B6} ", colors.browser.folder_icon),
|
||||
| TreeLineKind::Folder { expanded: false } => ("+ ", colors.browser.folder_icon),
|
||||
TreeLineKind::File => ("\u{266A} ", colors.browser.file_icon),
|
||||
};
|
||||
|
||||
@@ -158,15 +164,91 @@ impl<'a> SampleBrowser<'a> {
|
||||
Style::new().fg(icon_color)
|
||||
};
|
||||
|
||||
let spans = vec![
|
||||
let prefix_width = indent.len() + 2; // indent + icon
|
||||
let suffix = match entry.kind {
|
||||
TreeLineKind::File => format!(" {}", entry.index),
|
||||
TreeLineKind::Root { expanded: false }
|
||||
| TreeLineKind::Folder { expanded: false }
|
||||
if entry.child_count > 0 =>
|
||||
{
|
||||
format!(" ({})", entry.child_count)
|
||||
}
|
||||
_ => String::new(),
|
||||
};
|
||||
let max_label = (area.width as usize)
|
||||
.saturating_sub(prefix_width)
|
||||
.saturating_sub(suffix.len());
|
||||
let label: std::borrow::Cow<str> = if entry.label.len() > max_label && max_label > 1 {
|
||||
let truncated: String = entry.label.chars().take(max_label - 1).collect();
|
||||
format!("{}\u{2026}", truncated).into()
|
||||
} else {
|
||||
(&entry.label).into()
|
||||
};
|
||||
|
||||
let mut spans = vec![
|
||||
Span::raw(indent),
|
||||
Span::styled(icon, icon_style),
|
||||
Span::styled(&entry.label, label_style),
|
||||
Span::styled(label, label_style),
|
||||
];
|
||||
|
||||
match entry.kind {
|
||||
TreeLineKind::File => {
|
||||
let idx_style = Style::new().fg(colors.browser.empty_text);
|
||||
spans.push(Span::styled(suffix, idx_style));
|
||||
}
|
||||
_ if !suffix.is_empty() => {
|
||||
let dim_style = Style::new().fg(colors.browser.empty_text);
|
||||
spans.push(Span::styled(suffix, dim_style));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
lines.push(Line::from(spans));
|
||||
}
|
||||
|
||||
frame.render_widget(Paragraph::new(lines), area);
|
||||
}
|
||||
|
||||
fn render_empty_guide(&self, frame: &mut Frame, area: Rect, colors: &theme::ThemeColors) {
|
||||
let muted = Style::new().fg(colors.browser.empty_text);
|
||||
let heading = Style::new().fg(colors.ui.text_primary);
|
||||
let key = Style::new().fg(colors.hint.key);
|
||||
let desc = Style::new().fg(colors.hint.text);
|
||||
let code = Style::new().fg(colors.ui.accent);
|
||||
|
||||
let lines = vec![
|
||||
Line::from(Span::styled(" No samples loaded.", muted)),
|
||||
Line::from(""),
|
||||
Line::from(Span::styled(" Load from the Engine page:", heading)),
|
||||
Line::from(""),
|
||||
Line::from(vec![
|
||||
Span::styled(" F6 ", key),
|
||||
Span::styled("Go to Engine page", desc),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled(" A ", key),
|
||||
Span::styled("Add a sample folder", desc),
|
||||
]),
|
||||
Line::from(""),
|
||||
Line::from(Span::styled(" Organize samples like this:", heading)),
|
||||
Line::from(""),
|
||||
Line::from(Span::styled(" samples/", code)),
|
||||
Line::from(Span::styled(" \u{251C}\u{2500}\u{2500} kick/", code)),
|
||||
Line::from(Span::styled(" \u{2502} \u{2514}\u{2500}\u{2500} kick.wav", code)),
|
||||
Line::from(Span::styled(" \u{251C}\u{2500}\u{2500} snare/", code)),
|
||||
Line::from(Span::styled(" \u{2502} \u{2514}\u{2500}\u{2500} snare.wav", code)),
|
||||
Line::from(Span::styled(" \u{2514}\u{2500}\u{2500} hats/", code)),
|
||||
Line::from(Span::styled(" \u{251C}\u{2500}\u{2500} closed.wav", code)),
|
||||
Line::from(Span::styled(" \u{251C}\u{2500}\u{2500} open.wav", code)),
|
||||
Line::from(Span::styled(" \u{2514}\u{2500}\u{2500} pedal.wav", code)),
|
||||
Line::from(""),
|
||||
Line::from(Span::styled(" Folders become Forth words:", heading)),
|
||||
Line::from(""),
|
||||
Line::from(Span::styled(" kick sound .", code)),
|
||||
Line::from(Span::styled(" hats sound 2 n .", code)),
|
||||
Line::from(Span::styled(" snare sound 0.5 speed .", code)),
|
||||
];
|
||||
|
||||
frame.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), area);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Oscilloscope waveform widget using braille characters.
|
||||
|
||||
use crate::theme;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
@@ -9,12 +11,14 @@ thread_local! {
|
||||
static PATTERNS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Rendering direction for the oscilloscope.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Orientation {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
/// Single-channel oscilloscope using braille dot plotting.
|
||||
pub struct Scope<'a> {
|
||||
data: &'a [f32],
|
||||
orientation: Orientation,
|
||||
@@ -41,6 +45,11 @@ impl<'a> Scope<'a> {
|
||||
self.color = Some(c);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn gain(mut self, g: f32) -> Self {
|
||||
self.gain = g;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Scope<'_> {
|
||||
@@ -66,9 +75,6 @@ fn render_horizontal(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, g
|
||||
let fine_width = width * 2;
|
||||
let fine_height = height * 4;
|
||||
|
||||
let peak = data.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
|
||||
let auto_gain = if peak > 0.001 { gain / peak } else { gain };
|
||||
|
||||
PATTERNS.with(|p| {
|
||||
let mut patterns = p.borrow_mut();
|
||||
let size = width * height;
|
||||
@@ -77,7 +83,7 @@ fn render_horizontal(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, g
|
||||
|
||||
for fine_x in 0..fine_width {
|
||||
let sample_idx = (fine_x * data.len()) / fine_width;
|
||||
let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * auto_gain).clamp(-1.0, 1.0);
|
||||
let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * gain).clamp(-1.0, 1.0);
|
||||
|
||||
let fine_y = ((1.0 - sample) * 0.5 * (fine_height - 1) as f32).round() as usize;
|
||||
let fine_y = fine_y.min(fine_height - 1);
|
||||
@@ -122,9 +128,6 @@ fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gai
|
||||
let fine_width = width * 2;
|
||||
let fine_height = height * 4;
|
||||
|
||||
let peak = data.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
|
||||
let auto_gain = if peak > 0.001 { gain / peak } else { gain };
|
||||
|
||||
PATTERNS.with(|p| {
|
||||
let mut patterns = p.borrow_mut();
|
||||
let size = width * height;
|
||||
@@ -133,7 +136,7 @@ fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gai
|
||||
|
||||
for fine_y in 0..fine_height {
|
||||
let sample_idx = (fine_y * data.len()) / fine_height;
|
||||
let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * auto_gain).clamp(-1.0, 1.0);
|
||||
let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * gain).clamp(-1.0, 1.0);
|
||||
|
||||
let fine_x = ((sample + 1.0) * 0.5 * (fine_width - 1) as f32).round() as usize;
|
||||
let fine_x = fine_x.min(fine_width - 1);
|
||||
|
||||
57
crates/ratatui/src/scroll_indicators.rs
Normal file
57
crates/ratatui/src/scroll_indicators.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
//! Up/down arrow scroll indicators for bounded lists.
|
||||
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Color, Style};
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::Frame;
|
||||
|
||||
/// Horizontal alignment for scroll indicators.
|
||||
pub enum IndicatorAlign {
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
/// Render up/down scroll arrows when content overflows.
|
||||
pub fn render_scroll_indicators(
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
offset: usize,
|
||||
visible: usize,
|
||||
total: usize,
|
||||
color: Color,
|
||||
align: IndicatorAlign,
|
||||
) {
|
||||
let style = Style::new().fg(color);
|
||||
|
||||
match align {
|
||||
IndicatorAlign::Center => {
|
||||
if offset > 0 {
|
||||
let indicator = Paragraph::new("▲")
|
||||
.style(style)
|
||||
.alignment(ratatui::layout::Alignment::Center);
|
||||
frame.render_widget(indicator, Rect { height: 1, ..area });
|
||||
}
|
||||
if offset + visible < total {
|
||||
let y = area.y + area.height.saturating_sub(1);
|
||||
let indicator = Paragraph::new("▼")
|
||||
.style(style)
|
||||
.alignment(ratatui::layout::Alignment::Center);
|
||||
frame.render_widget(indicator, Rect { y, height: 1, ..area });
|
||||
}
|
||||
}
|
||||
IndicatorAlign::Right => {
|
||||
let x = area.x + area.width.saturating_sub(1);
|
||||
if offset > 0 {
|
||||
let indicator = Paragraph::new("▲").style(style);
|
||||
frame.render_widget(indicator, Rect::new(x, area.y, 1, 1));
|
||||
}
|
||||
if offset + visible < total {
|
||||
let indicator = Paragraph::new("▼").style(style);
|
||||
frame.render_widget(
|
||||
indicator,
|
||||
Rect::new(x, area.y + area.height.saturating_sub(1), 1, 1),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
crates/ratatui/src/search_bar.rs
Normal file
23
crates/ratatui/src/search_bar.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
//! Inline search bar with active/inactive styling.
|
||||
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Style;
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::theme;
|
||||
|
||||
/// Render a `/query` search bar.
|
||||
pub fn render_search_bar(frame: &mut Frame, area: Rect, query: &str, active: bool) {
|
||||
let theme = theme::get();
|
||||
let style = if active {
|
||||
Style::new().fg(theme.search.active)
|
||||
} else {
|
||||
Style::new().fg(theme.search.inactive)
|
||||
};
|
||||
let cursor = if active { "_" } else { "" };
|
||||
let text = format!(" /{query}{cursor}");
|
||||
let line = Line::from(Span::styled(text, style));
|
||||
frame.render_widget(Paragraph::new(vec![line]), area);
|
||||
}
|
||||
33
crates/ratatui/src/section_header.rs
Normal file
33
crates/ratatui/src/section_header.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
//! Section header with horizontal divider for engine-view panels.
|
||||
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::theme;
|
||||
|
||||
/// Render a section title with a horizontal divider below it.
|
||||
pub fn render_section_header(frame: &mut Frame, title: &str, focused: bool, area: Rect) {
|
||||
let theme = theme::get();
|
||||
let [header_area, divider_area] =
|
||||
Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(area);
|
||||
|
||||
let header_style = if focused {
|
||||
Style::new()
|
||||
.fg(theme.engine.header_focused)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::new()
|
||||
.fg(theme.engine.header)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
};
|
||||
|
||||
frame.render_widget(Paragraph::new(title).style(header_style), header_area);
|
||||
|
||||
let divider = "─".repeat(area.width as usize);
|
||||
frame.render_widget(
|
||||
Paragraph::new(divider).style(Style::new().fg(theme.engine.divider)),
|
||||
divider_area,
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Decorative particle effect using random Unicode glyphs.
|
||||
|
||||
use crate::theme;
|
||||
use rand::Rng;
|
||||
use ratatui::buffer::Buffer;
|
||||
@@ -14,6 +16,7 @@ struct Sparkle {
|
||||
life: u8,
|
||||
}
|
||||
|
||||
/// Animated sparkle particles for visual flair.
|
||||
#[derive(Default)]
|
||||
pub struct Sparkles {
|
||||
sparkles: Vec<Sparkle>,
|
||||
|
||||
@@ -1,18 +1,58 @@
|
||||
//! 32-band frequency spectrum display with optional peak hold.
|
||||
|
||||
use crate::theme;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Color;
|
||||
use ratatui::widgets::Widget;
|
||||
use std::cell::RefCell;
|
||||
|
||||
const BLOCKS: [char; 8] = ['\u{2581}', '\u{2582}', '\u{2583}', '\u{2584}', '\u{2585}', '\u{2586}', '\u{2587}', '\u{2588}'];
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum SpectrumStyle {
|
||||
#[default]
|
||||
Bars,
|
||||
Line,
|
||||
Filled,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static PEAKS: RefCell<[f32; 32]> = const { RefCell::new([0.0; 32]) };
|
||||
static PATTERNS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
/// 32-band spectrum analyzer using block characters.
|
||||
pub struct Spectrum<'a> {
|
||||
data: &'a [f32; 32],
|
||||
gain: f32,
|
||||
style: SpectrumStyle,
|
||||
peaks: bool,
|
||||
}
|
||||
|
||||
impl<'a> Spectrum<'a> {
|
||||
pub fn new(data: &'a [f32; 32]) -> Self {
|
||||
Self { data }
|
||||
Self {
|
||||
data,
|
||||
gain: 1.0,
|
||||
style: SpectrumStyle::Bars,
|
||||
peaks: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gain(mut self, g: f32) -> Self {
|
||||
self.gain = g;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, s: SpectrumStyle) -> Self {
|
||||
self.style = s;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn peaks(mut self, enabled: bool) -> Self {
|
||||
self.peaks = enabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,43 +62,177 @@ impl Widget for Spectrum<'_> {
|
||||
return;
|
||||
}
|
||||
|
||||
let colors = theme::get();
|
||||
let height = area.height as f32;
|
||||
let band_width = area.width as usize / 32;
|
||||
if band_width == 0 {
|
||||
return;
|
||||
// Update peak hold state
|
||||
let peak_values = if self.peaks {
|
||||
Some(PEAKS.with(|p| {
|
||||
let mut peaks = p.borrow_mut();
|
||||
for (i, &mag) in self.data.iter().enumerate() {
|
||||
let v = (mag * self.gain).min(1.0);
|
||||
if v >= peaks[i] {
|
||||
peaks[i] = v;
|
||||
} else {
|
||||
peaks[i] = (peaks[i] - 0.02).max(v);
|
||||
}
|
||||
}
|
||||
*peaks
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
match self.style {
|
||||
SpectrumStyle::Bars => render_bars(self.data, area, buf, self.gain, peak_values.as_ref()),
|
||||
SpectrumStyle::Line => render_braille(self.data, area, buf, self.gain, false, peak_values.as_ref()),
|
||||
SpectrumStyle::Filled => render_braille(self.data, area, buf, self.gain, true, peak_values.as_ref()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (band, &mag) in self.data.iter().enumerate() {
|
||||
let bar_height = mag * height;
|
||||
let full_cells = bar_height as usize;
|
||||
let frac = bar_height - full_cells as f32;
|
||||
let frac_idx = (frac * 8.0) as usize;
|
||||
|
||||
let x_start = area.x + (band * band_width) as u16;
|
||||
|
||||
for row in 0..area.height as usize {
|
||||
let y = area.y + area.height - 1 - row as u16;
|
||||
let ratio = row as f32 / area.height as f32;
|
||||
let color = if ratio < 0.33 {
|
||||
fn band_color(ratio: f32, colors: &theme::ThemeColors) -> Color {
|
||||
if ratio < 0.33 {
|
||||
Color::Rgb(colors.meter.low_rgb.0, colors.meter.low_rgb.1, colors.meter.low_rgb.2)
|
||||
} else if ratio < 0.66 {
|
||||
Color::Rgb(colors.meter.mid_rgb.0, colors.meter.mid_rgb.1, colors.meter.mid_rgb.2)
|
||||
} else {
|
||||
Color::Rgb(colors.meter.high_rgb.0, colors.meter.high_rgb.1, colors.meter.high_rgb.2)
|
||||
};
|
||||
for dx in 0..band_width as u16 {
|
||||
let x = x_start + dx;
|
||||
if x >= area.x + area.width {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
fn render_bars(data: &[f32; 32], area: Rect, buf: &mut Buffer, gain: f32, peaks: Option<&[f32; 32]>) {
|
||||
let colors = theme::get();
|
||||
let height = area.height as f32;
|
||||
let base = area.width as usize / 32;
|
||||
let remainder = area.width as usize % 32;
|
||||
if base == 0 && remainder == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut x_start = area.x;
|
||||
for (band, &mag) in data.iter().enumerate() {
|
||||
let w = base + if band < remainder { 1 } else { 0 };
|
||||
if w == 0 {
|
||||
continue;
|
||||
}
|
||||
let bar_height = (mag * gain).min(1.0) * height;
|
||||
let full_cells = bar_height as usize;
|
||||
let frac = bar_height - full_cells as f32;
|
||||
let frac_idx = (frac * 8.0) as usize;
|
||||
|
||||
// Peak hold row
|
||||
let peak_row = peaks.map(|p| {
|
||||
let ph = p[band] * height;
|
||||
let row = (height - ph).max(0.0) as usize;
|
||||
row.min(area.height as usize - 1)
|
||||
});
|
||||
|
||||
for row in 0..area.height as usize {
|
||||
let y = area.y + area.height - 1 - row as u16;
|
||||
let ratio = row as f32 / area.height as f32;
|
||||
let color = band_color(ratio, &colors);
|
||||
|
||||
for dx in 0..w as u16 {
|
||||
let x = x_start + dx;
|
||||
if row < full_cells {
|
||||
buf[(x, y)].set_char(BLOCKS[7]).set_fg(color);
|
||||
} else if row == full_cells && frac_idx > 0 {
|
||||
buf[(x, y)].set_char(BLOCKS[frac_idx - 1]).set_fg(color);
|
||||
} else if let Some(pr) = peak_row {
|
||||
// peak_row is from top (0 = top), row is from bottom
|
||||
let from_top = area.height as usize - 1 - row;
|
||||
if from_top == pr {
|
||||
buf[(x, y)].set_char('─').set_fg(colors.meter.high);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
x_start += w as u16;
|
||||
}
|
||||
}
|
||||
|
||||
fn render_braille(
|
||||
data: &[f32; 32],
|
||||
area: Rect,
|
||||
buf: &mut Buffer,
|
||||
gain: f32,
|
||||
filled: bool,
|
||||
peaks: Option<&[f32; 32]>,
|
||||
) {
|
||||
let colors = theme::get();
|
||||
let width = area.width as usize;
|
||||
let height = area.height as usize;
|
||||
let fine_w = width * 2;
|
||||
let fine_h = height * 4;
|
||||
|
||||
PATTERNS.with(|p| {
|
||||
let mut patterns = p.borrow_mut();
|
||||
patterns.clear();
|
||||
patterns.resize(width * height, 0);
|
||||
|
||||
// Interpolate 32 bands across fine_w columns
|
||||
for fx in 0..fine_w {
|
||||
let band_f = fx as f32 * 31.0 / (fine_w - 1).max(1) as f32;
|
||||
let lo = band_f as usize;
|
||||
let hi = (lo + 1).min(31);
|
||||
let t = band_f - lo as f32;
|
||||
let mag = ((data[lo] * (1.0 - t) + data[hi] * t) * gain).min(1.0);
|
||||
let fy = ((1.0 - mag) * (fine_h - 1) as f32).round() as usize;
|
||||
let fy = fy.min(fine_h - 1);
|
||||
|
||||
if filled {
|
||||
for y in fy..fine_h {
|
||||
let cy = y / 4;
|
||||
let dy = y % 4;
|
||||
let cx = fx / 2;
|
||||
let dx = fx % 2;
|
||||
patterns[cy * width + cx] |= braille_bit(dx, dy);
|
||||
}
|
||||
} else {
|
||||
let cy = fy / 4;
|
||||
let dy = fy % 4;
|
||||
let cx = fx / 2;
|
||||
let dx = fx % 2;
|
||||
patterns[cy * width + cx] |= braille_bit(dx, dy);
|
||||
}
|
||||
|
||||
// Peak dots
|
||||
if let Some(pk) = peaks {
|
||||
let pv = (pk[lo] * (1.0 - t) + pk[hi] * t).min(1.0);
|
||||
let py = ((1.0 - pv) * (fine_h - 1) as f32).round() as usize;
|
||||
let py = py.min(fine_h - 1);
|
||||
let cy = py / 4;
|
||||
let dy = py % 4;
|
||||
let cx = fx / 2;
|
||||
let dx = fx % 2;
|
||||
patterns[cy * width + cx] |= braille_bit(dx, dy);
|
||||
}
|
||||
}
|
||||
|
||||
for cy in 0..height {
|
||||
for cx in 0..width {
|
||||
let pattern = patterns[cy * width + cx];
|
||||
if pattern != 0 {
|
||||
let ratio = 1.0 - (cy as f32 / height as f32);
|
||||
let color = band_color(ratio, &colors);
|
||||
let ch = char::from_u32(0x2800 + pattern as u32).unwrap_or(' ');
|
||||
buf[(area.x + cx as u16, area.y + cy as u16)]
|
||||
.set_char(ch)
|
||||
.set_fg(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn braille_bit(dot_x: usize, dot_y: usize) -> u8 {
|
||||
match (dot_x, dot_y) {
|
||||
(0, 0) => 0x01,
|
||||
(0, 1) => 0x02,
|
||||
(0, 2) => 0x04,
|
||||
(0, 3) => 0x40,
|
||||
(1, 0) => 0x08,
|
||||
(1, 1) => 0x10,
|
||||
(1, 2) => 0x20,
|
||||
(1, 3) => 0x80,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Single-line text input modal with optional hint.
|
||||
|
||||
use crate::theme;
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::{Color, Style};
|
||||
@@ -7,6 +9,7 @@ use ratatui::Frame;
|
||||
|
||||
use super::ModalFrame;
|
||||
|
||||
/// Modal dialog with a single-line text input.
|
||||
pub struct TextInputModal<'a> {
|
||||
title: &'a str,
|
||||
input: &'a str,
|
||||
@@ -41,7 +44,7 @@ impl<'a> TextInputModal<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn render_centered(self, frame: &mut Frame, term: Rect) {
|
||||
pub fn render_centered(self, frame: &mut Frame, term: Rect) -> Rect {
|
||||
let colors = theme::get();
|
||||
let border_color = self.border_color.unwrap_or(colors.ui.text_primary);
|
||||
let height = if self.hint.is_some() { 6 } else { 5 };
|
||||
@@ -81,5 +84,7 @@ impl<'a> TextInputModal<'a> {
|
||||
inner,
|
||||
);
|
||||
}
|
||||
|
||||
inner
|
||||
}
|
||||
}
|
||||
|
||||
277
crates/ratatui/src/theme/build.rs
Normal file
277
crates/ratatui/src/theme/build.rs
Normal file
@@ -0,0 +1,277 @@
|
||||
//! Derive [`ThemeColors`] from a [`Palette`].
|
||||
|
||||
use super::*;
|
||||
use super::palette::{Palette, Rgb, darken, mid, rgb, tint};
|
||||
|
||||
/// Build a complete [`ThemeColors`] from a [`Palette`].
|
||||
pub fn build(p: &Palette) -> ThemeColors {
|
||||
let darker_bg = darken(p.bg, 0.15);
|
||||
|
||||
ThemeColors {
|
||||
ui: UiColors {
|
||||
bg: rgb(p.bg),
|
||||
bg_rgb: p.bg,
|
||||
text_primary: rgb(p.fg),
|
||||
text_muted: rgb(p.fg_dim),
|
||||
text_dim: rgb(p.fg_muted),
|
||||
border: rgb(p.surface2),
|
||||
header: rgb(p.cyan),
|
||||
unfocused: rgb(p.fg_muted),
|
||||
accent: rgb(p.accent),
|
||||
surface: rgb(p.surface),
|
||||
},
|
||||
status: StatusColors {
|
||||
playing_bg: rgb(tint(p.bg, p.green, 0.25)),
|
||||
playing_fg: rgb(p.green),
|
||||
stopped_bg: rgb(tint(p.bg, p.red, 0.25)),
|
||||
stopped_fg: rgb(p.red),
|
||||
fill_on: rgb(p.green),
|
||||
fill_off: rgb(p.fg_muted),
|
||||
fill_bg: rgb(p.surface),
|
||||
},
|
||||
selection: SelectionColors {
|
||||
cursor_bg: rgb(p.accent),
|
||||
cursor_fg: rgb(p.bg),
|
||||
selected_bg: rgb(tint(p.bg, p.accent, 0.30)),
|
||||
selected_fg: rgb(p.accent),
|
||||
in_range_bg: rgb(tint(p.bg, p.accent, 0.20)),
|
||||
in_range_fg: rgb(p.fg),
|
||||
cursor: rgb(p.accent),
|
||||
selected: rgb(tint(p.bg, p.accent, 0.30)),
|
||||
in_range: rgb(tint(p.bg, p.accent, 0.20)),
|
||||
},
|
||||
tile: TileColors {
|
||||
playing_active_bg: rgb(tint(p.bg, p.orange, 0.35)),
|
||||
playing_active_fg: rgb(p.orange),
|
||||
playing_inactive_bg: rgb(tint(p.bg, p.yellow, 0.30)),
|
||||
playing_inactive_fg: rgb(p.yellow),
|
||||
active_bg: rgb(tint(p.bg, p.cyan, 0.25)),
|
||||
active_fg: rgb(p.cyan),
|
||||
content_bg: rgb(tint(p.bg, p.cyan, 0.30)),
|
||||
inactive_bg: rgb(p.surface),
|
||||
inactive_fg: rgb(p.fg_dim),
|
||||
active_selected_bg: rgb(tint(p.bg, p.accent, 0.35)),
|
||||
active_in_range_bg: rgb(tint(p.bg, p.accent, 0.22)),
|
||||
link_bright: p.link_bright,
|
||||
link_dim: p.link_dim,
|
||||
},
|
||||
header: HeaderColors {
|
||||
tempo_bg: rgb(tint(p.bg, p.tempo_color, 0.30)),
|
||||
tempo_fg: rgb(p.tempo_color),
|
||||
bank_bg: rgb(tint(p.bg, p.bank_color, 0.25)),
|
||||
bank_fg: rgb(p.bank_color),
|
||||
pattern_bg: rgb(tint(p.bg, p.pattern_color, 0.25)),
|
||||
pattern_fg: rgb(p.pattern_color),
|
||||
stats_bg: rgb(p.surface),
|
||||
stats_fg: rgb(p.fg_dim),
|
||||
},
|
||||
modal: ModalColors {
|
||||
border: rgb(p.cyan),
|
||||
border_accent: rgb(p.accent),
|
||||
border_warn: rgb(p.orange),
|
||||
border_dim: rgb(p.fg_muted),
|
||||
confirm: rgb(p.orange),
|
||||
rename: rgb(p.purple),
|
||||
input: rgb(p.cyan),
|
||||
editor: rgb(p.cyan),
|
||||
preview: rgb(p.fg_muted),
|
||||
},
|
||||
flash: FlashColors {
|
||||
error_bg: rgb(tint(p.bg, p.red, 0.30)),
|
||||
error_fg: rgb(p.red),
|
||||
success_bg: rgb(tint(p.bg, p.green, 0.25)),
|
||||
success_fg: rgb(p.green),
|
||||
info_bg: rgb(p.surface),
|
||||
info_fg: rgb(p.fg),
|
||||
},
|
||||
list: ListColors {
|
||||
playing_bg: rgb(tint(p.bg, p.green, 0.25)),
|
||||
playing_fg: rgb(p.green),
|
||||
staged_play_bg: rgb(tint(p.bg, p.purple, 0.30)),
|
||||
staged_play_fg: rgb(p.purple),
|
||||
staged_stop_bg: rgb(tint(p.bg, p.red, 0.30)),
|
||||
staged_stop_fg: rgb(p.red),
|
||||
edit_bg: rgb(tint(p.bg, p.cyan, 0.25)),
|
||||
edit_fg: rgb(p.cyan),
|
||||
hover_bg: rgb(p.surface2),
|
||||
hover_fg: rgb(p.fg),
|
||||
muted_bg: rgb(tint(p.bg, p.surface, 0.30)),
|
||||
muted_fg: rgb(p.fg_muted),
|
||||
soloed_bg: rgb(tint(p.bg, p.yellow, 0.30)),
|
||||
soloed_fg: rgb(p.yellow),
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: rgb(p.red),
|
||||
connected: rgb(p.green),
|
||||
listening: rgb(p.yellow),
|
||||
},
|
||||
syntax: syntax_colors(p, darker_bg),
|
||||
table: TableColors {
|
||||
row_even: rgb(darker_bg),
|
||||
row_odd: rgb(p.bg),
|
||||
},
|
||||
values: ValuesColors {
|
||||
tempo: rgb(p.orange),
|
||||
value: rgb(p.fg_dim),
|
||||
},
|
||||
hint: HintColors {
|
||||
key: rgb(p.orange),
|
||||
text: rgb(p.fg_muted),
|
||||
},
|
||||
view_badge: ViewBadgeColors {
|
||||
bg: rgb(p.fg),
|
||||
fg: rgb(p.bg),
|
||||
},
|
||||
nav: NavColors {
|
||||
selected_bg: rgb(tint(p.bg, p.accent, 0.35)),
|
||||
selected_fg: rgb(p.fg),
|
||||
unselected_bg: rgb(p.surface),
|
||||
unselected_fg: rgb(p.fg_muted),
|
||||
},
|
||||
editor_widget: EditorWidgetColors {
|
||||
cursor_bg: rgb(p.fg),
|
||||
cursor_fg: rgb(p.bg),
|
||||
selection_bg: rgb(tint(p.bg, p.accent, 0.30)),
|
||||
completion_bg: rgb(p.surface),
|
||||
completion_fg: rgb(p.fg),
|
||||
completion_selected: rgb(p.orange),
|
||||
completion_example: rgb(p.cyan),
|
||||
},
|
||||
browser: BrowserColors {
|
||||
directory: rgb(p.blue),
|
||||
project_file: rgb(p.purple),
|
||||
selected: rgb(p.orange),
|
||||
file: rgb(p.fg),
|
||||
focused_border: rgb(p.orange),
|
||||
unfocused_border: rgb(p.fg_muted),
|
||||
root: rgb(p.fg),
|
||||
file_icon: rgb(p.fg_muted),
|
||||
folder_icon: rgb(p.blue),
|
||||
empty_text: rgb(p.fg_muted),
|
||||
},
|
||||
input: InputColors {
|
||||
text: rgb(p.cyan),
|
||||
cursor: rgb(p.fg),
|
||||
hint: rgb(p.fg_muted),
|
||||
},
|
||||
search: SearchColors {
|
||||
active: rgb(p.orange),
|
||||
inactive: rgb(p.fg_muted),
|
||||
match_bg: rgb(p.yellow),
|
||||
match_fg: rgb(p.bg),
|
||||
},
|
||||
markdown: MarkdownColors {
|
||||
h1: rgb(p.cyan),
|
||||
h2: rgb(p.orange),
|
||||
h3: rgb(p.purple),
|
||||
code: rgb(p.green),
|
||||
code_border: rgb(mid(p.surface2, p.fg_muted, 0.3)),
|
||||
link: rgb(p.accent),
|
||||
link_url: rgb(mid(p.fg_muted, p.fg_dim, 0.3)),
|
||||
quote: rgb(p.fg_muted),
|
||||
text: rgb(p.fg),
|
||||
list: rgb(p.fg),
|
||||
},
|
||||
engine: engine_colors(p),
|
||||
dict: dict_colors(p),
|
||||
title: TitleColors {
|
||||
big_title: rgb(p.title_accent),
|
||||
author: rgb(p.title_author),
|
||||
link: rgb(p.green),
|
||||
license: rgb(p.orange),
|
||||
prompt: rgb(mid(p.fg_dim, p.fg, 0.3)),
|
||||
subtitle: rgb(p.fg),
|
||||
},
|
||||
meter: MeterColors {
|
||||
low: rgb(p.green),
|
||||
mid: rgb(p.yellow),
|
||||
high: rgb(p.red),
|
||||
low_rgb: p.meter[0],
|
||||
mid_rgb: p.meter[1],
|
||||
high_rgb: p.meter[2],
|
||||
},
|
||||
sparkle: SparkleColors {
|
||||
colors: p.sparkle,
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: rgb(p.orange),
|
||||
button_selected_bg: rgb(p.orange),
|
||||
button_selected_fg: rgb(p.bg),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn syntax_colors(p: &Palette, darker_bg: Rgb) -> SyntaxColors {
|
||||
let syn_bg = |accent: Rgb| -> Color { rgb(tint(p.bg, accent, 0.20)) };
|
||||
let interval_fg = mid(p.green, p.fg, 0.3);
|
||||
|
||||
SyntaxColors {
|
||||
gap_bg: rgb(darker_bg),
|
||||
executed_bg: rgb(tint(p.bg, p.purple, 0.15)),
|
||||
selected_bg: rgb(tint(p.bg, p.orange, 0.30)),
|
||||
emit: (rgb(p.fg), syn_bg(p.accent)),
|
||||
number: (rgb(p.purple), syn_bg(p.purple)),
|
||||
string: (rgb(p.green), syn_bg(p.green)),
|
||||
comment: (rgb(p.fg_muted), rgb(darker_bg)),
|
||||
keyword: (rgb(p.accent), syn_bg(p.accent)),
|
||||
stack_op: (rgb(p.blue), syn_bg(p.blue)),
|
||||
operator: (rgb(p.red), syn_bg(p.red)),
|
||||
sound: (rgb(p.cyan), syn_bg(p.cyan)),
|
||||
param: (rgb(p.orange), syn_bg(p.orange)),
|
||||
context: (rgb(p.orange), syn_bg(p.orange)),
|
||||
note: (rgb(p.green), syn_bg(p.green)),
|
||||
interval: (rgb(interval_fg), syn_bg(p.green)),
|
||||
variable: (rgb(p.purple), syn_bg(p.purple)),
|
||||
vary: (rgb(p.yellow), syn_bg(p.yellow)),
|
||||
generator: (rgb(p.cyan), syn_bg(p.cyan)),
|
||||
user_defined: (rgb(p.secondary), syn_bg(p.secondary)),
|
||||
default: (rgb(p.fg_dim), rgb(darker_bg)),
|
||||
}
|
||||
}
|
||||
|
||||
fn engine_colors(p: &Palette) -> EngineColors {
|
||||
let divider = mid(p.surface2, p.fg_muted, 0.2);
|
||||
let label = mid(p.fg_muted, p.fg, 0.4);
|
||||
|
||||
EngineColors {
|
||||
header: rgb(p.cyan),
|
||||
header_focused: rgb(p.yellow),
|
||||
divider: rgb(divider),
|
||||
scroll_indicator: rgb(mid(divider, p.fg_muted, 0.3)),
|
||||
label: rgb(label),
|
||||
label_focused: rgb(mid(label, p.fg, 0.3)),
|
||||
label_dim: rgb(mid(p.fg_muted, label, 0.3)),
|
||||
value: rgb(mid(p.fg_dim, p.fg, 0.5)),
|
||||
focused: rgb(p.yellow),
|
||||
normal: rgb(p.fg),
|
||||
dim: rgb(mid(divider, p.fg_muted, 0.3)),
|
||||
path: rgb(label),
|
||||
border_magenta: rgb(p.purple),
|
||||
border_green: rgb(p.green),
|
||||
border_cyan: rgb(p.cyan),
|
||||
separator: rgb(divider),
|
||||
hint_active: rgb(mid(p.orange, p.yellow, 0.5)),
|
||||
hint_inactive: rgb(divider),
|
||||
}
|
||||
}
|
||||
|
||||
fn dict_colors(p: &Palette) -> DictColors {
|
||||
let divider = mid(p.surface2, p.fg_muted, 0.2);
|
||||
let label = mid(p.fg_muted, p.fg, 0.4);
|
||||
|
||||
DictColors {
|
||||
word_name: rgb(p.green),
|
||||
word_bg: rgb(tint(p.bg, p.cyan, 0.20)),
|
||||
alias: rgb(p.fg_muted),
|
||||
stack_sig: rgb(p.purple),
|
||||
description: rgb(p.fg),
|
||||
example: rgb(label),
|
||||
category_focused: rgb(p.yellow),
|
||||
category_selected: rgb(p.cyan),
|
||||
category_normal: rgb(p.fg),
|
||||
category_dimmed: rgb(mid(divider, p.fg_muted, 0.3)),
|
||||
border_focused: rgb(p.yellow),
|
||||
border_normal: rgb(divider),
|
||||
header_desc: rgb(mid(label, p.fg, 0.3)),
|
||||
}
|
||||
}
|
||||
@@ -1,282 +1,41 @@
|
||||
use super::*;
|
||||
use ratatui::style::Color;
|
||||
//! Catppuccin Latte palette.
|
||||
|
||||
pub fn theme() -> ThemeColors {
|
||||
let crust = Color::Rgb(220, 224, 232);
|
||||
let mantle = Color::Rgb(230, 233, 239);
|
||||
let base = Color::Rgb(239, 241, 245);
|
||||
let surface0 = Color::Rgb(204, 208, 218);
|
||||
let surface1 = Color::Rgb(188, 192, 204);
|
||||
let overlay0 = Color::Rgb(156, 160, 176);
|
||||
let overlay1 = Color::Rgb(140, 143, 161);
|
||||
let subtext0 = Color::Rgb(108, 111, 133);
|
||||
let subtext1 = Color::Rgb(92, 95, 119);
|
||||
let text = Color::Rgb(76, 79, 105);
|
||||
let pink = Color::Rgb(234, 118, 203);
|
||||
let mauve = Color::Rgb(136, 57, 239);
|
||||
let red = Color::Rgb(210, 15, 57);
|
||||
let maroon = Color::Rgb(230, 69, 83);
|
||||
let peach = Color::Rgb(254, 100, 11);
|
||||
let yellow = Color::Rgb(223, 142, 29);
|
||||
let green = Color::Rgb(64, 160, 43);
|
||||
let teal = Color::Rgb(23, 146, 153);
|
||||
let sapphire = Color::Rgb(32, 159, 181);
|
||||
let lavender = Color::Rgb(114, 135, 253);
|
||||
use super::palette::Palette;
|
||||
|
||||
ThemeColors {
|
||||
ui: UiColors {
|
||||
bg: base,
|
||||
bg_rgb: (239, 241, 245),
|
||||
text_primary: text,
|
||||
text_muted: subtext0,
|
||||
text_dim: overlay1,
|
||||
border: surface1,
|
||||
header: lavender,
|
||||
unfocused: overlay0,
|
||||
accent: mauve,
|
||||
surface: surface0,
|
||||
},
|
||||
status: StatusColors {
|
||||
playing_bg: Color::Rgb(220, 240, 225),
|
||||
playing_fg: green,
|
||||
stopped_bg: Color::Rgb(245, 220, 225),
|
||||
stopped_fg: red,
|
||||
fill_on: green,
|
||||
fill_off: overlay0,
|
||||
fill_bg: surface0,
|
||||
},
|
||||
selection: SelectionColors {
|
||||
cursor_bg: mauve,
|
||||
cursor_fg: base,
|
||||
selected_bg: Color::Rgb(200, 200, 230),
|
||||
selected_fg: lavender,
|
||||
in_range_bg: Color::Rgb(210, 210, 235),
|
||||
in_range_fg: subtext1,
|
||||
cursor: mauve,
|
||||
selected: Color::Rgb(200, 200, 230),
|
||||
in_range: Color::Rgb(210, 210, 235),
|
||||
},
|
||||
tile: TileColors {
|
||||
playing_active_bg: Color::Rgb(250, 220, 210),
|
||||
playing_active_fg: peach,
|
||||
playing_inactive_bg: Color::Rgb(250, 235, 200),
|
||||
playing_inactive_fg: yellow,
|
||||
active_bg: Color::Rgb(200, 235, 235),
|
||||
active_fg: teal,
|
||||
inactive_bg: surface0,
|
||||
inactive_fg: subtext0,
|
||||
active_selected_bg: Color::Rgb(215, 210, 240),
|
||||
active_in_range_bg: Color::Rgb(210, 215, 230),
|
||||
pub fn palette() -> Palette {
|
||||
Palette {
|
||||
bg: (239, 241, 245),
|
||||
surface: (204, 208, 218),
|
||||
surface2: (188, 192, 204),
|
||||
fg: (76, 79, 105),
|
||||
fg_dim: (108, 111, 133),
|
||||
fg_muted: (140, 143, 161),
|
||||
accent: (136, 57, 239), // mauve
|
||||
red: (210, 15, 57),
|
||||
green: (64, 160, 43),
|
||||
yellow: (223, 142, 29),
|
||||
blue: (32, 159, 181), // sapphire
|
||||
purple: (136, 57, 239),
|
||||
cyan: (23, 146, 153), // teal
|
||||
orange: (254, 100, 11), // peach
|
||||
tempo_color: (136, 57, 239),
|
||||
bank_color: (32, 159, 181),
|
||||
pattern_color: (23, 146, 153),
|
||||
title_accent: (136, 57, 239),
|
||||
title_author: (114, 135, 253),
|
||||
secondary: (230, 69, 83), // maroon
|
||||
link_bright: [
|
||||
(136, 57, 239),
|
||||
(234, 118, 203),
|
||||
(254, 100, 11),
|
||||
(4, 165, 229),
|
||||
(64, 160, 43),
|
||||
(136, 57, 239), (234, 118, 203), (254, 100, 11),
|
||||
(4, 165, 229), (64, 160, 43),
|
||||
],
|
||||
link_dim: [
|
||||
(210, 200, 240),
|
||||
(240, 210, 230),
|
||||
(250, 220, 200),
|
||||
(200, 230, 240),
|
||||
(210, 235, 210),
|
||||
(210, 200, 240), (240, 210, 230), (250, 220, 200),
|
||||
(200, 230, 240), (210, 235, 210),
|
||||
],
|
||||
},
|
||||
header: HeaderColors {
|
||||
tempo_bg: Color::Rgb(220, 210, 240),
|
||||
tempo_fg: mauve,
|
||||
bank_bg: Color::Rgb(200, 230, 235),
|
||||
bank_fg: sapphire,
|
||||
pattern_bg: Color::Rgb(200, 230, 225),
|
||||
pattern_fg: teal,
|
||||
stats_bg: surface0,
|
||||
stats_fg: subtext0,
|
||||
},
|
||||
modal: ModalColors {
|
||||
border: lavender,
|
||||
border_accent: mauve,
|
||||
border_warn: peach,
|
||||
border_dim: overlay1,
|
||||
confirm: peach,
|
||||
rename: mauve,
|
||||
input: sapphire,
|
||||
editor: lavender,
|
||||
preview: overlay1,
|
||||
},
|
||||
flash: FlashColors {
|
||||
error_bg: Color::Rgb(250, 215, 220),
|
||||
error_fg: red,
|
||||
success_bg: Color::Rgb(210, 240, 215),
|
||||
success_fg: green,
|
||||
info_bg: surface0,
|
||||
info_fg: text,
|
||||
event_rgb: (225, 215, 240),
|
||||
},
|
||||
list: ListColors {
|
||||
playing_bg: Color::Rgb(210, 235, 220),
|
||||
playing_fg: green,
|
||||
staged_play_bg: Color::Rgb(225, 215, 245),
|
||||
staged_play_fg: mauve,
|
||||
staged_stop_bg: Color::Rgb(245, 215, 225),
|
||||
staged_stop_fg: maroon,
|
||||
edit_bg: Color::Rgb(210, 235, 235),
|
||||
edit_fg: teal,
|
||||
hover_bg: surface1,
|
||||
hover_fg: text,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: red,
|
||||
connected: green,
|
||||
listening: yellow,
|
||||
},
|
||||
syntax: SyntaxColors {
|
||||
gap_bg: mantle,
|
||||
executed_bg: Color::Rgb(225, 220, 240),
|
||||
selected_bg: Color::Rgb(250, 235, 210),
|
||||
emit: (text, Color::Rgb(250, 220, 215)),
|
||||
number: (peach, Color::Rgb(252, 235, 220)),
|
||||
string: (green, Color::Rgb(215, 240, 215)),
|
||||
comment: (overlay1, crust),
|
||||
keyword: (mauve, Color::Rgb(230, 220, 245)),
|
||||
stack_op: (sapphire, Color::Rgb(215, 230, 240)),
|
||||
operator: (yellow, Color::Rgb(245, 235, 210)),
|
||||
sound: (teal, Color::Rgb(210, 240, 240)),
|
||||
param: (lavender, Color::Rgb(220, 225, 245)),
|
||||
context: (peach, Color::Rgb(252, 235, 220)),
|
||||
note: (green, Color::Rgb(215, 240, 215)),
|
||||
interval: (Color::Rgb(50, 140, 30), Color::Rgb(215, 240, 210)),
|
||||
variable: (pink, Color::Rgb(245, 220, 240)),
|
||||
vary: (yellow, Color::Rgb(245, 235, 210)),
|
||||
generator: (teal, Color::Rgb(210, 240, 235)),
|
||||
default: (subtext0, mantle),
|
||||
},
|
||||
table: TableColors {
|
||||
row_even: mantle,
|
||||
row_odd: base,
|
||||
},
|
||||
values: ValuesColors {
|
||||
tempo: peach,
|
||||
value: subtext0,
|
||||
},
|
||||
hint: HintColors {
|
||||
key: peach,
|
||||
text: overlay1,
|
||||
},
|
||||
view_badge: ViewBadgeColors { bg: text, fg: base },
|
||||
nav: NavColors {
|
||||
selected_bg: Color::Rgb(215, 205, 245),
|
||||
selected_fg: text,
|
||||
unselected_bg: surface0,
|
||||
unselected_fg: overlay1,
|
||||
},
|
||||
editor_widget: EditorWidgetColors {
|
||||
cursor_bg: text,
|
||||
cursor_fg: base,
|
||||
selection_bg: Color::Rgb(200, 210, 240),
|
||||
completion_bg: surface0,
|
||||
completion_fg: text,
|
||||
completion_selected: peach,
|
||||
completion_example: teal,
|
||||
},
|
||||
browser: BrowserColors {
|
||||
directory: sapphire,
|
||||
project_file: mauve,
|
||||
selected: peach,
|
||||
file: text,
|
||||
focused_border: peach,
|
||||
unfocused_border: overlay0,
|
||||
root: text,
|
||||
file_icon: overlay1,
|
||||
folder_icon: sapphire,
|
||||
empty_text: overlay1,
|
||||
},
|
||||
input: InputColors {
|
||||
text: sapphire,
|
||||
cursor: text,
|
||||
hint: overlay1,
|
||||
},
|
||||
search: SearchColors {
|
||||
active: peach,
|
||||
inactive: overlay0,
|
||||
match_bg: yellow,
|
||||
match_fg: base,
|
||||
},
|
||||
markdown: MarkdownColors {
|
||||
h1: sapphire,
|
||||
h2: peach,
|
||||
h3: mauve,
|
||||
code: green,
|
||||
code_border: Color::Rgb(190, 195, 205),
|
||||
link: teal,
|
||||
link_url: Color::Rgb(150, 150, 150),
|
||||
quote: overlay1,
|
||||
text,
|
||||
list: text,
|
||||
},
|
||||
engine: EngineColors {
|
||||
header: Color::Rgb(30, 120, 150),
|
||||
header_focused: yellow,
|
||||
divider: Color::Rgb(180, 185, 195),
|
||||
scroll_indicator: Color::Rgb(160, 165, 175),
|
||||
label: Color::Rgb(100, 105, 120),
|
||||
label_focused: Color::Rgb(70, 75, 90),
|
||||
label_dim: Color::Rgb(120, 125, 140),
|
||||
value: Color::Rgb(60, 65, 80),
|
||||
focused: yellow,
|
||||
normal: text,
|
||||
dim: Color::Rgb(160, 165, 175),
|
||||
path: Color::Rgb(100, 105, 120),
|
||||
border_magenta: mauve,
|
||||
border_green: green,
|
||||
border_cyan: sapphire,
|
||||
separator: Color::Rgb(180, 185, 200),
|
||||
hint_active: Color::Rgb(180, 140, 40),
|
||||
hint_inactive: Color::Rgb(190, 195, 205),
|
||||
},
|
||||
dict: DictColors {
|
||||
word_name: green,
|
||||
word_bg: Color::Rgb(210, 225, 235),
|
||||
alias: overlay1,
|
||||
stack_sig: mauve,
|
||||
description: text,
|
||||
example: Color::Rgb(100, 105, 115),
|
||||
category_focused: yellow,
|
||||
category_selected: sapphire,
|
||||
category_normal: text,
|
||||
category_dimmed: Color::Rgb(160, 165, 175),
|
||||
border_focused: yellow,
|
||||
border_normal: Color::Rgb(180, 185, 195),
|
||||
header_desc: Color::Rgb(90, 95, 110),
|
||||
},
|
||||
title: TitleColors {
|
||||
big_title: mauve,
|
||||
author: lavender,
|
||||
link: teal,
|
||||
license: peach,
|
||||
prompt: Color::Rgb(90, 100, 115),
|
||||
subtitle: text,
|
||||
},
|
||||
meter: MeterColors {
|
||||
low: green,
|
||||
mid: yellow,
|
||||
high: red,
|
||||
low_rgb: (50, 150, 40),
|
||||
mid_rgb: (200, 140, 30),
|
||||
high_rgb: (200, 40, 50),
|
||||
},
|
||||
sparkle: SparkleColors {
|
||||
colors: [
|
||||
(114, 135, 253),
|
||||
(254, 100, 11),
|
||||
(64, 160, 43),
|
||||
(234, 118, 203),
|
||||
(136, 57, 239),
|
||||
sparkle: [
|
||||
(114, 135, 253), (254, 100, 11), (64, 160, 43),
|
||||
(234, 118, 203), (136, 57, 239),
|
||||
],
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: peach,
|
||||
button_selected_bg: peach,
|
||||
button_selected_fg: base,
|
||||
},
|
||||
meter: [(50, 150, 40), (200, 140, 30), (200, 40, 50)],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,285 +1,41 @@
|
||||
use super::*;
|
||||
use ratatui::style::Color;
|
||||
//! Catppuccin Mocha palette.
|
||||
|
||||
pub fn theme() -> ThemeColors {
|
||||
let crust = Color::Rgb(17, 17, 27);
|
||||
let mantle = Color::Rgb(24, 24, 37);
|
||||
let base = Color::Rgb(30, 30, 46);
|
||||
let surface0 = Color::Rgb(49, 50, 68);
|
||||
let surface1 = Color::Rgb(69, 71, 90);
|
||||
let overlay0 = Color::Rgb(108, 112, 134);
|
||||
let overlay1 = Color::Rgb(127, 132, 156);
|
||||
let subtext0 = Color::Rgb(166, 173, 200);
|
||||
let subtext1 = Color::Rgb(186, 194, 222);
|
||||
let text = Color::Rgb(205, 214, 244);
|
||||
let pink = Color::Rgb(245, 194, 231);
|
||||
let mauve = Color::Rgb(203, 166, 247);
|
||||
let red = Color::Rgb(243, 139, 168);
|
||||
let maroon = Color::Rgb(235, 160, 172);
|
||||
let peach = Color::Rgb(250, 179, 135);
|
||||
let yellow = Color::Rgb(249, 226, 175);
|
||||
let green = Color::Rgb(166, 227, 161);
|
||||
let teal = Color::Rgb(148, 226, 213);
|
||||
let sapphire = Color::Rgb(116, 199, 236);
|
||||
let lavender = Color::Rgb(180, 190, 254);
|
||||
use super::palette::Palette;
|
||||
|
||||
ThemeColors {
|
||||
ui: UiColors {
|
||||
bg: base,
|
||||
bg_rgb: (30, 30, 46),
|
||||
text_primary: text,
|
||||
text_muted: subtext0,
|
||||
text_dim: overlay1,
|
||||
border: surface1,
|
||||
header: lavender,
|
||||
unfocused: overlay0,
|
||||
accent: mauve,
|
||||
surface: surface0,
|
||||
},
|
||||
status: StatusColors {
|
||||
playing_bg: Color::Rgb(30, 50, 40),
|
||||
playing_fg: green,
|
||||
stopped_bg: Color::Rgb(50, 30, 40),
|
||||
stopped_fg: red,
|
||||
fill_on: green,
|
||||
fill_off: overlay0,
|
||||
fill_bg: surface0,
|
||||
},
|
||||
selection: SelectionColors {
|
||||
cursor_bg: mauve,
|
||||
cursor_fg: crust,
|
||||
selected_bg: Color::Rgb(60, 60, 90),
|
||||
selected_fg: lavender,
|
||||
in_range_bg: Color::Rgb(50, 50, 75),
|
||||
in_range_fg: subtext1,
|
||||
cursor: mauve,
|
||||
selected: Color::Rgb(60, 60, 90),
|
||||
in_range: Color::Rgb(50, 50, 75),
|
||||
},
|
||||
tile: TileColors {
|
||||
playing_active_bg: Color::Rgb(80, 50, 60),
|
||||
playing_active_fg: peach,
|
||||
playing_inactive_bg: Color::Rgb(70, 55, 45),
|
||||
playing_inactive_fg: yellow,
|
||||
active_bg: Color::Rgb(40, 55, 55),
|
||||
active_fg: teal,
|
||||
inactive_bg: surface0,
|
||||
inactive_fg: subtext0,
|
||||
active_selected_bg: Color::Rgb(70, 60, 80),
|
||||
active_in_range_bg: Color::Rgb(55, 55, 70),
|
||||
pub fn palette() -> Palette {
|
||||
Palette {
|
||||
bg: (30, 30, 46),
|
||||
surface: (49, 50, 68),
|
||||
surface2: (69, 71, 90),
|
||||
fg: (205, 214, 244),
|
||||
fg_dim: (166, 173, 200),
|
||||
fg_muted: (127, 132, 156),
|
||||
accent: (203, 166, 247), // mauve
|
||||
red: (243, 139, 168),
|
||||
green: (166, 227, 161),
|
||||
yellow: (249, 226, 175),
|
||||
blue: (116, 199, 236), // sapphire
|
||||
purple: (203, 166, 247), // mauve
|
||||
cyan: (148, 226, 213), // teal
|
||||
orange: (250, 179, 135), // peach
|
||||
tempo_color: (203, 166, 247),
|
||||
bank_color: (116, 199, 236),
|
||||
pattern_color: (148, 226, 213),
|
||||
title_accent: (203, 166, 247),
|
||||
title_author: (180, 190, 254),
|
||||
secondary: (235, 160, 172), // maroon
|
||||
link_bright: [
|
||||
(203, 166, 247),
|
||||
(245, 194, 231),
|
||||
(250, 179, 135),
|
||||
(137, 220, 235),
|
||||
(166, 227, 161),
|
||||
(203, 166, 247), (245, 194, 231), (250, 179, 135),
|
||||
(137, 220, 235), (166, 227, 161),
|
||||
],
|
||||
link_dim: [
|
||||
(70, 55, 85),
|
||||
(85, 65, 80),
|
||||
(85, 60, 45),
|
||||
(45, 75, 80),
|
||||
(55, 80, 55),
|
||||
(70, 55, 85), (85, 65, 80), (85, 60, 45),
|
||||
(45, 75, 80), (55, 80, 55),
|
||||
],
|
||||
},
|
||||
header: HeaderColors {
|
||||
tempo_bg: Color::Rgb(50, 40, 60),
|
||||
tempo_fg: mauve,
|
||||
bank_bg: Color::Rgb(35, 50, 55),
|
||||
bank_fg: sapphire,
|
||||
pattern_bg: Color::Rgb(40, 50, 50),
|
||||
pattern_fg: teal,
|
||||
stats_bg: surface0,
|
||||
stats_fg: subtext0,
|
||||
},
|
||||
modal: ModalColors {
|
||||
border: lavender,
|
||||
border_accent: mauve,
|
||||
border_warn: peach,
|
||||
border_dim: overlay1,
|
||||
confirm: peach,
|
||||
rename: mauve,
|
||||
input: sapphire,
|
||||
editor: lavender,
|
||||
preview: overlay1,
|
||||
},
|
||||
flash: FlashColors {
|
||||
error_bg: Color::Rgb(50, 30, 40),
|
||||
error_fg: red,
|
||||
success_bg: Color::Rgb(30, 50, 40),
|
||||
success_fg: green,
|
||||
info_bg: surface0,
|
||||
info_fg: text,
|
||||
event_rgb: (55, 45, 70),
|
||||
},
|
||||
list: ListColors {
|
||||
playing_bg: Color::Rgb(35, 55, 45),
|
||||
playing_fg: green,
|
||||
staged_play_bg: Color::Rgb(55, 45, 65),
|
||||
staged_play_fg: mauve,
|
||||
staged_stop_bg: Color::Rgb(60, 40, 50),
|
||||
staged_stop_fg: maroon,
|
||||
edit_bg: Color::Rgb(40, 55, 55),
|
||||
edit_fg: teal,
|
||||
hover_bg: surface1,
|
||||
hover_fg: text,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: red,
|
||||
connected: green,
|
||||
listening: yellow,
|
||||
},
|
||||
syntax: SyntaxColors {
|
||||
gap_bg: mantle,
|
||||
executed_bg: Color::Rgb(45, 40, 55),
|
||||
selected_bg: Color::Rgb(70, 55, 40),
|
||||
emit: (text, Color::Rgb(80, 50, 60)),
|
||||
number: (peach, Color::Rgb(55, 45, 35)),
|
||||
string: (green, Color::Rgb(35, 50, 40)),
|
||||
comment: (overlay1, crust),
|
||||
keyword: (mauve, Color::Rgb(50, 40, 60)),
|
||||
stack_op: (sapphire, Color::Rgb(35, 45, 55)),
|
||||
operator: (yellow, Color::Rgb(55, 50, 35)),
|
||||
sound: (teal, Color::Rgb(35, 55, 55)),
|
||||
param: (lavender, Color::Rgb(45, 45, 60)),
|
||||
context: (peach, Color::Rgb(55, 45, 35)),
|
||||
note: (green, Color::Rgb(35, 50, 40)),
|
||||
interval: (Color::Rgb(180, 230, 150), Color::Rgb(40, 55, 35)),
|
||||
variable: (pink, Color::Rgb(55, 40, 55)),
|
||||
vary: (yellow, Color::Rgb(55, 50, 35)),
|
||||
generator: (teal, Color::Rgb(35, 55, 50)),
|
||||
default: (subtext0, mantle),
|
||||
},
|
||||
table: TableColors {
|
||||
row_even: mantle,
|
||||
row_odd: base,
|
||||
},
|
||||
values: ValuesColors {
|
||||
tempo: peach,
|
||||
value: subtext0,
|
||||
},
|
||||
hint: HintColors {
|
||||
key: peach,
|
||||
text: overlay1,
|
||||
},
|
||||
view_badge: ViewBadgeColors {
|
||||
bg: text,
|
||||
fg: crust,
|
||||
},
|
||||
nav: NavColors {
|
||||
selected_bg: Color::Rgb(60, 50, 75),
|
||||
selected_fg: text,
|
||||
unselected_bg: surface0,
|
||||
unselected_fg: overlay1,
|
||||
},
|
||||
editor_widget: EditorWidgetColors {
|
||||
cursor_bg: text,
|
||||
cursor_fg: crust,
|
||||
selection_bg: Color::Rgb(50, 60, 90),
|
||||
completion_bg: surface0,
|
||||
completion_fg: text,
|
||||
completion_selected: peach,
|
||||
completion_example: teal,
|
||||
},
|
||||
browser: BrowserColors {
|
||||
directory: sapphire,
|
||||
project_file: mauve,
|
||||
selected: peach,
|
||||
file: text,
|
||||
focused_border: peach,
|
||||
unfocused_border: overlay0,
|
||||
root: text,
|
||||
file_icon: overlay1,
|
||||
folder_icon: sapphire,
|
||||
empty_text: overlay1,
|
||||
},
|
||||
input: InputColors {
|
||||
text: sapphire,
|
||||
cursor: text,
|
||||
hint: overlay1,
|
||||
},
|
||||
search: SearchColors {
|
||||
active: peach,
|
||||
inactive: overlay0,
|
||||
match_bg: yellow,
|
||||
match_fg: crust,
|
||||
},
|
||||
markdown: MarkdownColors {
|
||||
h1: sapphire,
|
||||
h2: peach,
|
||||
h3: mauve,
|
||||
code: green,
|
||||
code_border: Color::Rgb(60, 60, 70),
|
||||
link: teal,
|
||||
link_url: Color::Rgb(100, 100, 100),
|
||||
quote: overlay1,
|
||||
text,
|
||||
list: text,
|
||||
},
|
||||
engine: EngineColors {
|
||||
header: Color::Rgb(100, 160, 180),
|
||||
header_focused: yellow,
|
||||
divider: Color::Rgb(60, 65, 70),
|
||||
scroll_indicator: Color::Rgb(80, 85, 95),
|
||||
label: Color::Rgb(120, 125, 135),
|
||||
label_focused: Color::Rgb(150, 155, 165),
|
||||
label_dim: Color::Rgb(100, 105, 115),
|
||||
value: Color::Rgb(180, 180, 190),
|
||||
focused: yellow,
|
||||
normal: text,
|
||||
dim: Color::Rgb(80, 85, 95),
|
||||
path: Color::Rgb(120, 125, 135),
|
||||
border_magenta: mauve,
|
||||
border_green: green,
|
||||
border_cyan: sapphire,
|
||||
separator: Color::Rgb(60, 65, 75),
|
||||
hint_active: Color::Rgb(180, 180, 100),
|
||||
hint_inactive: Color::Rgb(60, 60, 70),
|
||||
},
|
||||
dict: DictColors {
|
||||
word_name: green,
|
||||
word_bg: Color::Rgb(40, 50, 60),
|
||||
alias: overlay1,
|
||||
stack_sig: mauve,
|
||||
description: text,
|
||||
example: Color::Rgb(120, 130, 140),
|
||||
category_focused: yellow,
|
||||
category_selected: sapphire,
|
||||
category_normal: text,
|
||||
category_dimmed: Color::Rgb(80, 80, 90),
|
||||
border_focused: yellow,
|
||||
border_normal: Color::Rgb(60, 60, 70),
|
||||
header_desc: Color::Rgb(140, 145, 155),
|
||||
},
|
||||
title: TitleColors {
|
||||
big_title: mauve,
|
||||
author: lavender,
|
||||
link: teal,
|
||||
license: peach,
|
||||
prompt: Color::Rgb(140, 160, 170),
|
||||
subtitle: text,
|
||||
},
|
||||
meter: MeterColors {
|
||||
low: green,
|
||||
mid: yellow,
|
||||
high: red,
|
||||
low_rgb: (40, 180, 80),
|
||||
mid_rgb: (220, 180, 40),
|
||||
high_rgb: (220, 60, 40),
|
||||
},
|
||||
sparkle: SparkleColors {
|
||||
colors: [
|
||||
(200, 220, 255),
|
||||
(250, 179, 135),
|
||||
(166, 227, 161),
|
||||
(245, 194, 231),
|
||||
(203, 166, 247),
|
||||
sparkle: [
|
||||
(200, 220, 255), (250, 179, 135), (166, 227, 161),
|
||||
(245, 194, 231), (203, 166, 247),
|
||||
],
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: peach,
|
||||
button_selected_bg: peach,
|
||||
button_selected_fg: crust,
|
||||
},
|
||||
meter: [(40, 180, 80), (220, 180, 40), (220, 60, 40)],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,279 +1,41 @@
|
||||
use super::*;
|
||||
use ratatui::style::Color;
|
||||
//! Dracula palette.
|
||||
|
||||
pub fn theme() -> ThemeColors {
|
||||
let background = Color::Rgb(40, 42, 54);
|
||||
let current_line = Color::Rgb(68, 71, 90);
|
||||
let foreground = Color::Rgb(248, 248, 242);
|
||||
let comment = Color::Rgb(98, 114, 164);
|
||||
let cyan = Color::Rgb(139, 233, 253);
|
||||
let green = Color::Rgb(80, 250, 123);
|
||||
let orange = Color::Rgb(255, 184, 108);
|
||||
let pink = Color::Rgb(255, 121, 198);
|
||||
let purple = Color::Rgb(189, 147, 249);
|
||||
let red = Color::Rgb(255, 85, 85);
|
||||
let yellow = Color::Rgb(241, 250, 140);
|
||||
use super::palette::Palette;
|
||||
|
||||
let darker_bg = Color::Rgb(33, 34, 44);
|
||||
let lighter_bg = Color::Rgb(55, 57, 70);
|
||||
|
||||
ThemeColors {
|
||||
ui: UiColors {
|
||||
bg: background,
|
||||
bg_rgb: (40, 42, 54),
|
||||
text_primary: foreground,
|
||||
text_muted: comment,
|
||||
text_dim: Color::Rgb(80, 85, 110),
|
||||
border: current_line,
|
||||
header: purple,
|
||||
unfocused: comment,
|
||||
accent: purple,
|
||||
surface: current_line,
|
||||
},
|
||||
status: StatusColors {
|
||||
playing_bg: Color::Rgb(40, 60, 50),
|
||||
playing_fg: green,
|
||||
stopped_bg: Color::Rgb(65, 45, 50),
|
||||
stopped_fg: red,
|
||||
fill_on: green,
|
||||
fill_off: comment,
|
||||
fill_bg: current_line,
|
||||
},
|
||||
selection: SelectionColors {
|
||||
cursor_bg: purple,
|
||||
cursor_fg: background,
|
||||
selected_bg: Color::Rgb(80, 75, 110),
|
||||
selected_fg: purple,
|
||||
in_range_bg: Color::Rgb(65, 65, 90),
|
||||
in_range_fg: foreground,
|
||||
cursor: purple,
|
||||
selected: Color::Rgb(80, 75, 110),
|
||||
in_range: Color::Rgb(65, 65, 90),
|
||||
},
|
||||
tile: TileColors {
|
||||
playing_active_bg: Color::Rgb(85, 60, 65),
|
||||
playing_active_fg: orange,
|
||||
playing_inactive_bg: Color::Rgb(80, 75, 55),
|
||||
playing_inactive_fg: yellow,
|
||||
active_bg: Color::Rgb(50, 70, 70),
|
||||
active_fg: cyan,
|
||||
inactive_bg: current_line,
|
||||
inactive_fg: comment,
|
||||
active_selected_bg: Color::Rgb(80, 70, 95),
|
||||
active_in_range_bg: Color::Rgb(65, 65, 85),
|
||||
pub fn palette() -> Palette {
|
||||
Palette {
|
||||
bg: (40, 42, 54),
|
||||
surface: (68, 71, 90),
|
||||
surface2: (55, 57, 70),
|
||||
fg: (248, 248, 242),
|
||||
fg_dim: (98, 114, 164),
|
||||
fg_muted: (80, 85, 110),
|
||||
accent: (189, 147, 249), // purple
|
||||
red: (255, 85, 85),
|
||||
green: (80, 250, 123),
|
||||
yellow: (241, 250, 140),
|
||||
blue: (139, 233, 253), // cyan
|
||||
purple: (189, 147, 249),
|
||||
cyan: (139, 233, 253),
|
||||
orange: (255, 184, 108),
|
||||
tempo_color: (189, 147, 249),
|
||||
bank_color: (139, 233, 253),
|
||||
pattern_color: (80, 250, 123),
|
||||
title_accent: (189, 147, 249),
|
||||
title_author: (255, 121, 198),
|
||||
secondary: (255, 184, 108),
|
||||
link_bright: [
|
||||
(189, 147, 249),
|
||||
(255, 121, 198),
|
||||
(255, 184, 108),
|
||||
(139, 233, 253),
|
||||
(80, 250, 123),
|
||||
(189, 147, 249), (255, 121, 198), (255, 184, 108),
|
||||
(139, 233, 253), (80, 250, 123),
|
||||
],
|
||||
link_dim: [
|
||||
(75, 60, 95),
|
||||
(95, 55, 80),
|
||||
(95, 70, 50),
|
||||
(55, 90, 95),
|
||||
(40, 95, 55),
|
||||
(75, 60, 95), (95, 55, 80), (95, 70, 50),
|
||||
(55, 90, 95), (40, 95, 55),
|
||||
],
|
||||
},
|
||||
header: HeaderColors {
|
||||
tempo_bg: Color::Rgb(65, 50, 75),
|
||||
tempo_fg: purple,
|
||||
bank_bg: Color::Rgb(45, 65, 70),
|
||||
bank_fg: cyan,
|
||||
pattern_bg: Color::Rgb(40, 70, 60),
|
||||
pattern_fg: green,
|
||||
stats_bg: current_line,
|
||||
stats_fg: comment,
|
||||
},
|
||||
modal: ModalColors {
|
||||
border: purple,
|
||||
border_accent: pink,
|
||||
border_warn: orange,
|
||||
border_dim: comment,
|
||||
confirm: orange,
|
||||
rename: purple,
|
||||
input: cyan,
|
||||
editor: purple,
|
||||
preview: comment,
|
||||
},
|
||||
flash: FlashColors {
|
||||
error_bg: Color::Rgb(70, 45, 50),
|
||||
error_fg: red,
|
||||
success_bg: Color::Rgb(40, 65, 50),
|
||||
success_fg: green,
|
||||
info_bg: current_line,
|
||||
info_fg: foreground,
|
||||
event_rgb: (70, 55, 85),
|
||||
},
|
||||
list: ListColors {
|
||||
playing_bg: Color::Rgb(40, 65, 50),
|
||||
playing_fg: green,
|
||||
staged_play_bg: Color::Rgb(70, 55, 85),
|
||||
staged_play_fg: purple,
|
||||
staged_stop_bg: Color::Rgb(80, 50, 60),
|
||||
staged_stop_fg: red,
|
||||
edit_bg: Color::Rgb(45, 70, 70),
|
||||
edit_fg: cyan,
|
||||
hover_bg: lighter_bg,
|
||||
hover_fg: foreground,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: red,
|
||||
connected: green,
|
||||
listening: yellow,
|
||||
},
|
||||
syntax: SyntaxColors {
|
||||
gap_bg: darker_bg,
|
||||
executed_bg: Color::Rgb(55, 50, 70),
|
||||
selected_bg: Color::Rgb(85, 70, 50),
|
||||
emit: (foreground, Color::Rgb(85, 55, 65)),
|
||||
number: (orange, Color::Rgb(75, 55, 45)),
|
||||
string: (yellow, Color::Rgb(70, 70, 45)),
|
||||
comment: (comment, darker_bg),
|
||||
keyword: (pink, Color::Rgb(80, 50, 70)),
|
||||
stack_op: (cyan, Color::Rgb(45, 65, 75)),
|
||||
operator: (green, Color::Rgb(40, 70, 50)),
|
||||
sound: (cyan, Color::Rgb(45, 70, 70)),
|
||||
param: (purple, Color::Rgb(60, 50, 75)),
|
||||
context: (orange, Color::Rgb(75, 55, 45)),
|
||||
note: (green, Color::Rgb(40, 70, 50)),
|
||||
interval: (Color::Rgb(120, 255, 150), Color::Rgb(40, 75, 50)),
|
||||
variable: (pink, Color::Rgb(80, 50, 65)),
|
||||
vary: (yellow, Color::Rgb(70, 70, 45)),
|
||||
generator: (cyan, Color::Rgb(45, 70, 65)),
|
||||
default: (comment, darker_bg),
|
||||
},
|
||||
table: TableColors {
|
||||
row_even: darker_bg,
|
||||
row_odd: background,
|
||||
},
|
||||
values: ValuesColors {
|
||||
tempo: orange,
|
||||
value: comment,
|
||||
},
|
||||
hint: HintColors {
|
||||
key: orange,
|
||||
text: comment,
|
||||
},
|
||||
view_badge: ViewBadgeColors {
|
||||
bg: foreground,
|
||||
fg: background,
|
||||
},
|
||||
nav: NavColors {
|
||||
selected_bg: Color::Rgb(75, 65, 100),
|
||||
selected_fg: foreground,
|
||||
unselected_bg: current_line,
|
||||
unselected_fg: comment,
|
||||
},
|
||||
editor_widget: EditorWidgetColors {
|
||||
cursor_bg: foreground,
|
||||
cursor_fg: background,
|
||||
selection_bg: Color::Rgb(70, 75, 105),
|
||||
completion_bg: current_line,
|
||||
completion_fg: foreground,
|
||||
completion_selected: orange,
|
||||
completion_example: cyan,
|
||||
},
|
||||
browser: BrowserColors {
|
||||
directory: cyan,
|
||||
project_file: purple,
|
||||
selected: orange,
|
||||
file: foreground,
|
||||
focused_border: orange,
|
||||
unfocused_border: comment,
|
||||
root: foreground,
|
||||
file_icon: comment,
|
||||
folder_icon: cyan,
|
||||
empty_text: comment,
|
||||
},
|
||||
input: InputColors {
|
||||
text: cyan,
|
||||
cursor: foreground,
|
||||
hint: comment,
|
||||
},
|
||||
search: SearchColors {
|
||||
active: orange,
|
||||
inactive: comment,
|
||||
match_bg: yellow,
|
||||
match_fg: background,
|
||||
},
|
||||
markdown: MarkdownColors {
|
||||
h1: cyan,
|
||||
h2: orange,
|
||||
h3: purple,
|
||||
code: green,
|
||||
code_border: Color::Rgb(85, 90, 110),
|
||||
link: pink,
|
||||
link_url: Color::Rgb(120, 130, 150),
|
||||
quote: comment,
|
||||
text: foreground,
|
||||
list: foreground,
|
||||
},
|
||||
engine: EngineColors {
|
||||
header: cyan,
|
||||
header_focused: yellow,
|
||||
divider: Color::Rgb(80, 85, 105),
|
||||
scroll_indicator: Color::Rgb(95, 100, 120),
|
||||
label: Color::Rgb(140, 145, 165),
|
||||
label_focused: Color::Rgb(170, 175, 195),
|
||||
label_dim: Color::Rgb(110, 115, 135),
|
||||
value: Color::Rgb(200, 205, 220),
|
||||
focused: yellow,
|
||||
normal: foreground,
|
||||
dim: Color::Rgb(95, 100, 120),
|
||||
path: Color::Rgb(140, 145, 165),
|
||||
border_magenta: pink,
|
||||
border_green: green,
|
||||
border_cyan: cyan,
|
||||
separator: Color::Rgb(80, 85, 105),
|
||||
hint_active: Color::Rgb(220, 200, 100),
|
||||
hint_inactive: Color::Rgb(80, 85, 105),
|
||||
},
|
||||
dict: DictColors {
|
||||
word_name: green,
|
||||
word_bg: Color::Rgb(55, 65, 80),
|
||||
alias: comment,
|
||||
stack_sig: purple,
|
||||
description: foreground,
|
||||
example: Color::Rgb(140, 145, 165),
|
||||
category_focused: yellow,
|
||||
category_selected: cyan,
|
||||
category_normal: foreground,
|
||||
category_dimmed: Color::Rgb(95, 100, 120),
|
||||
border_focused: yellow,
|
||||
border_normal: Color::Rgb(80, 85, 105),
|
||||
header_desc: Color::Rgb(160, 165, 185),
|
||||
},
|
||||
title: TitleColors {
|
||||
big_title: purple,
|
||||
author: pink,
|
||||
link: cyan,
|
||||
license: orange,
|
||||
prompt: Color::Rgb(160, 165, 185),
|
||||
subtitle: foreground,
|
||||
},
|
||||
meter: MeterColors {
|
||||
low: green,
|
||||
mid: yellow,
|
||||
high: red,
|
||||
low_rgb: (70, 230, 110),
|
||||
mid_rgb: (230, 240, 130),
|
||||
high_rgb: (240, 80, 80),
|
||||
},
|
||||
sparkle: SparkleColors {
|
||||
colors: [
|
||||
(189, 147, 249),
|
||||
(255, 184, 108),
|
||||
(80, 250, 123),
|
||||
(255, 121, 198),
|
||||
(139, 233, 253),
|
||||
sparkle: [
|
||||
(189, 147, 249), (255, 184, 108), (80, 250, 123),
|
||||
(255, 121, 198), (139, 233, 253),
|
||||
],
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: orange,
|
||||
button_selected_bg: orange,
|
||||
button_selected_fg: background,
|
||||
},
|
||||
meter: [(70, 230, 110), (230, 240, 130), (240, 80, 80)],
|
||||
}
|
||||
}
|
||||
|
||||
41
crates/ratatui/src/theme/eden.rs
Normal file
41
crates/ratatui/src/theme/eden.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
//! Eden palette.
|
||||
|
||||
use super::palette::Palette;
|
||||
|
||||
pub fn palette() -> Palette {
|
||||
Palette {
|
||||
bg: (8, 12, 8),
|
||||
surface: (16, 24, 16),
|
||||
surface2: (32, 48, 32),
|
||||
fg: (200, 216, 192),
|
||||
fg_dim: (122, 144, 112),
|
||||
fg_muted: (64, 88, 56),
|
||||
accent: (64, 192, 64), // green
|
||||
red: (192, 80, 64),
|
||||
green: (96, 224, 96), // bright_green
|
||||
yellow: (160, 160, 64),
|
||||
blue: (80, 128, 160),
|
||||
purple: (128, 104, 144),
|
||||
cyan: (80, 168, 144),
|
||||
orange: (176, 128, 48),
|
||||
tempo_color: (128, 104, 144),
|
||||
bank_color: (80, 128, 160),
|
||||
pattern_color: (80, 168, 144),
|
||||
title_accent: (64, 192, 64),
|
||||
title_author: (80, 168, 144),
|
||||
secondary: (176, 128, 48),
|
||||
link_bright: [
|
||||
(64, 192, 64), (80, 168, 144), (160, 160, 64),
|
||||
(80, 128, 160), (192, 80, 64),
|
||||
],
|
||||
link_dim: [
|
||||
(14, 38, 14), (16, 34, 28), (32, 32, 14),
|
||||
(16, 26, 32), (38, 16, 14),
|
||||
],
|
||||
sparkle: [
|
||||
(64, 192, 64), (96, 224, 96), (80, 168, 144),
|
||||
(160, 160, 64), (48, 128, 48),
|
||||
],
|
||||
meter: [(64, 192, 64), (160, 160, 64), (192, 80, 64)],
|
||||
}
|
||||
}
|
||||
41
crates/ratatui/src/theme/ember.rs
Normal file
41
crates/ratatui/src/theme/ember.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
//! Ember palette.
|
||||
|
||||
use super::palette::Palette;
|
||||
|
||||
pub fn palette() -> Palette {
|
||||
Palette {
|
||||
bg: (10, 8, 8),
|
||||
surface: (20, 16, 16),
|
||||
surface2: (42, 32, 32),
|
||||
fg: (232, 221, 208),
|
||||
fg_dim: (160, 144, 128),
|
||||
fg_muted: (96, 80, 64),
|
||||
accent: (224, 128, 48), // orange
|
||||
red: (224, 80, 64),
|
||||
green: (128, 160, 80),
|
||||
yellow: (208, 160, 48),
|
||||
blue: (96, 128, 176),
|
||||
purple: (160, 112, 144),
|
||||
cyan: (112, 160, 160),
|
||||
orange: (224, 128, 48),
|
||||
tempo_color: (160, 112, 144),
|
||||
bank_color: (96, 128, 176),
|
||||
pattern_color: (112, 160, 160),
|
||||
title_accent: (224, 128, 48),
|
||||
title_author: (224, 80, 64),
|
||||
secondary: (160, 112, 144),
|
||||
link_bright: [
|
||||
(224, 128, 48), (224, 80, 64), (208, 160, 48),
|
||||
(112, 160, 160), (128, 160, 80),
|
||||
],
|
||||
link_dim: [
|
||||
(45, 28, 14), (45, 18, 14), (42, 32, 12),
|
||||
(22, 32, 32), (26, 32, 18),
|
||||
],
|
||||
sparkle: [
|
||||
(224, 128, 48), (224, 80, 64), (208, 160, 48),
|
||||
(112, 160, 160), (128, 160, 80),
|
||||
],
|
||||
meter: [(128, 160, 80), (208, 160, 48), (224, 80, 64)],
|
||||
}
|
||||
}
|
||||
41
crates/ratatui/src/theme/everforest.rs
Normal file
41
crates/ratatui/src/theme/everforest.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
//! Everforest palette.
|
||||
|
||||
use super::palette::Palette;
|
||||
|
||||
pub fn palette() -> Palette {
|
||||
Palette {
|
||||
bg: (45, 53, 59),
|
||||
surface: (52, 62, 68),
|
||||
surface2: (68, 80, 86),
|
||||
fg: (211, 198, 170),
|
||||
fg_dim: (135, 131, 116),
|
||||
fg_muted: (80, 80, 68),
|
||||
accent: (167, 192, 128),
|
||||
red: (230, 126, 128),
|
||||
green: (167, 192, 128),
|
||||
yellow: (219, 188, 127),
|
||||
blue: (127, 187, 179),
|
||||
purple: (214, 153, 182),
|
||||
cyan: (131, 192, 146),
|
||||
orange: (230, 152, 117),
|
||||
tempo_color: (214, 153, 182),
|
||||
bank_color: (127, 187, 179),
|
||||
pattern_color: (131, 192, 146),
|
||||
title_accent: (167, 192, 128),
|
||||
title_author: (127, 187, 179),
|
||||
secondary: (230, 152, 117),
|
||||
link_bright: [
|
||||
(167, 192, 128), (214, 153, 182), (230, 152, 117),
|
||||
(127, 187, 179), (219, 188, 127),
|
||||
],
|
||||
link_dim: [
|
||||
(56, 66, 46), (70, 52, 62), (72, 52, 42),
|
||||
(44, 64, 60), (70, 62, 44),
|
||||
],
|
||||
sparkle: [
|
||||
(167, 192, 128), (230, 152, 117), (131, 192, 146),
|
||||
(214, 153, 182), (219, 188, 127),
|
||||
],
|
||||
meter: [(148, 172, 110), (200, 170, 108), (210, 108, 110)],
|
||||
}
|
||||
}
|
||||
41
crates/ratatui/src/theme/fairyfloss.rs
Normal file
41
crates/ratatui/src/theme/fairyfloss.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
//! Fairyfloss palette.
|
||||
|
||||
use super::palette::Palette;
|
||||
|
||||
pub fn palette() -> Palette {
|
||||
Palette {
|
||||
bg: (90, 84, 117),
|
||||
surface: (113, 103, 153),
|
||||
surface2: (130, 120, 165),
|
||||
fg: (248, 248, 240),
|
||||
fg_dim: (197, 163, 255),
|
||||
fg_muted: (168, 164, 177),
|
||||
accent: (255, 184, 209), // pink
|
||||
red: (255, 133, 127), // coral
|
||||
green: (194, 255, 223), // mint
|
||||
yellow: (255, 243, 82),
|
||||
blue: (197, 163, 255), // lavender
|
||||
purple: (174, 129, 255),
|
||||
cyan: (194, 255, 223), // mint
|
||||
orange: (255, 133, 127), // coral
|
||||
tempo_color: (255, 184, 209),
|
||||
bank_color: (194, 255, 223),
|
||||
pattern_color: (174, 129, 255),
|
||||
title_accent: (255, 184, 209),
|
||||
title_author: (194, 255, 223),
|
||||
secondary: (255, 133, 127),
|
||||
link_bright: [
|
||||
(255, 184, 209), (174, 129, 255), (255, 133, 127),
|
||||
(194, 255, 223), (255, 243, 82),
|
||||
],
|
||||
link_dim: [
|
||||
(100, 75, 90), (85, 70, 105), (100, 65, 65),
|
||||
(75, 100, 95), (100, 95, 55),
|
||||
],
|
||||
sparkle: [
|
||||
(194, 255, 223), (255, 133, 127), (255, 243, 82),
|
||||
(255, 184, 209), (174, 129, 255),
|
||||
],
|
||||
meter: [(194, 255, 223), (255, 243, 82), (255, 133, 127)],
|
||||
}
|
||||
}
|
||||
41
crates/ratatui/src/theme/fauve.rs
Normal file
41
crates/ratatui/src/theme/fauve.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
//! Fauve palette.
|
||||
|
||||
use super::palette::Palette;
|
||||
|
||||
pub fn palette() -> Palette {
|
||||
Palette {
|
||||
bg: (28, 22, 18),
|
||||
surface: (42, 33, 26),
|
||||
surface2: (58, 46, 36),
|
||||
fg: (240, 228, 210),
|
||||
fg_dim: (170, 150, 130),
|
||||
fg_muted: (100, 82, 66),
|
||||
accent: (230, 60, 20),
|
||||
red: (220, 38, 32),
|
||||
green: (30, 170, 80),
|
||||
yellow: (255, 210, 0),
|
||||
blue: (20, 80, 200),
|
||||
purple: (170, 40, 150),
|
||||
cyan: (0, 150, 180),
|
||||
orange: (240, 120, 0),
|
||||
tempo_color: (230, 60, 20),
|
||||
bank_color: (20, 80, 200),
|
||||
pattern_color: (0, 150, 180),
|
||||
title_accent: (230, 60, 20),
|
||||
title_author: (20, 80, 200),
|
||||
secondary: (170, 40, 150),
|
||||
link_bright: [
|
||||
(230, 60, 20), (20, 80, 200), (240, 120, 0),
|
||||
(0, 150, 180), (30, 170, 80),
|
||||
],
|
||||
link_dim: [
|
||||
(72, 24, 10), (10, 28, 65), (76, 40, 6),
|
||||
(6, 48, 58), (14, 54, 28),
|
||||
],
|
||||
sparkle: [
|
||||
(230, 60, 20), (255, 210, 0), (30, 170, 80),
|
||||
(20, 80, 200), (170, 40, 150),
|
||||
],
|
||||
meter: [(26, 152, 72), (235, 190, 0), (200, 34, 28)],
|
||||
}
|
||||
}
|
||||
41
crates/ratatui/src/theme/georges.rs
Normal file
41
crates/ratatui/src/theme/georges.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
//! Georges palette (C64 colors on black).
|
||||
|
||||
use super::palette::Palette;
|
||||
|
||||
pub fn palette() -> Palette {
|
||||
Palette {
|
||||
bg: (0, 0, 0),
|
||||
surface: (16, 16, 16),
|
||||
surface2: (24, 24, 24),
|
||||
fg: (187, 187, 187),
|
||||
fg_dim: (119, 119, 119),
|
||||
fg_muted: (51, 51, 51),
|
||||
accent: (0, 136, 255), // lightblue
|
||||
red: (255, 119, 119), // lightred
|
||||
green: (0, 204, 85),
|
||||
yellow: (238, 238, 119),
|
||||
blue: (0, 136, 255), // lightblue
|
||||
purple: (204, 68, 204), // violet
|
||||
cyan: (170, 255, 238),
|
||||
orange: (221, 136, 85),
|
||||
tempo_color: (204, 68, 204),
|
||||
bank_color: (0, 136, 255),
|
||||
pattern_color: (0, 204, 85),
|
||||
title_accent: (0, 136, 255),
|
||||
title_author: (0, 204, 85),
|
||||
secondary: (221, 136, 85),
|
||||
link_bright: [
|
||||
(0, 136, 255), (0, 204, 85), (238, 238, 119),
|
||||
(204, 68, 204), (170, 255, 238),
|
||||
],
|
||||
link_dim: [
|
||||
(0, 20, 40), (0, 30, 12), (34, 34, 16),
|
||||
(30, 10, 30), (24, 36, 34),
|
||||
],
|
||||
sparkle: [
|
||||
(0, 136, 255), (170, 255, 238), (0, 204, 85),
|
||||
(238, 238, 119), (204, 68, 204),
|
||||
],
|
||||
meter: [(0, 204, 85), (238, 238, 119), (255, 119, 119)],
|
||||
}
|
||||
}
|
||||
@@ -1,278 +1,41 @@
|
||||
use super::*;
|
||||
use ratatui::style::Color;
|
||||
//! Gruvbox Dark palette.
|
||||
|
||||
pub fn theme() -> ThemeColors {
|
||||
let bg0 = Color::Rgb(40, 40, 40);
|
||||
let bg1 = Color::Rgb(60, 56, 54);
|
||||
let bg2 = Color::Rgb(80, 73, 69);
|
||||
let fg = Color::Rgb(235, 219, 178);
|
||||
let fg2 = Color::Rgb(213, 196, 161);
|
||||
let fg3 = Color::Rgb(189, 174, 147);
|
||||
let fg4 = Color::Rgb(168, 153, 132);
|
||||
let red = Color::Rgb(251, 73, 52);
|
||||
let green = Color::Rgb(184, 187, 38);
|
||||
let yellow = Color::Rgb(250, 189, 47);
|
||||
let blue = Color::Rgb(131, 165, 152);
|
||||
let purple = Color::Rgb(211, 134, 155);
|
||||
let aqua = Color::Rgb(142, 192, 124);
|
||||
let orange = Color::Rgb(254, 128, 25);
|
||||
use super::palette::Palette;
|
||||
|
||||
let darker_bg = Color::Rgb(29, 32, 33);
|
||||
|
||||
ThemeColors {
|
||||
ui: UiColors {
|
||||
bg: bg0,
|
||||
bg_rgb: (40, 40, 40),
|
||||
text_primary: fg,
|
||||
text_muted: fg3,
|
||||
text_dim: fg4,
|
||||
border: bg2,
|
||||
header: yellow,
|
||||
unfocused: fg4,
|
||||
accent: orange,
|
||||
surface: bg1,
|
||||
},
|
||||
status: StatusColors {
|
||||
playing_bg: Color::Rgb(50, 60, 45),
|
||||
playing_fg: green,
|
||||
stopped_bg: Color::Rgb(65, 45, 45),
|
||||
stopped_fg: red,
|
||||
fill_on: green,
|
||||
fill_off: fg4,
|
||||
fill_bg: bg1,
|
||||
},
|
||||
selection: SelectionColors {
|
||||
cursor_bg: orange,
|
||||
cursor_fg: bg0,
|
||||
selected_bg: Color::Rgb(80, 70, 55),
|
||||
selected_fg: yellow,
|
||||
in_range_bg: Color::Rgb(65, 60, 50),
|
||||
in_range_fg: fg2,
|
||||
cursor: orange,
|
||||
selected: Color::Rgb(80, 70, 55),
|
||||
in_range: Color::Rgb(65, 60, 50),
|
||||
},
|
||||
tile: TileColors {
|
||||
playing_active_bg: Color::Rgb(90, 65, 50),
|
||||
playing_active_fg: orange,
|
||||
playing_inactive_bg: Color::Rgb(80, 75, 45),
|
||||
playing_inactive_fg: yellow,
|
||||
active_bg: Color::Rgb(50, 65, 55),
|
||||
active_fg: aqua,
|
||||
inactive_bg: bg1,
|
||||
inactive_fg: fg3,
|
||||
active_selected_bg: Color::Rgb(85, 70, 60),
|
||||
active_in_range_bg: Color::Rgb(70, 65, 55),
|
||||
pub fn palette() -> Palette {
|
||||
Palette {
|
||||
bg: (40, 40, 40),
|
||||
surface: (60, 56, 54),
|
||||
surface2: (80, 73, 69),
|
||||
fg: (235, 219, 178),
|
||||
fg_dim: (189, 174, 147),
|
||||
fg_muted: (168, 153, 132),
|
||||
accent: (254, 128, 25), // orange
|
||||
red: (251, 73, 52),
|
||||
green: (184, 187, 38),
|
||||
yellow: (250, 189, 47),
|
||||
blue: (131, 165, 152),
|
||||
purple: (211, 134, 155),
|
||||
cyan: (142, 192, 124), // aqua
|
||||
orange: (254, 128, 25),
|
||||
tempo_color: (254, 128, 25),
|
||||
bank_color: (131, 165, 152),
|
||||
pattern_color: (142, 192, 124),
|
||||
title_accent: (254, 128, 25),
|
||||
title_author: (250, 189, 47),
|
||||
secondary: (254, 128, 25),
|
||||
link_bright: [
|
||||
(254, 128, 25),
|
||||
(211, 134, 155),
|
||||
(250, 189, 47),
|
||||
(131, 165, 152),
|
||||
(184, 187, 38),
|
||||
(254, 128, 25), (211, 134, 155), (250, 189, 47),
|
||||
(131, 165, 152), (184, 187, 38),
|
||||
],
|
||||
link_dim: [
|
||||
(85, 55, 35),
|
||||
(75, 55, 65),
|
||||
(80, 70, 40),
|
||||
(50, 60, 60),
|
||||
(60, 65, 35),
|
||||
(85, 55, 35), (75, 55, 65), (80, 70, 40),
|
||||
(50, 60, 60), (60, 65, 35),
|
||||
],
|
||||
},
|
||||
header: HeaderColors {
|
||||
tempo_bg: Color::Rgb(75, 55, 40),
|
||||
tempo_fg: orange,
|
||||
bank_bg: Color::Rgb(50, 60, 60),
|
||||
bank_fg: blue,
|
||||
pattern_bg: Color::Rgb(50, 65, 50),
|
||||
pattern_fg: aqua,
|
||||
stats_bg: bg1,
|
||||
stats_fg: fg3,
|
||||
},
|
||||
modal: ModalColors {
|
||||
border: yellow,
|
||||
border_accent: orange,
|
||||
border_warn: red,
|
||||
border_dim: fg4,
|
||||
confirm: orange,
|
||||
rename: purple,
|
||||
input: blue,
|
||||
editor: yellow,
|
||||
preview: fg4,
|
||||
},
|
||||
flash: FlashColors {
|
||||
error_bg: Color::Rgb(70, 45, 45),
|
||||
error_fg: red,
|
||||
success_bg: Color::Rgb(50, 65, 45),
|
||||
success_fg: green,
|
||||
info_bg: bg1,
|
||||
info_fg: fg,
|
||||
event_rgb: (70, 55, 45),
|
||||
},
|
||||
list: ListColors {
|
||||
playing_bg: Color::Rgb(50, 65, 45),
|
||||
playing_fg: green,
|
||||
staged_play_bg: Color::Rgb(70, 55, 60),
|
||||
staged_play_fg: purple,
|
||||
staged_stop_bg: Color::Rgb(75, 50, 50),
|
||||
staged_stop_fg: red,
|
||||
edit_bg: Color::Rgb(50, 65, 55),
|
||||
edit_fg: aqua,
|
||||
hover_bg: bg2,
|
||||
hover_fg: fg,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: red,
|
||||
connected: green,
|
||||
listening: yellow,
|
||||
},
|
||||
syntax: SyntaxColors {
|
||||
gap_bg: darker_bg,
|
||||
executed_bg: Color::Rgb(55, 50, 45),
|
||||
selected_bg: Color::Rgb(85, 70, 45),
|
||||
emit: (fg, Color::Rgb(80, 55, 50)),
|
||||
number: (orange, Color::Rgb(70, 50, 40)),
|
||||
string: (green, Color::Rgb(50, 60, 40)),
|
||||
comment: (fg4, darker_bg),
|
||||
keyword: (red, Color::Rgb(70, 45, 45)),
|
||||
stack_op: (blue, Color::Rgb(50, 55, 60)),
|
||||
operator: (yellow, Color::Rgb(70, 65, 40)),
|
||||
sound: (aqua, Color::Rgb(45, 60, 50)),
|
||||
param: (purple, Color::Rgb(65, 50, 55)),
|
||||
context: (orange, Color::Rgb(70, 50, 40)),
|
||||
note: (green, Color::Rgb(50, 60, 40)),
|
||||
interval: (Color::Rgb(170, 200, 100), Color::Rgb(55, 65, 40)),
|
||||
variable: (purple, Color::Rgb(65, 50, 55)),
|
||||
vary: (yellow, Color::Rgb(70, 65, 40)),
|
||||
generator: (aqua, Color::Rgb(45, 60, 50)),
|
||||
default: (fg3, darker_bg),
|
||||
},
|
||||
table: TableColors {
|
||||
row_even: darker_bg,
|
||||
row_odd: bg0,
|
||||
},
|
||||
values: ValuesColors {
|
||||
tempo: orange,
|
||||
value: fg3,
|
||||
},
|
||||
hint: HintColors {
|
||||
key: orange,
|
||||
text: fg4,
|
||||
},
|
||||
view_badge: ViewBadgeColors { bg: fg, fg: bg0 },
|
||||
nav: NavColors {
|
||||
selected_bg: Color::Rgb(80, 65, 50),
|
||||
selected_fg: fg,
|
||||
unselected_bg: bg1,
|
||||
unselected_fg: fg4,
|
||||
},
|
||||
editor_widget: EditorWidgetColors {
|
||||
cursor_bg: fg,
|
||||
cursor_fg: bg0,
|
||||
selection_bg: Color::Rgb(70, 65, 55),
|
||||
completion_bg: bg1,
|
||||
completion_fg: fg,
|
||||
completion_selected: orange,
|
||||
completion_example: aqua,
|
||||
},
|
||||
browser: BrowserColors {
|
||||
directory: blue,
|
||||
project_file: purple,
|
||||
selected: orange,
|
||||
file: fg,
|
||||
focused_border: orange,
|
||||
unfocused_border: fg4,
|
||||
root: fg,
|
||||
file_icon: fg4,
|
||||
folder_icon: blue,
|
||||
empty_text: fg4,
|
||||
},
|
||||
input: InputColors {
|
||||
text: blue,
|
||||
cursor: fg,
|
||||
hint: fg4,
|
||||
},
|
||||
search: SearchColors {
|
||||
active: orange,
|
||||
inactive: fg4,
|
||||
match_bg: yellow,
|
||||
match_fg: bg0,
|
||||
},
|
||||
markdown: MarkdownColors {
|
||||
h1: blue,
|
||||
h2: orange,
|
||||
h3: purple,
|
||||
code: green,
|
||||
code_border: Color::Rgb(80, 75, 70),
|
||||
link: aqua,
|
||||
link_url: Color::Rgb(120, 115, 105),
|
||||
quote: fg4,
|
||||
text: fg,
|
||||
list: fg,
|
||||
},
|
||||
engine: EngineColors {
|
||||
header: blue,
|
||||
header_focused: yellow,
|
||||
divider: Color::Rgb(75, 70, 65),
|
||||
scroll_indicator: Color::Rgb(90, 85, 80),
|
||||
label: Color::Rgb(145, 135, 125),
|
||||
label_focused: Color::Rgb(175, 165, 155),
|
||||
label_dim: Color::Rgb(115, 105, 95),
|
||||
value: Color::Rgb(200, 190, 175),
|
||||
focused: yellow,
|
||||
normal: fg,
|
||||
dim: Color::Rgb(90, 85, 80),
|
||||
path: Color::Rgb(145, 135, 125),
|
||||
border_magenta: purple,
|
||||
border_green: green,
|
||||
border_cyan: aqua,
|
||||
separator: Color::Rgb(75, 70, 65),
|
||||
hint_active: Color::Rgb(220, 180, 80),
|
||||
hint_inactive: Color::Rgb(75, 70, 65),
|
||||
},
|
||||
dict: DictColors {
|
||||
word_name: green,
|
||||
word_bg: Color::Rgb(55, 60, 55),
|
||||
alias: fg4,
|
||||
stack_sig: purple,
|
||||
description: fg,
|
||||
example: Color::Rgb(145, 135, 125),
|
||||
category_focused: yellow,
|
||||
category_selected: blue,
|
||||
category_normal: fg,
|
||||
category_dimmed: Color::Rgb(90, 85, 80),
|
||||
border_focused: yellow,
|
||||
border_normal: Color::Rgb(75, 70, 65),
|
||||
header_desc: Color::Rgb(165, 155, 145),
|
||||
},
|
||||
title: TitleColors {
|
||||
big_title: orange,
|
||||
author: yellow,
|
||||
link: aqua,
|
||||
license: purple,
|
||||
prompt: Color::Rgb(165, 155, 145),
|
||||
subtitle: fg,
|
||||
},
|
||||
meter: MeterColors {
|
||||
low: green,
|
||||
mid: yellow,
|
||||
high: red,
|
||||
low_rgb: (170, 175, 35),
|
||||
mid_rgb: (235, 180, 45),
|
||||
high_rgb: (240, 70, 50),
|
||||
},
|
||||
sparkle: SparkleColors {
|
||||
colors: [
|
||||
(250, 189, 47),
|
||||
(254, 128, 25),
|
||||
(184, 187, 38),
|
||||
(211, 134, 155),
|
||||
(131, 165, 152),
|
||||
sparkle: [
|
||||
(250, 189, 47), (254, 128, 25), (184, 187, 38),
|
||||
(211, 134, 155), (131, 165, 152),
|
||||
],
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: orange,
|
||||
button_selected_bg: orange,
|
||||
button_selected_fg: bg0,
|
||||
},
|
||||
meter: [(170, 175, 35), (235, 180, 45), (240, 70, 50)],
|
||||
}
|
||||
}
|
||||
|
||||
41
crates/ratatui/src/theme/hot_dog_stand.rs
Normal file
41
crates/ratatui/src/theme/hot_dog_stand.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
//! Hot Dog Stand palette.
|
||||
|
||||
use super::palette::Palette;
|
||||
|
||||
pub fn palette() -> Palette {
|
||||
Palette {
|
||||
bg: (255, 0, 0),
|
||||
surface: (215, 0, 0),
|
||||
surface2: (175, 0, 0),
|
||||
fg: (255, 255, 0),
|
||||
fg_dim: (255, 255, 95),
|
||||
fg_muted: (255, 215, 0),
|
||||
accent: (255, 255, 0),
|
||||
red: (255, 255, 255),
|
||||
green: (255, 255, 0),
|
||||
yellow: (255, 215, 0),
|
||||
blue: (255, 255, 0),
|
||||
purple: (255, 255, 255),
|
||||
cyan: (255, 255, 0),
|
||||
orange: (255, 215, 0),
|
||||
tempo_color: (255, 255, 0),
|
||||
bank_color: (255, 255, 0),
|
||||
pattern_color: (255, 255, 0),
|
||||
title_accent: (255, 255, 0),
|
||||
title_author: (255, 255, 255),
|
||||
secondary: (255, 215, 0),
|
||||
link_bright: [
|
||||
(255, 255, 0), (255, 255, 255), (255, 215, 0),
|
||||
(255, 255, 95), (255, 255, 0),
|
||||
],
|
||||
link_dim: [
|
||||
(140, 140, 0), (140, 140, 140), (140, 120, 0),
|
||||
(140, 140, 60), (140, 140, 0),
|
||||
],
|
||||
sparkle: [
|
||||
(255, 255, 0), (255, 255, 255), (255, 215, 0),
|
||||
(255, 255, 95), (255, 255, 0),
|
||||
],
|
||||
meter: [(255, 255, 0), (255, 215, 0), (255, 255, 255)],
|
||||
}
|
||||
}
|
||||
41
crates/ratatui/src/theme/iceberg.rs
Normal file
41
crates/ratatui/src/theme/iceberg.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
//! Iceberg palette.
|
||||
|
||||
use super::palette::Palette;
|
||||
|
||||
pub fn palette() -> Palette {
|
||||
Palette {
|
||||
bg: (22, 24, 33),
|
||||
surface: (30, 33, 46),
|
||||
surface2: (45, 48, 64),
|
||||
fg: (198, 200, 209),
|
||||
fg_dim: (109, 112, 126),
|
||||
fg_muted: (64, 66, 78),
|
||||
accent: (132, 160, 198),
|
||||
red: (226, 120, 120),
|
||||
green: (180, 190, 130),
|
||||
yellow: (226, 164, 120),
|
||||
blue: (132, 160, 198),
|
||||
purple: (160, 147, 199),
|
||||
cyan: (137, 184, 194),
|
||||
orange: (226, 164, 120),
|
||||
tempo_color: (160, 147, 199),
|
||||
bank_color: (132, 160, 198),
|
||||
pattern_color: (137, 184, 194),
|
||||
title_accent: (132, 160, 198),
|
||||
title_author: (160, 147, 199),
|
||||
secondary: (226, 164, 120),
|
||||
link_bright: [
|
||||
(132, 160, 198), (160, 147, 199), (226, 164, 120),
|
||||
(137, 184, 194), (180, 190, 130),
|
||||
],
|
||||
link_dim: [
|
||||
(45, 55, 70), (55, 50, 68), (70, 55, 42),
|
||||
(46, 62, 66), (58, 62, 44),
|
||||
],
|
||||
sparkle: [
|
||||
(132, 160, 198), (226, 164, 120), (180, 190, 130),
|
||||
(160, 147, 199), (226, 120, 120),
|
||||
],
|
||||
meter: [(160, 175, 115), (210, 150, 105), (200, 105, 105)],
|
||||
}
|
||||
}
|
||||
41
crates/ratatui/src/theme/jaipur.rs
Normal file
41
crates/ratatui/src/theme/jaipur.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
//! Jaipur palette.
|
||||
|
||||
use super::palette::Palette;
|
||||
|
||||
pub fn palette() -> Palette {
|
||||
Palette {
|
||||
bg: (30, 24, 22),
|
||||
surface: (44, 36, 32),
|
||||
surface2: (60, 48, 42),
|
||||
fg: (238, 222, 200),
|
||||
fg_dim: (165, 145, 125),
|
||||
fg_muted: (95, 78, 65),
|
||||
accent: (210, 90, 100),
|
||||
red: (200, 44, 52),
|
||||
green: (30, 160, 120),
|
||||
yellow: (240, 180, 20),
|
||||
blue: (60, 60, 180),
|
||||
purple: (150, 50, 120),
|
||||
cyan: (0, 155, 155),
|
||||
orange: (220, 120, 50),
|
||||
tempo_color: (210, 90, 100),
|
||||
bank_color: (60, 60, 180),
|
||||
pattern_color: (0, 155, 155),
|
||||
title_accent: (210, 90, 100),
|
||||
title_author: (60, 60, 180),
|
||||
secondary: (220, 120, 50),
|
||||
link_bright: [
|
||||
(210, 90, 100), (60, 60, 180), (220, 120, 50),
|
||||
(0, 155, 155), (30, 160, 120),
|
||||
],
|
||||
link_dim: [
|
||||
(66, 30, 34), (22, 22, 58), (70, 40, 18),
|
||||
(6, 48, 48), (12, 50, 38),
|
||||
],
|
||||
sparkle: [
|
||||
(210, 90, 100), (240, 180, 20), (30, 160, 120),
|
||||
(60, 60, 180), (150, 50, 120),
|
||||
],
|
||||
meter: [(26, 144, 106), (222, 164, 18), (184, 40, 46)],
|
||||
}
|
||||
}
|
||||
@@ -1,278 +1,41 @@
|
||||
use super::*;
|
||||
use ratatui::style::Color;
|
||||
//! Kanagawa palette.
|
||||
|
||||
pub fn theme() -> ThemeColors {
|
||||
let bg = Color::Rgb(31, 31, 40);
|
||||
let bg_light = Color::Rgb(43, 43, 54);
|
||||
let bg_lighter = Color::Rgb(54, 54, 70);
|
||||
let fg = Color::Rgb(220, 215, 186);
|
||||
let fg_dim = Color::Rgb(160, 158, 140);
|
||||
let comment = Color::Rgb(114, 113, 105);
|
||||
let crystal_blue = Color::Rgb(126, 156, 216);
|
||||
let oni_violet = Color::Rgb(149, 127, 184);
|
||||
let autumn_green = Color::Rgb(118, 148, 106);
|
||||
let autumn_red = Color::Rgb(195, 64, 67);
|
||||
let carp_yellow = Color::Rgb(230, 195, 132);
|
||||
let spring_blue = Color::Rgb(127, 180, 202);
|
||||
let wave_red = Color::Rgb(226, 109, 115);
|
||||
let sakura_pink = Color::Rgb(212, 140, 149);
|
||||
use super::palette::Palette;
|
||||
|
||||
let darker_bg = Color::Rgb(26, 26, 34);
|
||||
|
||||
ThemeColors {
|
||||
ui: UiColors {
|
||||
bg,
|
||||
bg_rgb: (31, 31, 40),
|
||||
text_primary: fg,
|
||||
text_muted: fg_dim,
|
||||
text_dim: comment,
|
||||
border: bg_lighter,
|
||||
header: crystal_blue,
|
||||
unfocused: comment,
|
||||
accent: sakura_pink,
|
||||
surface: bg_light,
|
||||
},
|
||||
status: StatusColors {
|
||||
playing_bg: Color::Rgb(40, 55, 45),
|
||||
playing_fg: autumn_green,
|
||||
stopped_bg: Color::Rgb(60, 40, 45),
|
||||
stopped_fg: autumn_red,
|
||||
fill_on: autumn_green,
|
||||
fill_off: comment,
|
||||
fill_bg: bg_light,
|
||||
},
|
||||
selection: SelectionColors {
|
||||
cursor_bg: sakura_pink,
|
||||
cursor_fg: bg,
|
||||
selected_bg: Color::Rgb(65, 55, 70),
|
||||
selected_fg: sakura_pink,
|
||||
in_range_bg: Color::Rgb(50, 50, 60),
|
||||
in_range_fg: fg,
|
||||
cursor: sakura_pink,
|
||||
selected: Color::Rgb(65, 55, 70),
|
||||
in_range: Color::Rgb(50, 50, 60),
|
||||
},
|
||||
tile: TileColors {
|
||||
playing_active_bg: Color::Rgb(65, 60, 50),
|
||||
playing_active_fg: carp_yellow,
|
||||
playing_inactive_bg: Color::Rgb(55, 55, 50),
|
||||
playing_inactive_fg: fg_dim,
|
||||
active_bg: Color::Rgb(45, 55, 70),
|
||||
active_fg: crystal_blue,
|
||||
inactive_bg: bg_light,
|
||||
inactive_fg: fg_dim,
|
||||
active_selected_bg: Color::Rgb(65, 55, 70),
|
||||
active_in_range_bg: Color::Rgb(50, 50, 60),
|
||||
pub fn palette() -> Palette {
|
||||
Palette {
|
||||
bg: (31, 31, 40),
|
||||
surface: (43, 43, 54),
|
||||
surface2: (54, 54, 70),
|
||||
fg: (220, 215, 186),
|
||||
fg_dim: (160, 158, 140),
|
||||
fg_muted: (114, 113, 105),
|
||||
accent: (210, 126, 153), // sakura_pink
|
||||
red: (195, 64, 67),
|
||||
green: (118, 148, 106),
|
||||
yellow: (230, 195, 132), // carp_yellow
|
||||
blue: (126, 156, 216), // crystal_blue
|
||||
purple: (149, 127, 184), // oni_violet
|
||||
cyan: (127, 180, 202), // spring_blue
|
||||
orange: (230, 195, 132), // carp_yellow
|
||||
tempo_color: (149, 127, 184),
|
||||
bank_color: (126, 156, 216),
|
||||
pattern_color: (118, 148, 106),
|
||||
title_accent: (210, 126, 153),
|
||||
title_author: (126, 156, 216),
|
||||
secondary: (210, 126, 153),
|
||||
link_bright: [
|
||||
(226, 109, 115),
|
||||
(149, 127, 184),
|
||||
(230, 195, 132),
|
||||
(127, 180, 202),
|
||||
(118, 148, 106),
|
||||
(228, 104, 118), (149, 127, 184), (230, 195, 132),
|
||||
(127, 180, 202), (118, 148, 106),
|
||||
],
|
||||
link_dim: [
|
||||
(75, 45, 50),
|
||||
(55, 50, 70),
|
||||
(70, 60, 50),
|
||||
(45, 60, 70),
|
||||
(45, 55, 45),
|
||||
(75, 45, 50), (55, 50, 70), (70, 60, 50),
|
||||
(45, 60, 70), (45, 55, 45),
|
||||
],
|
||||
},
|
||||
header: HeaderColors {
|
||||
tempo_bg: Color::Rgb(55, 50, 65),
|
||||
tempo_fg: oni_violet,
|
||||
bank_bg: Color::Rgb(45, 55, 70),
|
||||
bank_fg: crystal_blue,
|
||||
pattern_bg: Color::Rgb(45, 55, 45),
|
||||
pattern_fg: autumn_green,
|
||||
stats_bg: bg_light,
|
||||
stats_fg: fg_dim,
|
||||
},
|
||||
modal: ModalColors {
|
||||
border: crystal_blue,
|
||||
border_accent: sakura_pink,
|
||||
border_warn: carp_yellow,
|
||||
border_dim: comment,
|
||||
confirm: carp_yellow,
|
||||
rename: oni_violet,
|
||||
input: crystal_blue,
|
||||
editor: crystal_blue,
|
||||
preview: comment,
|
||||
},
|
||||
flash: FlashColors {
|
||||
error_bg: Color::Rgb(60, 40, 45),
|
||||
error_fg: wave_red,
|
||||
success_bg: Color::Rgb(40, 55, 45),
|
||||
success_fg: autumn_green,
|
||||
info_bg: bg_light,
|
||||
info_fg: fg,
|
||||
event_rgb: (50, 50, 60),
|
||||
},
|
||||
list: ListColors {
|
||||
playing_bg: Color::Rgb(40, 55, 45),
|
||||
playing_fg: autumn_green,
|
||||
staged_play_bg: Color::Rgb(55, 50, 70),
|
||||
staged_play_fg: oni_violet,
|
||||
staged_stop_bg: Color::Rgb(65, 45, 50),
|
||||
staged_stop_fg: wave_red,
|
||||
edit_bg: Color::Rgb(45, 55, 70),
|
||||
edit_fg: crystal_blue,
|
||||
hover_bg: bg_lighter,
|
||||
hover_fg: fg,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: autumn_red,
|
||||
connected: autumn_green,
|
||||
listening: carp_yellow,
|
||||
},
|
||||
syntax: SyntaxColors {
|
||||
gap_bg: darker_bg,
|
||||
executed_bg: Color::Rgb(45, 45, 55),
|
||||
selected_bg: Color::Rgb(65, 60, 50),
|
||||
emit: (fg, Color::Rgb(60, 50, 60)),
|
||||
number: (oni_violet, Color::Rgb(55, 50, 65)),
|
||||
string: (autumn_green, Color::Rgb(45, 55, 45)),
|
||||
comment: (comment, darker_bg),
|
||||
keyword: (sakura_pink, Color::Rgb(60, 50, 55)),
|
||||
stack_op: (spring_blue, Color::Rgb(45, 55, 65)),
|
||||
operator: (wave_red, Color::Rgb(60, 45, 50)),
|
||||
sound: (crystal_blue, Color::Rgb(45, 55, 70)),
|
||||
param: (carp_yellow, Color::Rgb(65, 60, 50)),
|
||||
context: (carp_yellow, Color::Rgb(65, 60, 50)),
|
||||
note: (autumn_green, Color::Rgb(45, 55, 45)),
|
||||
interval: (Color::Rgb(150, 180, 130), Color::Rgb(45, 55, 45)),
|
||||
variable: (autumn_green, Color::Rgb(45, 55, 45)),
|
||||
vary: (carp_yellow, Color::Rgb(65, 60, 50)),
|
||||
generator: (spring_blue, Color::Rgb(45, 55, 65)),
|
||||
default: (fg_dim, darker_bg),
|
||||
},
|
||||
table: TableColors {
|
||||
row_even: darker_bg,
|
||||
row_odd: bg,
|
||||
},
|
||||
values: ValuesColors {
|
||||
tempo: carp_yellow,
|
||||
value: fg_dim,
|
||||
},
|
||||
hint: HintColors {
|
||||
key: carp_yellow,
|
||||
text: comment,
|
||||
},
|
||||
view_badge: ViewBadgeColors { bg: fg, fg: bg },
|
||||
nav: NavColors {
|
||||
selected_bg: Color::Rgb(60, 50, 70),
|
||||
selected_fg: fg,
|
||||
unselected_bg: bg_light,
|
||||
unselected_fg: comment,
|
||||
},
|
||||
editor_widget: EditorWidgetColors {
|
||||
cursor_bg: fg,
|
||||
cursor_fg: bg,
|
||||
selection_bg: Color::Rgb(55, 55, 70),
|
||||
completion_bg: bg_light,
|
||||
completion_fg: fg,
|
||||
completion_selected: carp_yellow,
|
||||
completion_example: spring_blue,
|
||||
},
|
||||
browser: BrowserColors {
|
||||
directory: crystal_blue,
|
||||
project_file: oni_violet,
|
||||
selected: carp_yellow,
|
||||
file: fg,
|
||||
focused_border: carp_yellow,
|
||||
unfocused_border: comment,
|
||||
root: fg,
|
||||
file_icon: comment,
|
||||
folder_icon: crystal_blue,
|
||||
empty_text: comment,
|
||||
},
|
||||
input: InputColors {
|
||||
text: crystal_blue,
|
||||
cursor: fg,
|
||||
hint: comment,
|
||||
},
|
||||
search: SearchColors {
|
||||
active: carp_yellow,
|
||||
inactive: comment,
|
||||
match_bg: carp_yellow,
|
||||
match_fg: bg,
|
||||
},
|
||||
markdown: MarkdownColors {
|
||||
h1: crystal_blue,
|
||||
h2: carp_yellow,
|
||||
h3: oni_violet,
|
||||
code: autumn_green,
|
||||
code_border: Color::Rgb(65, 65, 80),
|
||||
link: sakura_pink,
|
||||
link_url: Color::Rgb(100, 100, 115),
|
||||
quote: comment,
|
||||
text: fg,
|
||||
list: fg,
|
||||
},
|
||||
engine: EngineColors {
|
||||
header: crystal_blue,
|
||||
header_focused: carp_yellow,
|
||||
divider: Color::Rgb(60, 60, 75),
|
||||
scroll_indicator: Color::Rgb(75, 75, 92),
|
||||
label: Color::Rgb(140, 138, 125),
|
||||
label_focused: Color::Rgb(170, 168, 155),
|
||||
label_dim: Color::Rgb(110, 108, 100),
|
||||
value: Color::Rgb(200, 195, 175),
|
||||
focused: carp_yellow,
|
||||
normal: fg,
|
||||
dim: Color::Rgb(75, 75, 92),
|
||||
path: Color::Rgb(140, 138, 125),
|
||||
border_magenta: oni_violet,
|
||||
border_green: autumn_green,
|
||||
border_cyan: spring_blue,
|
||||
separator: Color::Rgb(60, 60, 75),
|
||||
hint_active: Color::Rgb(220, 185, 120),
|
||||
hint_inactive: Color::Rgb(60, 60, 75),
|
||||
},
|
||||
dict: DictColors {
|
||||
word_name: autumn_green,
|
||||
word_bg: Color::Rgb(45, 50, 50),
|
||||
alias: comment,
|
||||
stack_sig: oni_violet,
|
||||
description: fg,
|
||||
example: Color::Rgb(140, 138, 125),
|
||||
category_focused: carp_yellow,
|
||||
category_selected: crystal_blue,
|
||||
category_normal: fg,
|
||||
category_dimmed: Color::Rgb(75, 75, 92),
|
||||
border_focused: carp_yellow,
|
||||
border_normal: Color::Rgb(60, 60, 75),
|
||||
header_desc: Color::Rgb(160, 158, 145),
|
||||
},
|
||||
title: TitleColors {
|
||||
big_title: sakura_pink,
|
||||
author: crystal_blue,
|
||||
link: autumn_green,
|
||||
license: carp_yellow,
|
||||
prompt: Color::Rgb(160, 158, 145),
|
||||
subtitle: fg,
|
||||
},
|
||||
meter: MeterColors {
|
||||
low: autumn_green,
|
||||
mid: carp_yellow,
|
||||
high: wave_red,
|
||||
low_rgb: (118, 148, 106),
|
||||
mid_rgb: (230, 195, 132),
|
||||
high_rgb: (226, 109, 115),
|
||||
},
|
||||
sparkle: SparkleColors {
|
||||
colors: [
|
||||
(127, 180, 202),
|
||||
(230, 195, 132),
|
||||
(118, 148, 106),
|
||||
(226, 109, 115),
|
||||
(149, 127, 184),
|
||||
sparkle: [
|
||||
(127, 180, 202), (230, 195, 132), (118, 148, 106),
|
||||
(228, 104, 118), (149, 127, 184),
|
||||
],
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: carp_yellow,
|
||||
button_selected_bg: carp_yellow,
|
||||
button_selected_fg: bg,
|
||||
},
|
||||
meter: [(118, 148, 106), (230, 195, 132), (228, 104, 118)],
|
||||
}
|
||||
}
|
||||
|
||||
41
crates/ratatui/src/theme/letz_light.rs
Normal file
41
crates/ratatui/src/theme/letz_light.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
//! Letz Light palette.
|
||||
|
||||
use super::palette::Palette;
|
||||
|
||||
pub fn palette() -> Palette {
|
||||
Palette {
|
||||
bg: (255, 255, 255),
|
||||
surface: (235, 235, 240),
|
||||
surface2: (210, 210, 215),
|
||||
fg: (29, 29, 31),
|
||||
fg_dim: (110, 110, 115),
|
||||
fg_muted: (160, 160, 165),
|
||||
accent: (0, 112, 243),
|
||||
red: (209, 47, 27),
|
||||
green: (112, 127, 52),
|
||||
yellow: (200, 150, 20),
|
||||
blue: (0, 112, 243),
|
||||
purple: (173, 61, 164), // keyword
|
||||
cyan: (62, 128, 135), // function
|
||||
orange: (120, 73, 42), // preproc
|
||||
tempo_color: (112, 61, 170),
|
||||
bank_color: (0, 112, 243),
|
||||
pattern_color: (62, 128, 135),
|
||||
title_accent: (0, 112, 243),
|
||||
title_author: (112, 61, 170),
|
||||
secondary: (120, 73, 42),
|
||||
link_bright: [
|
||||
(173, 61, 164), (0, 112, 243), (120, 73, 42),
|
||||
(62, 128, 135), (112, 127, 52),
|
||||
],
|
||||
link_dim: [
|
||||
(235, 215, 235), (210, 225, 250), (240, 225, 210),
|
||||
(215, 235, 240), (225, 235, 215),
|
||||
],
|
||||
sparkle: [
|
||||
(0, 112, 243), (173, 61, 164), (112, 127, 52),
|
||||
(62, 128, 135), (120, 73, 42),
|
||||
],
|
||||
meter: [(112, 127, 52), (200, 150, 20), (209, 47, 27)],
|
||||
}
|
||||
}
|
||||
@@ -1,55 +1,86 @@
|
||||
//! Centralized color definitions for Cagire TUI.
|
||||
//! Supports multiple color schemes with runtime switching.
|
||||
|
||||
pub mod palette;
|
||||
pub mod build;
|
||||
mod catppuccin_latte;
|
||||
mod catppuccin_mocha;
|
||||
mod dracula;
|
||||
mod eden;
|
||||
mod ember;
|
||||
mod everforest;
|
||||
mod georges;
|
||||
mod fairyfloss;
|
||||
mod gruvbox_dark;
|
||||
mod hot_dog_stand;
|
||||
mod iceberg;
|
||||
mod jaipur;
|
||||
mod kanagawa;
|
||||
mod letz_light;
|
||||
mod monochrome_black;
|
||||
mod monochrome_white;
|
||||
mod monokai;
|
||||
mod nord;
|
||||
mod fauve;
|
||||
mod pitch_black;
|
||||
mod tropicalia;
|
||||
mod rose_pine;
|
||||
mod tokyo_night;
|
||||
pub mod transform;
|
||||
|
||||
use ratatui::style::Color;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Entry in the theme registry: id, display label, and palette constructor.
|
||||
pub struct ThemeEntry {
|
||||
pub id: &'static str,
|
||||
pub label: &'static str,
|
||||
pub colors: fn() -> ThemeColors,
|
||||
pub palette: fn() -> palette::Palette,
|
||||
}
|
||||
|
||||
/// All available themes.
|
||||
pub const THEMES: &[ThemeEntry] = &[
|
||||
ThemeEntry { id: "CatppuccinMocha", label: "Catppuccin Mocha", colors: catppuccin_mocha::theme },
|
||||
ThemeEntry { id: "CatppuccinLatte", label: "Catppuccin Latte", colors: catppuccin_latte::theme },
|
||||
ThemeEntry { id: "Nord", label: "Nord", colors: nord::theme },
|
||||
ThemeEntry { id: "Dracula", label: "Dracula", colors: dracula::theme },
|
||||
ThemeEntry { id: "GruvboxDark", label: "Gruvbox Dark", colors: gruvbox_dark::theme },
|
||||
ThemeEntry { id: "Monokai", label: "Monokai", colors: monokai::theme },
|
||||
ThemeEntry { id: "MonochromeBlack", label: "Monochrome (Black)", colors: monochrome_black::theme },
|
||||
ThemeEntry { id: "MonochromeWhite", label: "Monochrome (White)", colors: monochrome_white::theme },
|
||||
ThemeEntry { id: "PitchBlack", label: "Pitch Black", colors: pitch_black::theme },
|
||||
ThemeEntry { id: "TokyoNight", label: "Tokyo Night", colors: tokyo_night::theme },
|
||||
ThemeEntry { id: "RosePine", label: "Rosé Pine", colors: rose_pine::theme },
|
||||
ThemeEntry { id: "Kanagawa", label: "Kanagawa", colors: kanagawa::theme },
|
||||
ThemeEntry { id: "CatppuccinMocha", label: "Catppuccin Mocha", palette: catppuccin_mocha::palette },
|
||||
ThemeEntry { id: "CatppuccinLatte", label: "Catppuccin Latte", palette: catppuccin_latte::palette },
|
||||
ThemeEntry { id: "Nord", label: "Nord", palette: nord::palette },
|
||||
ThemeEntry { id: "Dracula", label: "Dracula", palette: dracula::palette },
|
||||
ThemeEntry { id: "GruvboxDark", label: "Gruvbox Dark", palette: gruvbox_dark::palette },
|
||||
ThemeEntry { id: "Monokai", label: "Monokai", palette: monokai::palette },
|
||||
ThemeEntry { id: "MonochromeBlack", label: "Monochrome (Black)", palette: monochrome_black::palette },
|
||||
ThemeEntry { id: "MonochromeWhite", label: "Monochrome (White)", palette: monochrome_white::palette },
|
||||
ThemeEntry { id: "PitchBlack", label: "Pitch Black", palette: pitch_black::palette },
|
||||
ThemeEntry { id: "TokyoNight", label: "Tokyo Night", palette: tokyo_night::palette },
|
||||
ThemeEntry { id: "RosePine", label: "Rosé Pine", palette: rose_pine::palette },
|
||||
ThemeEntry { id: "Kanagawa", label: "Kanagawa", palette: kanagawa::palette },
|
||||
ThemeEntry { id: "Fairyfloss", label: "Fairyfloss", palette: fairyfloss::palette },
|
||||
ThemeEntry { id: "HotDogStand", label: "Hot Dog Stand", palette: hot_dog_stand::palette },
|
||||
ThemeEntry { id: "LetzLight", label: "Letz Light", palette: letz_light::palette },
|
||||
ThemeEntry { id: "Ember", label: "Ember", palette: ember::palette },
|
||||
ThemeEntry { id: "Eden", label: "Eden", palette: eden::palette },
|
||||
ThemeEntry { id: "Georges", label: "Georges", palette: georges::palette },
|
||||
ThemeEntry { id: "Iceberg", label: "Iceberg", palette: iceberg::palette },
|
||||
ThemeEntry { id: "Everforest", label: "Everforest", palette: everforest::palette },
|
||||
ThemeEntry { id: "Fauve", label: "Fauve", palette: fauve::palette },
|
||||
ThemeEntry { id: "Tropicalia", label: "Tropicalia", palette: tropicalia::palette },
|
||||
ThemeEntry { id: "Jaipur", label: "Jaipur", palette: jaipur::palette },
|
||||
];
|
||||
|
||||
thread_local! {
|
||||
static CURRENT_THEME: RefCell<ThemeColors> = RefCell::new((THEMES[0].colors)());
|
||||
static CURRENT_THEME: RefCell<Rc<ThemeColors>> = RefCell::new(Rc::new(build::build(&(THEMES[0].palette)())));
|
||||
}
|
||||
|
||||
pub fn get() -> ThemeColors {
|
||||
CURRENT_THEME.with(|t| t.borrow().clone())
|
||||
/// Return the current thread-local theme (cheap Rc clone, not a deep copy).
|
||||
pub fn get() -> Rc<ThemeColors> {
|
||||
CURRENT_THEME.with(|t| Rc::clone(&t.borrow()))
|
||||
}
|
||||
|
||||
/// Set the current thread-local theme.
|
||||
pub fn set(theme: ThemeColors) {
|
||||
CURRENT_THEME.with(|t| *t.borrow_mut() = theme);
|
||||
CURRENT_THEME.with(|t| *t.borrow_mut() = Rc::new(theme));
|
||||
}
|
||||
|
||||
/// Complete set of resolved colors for all UI components.
|
||||
#[derive(Clone)]
|
||||
pub struct ThemeColors {
|
||||
pub ui: UiColors,
|
||||
@@ -80,6 +111,7 @@ pub struct ThemeColors {
|
||||
pub confirm: ConfirmColors,
|
||||
}
|
||||
|
||||
/// Core UI colors: background, text, borders.
|
||||
#[derive(Clone)]
|
||||
pub struct UiColors {
|
||||
pub bg: Color,
|
||||
@@ -94,6 +126,7 @@ pub struct UiColors {
|
||||
pub surface: Color,
|
||||
}
|
||||
|
||||
/// Playback status bar colors.
|
||||
#[derive(Clone)]
|
||||
pub struct StatusColors {
|
||||
pub playing_bg: Color,
|
||||
@@ -105,6 +138,7 @@ pub struct StatusColors {
|
||||
pub fill_bg: Color,
|
||||
}
|
||||
|
||||
/// Step grid selection and cursor colors.
|
||||
#[derive(Clone)]
|
||||
pub struct SelectionColors {
|
||||
pub cursor_bg: Color,
|
||||
@@ -118,6 +152,7 @@ pub struct SelectionColors {
|
||||
pub in_range: Color,
|
||||
}
|
||||
|
||||
/// Step tile colors for various states.
|
||||
#[derive(Clone)]
|
||||
pub struct TileColors {
|
||||
pub playing_active_bg: Color,
|
||||
@@ -126,6 +161,7 @@ pub struct TileColors {
|
||||
pub playing_inactive_fg: Color,
|
||||
pub active_bg: Color,
|
||||
pub active_fg: Color,
|
||||
pub content_bg: Color,
|
||||
pub inactive_bg: Color,
|
||||
pub inactive_fg: Color,
|
||||
pub active_selected_bg: Color,
|
||||
@@ -134,6 +170,7 @@ pub struct TileColors {
|
||||
pub link_dim: [(u8, u8, u8); 5],
|
||||
}
|
||||
|
||||
/// Top header bar segment colors.
|
||||
#[derive(Clone)]
|
||||
pub struct HeaderColors {
|
||||
pub tempo_bg: Color,
|
||||
@@ -146,6 +183,7 @@ pub struct HeaderColors {
|
||||
pub stats_fg: Color,
|
||||
}
|
||||
|
||||
/// Modal dialog border colors.
|
||||
#[derive(Clone)]
|
||||
pub struct ModalColors {
|
||||
pub border: Color,
|
||||
@@ -159,6 +197,7 @@ pub struct ModalColors {
|
||||
pub preview: Color,
|
||||
}
|
||||
|
||||
/// Flash notification colors.
|
||||
#[derive(Clone)]
|
||||
pub struct FlashColors {
|
||||
pub error_bg: Color,
|
||||
@@ -167,9 +206,9 @@ pub struct FlashColors {
|
||||
pub success_fg: Color,
|
||||
pub info_bg: Color,
|
||||
pub info_fg: Color,
|
||||
pub event_rgb: (u8, u8, u8),
|
||||
}
|
||||
|
||||
/// Pattern list row state colors.
|
||||
#[derive(Clone)]
|
||||
pub struct ListColors {
|
||||
pub playing_bg: Color,
|
||||
@@ -182,8 +221,13 @@ pub struct ListColors {
|
||||
pub edit_fg: Color,
|
||||
pub hover_bg: Color,
|
||||
pub hover_fg: Color,
|
||||
pub muted_bg: Color,
|
||||
pub muted_fg: Color,
|
||||
pub soloed_bg: Color,
|
||||
pub soloed_fg: Color,
|
||||
}
|
||||
|
||||
/// Ableton Link status indicator colors.
|
||||
#[derive(Clone)]
|
||||
pub struct LinkStatusColors {
|
||||
pub disabled: Color,
|
||||
@@ -191,6 +235,7 @@ pub struct LinkStatusColors {
|
||||
pub listening: Color,
|
||||
}
|
||||
|
||||
/// Syntax highlighting (fg, bg) pairs per token category.
|
||||
#[derive(Clone)]
|
||||
pub struct SyntaxColors {
|
||||
pub gap_bg: Color,
|
||||
@@ -211,33 +256,39 @@ pub struct SyntaxColors {
|
||||
pub variable: (Color, Color),
|
||||
pub vary: (Color, Color),
|
||||
pub generator: (Color, Color),
|
||||
pub user_defined: (Color, Color),
|
||||
pub default: (Color, Color),
|
||||
}
|
||||
|
||||
/// Alternating table row colors.
|
||||
#[derive(Clone)]
|
||||
pub struct TableColors {
|
||||
pub row_even: Color,
|
||||
pub row_odd: Color,
|
||||
}
|
||||
|
||||
/// Value display colors.
|
||||
#[derive(Clone)]
|
||||
pub struct ValuesColors {
|
||||
pub tempo: Color,
|
||||
pub value: Color,
|
||||
}
|
||||
|
||||
/// Keyboard hint key/text colors.
|
||||
#[derive(Clone)]
|
||||
pub struct HintColors {
|
||||
pub key: Color,
|
||||
pub text: Color,
|
||||
}
|
||||
|
||||
/// View badge pill colors.
|
||||
#[derive(Clone)]
|
||||
pub struct ViewBadgeColors {
|
||||
pub bg: Color,
|
||||
pub fg: Color,
|
||||
}
|
||||
|
||||
/// Navigation minimap tile colors.
|
||||
#[derive(Clone)]
|
||||
pub struct NavColors {
|
||||
pub selected_bg: Color,
|
||||
@@ -246,6 +297,7 @@ pub struct NavColors {
|
||||
pub unselected_fg: Color,
|
||||
}
|
||||
|
||||
/// Script editor colors.
|
||||
#[derive(Clone)]
|
||||
pub struct EditorWidgetColors {
|
||||
pub cursor_bg: Color,
|
||||
@@ -257,6 +309,7 @@ pub struct EditorWidgetColors {
|
||||
pub completion_example: Color,
|
||||
}
|
||||
|
||||
/// File and sample browser colors.
|
||||
#[derive(Clone)]
|
||||
pub struct BrowserColors {
|
||||
pub directory: Color,
|
||||
@@ -271,6 +324,7 @@ pub struct BrowserColors {
|
||||
pub empty_text: Color,
|
||||
}
|
||||
|
||||
/// Text input field colors.
|
||||
#[derive(Clone)]
|
||||
pub struct InputColors {
|
||||
pub text: Color,
|
||||
@@ -278,6 +332,7 @@ pub struct InputColors {
|
||||
pub hint: Color,
|
||||
}
|
||||
|
||||
/// Search bar and match highlight colors.
|
||||
#[derive(Clone)]
|
||||
pub struct SearchColors {
|
||||
pub active: Color,
|
||||
@@ -286,6 +341,7 @@ pub struct SearchColors {
|
||||
pub match_fg: Color,
|
||||
}
|
||||
|
||||
/// Markdown renderer colors.
|
||||
#[derive(Clone)]
|
||||
pub struct MarkdownColors {
|
||||
pub h1: Color,
|
||||
@@ -300,6 +356,7 @@ pub struct MarkdownColors {
|
||||
pub list: Color,
|
||||
}
|
||||
|
||||
/// Engine view panel colors.
|
||||
#[derive(Clone)]
|
||||
pub struct EngineColors {
|
||||
pub header: Color,
|
||||
@@ -322,6 +379,7 @@ pub struct EngineColors {
|
||||
pub hint_inactive: Color,
|
||||
}
|
||||
|
||||
/// Dictionary view colors.
|
||||
#[derive(Clone)]
|
||||
pub struct DictColors {
|
||||
pub word_name: Color,
|
||||
@@ -339,6 +397,7 @@ pub struct DictColors {
|
||||
pub header_desc: Color,
|
||||
}
|
||||
|
||||
/// Title screen colors.
|
||||
#[derive(Clone)]
|
||||
pub struct TitleColors {
|
||||
pub big_title: Color,
|
||||
@@ -349,6 +408,7 @@ pub struct TitleColors {
|
||||
pub subtitle: Color,
|
||||
}
|
||||
|
||||
/// VU meter and spectrum level colors.
|
||||
#[derive(Clone)]
|
||||
pub struct MeterColors {
|
||||
pub low: Color,
|
||||
@@ -359,11 +419,13 @@ pub struct MeterColors {
|
||||
pub high_rgb: (u8, u8, u8),
|
||||
}
|
||||
|
||||
/// Sparkle particle colors.
|
||||
#[derive(Clone)]
|
||||
pub struct SparkleColors {
|
||||
pub colors: [(u8, u8, u8); 5],
|
||||
}
|
||||
|
||||
/// Confirm dialog colors.
|
||||
#[derive(Clone)]
|
||||
pub struct ConfirmColors {
|
||||
pub border: Color,
|
||||
|
||||
@@ -1,275 +1,41 @@
|
||||
use super::*;
|
||||
use ratatui::style::Color;
|
||||
//! Monochrome (black background) palette.
|
||||
|
||||
pub fn theme() -> ThemeColors {
|
||||
let bg = Color::Rgb(0, 0, 0);
|
||||
let surface = Color::Rgb(18, 18, 18);
|
||||
let surface2 = Color::Rgb(30, 30, 30);
|
||||
let border = Color::Rgb(60, 60, 60);
|
||||
let fg = Color::Rgb(255, 255, 255);
|
||||
let fg_dim = Color::Rgb(180, 180, 180);
|
||||
let fg_muted = Color::Rgb(120, 120, 120);
|
||||
use super::palette::Palette;
|
||||
|
||||
let bright = Color::Rgb(255, 255, 255);
|
||||
let medium = Color::Rgb(180, 180, 180);
|
||||
let dim = Color::Rgb(120, 120, 120);
|
||||
let dark = Color::Rgb(80, 80, 80);
|
||||
let darker = Color::Rgb(50, 50, 50);
|
||||
|
||||
ThemeColors {
|
||||
ui: UiColors {
|
||||
bg,
|
||||
bg_rgb: (0, 0, 0),
|
||||
text_primary: fg,
|
||||
text_muted: fg_dim,
|
||||
text_dim: fg_muted,
|
||||
border,
|
||||
header: bright,
|
||||
unfocused: fg_muted,
|
||||
accent: bright,
|
||||
surface,
|
||||
},
|
||||
status: StatusColors {
|
||||
playing_bg: Color::Rgb(40, 40, 40),
|
||||
playing_fg: bright,
|
||||
stopped_bg: Color::Rgb(25, 25, 25),
|
||||
stopped_fg: medium,
|
||||
fill_on: bright,
|
||||
fill_off: dark,
|
||||
fill_bg: surface,
|
||||
},
|
||||
selection: SelectionColors {
|
||||
cursor_bg: bright,
|
||||
cursor_fg: bg,
|
||||
selected_bg: Color::Rgb(60, 60, 60),
|
||||
selected_fg: bright,
|
||||
in_range_bg: Color::Rgb(40, 40, 40),
|
||||
in_range_fg: fg,
|
||||
cursor: bright,
|
||||
selected: Color::Rgb(60, 60, 60),
|
||||
in_range: Color::Rgb(40, 40, 40),
|
||||
},
|
||||
tile: TileColors {
|
||||
playing_active_bg: Color::Rgb(70, 70, 70),
|
||||
playing_active_fg: bright,
|
||||
playing_inactive_bg: Color::Rgb(50, 50, 50),
|
||||
playing_inactive_fg: medium,
|
||||
active_bg: Color::Rgb(45, 45, 45),
|
||||
active_fg: bright,
|
||||
inactive_bg: surface,
|
||||
inactive_fg: fg_dim,
|
||||
active_selected_bg: Color::Rgb(80, 80, 80),
|
||||
active_in_range_bg: Color::Rgb(55, 55, 55),
|
||||
pub fn palette() -> Palette {
|
||||
Palette {
|
||||
bg: (0, 0, 0),
|
||||
surface: (18, 18, 18),
|
||||
surface2: (30, 30, 30),
|
||||
fg: (255, 255, 255),
|
||||
fg_dim: (180, 180, 180),
|
||||
fg_muted: (120, 120, 120),
|
||||
accent: (255, 255, 255),
|
||||
red: (180, 180, 180),
|
||||
green: (255, 255, 255),
|
||||
yellow: (180, 180, 180),
|
||||
blue: (180, 180, 180),
|
||||
purple: (180, 180, 180),
|
||||
cyan: (255, 255, 255),
|
||||
orange: (255, 255, 255),
|
||||
tempo_color: (255, 255, 255),
|
||||
bank_color: (180, 180, 180),
|
||||
pattern_color: (180, 180, 180),
|
||||
title_accent: (255, 255, 255),
|
||||
title_author: (180, 180, 180),
|
||||
secondary: (120, 120, 120),
|
||||
link_bright: [
|
||||
(255, 255, 255),
|
||||
(200, 200, 200),
|
||||
(160, 160, 160),
|
||||
(220, 220, 220),
|
||||
(180, 180, 180),
|
||||
(255, 255, 255), (200, 200, 200), (160, 160, 160),
|
||||
(220, 220, 220), (180, 180, 180),
|
||||
],
|
||||
link_dim: [
|
||||
(60, 60, 60),
|
||||
(50, 50, 50),
|
||||
(45, 45, 45),
|
||||
(55, 55, 55),
|
||||
(48, 48, 48),
|
||||
(60, 60, 60), (50, 50, 50), (45, 45, 45),
|
||||
(55, 55, 55), (48, 48, 48),
|
||||
],
|
||||
},
|
||||
header: HeaderColors {
|
||||
tempo_bg: Color::Rgb(50, 50, 50),
|
||||
tempo_fg: bright,
|
||||
bank_bg: Color::Rgb(40, 40, 40),
|
||||
bank_fg: medium,
|
||||
pattern_bg: Color::Rgb(35, 35, 35),
|
||||
pattern_fg: medium,
|
||||
stats_bg: surface,
|
||||
stats_fg: fg_dim,
|
||||
},
|
||||
modal: ModalColors {
|
||||
border: bright,
|
||||
border_accent: medium,
|
||||
border_warn: fg_dim,
|
||||
border_dim: fg_muted,
|
||||
confirm: medium,
|
||||
rename: medium,
|
||||
input: bright,
|
||||
editor: bright,
|
||||
preview: fg_muted,
|
||||
},
|
||||
flash: FlashColors {
|
||||
error_bg: Color::Rgb(60, 60, 60),
|
||||
error_fg: bright,
|
||||
success_bg: Color::Rgb(50, 50, 50),
|
||||
success_fg: bright,
|
||||
info_bg: surface,
|
||||
info_fg: fg,
|
||||
event_rgb: (40, 40, 40),
|
||||
},
|
||||
list: ListColors {
|
||||
playing_bg: Color::Rgb(50, 50, 50),
|
||||
playing_fg: bright,
|
||||
staged_play_bg: Color::Rgb(45, 45, 45),
|
||||
staged_play_fg: medium,
|
||||
staged_stop_bg: Color::Rgb(35, 35, 35),
|
||||
staged_stop_fg: dim,
|
||||
edit_bg: Color::Rgb(40, 40, 40),
|
||||
edit_fg: bright,
|
||||
hover_bg: surface2,
|
||||
hover_fg: fg,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: dim,
|
||||
connected: bright,
|
||||
listening: medium,
|
||||
},
|
||||
syntax: SyntaxColors {
|
||||
gap_bg: bg,
|
||||
executed_bg: Color::Rgb(35, 35, 35),
|
||||
selected_bg: Color::Rgb(55, 55, 55),
|
||||
emit: (bright, Color::Rgb(45, 45, 45)),
|
||||
number: (medium, Color::Rgb(35, 35, 35)),
|
||||
string: (bright, Color::Rgb(40, 40, 40)),
|
||||
comment: (dark, bg),
|
||||
keyword: (bright, Color::Rgb(50, 50, 50)),
|
||||
stack_op: (medium, Color::Rgb(30, 30, 30)),
|
||||
operator: (medium, Color::Rgb(35, 35, 35)),
|
||||
sound: (bright, Color::Rgb(45, 45, 45)),
|
||||
param: (medium, Color::Rgb(35, 35, 35)),
|
||||
context: (medium, Color::Rgb(30, 30, 30)),
|
||||
note: (bright, Color::Rgb(40, 40, 40)),
|
||||
interval: (medium, Color::Rgb(35, 35, 35)),
|
||||
variable: (medium, Color::Rgb(30, 30, 30)),
|
||||
vary: (dim, Color::Rgb(25, 25, 25)),
|
||||
generator: (bright, Color::Rgb(45, 45, 45)),
|
||||
default: (fg_dim, bg),
|
||||
},
|
||||
table: TableColors {
|
||||
row_even: bg,
|
||||
row_odd: surface,
|
||||
},
|
||||
values: ValuesColors {
|
||||
tempo: bright,
|
||||
value: fg_dim,
|
||||
},
|
||||
hint: HintColors {
|
||||
key: bright,
|
||||
text: fg_muted,
|
||||
},
|
||||
view_badge: ViewBadgeColors { bg: fg, fg: bg },
|
||||
nav: NavColors {
|
||||
selected_bg: Color::Rgb(60, 60, 60),
|
||||
selected_fg: fg,
|
||||
unselected_bg: surface,
|
||||
unselected_fg: fg_muted,
|
||||
},
|
||||
editor_widget: EditorWidgetColors {
|
||||
cursor_bg: fg,
|
||||
cursor_fg: bg,
|
||||
selection_bg: Color::Rgb(60, 60, 60),
|
||||
completion_bg: surface,
|
||||
completion_fg: fg,
|
||||
completion_selected: bright,
|
||||
completion_example: medium,
|
||||
},
|
||||
browser: BrowserColors {
|
||||
directory: medium,
|
||||
project_file: bright,
|
||||
selected: bright,
|
||||
file: fg,
|
||||
focused_border: bright,
|
||||
unfocused_border: fg_muted,
|
||||
root: fg,
|
||||
file_icon: fg_muted,
|
||||
folder_icon: medium,
|
||||
empty_text: fg_muted,
|
||||
},
|
||||
input: InputColors {
|
||||
text: bright,
|
||||
cursor: fg,
|
||||
hint: fg_muted,
|
||||
},
|
||||
search: SearchColors {
|
||||
active: bright,
|
||||
inactive: fg_muted,
|
||||
match_bg: bright,
|
||||
match_fg: bg,
|
||||
},
|
||||
markdown: MarkdownColors {
|
||||
h1: bright,
|
||||
h2: medium,
|
||||
h3: dim,
|
||||
code: medium,
|
||||
code_border: Color::Rgb(60, 60, 60),
|
||||
link: bright,
|
||||
link_url: dim,
|
||||
quote: fg_muted,
|
||||
text: fg,
|
||||
list: fg,
|
||||
},
|
||||
engine: EngineColors {
|
||||
header: bright,
|
||||
header_focused: bright,
|
||||
divider: Color::Rgb(50, 50, 50),
|
||||
scroll_indicator: Color::Rgb(70, 70, 70),
|
||||
label: dim,
|
||||
label_focused: medium,
|
||||
label_dim: dark,
|
||||
value: fg,
|
||||
focused: bright,
|
||||
normal: fg,
|
||||
dim: dark,
|
||||
path: dim,
|
||||
border_magenta: medium,
|
||||
border_green: medium,
|
||||
border_cyan: medium,
|
||||
separator: Color::Rgb(50, 50, 50),
|
||||
hint_active: bright,
|
||||
hint_inactive: darker,
|
||||
},
|
||||
dict: DictColors {
|
||||
word_name: bright,
|
||||
word_bg: Color::Rgb(30, 30, 30),
|
||||
alias: fg_muted,
|
||||
stack_sig: medium,
|
||||
description: fg,
|
||||
example: dim,
|
||||
category_focused: bright,
|
||||
category_selected: medium,
|
||||
category_normal: fg,
|
||||
category_dimmed: dark,
|
||||
border_focused: bright,
|
||||
border_normal: darker,
|
||||
header_desc: dim,
|
||||
},
|
||||
title: TitleColors {
|
||||
big_title: bright,
|
||||
author: medium,
|
||||
link: medium,
|
||||
license: dim,
|
||||
prompt: dim,
|
||||
subtitle: fg,
|
||||
},
|
||||
meter: MeterColors {
|
||||
low: dim,
|
||||
mid: medium,
|
||||
high: bright,
|
||||
low_rgb: (120, 120, 120),
|
||||
mid_rgb: (180, 180, 180),
|
||||
high_rgb: (255, 255, 255),
|
||||
},
|
||||
sparkle: SparkleColors {
|
||||
colors: [
|
||||
(255, 255, 255),
|
||||
(200, 200, 200),
|
||||
(160, 160, 160),
|
||||
(220, 220, 220),
|
||||
(180, 180, 180),
|
||||
sparkle: [
|
||||
(255, 255, 255), (200, 200, 200), (160, 160, 160),
|
||||
(220, 220, 220), (180, 180, 180),
|
||||
],
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: bright,
|
||||
button_selected_bg: bright,
|
||||
button_selected_fg: bg,
|
||||
},
|
||||
meter: [(120, 120, 120), (180, 180, 180), (255, 255, 255)],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,275 +1,41 @@
|
||||
use super::*;
|
||||
use ratatui::style::Color;
|
||||
//! Monochrome (white background) palette.
|
||||
|
||||
pub fn theme() -> ThemeColors {
|
||||
let bg = Color::Rgb(255, 255, 255);
|
||||
let surface = Color::Rgb(240, 240, 240);
|
||||
let surface2 = Color::Rgb(225, 225, 225);
|
||||
let border = Color::Rgb(180, 180, 180);
|
||||
let fg = Color::Rgb(0, 0, 0);
|
||||
let fg_dim = Color::Rgb(80, 80, 80);
|
||||
let fg_muted = Color::Rgb(140, 140, 140);
|
||||
use super::palette::Palette;
|
||||
|
||||
let dark = Color::Rgb(0, 0, 0);
|
||||
let medium = Color::Rgb(80, 80, 80);
|
||||
let dim = Color::Rgb(140, 140, 140);
|
||||
let light = Color::Rgb(180, 180, 180);
|
||||
let lighter = Color::Rgb(210, 210, 210);
|
||||
|
||||
ThemeColors {
|
||||
ui: UiColors {
|
||||
bg,
|
||||
bg_rgb: (255, 255, 255),
|
||||
text_primary: fg,
|
||||
text_muted: fg_dim,
|
||||
text_dim: fg_muted,
|
||||
border,
|
||||
header: dark,
|
||||
unfocused: fg_muted,
|
||||
accent: dark,
|
||||
surface,
|
||||
},
|
||||
status: StatusColors {
|
||||
playing_bg: Color::Rgb(210, 210, 210),
|
||||
playing_fg: dark,
|
||||
stopped_bg: Color::Rgb(230, 230, 230),
|
||||
stopped_fg: medium,
|
||||
fill_on: dark,
|
||||
fill_off: light,
|
||||
fill_bg: surface,
|
||||
},
|
||||
selection: SelectionColors {
|
||||
cursor_bg: dark,
|
||||
cursor_fg: bg,
|
||||
selected_bg: Color::Rgb(200, 200, 200),
|
||||
selected_fg: dark,
|
||||
in_range_bg: Color::Rgb(220, 220, 220),
|
||||
in_range_fg: fg,
|
||||
cursor: dark,
|
||||
selected: Color::Rgb(200, 200, 200),
|
||||
in_range: Color::Rgb(220, 220, 220),
|
||||
},
|
||||
tile: TileColors {
|
||||
playing_active_bg: Color::Rgb(180, 180, 180),
|
||||
playing_active_fg: dark,
|
||||
playing_inactive_bg: Color::Rgb(200, 200, 200),
|
||||
playing_inactive_fg: medium,
|
||||
active_bg: Color::Rgb(210, 210, 210),
|
||||
active_fg: dark,
|
||||
inactive_bg: surface,
|
||||
inactive_fg: fg_dim,
|
||||
active_selected_bg: Color::Rgb(170, 170, 170),
|
||||
active_in_range_bg: Color::Rgb(195, 195, 195),
|
||||
pub fn palette() -> Palette {
|
||||
Palette {
|
||||
bg: (255, 255, 255),
|
||||
surface: (240, 240, 240),
|
||||
surface2: (225, 225, 225),
|
||||
fg: (0, 0, 0),
|
||||
fg_dim: (80, 80, 80),
|
||||
fg_muted: (140, 140, 140),
|
||||
accent: (0, 0, 0),
|
||||
red: (140, 140, 140),
|
||||
green: (0, 0, 0),
|
||||
yellow: (80, 80, 80),
|
||||
blue: (80, 80, 80),
|
||||
purple: (80, 80, 80),
|
||||
cyan: (0, 0, 0),
|
||||
orange: (0, 0, 0),
|
||||
tempo_color: (0, 0, 0),
|
||||
bank_color: (80, 80, 80),
|
||||
pattern_color: (80, 80, 80),
|
||||
title_accent: (0, 0, 0),
|
||||
title_author: (80, 80, 80),
|
||||
secondary: (140, 140, 140),
|
||||
link_bright: [
|
||||
(0, 0, 0),
|
||||
(60, 60, 60),
|
||||
(100, 100, 100),
|
||||
(40, 40, 40),
|
||||
(80, 80, 80),
|
||||
(0, 0, 0), (60, 60, 60), (100, 100, 100),
|
||||
(40, 40, 40), (80, 80, 80),
|
||||
],
|
||||
link_dim: [
|
||||
(200, 200, 200),
|
||||
(210, 210, 210),
|
||||
(215, 215, 215),
|
||||
(205, 205, 205),
|
||||
(212, 212, 212),
|
||||
(200, 200, 200), (210, 210, 210), (215, 215, 215),
|
||||
(205, 205, 205), (212, 212, 212),
|
||||
],
|
||||
},
|
||||
header: HeaderColors {
|
||||
tempo_bg: Color::Rgb(200, 200, 200),
|
||||
tempo_fg: dark,
|
||||
bank_bg: Color::Rgb(215, 215, 215),
|
||||
bank_fg: medium,
|
||||
pattern_bg: Color::Rgb(220, 220, 220),
|
||||
pattern_fg: medium,
|
||||
stats_bg: surface,
|
||||
stats_fg: fg_dim,
|
||||
},
|
||||
modal: ModalColors {
|
||||
border: dark,
|
||||
border_accent: medium,
|
||||
border_warn: fg_dim,
|
||||
border_dim: fg_muted,
|
||||
confirm: medium,
|
||||
rename: medium,
|
||||
input: dark,
|
||||
editor: dark,
|
||||
preview: fg_muted,
|
||||
},
|
||||
flash: FlashColors {
|
||||
error_bg: Color::Rgb(200, 200, 200),
|
||||
error_fg: dark,
|
||||
success_bg: Color::Rgb(210, 210, 210),
|
||||
success_fg: dark,
|
||||
info_bg: surface,
|
||||
info_fg: fg,
|
||||
event_rgb: (220, 220, 220),
|
||||
},
|
||||
list: ListColors {
|
||||
playing_bg: Color::Rgb(200, 200, 200),
|
||||
playing_fg: dark,
|
||||
staged_play_bg: Color::Rgb(210, 210, 210),
|
||||
staged_play_fg: medium,
|
||||
staged_stop_bg: Color::Rgb(220, 220, 220),
|
||||
staged_stop_fg: dim,
|
||||
edit_bg: Color::Rgb(215, 215, 215),
|
||||
edit_fg: dark,
|
||||
hover_bg: surface2,
|
||||
hover_fg: fg,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: dim,
|
||||
connected: dark,
|
||||
listening: medium,
|
||||
},
|
||||
syntax: SyntaxColors {
|
||||
gap_bg: bg,
|
||||
executed_bg: Color::Rgb(220, 220, 220),
|
||||
selected_bg: Color::Rgb(200, 200, 200),
|
||||
emit: (dark, Color::Rgb(215, 215, 215)),
|
||||
number: (medium, Color::Rgb(225, 225, 225)),
|
||||
string: (dark, Color::Rgb(220, 220, 220)),
|
||||
comment: (light, bg),
|
||||
keyword: (dark, Color::Rgb(205, 205, 205)),
|
||||
stack_op: (medium, Color::Rgb(230, 230, 230)),
|
||||
operator: (medium, Color::Rgb(225, 225, 225)),
|
||||
sound: (dark, Color::Rgb(215, 215, 215)),
|
||||
param: (medium, Color::Rgb(225, 225, 225)),
|
||||
context: (medium, Color::Rgb(230, 230, 230)),
|
||||
note: (dark, Color::Rgb(220, 220, 220)),
|
||||
interval: (medium, Color::Rgb(225, 225, 225)),
|
||||
variable: (medium, Color::Rgb(230, 230, 230)),
|
||||
vary: (dim, Color::Rgb(235, 235, 235)),
|
||||
generator: (dark, Color::Rgb(215, 215, 215)),
|
||||
default: (fg_dim, bg),
|
||||
},
|
||||
table: TableColors {
|
||||
row_even: bg,
|
||||
row_odd: surface,
|
||||
},
|
||||
values: ValuesColors {
|
||||
tempo: dark,
|
||||
value: fg_dim,
|
||||
},
|
||||
hint: HintColors {
|
||||
key: dark,
|
||||
text: fg_muted,
|
||||
},
|
||||
view_badge: ViewBadgeColors { bg: fg, fg: bg },
|
||||
nav: NavColors {
|
||||
selected_bg: Color::Rgb(200, 200, 200),
|
||||
selected_fg: fg,
|
||||
unselected_bg: surface,
|
||||
unselected_fg: fg_muted,
|
||||
},
|
||||
editor_widget: EditorWidgetColors {
|
||||
cursor_bg: fg,
|
||||
cursor_fg: bg,
|
||||
selection_bg: Color::Rgb(200, 200, 200),
|
||||
completion_bg: surface,
|
||||
completion_fg: fg,
|
||||
completion_selected: dark,
|
||||
completion_example: medium,
|
||||
},
|
||||
browser: BrowserColors {
|
||||
directory: medium,
|
||||
project_file: dark,
|
||||
selected: dark,
|
||||
file: fg,
|
||||
focused_border: dark,
|
||||
unfocused_border: fg_muted,
|
||||
root: fg,
|
||||
file_icon: fg_muted,
|
||||
folder_icon: medium,
|
||||
empty_text: fg_muted,
|
||||
},
|
||||
input: InputColors {
|
||||
text: dark,
|
||||
cursor: fg,
|
||||
hint: fg_muted,
|
||||
},
|
||||
search: SearchColors {
|
||||
active: dark,
|
||||
inactive: fg_muted,
|
||||
match_bg: dark,
|
||||
match_fg: bg,
|
||||
},
|
||||
markdown: MarkdownColors {
|
||||
h1: dark,
|
||||
h2: medium,
|
||||
h3: dim,
|
||||
code: medium,
|
||||
code_border: Color::Rgb(200, 200, 200),
|
||||
link: dark,
|
||||
link_url: dim,
|
||||
quote: fg_muted,
|
||||
text: fg,
|
||||
list: fg,
|
||||
},
|
||||
engine: EngineColors {
|
||||
header: dark,
|
||||
header_focused: dark,
|
||||
divider: Color::Rgb(210, 210, 210),
|
||||
scroll_indicator: Color::Rgb(180, 180, 180),
|
||||
label: dim,
|
||||
label_focused: medium,
|
||||
label_dim: light,
|
||||
value: fg,
|
||||
focused: dark,
|
||||
normal: fg,
|
||||
dim: light,
|
||||
path: dim,
|
||||
border_magenta: medium,
|
||||
border_green: medium,
|
||||
border_cyan: medium,
|
||||
separator: Color::Rgb(210, 210, 210),
|
||||
hint_active: dark,
|
||||
hint_inactive: lighter,
|
||||
},
|
||||
dict: DictColors {
|
||||
word_name: dark,
|
||||
word_bg: Color::Rgb(230, 230, 230),
|
||||
alias: fg_muted,
|
||||
stack_sig: medium,
|
||||
description: fg,
|
||||
example: dim,
|
||||
category_focused: dark,
|
||||
category_selected: medium,
|
||||
category_normal: fg,
|
||||
category_dimmed: light,
|
||||
border_focused: dark,
|
||||
border_normal: lighter,
|
||||
header_desc: dim,
|
||||
},
|
||||
title: TitleColors {
|
||||
big_title: dark,
|
||||
author: medium,
|
||||
link: medium,
|
||||
license: dim,
|
||||
prompt: dim,
|
||||
subtitle: fg,
|
||||
},
|
||||
meter: MeterColors {
|
||||
low: dim,
|
||||
mid: medium,
|
||||
high: dark,
|
||||
low_rgb: (140, 140, 140),
|
||||
mid_rgb: (80, 80, 80),
|
||||
high_rgb: (0, 0, 0),
|
||||
},
|
||||
sparkle: SparkleColors {
|
||||
colors: [
|
||||
(0, 0, 0),
|
||||
(60, 60, 60),
|
||||
(100, 100, 100),
|
||||
(40, 40, 40),
|
||||
(80, 80, 80),
|
||||
sparkle: [
|
||||
(0, 0, 0), (60, 60, 60), (100, 100, 100),
|
||||
(40, 40, 40), (80, 80, 80),
|
||||
],
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: dark,
|
||||
button_selected_bg: dark,
|
||||
button_selected_fg: bg,
|
||||
},
|
||||
meter: [(140, 140, 140), (80, 80, 80), (0, 0, 0)],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,276 +1,41 @@
|
||||
use super::*;
|
||||
use ratatui::style::Color;
|
||||
//! Monokai palette.
|
||||
|
||||
pub fn theme() -> ThemeColors {
|
||||
let bg = Color::Rgb(39, 40, 34);
|
||||
let bg_light = Color::Rgb(53, 54, 47);
|
||||
let bg_lighter = Color::Rgb(70, 71, 62);
|
||||
let fg = Color::Rgb(248, 248, 242);
|
||||
let fg_dim = Color::Rgb(190, 190, 180);
|
||||
let comment = Color::Rgb(117, 113, 94);
|
||||
let pink = Color::Rgb(249, 38, 114);
|
||||
let green = Color::Rgb(166, 226, 46);
|
||||
let yellow = Color::Rgb(230, 219, 116);
|
||||
let blue = Color::Rgb(102, 217, 239);
|
||||
let purple = Color::Rgb(174, 129, 255);
|
||||
let orange = Color::Rgb(253, 151, 31);
|
||||
use super::palette::Palette;
|
||||
|
||||
let darker_bg = Color::Rgb(30, 31, 26);
|
||||
|
||||
ThemeColors {
|
||||
ui: UiColors {
|
||||
bg,
|
||||
bg_rgb: (39, 40, 34),
|
||||
text_primary: fg,
|
||||
text_muted: fg_dim,
|
||||
text_dim: comment,
|
||||
border: bg_lighter,
|
||||
header: blue,
|
||||
unfocused: comment,
|
||||
accent: pink,
|
||||
surface: bg_light,
|
||||
},
|
||||
status: StatusColors {
|
||||
playing_bg: Color::Rgb(50, 65, 40),
|
||||
playing_fg: green,
|
||||
stopped_bg: Color::Rgb(70, 40, 55),
|
||||
stopped_fg: pink,
|
||||
fill_on: green,
|
||||
fill_off: comment,
|
||||
fill_bg: bg_light,
|
||||
},
|
||||
selection: SelectionColors {
|
||||
cursor_bg: pink,
|
||||
cursor_fg: bg,
|
||||
selected_bg: Color::Rgb(85, 70, 80),
|
||||
selected_fg: pink,
|
||||
in_range_bg: Color::Rgb(70, 65, 70),
|
||||
in_range_fg: fg,
|
||||
cursor: pink,
|
||||
selected: Color::Rgb(85, 70, 80),
|
||||
in_range: Color::Rgb(70, 65, 70),
|
||||
},
|
||||
tile: TileColors {
|
||||
playing_active_bg: Color::Rgb(90, 65, 45),
|
||||
playing_active_fg: orange,
|
||||
playing_inactive_bg: Color::Rgb(80, 75, 50),
|
||||
playing_inactive_fg: yellow,
|
||||
active_bg: Color::Rgb(55, 75, 70),
|
||||
active_fg: blue,
|
||||
inactive_bg: bg_light,
|
||||
inactive_fg: fg_dim,
|
||||
active_selected_bg: Color::Rgb(85, 65, 80),
|
||||
active_in_range_bg: Color::Rgb(70, 65, 70),
|
||||
pub fn palette() -> Palette {
|
||||
Palette {
|
||||
bg: (39, 40, 34),
|
||||
surface: (53, 54, 47),
|
||||
surface2: (70, 71, 62),
|
||||
fg: (248, 248, 242),
|
||||
fg_dim: (190, 190, 180),
|
||||
fg_muted: (117, 113, 94),
|
||||
accent: (249, 38, 114), // pink
|
||||
red: (249, 38, 114),
|
||||
green: (166, 226, 46),
|
||||
yellow: (230, 219, 116),
|
||||
blue: (102, 217, 239),
|
||||
purple: (174, 129, 255),
|
||||
cyan: (102, 217, 239),
|
||||
orange: (253, 151, 31),
|
||||
tempo_color: (249, 38, 114),
|
||||
bank_color: (102, 217, 239),
|
||||
pattern_color: (166, 226, 46),
|
||||
title_accent: (249, 38, 114),
|
||||
title_author: (102, 217, 239),
|
||||
secondary: (253, 151, 31),
|
||||
link_bright: [
|
||||
(249, 38, 114),
|
||||
(174, 129, 255),
|
||||
(253, 151, 31),
|
||||
(102, 217, 239),
|
||||
(166, 226, 46),
|
||||
(249, 38, 114), (174, 129, 255), (253, 151, 31),
|
||||
(102, 217, 239), (166, 226, 46),
|
||||
],
|
||||
link_dim: [
|
||||
(90, 40, 60),
|
||||
(70, 55, 90),
|
||||
(85, 60, 35),
|
||||
(50, 75, 85),
|
||||
(60, 80, 40),
|
||||
(90, 40, 60), (70, 55, 90), (85, 60, 35),
|
||||
(50, 75, 85), (60, 80, 40),
|
||||
],
|
||||
},
|
||||
header: HeaderColors {
|
||||
tempo_bg: Color::Rgb(75, 50, 65),
|
||||
tempo_fg: pink,
|
||||
bank_bg: Color::Rgb(50, 70, 75),
|
||||
bank_fg: blue,
|
||||
pattern_bg: Color::Rgb(55, 75, 50),
|
||||
pattern_fg: green,
|
||||
stats_bg: bg_light,
|
||||
stats_fg: fg_dim,
|
||||
},
|
||||
modal: ModalColors {
|
||||
border: blue,
|
||||
border_accent: pink,
|
||||
border_warn: orange,
|
||||
border_dim: comment,
|
||||
confirm: orange,
|
||||
rename: purple,
|
||||
input: blue,
|
||||
editor: blue,
|
||||
preview: comment,
|
||||
},
|
||||
flash: FlashColors {
|
||||
error_bg: Color::Rgb(75, 40, 55),
|
||||
error_fg: pink,
|
||||
success_bg: Color::Rgb(50, 70, 45),
|
||||
success_fg: green,
|
||||
info_bg: bg_light,
|
||||
info_fg: fg,
|
||||
event_rgb: (70, 55, 70),
|
||||
},
|
||||
list: ListColors {
|
||||
playing_bg: Color::Rgb(50, 70, 45),
|
||||
playing_fg: green,
|
||||
staged_play_bg: Color::Rgb(70, 55, 80),
|
||||
staged_play_fg: purple,
|
||||
staged_stop_bg: Color::Rgb(80, 45, 60),
|
||||
staged_stop_fg: pink,
|
||||
edit_bg: Color::Rgb(50, 70, 70),
|
||||
edit_fg: blue,
|
||||
hover_bg: bg_lighter,
|
||||
hover_fg: fg,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: pink,
|
||||
connected: green,
|
||||
listening: yellow,
|
||||
},
|
||||
syntax: SyntaxColors {
|
||||
gap_bg: darker_bg,
|
||||
executed_bg: Color::Rgb(55, 50, 55),
|
||||
selected_bg: Color::Rgb(85, 75, 50),
|
||||
emit: (fg, Color::Rgb(85, 55, 65)),
|
||||
number: (purple, Color::Rgb(60, 50, 75)),
|
||||
string: (yellow, Color::Rgb(70, 65, 45)),
|
||||
comment: (comment, darker_bg),
|
||||
keyword: (pink, Color::Rgb(80, 45, 60)),
|
||||
stack_op: (blue, Color::Rgb(50, 70, 75)),
|
||||
operator: (pink, Color::Rgb(80, 45, 60)),
|
||||
sound: (blue, Color::Rgb(50, 70, 75)),
|
||||
param: (orange, Color::Rgb(80, 60, 40)),
|
||||
context: (orange, Color::Rgb(80, 60, 40)),
|
||||
note: (green, Color::Rgb(55, 75, 45)),
|
||||
interval: (Color::Rgb(180, 235, 80), Color::Rgb(55, 75, 40)),
|
||||
variable: (green, Color::Rgb(55, 75, 45)),
|
||||
vary: (yellow, Color::Rgb(70, 65, 45)),
|
||||
generator: (blue, Color::Rgb(50, 70, 70)),
|
||||
default: (fg_dim, darker_bg),
|
||||
},
|
||||
table: TableColors {
|
||||
row_even: darker_bg,
|
||||
row_odd: bg,
|
||||
},
|
||||
values: ValuesColors {
|
||||
tempo: orange,
|
||||
value: fg_dim,
|
||||
},
|
||||
hint: HintColors {
|
||||
key: orange,
|
||||
text: comment,
|
||||
},
|
||||
view_badge: ViewBadgeColors { bg: fg, fg: bg },
|
||||
nav: NavColors {
|
||||
selected_bg: Color::Rgb(80, 60, 75),
|
||||
selected_fg: fg,
|
||||
unselected_bg: bg_light,
|
||||
unselected_fg: comment,
|
||||
},
|
||||
editor_widget: EditorWidgetColors {
|
||||
cursor_bg: fg,
|
||||
cursor_fg: bg,
|
||||
selection_bg: Color::Rgb(75, 70, 75),
|
||||
completion_bg: bg_light,
|
||||
completion_fg: fg,
|
||||
completion_selected: orange,
|
||||
completion_example: blue,
|
||||
},
|
||||
browser: BrowserColors {
|
||||
directory: blue,
|
||||
project_file: purple,
|
||||
selected: orange,
|
||||
file: fg,
|
||||
focused_border: orange,
|
||||
unfocused_border: comment,
|
||||
root: fg,
|
||||
file_icon: comment,
|
||||
folder_icon: blue,
|
||||
empty_text: comment,
|
||||
},
|
||||
input: InputColors {
|
||||
text: blue,
|
||||
cursor: fg,
|
||||
hint: comment,
|
||||
},
|
||||
search: SearchColors {
|
||||
active: orange,
|
||||
inactive: comment,
|
||||
match_bg: yellow,
|
||||
match_fg: bg,
|
||||
},
|
||||
markdown: MarkdownColors {
|
||||
h1: blue,
|
||||
h2: orange,
|
||||
h3: purple,
|
||||
code: green,
|
||||
code_border: Color::Rgb(85, 85, 75),
|
||||
link: pink,
|
||||
link_url: Color::Rgb(130, 125, 115),
|
||||
quote: comment,
|
||||
text: fg,
|
||||
list: fg,
|
||||
},
|
||||
engine: EngineColors {
|
||||
header: blue,
|
||||
header_focused: yellow,
|
||||
divider: Color::Rgb(80, 80, 72),
|
||||
scroll_indicator: Color::Rgb(95, 95, 88),
|
||||
label: Color::Rgb(150, 145, 135),
|
||||
label_focused: Color::Rgb(180, 175, 165),
|
||||
label_dim: Color::Rgb(120, 115, 105),
|
||||
value: Color::Rgb(210, 205, 195),
|
||||
focused: yellow,
|
||||
normal: fg,
|
||||
dim: Color::Rgb(95, 95, 88),
|
||||
path: Color::Rgb(150, 145, 135),
|
||||
border_magenta: pink,
|
||||
border_green: green,
|
||||
border_cyan: blue,
|
||||
separator: Color::Rgb(80, 80, 72),
|
||||
hint_active: Color::Rgb(220, 200, 100),
|
||||
hint_inactive: Color::Rgb(80, 80, 72),
|
||||
},
|
||||
dict: DictColors {
|
||||
word_name: green,
|
||||
word_bg: Color::Rgb(55, 65, 60),
|
||||
alias: comment,
|
||||
stack_sig: purple,
|
||||
description: fg,
|
||||
example: Color::Rgb(150, 145, 135),
|
||||
category_focused: yellow,
|
||||
category_selected: blue,
|
||||
category_normal: fg,
|
||||
category_dimmed: Color::Rgb(95, 95, 88),
|
||||
border_focused: yellow,
|
||||
border_normal: Color::Rgb(80, 80, 72),
|
||||
header_desc: Color::Rgb(170, 165, 155),
|
||||
},
|
||||
title: TitleColors {
|
||||
big_title: pink,
|
||||
author: blue,
|
||||
link: green,
|
||||
license: orange,
|
||||
prompt: Color::Rgb(170, 165, 155),
|
||||
subtitle: fg,
|
||||
},
|
||||
meter: MeterColors {
|
||||
low: green,
|
||||
mid: yellow,
|
||||
high: pink,
|
||||
low_rgb: (155, 215, 45),
|
||||
mid_rgb: (220, 210, 105),
|
||||
high_rgb: (240, 50, 110),
|
||||
},
|
||||
sparkle: SparkleColors {
|
||||
colors: [
|
||||
(102, 217, 239),
|
||||
(253, 151, 31),
|
||||
(166, 226, 46),
|
||||
(249, 38, 114),
|
||||
(174, 129, 255),
|
||||
sparkle: [
|
||||
(102, 217, 239), (253, 151, 31), (166, 226, 46),
|
||||
(249, 38, 114), (174, 129, 255),
|
||||
],
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: orange,
|
||||
button_selected_bg: orange,
|
||||
button_selected_fg: bg,
|
||||
},
|
||||
meter: [(155, 215, 45), (220, 210, 105), (240, 50, 110)],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,279 +1,41 @@
|
||||
use super::*;
|
||||
use ratatui::style::Color;
|
||||
//! Nord palette.
|
||||
|
||||
pub fn theme() -> ThemeColors {
|
||||
let polar_night0 = Color::Rgb(46, 52, 64);
|
||||
let polar_night1 = Color::Rgb(59, 66, 82);
|
||||
let polar_night2 = Color::Rgb(67, 76, 94);
|
||||
let polar_night3 = Color::Rgb(76, 86, 106);
|
||||
let snow_storm0 = Color::Rgb(216, 222, 233);
|
||||
let snow_storm2 = Color::Rgb(236, 239, 244);
|
||||
let frost0 = Color::Rgb(143, 188, 187);
|
||||
let frost1 = Color::Rgb(136, 192, 208);
|
||||
let frost2 = Color::Rgb(129, 161, 193);
|
||||
let aurora_red = Color::Rgb(191, 97, 106);
|
||||
let aurora_orange = Color::Rgb(208, 135, 112);
|
||||
let aurora_yellow = Color::Rgb(235, 203, 139);
|
||||
let aurora_green = Color::Rgb(163, 190, 140);
|
||||
let aurora_purple = Color::Rgb(180, 142, 173);
|
||||
use super::palette::Palette;
|
||||
|
||||
ThemeColors {
|
||||
ui: UiColors {
|
||||
bg: polar_night0,
|
||||
bg_rgb: (46, 52, 64),
|
||||
text_primary: snow_storm2,
|
||||
text_muted: snow_storm0,
|
||||
text_dim: polar_night3,
|
||||
border: polar_night2,
|
||||
header: frost1,
|
||||
unfocused: polar_night3,
|
||||
accent: frost1,
|
||||
surface: polar_night1,
|
||||
},
|
||||
status: StatusColors {
|
||||
playing_bg: Color::Rgb(50, 65, 60),
|
||||
playing_fg: aurora_green,
|
||||
stopped_bg: Color::Rgb(65, 50, 55),
|
||||
stopped_fg: aurora_red,
|
||||
fill_on: aurora_green,
|
||||
fill_off: polar_night3,
|
||||
fill_bg: polar_night1,
|
||||
},
|
||||
selection: SelectionColors {
|
||||
cursor_bg: frost1,
|
||||
cursor_fg: polar_night0,
|
||||
selected_bg: Color::Rgb(70, 85, 105),
|
||||
selected_fg: frost1,
|
||||
in_range_bg: Color::Rgb(60, 70, 90),
|
||||
in_range_fg: snow_storm0,
|
||||
cursor: frost1,
|
||||
selected: Color::Rgb(70, 85, 105),
|
||||
in_range: Color::Rgb(60, 70, 90),
|
||||
},
|
||||
tile: TileColors {
|
||||
playing_active_bg: Color::Rgb(80, 70, 65),
|
||||
playing_active_fg: aurora_orange,
|
||||
playing_inactive_bg: Color::Rgb(75, 70, 55),
|
||||
playing_inactive_fg: aurora_yellow,
|
||||
active_bg: Color::Rgb(50, 65, 65),
|
||||
active_fg: frost0,
|
||||
inactive_bg: polar_night1,
|
||||
inactive_fg: snow_storm0,
|
||||
active_selected_bg: Color::Rgb(75, 75, 95),
|
||||
active_in_range_bg: Color::Rgb(60, 70, 85),
|
||||
pub fn palette() -> Palette {
|
||||
Palette {
|
||||
bg: (46, 52, 64),
|
||||
surface: (59, 66, 82),
|
||||
surface2: (67, 76, 94),
|
||||
fg: (236, 239, 244),
|
||||
fg_dim: (216, 222, 233),
|
||||
fg_muted: (76, 86, 106),
|
||||
accent: (136, 192, 208), // frost1
|
||||
red: (191, 97, 106),
|
||||
green: (163, 190, 140),
|
||||
yellow: (235, 203, 139),
|
||||
blue: (129, 161, 193), // frost2
|
||||
purple: (180, 142, 173),
|
||||
cyan: (143, 188, 187), // frost0
|
||||
orange: (208, 135, 112),
|
||||
tempo_color: (180, 142, 173),
|
||||
bank_color: (129, 161, 193),
|
||||
pattern_color: (143, 188, 187),
|
||||
title_accent: (136, 192, 208),
|
||||
title_author: (129, 161, 193),
|
||||
secondary: (208, 135, 112),
|
||||
link_bright: [
|
||||
(136, 192, 208),
|
||||
(180, 142, 173),
|
||||
(208, 135, 112),
|
||||
(143, 188, 187),
|
||||
(163, 190, 140),
|
||||
(136, 192, 208), (180, 142, 173), (208, 135, 112),
|
||||
(143, 188, 187), (163, 190, 140),
|
||||
],
|
||||
link_dim: [
|
||||
(55, 75, 85),
|
||||
(70, 60, 70),
|
||||
(75, 55, 50),
|
||||
(55, 75, 75),
|
||||
(60, 75, 55),
|
||||
(55, 75, 85), (70, 60, 70), (75, 55, 50),
|
||||
(55, 75, 75), (60, 75, 55),
|
||||
],
|
||||
},
|
||||
header: HeaderColors {
|
||||
tempo_bg: Color::Rgb(65, 55, 70),
|
||||
tempo_fg: aurora_purple,
|
||||
bank_bg: Color::Rgb(45, 60, 70),
|
||||
bank_fg: frost2,
|
||||
pattern_bg: Color::Rgb(50, 65, 65),
|
||||
pattern_fg: frost0,
|
||||
stats_bg: polar_night1,
|
||||
stats_fg: snow_storm0,
|
||||
},
|
||||
modal: ModalColors {
|
||||
border: frost1,
|
||||
border_accent: aurora_purple,
|
||||
border_warn: aurora_orange,
|
||||
border_dim: polar_night3,
|
||||
confirm: aurora_orange,
|
||||
rename: aurora_purple,
|
||||
input: frost2,
|
||||
editor: frost1,
|
||||
preview: polar_night3,
|
||||
},
|
||||
flash: FlashColors {
|
||||
error_bg: Color::Rgb(65, 50, 55),
|
||||
error_fg: aurora_red,
|
||||
success_bg: Color::Rgb(50, 65, 55),
|
||||
success_fg: aurora_green,
|
||||
info_bg: polar_night1,
|
||||
info_fg: snow_storm2,
|
||||
event_rgb: (60, 55, 75),
|
||||
},
|
||||
list: ListColors {
|
||||
playing_bg: Color::Rgb(50, 65, 55),
|
||||
playing_fg: aurora_green,
|
||||
staged_play_bg: Color::Rgb(65, 55, 70),
|
||||
staged_play_fg: aurora_purple,
|
||||
staged_stop_bg: Color::Rgb(70, 55, 60),
|
||||
staged_stop_fg: aurora_red,
|
||||
edit_bg: Color::Rgb(50, 65, 65),
|
||||
edit_fg: frost0,
|
||||
hover_bg: polar_night2,
|
||||
hover_fg: snow_storm2,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: aurora_red,
|
||||
connected: aurora_green,
|
||||
listening: aurora_yellow,
|
||||
},
|
||||
syntax: SyntaxColors {
|
||||
gap_bg: polar_night1,
|
||||
executed_bg: Color::Rgb(55, 55, 70),
|
||||
selected_bg: Color::Rgb(80, 70, 55),
|
||||
emit: (snow_storm2, Color::Rgb(75, 55, 60)),
|
||||
number: (aurora_orange, Color::Rgb(65, 55, 50)),
|
||||
string: (aurora_green, Color::Rgb(50, 60, 50)),
|
||||
comment: (polar_night3, polar_night0),
|
||||
keyword: (aurora_purple, Color::Rgb(60, 50, 65)),
|
||||
stack_op: (frost2, Color::Rgb(45, 55, 70)),
|
||||
operator: (aurora_yellow, Color::Rgb(65, 60, 45)),
|
||||
sound: (frost0, Color::Rgb(45, 60, 60)),
|
||||
param: (frost1, Color::Rgb(50, 60, 70)),
|
||||
context: (aurora_orange, Color::Rgb(65, 55, 50)),
|
||||
note: (aurora_green, Color::Rgb(50, 60, 50)),
|
||||
interval: (Color::Rgb(170, 200, 150), Color::Rgb(50, 60, 45)),
|
||||
variable: (aurora_purple, Color::Rgb(60, 50, 60)),
|
||||
vary: (aurora_yellow, Color::Rgb(65, 60, 45)),
|
||||
generator: (frost0, Color::Rgb(45, 60, 55)),
|
||||
default: (snow_storm0, polar_night1),
|
||||
},
|
||||
table: TableColors {
|
||||
row_even: polar_night1,
|
||||
row_odd: polar_night0,
|
||||
},
|
||||
values: ValuesColors {
|
||||
tempo: aurora_orange,
|
||||
value: snow_storm0,
|
||||
},
|
||||
hint: HintColors {
|
||||
key: aurora_orange,
|
||||
text: polar_night3,
|
||||
},
|
||||
view_badge: ViewBadgeColors {
|
||||
bg: snow_storm2,
|
||||
fg: polar_night0,
|
||||
},
|
||||
nav: NavColors {
|
||||
selected_bg: Color::Rgb(65, 75, 95),
|
||||
selected_fg: snow_storm2,
|
||||
unselected_bg: polar_night1,
|
||||
unselected_fg: polar_night3,
|
||||
},
|
||||
editor_widget: EditorWidgetColors {
|
||||
cursor_bg: snow_storm2,
|
||||
cursor_fg: polar_night0,
|
||||
selection_bg: Color::Rgb(60, 75, 100),
|
||||
completion_bg: polar_night1,
|
||||
completion_fg: snow_storm2,
|
||||
completion_selected: aurora_orange,
|
||||
completion_example: frost0,
|
||||
},
|
||||
browser: BrowserColors {
|
||||
directory: frost2,
|
||||
project_file: aurora_purple,
|
||||
selected: aurora_orange,
|
||||
file: snow_storm2,
|
||||
focused_border: aurora_orange,
|
||||
unfocused_border: polar_night3,
|
||||
root: snow_storm2,
|
||||
file_icon: polar_night3,
|
||||
folder_icon: frost2,
|
||||
empty_text: polar_night3,
|
||||
},
|
||||
input: InputColors {
|
||||
text: frost2,
|
||||
cursor: snow_storm2,
|
||||
hint: polar_night3,
|
||||
},
|
||||
search: SearchColors {
|
||||
active: aurora_orange,
|
||||
inactive: polar_night3,
|
||||
match_bg: aurora_yellow,
|
||||
match_fg: polar_night0,
|
||||
},
|
||||
markdown: MarkdownColors {
|
||||
h1: frost2,
|
||||
h2: aurora_orange,
|
||||
h3: aurora_purple,
|
||||
code: aurora_green,
|
||||
code_border: Color::Rgb(75, 85, 100),
|
||||
link: frost0,
|
||||
link_url: Color::Rgb(100, 110, 125),
|
||||
quote: polar_night3,
|
||||
text: snow_storm2,
|
||||
list: snow_storm2,
|
||||
},
|
||||
engine: EngineColors {
|
||||
header: frost1,
|
||||
header_focused: aurora_yellow,
|
||||
divider: Color::Rgb(70, 80, 95),
|
||||
scroll_indicator: Color::Rgb(85, 95, 110),
|
||||
label: Color::Rgb(130, 140, 155),
|
||||
label_focused: Color::Rgb(160, 170, 185),
|
||||
label_dim: Color::Rgb(100, 110, 125),
|
||||
value: Color::Rgb(190, 200, 215),
|
||||
focused: aurora_yellow,
|
||||
normal: snow_storm2,
|
||||
dim: Color::Rgb(85, 95, 110),
|
||||
path: Color::Rgb(130, 140, 155),
|
||||
border_magenta: aurora_purple,
|
||||
border_green: aurora_green,
|
||||
border_cyan: frost2,
|
||||
separator: Color::Rgb(70, 80, 95),
|
||||
hint_active: Color::Rgb(200, 180, 100),
|
||||
hint_inactive: Color::Rgb(70, 80, 95),
|
||||
},
|
||||
dict: DictColors {
|
||||
word_name: aurora_green,
|
||||
word_bg: Color::Rgb(50, 60, 75),
|
||||
alias: polar_night3,
|
||||
stack_sig: aurora_purple,
|
||||
description: snow_storm2,
|
||||
example: Color::Rgb(130, 140, 155),
|
||||
category_focused: aurora_yellow,
|
||||
category_selected: frost2,
|
||||
category_normal: snow_storm2,
|
||||
category_dimmed: Color::Rgb(85, 95, 110),
|
||||
border_focused: aurora_yellow,
|
||||
border_normal: Color::Rgb(70, 80, 95),
|
||||
header_desc: Color::Rgb(150, 160, 175),
|
||||
},
|
||||
title: TitleColors {
|
||||
big_title: frost1,
|
||||
author: frost2,
|
||||
link: frost0,
|
||||
license: aurora_orange,
|
||||
prompt: Color::Rgb(150, 160, 175),
|
||||
subtitle: snow_storm2,
|
||||
},
|
||||
meter: MeterColors {
|
||||
low: aurora_green,
|
||||
mid: aurora_yellow,
|
||||
high: aurora_red,
|
||||
low_rgb: (140, 180, 130),
|
||||
mid_rgb: (220, 190, 120),
|
||||
high_rgb: (180, 90, 100),
|
||||
},
|
||||
sparkle: SparkleColors {
|
||||
colors: [
|
||||
(136, 192, 208),
|
||||
(208, 135, 112),
|
||||
(163, 190, 140),
|
||||
(180, 142, 173),
|
||||
(235, 203, 139),
|
||||
sparkle: [
|
||||
(136, 192, 208), (208, 135, 112), (163, 190, 140),
|
||||
(180, 142, 173), (235, 203, 139),
|
||||
],
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: aurora_orange,
|
||||
button_selected_bg: aurora_orange,
|
||||
button_selected_fg: polar_night0,
|
||||
},
|
||||
meter: [(140, 180, 130), (220, 190, 120), (180, 90, 100)],
|
||||
}
|
||||
}
|
||||
|
||||
63
crates/ratatui/src/theme/palette.rs
Normal file
63
crates/ratatui/src/theme/palette.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
//! Palette definition and color mixing utilities.
|
||||
|
||||
use ratatui::style::Color;
|
||||
|
||||
/// RGB color triple.
|
||||
pub type Rgb = (u8, u8, u8);
|
||||
|
||||
/// Base color palette that themes are derived from.
|
||||
pub struct Palette {
|
||||
// Core
|
||||
pub bg: Rgb,
|
||||
pub surface: Rgb,
|
||||
pub surface2: Rgb,
|
||||
pub fg: Rgb,
|
||||
pub fg_dim: Rgb,
|
||||
pub fg_muted: Rgb,
|
||||
// Semantic accents
|
||||
pub accent: Rgb,
|
||||
pub red: Rgb,
|
||||
pub green: Rgb,
|
||||
pub yellow: Rgb,
|
||||
pub blue: Rgb,
|
||||
pub purple: Rgb,
|
||||
pub cyan: Rgb,
|
||||
pub orange: Rgb,
|
||||
// Role assignments
|
||||
pub tempo_color: Rgb,
|
||||
pub bank_color: Rgb,
|
||||
pub pattern_color: Rgb,
|
||||
pub title_accent: Rgb,
|
||||
pub title_author: Rgb,
|
||||
pub secondary: Rgb,
|
||||
// Arrays
|
||||
pub link_bright: [Rgb; 5],
|
||||
pub link_dim: [Rgb; 5],
|
||||
pub sparkle: [Rgb; 5],
|
||||
pub meter: [Rgb; 3],
|
||||
}
|
||||
|
||||
/// Convert an RGB triple to a ratatui [`Color`].
|
||||
pub fn rgb(c: Rgb) -> Color {
|
||||
Color::Rgb(c.0, c.1, c.2)
|
||||
}
|
||||
|
||||
/// Blend `bg` toward `accent` by `amount` (0.0–1.0).
|
||||
pub fn tint(bg: Rgb, accent: Rgb, amount: f32) -> Rgb {
|
||||
let mix = |b: u8, a: u8| -> u8 {
|
||||
let v = b as f32 + (a as f32 - b as f32) * amount;
|
||||
v.clamp(0.0, 255.0) as u8
|
||||
};
|
||||
(mix(bg.0, accent.0), mix(bg.1, accent.1), mix(bg.2, accent.2))
|
||||
}
|
||||
|
||||
/// Linearly interpolate between two colors.
|
||||
pub fn mid(a: Rgb, b: Rgb, t: f32) -> Rgb {
|
||||
tint(a, b, t)
|
||||
}
|
||||
|
||||
/// Darken a color by reducing brightness.
|
||||
pub fn darken(c: Rgb, amount: f32) -> Rgb {
|
||||
let d = |v: u8| -> u8 { (v as f32 * (1.0 - amount)).clamp(0.0, 255.0) as u8 };
|
||||
(d(c.0), d(c.1), d(c.2))
|
||||
}
|
||||
@@ -1,277 +1,41 @@
|
||||
use super::*;
|
||||
use ratatui::style::Color;
|
||||
//! Pitch Black palette.
|
||||
|
||||
pub fn theme() -> ThemeColors {
|
||||
let bg = Color::Rgb(0, 0, 0);
|
||||
let surface = Color::Rgb(10, 10, 10);
|
||||
let surface2 = Color::Rgb(21, 21, 21);
|
||||
let border = Color::Rgb(40, 40, 40);
|
||||
let fg = Color::Rgb(230, 230, 230);
|
||||
let fg_dim = Color::Rgb(160, 160, 160);
|
||||
let fg_muted = Color::Rgb(100, 100, 100);
|
||||
use super::palette::Palette;
|
||||
|
||||
let red = Color::Rgb(255, 80, 80);
|
||||
let green = Color::Rgb(80, 255, 120);
|
||||
let yellow = Color::Rgb(255, 230, 80);
|
||||
let blue = Color::Rgb(80, 180, 255);
|
||||
let purple = Color::Rgb(200, 120, 255);
|
||||
let cyan = Color::Rgb(80, 230, 230);
|
||||
let orange = Color::Rgb(255, 160, 60);
|
||||
|
||||
ThemeColors {
|
||||
ui: UiColors {
|
||||
bg,
|
||||
bg_rgb: (0, 0, 0),
|
||||
text_primary: fg,
|
||||
text_muted: fg_dim,
|
||||
text_dim: fg_muted,
|
||||
border,
|
||||
header: blue,
|
||||
unfocused: fg_muted,
|
||||
accent: cyan,
|
||||
surface,
|
||||
},
|
||||
status: StatusColors {
|
||||
playing_bg: Color::Rgb(15, 35, 20),
|
||||
playing_fg: green,
|
||||
stopped_bg: Color::Rgb(40, 15, 20),
|
||||
stopped_fg: red,
|
||||
fill_on: green,
|
||||
fill_off: fg_muted,
|
||||
fill_bg: surface,
|
||||
},
|
||||
selection: SelectionColors {
|
||||
cursor_bg: cyan,
|
||||
cursor_fg: bg,
|
||||
selected_bg: Color::Rgb(40, 50, 60),
|
||||
selected_fg: cyan,
|
||||
in_range_bg: Color::Rgb(25, 35, 45),
|
||||
in_range_fg: fg,
|
||||
cursor: cyan,
|
||||
selected: Color::Rgb(40, 50, 60),
|
||||
in_range: Color::Rgb(25, 35, 45),
|
||||
},
|
||||
tile: TileColors {
|
||||
playing_active_bg: Color::Rgb(50, 35, 20),
|
||||
playing_active_fg: orange,
|
||||
playing_inactive_bg: Color::Rgb(45, 40, 15),
|
||||
playing_inactive_fg: yellow,
|
||||
active_bg: Color::Rgb(15, 40, 40),
|
||||
active_fg: cyan,
|
||||
inactive_bg: surface,
|
||||
inactive_fg: fg_dim,
|
||||
active_selected_bg: Color::Rgb(45, 40, 55),
|
||||
active_in_range_bg: Color::Rgb(30, 35, 45),
|
||||
pub fn palette() -> Palette {
|
||||
Palette {
|
||||
bg: (0, 0, 0),
|
||||
surface: (10, 10, 10),
|
||||
surface2: (21, 21, 21),
|
||||
fg: (230, 230, 230),
|
||||
fg_dim: (160, 160, 160),
|
||||
fg_muted: (100, 100, 100),
|
||||
accent: (80, 230, 230), // cyan
|
||||
red: (255, 80, 80),
|
||||
green: (80, 255, 120),
|
||||
yellow: (255, 230, 80),
|
||||
blue: (80, 180, 255),
|
||||
purple: (200, 120, 255),
|
||||
cyan: (80, 230, 230),
|
||||
orange: (255, 160, 60),
|
||||
tempo_color: (200, 120, 255),
|
||||
bank_color: (80, 180, 255),
|
||||
pattern_color: (80, 230, 230),
|
||||
title_accent: (80, 230, 230),
|
||||
title_author: (80, 180, 255),
|
||||
secondary: (255, 160, 60),
|
||||
link_bright: [
|
||||
(80, 230, 230),
|
||||
(200, 120, 255),
|
||||
(255, 160, 60),
|
||||
(80, 180, 255),
|
||||
(80, 255, 120),
|
||||
(80, 230, 230), (200, 120, 255), (255, 160, 60),
|
||||
(80, 180, 255), (80, 255, 120),
|
||||
],
|
||||
link_dim: [
|
||||
(25, 60, 60),
|
||||
(50, 35, 65),
|
||||
(60, 45, 20),
|
||||
(25, 50, 70),
|
||||
(25, 65, 35),
|
||||
(25, 60, 60), (50, 35, 65), (60, 45, 20),
|
||||
(25, 50, 70), (25, 65, 35),
|
||||
],
|
||||
},
|
||||
header: HeaderColors {
|
||||
tempo_bg: Color::Rgb(50, 35, 55),
|
||||
tempo_fg: purple,
|
||||
bank_bg: Color::Rgb(20, 45, 60),
|
||||
bank_fg: blue,
|
||||
pattern_bg: Color::Rgb(20, 55, 50),
|
||||
pattern_fg: cyan,
|
||||
stats_bg: surface,
|
||||
stats_fg: fg_dim,
|
||||
},
|
||||
modal: ModalColors {
|
||||
border: cyan,
|
||||
border_accent: purple,
|
||||
border_warn: orange,
|
||||
border_dim: fg_muted,
|
||||
confirm: orange,
|
||||
rename: purple,
|
||||
input: blue,
|
||||
editor: cyan,
|
||||
preview: fg_muted,
|
||||
},
|
||||
flash: FlashColors {
|
||||
error_bg: Color::Rgb(50, 15, 20),
|
||||
error_fg: red,
|
||||
success_bg: Color::Rgb(15, 45, 25),
|
||||
success_fg: green,
|
||||
info_bg: surface,
|
||||
info_fg: fg,
|
||||
event_rgb: (40, 30, 50),
|
||||
},
|
||||
list: ListColors {
|
||||
playing_bg: Color::Rgb(15, 45, 25),
|
||||
playing_fg: green,
|
||||
staged_play_bg: Color::Rgb(45, 30, 55),
|
||||
staged_play_fg: purple,
|
||||
staged_stop_bg: Color::Rgb(55, 25, 30),
|
||||
staged_stop_fg: red,
|
||||
edit_bg: Color::Rgb(15, 45, 45),
|
||||
edit_fg: cyan,
|
||||
hover_bg: surface2,
|
||||
hover_fg: fg,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: red,
|
||||
connected: green,
|
||||
listening: yellow,
|
||||
},
|
||||
syntax: SyntaxColors {
|
||||
gap_bg: bg,
|
||||
executed_bg: Color::Rgb(25, 25, 35),
|
||||
selected_bg: Color::Rgb(55, 45, 25),
|
||||
emit: (fg, Color::Rgb(50, 30, 35)),
|
||||
number: (orange, Color::Rgb(50, 35, 20)),
|
||||
string: (green, Color::Rgb(20, 45, 25)),
|
||||
comment: (fg_muted, bg),
|
||||
keyword: (purple, Color::Rgb(40, 25, 50)),
|
||||
stack_op: (blue, Color::Rgb(20, 40, 55)),
|
||||
operator: (yellow, Color::Rgb(50, 45, 20)),
|
||||
sound: (cyan, Color::Rgb(20, 45, 45)),
|
||||
param: (purple, Color::Rgb(40, 25, 50)),
|
||||
context: (orange, Color::Rgb(50, 35, 20)),
|
||||
note: (green, Color::Rgb(20, 45, 25)),
|
||||
interval: (Color::Rgb(130, 255, 150), Color::Rgb(25, 55, 35)),
|
||||
variable: (purple, Color::Rgb(40, 25, 50)),
|
||||
vary: (yellow, Color::Rgb(50, 45, 20)),
|
||||
generator: (cyan, Color::Rgb(20, 45, 40)),
|
||||
default: (fg_dim, bg),
|
||||
},
|
||||
table: TableColors {
|
||||
row_even: bg,
|
||||
row_odd: surface,
|
||||
},
|
||||
values: ValuesColors {
|
||||
tempo: orange,
|
||||
value: fg_dim,
|
||||
},
|
||||
hint: HintColors {
|
||||
key: orange,
|
||||
text: fg_muted,
|
||||
},
|
||||
view_badge: ViewBadgeColors { bg: fg, fg: bg },
|
||||
nav: NavColors {
|
||||
selected_bg: Color::Rgb(40, 45, 55),
|
||||
selected_fg: fg,
|
||||
unselected_bg: surface,
|
||||
unselected_fg: fg_muted,
|
||||
},
|
||||
editor_widget: EditorWidgetColors {
|
||||
cursor_bg: fg,
|
||||
cursor_fg: bg,
|
||||
selection_bg: Color::Rgb(40, 50, 65),
|
||||
completion_bg: surface,
|
||||
completion_fg: fg,
|
||||
completion_selected: orange,
|
||||
completion_example: cyan,
|
||||
},
|
||||
browser: BrowserColors {
|
||||
directory: blue,
|
||||
project_file: purple,
|
||||
selected: orange,
|
||||
file: fg,
|
||||
focused_border: orange,
|
||||
unfocused_border: fg_muted,
|
||||
root: fg,
|
||||
file_icon: fg_muted,
|
||||
folder_icon: blue,
|
||||
empty_text: fg_muted,
|
||||
},
|
||||
input: InputColors {
|
||||
text: blue,
|
||||
cursor: fg,
|
||||
hint: fg_muted,
|
||||
},
|
||||
search: SearchColors {
|
||||
active: orange,
|
||||
inactive: fg_muted,
|
||||
match_bg: yellow,
|
||||
match_fg: bg,
|
||||
},
|
||||
markdown: MarkdownColors {
|
||||
h1: blue,
|
||||
h2: orange,
|
||||
h3: purple,
|
||||
code: green,
|
||||
code_border: Color::Rgb(50, 50, 50),
|
||||
link: cyan,
|
||||
link_url: Color::Rgb(90, 90, 90),
|
||||
quote: fg_muted,
|
||||
text: fg,
|
||||
list: fg,
|
||||
},
|
||||
engine: EngineColors {
|
||||
header: blue,
|
||||
header_focused: yellow,
|
||||
divider: Color::Rgb(45, 45, 45),
|
||||
scroll_indicator: Color::Rgb(60, 60, 60),
|
||||
label: Color::Rgb(130, 130, 130),
|
||||
label_focused: Color::Rgb(170, 170, 170),
|
||||
label_dim: Color::Rgb(90, 90, 90),
|
||||
value: Color::Rgb(200, 200, 200),
|
||||
focused: yellow,
|
||||
normal: fg,
|
||||
dim: Color::Rgb(60, 60, 60),
|
||||
path: Color::Rgb(130, 130, 130),
|
||||
border_magenta: purple,
|
||||
border_green: green,
|
||||
border_cyan: cyan,
|
||||
separator: Color::Rgb(45, 45, 45),
|
||||
hint_active: Color::Rgb(220, 200, 80),
|
||||
hint_inactive: Color::Rgb(45, 45, 45),
|
||||
},
|
||||
dict: DictColors {
|
||||
word_name: green,
|
||||
word_bg: Color::Rgb(20, 30, 35),
|
||||
alias: fg_muted,
|
||||
stack_sig: purple,
|
||||
description: fg,
|
||||
example: Color::Rgb(130, 130, 130),
|
||||
category_focused: yellow,
|
||||
category_selected: blue,
|
||||
category_normal: fg,
|
||||
category_dimmed: Color::Rgb(60, 60, 60),
|
||||
border_focused: yellow,
|
||||
border_normal: Color::Rgb(45, 45, 45),
|
||||
header_desc: Color::Rgb(150, 150, 150),
|
||||
},
|
||||
title: TitleColors {
|
||||
big_title: cyan,
|
||||
author: blue,
|
||||
link: green,
|
||||
license: orange,
|
||||
prompt: Color::Rgb(150, 150, 150),
|
||||
subtitle: fg,
|
||||
},
|
||||
meter: MeterColors {
|
||||
low: green,
|
||||
mid: yellow,
|
||||
high: red,
|
||||
low_rgb: (70, 240, 110),
|
||||
mid_rgb: (245, 220, 75),
|
||||
high_rgb: (245, 75, 75),
|
||||
},
|
||||
sparkle: SparkleColors {
|
||||
colors: [
|
||||
(80, 230, 230),
|
||||
(255, 160, 60),
|
||||
(80, 255, 120),
|
||||
(200, 120, 255),
|
||||
(80, 180, 255),
|
||||
sparkle: [
|
||||
(80, 230, 230), (255, 160, 60), (80, 255, 120),
|
||||
(200, 120, 255), (80, 180, 255),
|
||||
],
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: orange,
|
||||
button_selected_bg: orange,
|
||||
button_selected_fg: bg,
|
||||
},
|
||||
meter: [(70, 240, 110), (245, 220, 75), (245, 75, 75)],
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user