Compare commits
151 Commits
v0.1.0
...
5385bf675a
| Author | SHA1 | Date | |
|---|---|---|---|
| 5385bf675a | |||
| 211e71f5a9 | |||
| 23c7abb145 | |||
| 670ae0b6b6 | |||
| 10ca567ac5 | |||
| b2871ac251 | |||
| 8ba89f91a0 | |||
| 7d670dacb9 | |||
| 1de8c068f6 | |||
| d792f011ee | |||
| 897f1a776e | |||
| 869d3af244 | |||
| a5f17687f1 | |||
| 5b851751e5 | |||
| bc5d12e53a | |||
| d6bbae173b | |||
| 1f339f1503 | |||
| 8ffe2c22c7 | |||
| 20c32ce0d8 | |||
| a326d58d30 | |||
| c72733bac8 | |||
| 5758b18d58 | |||
| 52cc890a67 | |||
| 0f9d750069 | |||
| 66ee2e28ff | |||
| 6ec3a86568 | |||
| 51f52be4ce | |||
| 2c98a915fa | |||
| e42476dd4d | |||
| 3e364a6622 | |||
| 1248f74b25 | |||
| fc2ab0757b | |||
| 10ed5a629a | |||
| 88c2b51720 | |||
| 5cda1a8f95 | |||
| 200832f230 | |||
| 91bc9011b2 | |||
| de56598fca | |||
| abafea8ddf | |||
| e6f776bdf4 | |||
| d40d713649 | |||
| 767575b25d | |||
| 82b0668bcf | |||
| 6cf9d2eec1 | |||
| 2097997372 | |||
| 5579708f69 | |||
| 1b01491e87 | |||
| 5581ba1881 | |||
| 8983b3f21c | |||
| 4a7ae83019 | |||
| 61a6d7aad0 | |||
| 1b01e3b805 | |||
| 2a57cc415b | |||
| 7c76bdb8d6 | |||
| 1facc72a67 | |||
| 726ea16e92 | |||
| 154cac6547 | |||
| 3380e454df | |||
| 660f48216a | |||
| fb1f73ebd6 | |||
| cd223592a7 | |||
| af81c94207 | |||
| b53e4a76ab | |||
| 8c31ed4196 | |||
| 8024c18bb0 | |||
| 194030d953 | |||
| e4799c1f42 | |||
| 636129688d | |||
| a2ee0e5a50 | |||
| 96ed74c6fe | |||
| a67d982fcd | |||
| c9ab7a4f0b | |||
| 772d21a8ed | |||
| 4396147a8b | |||
| c396c39b6b | |||
| f6b43cb021 | |||
| 60d1d7ca74 | |||
| 9864cc6d61 | |||
| 985ab687d7 | |||
| 9b925d881e | |||
| 71146c7cea | |||
| 6b95f31afd | |||
| adee8d0d57 | |||
| f9c284effd | |||
| 57fd51be3e | |||
| ce70251057 | |||
| b47c789612 | |||
| dd853b8e1b | |||
| a0585b0814 | |||
| 2100b82dad | |||
| 15a4300db5 | |||
| fed39c01e8 | |||
| 0a4f1419eb | |||
| 793c83e18c | |||
| 20bc0ffcb4 | |||
| 8e09fd106e | |||
| 73ca0ff096 | |||
| 425f1c8627 | |||
| 730332cfb0 | |||
| 1d70a83759 | |||
| 0299012725 | |||
| 08029ec604 | |||
| 4f9b1f39f9 | |||
| 4772b02f77 | |||
| 4049c7787c | |||
| 4c635500dd | |||
| d0e37e13e6 | |||
| 7658cf9d51 | |||
| 584dbb6aad | |||
| 2731eea037 | |||
| 22ee5f97e6 | |||
| 5fb059ea20 | |||
| 705d93702b | |||
| 77a6aa9eb7 | |||
| d25b1317fc | |||
| 2851785e0d | |||
| a72772c8cc | |||
| 4d22bd5d2b | |||
| 495bfb3bdc | |||
| 73db616139 | |||
| 8efafffaff | |||
| 48f5920fed | |||
| d106711708 | |||
| 2be15d11f4 | |||
| 5952807240 | |||
| 0beed16c31 | |||
| c6860105a6 | |||
| f4eafdf5b2 | |||
| 935df84920 | |||
| a3a39ea28e | |||
| 574625735b | |||
| 40c509e295 | |||
| 61daa9d79d | |||
| 9e597258e4 | |||
| 223679acf8 | |||
| 2235a4b0a1 | |||
| 2453b78237 | |||
| fcb6adb6af | |||
| ce98acacd0 | |||
| d2d6ef5b06 | |||
| 6efcabd32d | |||
| 250e359fc5 | |||
| cf5994e604 | |||
| e1aff189cd | |||
| b3c56bc56c | |||
| 3bb19cbda8 | |||
| 42ad77d9ae | |||
| e853e67492 | |||
| f7e6f96cbf | |||
| 8af64fc4e2 | |||
| 183dd5b516 |
240
.github/workflows/ci.yml
vendored
Normal file
240
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,240 @@
|
||||
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
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: ${{ matrix.target }}
|
||||
|
||||
- name: Install 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
|
||||
|
||||
universal-macos:
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: macos-14
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- name: Download macOS artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: cagire-macos-*
|
||||
path: artifacts
|
||||
|
||||
- name: Create universal CLI binary
|
||||
run: |
|
||||
lipo -create \
|
||||
artifacts/cagire-macos-x86_64/cagire \
|
||||
artifacts/cagire-macos-aarch64/cagire \
|
||||
-output cagire
|
||||
chmod +x cagire
|
||||
lipo -info cagire
|
||||
|
||||
- name: Create universal app bundle
|
||||
run: |
|
||||
cd artifacts/cagire-macos-aarch64-desktop
|
||||
unzip Cagire.app.zip
|
||||
cd ../cagire-macos-x86_64-desktop
|
||||
unzip Cagire.app.zip
|
||||
cd ../..
|
||||
cp -R artifacts/cagire-macos-aarch64-desktop/Cagire.app Cagire.app
|
||||
lipo -create \
|
||||
artifacts/cagire-macos-x86_64-desktop/Cagire.app/Contents/MacOS/cagire-desktop \
|
||||
artifacts/cagire-macos-aarch64-desktop/Cagire.app/Contents/MacOS/cagire-desktop \
|
||||
-output Cagire.app/Contents/MacOS/cagire-desktop
|
||||
lipo -info Cagire.app/Contents/MacOS/cagire-desktop
|
||||
zip -r Cagire.app.zip Cagire.app
|
||||
|
||||
- name: Build .pkg installer
|
||||
run: |
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
mkdir -p pkg-root/Applications pkg-root/usr/local/bin
|
||||
cp -R Cagire.app pkg-root/Applications/
|
||||
cp cagire pkg-root/usr/local/bin/
|
||||
pkgbuild --analyze --root pkg-root component.plist
|
||||
plutil -replace BundleIsRelocatable -bool NO component.plist
|
||||
pkgbuild --root pkg-root --identifier com.sova.cagire \
|
||||
--version "$VERSION" --install-location / \
|
||||
--component-plist component.plist \
|
||||
"Cagire-${VERSION}-universal.pkg"
|
||||
|
||||
- name: Upload universal CLI
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-macos-universal
|
||||
path: cagire
|
||||
|
||||
- name: Upload universal app bundle
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-macos-universal-desktop
|
||||
path: Cagire.app.zip
|
||||
|
||||
- name: Upload .pkg installer
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-macos-universal-pkg
|
||||
path: Cagire-*-universal.pkg
|
||||
|
||||
release:
|
||||
needs: [build, universal-macos]
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Prepare release files
|
||||
run: |
|
||||
mkdir -p release
|
||||
for dir in artifacts/*/; do
|
||||
name=$(basename "$dir")
|
||||
if [[ "$name" == "cagire-macos-universal-pkg" ]]; then
|
||||
cp "$dir"/*.pkg release/
|
||||
elif [[ "$name" == "cagire-macos-universal-desktop" ]]; then
|
||||
cp "$dir/Cagire.app.zip" "release/cagire-macos-universal-desktop.app.zip"
|
||||
elif [[ "$name" == "cagire-macos-universal" ]]; then
|
||||
cp "$dir/cagire" "release/cagire-macos-universal"
|
||||
elif [[ "$name" == *-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
|
||||
58
.github/workflows/pages.yml
vendored
Normal file
58
.github/workflows/pages.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: Deploy Website
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
cache-dependency-path: website/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
working-directory: website
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
working-directory: website
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: website/dist
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -2,3 +2,7 @@
|
||||
Cargo.lock
|
||||
*.prof
|
||||
.DS_Store
|
||||
|
||||
# Claude
|
||||
.claude/
|
||||
CLAUDE.md
|
||||
|
||||
161
CHANGELOG.md
Normal file
161
CHANGELOG.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [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
|
||||
|
||||
### Added
|
||||
- Polyphonic parameters: param words (`note`, `freq`, `gain`, etc.) and sound words now consume the entire stack, enabling polyphony (e.g., `60 64 67 note sine s .` emits 3 voices).
|
||||
- New random distribution words: `exprand` (exponential) and `logrand` (logarithmic).
|
||||
- Music theory chord words: `maj`, `m`, `dim`, `aug`, `sus2`, `sus4`, `maj7`, `min7`, `dom7`, `dim7`, `m7b5`, `minmaj7`, `aug7`, `maj6`, `min6`, `dom9`, `maj9`, `min9`, `dom11`, `min11`, `dom13`, `add9`, `add11`, `madd9`, `dom7b9`, `dom7s9`, `dom7b5`, `dom7s5`.
|
||||
- Playing patterns are now saved with the project and restored on load.
|
||||
|
||||
### Changed
|
||||
- `at` now consumes the entire stack for time offsets; polyphony multiplies with deltas (2 notes × 2 times = 4 voices).
|
||||
- Iterator (`iter`) now resets when a pattern restarts.
|
||||
- Project loading now properly resets state: stops all patterns, clears user variables/dictionary, and clears queued changes.
|
||||
|
||||
### Removed
|
||||
- `tcycle` word (replaced by polyphonic parameter behavior).
|
||||
|
||||
## [0.0.2] - 2026-02-01
|
||||
- CI testing and codebase cleanup
|
||||
|
||||
## [0.0.1] - Initial Release
|
||||
- CI testing
|
||||
93
Cargo.toml
93
Cargo.toml
@@ -1,7 +1,24 @@
|
||||
[workspace]
|
||||
members = ["crates/forth", "crates/markdown", "crates/project", "crates/ratatui"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.0.9"
|
||||
edition = "2021"
|
||||
authors = ["Raphaël Forment <raphael.forment@gmail.com>"]
|
||||
license = "AGPL-3.0"
|
||||
repository = "https://github.com/Bubobubobubobubo/cagire"
|
||||
homepage = "https://cagire.raphaelforment.fr"
|
||||
description = "Forth-based live coding music sequencer"
|
||||
|
||||
[package]
|
||||
name = "cagire"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
description.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "cagire"
|
||||
@@ -11,20 +28,74 @@ path = "src/lib.rs"
|
||||
name = "cagire"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
doux = { git = "https://github.com/Bubobubobubobubo/doux", features = ["native"] }
|
||||
rusty_link = "0.4"
|
||||
ratatui = "0.29"
|
||||
crossterm = "0.28"
|
||||
cpal = "0.15"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
[[bin]]
|
||||
name = "cagire-desktop"
|
||||
path = "src/bin/desktop/main.rs"
|
||||
required-features = ["desktop"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
desktop = [
|
||||
"cagire-forth/desktop",
|
||||
"dep:egui",
|
||||
"dep:eframe",
|
||||
"dep:egui_ratatui",
|
||||
"dep:soft_ratatui",
|
||||
"dep:rustc-hash",
|
||||
"dep:image",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
cagire-forth = { path = "crates/forth" }
|
||||
cagire-markdown = { path = "crates/markdown" }
|
||||
cagire-project = { path = "crates/project" }
|
||||
cagire-ratatui = { path = "crates/ratatui" }
|
||||
doux = { path = "/Users/bubo/doux", features = ["native"] }
|
||||
rusty_link = "0.4"
|
||||
ratatui = "0.30"
|
||||
crossterm = "0.29"
|
||||
cpal = { version = "0.17", features = ["jack"] }
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
rand = "0.8"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tui-textarea = "0.7"
|
||||
tui-big-text = "0.7"
|
||||
tachyonfx = { version = "0.22", features = ["std-duration"] }
|
||||
tui-big-text = "0.8"
|
||||
arboard = "3"
|
||||
minimad = "0.13"
|
||||
crossbeam-channel = "0.5"
|
||||
confy = "2"
|
||||
rustfft = "6"
|
||||
thread-priority = "1"
|
||||
ringbuf = "0.4"
|
||||
arc-swap = "1"
|
||||
midir = "0.10"
|
||||
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(windows)'.build-dependencies]
|
||||
winres = "0.1"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
strip = true
|
||||
|
||||
[package.metadata.bundle.bin.cagire-desktop]
|
||||
name = "Cagire"
|
||||
identifier = "com.sova.cagire"
|
||||
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"
|
||||
|
||||
22
README.md
22
README.md
@@ -1,19 +1,37 @@
|
||||
# Cagire
|
||||
<h1 align="center">Cagire</h1>
|
||||
|
||||
A Forth Music Sequencer.
|
||||
<p align="center"><em>A Forth Music Sequencer</em></p>
|
||||
|
||||
<p align="center">
|
||||
<img src="cagire_pixel.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.
|
||||
|
||||
## 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
|
||||
|
||||
AGPL-3.0
|
||||
|
||||
BIN
assets/Cagire.icns
Normal file
BIN
assets/Cagire.icns
Normal file
Binary file not shown.
BIN
assets/Cagire.ico
Normal file
BIN
assets/Cagire.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 137 KiB |
BIN
assets/Cagire.png
Normal file
BIN
assets/Cagire.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
11
build.rs
Normal file
11
build.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
fn main() {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let mut res = winres::WindowsResource::new();
|
||||
res.set_icon("assets/Cagire.ico")
|
||||
.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");
|
||||
}
|
||||
}
|
||||
17
crates/forth/Cargo.toml
Normal file
17
crates/forth/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "cagire-forth"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Forth virtual machine for cagire sequencer"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
desktop = []
|
||||
|
||||
[dependencies]
|
||||
rand = "0.8"
|
||||
parking_lot = "0.12"
|
||||
arc-swap = "1"
|
||||
374
crates/forth/src/compiler.rs
Normal file
374
crates/forth/src/compiler.rs
Normal file
@@ -0,0 +1,374 @@
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::ops::Op;
|
||||
use super::types::{Dictionary, SourceSpan};
|
||||
use super::words::compile_word;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Token {
|
||||
Int(i64, SourceSpan),
|
||||
Float(f64, SourceSpan),
|
||||
Str(String, SourceSpan),
|
||||
Word(String, SourceSpan),
|
||||
}
|
||||
|
||||
pub(super) fn compile_script(input: &str, dict: &Dictionary) -> Result<Vec<Op>, String> {
|
||||
let tokens = tokenize(input);
|
||||
compile(&tokens, dict)
|
||||
}
|
||||
|
||||
fn tokenize(input: &str) -> Vec<Token> {
|
||||
let mut tokens = Vec::new();
|
||||
let mut chars = input.char_indices().peekable();
|
||||
|
||||
while let Some(&(pos, c)) = chars.peek() {
|
||||
if c.is_whitespace() {
|
||||
chars.next();
|
||||
continue;
|
||||
}
|
||||
|
||||
if c == '(' || c == ')' {
|
||||
chars.next();
|
||||
continue;
|
||||
}
|
||||
|
||||
if c == '"' {
|
||||
let start = pos;
|
||||
chars.next();
|
||||
let mut s = String::new();
|
||||
let mut end = start + 1;
|
||||
while let Some(&(i, ch)) = chars.peek() {
|
||||
end = i + ch.len_utf8();
|
||||
chars.next();
|
||||
if ch == '"' {
|
||||
break;
|
||||
}
|
||||
s.push(ch);
|
||||
}
|
||||
tokens.push(Token::Str(s, SourceSpan { start: start as u32, end: end as u32 }));
|
||||
continue;
|
||||
}
|
||||
|
||||
if c == ';' {
|
||||
chars.next(); // consume first ;
|
||||
if let Some(&(_, ';')) = chars.peek() {
|
||||
// ;; starts a comment to end of line
|
||||
chars.next(); // consume second ;
|
||||
while let Some(&(_, ch)) = chars.peek() {
|
||||
if ch == '\n' {
|
||||
break;
|
||||
}
|
||||
chars.next();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// single ; is a word, create token
|
||||
tokens.push(Token::Word(
|
||||
";".to_string(),
|
||||
SourceSpan {
|
||||
start: pos as u32,
|
||||
end: (pos + 1) as u32,
|
||||
},
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
let start = pos;
|
||||
let mut word = String::new();
|
||||
let mut end = start;
|
||||
while let Some(&(i, ch)) = chars.peek() {
|
||||
if ch.is_whitespace() {
|
||||
break;
|
||||
}
|
||||
end = i + ch.len_utf8();
|
||||
word.push(ch);
|
||||
chars.next();
|
||||
}
|
||||
|
||||
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_to_parse.parse::<f64>() {
|
||||
tokens.push(Token::Float(f, span));
|
||||
} else {
|
||||
tokens.push(Token::Word(word, span));
|
||||
}
|
||||
}
|
||||
|
||||
tokens
|
||||
}
|
||||
|
||||
fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
|
||||
let mut ops = Vec::new();
|
||||
let mut i = 0;
|
||||
|
||||
while i < tokens.len() {
|
||||
match &tokens[i] {
|
||||
Token::Int(n, span) => {
|
||||
ops.push(Op::PushInt(*n, Some(*span)));
|
||||
}
|
||||
Token::Float(f, span) => {
|
||||
ops.push(Op::PushFloat(*f, 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 == "{" {
|
||||
let (quote_ops, consumed, end_span) =
|
||||
compile_quotation(&tokens[i + 1..], dict)?;
|
||||
i += consumed;
|
||||
let body_span = SourceSpan {
|
||||
start: span.start,
|
||||
end: end_span.end,
|
||||
};
|
||||
ops.push(Op::Quotation(Arc::from(quote_ops), Some(body_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().insert(name, body);
|
||||
} else if word == ";" {
|
||||
return Err("unexpected ;".into());
|
||||
} else if word == "if" {
|
||||
let (then_ops, else_ops, consumed, then_span, else_span) =
|
||||
compile_if(&tokens[i + 1..], dict)?;
|
||||
i += consumed;
|
||||
if else_ops.is_empty() {
|
||||
ops.push(Op::BranchIfZero(then_ops.len(), then_span, None));
|
||||
ops.extend(then_ops);
|
||||
} else {
|
||||
ops.push(Op::BranchIfZero(then_ops.len() + 1, then_span, else_span));
|
||||
ops.extend(then_ops);
|
||||
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}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
Ok(ops)
|
||||
}
|
||||
|
||||
fn compile_quotation(
|
||||
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 quote_ops = compile(&tokens[..end_idx], dict)?;
|
||||
Ok((quote_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),
|
||||
}
|
||||
}
|
||||
|
||||
fn compile_colon_def(
|
||||
tokens: &[Token],
|
||||
dict: &Dictionary,
|
||||
) -> Result<(usize, String, Vec<Op>), String> {
|
||||
if tokens.is_empty() {
|
||||
return Err("expected word name after ':'".into());
|
||||
}
|
||||
let name = match &tokens[0] {
|
||||
Token::Word(w, _) => w.clone(),
|
||||
Token::Int(n, _) => n.to_string(),
|
||||
Token::Float(f, _) => f.to_string(),
|
||||
Token::Str(s, _) => s.clone(),
|
||||
};
|
||||
let mut semi_pos = None;
|
||||
for (i, tok) in tokens[1..].iter().enumerate() {
|
||||
if let Token::Word(w, _) = tok {
|
||||
if w == ";" {
|
||||
semi_pos = Some(i + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let semi_pos = semi_pos.ok_or("missing ';' in word definition")?;
|
||||
let body_tokens = &tokens[1..semi_pos];
|
||||
let body_ops = compile(body_tokens, dict)?;
|
||||
Ok((semi_pos + 1, name, body_ops))
|
||||
}
|
||||
|
||||
fn tokens_span(tokens: &[Token]) -> Option<SourceSpan> {
|
||||
let first = tokens.first().and_then(token_span)?;
|
||||
let last = tokens.last().and_then(token_span)?;
|
||||
Some(SourceSpan {
|
||||
start: first.start,
|
||||
end: last.end,
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn compile_if(
|
||||
tokens: &[Token],
|
||||
dict: &Dictionary,
|
||||
) -> Result<
|
||||
(
|
||||
Vec<Op>,
|
||||
Vec<Op>,
|
||||
usize,
|
||||
Option<SourceSpan>,
|
||||
Option<SourceSpan>,
|
||||
),
|
||||
String,
|
||||
> {
|
||||
let mut depth = 1;
|
||||
let mut else_pos = None;
|
||||
let mut then_pos = None;
|
||||
|
||||
for (i, tok) in tokens.iter().enumerate() {
|
||||
if let Token::Word(w, _) = tok {
|
||||
match w.as_str() {
|
||||
"if" => depth += 1,
|
||||
"else" if depth == 1 => else_pos = Some(i),
|
||||
"then" => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
then_pos = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let then_pos = then_pos.ok_or("missing 'then'")?;
|
||||
|
||||
let (then_ops, else_ops, then_span, else_span) = if let Some(ep) = else_pos {
|
||||
let then_slice = &tokens[..ep];
|
||||
let else_slice = &tokens[ep + 1..then_pos];
|
||||
let then_span = tokens_span(then_slice);
|
||||
let else_span = tokens_span(else_slice);
|
||||
let then_ops = compile(then_slice, dict)?;
|
||||
let else_ops = compile(else_slice, dict)?;
|
||||
(then_ops, else_ops, then_span, else_span)
|
||||
} else {
|
||||
let then_slice = &tokens[..then_pos];
|
||||
let then_span = tokens_span(then_slice);
|
||||
let then_ops = compile(then_slice, dict)?;
|
||||
(then_ops, Vec::new(), then_span, None)
|
||||
};
|
||||
|
||||
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))
|
||||
}
|
||||
13
crates/forth/src/lib.rs
Normal file
13
crates/forth/src/lib.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
mod compiler;
|
||||
mod ops;
|
||||
mod theory;
|
||||
mod types;
|
||||
mod vm;
|
||||
mod words;
|
||||
|
||||
pub use types::{
|
||||
CcAccess, Dictionary, ExecutionTrace, ResolvedValue, Rng, SourceSpan, StepContext, Value,
|
||||
Variables, VariablesMap,
|
||||
};
|
||||
pub use vm::Forth;
|
||||
pub use words::{lookup_word, Word, WordCompile, WORDS};
|
||||
125
crates/forth/src/ops.rs
Normal file
125
crates/forth/src/ops.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::types::SourceSpan;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Op {
|
||||
PushInt(i64, Option<SourceSpan>),
|
||||
PushFloat(f64, Option<SourceSpan>),
|
||||
PushStr(Arc<str>, Option<SourceSpan>),
|
||||
Dup,
|
||||
Dupn,
|
||||
Drop,
|
||||
Swap,
|
||||
Over,
|
||||
Rot,
|
||||
Nip,
|
||||
Tuck,
|
||||
Dup2,
|
||||
Drop2,
|
||||
Swap2,
|
||||
Over2,
|
||||
Rev,
|
||||
Shuffle,
|
||||
Sort,
|
||||
RSort,
|
||||
Sum,
|
||||
Prod,
|
||||
Forget,
|
||||
Add,
|
||||
Sub,
|
||||
Mul,
|
||||
Div,
|
||||
Mod,
|
||||
Neg,
|
||||
Abs,
|
||||
Floor,
|
||||
Ceil,
|
||||
Round,
|
||||
Min,
|
||||
Max,
|
||||
Pow,
|
||||
Sqrt,
|
||||
Sin,
|
||||
Cos,
|
||||
Log,
|
||||
Eq,
|
||||
Ne,
|
||||
Lt,
|
||||
Gt,
|
||||
Le,
|
||||
Ge,
|
||||
And,
|
||||
Or,
|
||||
Not,
|
||||
Xor,
|
||||
Nand,
|
||||
Nor,
|
||||
IfElse,
|
||||
Pick,
|
||||
BranchIfZero(usize, Option<SourceSpan>, Option<SourceSpan>),
|
||||
Branch(usize),
|
||||
NewCmd,
|
||||
SetParam(&'static str),
|
||||
Emit,
|
||||
Get,
|
||||
Set,
|
||||
SetKeep,
|
||||
GetContext(&'static str),
|
||||
Rand(Option<SourceSpan>),
|
||||
ExpRand(Option<SourceSpan>),
|
||||
LogRand(Option<SourceSpan>),
|
||||
Seed,
|
||||
Cycle(Option<SourceSpan>),
|
||||
PCycle(Option<SourceSpan>),
|
||||
Choose(Option<SourceSpan>),
|
||||
Bounce(Option<SourceSpan>),
|
||||
WChoose(Option<SourceSpan>),
|
||||
ChanceExec(Option<SourceSpan>),
|
||||
ProbExec(Option<SourceSpan>),
|
||||
Coin(Option<SourceSpan>),
|
||||
Mtof,
|
||||
Ftom,
|
||||
SetTempo,
|
||||
Every(Option<SourceSpan>),
|
||||
Bjork(Option<SourceSpan>),
|
||||
PBjork(Option<SourceSpan>),
|
||||
Quotation(Arc<[Op]>, Option<SourceSpan>),
|
||||
When,
|
||||
Unless,
|
||||
Adsr,
|
||||
Ad,
|
||||
Apply,
|
||||
Ramp,
|
||||
Triangle,
|
||||
Range,
|
||||
Perlin,
|
||||
Chain,
|
||||
Loop,
|
||||
Degree(&'static [i64]),
|
||||
Oct,
|
||||
ClearCmd,
|
||||
SetSpeed,
|
||||
At,
|
||||
Arp,
|
||||
IntRange,
|
||||
StepRange,
|
||||
Generate,
|
||||
GeomRange,
|
||||
Euclid,
|
||||
EuclidRot,
|
||||
Times,
|
||||
Chord(&'static [i64]),
|
||||
// Audio-rate modulation DSL
|
||||
ModLfo(u8),
|
||||
ModSlide(u8),
|
||||
ModRnd(u8),
|
||||
ModEnv,
|
||||
// MIDI
|
||||
MidiEmit,
|
||||
GetMidiCC,
|
||||
MidiClock,
|
||||
MidiStart,
|
||||
MidiStop,
|
||||
MidiContinue,
|
||||
}
|
||||
129
crates/forth/src/theory/chords.rs
Normal file
129
crates/forth/src/theory/chords.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
pub struct Chord {
|
||||
pub name: &'static str,
|
||||
pub intervals: &'static [i64],
|
||||
}
|
||||
|
||||
pub static CHORDS: &[Chord] = &[
|
||||
// Triads
|
||||
Chord {
|
||||
name: "maj",
|
||||
intervals: &[0, 4, 7],
|
||||
},
|
||||
Chord {
|
||||
name: "m",
|
||||
intervals: &[0, 3, 7],
|
||||
},
|
||||
Chord {
|
||||
name: "dim",
|
||||
intervals: &[0, 3, 6],
|
||||
},
|
||||
Chord {
|
||||
name: "aug",
|
||||
intervals: &[0, 4, 8],
|
||||
},
|
||||
Chord {
|
||||
name: "sus2",
|
||||
intervals: &[0, 2, 7],
|
||||
},
|
||||
Chord {
|
||||
name: "sus4",
|
||||
intervals: &[0, 5, 7],
|
||||
},
|
||||
// Seventh chords
|
||||
Chord {
|
||||
name: "maj7",
|
||||
intervals: &[0, 4, 7, 11],
|
||||
},
|
||||
Chord {
|
||||
name: "min7",
|
||||
intervals: &[0, 3, 7, 10],
|
||||
},
|
||||
Chord {
|
||||
name: "dom7",
|
||||
intervals: &[0, 4, 7, 10],
|
||||
},
|
||||
Chord {
|
||||
name: "dim7",
|
||||
intervals: &[0, 3, 6, 9],
|
||||
},
|
||||
Chord {
|
||||
name: "m7b5",
|
||||
intervals: &[0, 3, 6, 10],
|
||||
},
|
||||
Chord {
|
||||
name: "minmaj7",
|
||||
intervals: &[0, 3, 7, 11],
|
||||
},
|
||||
Chord {
|
||||
name: "aug7",
|
||||
intervals: &[0, 4, 8, 10],
|
||||
},
|
||||
// Sixth chords
|
||||
Chord {
|
||||
name: "maj6",
|
||||
intervals: &[0, 4, 7, 9],
|
||||
},
|
||||
Chord {
|
||||
name: "min6",
|
||||
intervals: &[0, 3, 7, 9],
|
||||
},
|
||||
// Extended chords
|
||||
Chord {
|
||||
name: "dom9",
|
||||
intervals: &[0, 4, 7, 10, 14],
|
||||
},
|
||||
Chord {
|
||||
name: "maj9",
|
||||
intervals: &[0, 4, 7, 11, 14],
|
||||
},
|
||||
Chord {
|
||||
name: "min9",
|
||||
intervals: &[0, 3, 7, 10, 14],
|
||||
},
|
||||
Chord {
|
||||
name: "dom11",
|
||||
intervals: &[0, 4, 7, 10, 14, 17],
|
||||
},
|
||||
Chord {
|
||||
name: "min11",
|
||||
intervals: &[0, 3, 7, 10, 14, 17],
|
||||
},
|
||||
Chord {
|
||||
name: "dom13",
|
||||
intervals: &[0, 4, 7, 10, 14, 21],
|
||||
},
|
||||
// Add chords
|
||||
Chord {
|
||||
name: "add9",
|
||||
intervals: &[0, 4, 7, 14],
|
||||
},
|
||||
Chord {
|
||||
name: "add11",
|
||||
intervals: &[0, 4, 7, 17],
|
||||
},
|
||||
Chord {
|
||||
name: "madd9",
|
||||
intervals: &[0, 3, 7, 14],
|
||||
},
|
||||
// Altered dominants
|
||||
Chord {
|
||||
name: "dom7b9",
|
||||
intervals: &[0, 4, 7, 10, 13],
|
||||
},
|
||||
Chord {
|
||||
name: "dom7s9",
|
||||
intervals: &[0, 4, 7, 10, 15],
|
||||
},
|
||||
Chord {
|
||||
name: "dom7b5",
|
||||
intervals: &[0, 4, 6, 10],
|
||||
},
|
||||
Chord {
|
||||
name: "dom7s5",
|
||||
intervals: &[0, 4, 8, 10],
|
||||
},
|
||||
];
|
||||
|
||||
pub fn lookup(name: &str) -> Option<&'static [i64]> {
|
||||
CHORDS.iter().find(|c| c.name == name).map(|c| c.intervals)
|
||||
}
|
||||
4
crates/forth/src/theory/mod.rs
Normal file
4
crates/forth/src/theory/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod chords;
|
||||
mod scales;
|
||||
|
||||
pub use scales::lookup;
|
||||
130
crates/forth/src/theory/scales.rs
Normal file
130
crates/forth/src/theory/scales.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
pub struct Scale {
|
||||
pub name: &'static str,
|
||||
pub pattern: &'static [i64],
|
||||
}
|
||||
|
||||
pub static SCALES: &[Scale] = &[
|
||||
Scale {
|
||||
name: "major",
|
||||
pattern: &[0, 2, 4, 5, 7, 9, 11],
|
||||
},
|
||||
Scale {
|
||||
name: "minor",
|
||||
pattern: &[0, 2, 3, 5, 7, 8, 10],
|
||||
},
|
||||
Scale {
|
||||
name: "dorian",
|
||||
pattern: &[0, 2, 3, 5, 7, 9, 10],
|
||||
},
|
||||
Scale {
|
||||
name: "phrygian",
|
||||
pattern: &[0, 1, 3, 5, 7, 8, 10],
|
||||
},
|
||||
Scale {
|
||||
name: "lydian",
|
||||
pattern: &[0, 2, 4, 6, 7, 9, 11],
|
||||
},
|
||||
Scale {
|
||||
name: "mixolydian",
|
||||
pattern: &[0, 2, 4, 5, 7, 9, 10],
|
||||
},
|
||||
Scale {
|
||||
name: "aeolian",
|
||||
pattern: &[0, 2, 3, 5, 7, 8, 10],
|
||||
},
|
||||
Scale {
|
||||
name: "locrian",
|
||||
pattern: &[0, 1, 3, 5, 6, 8, 10],
|
||||
},
|
||||
Scale {
|
||||
name: "pentatonic",
|
||||
pattern: &[0, 2, 4, 7, 9],
|
||||
},
|
||||
Scale {
|
||||
name: "minpent",
|
||||
pattern: &[0, 3, 5, 7, 10],
|
||||
},
|
||||
Scale {
|
||||
name: "blues",
|
||||
pattern: &[0, 3, 5, 6, 7, 10],
|
||||
},
|
||||
Scale {
|
||||
name: "chromatic",
|
||||
pattern: &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
|
||||
},
|
||||
Scale {
|
||||
name: "wholetone",
|
||||
pattern: &[0, 2, 4, 6, 8, 10],
|
||||
},
|
||||
Scale {
|
||||
name: "harmonicminor",
|
||||
pattern: &[0, 2, 3, 5, 7, 8, 11],
|
||||
},
|
||||
Scale {
|
||||
name: "melodicminor",
|
||||
pattern: &[0, 2, 3, 5, 7, 9, 11],
|
||||
},
|
||||
// Jazz/Bebop
|
||||
Scale {
|
||||
name: "bebop",
|
||||
pattern: &[0, 2, 4, 5, 7, 9, 10, 11],
|
||||
},
|
||||
Scale {
|
||||
name: "bebopmaj",
|
||||
pattern: &[0, 2, 4, 5, 7, 8, 9, 11],
|
||||
},
|
||||
Scale {
|
||||
name: "bebopmin",
|
||||
pattern: &[0, 2, 3, 5, 7, 8, 9, 10],
|
||||
},
|
||||
Scale {
|
||||
name: "altered",
|
||||
pattern: &[0, 1, 3, 4, 6, 8, 10],
|
||||
},
|
||||
Scale {
|
||||
name: "lyddom",
|
||||
pattern: &[0, 2, 4, 6, 7, 9, 10],
|
||||
},
|
||||
Scale {
|
||||
name: "halfwhole",
|
||||
pattern: &[0, 1, 3, 4, 6, 7, 9, 10],
|
||||
},
|
||||
Scale {
|
||||
name: "wholehalf",
|
||||
pattern: &[0, 2, 3, 5, 6, 8, 9, 11],
|
||||
},
|
||||
// Symmetric
|
||||
Scale {
|
||||
name: "augmented",
|
||||
pattern: &[0, 3, 4, 7, 8, 11],
|
||||
},
|
||||
Scale {
|
||||
name: "tritone",
|
||||
pattern: &[0, 1, 4, 6, 7, 10],
|
||||
},
|
||||
Scale {
|
||||
name: "prometheus",
|
||||
pattern: &[0, 2, 4, 6, 9, 10],
|
||||
},
|
||||
// Modal variants (from melodic minor)
|
||||
Scale {
|
||||
name: "dorianb2",
|
||||
pattern: &[0, 1, 3, 5, 7, 9, 10],
|
||||
},
|
||||
Scale {
|
||||
name: "lydianaug",
|
||||
pattern: &[0, 2, 4, 6, 8, 9, 11],
|
||||
},
|
||||
Scale {
|
||||
name: "mixb6",
|
||||
pattern: &[0, 2, 4, 5, 7, 8, 10],
|
||||
},
|
||||
Scale {
|
||||
name: "locrian2",
|
||||
pattern: &[0, 2, 3, 5, 6, 8, 10],
|
||||
},
|
||||
];
|
||||
|
||||
pub fn lookup(name: &str) -> Option<&'static [i64]> {
|
||||
SCALES.iter().find(|s| s.name == name).map(|s| s.pattern)
|
||||
}
|
||||
213
crates/forth/src/types.rs
Normal file
213
crates/forth/src/types.rs
Normal file
@@ -0,0 +1,213 @@
|
||||
use arc_swap::ArcSwap;
|
||||
use parking_lot::Mutex;
|
||||
use rand::rngs::StdRng;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::ops::Op;
|
||||
|
||||
/// Trait for accessing MIDI CC values. Implement this to provide CC memory to the Forth VM.
|
||||
pub trait CcAccess: Send + Sync {
|
||||
/// Get the CC value for a given device, channel (0-15), and CC number (0-127).
|
||||
fn get_cc(&self, device: usize, channel: usize, cc: usize) -> u8;
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct SourceSpan {
|
||||
pub start: u32,
|
||||
pub end: u32,
|
||||
}
|
||||
|
||||
#[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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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<'a> {
|
||||
pub step: usize,
|
||||
pub beat: f64,
|
||||
pub bank: usize,
|
||||
pub pattern: usize,
|
||||
pub tempo: f64,
|
||||
pub phase: f64,
|
||||
pub slot: usize,
|
||||
pub runs: usize,
|
||||
pub iter: usize,
|
||||
pub speed: f64,
|
||||
pub fill: bool,
|
||||
pub nudge_secs: f64,
|
||||
pub cc_access: Option<&'a dyn CcAccess>,
|
||||
pub speed_key: &'a str,
|
||||
pub chain_key: &'a str,
|
||||
#[cfg(feature = "desktop")]
|
||||
pub mouse_x: f64,
|
||||
#[cfg(feature = "desktop")]
|
||||
pub mouse_y: f64,
|
||||
#[cfg(feature = "desktop")]
|
||||
pub mouse_down: f64,
|
||||
}
|
||||
|
||||
impl StepContext<'_> {
|
||||
pub fn step_duration(&self) -> f64 {
|
||||
60.0 / self.tempo / 4.0 / self.speed
|
||||
}
|
||||
}
|
||||
|
||||
pub type VariablesMap = HashMap<String, Value>;
|
||||
pub type Variables = Arc<ArcSwap<VariablesMap>>;
|
||||
pub type Dictionary = Arc<Mutex<HashMap<String, Vec<Op>>>>;
|
||||
pub type Rng = Arc<Mutex<StdRng>>;
|
||||
pub type Stack = Mutex<Vec<Value>>;
|
||||
pub(super) type CmdSnapshot<'a> = (Option<&'a Value>, &'a [(&'static str, Value)]);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Value {
|
||||
Int(i64, Option<SourceSpan>),
|
||||
Float(f64, Option<SourceSpan>),
|
||||
Str(Arc<str>, Option<SourceSpan>),
|
||||
Quotation(Arc<[Op]>, Option<SourceSpan>),
|
||||
CycleList(Arc<[Value]>),
|
||||
ArpList(Arc<[Value]>),
|
||||
}
|
||||
|
||||
impl PartialEq for Value {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Value::Int(a, _), Value::Int(b, _)) => a == b,
|
||||
(Value::Float(a, _), Value::Float(b, _)) => a == b,
|
||||
(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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Value {
|
||||
pub fn as_float(&self) -> Result<f64, String> {
|
||||
match self {
|
||||
Value::Float(f, _) => Ok(*f),
|
||||
Value::Int(i, _) => Ok(*i as f64),
|
||||
_ => Err("expected number".into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn as_int(&self) -> Result<i64, String> {
|
||||
match self {
|
||||
Value::Int(i, _) => Ok(*i),
|
||||
Value::Float(f, _) => Ok(*f as i64),
|
||||
_ => Err("expected number".into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn as_str(&self) -> Result<&str, String> {
|
||||
match self {
|
||||
Value::Str(s, _) => Ok(s),
|
||||
_ => Err("expected string".into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn is_truthy(&self) -> bool {
|
||||
match self {
|
||||
Value::Int(i, _) => *i != 0,
|
||||
Value::Float(f, _) => *f != 0.0,
|
||||
Value::Str(s, _) => !s.is_empty(),
|
||||
Value::Quotation(..) => true,
|
||||
Value::CycleList(items) | Value::ArpList(items) => !items.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn to_param_string(&self) -> String {
|
||||
match self {
|
||||
Value::Int(i, _) => i.to_string(),
|
||||
Value::Float(f, _) => f.to_string(),
|
||||
Value::Str(s, _) => s.to_string(),
|
||||
Value::Quotation(..) => 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(_) | Value::ArpList(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(super) struct CmdRegister {
|
||||
sound: Option<Value>,
|
||||
params: Vec<(&'static str, Value)>,
|
||||
deltas: Vec<Value>,
|
||||
}
|
||||
|
||||
impl CmdRegister {
|
||||
pub(super) fn new() -> Self {
|
||||
Self {
|
||||
sound: None,
|
||||
params: Vec::with_capacity(16),
|
||||
deltas: Vec::with_capacity(4),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn set_sound(&mut self, val: Value) {
|
||||
self.sound = Some(val);
|
||||
}
|
||||
|
||||
pub(super) fn set_param(&mut self, key: &'static str, val: Value) {
|
||||
self.params.push((key, val));
|
||||
}
|
||||
|
||||
pub(super) fn set_deltas(&mut self, deltas: Vec<Value>) {
|
||||
self.deltas = deltas;
|
||||
}
|
||||
|
||||
pub(super) fn deltas(&self) -> &[Value] {
|
||||
&self.deltas
|
||||
}
|
||||
|
||||
pub(super) fn sound(&self) -> Option<&Value> {
|
||||
self.sound.as_ref()
|
||||
}
|
||||
|
||||
pub(super) fn params(&self) -> &[(&'static str, Value)] {
|
||||
&self.params
|
||||
}
|
||||
|
||||
pub(super) fn snapshot(&self) -> Option<CmdSnapshot<'_>> {
|
||||
if self.sound.is_some() || !self.params.is_empty() {
|
||||
Some((self.sound.as_ref(), self.params.as_slice()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn clear(&mut self) {
|
||||
self.sound = None;
|
||||
self.params.clear();
|
||||
self.deltas.clear();
|
||||
}
|
||||
}
|
||||
1691
crates/forth/src/vm.rs
Normal file
1691
crates/forth/src/vm.rs
Normal file
File diff suppressed because it is too large
Load Diff
307
crates/forth/src/words/compile.rs
Normal file
307
crates/forth/src/words/compile.rs
Normal file
@@ -0,0 +1,307 @@
|
||||
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,
|
||||
"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,
|
||||
"pick" => 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),
|
||||
"wchoose" => Op::WChoose(None),
|
||||
"every" => Op::Every(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,
|
||||
"perlin" => Op::Perlin,
|
||||
"chain" => Op::Chain,
|
||||
"loop" => Op::Loop,
|
||||
"oct" => Op::Oct,
|
||||
"clear" => Op::ClearCmd,
|
||||
".." => Op::IntRange,
|
||||
".," => Op::StepRange,
|
||||
"gen" => Op::Generate,
|
||||
"geom.." => Op::GeomRange,
|
||||
"euclid" => Op::Euclid,
|
||||
"euclidrot" => Op::EuclidRot,
|
||||
"times" => Op::Times,
|
||||
"m." => Op::MidiEmit,
|
||||
"ccval" => Op::GetMidiCC,
|
||||
"mclock" => Op::MidiClock,
|
||||
"mstart" => Op::MidiStart,
|
||||
"mstop" => Op::MidiStop,
|
||||
"mcont" => Op::MidiContinue,
|
||||
"forget" => Op::Forget,
|
||||
"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::ChanceExec(s) | Op::ProbExec(s)
|
||||
| Op::Every(s)
|
||||
| Op::Bjork(s) | Op::PBjork(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 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
|
||||
}
|
||||
602
crates/forth/src/words/core.rs
Normal file
602
crates/forth/src/words/core.rs
Normal file
@@ -0,0 +1,602 @@
|
||||
use super::{Word, WordCompile::*};
|
||||
|
||||
// Stack, Arithmetic, Comparison, Logic, Control, Variables, Definitions
|
||||
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: "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,
|
||||
},
|
||||
// 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: "pick",
|
||||
aliases: &[],
|
||||
category: "Logic",
|
||||
stack: "(..quots n --)",
|
||||
desc: "Execute nth quotation (0-indexed)",
|
||||
example: "{ 1 } { 2 } { 3 } 2 pick => 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,
|
||||
},
|
||||
// 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,
|
||||
},
|
||||
];
|
||||
962
crates/forth/src/words/effects.rs
Normal file
962
crates/forth/src/words/effects.rs
Normal file
@@ -0,0 +1,962 @@
|
||||
use super::{Word, WordCompile::*};
|
||||
|
||||
// Filter, Envelope, Reverb, Delay, Lo-fi, Stereo, Mod FX
|
||||
pub(super) const WORDS: &[Word] = &[
|
||||
// Envelope
|
||||
Word {
|
||||
name: "gain",
|
||||
aliases: &[],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set volume (0-1)",
|
||||
example: "0.8 gain",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "postgain",
|
||||
aliases: &[],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set post gain",
|
||||
example: "1.2 postgain",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "velocity",
|
||||
aliases: &[],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set velocity",
|
||||
example: "100 velocity",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "attack",
|
||||
aliases: &["att"],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set attack time",
|
||||
example: "0.01 attack",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "decay",
|
||||
aliases: &["dec"],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set decay time",
|
||||
example: "0.1 decay",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "sustain",
|
||||
aliases: &["sus"],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set sustain level",
|
||||
example: "0.5 sustain",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "release",
|
||||
aliases: &["rel"],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set release time",
|
||||
example: "0.3 release",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "adsr",
|
||||
aliases: &[],
|
||||
category: "Envelope",
|
||||
stack: "(a d s r --)",
|
||||
desc: "Set attack, decay, sustain, release",
|
||||
example: "0.01 0.1 0.5 0.3 adsr",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "ad",
|
||||
aliases: &[],
|
||||
category: "Envelope",
|
||||
stack: "(a d --)",
|
||||
desc: "Set attack, decay (sustain=0)",
|
||||
example: "0.01 0.1 ad",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "penv",
|
||||
aliases: &[],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set pitch envelope",
|
||||
example: "0.5 penv",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "patt",
|
||||
aliases: &[],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set pitch attack",
|
||||
example: "0.01 patt",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "pdec",
|
||||
aliases: &[],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set pitch decay",
|
||||
example: "0.1 pdec",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "psus",
|
||||
aliases: &[],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set pitch sustain",
|
||||
example: "0 psus",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "prel",
|
||||
aliases: &[],
|
||||
category: "Envelope",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set pitch release",
|
||||
example: "0.1 prel",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
// Filter
|
||||
Word {
|
||||
name: "lpf",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set lowpass frequency",
|
||||
example: "2000 lpf",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "lpq",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set lowpass resonance",
|
||||
example: "0.5 lpq",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "lpe",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set lowpass envelope",
|
||||
example: "0.5 lpe",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "lpa",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set lowpass attack",
|
||||
example: "0.01 lpa",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "lpd",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set lowpass decay",
|
||||
example: "0.1 lpd",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "lps",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set lowpass sustain",
|
||||
example: "0.5 lps",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "lpr",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set lowpass release",
|
||||
example: "0.3 lpr",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "hpf",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set highpass frequency",
|
||||
example: "100 hpf",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "hpq",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set highpass resonance",
|
||||
example: "0.5 hpq",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "hpe",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set highpass envelope",
|
||||
example: "0.5 hpe",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "hpa",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set highpass attack",
|
||||
example: "0.01 hpa",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "hpd",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set highpass decay",
|
||||
example: "0.1 hpd",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "hps",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set highpass sustain",
|
||||
example: "0.5 hps",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "hpr",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set highpass release",
|
||||
example: "0.3 hpr",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "bpf",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set bandpass frequency",
|
||||
example: "1000 bpf",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "bpq",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set bandpass resonance",
|
||||
example: "0.5 bpq",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "bpe",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set bandpass envelope",
|
||||
example: "0.5 bpe",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "bpa",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set bandpass attack",
|
||||
example: "0.01 bpa",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "bpd",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set bandpass decay",
|
||||
example: "0.1 bpd",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "bps",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set bandpass sustain",
|
||||
example: "0.5 bps",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "bpr",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set bandpass release",
|
||||
example: "0.3 bpr",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "llpf",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set ladder lowpass frequency",
|
||||
example: "2000 llpf",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "llpq",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set ladder lowpass resonance",
|
||||
example: "0.5 llpq",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "lhpf",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set ladder highpass frequency",
|
||||
example: "100 lhpf",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "lhpq",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set ladder highpass resonance",
|
||||
example: "0.5 lhpq",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "lbpf",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set ladder bandpass frequency",
|
||||
example: "1000 lbpf",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "lbpq",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set ladder bandpass resonance",
|
||||
example: "0.5 lbpq",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "ftype",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set filter type",
|
||||
example: "1 ftype",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "eqlo",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set low shelf gain (dB)",
|
||||
example: "3 eqlo",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "eqmid",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set mid peak gain (dB)",
|
||||
example: "-2 eqmid",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "eqhi",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set high shelf gain (dB)",
|
||||
example: "1 eqhi",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "tilt",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set tilt EQ (-1 dark, 1 bright)",
|
||||
example: "-0.5 tilt",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "comb",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set comb filter mix",
|
||||
example: "0.5 comb",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "combfreq",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set comb frequency",
|
||||
example: "200 combfreq",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "combfeedback",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set comb feedback",
|
||||
example: "0.5 combfeedback",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "combdamp",
|
||||
aliases: &[],
|
||||
category: "Filter",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set comb damping",
|
||||
example: "0.5 combdamp",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
// Reverb
|
||||
Word {
|
||||
name: "verb",
|
||||
aliases: &[],
|
||||
category: "Reverb",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set reverb mix",
|
||||
example: "0.3 verb",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "verbdecay",
|
||||
aliases: &[],
|
||||
category: "Reverb",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set reverb decay (0-1)",
|
||||
example: "0.75 verbdecay",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "verbdamp",
|
||||
aliases: &[],
|
||||
category: "Reverb",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set reverb damping",
|
||||
example: "0.5 verbdamp",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "verbpredelay",
|
||||
aliases: &[],
|
||||
category: "Reverb",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set reverb predelay (0-1)",
|
||||
example: "0.1 verbpredelay",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "verbdiff",
|
||||
aliases: &[],
|
||||
category: "Reverb",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set reverb diffusion",
|
||||
example: "0.7 verbdiff",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "verbtype",
|
||||
aliases: &[],
|
||||
category: "Reverb",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set reverb algorithm (vital or dattorro)",
|
||||
example: "vital verbtype",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "verbchorus",
|
||||
aliases: &[],
|
||||
category: "Reverb",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set reverb chorus amount (0-1)",
|
||||
example: "0.3 verbchorus",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "verbchorusfreq",
|
||||
aliases: &[],
|
||||
category: "Reverb",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set reverb chorus frequency (0-1)",
|
||||
example: "0.2 verbchorusfreq",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "verbprelow",
|
||||
aliases: &[],
|
||||
category: "Reverb",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set reverb pre-low filter (0-1)",
|
||||
example: "0.2 verbprelow",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "verbprehigh",
|
||||
aliases: &[],
|
||||
category: "Reverb",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set reverb pre-high filter (0-1)",
|
||||
example: "0.8 verbprehigh",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "verblowcut",
|
||||
aliases: &[],
|
||||
category: "Reverb",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set reverb low cut frequency (0-1)",
|
||||
example: "0.5 verblowcut",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "verbhighcut",
|
||||
aliases: &[],
|
||||
category: "Reverb",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set reverb high cut frequency (0-1)",
|
||||
example: "0.7 verbhighcut",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "verblowgain",
|
||||
aliases: &[],
|
||||
category: "Reverb",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set reverb low gain (0-1)",
|
||||
example: "0.4 verblowgain",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "size",
|
||||
aliases: &[],
|
||||
category: "Reverb",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set size",
|
||||
example: "1 size",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
// Delay
|
||||
Word {
|
||||
name: "delay",
|
||||
aliases: &[],
|
||||
category: "Delay",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set delay mix",
|
||||
example: "0.3 delay",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "delaytime",
|
||||
aliases: &[],
|
||||
category: "Delay",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set delay time",
|
||||
example: "0.25 delaytime",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "delayfeedback",
|
||||
aliases: &[],
|
||||
category: "Delay",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set delay feedback",
|
||||
example: "0.5 delayfeedback",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "delaytype",
|
||||
aliases: &[],
|
||||
category: "Delay",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set delay type",
|
||||
example: "1 delaytype",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
// Lo-fi
|
||||
Word {
|
||||
name: "crush",
|
||||
aliases: &[],
|
||||
category: "Lo-fi",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set bit crush",
|
||||
example: "8 crush",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "fold",
|
||||
aliases: &[],
|
||||
category: "Lo-fi",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set wave fold",
|
||||
example: "2 fold",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "wrap",
|
||||
aliases: &[],
|
||||
category: "Lo-fi",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set wave wrap",
|
||||
example: "0.5 wrap",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "distort",
|
||||
aliases: &[],
|
||||
category: "Lo-fi",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set distortion",
|
||||
example: "0.5 distort",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "distortvol",
|
||||
aliases: &[],
|
||||
category: "Lo-fi",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set distortion volume",
|
||||
example: "0.8 distortvol",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
// Stereo
|
||||
Word {
|
||||
name: "pan",
|
||||
aliases: &[],
|
||||
category: "Stereo",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set pan (-1 to 1)",
|
||||
example: "0.5 pan",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "width",
|
||||
aliases: &[],
|
||||
category: "Stereo",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set stereo width (0 mono, 1 normal, 2 wide)",
|
||||
example: "0 width",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "haas",
|
||||
aliases: &[],
|
||||
category: "Stereo",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set Haas delay in ms (spatial placement)",
|
||||
example: "8 haas",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
// Mod FX
|
||||
Word {
|
||||
name: "phaser",
|
||||
aliases: &[],
|
||||
category: "Mod FX",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set phaser rate",
|
||||
example: "1 phaser",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "phaserdepth",
|
||||
aliases: &[],
|
||||
category: "Mod FX",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set phaser depth",
|
||||
example: "0.5 phaserdepth",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "phasersweep",
|
||||
aliases: &[],
|
||||
category: "Mod FX",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set phaser sweep",
|
||||
example: "0.5 phasersweep",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "phasercenter",
|
||||
aliases: &[],
|
||||
category: "Mod FX",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set phaser center",
|
||||
example: "1000 phasercenter",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "flanger",
|
||||
aliases: &[],
|
||||
category: "Mod FX",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set flanger rate",
|
||||
example: "0.5 flanger",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "flangerdepth",
|
||||
aliases: &[],
|
||||
category: "Mod FX",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set flanger depth",
|
||||
example: "0.5 flangerdepth",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "flangerfeedback",
|
||||
aliases: &[],
|
||||
category: "Mod FX",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set flanger feedback",
|
||||
example: "0.5 flangerfeedback",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "smear",
|
||||
aliases: &[],
|
||||
category: "Mod FX",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set smear allpass chain wet/dry mix (0=bypass, 1=full wet)",
|
||||
example: "0.5 smear",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "smearfreq",
|
||||
aliases: &[],
|
||||
category: "Mod FX",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set smear allpass break frequency in Hz",
|
||||
example: "800 smearfreq",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "smearfb",
|
||||
aliases: &[],
|
||||
category: "Mod FX",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set smear feedback for resonance (0-0.95)",
|
||||
example: "0.8 smearfb",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "chorus",
|
||||
aliases: &[],
|
||||
category: "Mod FX",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set chorus rate",
|
||||
example: "1 chorus",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "chorusdepth",
|
||||
aliases: &[],
|
||||
category: "Mod FX",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set chorus depth",
|
||||
example: "0.5 chorusdepth",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "chorusdelay",
|
||||
aliases: &[],
|
||||
category: "Mod FX",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set chorus delay",
|
||||
example: "0.02 chorusdelay",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "feedback",
|
||||
aliases: &["fb"],
|
||||
category: "Mod FX",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set feedback delay level",
|
||||
example: "0.7 feedback",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "fbtime",
|
||||
aliases: &["fbt"],
|
||||
category: "Mod FX",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set feedback delay time in ms",
|
||||
example: "30 fbtime",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "fbdamp",
|
||||
aliases: &["fbd"],
|
||||
category: "Mod FX",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set feedback delay damping",
|
||||
example: "0.3 fbdamp",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "fblfo",
|
||||
aliases: &[],
|
||||
category: "Mod FX",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set feedback delay LFO rate in Hz",
|
||||
example: "2 fblfo",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "fblfodepth",
|
||||
aliases: &[],
|
||||
category: "Mod FX",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set feedback delay LFO depth",
|
||||
example: "0.5 fblfodepth",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "fblfoshape",
|
||||
aliases: &[],
|
||||
category: "Mod FX",
|
||||
stack: "(v.. --)",
|
||||
desc: "Set feedback delay LFO shape",
|
||||
example: "tri fblfoshape",
|
||||
compile: Param,
|
||||
varargs: true,
|
||||
},
|
||||
];
|
||||
135
crates/forth/src/words/midi.rs
Normal file
135
crates/forth/src/words/midi.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
use super::{Word, WordCompile::*};
|
||||
|
||||
// MIDI
|
||||
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,
|
||||
},
|
||||
];
|
||||
58
crates/forth/src/words/mod.rs
Normal file
58
crates/forth/src/words/mod.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
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;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum WordCompile {
|
||||
Simple,
|
||||
Context(&'static str),
|
||||
Param,
|
||||
Probability(f64),
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
pub fn lookup_word(name: &str) -> Option<&'static Word> {
|
||||
WORD_MAP.get(name).copied()
|
||||
}
|
||||
312
crates/forth/src/words/music.rs
Normal file
312
crates/forth/src/words/music.rs
Normal file
@@ -0,0 +1,312 @@
|
||||
use super::{Word, WordCompile::*};
|
||||
|
||||
// Music, Chord
|
||||
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,
|
||||
},
|
||||
// Chords - Triads
|
||||
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,
|
||||
},
|
||||
// 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,
|
||||
},
|
||||
// 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: "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,
|
||||
},
|
||||
// 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,
|
||||
},
|
||||
];
|
||||
483
crates/forth/src/words/sequencing.rs
Normal file
483
crates/forth/src/words/sequencing.rs
Normal file
@@ -0,0 +1,483 @@
|
||||
use super::{Word, WordCompile::*};
|
||||
|
||||
// Time, Context, Probability, Generator, Desktop
|
||||
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: "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: "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 beats",
|
||||
example: "\"break\" s 4 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: "chain",
|
||||
aliases: &[],
|
||||
category: "Time",
|
||||
stack: "(bank pattern --)",
|
||||
desc: "Chain to bank/pattern (1-indexed) when current pattern ends",
|
||||
example: "1 4 chain",
|
||||
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,
|
||||
},
|
||||
];
|
||||
783
crates/forth/src/words/sound.rs
Normal file
783
crates/forth/src/words/sound.rs
Normal file
@@ -0,0 +1,783 @@
|
||||
use super::{Word, WordCompile::*};
|
||||
|
||||
// Sound, Oscillator, Sample, Wavetable, FM, Modulation, LFO
|
||||
pub(super) const WORDS: &[Word] = &[
|
||||
// Sound
|
||||
Word {
|
||||
name: "sound",
|
||||
aliases: &["s"],
|
||||
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,
|
||||
},
|
||||
// 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: "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: "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: "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: "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,
|
||||
},
|
||||
];
|
||||
12
crates/markdown/Cargo.toml
Normal file
12
crates/markdown/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "cagire-markdown"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Markdown rendering for cagire sequencer"
|
||||
|
||||
[dependencies]
|
||||
minimad = "0.13"
|
||||
ratatui = "0.30"
|
||||
13
crates/markdown/src/highlighter.rs
Normal file
13
crates/markdown/src/highlighter.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use ratatui::style::Style;
|
||||
|
||||
pub trait CodeHighlighter {
|
||||
fn highlight(&self, line: &str) -> Vec<(Style, String)>;
|
||||
}
|
||||
|
||||
pub struct NoHighlight;
|
||||
|
||||
impl CodeHighlighter for NoHighlight {
|
||||
fn highlight(&self, line: &str) -> Vec<(Style, String)> {
|
||||
vec![(Style::default(), line.to_string())]
|
||||
}
|
||||
}
|
||||
7
crates/markdown/src/lib.rs
Normal file
7
crates/markdown/src/lib.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
mod highlighter;
|
||||
mod parser;
|
||||
mod theme;
|
||||
|
||||
pub use highlighter::{CodeHighlighter, NoHighlight};
|
||||
pub use parser::{parse, CodeBlock, ParsedMarkdown};
|
||||
pub use theme::{DefaultTheme, MarkdownTheme};
|
||||
385
crates/markdown/src/parser.rs
Normal file
385
crates/markdown/src/parser.rs
Normal file
@@ -0,0 +1,385 @@
|
||||
use minimad::{Composite, CompositeStyle, Compound, Line, TableRow};
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::text::{Line as RLine, Span};
|
||||
|
||||
use crate::highlighter::CodeHighlighter;
|
||||
use crate::theme::MarkdownTheme;
|
||||
|
||||
pub struct CodeBlock {
|
||||
pub start_line: usize,
|
||||
pub end_line: usize,
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
pub struct ParsedMarkdown {
|
||||
pub lines: Vec<RLine<'static>>,
|
||||
pub code_blocks: Vec<CodeBlock>,
|
||||
}
|
||||
|
||||
pub fn parse<T: MarkdownTheme, H: CodeHighlighter>(
|
||||
md: &str,
|
||||
theme: &T,
|
||||
highlighter: &H,
|
||||
) -> 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() {
|
||||
return;
|
||||
}
|
||||
let col_widths = compute_column_widths(buf);
|
||||
for (row_idx, row) in buf.drain(..).enumerate() {
|
||||
out.push(render_table_row(row, row_idx, &col_widths, theme));
|
||||
}
|
||||
};
|
||||
|
||||
let close_block = |start: Option<usize>,
|
||||
source: &mut Vec<String>,
|
||||
blocks: &mut Vec<CodeBlock>,
|
||||
lines: &Vec<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()),
|
||||
];
|
||||
spans.extend(
|
||||
highlighter
|
||||
.highlight(&raw)
|
||||
.into_iter()
|
||||
.map(|(style, text)| Span::styled(text, style)),
|
||||
);
|
||||
lines.push(RLine::from(spans));
|
||||
}
|
||||
Line::Normal(composite) => {
|
||||
flush_table(&mut table_buffer, &mut lines, theme);
|
||||
code_line_nr = 0;
|
||||
lines.push(composite_to_line(composite, theme));
|
||||
}
|
||||
Line::TableRow(row) => {
|
||||
code_line_nr = 0;
|
||||
table_buffer.push(row);
|
||||
}
|
||||
Line::TableRule(_) => {}
|
||||
_ => {
|
||||
flush_table(&mut table_buffer, &mut lines, theme);
|
||||
code_line_nr = 0;
|
||||
lines.push(RLine::from(""));
|
||||
}
|
||||
}
|
||||
}
|
||||
close_block(
|
||||
current_block_start.take(),
|
||||
&mut current_block_source,
|
||||
&mut code_blocks,
|
||||
&lines,
|
||||
);
|
||||
flush_table(&mut table_buffer, &mut lines, theme);
|
||||
|
||||
ParsedMarkdown { lines, code_blocks }
|
||||
}
|
||||
|
||||
pub 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);
|
||||
let mut result = String::with_capacity(line.len());
|
||||
let mut chars = line.char_indices().peekable();
|
||||
let bytes = line.as_bytes();
|
||||
while let Some((i, c)) = chars.next() {
|
||||
if c == '`' {
|
||||
result.push(c);
|
||||
for (_, ch) in chars.by_ref() {
|
||||
result.push(ch);
|
||||
if ch == '`' {
|
||||
break;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if c == '_' {
|
||||
let before_is_space = i == 0 || bytes[i - 1] == b' ';
|
||||
if before_is_space {
|
||||
if let Some(end) = line[i + 1..].find('_') {
|
||||
let inner = &line[i + 1..i + 1 + end];
|
||||
if !inner.is_empty() {
|
||||
result.push('*');
|
||||
result.push_str(inner);
|
||||
result.push('*');
|
||||
for _ in 0..end {
|
||||
chars.next();
|
||||
}
|
||||
chars.next();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result.push(c);
|
||||
}
|
||||
out.push_str(&result);
|
||||
out.push('\n');
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub 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();
|
||||
format!("{}* {}", " ".repeat(indent), rest)
|
||||
} else {
|
||||
line.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn cell_text_width(cell: &Composite) -> usize {
|
||||
cell.compounds.iter().map(|c| c.src.chars().count()).sum()
|
||||
}
|
||||
|
||||
fn compute_column_widths(rows: &[TableRow]) -> Vec<usize> {
|
||||
let mut widths: Vec<usize> = Vec::new();
|
||||
for row in rows {
|
||||
for (i, cell) in row.cells.iter().enumerate() {
|
||||
let w = cell_text_width(cell);
|
||||
if i >= widths.len() {
|
||||
widths.push(w);
|
||||
} else if w > widths[i] {
|
||||
widths[i] = w;
|
||||
}
|
||||
}
|
||||
}
|
||||
widths
|
||||
}
|
||||
|
||||
fn render_table_row<T: MarkdownTheme>(
|
||||
row: TableRow,
|
||||
row_idx: usize,
|
||||
col_widths: &[usize],
|
||||
theme: &T,
|
||||
) -> RLine<'static> {
|
||||
let is_header = row_idx == 0;
|
||||
let bg = if is_header {
|
||||
theme.table_header_bg()
|
||||
} else if row_idx.is_multiple_of(2) {
|
||||
theme.table_row_even()
|
||||
} else {
|
||||
theme.table_row_odd()
|
||||
};
|
||||
|
||||
let base_style = if is_header {
|
||||
theme.text().bg(bg).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
theme.text().bg(bg)
|
||||
};
|
||||
|
||||
let sep_style = theme.code_border().bg(bg);
|
||||
let mut spans: Vec<Span<'static>> = Vec::new();
|
||||
|
||||
for (i, cell) in row.cells.into_iter().enumerate() {
|
||||
if i > 0 {
|
||||
spans.push(Span::styled(" │ ", sep_style));
|
||||
}
|
||||
let target_width = col_widths.get(i).copied().unwrap_or(0);
|
||||
let cell_width = cell
|
||||
.compounds
|
||||
.iter()
|
||||
.map(|c| c.src.chars().count())
|
||||
.sum::<usize>();
|
||||
|
||||
for compound in cell.compounds {
|
||||
compound_to_spans(compound, base_style, &mut spans, theme);
|
||||
}
|
||||
|
||||
let padding = target_width.saturating_sub(cell_width);
|
||||
if padding > 0 {
|
||||
spans.push(Span::styled(" ".repeat(padding), base_style));
|
||||
}
|
||||
}
|
||||
|
||||
RLine::from(spans)
|
||||
}
|
||||
|
||||
fn composite_to_line<T: MarkdownTheme>(composite: Composite, theme: &T) -> RLine<'static> {
|
||||
let base_style = match composite.style {
|
||||
CompositeStyle::Header(1) => theme.h1(),
|
||||
CompositeStyle::Header(2) => theme.h2(),
|
||||
CompositeStyle::Header(_) => theme.h3(),
|
||||
CompositeStyle::ListItem(_) => theme.list(),
|
||||
CompositeStyle::Quote => theme.quote(),
|
||||
CompositeStyle::Code => theme.code(),
|
||||
CompositeStyle::Paragraph => theme.text(),
|
||||
};
|
||||
|
||||
let prefix: String = match composite.style {
|
||||
CompositeStyle::ListItem(depth) => {
|
||||
let indent = " ".repeat(depth as usize);
|
||||
format!("{indent}• ")
|
||||
}
|
||||
CompositeStyle::Quote => " │ ".to_string(),
|
||||
_ => String::new(),
|
||||
};
|
||||
|
||||
let mut spans: Vec<Span<'static>> = Vec::new();
|
||||
if !prefix.is_empty() {
|
||||
spans.push(Span::styled(prefix, base_style));
|
||||
}
|
||||
|
||||
for compound in composite.compounds {
|
||||
compound_to_spans(compound, base_style, &mut spans, theme);
|
||||
}
|
||||
|
||||
RLine::from(spans)
|
||||
}
|
||||
|
||||
fn compound_to_spans<T: MarkdownTheme>(
|
||||
compound: Compound,
|
||||
base: Style,
|
||||
out: &mut Vec<Span<'static>>,
|
||||
theme: &T,
|
||||
) {
|
||||
let mut style = base;
|
||||
|
||||
if compound.bold {
|
||||
style = style.add_modifier(Modifier::BOLD);
|
||||
}
|
||||
if compound.italic {
|
||||
style = style.add_modifier(Modifier::ITALIC);
|
||||
}
|
||||
if compound.code {
|
||||
style = theme.code();
|
||||
}
|
||||
if compound.strikeout {
|
||||
style = style.add_modifier(Modifier::CROSSED_OUT);
|
||||
}
|
||||
|
||||
let src = compound.src.to_string();
|
||||
let link_style = theme.link();
|
||||
|
||||
let mut rest = src.as_str();
|
||||
while let Some(start) = rest.find('[') {
|
||||
let after_bracket = &rest[start + 1..];
|
||||
if let Some(text_end) = after_bracket.find("](") {
|
||||
let url_start = start + 1 + text_end + 2;
|
||||
if let Some(url_end) = rest[url_start..].find(')') {
|
||||
if start > 0 {
|
||||
out.push(Span::styled(rest[..start].to_string(), style));
|
||||
}
|
||||
let text = &rest[start + 1..start + 1 + text_end];
|
||||
let url = &rest[url_start..url_start + url_end];
|
||||
if text == url {
|
||||
out.push(Span::styled(url.to_string(), link_style));
|
||||
} else {
|
||||
out.push(Span::styled(text.to_string(), link_style));
|
||||
out.push(Span::styled(format!(" ({url})"), theme.link_url()));
|
||||
}
|
||||
rest = &rest[url_start + url_end + 1..];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
out.push(Span::styled(rest[..start + 1].to_string(), style));
|
||||
rest = &rest[start + 1..];
|
||||
}
|
||||
if !rest.is_empty() {
|
||||
out.push(Span::styled(rest.to_string(), style));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::highlighter::NoHighlight;
|
||||
use crate::theme::DefaultTheme;
|
||||
|
||||
#[test]
|
||||
fn test_preprocess_underscores() {
|
||||
assert_eq!(preprocess_markdown("_italic_"), "*italic*\n");
|
||||
assert_eq!(preprocess_markdown("word_with_underscores"), "word_with_underscores\n");
|
||||
assert_eq!(preprocess_markdown("hello _world_"), "hello *world*\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preprocess_dash_lists() {
|
||||
assert_eq!(convert_dash_lists("- item"), "* item");
|
||||
assert_eq!(convert_dash_lists(" - nested"), " * nested");
|
||||
assert_eq!(convert_dash_lists("not-a-list"), "not-a-list");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_headings() {
|
||||
let md = "# H1\n## H2\n### H3";
|
||||
let parsed = parse(md, &DefaultTheme, &NoHighlight);
|
||||
assert_eq!(parsed.lines.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_code_block() {
|
||||
let md = "```\ncode line\n```";
|
||||
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 parsed = parse(md, &DefaultTheme, &NoHighlight);
|
||||
assert_eq!(parsed.lines.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_theme_works() {
|
||||
let md = "Hello **world**";
|
||||
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");
|
||||
}
|
||||
}
|
||||
77
crates/markdown/src/theme.rs
Normal file
77
crates/markdown/src/theme.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
|
||||
pub trait MarkdownTheme {
|
||||
fn h1(&self) -> Style;
|
||||
fn h2(&self) -> Style;
|
||||
fn h3(&self) -> Style;
|
||||
fn text(&self) -> Style;
|
||||
fn code(&self) -> Style;
|
||||
fn code_border(&self) -> Style;
|
||||
fn link(&self) -> Style;
|
||||
fn link_url(&self) -> Style;
|
||||
fn quote(&self) -> Style;
|
||||
fn list(&self) -> Style;
|
||||
fn table_header_bg(&self) -> Color;
|
||||
fn table_row_even(&self) -> Color;
|
||||
fn table_row_odd(&self) -> Color;
|
||||
}
|
||||
|
||||
pub struct DefaultTheme;
|
||||
|
||||
impl MarkdownTheme for DefaultTheme {
|
||||
fn h1(&self) -> Style {
|
||||
Style::new()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED)
|
||||
}
|
||||
|
||||
fn h2(&self) -> Style {
|
||||
Style::new().fg(Color::Blue).add_modifier(Modifier::BOLD)
|
||||
}
|
||||
|
||||
fn h3(&self) -> Style {
|
||||
Style::new().fg(Color::Magenta).add_modifier(Modifier::BOLD)
|
||||
}
|
||||
|
||||
fn text(&self) -> Style {
|
||||
Style::new().fg(Color::White)
|
||||
}
|
||||
|
||||
fn code(&self) -> Style {
|
||||
Style::new().fg(Color::Yellow)
|
||||
}
|
||||
|
||||
fn code_border(&self) -> Style {
|
||||
Style::new().fg(Color::DarkGray)
|
||||
}
|
||||
|
||||
fn link(&self) -> Style {
|
||||
Style::new()
|
||||
.fg(Color::Blue)
|
||||
.add_modifier(Modifier::UNDERLINED)
|
||||
}
|
||||
|
||||
fn link_url(&self) -> Style {
|
||||
Style::new().fg(Color::DarkGray)
|
||||
}
|
||||
|
||||
fn quote(&self) -> Style {
|
||||
Style::new().fg(Color::Gray)
|
||||
}
|
||||
|
||||
fn list(&self) -> Style {
|
||||
Style::new().fg(Color::White)
|
||||
}
|
||||
|
||||
fn table_header_bg(&self) -> Color {
|
||||
Color::DarkGray
|
||||
}
|
||||
|
||||
fn table_row_even(&self) -> Color {
|
||||
Color::Reset
|
||||
}
|
||||
|
||||
fn table_row_odd(&self) -> Color {
|
||||
Color::Reset
|
||||
}
|
||||
}
|
||||
12
crates/project/Cargo.toml
Normal file
12
crates/project/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "cagire-project"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Project data structures for cagire sequencer"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
@@ -4,9 +4,18 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{Bank, Project};
|
||||
use crate::project::{Bank, Project};
|
||||
|
||||
const VERSION: u8 = 1;
|
||||
pub const EXTENSION: &str = "cagire";
|
||||
|
||||
pub fn ensure_extension(path: &Path) -> PathBuf {
|
||||
if path.extension().map(|e| e == EXTENSION).unwrap_or(false) {
|
||||
path.to_path_buf()
|
||||
} else {
|
||||
path.with_extension(EXTENSION)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct ProjectFile {
|
||||
@@ -16,6 +25,10 @@ struct ProjectFile {
|
||||
sample_paths: Vec<PathBuf>,
|
||||
#[serde(default = "default_tempo")]
|
||||
tempo: f64,
|
||||
#[serde(default)]
|
||||
playing_patterns: Vec<(usize, usize)>,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
prelude: String,
|
||||
}
|
||||
|
||||
fn default_tempo() -> f64 {
|
||||
@@ -29,17 +42,23 @@ impl From<&Project> for ProjectFile {
|
||||
banks: project.banks.clone(),
|
||||
sample_paths: project.sample_paths.clone(),
|
||||
tempo: project.tempo,
|
||||
playing_patterns: project.playing_patterns.clone(),
|
||||
prelude: project.prelude.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ProjectFile> for Project {
|
||||
fn from(file: ProjectFile) -> Self {
|
||||
Self {
|
||||
let mut project = Self {
|
||||
banks: file.banks,
|
||||
sample_paths: file.sample_paths,
|
||||
tempo: file.tempo,
|
||||
}
|
||||
playing_patterns: file.playing_patterns,
|
||||
prelude: file.prelude,
|
||||
};
|
||||
project.normalize();
|
||||
project
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,11 +91,12 @@ impl From<serde_json::Error> for FileError {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save(project: &Project, path: &Path) -> Result<(), FileError> {
|
||||
pub fn save(project: &Project, path: &Path) -> Result<PathBuf, FileError> {
|
||||
let path = ensure_extension(path);
|
||||
let file = ProjectFile::from(project);
|
||||
let json = serde_json::to_string_pretty(&file)?;
|
||||
fs::write(path, json)?;
|
||||
Ok(())
|
||||
fs::write(&path, json)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
pub fn load(path: &Path) -> Result<Project, FileError> {
|
||||
10
crates/project/src/lib.rs
Normal file
10
crates/project/src/lib.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
mod file;
|
||||
mod project;
|
||||
|
||||
pub const MAX_BANKS: usize = 32;
|
||||
pub const MAX_PATTERNS: usize = 32;
|
||||
pub const MAX_STEPS: usize = 1024;
|
||||
pub const DEFAULT_LENGTH: usize = 16;
|
||||
|
||||
pub use file::{load, save, FileError};
|
||||
pub use project::{Bank, LaunchQuantization, Pattern, PatternSpeed, Project, Step, SyncMode};
|
||||
493
crates/project/src/project.rs
Normal file
493
crates/project/src/project.rs
Normal file
@@ -0,0 +1,493 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
use crate::{DEFAULT_LENGTH, MAX_BANKS, MAX_PATTERNS, MAX_STEPS};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub struct PatternSpeed {
|
||||
pub num: u8,
|
||||
pub denom: u8,
|
||||
}
|
||||
|
||||
impl PatternSpeed {
|
||||
pub const EIGHTH: Self = Self { num: 1, denom: 8 };
|
||||
pub const FIFTH: Self = Self { num: 1, denom: 5 };
|
||||
pub const QUARTER: Self = Self { num: 1, denom: 4 };
|
||||
pub const THIRD: Self = Self { num: 1, denom: 3 };
|
||||
pub const HALF: Self = Self { num: 1, denom: 2 };
|
||||
pub const TWO_THIRDS: Self = Self { num: 2, denom: 3 };
|
||||
pub const NORMAL: Self = Self { num: 1, denom: 1 };
|
||||
pub const DOUBLE: Self = Self { num: 2, denom: 1 };
|
||||
pub const QUAD: Self = Self { num: 4, denom: 1 };
|
||||
pub const OCTO: Self = Self { num: 8, denom: 1 };
|
||||
|
||||
const PRESETS: &[Self] = &[
|
||||
Self::EIGHTH,
|
||||
Self::FIFTH,
|
||||
Self::QUARTER,
|
||||
Self::THIRD,
|
||||
Self::HALF,
|
||||
Self::TWO_THIRDS,
|
||||
Self::NORMAL,
|
||||
Self::DOUBLE,
|
||||
Self::QUAD,
|
||||
Self::OCTO,
|
||||
];
|
||||
|
||||
pub fn multiplier(&self) -> f64 {
|
||||
self.num as f64 / self.denom as f64
|
||||
}
|
||||
|
||||
pub fn label(&self) -> String {
|
||||
if self.denom == 1 {
|
||||
format!("{}x", self.num)
|
||||
} else {
|
||||
format!("{}/{}x", self.num, self.denom)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&self) -> Self {
|
||||
let current = self.multiplier();
|
||||
Self::PRESETS
|
||||
.iter()
|
||||
.find(|p| p.multiplier() > current + 0.0001)
|
||||
.copied()
|
||||
.unwrap_or(*self)
|
||||
}
|
||||
|
||||
pub fn prev(&self) -> Self {
|
||||
let current = self.multiplier();
|
||||
Self::PRESETS
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|p| p.multiplier() < current - 0.0001)
|
||||
.copied()
|
||||
.unwrap_or(*self)
|
||||
}
|
||||
|
||||
pub fn from_label(s: &str) -> Option<Self> {
|
||||
let s = s.trim().trim_end_matches('x');
|
||||
if let Some((num, denom)) = s.split_once('/') {
|
||||
let num: u8 = num.parse().ok()?;
|
||||
let denom: u8 = denom.parse().ok()?;
|
||||
if denom == 0 {
|
||||
return None;
|
||||
}
|
||||
return Some(Self { num, denom });
|
||||
}
|
||||
if let Ok(val) = s.parse::<f64>() {
|
||||
if val <= 0.0 || val > 255.0 {
|
||||
return None;
|
||||
}
|
||||
if (val - val.round()).abs() < 0.0001 {
|
||||
return Some(Self {
|
||||
num: val.round() as u8,
|
||||
denom: 1,
|
||||
});
|
||||
}
|
||||
for denom in 1..=16u8 {
|
||||
let num = val * denom as f64;
|
||||
if (num - num.round()).abs() < 0.0001 && (1.0..=255.0).contains(&num) {
|
||||
return Some(Self {
|
||||
num: num.round() as u8,
|
||||
denom,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PatternSpeed {
|
||||
fn default() -> Self {
|
||||
Self::NORMAL
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for PatternSpeed {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
(self.num, self.denom).serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for PatternSpeed {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum SpeedFormat {
|
||||
Tuple((u8, u8)),
|
||||
Legacy(String),
|
||||
}
|
||||
|
||||
match SpeedFormat::deserialize(deserializer)? {
|
||||
SpeedFormat::Tuple((num, denom)) => Ok(Self { num, denom }),
|
||||
SpeedFormat::Legacy(s) => Ok(match s.as_str() {
|
||||
"Eighth" => Self::EIGHTH,
|
||||
"Quarter" => Self::QUARTER,
|
||||
"Half" => Self::HALF,
|
||||
"Normal" => Self::NORMAL,
|
||||
"Double" => Self::DOUBLE,
|
||||
"Quad" => Self::QUAD,
|
||||
"Octo" => Self::OCTO,
|
||||
_ => Self::NORMAL,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||
pub enum LaunchQuantization {
|
||||
Immediate,
|
||||
Beat,
|
||||
#[default]
|
||||
Bar,
|
||||
Bars2,
|
||||
Bars4,
|
||||
Bars8,
|
||||
}
|
||||
|
||||
impl LaunchQuantization {
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Immediate => "Immediate",
|
||||
Self::Beat => "Beat",
|
||||
Self::Bar => "1 Bar",
|
||||
Self::Bars2 => "2 Bars",
|
||||
Self::Bars4 => "4 Bars",
|
||||
Self::Bars8 => "8 Bars",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&self) -> Self {
|
||||
match self {
|
||||
Self::Immediate => Self::Beat,
|
||||
Self::Beat => Self::Bar,
|
||||
Self::Bar => Self::Bars2,
|
||||
Self::Bars2 => Self::Bars4,
|
||||
Self::Bars4 => Self::Bars8,
|
||||
Self::Bars8 => Self::Bars8,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prev(&self) -> Self {
|
||||
match self {
|
||||
Self::Immediate => Self::Immediate,
|
||||
Self::Beat => Self::Immediate,
|
||||
Self::Bar => Self::Beat,
|
||||
Self::Bars2 => Self::Bar,
|
||||
Self::Bars4 => Self::Bars2,
|
||||
Self::Bars8 => Self::Bars4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||
pub enum SyncMode {
|
||||
#[default]
|
||||
Reset,
|
||||
PhaseLock,
|
||||
}
|
||||
|
||||
impl SyncMode {
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Reset => "Reset",
|
||||
Self::PhaseLock => "Phase-Lock",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle(&self) -> Self {
|
||||
match self {
|
||||
Self::Reset => Self::PhaseLock,
|
||||
Self::PhaseLock => Self::Reset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Step {
|
||||
pub active: bool,
|
||||
pub script: String,
|
||||
#[serde(default)]
|
||||
pub source: Option<u8>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
impl Step {
|
||||
pub fn is_default(&self) -> bool {
|
||||
self.active && self.script.is_empty() && self.source.is_none() && self.name.is_none()
|
||||
}
|
||||
|
||||
pub fn has_content(&self) -> bool {
|
||||
!self.script.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Step {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
active: true,
|
||||
script: String::new(),
|
||||
source: None,
|
||||
name: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Pattern {
|
||||
pub steps: Vec<Step>,
|
||||
pub length: usize,
|
||||
pub speed: PatternSpeed,
|
||||
pub name: Option<String>,
|
||||
pub quantization: LaunchQuantization,
|
||||
pub sync_mode: SyncMode,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SparseStep {
|
||||
i: usize,
|
||||
#[serde(default = "default_active", skip_serializing_if = "is_true")]
|
||||
active: bool,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
script: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
source: Option<u8>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
fn default_active() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_true(v: &bool) -> bool {
|
||||
*v
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SparsePattern {
|
||||
steps: Vec<SparseStep>,
|
||||
length: usize,
|
||||
#[serde(default)]
|
||||
speed: PatternSpeed,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
name: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "is_default_quantization")]
|
||||
quantization: LaunchQuantization,
|
||||
#[serde(default, skip_serializing_if = "is_default_sync_mode")]
|
||||
sync_mode: SyncMode,
|
||||
}
|
||||
|
||||
fn is_default_quantization(q: &LaunchQuantization) -> bool {
|
||||
*q == LaunchQuantization::default()
|
||||
}
|
||||
|
||||
fn is_default_sync_mode(s: &SyncMode) -> bool {
|
||||
*s == SyncMode::default()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LegacyPattern {
|
||||
steps: Vec<Step>,
|
||||
length: usize,
|
||||
#[serde(default)]
|
||||
speed: PatternSpeed,
|
||||
#[serde(default)]
|
||||
name: Option<String>,
|
||||
#[serde(default)]
|
||||
quantization: LaunchQuantization,
|
||||
#[serde(default)]
|
||||
sync_mode: SyncMode,
|
||||
}
|
||||
|
||||
impl Serialize for Pattern {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
let sparse_steps: Vec<SparseStep> = self
|
||||
.steps
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, step)| !step.is_default())
|
||||
.map(|(i, step)| SparseStep {
|
||||
i,
|
||||
active: step.active,
|
||||
script: step.script.clone(),
|
||||
source: step.source,
|
||||
name: step.name.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let sparse = SparsePattern {
|
||||
steps: sparse_steps,
|
||||
length: self.length,
|
||||
speed: self.speed,
|
||||
name: self.name.clone(),
|
||||
quantization: self.quantization,
|
||||
sync_mode: self.sync_mode,
|
||||
};
|
||||
sparse.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Pattern {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum PatternFormat {
|
||||
Sparse(SparsePattern),
|
||||
Legacy(LegacyPattern),
|
||||
}
|
||||
|
||||
match PatternFormat::deserialize(deserializer)? {
|
||||
PatternFormat::Sparse(sparse) => {
|
||||
let mut steps: Vec<Step> = (0..MAX_STEPS).map(|_| Step::default()).collect();
|
||||
for ss in sparse.steps {
|
||||
if ss.i < MAX_STEPS {
|
||||
steps[ss.i] = Step {
|
||||
active: ss.active,
|
||||
script: ss.script,
|
||||
source: ss.source,
|
||||
name: ss.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(Pattern {
|
||||
steps,
|
||||
length: sparse.length,
|
||||
speed: sparse.speed,
|
||||
name: sparse.name,
|
||||
quantization: sparse.quantization,
|
||||
sync_mode: sparse.sync_mode,
|
||||
})
|
||||
}
|
||||
PatternFormat::Legacy(legacy) => Ok(Pattern {
|
||||
steps: legacy.steps,
|
||||
length: legacy.length,
|
||||
speed: legacy.speed,
|
||||
name: legacy.name,
|
||||
quantization: legacy.quantization,
|
||||
sync_mode: legacy.sync_mode,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Pattern {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
steps: (0..MAX_STEPS).map(|_| Step::default()).collect(),
|
||||
length: DEFAULT_LENGTH,
|
||||
speed: PatternSpeed::default(),
|
||||
name: None,
|
||||
quantization: LaunchQuantization::default(),
|
||||
sync_mode: SyncMode::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Pattern {
|
||||
pub fn step(&self, index: usize) -> Option<&Step> {
|
||||
self.steps.get(index)
|
||||
}
|
||||
|
||||
pub fn step_mut(&mut self, index: usize) -> Option<&mut Step> {
|
||||
self.steps.get_mut(index)
|
||||
}
|
||||
|
||||
pub fn set_length(&mut self, length: usize) {
|
||||
let length = length.clamp(1, MAX_STEPS);
|
||||
while self.steps.len() < length {
|
||||
self.steps.push(Step::default());
|
||||
}
|
||||
self.length = length;
|
||||
}
|
||||
|
||||
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 as usize;
|
||||
} else {
|
||||
return current;
|
||||
}
|
||||
} else {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Bank {
|
||||
pub patterns: Vec<Pattern>,
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for Bank {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
patterns: (0..MAX_PATTERNS).map(|_| Pattern::default()).collect(),
|
||||
name: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Project {
|
||||
pub banks: Vec<Bank>,
|
||||
#[serde(default)]
|
||||
pub sample_paths: Vec<PathBuf>,
|
||||
#[serde(default = "default_tempo")]
|
||||
pub tempo: f64,
|
||||
#[serde(default)]
|
||||
pub playing_patterns: Vec<(usize, usize)>,
|
||||
#[serde(default)]
|
||||
pub prelude: String,
|
||||
}
|
||||
|
||||
fn default_tempo() -> f64 {
|
||||
120.0
|
||||
}
|
||||
|
||||
impl Default for Project {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
banks: (0..MAX_BANKS).map(|_| Bank::default()).collect(),
|
||||
sample_paths: Vec::new(),
|
||||
tempo: default_tempo(),
|
||||
playing_patterns: Vec::new(),
|
||||
prelude: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Project {
|
||||
pub fn pattern_at(&self, bank: usize, pattern: usize) -> &Pattern {
|
||||
&self.banks[bank].patterns[pattern]
|
||||
}
|
||||
|
||||
pub fn pattern_at_mut(&mut self, bank: usize, pattern: usize) -> &mut Pattern {
|
||||
&mut self.banks[bank].patterns[pattern]
|
||||
}
|
||||
|
||||
pub fn normalize(&mut self) {
|
||||
self.banks.resize_with(MAX_BANKS, Bank::default);
|
||||
for bank in &mut self.banks {
|
||||
bank.patterns.resize_with(MAX_PATTERNS, Pattern::default);
|
||||
for pattern in &mut bank.patterns {
|
||||
pattern.steps.resize_with(MAX_STEPS, Step::default);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
crates/ratatui/Cargo.toml
Normal file
14
crates/ratatui/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "cagire-ratatui"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
description = "TUI components for cagire sequencer"
|
||||
|
||||
[dependencies]
|
||||
rand = "0.8"
|
||||
ratatui = "0.30"
|
||||
regex = "1"
|
||||
tui-textarea = { git = "https://github.com/phsym/tui-textarea", branch = "main", features = ["search"] }
|
||||
104
crates/ratatui/src/active_patterns.rs
Normal file
104
crates/ratatui/src/active_patterns.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
use crate::theme;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::widgets::Widget;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MuteStatus {
|
||||
Normal,
|
||||
Muted,
|
||||
Soloed,
|
||||
EffectivelyMuted, // Solo active on another pattern
|
||||
}
|
||||
|
||||
pub struct ActivePatterns<'a> {
|
||||
patterns: &'a [(usize, usize, usize)], // (bank, pattern, iter)
|
||||
mute_status: Option<&'a [MuteStatus]>,
|
||||
current_step: Option<(usize, usize)>, // (current_step, total_steps)
|
||||
}
|
||||
|
||||
impl<'a> ActivePatterns<'a> {
|
||||
pub fn new(patterns: &'a [(usize, usize, usize)]) -> Self {
|
||||
Self {
|
||||
patterns,
|
||||
mute_status: None,
|
||||
current_step: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_step(mut self, current: usize, total: usize) -> Self {
|
||||
self.current_step = Some((current, total));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_mute_status(mut self, status: &'a [MuteStatus]) -> Self {
|
||||
self.mute_status = Some(status);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for ActivePatterns<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
if area.width < 10 || area.height == 0 {
|
||||
return;
|
||||
}
|
||||
let theme = theme::get();
|
||||
|
||||
let max_pattern_rows = if self.current_step.is_some() {
|
||||
area.height.saturating_sub(1) as usize
|
||||
} else {
|
||||
area.height as usize
|
||||
};
|
||||
|
||||
for (row, &(bank, pattern, iter)) in self.patterns.iter().enumerate() {
|
||||
if row >= max_pattern_rows {
|
||||
break;
|
||||
}
|
||||
|
||||
let mute_status = self
|
||||
.mute_status
|
||||
.and_then(|s| s.get(row))
|
||||
.copied()
|
||||
.unwrap_or(MuteStatus::Normal);
|
||||
|
||||
let (prefix, fg, bg) = match mute_status {
|
||||
MuteStatus::Soloed => ("S", theme.list.soloed_fg, theme.list.soloed_bg),
|
||||
MuteStatus::Muted => ("M", theme.list.muted_fg, theme.list.muted_bg),
|
||||
MuteStatus::EffectivelyMuted => (" ", theme.list.muted_fg, theme.list.muted_bg),
|
||||
MuteStatus::Normal => {
|
||||
let bg = if row % 2 == 0 {
|
||||
theme.table.row_even
|
||||
} else {
|
||||
theme.table.row_odd
|
||||
};
|
||||
(" ", theme.ui.text_primary, bg)
|
||||
}
|
||||
};
|
||||
|
||||
let text = format!("{}B{:02}:{:02}({:02})", prefix, bank + 1, pattern + 1, iter.min(99));
|
||||
let y = area.y + row as u16;
|
||||
|
||||
let mut chars = text.chars();
|
||||
for col in 0..area.width as usize {
|
||||
let ch = chars.next().unwrap_or(' ');
|
||||
buf[(area.x + col as u16, y)]
|
||||
.set_char(ch)
|
||||
.set_fg(fg)
|
||||
.set_bg(bg);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((current, total)) = self.current_step {
|
||||
let text = format!("{:02}/{:02}", current + 1, total);
|
||||
let y = area.y + area.height.saturating_sub(1);
|
||||
let mut chars = text.chars();
|
||||
for col in 0..area.width as usize {
|
||||
let ch = chars.next().unwrap_or(' ');
|
||||
buf[(area.x + col as u16, y)]
|
||||
.set_char(ch)
|
||||
.set_fg(theme.ui.text_primary)
|
||||
.set_bg(theme.table.row_even);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
145
crates/ratatui/src/category_list.rs
Normal file
145
crates/ratatui/src/category_list.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::widgets::{Block, Borders, List, ListItem};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::theme;
|
||||
|
||||
pub struct CategoryItem<'a> {
|
||||
pub label: &'a str,
|
||||
pub is_section: bool,
|
||||
}
|
||||
|
||||
pub struct CategoryList<'a> {
|
||||
items: &'a [CategoryItem<'a>],
|
||||
selected: usize,
|
||||
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>], selected: usize) -> Self {
|
||||
let theme = theme::get();
|
||||
Self {
|
||||
items,
|
||||
selected,
|
||||
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
|
||||
}
|
||||
|
||||
pub fn render(self, frame: &mut Frame, area: Rect) {
|
||||
let theme = theme::get();
|
||||
|
||||
let visible_height = area.height.saturating_sub(2) as usize;
|
||||
let total_items = self.items.len();
|
||||
|
||||
let selected_visual_idx = {
|
||||
let mut visual = 0;
|
||||
let mut selectable_count = 0;
|
||||
for item in self.items.iter() {
|
||||
if !item.is_section {
|
||||
if selectable_count == self.selected {
|
||||
break;
|
||||
}
|
||||
selectable_count += 1;
|
||||
}
|
||||
visual += 1;
|
||||
}
|
||||
visual
|
||||
};
|
||||
|
||||
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 mut selectable_idx = self.items
|
||||
.iter()
|
||||
.take(scroll)
|
||||
.filter(|e| !e.is_section)
|
||||
.count();
|
||||
|
||||
let is_dimmed = self.dimmed_color.is_some();
|
||||
|
||||
let items: Vec<ListItem> = self.items
|
||||
.iter()
|
||||
.skip(scroll)
|
||||
.take(visible_height)
|
||||
.map(|item| {
|
||||
if item.is_section {
|
||||
let style = Style::new().fg(self.section_color);
|
||||
ListItem::new(format!("─ {} ─", item.label)).style(style)
|
||||
} else {
|
||||
let is_selected = selectable_idx == self.selected;
|
||||
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 { " " };
|
||||
selectable_idx += 1;
|
||||
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,5 +1,6 @@
|
||||
use crate::theme;
|
||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||
use ratatui::style::{Color, Style};
|
||||
use ratatui::style::Style;
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::Frame;
|
||||
@@ -21,11 +22,12 @@ 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)
|
||||
.height(5)
|
||||
.border_color(Color::Yellow)
|
||||
.border_color(t.confirm.border)
|
||||
.render_centered(frame, term);
|
||||
|
||||
let rows = Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).split(inner);
|
||||
@@ -36,12 +38,12 @@ impl<'a> ConfirmModal<'a> {
|
||||
);
|
||||
|
||||
let yes_style = if self.selected {
|
||||
Style::new().fg(Color::Black).bg(Color::Yellow)
|
||||
Style::new().fg(t.confirm.button_selected_fg).bg(t.confirm.button_selected_bg)
|
||||
} else {
|
||||
Style::default()
|
||||
};
|
||||
let no_style = if !self.selected {
|
||||
Style::new().fg(Color::Black).bg(Color::Yellow)
|
||||
Style::new().fg(t.confirm.button_selected_fg).bg(t.confirm.button_selected_bg)
|
||||
} else {
|
||||
Style::default()
|
||||
};
|
||||
@@ -56,5 +58,7 @@ impl<'a> ConfirmModal<'a> {
|
||||
Paragraph::new(buttons).alignment(Alignment::Center),
|
||||
rows[1],
|
||||
);
|
||||
|
||||
inner
|
||||
}
|
||||
}
|
||||
732
crates/ratatui/src/editor.rs
Normal file
732
crates/ratatui/src/editor.rs
Normal file
@@ -0,0 +1,732 @@
|
||||
use std::cell::Cell;
|
||||
|
||||
use crate::theme;
|
||||
use ratatui::{
|
||||
layout::Rect,
|
||||
style::{Modifier, Style},
|
||||
text::{Line, Span},
|
||||
widgets::{Clear, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
use tui_textarea::TextArea;
|
||||
|
||||
pub type Highlighter<'a> = &'a dyn Fn(usize, &str) -> Vec<(Style, String, bool)>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CompletionCandidate {
|
||||
pub name: String,
|
||||
pub signature: String,
|
||||
pub description: String,
|
||||
pub example: String,
|
||||
}
|
||||
|
||||
struct CompletionState {
|
||||
candidates: Vec<CompletionCandidate>,
|
||||
matches: Vec<usize>,
|
||||
cursor: usize,
|
||||
prefix: String,
|
||||
prefix_start_col: usize,
|
||||
active: bool,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
impl CompletionState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
candidates: Vec::new(),
|
||||
matches: Vec::new(),
|
||||
cursor: 0,
|
||||
prefix: String::new(),
|
||||
prefix_start_col: 0,
|
||||
active: false,
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
impl SearchState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
query: String::new(),
|
||||
active: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Editor {
|
||||
text: TextArea<'static>,
|
||||
completion: CompletionState,
|
||||
sample_finder: SampleFinderState,
|
||||
search: SearchState,
|
||||
scroll_offset: Cell<u16>,
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
pub fn start_selection(&mut self) {
|
||||
self.text.start_selection();
|
||||
}
|
||||
|
||||
pub fn cancel_selection(&mut self) {
|
||||
self.text.cancel_selection();
|
||||
}
|
||||
|
||||
pub fn is_selecting(&self) -> bool {
|
||||
self.text.is_selecting()
|
||||
}
|
||||
|
||||
pub fn copy(&mut self) {
|
||||
self.text.copy();
|
||||
}
|
||||
|
||||
pub fn cut(&mut self) -> bool {
|
||||
self.text.cut()
|
||||
}
|
||||
|
||||
pub fn paste(&mut self) -> bool {
|
||||
self.text.paste()
|
||||
}
|
||||
|
||||
pub fn select_all(&mut self) {
|
||||
self.text.select_all();
|
||||
}
|
||||
|
||||
pub fn selection_range(&self) -> Option<((usize, usize), (usize, usize))> {
|
||||
self.text.selection_range()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Editor {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
pub fn new() -> Self {
|
||||
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>) {
|
||||
self.text = TextArea::new(lines);
|
||||
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>) {
|
||||
self.completion.candidates = candidates;
|
||||
}
|
||||
|
||||
pub fn insert_str(&mut self, s: &str) {
|
||||
self.text.insert_str(s);
|
||||
}
|
||||
|
||||
pub fn content(&self) -> String {
|
||||
self.text.lines().join("\n")
|
||||
}
|
||||
|
||||
pub fn lines(&self) -> &[String] {
|
||||
self.text.lines()
|
||||
}
|
||||
|
||||
pub fn cursor(&self) -> (usize, usize) {
|
||||
self.text.cursor()
|
||||
}
|
||||
|
||||
pub fn completion_active(&self) -> bool {
|
||||
self.completion.active
|
||||
}
|
||||
|
||||
pub fn dismiss_completion(&mut self) {
|
||||
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 {
|
||||
self.completion.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn activate_search(&mut self) {
|
||||
self.search.active = true;
|
||||
self.completion.active = false;
|
||||
}
|
||||
|
||||
pub fn search_active(&self) -> bool {
|
||||
self.search.active
|
||||
}
|
||||
|
||||
pub fn search_query(&self) -> &str {
|
||||
&self.search.query
|
||||
}
|
||||
|
||||
pub fn search_input(&mut self, c: char) {
|
||||
self.search.query.push(c);
|
||||
self.apply_search_pattern();
|
||||
}
|
||||
|
||||
pub fn search_backspace(&mut self) {
|
||||
self.search.query.pop();
|
||||
self.apply_search_pattern();
|
||||
}
|
||||
|
||||
pub fn search_confirm(&mut self) {
|
||||
self.search.active = false;
|
||||
}
|
||||
|
||||
pub fn search_clear(&mut self) {
|
||||
self.search.query.clear();
|
||||
self.search.active = false;
|
||||
let _ = self.text.set_search_pattern("");
|
||||
}
|
||||
|
||||
pub fn search_next(&mut self) -> bool {
|
||||
if self.search.query.is_empty() {
|
||||
return false;
|
||||
}
|
||||
self.text.search_forward(false)
|
||||
}
|
||||
|
||||
pub fn search_prev(&mut self) -> bool {
|
||||
if self.search.query.is_empty() {
|
||||
return false;
|
||||
}
|
||||
self.text.search_back(false)
|
||||
}
|
||||
|
||||
fn apply_search_pattern(&mut self) {
|
||||
if self.search.query.is_empty() {
|
||||
let _ = self.text.set_search_pattern("");
|
||||
} else {
|
||||
let pattern = format!("(?i){}", regex::escape(&self.search.query));
|
||||
let _ = self.text.set_search_pattern(&pattern);
|
||||
}
|
||||
}
|
||||
|
||||
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::Tab, .. } => {
|
||||
self.accept_completion();
|
||||
return;
|
||||
}
|
||||
tui_textarea::Input { key: tui_textarea::Key::Esc, .. } => {
|
||||
self.completion.active = false;
|
||||
return;
|
||||
}
|
||||
tui_textarea::Input { key: tui_textarea::Key::Char(c), .. } => {
|
||||
if !is_word_char(*c) {
|
||||
self.completion.active = false;
|
||||
}
|
||||
self.text.input(input);
|
||||
self.update_completion();
|
||||
return;
|
||||
}
|
||||
_ => {
|
||||
self.completion.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.text.input(input);
|
||||
if !has_modifier {
|
||||
self.update_completion();
|
||||
}
|
||||
}
|
||||
|
||||
fn update_completion(&mut self) {
|
||||
if !self.completion.enabled || self.completion.candidates.is_empty() || self.sample_finder.active {
|
||||
return;
|
||||
}
|
||||
|
||||
let (row, col) = self.text.cursor();
|
||||
let line = &self.text.lines()[row];
|
||||
|
||||
// col is a character index; convert to byte offset for slicing
|
||||
let byte_col = line.char_indices()
|
||||
.nth(col)
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or(line.len());
|
||||
|
||||
let prefix_start = line[..byte_col]
|
||||
.char_indices()
|
||||
.rev()
|
||||
.take_while(|(_, c)| is_word_char(*c))
|
||||
.last()
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or(byte_col);
|
||||
|
||||
let prefix = &line[prefix_start..byte_col];
|
||||
|
||||
if prefix.len() < 2 {
|
||||
self.completion.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let prefix_lower = prefix.to_lowercase();
|
||||
let matches: Vec<usize> = self
|
||||
.completion
|
||||
.candidates
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, c)| c.name.to_lowercase().starts_with(&prefix_lower))
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
|
||||
if matches.is_empty() {
|
||||
self.completion.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if matches.len() == 1
|
||||
&& self.completion.candidates[matches[0]].name.to_lowercase() == prefix_lower
|
||||
{
|
||||
self.completion.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
self.completion.prefix = prefix.to_string();
|
||||
self.completion.prefix_start_col = prefix_start;
|
||||
self.completion.matches = matches;
|
||||
self.completion.cursor = self.completion.cursor.min(
|
||||
self.completion.matches.len().saturating_sub(1),
|
||||
);
|
||||
self.completion.active = true;
|
||||
}
|
||||
|
||||
fn accept_completion(&mut self) {
|
||||
if self.completion.matches.is_empty() {
|
||||
self.completion.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let idx = self.completion.matches[self.completion.cursor];
|
||||
let name = self.completion.candidates[idx].name.clone();
|
||||
let prefix_len = self.completion.prefix.len();
|
||||
|
||||
for _ in 0..prefix_len {
|
||||
self.text.delete_char();
|
||||
}
|
||||
self.text.insert_str(&name);
|
||||
|
||||
self.completion.active = false;
|
||||
}
|
||||
|
||||
pub fn render(&self, frame: &mut Frame, area: Rect, highlighter: Highlighter) {
|
||||
let t = theme::get();
|
||||
let (cursor_row, cursor_col) = self.text.cursor();
|
||||
let cursor_style = Style::default().bg(t.editor_widget.cursor_bg).fg(t.editor_widget.cursor_fg);
|
||||
let selection_style = Style::default().bg(t.editor_widget.selection_bg);
|
||||
|
||||
let selection = self.text.selection_range();
|
||||
|
||||
let lines: Vec<Line> = self
|
||||
.text
|
||||
.lines()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(row, line)| {
|
||||
let tokens = highlighter(row, line);
|
||||
let mut spans: Vec<Span> = Vec::new();
|
||||
let mut col = 0;
|
||||
|
||||
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);
|
||||
if is_cursor {
|
||||
cursor_style
|
||||
} else if is_selected {
|
||||
base_style.bg(selection_style.bg.unwrap())
|
||||
} 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));
|
||||
}
|
||||
|
||||
Line::from(spans)
|
||||
})
|
||||
.collect();
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_completion(&self, frame: &mut Frame, editor_area: Rect, cursor_row: usize) {
|
||||
let t = theme::get();
|
||||
let max_visible: usize = 6;
|
||||
let list_width: u16 = 18;
|
||||
let doc_width: u16 = 40;
|
||||
let total_width = list_width + doc_width;
|
||||
|
||||
let visible_count = self.completion.matches.len().min(max_visible);
|
||||
let list_height = visible_count as u16;
|
||||
let doc_height = 4u16;
|
||||
let total_height = list_height.max(doc_height);
|
||||
|
||||
let popup_x = (editor_area.x + self.completion.prefix_start_col as u16)
|
||||
.min(editor_area.x + editor_area.width.saturating_sub(total_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 scroll_offset = if self.completion.cursor >= max_visible {
|
||||
self.completion.cursor - max_visible + 1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// List panel
|
||||
let list_area = Rect::new(popup_x, popup_y, list_width, total_height);
|
||||
frame.render_widget(Clear, list_area);
|
||||
|
||||
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 bg_style = Style::default().bg(t.editor_widget.completion_bg);
|
||||
|
||||
let list_lines: Vec<Line> = (scroll_offset..scroll_offset + visible_count)
|
||||
.map(|i| {
|
||||
let idx = self.completion.matches[i];
|
||||
let name = &self.completion.candidates[idx].name;
|
||||
let style = if i == self.completion.cursor {
|
||||
highlight_style
|
||||
} else {
|
||||
normal_style
|
||||
};
|
||||
let prefix = if i == self.completion.cursor { "> " } else { " " };
|
||||
let display = format!("{prefix}{name:<width$}", width = list_width as usize - 2);
|
||||
Line::from(Span::styled(display, style.bg(t.editor_widget.completion_bg)))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Fill remaining height with empty bg lines
|
||||
let mut all_list_lines = list_lines;
|
||||
for _ in visible_count as u16..total_height {
|
||||
all_list_lines.push(Line::from(Span::styled(
|
||||
" ".repeat(list_width as usize),
|
||||
bg_style,
|
||||
)));
|
||||
}
|
||||
|
||||
frame.render_widget(Paragraph::new(all_list_lines), list_area);
|
||||
|
||||
// Doc panel
|
||||
let doc_area = Rect::new(popup_x + list_width, popup_y, doc_width, total_height);
|
||||
frame.render_widget(Clear, doc_area);
|
||||
|
||||
let selected_idx = self.completion.matches[self.completion.cursor];
|
||||
let candidate = &self.completion.candidates[selected_idx];
|
||||
|
||||
let name_style = Style::default()
|
||||
.fg(t.editor_widget.completion_selected)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(t.editor_widget.completion_bg);
|
||||
let desc_style = Style::default()
|
||||
.fg(t.editor_widget.completion_fg)
|
||||
.bg(t.editor_widget.completion_bg);
|
||||
let example_style = Style::default()
|
||||
.fg(t.editor_widget.completion_example)
|
||||
.bg(t.editor_widget.completion_bg);
|
||||
|
||||
let w = doc_width as usize;
|
||||
let mut doc_lines: Vec<Line> = Vec::new();
|
||||
|
||||
let header = format!(" {} {}", candidate.name, candidate.signature);
|
||||
doc_lines.push(Line::from(Span::styled(
|
||||
format!("{header:<w$}"),
|
||||
name_style,
|
||||
)));
|
||||
|
||||
let desc = format!(" {}", candidate.description);
|
||||
doc_lines.push(Line::from(Span::styled(
|
||||
format!("{desc:<w$}"),
|
||||
desc_style,
|
||||
)));
|
||||
|
||||
doc_lines.push(Line::from(Span::styled(" ".repeat(w), bg_style)));
|
||||
|
||||
if !candidate.example.is_empty() {
|
||||
let ex = format!(" {}", candidate.example);
|
||||
doc_lines.push(Line::from(Span::styled(
|
||||
format!("{ex:<w$}"),
|
||||
example_style,
|
||||
)));
|
||||
}
|
||||
|
||||
for _ in doc_lines.len() as u16..total_height {
|
||||
doc_lines.push(Line::from(Span::styled(" ".repeat(w), bg_style)));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
c.is_alphanumeric() || matches!(c, '!' | '@' | '?' | '.' | ':' | '_' | '#')
|
||||
}
|
||||
|
||||
fn is_in_selection(row: usize, col: usize, selection: Option<((usize, usize), (usize, usize))>) -> bool {
|
||||
let Some(((start_row, start_col), (end_row, end_col))) = selection else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if row < start_row || row > end_row {
|
||||
return false;
|
||||
}
|
||||
|
||||
if row == start_row && row == end_row {
|
||||
col >= start_col && col < end_col
|
||||
} else if row == start_row {
|
||||
col >= start_col
|
||||
} else if row == end_row {
|
||||
col < end_col
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
118
crates/ratatui/src/file_browser.rs
Normal file
118
crates/ratatui/src/file_browser.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
use crate::theme;
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::{Color, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::Frame;
|
||||
|
||||
use super::ModalFrame;
|
||||
|
||||
pub struct FileBrowserModal<'a> {
|
||||
title: &'a str,
|
||||
input: &'a str,
|
||||
entries: &'a [(String, bool, bool)],
|
||||
selected: usize,
|
||||
scroll_offset: usize,
|
||||
border_color: Option<Color>,
|
||||
width: u16,
|
||||
height: u16,
|
||||
}
|
||||
|
||||
impl<'a> FileBrowserModal<'a> {
|
||||
pub fn new(title: &'a str, input: &'a str, entries: &'a [(String, bool, bool)]) -> Self {
|
||||
Self {
|
||||
title,
|
||||
input,
|
||||
entries,
|
||||
selected: 0,
|
||||
scroll_offset: 0,
|
||||
border_color: None,
|
||||
width: 60,
|
||||
height: 16,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selected(mut self, idx: usize) -> Self {
|
||||
self.selected = idx;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn scroll_offset(mut self, offset: usize) -> Self {
|
||||
self.scroll_offset = offset;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn border_color(mut self, c: Color) -> Self {
|
||||
self.border_color = Some(c);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn width(mut self, w: u16) -> Self {
|
||||
self.width = w;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn height(mut self, h: u16) -> Self {
|
||||
self.height = h;
|
||||
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);
|
||||
|
||||
let inner = ModalFrame::new(self.title)
|
||||
.width(self.width)
|
||||
.height(self.height)
|
||||
.border_color(border_color)
|
||||
.render_centered(frame, term);
|
||||
|
||||
let rows = Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).split(inner);
|
||||
|
||||
// Input line
|
||||
frame.render_widget(
|
||||
Paragraph::new(Line::from(vec![
|
||||
Span::raw("> "),
|
||||
Span::styled(self.input, Style::new().fg(colors.input.text)),
|
||||
Span::styled("█", Style::new().fg(colors.input.cursor)),
|
||||
])),
|
||||
rows[0],
|
||||
);
|
||||
|
||||
// Entries list
|
||||
let visible_height = rows[1].height as usize;
|
||||
let visible_entries = self
|
||||
.entries
|
||||
.iter()
|
||||
.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;
|
||||
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 {
|
||||
colors.browser.directory
|
||||
} else if *is_cagire {
|
||||
colors.browser.project_file
|
||||
} else {
|
||||
colors.browser.file
|
||||
};
|
||||
Line::from(Span::styled(display, Style::new().fg(color)))
|
||||
})
|
||||
.collect();
|
||||
|
||||
frame.render_widget(Paragraph::new(lines), rows[1]);
|
||||
|
||||
inner
|
||||
}
|
||||
}
|
||||
27
crates/ratatui/src/hint_bar.rs
Normal file
27
crates/ratatui/src/hint_bar.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::style::Style;
|
||||
|
||||
use crate::theme;
|
||||
|
||||
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)
|
||||
}
|
||||
42
crates/ratatui/src/lib.rs
Normal file
42
crates/ratatui/src/lib.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
mod active_patterns;
|
||||
mod category_list;
|
||||
mod confirm;
|
||||
mod editor;
|
||||
mod file_browser;
|
||||
mod hint_bar;
|
||||
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 active_patterns::{ActivePatterns, MuteStatus};
|
||||
pub use category_list::{CategoryItem, CategoryList};
|
||||
pub use confirm::ConfirmModal;
|
||||
pub use editor::{fuzzy_match, CompletionCandidate, Editor};
|
||||
pub use file_browser::FileBrowserModal;
|
||||
pub use hint_bar::hint_line;
|
||||
pub use list_select::ListSelect;
|
||||
pub use modal::ModalFrame;
|
||||
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 text_input::TextInputModal;
|
||||
pub use vu_meter::VuMeter;
|
||||
pub use waveform::Waveform;
|
||||
105
crates/ratatui/src/list_select.rs
Normal file
105
crates/ratatui/src/list_select.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
use crate::theme;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::Frame;
|
||||
|
||||
pub struct ListSelect<'a> {
|
||||
items: &'a [String],
|
||||
selected: usize,
|
||||
cursor: usize,
|
||||
focused: bool,
|
||||
visible_count: usize,
|
||||
scroll_offset: usize,
|
||||
}
|
||||
|
||||
impl<'a> ListSelect<'a> {
|
||||
pub fn new(items: &'a [String], selected: usize, cursor: usize) -> Self {
|
||||
Self {
|
||||
items,
|
||||
selected,
|
||||
cursor,
|
||||
focused: false,
|
||||
visible_count: 5,
|
||||
scroll_offset: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focused(mut self, focused: bool) -> Self {
|
||||
self.focused = focused;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn scroll_offset(mut self, offset: usize) -> Self {
|
||||
self.scroll_offset = offset;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn visible_count(mut self, n: usize) -> Self {
|
||||
self.visible_count = n;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn height(&self) -> u16 {
|
||||
let item_lines = self.items.len().min(self.visible_count) as u16;
|
||||
if self.items.len() > self.visible_count {
|
||||
item_lines + 1
|
||||
} else {
|
||||
item_lines
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(self, frame: &mut Frame, area: Rect) {
|
||||
let colors = theme::get();
|
||||
let cursor_style = Style::new().fg(colors.hint.key).add_modifier(Modifier::BOLD);
|
||||
let selected_style = Style::new().fg(colors.ui.accent);
|
||||
let normal_style = Style::default();
|
||||
let indicator_style = Style::new().fg(colors.ui.text_dim);
|
||||
|
||||
let visible_end = (self.scroll_offset + self.visible_count).min(self.items.len());
|
||||
let has_above = self.scroll_offset > 0;
|
||||
let has_below = visible_end < self.items.len();
|
||||
|
||||
let mut lines: Vec<Line> = Vec::new();
|
||||
|
||||
for i in self.scroll_offset..visible_end {
|
||||
let name = &self.items[i];
|
||||
let is_cursor = self.focused && i == self.cursor;
|
||||
let is_selected = i == self.selected;
|
||||
|
||||
let style = if is_cursor {
|
||||
cursor_style
|
||||
} else if is_selected {
|
||||
selected_style
|
||||
} else {
|
||||
normal_style
|
||||
};
|
||||
|
||||
let prefix = if is_selected { "x " } else { " " };
|
||||
let mut spans = vec![
|
||||
Span::styled(prefix.to_string(), style),
|
||||
Span::styled(name.clone(), style),
|
||||
];
|
||||
|
||||
if has_above && i == self.scroll_offset {
|
||||
spans.push(Span::styled(" ▲", indicator_style));
|
||||
} else if has_below && i == visible_end - 1 {
|
||||
spans.push(Span::styled(" ▼", indicator_style));
|
||||
}
|
||||
|
||||
lines.push(Line::from(spans));
|
||||
}
|
||||
|
||||
if self.items.len() > self.visible_count {
|
||||
let position = self.cursor + 1;
|
||||
let total = self.items.len();
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!(" ({position}/{total})"),
|
||||
indicator_style,
|
||||
)));
|
||||
}
|
||||
|
||||
frame.render_widget(Paragraph::new(lines), area);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
use crate::theme;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Color, Style};
|
||||
use ratatui::widgets::{Block, Borders, Clear};
|
||||
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
|
||||
use ratatui::Frame;
|
||||
|
||||
pub struct ModalFrame<'a> {
|
||||
title: &'a str,
|
||||
width: u16,
|
||||
height: u16,
|
||||
border_color: Color,
|
||||
border_color: Option<Color>,
|
||||
}
|
||||
|
||||
impl<'a> ModalFrame<'a> {
|
||||
@@ -16,7 +17,7 @@ impl<'a> ModalFrame<'a> {
|
||||
title,
|
||||
width: 40,
|
||||
height: 5,
|
||||
border_color: Color::White,
|
||||
border_color: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,11 +32,12 @@ impl<'a> ModalFrame<'a> {
|
||||
}
|
||||
|
||||
pub fn border_color(mut self, c: Color) -> Self {
|
||||
self.border_color = c;
|
||||
self.border_color = Some(c);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn render_centered(&self, frame: &mut Frame, term: Rect) -> Rect {
|
||||
let t = theme::get();
|
||||
let width = self.width.min(term.width.saturating_sub(4));
|
||||
let height = self.height.min(term.height.saturating_sub(4));
|
||||
|
||||
@@ -45,10 +47,21 @@ impl<'a> ModalFrame<'a> {
|
||||
|
||||
frame.render_widget(Clear, area);
|
||||
|
||||
// Fill background with theme color
|
||||
let bg_fill = " ".repeat(area.width as usize);
|
||||
for row in 0..area.height {
|
||||
let line_area = Rect::new(area.x, area.y + row, area.width, 1);
|
||||
frame.render_widget(
|
||||
Paragraph::new(bg_fill.clone()).style(Style::new().bg(t.ui.bg)),
|
||||
line_area,
|
||||
);
|
||||
}
|
||||
|
||||
let border_color = self.border_color.unwrap_or(t.ui.text_primary);
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(self.title)
|
||||
.border_style(Style::new().fg(self.border_color));
|
||||
.border_style(Style::new().fg(border_color));
|
||||
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
117
crates/ratatui/src/nav_minimap.rs
Normal file
117
crates/ratatui/src/nav_minimap.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
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,
|
||||
pub row: i8,
|
||||
pub name: &'static str,
|
||||
}
|
||||
|
||||
/// Navigation minimap widget that renders a grid of page tiles
|
||||
pub struct NavMinimap<'a> {
|
||||
tiles: &'a [NavTile],
|
||||
selected: (i8, i8),
|
||||
}
|
||||
|
||||
impl<'a> NavMinimap<'a> {
|
||||
pub fn new(tiles: &'a [NavTile], selected: (i8, i8)) -> Self {
|
||||
Self { tiles, selected }
|
||||
}
|
||||
|
||||
pub fn render_centered(self, frame: &mut Frame, term: Rect) {
|
||||
if self.tiles.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let area = minimap_area(term);
|
||||
|
||||
frame.render_widget(Clear, area);
|
||||
|
||||
// Fill background with theme color
|
||||
let t = theme::get();
|
||||
let bg_fill = " ".repeat(area.width as usize);
|
||||
for row in 0..area.height {
|
||||
let line_area = Rect::new(area.x, area.y + row, area.width, 1);
|
||||
frame.render_widget(
|
||||
Paragraph::new(bg_fill.clone()).style(Style::new().bg(t.ui.bg)),
|
||||
line_area,
|
||||
);
|
||||
}
|
||||
|
||||
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 is_selected = (tile.col, tile.row) == self.selected;
|
||||
self.render_tile(frame, tile_area, tile.name, is_selected);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_tile(&self, frame: &mut Frame, area: Rect, label: &str, is_selected: bool) {
|
||||
let t = theme::get();
|
||||
let (bg, fg) = if is_selected {
|
||||
(t.nav.selected_bg, t.nav.selected_fg)
|
||||
} else {
|
||||
(t.nav.unselected_bg, t.nav.unselected_fg)
|
||||
};
|
||||
|
||||
// Fill background
|
||||
for row in 0..area.height {
|
||||
let line_area = Rect::new(area.x, area.y + row, area.width, 1);
|
||||
let fill = " ".repeat(area.width as usize);
|
||||
frame.render_widget(Paragraph::new(fill).style(Style::new().bg(bg)), line_area);
|
||||
}
|
||||
|
||||
// Center text vertically
|
||||
let text_y = area.y + area.height / 2;
|
||||
let text_area = Rect::new(area.x, text_y, area.width, 1);
|
||||
let paragraph = Paragraph::new(label)
|
||||
.style(Style::new().bg(bg).fg(fg))
|
||||
.alignment(Alignment::Center);
|
||||
|
||||
frame.render_widget(paragraph, text_area);
|
||||
}
|
||||
}
|
||||
42
crates/ratatui/src/props_form.rs
Normal file
42
crates/ratatui/src/props_form.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::theme;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
177
crates/ratatui/src/sample_browser.rs
Normal file
177
crates/ratatui/src/sample_browser.rs
Normal file
@@ -0,0 +1,177 @@
|
||||
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::Frame;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum TreeLineKind {
|
||||
Root { expanded: bool },
|
||||
Folder { expanded: bool },
|
||||
File,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TreeLine {
|
||||
pub depth: u8,
|
||||
pub kind: TreeLineKind,
|
||||
pub label: String,
|
||||
pub folder: String,
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
pub struct SampleBrowser<'a> {
|
||||
entries: &'a [TreeLine],
|
||||
cursor: usize,
|
||||
scroll_offset: usize,
|
||||
search_query: &'a str,
|
||||
search_active: bool,
|
||||
focused: bool,
|
||||
}
|
||||
|
||||
impl<'a> SampleBrowser<'a> {
|
||||
pub fn new(entries: &'a [TreeLine], cursor: usize) -> Self {
|
||||
Self {
|
||||
entries,
|
||||
cursor,
|
||||
scroll_offset: 0,
|
||||
search_query: "",
|
||||
search_active: false,
|
||||
focused: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scroll_offset(mut self, offset: usize) -> Self {
|
||||
self.scroll_offset = offset;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn search(mut self, query: &'a str, active: bool) -> Self {
|
||||
self.search_query = query;
|
||||
self.search_active = active;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn focused(mut self, focused: bool) -> Self {
|
||||
self.focused = focused;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn render(self, frame: &mut Frame, area: Rect) {
|
||||
let colors = theme::get();
|
||||
let border_style = if self.focused {
|
||||
Style::new().fg(colors.browser.focused_border)
|
||||
} else {
|
||||
Style::new().fg(colors.browser.unfocused_border)
|
||||
};
|
||||
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(border_style)
|
||||
.title(" Samples ");
|
||||
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
|
||||
if inner.height == 0 || inner.width == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let show_search = self.search_active || !self.search_query.is_empty();
|
||||
let (search_area, list_area) = if show_search {
|
||||
let [s, l] = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Fill(1),
|
||||
])
|
||||
.areas(inner);
|
||||
(Some(s), l)
|
||||
} else {
|
||||
(None, inner)
|
||||
};
|
||||
|
||||
if let Some(sa) = search_area {
|
||||
self.render_search(frame, sa, &colors);
|
||||
}
|
||||
self.render_tree(frame, list_area, &colors);
|
||||
}
|
||||
|
||||
fn render_search(&self, frame: &mut Frame, area: Rect, colors: &theme::ThemeColors) {
|
||||
let style = if self.search_active {
|
||||
Style::new().fg(colors.search.active)
|
||||
} else {
|
||||
Style::new().fg(colors.search.inactive)
|
||||
};
|
||||
let cursor = if self.search_active { "_" } else { "" };
|
||||
let text = format!("/{}{}", self.search_query, cursor);
|
||||
let line = Line::from(Span::styled(text, style));
|
||||
frame.render_widget(Paragraph::new(vec![line]), area);
|
||||
}
|
||||
|
||||
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"
|
||||
} else {
|
||||
"No matches"
|
||||
};
|
||||
let line = Line::from(Span::styled(msg, Style::new().fg(colors.browser.empty_text)));
|
||||
frame.render_widget(Paragraph::new(vec![line]), area);
|
||||
return;
|
||||
}
|
||||
|
||||
let visible_end = (self.scroll_offset + height).min(self.entries.len());
|
||||
let mut lines: Vec<Line> = Vec::with_capacity(height);
|
||||
|
||||
for i in self.scroll_offset..visible_end {
|
||||
let entry = &self.entries[i];
|
||||
let is_cursor = i == self.cursor;
|
||||
let indent = " ".repeat(entry.depth as usize);
|
||||
|
||||
let (icon, icon_color) = match entry.kind {
|
||||
TreeLineKind::Root { expanded: true } | TreeLineKind::Folder { expanded: true } => {
|
||||
("\u{25BC} ", colors.browser.folder_icon)
|
||||
}
|
||||
TreeLineKind::Root { expanded: false }
|
||||
| TreeLineKind::Folder { expanded: false } => ("\u{25B6} ", colors.browser.folder_icon),
|
||||
TreeLineKind::File => ("\u{266A} ", colors.browser.file_icon),
|
||||
};
|
||||
|
||||
let label_style = if is_cursor && self.focused {
|
||||
Style::new().fg(colors.browser.selected).add_modifier(Modifier::BOLD)
|
||||
} else if is_cursor {
|
||||
Style::new().fg(colors.browser.file)
|
||||
} else {
|
||||
match entry.kind {
|
||||
TreeLineKind::Root { .. } => {
|
||||
Style::new().fg(colors.browser.root).add_modifier(Modifier::BOLD)
|
||||
}
|
||||
TreeLineKind::Folder { .. } => Style::new().fg(colors.browser.directory),
|
||||
TreeLineKind::File => Style::default(),
|
||||
}
|
||||
};
|
||||
|
||||
let icon_style = if is_cursor && self.focused {
|
||||
label_style
|
||||
} else {
|
||||
Style::new().fg(icon_color)
|
||||
};
|
||||
|
||||
let mut spans = vec![
|
||||
Span::raw(indent),
|
||||
Span::styled(icon, icon_style),
|
||||
Span::styled(&entry.label, label_style),
|
||||
];
|
||||
|
||||
if matches!(entry.kind, TreeLineKind::File) {
|
||||
let idx_style = Style::new().fg(colors.browser.empty_text);
|
||||
spans.push(Span::styled(format!(" {}", entry.index), idx_style));
|
||||
}
|
||||
|
||||
lines.push(Line::from(spans));
|
||||
}
|
||||
|
||||
frame.render_widget(Paragraph::new(lines), area);
|
||||
}
|
||||
}
|
||||
173
crates/ratatui/src/scope.rs
Normal file
173
crates/ratatui/src/scope.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
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()) };
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Orientation {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
pub struct Scope<'a> {
|
||||
data: &'a [f32],
|
||||
orientation: Orientation,
|
||||
color: Option<Color>,
|
||||
gain: f32,
|
||||
}
|
||||
|
||||
impl<'a> Scope<'a> {
|
||||
pub fn new(data: &'a [f32]) -> Self {
|
||||
Self {
|
||||
data,
|
||||
orientation: Orientation::Horizontal,
|
||||
color: None,
|
||||
gain: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn orientation(mut self, o: Orientation) -> Self {
|
||||
self.orientation = o;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn color(mut self, c: Color) -> Self {
|
||||
self.color = Some(c);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Scope<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
if area.width == 0 || area.height == 0 || self.data.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let color = self.color.unwrap_or_else(|| theme::get().meter.low);
|
||||
|
||||
match self.orientation {
|
||||
Orientation::Horizontal => {
|
||||
render_horizontal(self.data, area, buf, color, self.gain)
|
||||
}
|
||||
Orientation::Vertical => render_vertical(self.data, area, buf, color, self.gain),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_horizontal(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gain: f32) {
|
||||
let width = area.width as usize;
|
||||
let height = area.height as usize;
|
||||
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;
|
||||
patterns.clear();
|
||||
patterns.resize(size, 0);
|
||||
|
||||
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 fine_y = ((1.0 - sample) * 0.5 * (fine_height - 1) as f32).round() as usize;
|
||||
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;
|
||||
|
||||
let bit = 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!(),
|
||||
};
|
||||
|
||||
patterns[char_y * width + char_x] |= bit;
|
||||
}
|
||||
|
||||
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_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gain: f32) {
|
||||
let width = area.width as usize;
|
||||
let height = area.height as usize;
|
||||
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;
|
||||
patterns.clear();
|
||||
patterns.resize(size, 0);
|
||||
|
||||
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 fine_x = ((sample + 1.0) * 0.5 * (fine_width - 1) as f32).round() as usize;
|
||||
let fine_x = fine_x.min(fine_width - 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;
|
||||
|
||||
let bit = 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!(),
|
||||
};
|
||||
|
||||
patterns[char_y * width + char_x] |= bit;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
53
crates/ratatui/src/scroll_indicators.rs
Normal file
53
crates/ratatui/src/scroll_indicators.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Color, Style};
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::Frame;
|
||||
|
||||
pub enum IndicatorAlign {
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
crates/ratatui/src/search_bar.rs
Normal file
20
crates/ratatui/src/search_bar.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Style;
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::theme;
|
||||
|
||||
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);
|
||||
}
|
||||
30
crates/ratatui/src/section_header.rs
Normal file
30
crates/ratatui/src/section_header.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::theme;
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
60
crates/ratatui/src/sparkles.rs
Normal file
60
crates/ratatui/src/sparkles.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use crate::theme;
|
||||
use rand::Rng;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Color, Style};
|
||||
use ratatui::widgets::Widget;
|
||||
|
||||
const CHARS: &[char] = &['·', '✦', '✧', '°', '•', '+', '⋆', '*'];
|
||||
|
||||
struct Sparkle {
|
||||
x: u16,
|
||||
y: u16,
|
||||
char_idx: usize,
|
||||
life: u8,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Sparkles {
|
||||
sparkles: Vec<Sparkle>,
|
||||
}
|
||||
|
||||
impl Sparkles {
|
||||
pub fn tick(&mut self, area: Rect) {
|
||||
let mut rng = rand::thread_rng();
|
||||
for _ in 0..3 {
|
||||
if rng.gen_bool(0.6) {
|
||||
self.sparkles.push(Sparkle {
|
||||
x: rng.gen_range(0..area.width),
|
||||
y: rng.gen_range(0..area.height),
|
||||
char_idx: rng.gen_range(0..CHARS.len()),
|
||||
life: rng.gen_range(15..40),
|
||||
});
|
||||
}
|
||||
}
|
||||
self.sparkles
|
||||
.iter_mut()
|
||||
.for_each(|s| s.life = s.life.saturating_sub(1));
|
||||
self.sparkles.retain(|s| s.life > 0);
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &Sparkles {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let colors = theme::get().sparkle.colors;
|
||||
for sp in &self.sparkles {
|
||||
let color = colors[sp.char_idx % colors.len()];
|
||||
let intensity = (sp.life as f32 / 30.0).min(1.0);
|
||||
let r = (color.0 as f32 * intensity) as u8;
|
||||
let g = (color.1 as f32 * intensity) as u8;
|
||||
let b = (color.2 as f32 * intensity) as u8;
|
||||
|
||||
if sp.x < area.width && sp.y < area.height {
|
||||
let x = area.x + sp.x;
|
||||
let y = area.y + sp.y;
|
||||
let ch = CHARS[sp.char_idx];
|
||||
buf[(x, y)].set_char(ch).set_style(Style::new().fg(Color::Rgb(r, g, b)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
66
crates/ratatui/src/spectrum.rs
Normal file
66
crates/ratatui/src/spectrum.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use crate::theme;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Color;
|
||||
use ratatui::widgets::Widget;
|
||||
|
||||
const BLOCKS: [char; 8] = ['\u{2581}', '\u{2582}', '\u{2583}', '\u{2584}', '\u{2585}', '\u{2586}', '\u{2587}', '\u{2588}'];
|
||||
|
||||
pub struct Spectrum<'a> {
|
||||
data: &'a [f32; 32],
|
||||
}
|
||||
|
||||
impl<'a> Spectrum<'a> {
|
||||
pub fn new(data: &'a [f32; 32]) -> Self {
|
||||
Self { data }
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Spectrum<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
if area.width == 0 || area.height == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
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 self.data.iter().enumerate() {
|
||||
let w = base + if band < remainder { 1 } else { 0 };
|
||||
if w == 0 {
|
||||
continue;
|
||||
}
|
||||
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;
|
||||
|
||||
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 {
|
||||
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..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);
|
||||
}
|
||||
}
|
||||
}
|
||||
x_start += w as u16;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::theme;
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::{Color, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
@@ -10,7 +11,7 @@ pub struct TextInputModal<'a> {
|
||||
title: &'a str,
|
||||
input: &'a str,
|
||||
hint: Option<&'a str>,
|
||||
border_color: Color,
|
||||
border_color: Option<Color>,
|
||||
width: u16,
|
||||
}
|
||||
|
||||
@@ -20,7 +21,7 @@ impl<'a> TextInputModal<'a> {
|
||||
title,
|
||||
input,
|
||||
hint: None,
|
||||
border_color: Color::White,
|
||||
border_color: None,
|
||||
width: 50,
|
||||
}
|
||||
}
|
||||
@@ -31,7 +32,7 @@ impl<'a> TextInputModal<'a> {
|
||||
}
|
||||
|
||||
pub fn border_color(mut self, c: Color) -> Self {
|
||||
self.border_color = c;
|
||||
self.border_color = Some(c);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -40,13 +41,15 @@ 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 };
|
||||
|
||||
let inner = ModalFrame::new(self.title)
|
||||
.width(self.width)
|
||||
.height(height)
|
||||
.border_color(self.border_color)
|
||||
.border_color(border_color)
|
||||
.render_centered(frame, term);
|
||||
|
||||
if self.hint.is_some() {
|
||||
@@ -56,15 +59,15 @@ impl<'a> TextInputModal<'a> {
|
||||
frame.render_widget(
|
||||
Paragraph::new(Line::from(vec![
|
||||
Span::raw("> "),
|
||||
Span::styled(self.input, Style::new().fg(Color::Cyan)),
|
||||
Span::styled("█", Style::new().fg(Color::White)),
|
||||
Span::styled(self.input, Style::new().fg(colors.input.text)),
|
||||
Span::styled("█", Style::new().fg(colors.input.cursor)),
|
||||
])),
|
||||
rows[0],
|
||||
);
|
||||
|
||||
if let Some(hint) = self.hint {
|
||||
frame.render_widget(
|
||||
Paragraph::new(Span::styled(hint, Style::new().fg(Color::DarkGray))),
|
||||
Paragraph::new(Span::styled(hint, Style::new().fg(colors.input.hint))),
|
||||
rows[1],
|
||||
);
|
||||
}
|
||||
@@ -72,11 +75,13 @@ impl<'a> TextInputModal<'a> {
|
||||
frame.render_widget(
|
||||
Paragraph::new(Line::from(vec![
|
||||
Span::raw("> "),
|
||||
Span::styled(self.input, Style::new().fg(Color::Cyan)),
|
||||
Span::styled("█", Style::new().fg(Color::White)),
|
||||
Span::styled(self.input, Style::new().fg(colors.input.text)),
|
||||
Span::styled("█", Style::new().fg(colors.input.cursor)),
|
||||
])),
|
||||
inner,
|
||||
);
|
||||
}
|
||||
|
||||
inner
|
||||
}
|
||||
}
|
||||
287
crates/ratatui/src/theme/catppuccin_latte.rs
Normal file
287
crates/ratatui/src/theme/catppuccin_latte.rs
Normal file
@@ -0,0 +1,287 @@
|
||||
use super::*;
|
||||
use ratatui::style::Color;
|
||||
|
||||
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);
|
||||
|
||||
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,
|
||||
content_bg: Color::Rgb(185, 225, 225),
|
||||
inactive_bg: surface0,
|
||||
inactive_fg: subtext0,
|
||||
active_selected_bg: Color::Rgb(215, 210, 240),
|
||||
active_in_range_bg: Color::Rgb(210, 215, 230),
|
||||
link_bright: [
|
||||
(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),
|
||||
],
|
||||
},
|
||||
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,
|
||||
},
|
||||
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,
|
||||
muted_bg: Color::Rgb(215, 215, 225),
|
||||
muted_fg: overlay0,
|
||||
soloed_bg: Color::Rgb(250, 235, 200),
|
||||
soloed_fg: yellow,
|
||||
},
|
||||
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)),
|
||||
user_defined: (maroon, Color::Rgb(245, 225, 230)),
|
||||
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),
|
||||
],
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: peach,
|
||||
button_selected_bg: peach,
|
||||
button_selected_fg: base,
|
||||
},
|
||||
}
|
||||
}
|
||||
290
crates/ratatui/src/theme/catppuccin_mocha.rs
Normal file
290
crates/ratatui/src/theme/catppuccin_mocha.rs
Normal file
@@ -0,0 +1,290 @@
|
||||
use super::*;
|
||||
use ratatui::style::Color;
|
||||
|
||||
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);
|
||||
|
||||
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,
|
||||
content_bg: Color::Rgb(47, 62, 62),
|
||||
inactive_bg: surface0,
|
||||
inactive_fg: subtext0,
|
||||
active_selected_bg: Color::Rgb(70, 60, 80),
|
||||
active_in_range_bg: Color::Rgb(55, 55, 70),
|
||||
link_bright: [
|
||||
(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),
|
||||
],
|
||||
},
|
||||
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,
|
||||
},
|
||||
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,
|
||||
muted_bg: Color::Rgb(40, 40, 50),
|
||||
muted_fg: overlay0,
|
||||
soloed_bg: Color::Rgb(60, 55, 35),
|
||||
soloed_fg: yellow,
|
||||
},
|
||||
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)),
|
||||
user_defined: (maroon, Color::Rgb(55, 35, 40)),
|
||||
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),
|
||||
],
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: peach,
|
||||
button_selected_bg: peach,
|
||||
button_selected_fg: crust,
|
||||
},
|
||||
}
|
||||
}
|
||||
284
crates/ratatui/src/theme/dracula.rs
Normal file
284
crates/ratatui/src/theme/dracula.rs
Normal file
@@ -0,0 +1,284 @@
|
||||
use super::*;
|
||||
use ratatui::style::Color;
|
||||
|
||||
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);
|
||||
|
||||
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,
|
||||
content_bg: Color::Rgb(57, 77, 77),
|
||||
inactive_bg: current_line,
|
||||
inactive_fg: comment,
|
||||
active_selected_bg: Color::Rgb(80, 70, 95),
|
||||
active_in_range_bg: Color::Rgb(65, 65, 85),
|
||||
link_bright: [
|
||||
(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),
|
||||
],
|
||||
},
|
||||
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,
|
||||
},
|
||||
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,
|
||||
muted_bg: Color::Rgb(50, 52, 65),
|
||||
muted_fg: comment,
|
||||
soloed_bg: Color::Rgb(70, 70, 50),
|
||||
soloed_fg: yellow,
|
||||
},
|
||||
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)),
|
||||
user_defined: (orange, Color::Rgb(65, 55, 40)),
|
||||
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),
|
||||
],
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: orange,
|
||||
button_selected_bg: orange,
|
||||
button_selected_fg: background,
|
||||
},
|
||||
}
|
||||
}
|
||||
284
crates/ratatui/src/theme/eden.rs
Normal file
284
crates/ratatui/src/theme/eden.rs
Normal file
@@ -0,0 +1,284 @@
|
||||
use super::*;
|
||||
use ratatui::style::Color;
|
||||
|
||||
pub fn theme() -> ThemeColors {
|
||||
let bg = Color::Rgb(8, 12, 8);
|
||||
let surface = Color::Rgb(16, 24, 16);
|
||||
let surface2 = Color::Rgb(24, 32, 24);
|
||||
let border = Color::Rgb(32, 48, 32);
|
||||
let fg = Color::Rgb(200, 216, 192);
|
||||
let fg_dim = Color::Rgb(122, 144, 112);
|
||||
let fg_muted = Color::Rgb(64, 88, 56);
|
||||
|
||||
let green = Color::Rgb(64, 192, 64);
|
||||
let bright_green = Color::Rgb(96, 224, 96);
|
||||
let dark_green = Color::Rgb(48, 128, 48);
|
||||
let cyan = Color::Rgb(80, 168, 144);
|
||||
let yellow = Color::Rgb(160, 160, 64);
|
||||
let red = Color::Rgb(192, 80, 64);
|
||||
let orange = Color::Rgb(176, 128, 48);
|
||||
let blue = Color::Rgb(80, 128, 160);
|
||||
let purple = Color::Rgb(128, 104, 144);
|
||||
|
||||
ThemeColors {
|
||||
ui: UiColors {
|
||||
bg,
|
||||
bg_rgb: (8, 12, 8),
|
||||
text_primary: fg,
|
||||
text_muted: fg_dim,
|
||||
text_dim: fg_muted,
|
||||
border,
|
||||
header: green,
|
||||
unfocused: fg_muted,
|
||||
accent: green,
|
||||
surface,
|
||||
},
|
||||
status: StatusColors {
|
||||
playing_bg: Color::Rgb(14, 30, 14),
|
||||
playing_fg: bright_green,
|
||||
stopped_bg: Color::Rgb(36, 16, 14),
|
||||
stopped_fg: red,
|
||||
fill_on: green,
|
||||
fill_off: dark_green,
|
||||
fill_bg: surface,
|
||||
},
|
||||
selection: SelectionColors {
|
||||
cursor_bg: green,
|
||||
cursor_fg: bg,
|
||||
selected_bg: Color::Rgb(20, 40, 20),
|
||||
selected_fg: bright_green,
|
||||
in_range_bg: Color::Rgb(16, 30, 16),
|
||||
in_range_fg: fg,
|
||||
cursor: green,
|
||||
selected: Color::Rgb(20, 40, 20),
|
||||
in_range: Color::Rgb(16, 30, 16),
|
||||
},
|
||||
tile: TileColors {
|
||||
playing_active_bg: Color::Rgb(16, 38, 16),
|
||||
playing_active_fg: bright_green,
|
||||
playing_inactive_bg: Color::Rgb(28, 36, 14),
|
||||
playing_inactive_fg: yellow,
|
||||
active_bg: Color::Rgb(14, 28, 26),
|
||||
active_fg: cyan,
|
||||
content_bg: Color::Rgb(21, 35, 33),
|
||||
inactive_bg: surface,
|
||||
inactive_fg: fg_dim,
|
||||
active_selected_bg: Color::Rgb(22, 36, 28),
|
||||
active_in_range_bg: Color::Rgb(16, 28, 20),
|
||||
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),
|
||||
],
|
||||
},
|
||||
header: HeaderColors {
|
||||
tempo_bg: Color::Rgb(26, 22, 30),
|
||||
tempo_fg: purple,
|
||||
bank_bg: Color::Rgb(16, 26, 32),
|
||||
bank_fg: blue,
|
||||
pattern_bg: Color::Rgb(14, 28, 26),
|
||||
pattern_fg: cyan,
|
||||
stats_bg: surface,
|
||||
stats_fg: fg_dim,
|
||||
},
|
||||
modal: ModalColors {
|
||||
border: green,
|
||||
border_accent: bright_green,
|
||||
border_warn: yellow,
|
||||
border_dim: fg_muted,
|
||||
confirm: red,
|
||||
rename: green,
|
||||
input: cyan,
|
||||
editor: green,
|
||||
preview: fg_muted,
|
||||
},
|
||||
flash: FlashColors {
|
||||
error_bg: Color::Rgb(40, 16, 14),
|
||||
error_fg: red,
|
||||
success_bg: Color::Rgb(14, 32, 14),
|
||||
success_fg: bright_green,
|
||||
info_bg: surface,
|
||||
info_fg: fg,
|
||||
},
|
||||
list: ListColors {
|
||||
playing_bg: Color::Rgb(14, 32, 14),
|
||||
playing_fg: bright_green,
|
||||
staged_play_bg: Color::Rgb(26, 22, 30),
|
||||
staged_play_fg: purple,
|
||||
staged_stop_bg: Color::Rgb(40, 16, 14),
|
||||
staged_stop_fg: red,
|
||||
edit_bg: Color::Rgb(14, 28, 26),
|
||||
edit_fg: cyan,
|
||||
hover_bg: surface2,
|
||||
hover_fg: fg,
|
||||
muted_bg: Color::Rgb(10, 14, 10),
|
||||
muted_fg: fg_muted,
|
||||
soloed_bg: Color::Rgb(30, 30, 14),
|
||||
soloed_fg: yellow,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: red,
|
||||
connected: bright_green,
|
||||
listening: yellow,
|
||||
},
|
||||
syntax: SyntaxColors {
|
||||
gap_bg: bg,
|
||||
executed_bg: Color::Rgb(14, 24, 14),
|
||||
selected_bg: Color::Rgb(20, 40, 16),
|
||||
emit: (fg, Color::Rgb(20, 34, 20)),
|
||||
number: (yellow, Color::Rgb(30, 30, 12)),
|
||||
string: (bright_green, Color::Rgb(16, 32, 16)),
|
||||
comment: (fg_muted, bg),
|
||||
keyword: (red, Color::Rgb(38, 18, 14)),
|
||||
stack_op: (blue, Color::Rgb(16, 26, 32)),
|
||||
operator: (green, Color::Rgb(14, 36, 14)),
|
||||
sound: (cyan, Color::Rgb(16, 30, 28)),
|
||||
param: (purple, Color::Rgb(26, 22, 28)),
|
||||
context: (orange, Color::Rgb(34, 26, 12)),
|
||||
note: (bright_green, Color::Rgb(16, 34, 16)),
|
||||
interval: (Color::Rgb(100, 180, 100), Color::Rgb(18, 34, 18)),
|
||||
variable: (purple, Color::Rgb(26, 22, 28)),
|
||||
vary: (yellow, Color::Rgb(30, 30, 12)),
|
||||
generator: (cyan, Color::Rgb(16, 30, 26)),
|
||||
user_defined: (orange, Color::Rgb(30, 22, 10)),
|
||||
default: (fg_dim, bg),
|
||||
},
|
||||
table: TableColors {
|
||||
row_even: bg,
|
||||
row_odd: surface,
|
||||
},
|
||||
values: ValuesColors {
|
||||
tempo: green,
|
||||
value: fg_dim,
|
||||
},
|
||||
hint: HintColors {
|
||||
key: green,
|
||||
text: fg_muted,
|
||||
},
|
||||
view_badge: ViewBadgeColors { bg: fg, fg: bg },
|
||||
nav: NavColors {
|
||||
selected_bg: Color::Rgb(18, 38, 18),
|
||||
selected_fg: fg,
|
||||
unselected_bg: surface,
|
||||
unselected_fg: fg_muted,
|
||||
},
|
||||
editor_widget: EditorWidgetColors {
|
||||
cursor_bg: fg,
|
||||
cursor_fg: bg,
|
||||
selection_bg: Color::Rgb(24, 48, 24),
|
||||
completion_bg: surface,
|
||||
completion_fg: fg,
|
||||
completion_selected: green,
|
||||
completion_example: cyan,
|
||||
},
|
||||
browser: BrowserColors {
|
||||
directory: blue,
|
||||
project_file: green,
|
||||
selected: bright_green,
|
||||
file: fg,
|
||||
focused_border: green,
|
||||
unfocused_border: fg_muted,
|
||||
root: fg,
|
||||
file_icon: fg_muted,
|
||||
folder_icon: blue,
|
||||
empty_text: fg_muted,
|
||||
},
|
||||
input: InputColors {
|
||||
text: green,
|
||||
cursor: fg,
|
||||
hint: fg_muted,
|
||||
},
|
||||
search: SearchColors {
|
||||
active: green,
|
||||
inactive: fg_muted,
|
||||
match_bg: yellow,
|
||||
match_fg: bg,
|
||||
},
|
||||
markdown: MarkdownColors {
|
||||
h1: green,
|
||||
h2: cyan,
|
||||
h3: purple,
|
||||
code: bright_green,
|
||||
code_border: Color::Rgb(28, 42, 28),
|
||||
link: cyan,
|
||||
link_url: Color::Rgb(56, 72, 52),
|
||||
quote: fg_muted,
|
||||
text: fg,
|
||||
list: fg,
|
||||
},
|
||||
engine: EngineColors {
|
||||
header: green,
|
||||
header_focused: bright_green,
|
||||
divider: Color::Rgb(24, 36, 24),
|
||||
scroll_indicator: Color::Rgb(36, 52, 36),
|
||||
label: Color::Rgb(100, 120, 92),
|
||||
label_focused: Color::Rgb(140, 160, 130),
|
||||
label_dim: Color::Rgb(68, 84, 60),
|
||||
value: Color::Rgb(180, 196, 172),
|
||||
focused: bright_green,
|
||||
normal: fg,
|
||||
dim: Color::Rgb(36, 52, 36),
|
||||
path: Color::Rgb(100, 120, 92),
|
||||
border_magenta: purple,
|
||||
border_green: green,
|
||||
border_cyan: cyan,
|
||||
separator: Color::Rgb(24, 36, 24),
|
||||
hint_active: green,
|
||||
hint_inactive: Color::Rgb(24, 36, 24),
|
||||
},
|
||||
dict: DictColors {
|
||||
word_name: bright_green,
|
||||
word_bg: Color::Rgb(14, 24, 14),
|
||||
alias: fg_muted,
|
||||
stack_sig: purple,
|
||||
description: fg,
|
||||
example: Color::Rgb(100, 120, 92),
|
||||
category_focused: bright_green,
|
||||
category_selected: green,
|
||||
category_normal: fg,
|
||||
category_dimmed: Color::Rgb(36, 52, 36),
|
||||
border_focused: bright_green,
|
||||
border_normal: Color::Rgb(24, 36, 24),
|
||||
header_desc: Color::Rgb(120, 140, 112),
|
||||
},
|
||||
title: TitleColors {
|
||||
big_title: green,
|
||||
author: cyan,
|
||||
link: bright_green,
|
||||
license: yellow,
|
||||
prompt: Color::Rgb(100, 120, 92),
|
||||
subtitle: fg,
|
||||
},
|
||||
meter: MeterColors {
|
||||
low: green,
|
||||
mid: yellow,
|
||||
high: red,
|
||||
low_rgb: (64, 192, 64),
|
||||
mid_rgb: (160, 160, 64),
|
||||
high_rgb: (192, 80, 64),
|
||||
},
|
||||
sparkle: SparkleColors {
|
||||
colors: [
|
||||
(64, 192, 64),
|
||||
(96, 224, 96),
|
||||
(80, 168, 144),
|
||||
(160, 160, 64),
|
||||
(48, 128, 48),
|
||||
],
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: red,
|
||||
button_selected_bg: green,
|
||||
button_selected_fg: bg,
|
||||
},
|
||||
}
|
||||
}
|
||||
282
crates/ratatui/src/theme/ember.rs
Normal file
282
crates/ratatui/src/theme/ember.rs
Normal file
@@ -0,0 +1,282 @@
|
||||
use super::*;
|
||||
use ratatui::style::Color;
|
||||
|
||||
pub fn theme() -> ThemeColors {
|
||||
let bg = Color::Rgb(10, 8, 8);
|
||||
let surface = Color::Rgb(20, 16, 16);
|
||||
let surface2 = Color::Rgb(26, 20, 20);
|
||||
let border = Color::Rgb(42, 32, 32);
|
||||
let fg = Color::Rgb(232, 221, 208);
|
||||
let fg_dim = Color::Rgb(160, 144, 128);
|
||||
let fg_muted = Color::Rgb(96, 80, 64);
|
||||
|
||||
let red = Color::Rgb(224, 80, 64);
|
||||
let orange = Color::Rgb(224, 128, 48);
|
||||
let yellow = Color::Rgb(208, 160, 48);
|
||||
let green = Color::Rgb(128, 160, 80);
|
||||
let cyan = Color::Rgb(112, 160, 160);
|
||||
let purple = Color::Rgb(160, 112, 144);
|
||||
let blue = Color::Rgb(96, 128, 176);
|
||||
|
||||
ThemeColors {
|
||||
ui: UiColors {
|
||||
bg,
|
||||
bg_rgb: (10, 8, 8),
|
||||
text_primary: fg,
|
||||
text_muted: fg_dim,
|
||||
text_dim: fg_muted,
|
||||
border,
|
||||
header: orange,
|
||||
unfocused: fg_muted,
|
||||
accent: orange,
|
||||
surface,
|
||||
},
|
||||
status: StatusColors {
|
||||
playing_bg: Color::Rgb(20, 28, 16),
|
||||
playing_fg: green,
|
||||
stopped_bg: Color::Rgb(40, 16, 14),
|
||||
stopped_fg: red,
|
||||
fill_on: orange,
|
||||
fill_off: fg_muted,
|
||||
fill_bg: surface,
|
||||
},
|
||||
selection: SelectionColors {
|
||||
cursor_bg: orange,
|
||||
cursor_fg: bg,
|
||||
selected_bg: Color::Rgb(50, 35, 20),
|
||||
selected_fg: orange,
|
||||
in_range_bg: Color::Rgb(35, 25, 18),
|
||||
in_range_fg: fg,
|
||||
cursor: orange,
|
||||
selected: Color::Rgb(50, 35, 20),
|
||||
in_range: Color::Rgb(35, 25, 18),
|
||||
},
|
||||
tile: TileColors {
|
||||
playing_active_bg: Color::Rgb(45, 25, 15),
|
||||
playing_active_fg: orange,
|
||||
playing_inactive_bg: Color::Rgb(40, 32, 12),
|
||||
playing_inactive_fg: yellow,
|
||||
active_bg: Color::Rgb(20, 30, 30),
|
||||
active_fg: cyan,
|
||||
content_bg: Color::Rgb(27, 37, 37),
|
||||
inactive_bg: surface,
|
||||
inactive_fg: fg_dim,
|
||||
active_selected_bg: Color::Rgb(40, 30, 35),
|
||||
active_in_range_bg: Color::Rgb(30, 25, 25),
|
||||
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),
|
||||
],
|
||||
},
|
||||
header: HeaderColors {
|
||||
tempo_bg: Color::Rgb(40, 28, 30),
|
||||
tempo_fg: purple,
|
||||
bank_bg: Color::Rgb(22, 30, 40),
|
||||
bank_fg: blue,
|
||||
pattern_bg: Color::Rgb(22, 34, 34),
|
||||
pattern_fg: cyan,
|
||||
stats_bg: surface,
|
||||
stats_fg: fg_dim,
|
||||
},
|
||||
modal: ModalColors {
|
||||
border: orange,
|
||||
border_accent: red,
|
||||
border_warn: yellow,
|
||||
border_dim: fg_muted,
|
||||
confirm: red,
|
||||
rename: orange,
|
||||
input: cyan,
|
||||
editor: orange,
|
||||
preview: fg_muted,
|
||||
},
|
||||
flash: FlashColors {
|
||||
error_bg: Color::Rgb(50, 16, 14),
|
||||
error_fg: red,
|
||||
success_bg: Color::Rgb(20, 35, 18),
|
||||
success_fg: green,
|
||||
info_bg: surface,
|
||||
info_fg: fg,
|
||||
},
|
||||
list: ListColors {
|
||||
playing_bg: Color::Rgb(20, 35, 18),
|
||||
playing_fg: green,
|
||||
staged_play_bg: Color::Rgb(35, 25, 30),
|
||||
staged_play_fg: purple,
|
||||
staged_stop_bg: Color::Rgb(45, 18, 16),
|
||||
staged_stop_fg: red,
|
||||
edit_bg: Color::Rgb(20, 32, 32),
|
||||
edit_fg: cyan,
|
||||
hover_bg: surface2,
|
||||
hover_fg: fg,
|
||||
muted_bg: Color::Rgb(18, 14, 14),
|
||||
muted_fg: fg_muted,
|
||||
soloed_bg: Color::Rgb(40, 32, 12),
|
||||
soloed_fg: yellow,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: red,
|
||||
connected: green,
|
||||
listening: yellow,
|
||||
},
|
||||
syntax: SyntaxColors {
|
||||
gap_bg: bg,
|
||||
executed_bg: Color::Rgb(25, 18, 14),
|
||||
selected_bg: Color::Rgb(45, 30, 15),
|
||||
emit: (fg, Color::Rgb(45, 20, 18)),
|
||||
number: (yellow, Color::Rgb(40, 30, 12)),
|
||||
string: (green, Color::Rgb(25, 30, 18)),
|
||||
comment: (fg_muted, bg),
|
||||
keyword: (red, Color::Rgb(42, 18, 15)),
|
||||
stack_op: (blue, Color::Rgb(22, 28, 38)),
|
||||
operator: (orange, Color::Rgb(42, 26, 12)),
|
||||
sound: (cyan, Color::Rgb(22, 32, 32)),
|
||||
param: (purple, Color::Rgb(32, 24, 28)),
|
||||
context: (orange, Color::Rgb(42, 26, 12)),
|
||||
note: (green, Color::Rgb(25, 30, 18)),
|
||||
interval: (Color::Rgb(150, 180, 100), Color::Rgb(28, 34, 20)),
|
||||
variable: (purple, Color::Rgb(32, 24, 28)),
|
||||
vary: (yellow, Color::Rgb(40, 30, 12)),
|
||||
generator: (cyan, Color::Rgb(22, 32, 30)),
|
||||
user_defined: (purple, Color::Rgb(28, 20, 26)),
|
||||
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(45, 30, 20),
|
||||
selected_fg: fg,
|
||||
unselected_bg: surface,
|
||||
unselected_fg: fg_muted,
|
||||
},
|
||||
editor_widget: EditorWidgetColors {
|
||||
cursor_bg: fg,
|
||||
cursor_fg: bg,
|
||||
selection_bg: Color::Rgb(50, 35, 25),
|
||||
completion_bg: surface,
|
||||
completion_fg: fg,
|
||||
completion_selected: orange,
|
||||
completion_example: cyan,
|
||||
},
|
||||
browser: BrowserColors {
|
||||
directory: blue,
|
||||
project_file: orange,
|
||||
selected: red,
|
||||
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: orange,
|
||||
cursor: fg,
|
||||
hint: fg_muted,
|
||||
},
|
||||
search: SearchColors {
|
||||
active: orange,
|
||||
inactive: fg_muted,
|
||||
match_bg: yellow,
|
||||
match_fg: bg,
|
||||
},
|
||||
markdown: MarkdownColors {
|
||||
h1: orange,
|
||||
h2: red,
|
||||
h3: purple,
|
||||
code: green,
|
||||
code_border: Color::Rgb(50, 38, 38),
|
||||
link: cyan,
|
||||
link_url: Color::Rgb(80, 68, 56),
|
||||
quote: fg_muted,
|
||||
text: fg,
|
||||
list: fg,
|
||||
},
|
||||
engine: EngineColors {
|
||||
header: orange,
|
||||
header_focused: yellow,
|
||||
divider: Color::Rgb(38, 30, 30),
|
||||
scroll_indicator: Color::Rgb(55, 42, 42),
|
||||
label: Color::Rgb(130, 115, 100),
|
||||
label_focused: Color::Rgb(170, 155, 140),
|
||||
label_dim: Color::Rgb(90, 76, 64),
|
||||
value: Color::Rgb(200, 188, 175),
|
||||
focused: yellow,
|
||||
normal: fg,
|
||||
dim: Color::Rgb(55, 42, 42),
|
||||
path: Color::Rgb(130, 115, 100),
|
||||
border_magenta: purple,
|
||||
border_green: green,
|
||||
border_cyan: cyan,
|
||||
separator: Color::Rgb(38, 30, 30),
|
||||
hint_active: orange,
|
||||
hint_inactive: Color::Rgb(38, 30, 30),
|
||||
},
|
||||
dict: DictColors {
|
||||
word_name: green,
|
||||
word_bg: Color::Rgb(22, 28, 22),
|
||||
alias: fg_muted,
|
||||
stack_sig: purple,
|
||||
description: fg,
|
||||
example: Color::Rgb(130, 115, 100),
|
||||
category_focused: yellow,
|
||||
category_selected: orange,
|
||||
category_normal: fg,
|
||||
category_dimmed: Color::Rgb(55, 42, 42),
|
||||
border_focused: yellow,
|
||||
border_normal: Color::Rgb(38, 30, 30),
|
||||
header_desc: Color::Rgb(145, 130, 115),
|
||||
},
|
||||
title: TitleColors {
|
||||
big_title: orange,
|
||||
author: red,
|
||||
link: cyan,
|
||||
license: yellow,
|
||||
prompt: Color::Rgb(130, 115, 100),
|
||||
subtitle: fg,
|
||||
},
|
||||
meter: MeterColors {
|
||||
low: green,
|
||||
mid: yellow,
|
||||
high: red,
|
||||
low_rgb: (128, 160, 80),
|
||||
mid_rgb: (208, 160, 48),
|
||||
high_rgb: (224, 80, 64),
|
||||
},
|
||||
sparkle: SparkleColors {
|
||||
colors: [
|
||||
(224, 128, 48),
|
||||
(224, 80, 64),
|
||||
(208, 160, 48),
|
||||
(112, 160, 160),
|
||||
(128, 160, 80),
|
||||
],
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: red,
|
||||
button_selected_bg: orange,
|
||||
button_selected_fg: bg,
|
||||
},
|
||||
}
|
||||
}
|
||||
282
crates/ratatui/src/theme/fairyfloss.rs
Normal file
282
crates/ratatui/src/theme/fairyfloss.rs
Normal file
@@ -0,0 +1,282 @@
|
||||
use super::*;
|
||||
use ratatui::style::Color;
|
||||
|
||||
pub fn theme() -> ThemeColors {
|
||||
let bg = Color::Rgb(90, 84, 117);
|
||||
let bg_light = Color::Rgb(113, 103, 153);
|
||||
let bg_lighter = Color::Rgb(130, 120, 165);
|
||||
let fg = Color::Rgb(248, 248, 240);
|
||||
let fg_dim = Color::Rgb(197, 163, 255);
|
||||
let muted = Color::Rgb(168, 164, 177);
|
||||
let dark = Color::Rgb(55, 51, 72);
|
||||
|
||||
let purple = Color::Rgb(174, 129, 255);
|
||||
let pink = Color::Rgb(255, 184, 209);
|
||||
let coral = Color::Rgb(255, 133, 127);
|
||||
let yellow = Color::Rgb(255, 243, 82);
|
||||
let gold = Color::Rgb(230, 192, 0);
|
||||
let mint = Color::Rgb(194, 255, 223);
|
||||
let lavender = Color::Rgb(197, 163, 255);
|
||||
|
||||
ThemeColors {
|
||||
ui: UiColors {
|
||||
bg,
|
||||
bg_rgb: (90, 84, 117),
|
||||
text_primary: fg,
|
||||
text_muted: fg_dim,
|
||||
text_dim: muted,
|
||||
border: bg_lighter,
|
||||
header: mint,
|
||||
unfocused: muted,
|
||||
accent: pink,
|
||||
surface: bg_light,
|
||||
},
|
||||
status: StatusColors {
|
||||
playing_bg: Color::Rgb(70, 95, 85),
|
||||
playing_fg: mint,
|
||||
stopped_bg: Color::Rgb(100, 70, 85),
|
||||
stopped_fg: coral,
|
||||
fill_on: mint,
|
||||
fill_off: muted,
|
||||
fill_bg: bg_light,
|
||||
},
|
||||
selection: SelectionColors {
|
||||
cursor_bg: pink,
|
||||
cursor_fg: dark,
|
||||
selected_bg: Color::Rgb(120, 90, 130),
|
||||
selected_fg: pink,
|
||||
in_range_bg: Color::Rgb(100, 95, 125),
|
||||
in_range_fg: fg,
|
||||
cursor: pink,
|
||||
selected: Color::Rgb(120, 90, 130),
|
||||
in_range: Color::Rgb(100, 95, 125),
|
||||
},
|
||||
tile: TileColors {
|
||||
playing_active_bg: Color::Rgb(100, 85, 60),
|
||||
playing_active_fg: gold,
|
||||
playing_inactive_bg: Color::Rgb(95, 90, 70),
|
||||
playing_inactive_fg: yellow,
|
||||
active_bg: Color::Rgb(70, 100, 100),
|
||||
active_fg: mint,
|
||||
content_bg: Color::Rgb(77, 107, 107),
|
||||
inactive_bg: bg_light,
|
||||
inactive_fg: fg_dim,
|
||||
active_selected_bg: Color::Rgb(120, 90, 130),
|
||||
active_in_range_bg: Color::Rgb(100, 95, 125),
|
||||
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),
|
||||
],
|
||||
},
|
||||
header: HeaderColors {
|
||||
tempo_bg: Color::Rgb(100, 75, 95),
|
||||
tempo_fg: pink,
|
||||
bank_bg: Color::Rgb(70, 95, 95),
|
||||
bank_fg: mint,
|
||||
pattern_bg: Color::Rgb(85, 75, 110),
|
||||
pattern_fg: purple,
|
||||
stats_bg: bg_light,
|
||||
stats_fg: fg_dim,
|
||||
},
|
||||
modal: ModalColors {
|
||||
border: mint,
|
||||
border_accent: pink,
|
||||
border_warn: coral,
|
||||
border_dim: muted,
|
||||
confirm: coral,
|
||||
rename: purple,
|
||||
input: mint,
|
||||
editor: mint,
|
||||
preview: muted,
|
||||
},
|
||||
flash: FlashColors {
|
||||
error_bg: Color::Rgb(100, 65, 70),
|
||||
error_fg: coral,
|
||||
success_bg: Color::Rgb(65, 95, 85),
|
||||
success_fg: mint,
|
||||
info_bg: bg_light,
|
||||
info_fg: fg,
|
||||
},
|
||||
list: ListColors {
|
||||
playing_bg: Color::Rgb(65, 95, 85),
|
||||
playing_fg: mint,
|
||||
staged_play_bg: Color::Rgb(95, 80, 120),
|
||||
staged_play_fg: purple,
|
||||
staged_stop_bg: Color::Rgb(105, 70, 85),
|
||||
staged_stop_fg: pink,
|
||||
edit_bg: Color::Rgb(70, 95, 100),
|
||||
edit_fg: mint,
|
||||
hover_bg: bg_lighter,
|
||||
hover_fg: fg,
|
||||
muted_bg: Color::Rgb(75, 70, 95),
|
||||
muted_fg: muted,
|
||||
soloed_bg: Color::Rgb(100, 95, 65),
|
||||
soloed_fg: yellow,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: coral,
|
||||
connected: mint,
|
||||
listening: yellow,
|
||||
},
|
||||
syntax: SyntaxColors {
|
||||
gap_bg: dark,
|
||||
executed_bg: Color::Rgb(80, 75, 100),
|
||||
selected_bg: Color::Rgb(110, 100, 70),
|
||||
emit: (fg, Color::Rgb(110, 80, 100)),
|
||||
number: (purple, Color::Rgb(85, 75, 110)),
|
||||
string: (yellow, Color::Rgb(100, 95, 60)),
|
||||
comment: (muted, dark),
|
||||
keyword: (pink, Color::Rgb(105, 75, 90)),
|
||||
stack_op: (mint, Color::Rgb(70, 100, 95)),
|
||||
operator: (pink, Color::Rgb(105, 75, 90)),
|
||||
sound: (mint, Color::Rgb(70, 100, 95)),
|
||||
param: (coral, Color::Rgb(105, 70, 70)),
|
||||
context: (coral, Color::Rgb(105, 70, 70)),
|
||||
note: (lavender, Color::Rgb(85, 75, 110)),
|
||||
interval: (Color::Rgb(220, 190, 255), Color::Rgb(85, 75, 100)),
|
||||
variable: (lavender, Color::Rgb(85, 75, 110)),
|
||||
vary: (yellow, Color::Rgb(100, 95, 60)),
|
||||
generator: (mint, Color::Rgb(70, 95, 95)),
|
||||
user_defined: (coral, Color::Rgb(100, 75, 75)),
|
||||
default: (fg_dim, dark),
|
||||
},
|
||||
table: TableColors {
|
||||
row_even: dark,
|
||||
row_odd: bg,
|
||||
},
|
||||
values: ValuesColors {
|
||||
tempo: coral,
|
||||
value: fg_dim,
|
||||
},
|
||||
hint: HintColors {
|
||||
key: coral,
|
||||
text: muted,
|
||||
},
|
||||
view_badge: ViewBadgeColors { bg: fg, fg: bg },
|
||||
nav: NavColors {
|
||||
selected_bg: Color::Rgb(110, 85, 120),
|
||||
selected_fg: fg,
|
||||
unselected_bg: bg_light,
|
||||
unselected_fg: muted,
|
||||
},
|
||||
editor_widget: EditorWidgetColors {
|
||||
cursor_bg: fg,
|
||||
cursor_fg: bg,
|
||||
selection_bg: Color::Rgb(105, 95, 125),
|
||||
completion_bg: bg_light,
|
||||
completion_fg: fg,
|
||||
completion_selected: coral,
|
||||
completion_example: mint,
|
||||
},
|
||||
browser: BrowserColors {
|
||||
directory: mint,
|
||||
project_file: purple,
|
||||
selected: coral,
|
||||
file: fg,
|
||||
focused_border: coral,
|
||||
unfocused_border: muted,
|
||||
root: fg,
|
||||
file_icon: muted,
|
||||
folder_icon: mint,
|
||||
empty_text: muted,
|
||||
},
|
||||
input: InputColors {
|
||||
text: mint,
|
||||
cursor: fg,
|
||||
hint: muted,
|
||||
},
|
||||
search: SearchColors {
|
||||
active: coral,
|
||||
inactive: muted,
|
||||
match_bg: yellow,
|
||||
match_fg: dark,
|
||||
},
|
||||
markdown: MarkdownColors {
|
||||
h1: mint,
|
||||
h2: coral,
|
||||
h3: purple,
|
||||
code: lavender,
|
||||
code_border: Color::Rgb(120, 115, 140),
|
||||
link: pink,
|
||||
link_url: Color::Rgb(150, 145, 165),
|
||||
quote: muted,
|
||||
text: fg,
|
||||
list: fg,
|
||||
},
|
||||
engine: EngineColors {
|
||||
header: mint,
|
||||
header_focused: yellow,
|
||||
divider: Color::Rgb(110, 105, 130),
|
||||
scroll_indicator: Color::Rgb(125, 120, 145),
|
||||
label: Color::Rgb(175, 170, 190),
|
||||
label_focused: Color::Rgb(210, 205, 225),
|
||||
label_dim: Color::Rgb(145, 140, 160),
|
||||
value: Color::Rgb(230, 225, 240),
|
||||
focused: yellow,
|
||||
normal: fg,
|
||||
dim: Color::Rgb(125, 120, 145),
|
||||
path: Color::Rgb(175, 170, 190),
|
||||
border_magenta: pink,
|
||||
border_green: mint,
|
||||
border_cyan: lavender,
|
||||
separator: Color::Rgb(110, 105, 130),
|
||||
hint_active: Color::Rgb(240, 230, 120),
|
||||
hint_inactive: Color::Rgb(110, 105, 130),
|
||||
},
|
||||
dict: DictColors {
|
||||
word_name: lavender,
|
||||
word_bg: Color::Rgb(75, 85, 105),
|
||||
alias: muted,
|
||||
stack_sig: purple,
|
||||
description: fg,
|
||||
example: Color::Rgb(175, 170, 190),
|
||||
category_focused: yellow,
|
||||
category_selected: mint,
|
||||
category_normal: fg,
|
||||
category_dimmed: Color::Rgb(125, 120, 145),
|
||||
border_focused: yellow,
|
||||
border_normal: Color::Rgb(110, 105, 130),
|
||||
header_desc: Color::Rgb(195, 190, 210),
|
||||
},
|
||||
title: TitleColors {
|
||||
big_title: pink,
|
||||
author: mint,
|
||||
link: lavender,
|
||||
license: coral,
|
||||
prompt: Color::Rgb(195, 190, 210),
|
||||
subtitle: fg,
|
||||
},
|
||||
meter: MeterColors {
|
||||
low: mint,
|
||||
mid: yellow,
|
||||
high: coral,
|
||||
low_rgb: (194, 255, 223),
|
||||
mid_rgb: (255, 243, 82),
|
||||
high_rgb: (255, 133, 127),
|
||||
},
|
||||
sparkle: SparkleColors {
|
||||
colors: [
|
||||
(194, 255, 223),
|
||||
(255, 133, 127),
|
||||
(255, 243, 82),
|
||||
(255, 184, 209),
|
||||
(174, 129, 255),
|
||||
],
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: coral,
|
||||
button_selected_bg: coral,
|
||||
button_selected_fg: dark,
|
||||
},
|
||||
}
|
||||
}
|
||||
286
crates/ratatui/src/theme/georges.rs
Normal file
286
crates/ratatui/src/theme/georges.rs
Normal file
@@ -0,0 +1,286 @@
|
||||
use super::*;
|
||||
use ratatui::style::Color;
|
||||
|
||||
// C64 palette on pure black
|
||||
pub fn theme() -> ThemeColors {
|
||||
let bg = Color::Rgb(0, 0, 0);
|
||||
let surface = Color::Rgb(16, 16, 16);
|
||||
let surface2 = Color::Rgb(24, 24, 24);
|
||||
let border = Color::Rgb(51, 51, 51);
|
||||
let fg = Color::Rgb(187, 187, 187);
|
||||
let fg_dim = Color::Rgb(119, 119, 119);
|
||||
let fg_muted = Color::Rgb(51, 51, 51);
|
||||
|
||||
let white = Color::Rgb(255, 255, 255);
|
||||
let lightred = Color::Rgb(255, 119, 119);
|
||||
let cyan = Color::Rgb(170, 255, 238);
|
||||
let violet = Color::Rgb(204, 68, 204);
|
||||
let green = Color::Rgb(0, 204, 85);
|
||||
let lightgreen = Color::Rgb(170, 255, 102);
|
||||
let lightblue = Color::Rgb(0, 136, 255);
|
||||
let yellow = Color::Rgb(238, 238, 119);
|
||||
let orange = Color::Rgb(221, 136, 85);
|
||||
let brown = Color::Rgb(102, 68, 0);
|
||||
|
||||
ThemeColors {
|
||||
ui: UiColors {
|
||||
bg,
|
||||
bg_rgb: (0, 0, 0),
|
||||
text_primary: fg,
|
||||
text_muted: fg_dim,
|
||||
text_dim: fg_muted,
|
||||
border,
|
||||
header: lightblue,
|
||||
unfocused: fg_muted,
|
||||
accent: lightblue,
|
||||
surface,
|
||||
},
|
||||
status: StatusColors {
|
||||
playing_bg: Color::Rgb(0, 24, 10),
|
||||
playing_fg: green,
|
||||
stopped_bg: Color::Rgb(28, 0, 0),
|
||||
stopped_fg: lightred,
|
||||
fill_on: lightblue,
|
||||
fill_off: brown,
|
||||
fill_bg: surface,
|
||||
},
|
||||
selection: SelectionColors {
|
||||
cursor_bg: lightblue,
|
||||
cursor_fg: bg,
|
||||
selected_bg: Color::Rgb(0, 20, 40),
|
||||
selected_fg: cyan,
|
||||
in_range_bg: Color::Rgb(0, 14, 28),
|
||||
in_range_fg: fg,
|
||||
cursor: lightblue,
|
||||
selected: Color::Rgb(0, 20, 40),
|
||||
in_range: Color::Rgb(0, 14, 28),
|
||||
},
|
||||
tile: TileColors {
|
||||
playing_active_bg: Color::Rgb(0, 30, 12),
|
||||
playing_active_fg: lightgreen,
|
||||
playing_inactive_bg: Color::Rgb(30, 30, 14),
|
||||
playing_inactive_fg: yellow,
|
||||
active_bg: Color::Rgb(0, 16, 32),
|
||||
active_fg: lightblue,
|
||||
content_bg: Color::Rgb(7, 23, 39),
|
||||
inactive_bg: surface,
|
||||
inactive_fg: fg_dim,
|
||||
active_selected_bg: Color::Rgb(10, 20, 36),
|
||||
active_in_range_bg: Color::Rgb(6, 14, 28),
|
||||
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),
|
||||
],
|
||||
},
|
||||
header: HeaderColors {
|
||||
tempo_bg: Color::Rgb(28, 10, 28),
|
||||
tempo_fg: violet,
|
||||
bank_bg: Color::Rgb(0, 0, 24),
|
||||
bank_fg: lightblue,
|
||||
pattern_bg: Color::Rgb(0, 24, 10),
|
||||
pattern_fg: green,
|
||||
stats_bg: surface,
|
||||
stats_fg: fg_dim,
|
||||
},
|
||||
modal: ModalColors {
|
||||
border: lightblue,
|
||||
border_accent: cyan,
|
||||
border_warn: yellow,
|
||||
border_dim: fg_muted,
|
||||
confirm: lightred,
|
||||
rename: lightblue,
|
||||
input: cyan,
|
||||
editor: lightblue,
|
||||
preview: fg_muted,
|
||||
},
|
||||
flash: FlashColors {
|
||||
error_bg: Color::Rgb(28, 0, 0),
|
||||
error_fg: lightred,
|
||||
success_bg: Color::Rgb(0, 24, 10),
|
||||
success_fg: green,
|
||||
info_bg: surface,
|
||||
info_fg: fg,
|
||||
},
|
||||
list: ListColors {
|
||||
playing_bg: Color::Rgb(0, 24, 10),
|
||||
playing_fg: green,
|
||||
staged_play_bg: Color::Rgb(28, 10, 28),
|
||||
staged_play_fg: violet,
|
||||
staged_stop_bg: Color::Rgb(28, 0, 0),
|
||||
staged_stop_fg: lightred,
|
||||
edit_bg: Color::Rgb(0, 16, 32),
|
||||
edit_fg: lightblue,
|
||||
hover_bg: surface2,
|
||||
hover_fg: fg,
|
||||
muted_bg: Color::Rgb(8, 8, 8),
|
||||
muted_fg: fg_muted,
|
||||
soloed_bg: Color::Rgb(30, 30, 14),
|
||||
soloed_fg: yellow,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: lightred,
|
||||
connected: green,
|
||||
listening: yellow,
|
||||
},
|
||||
syntax: SyntaxColors {
|
||||
gap_bg: bg,
|
||||
executed_bg: Color::Rgb(0, 12, 24),
|
||||
selected_bg: Color::Rgb(0, 20, 40),
|
||||
emit: (white, Color::Rgb(20, 20, 20)),
|
||||
number: (yellow, Color::Rgb(30, 30, 14)),
|
||||
string: (lightgreen, Color::Rgb(18, 30, 12)),
|
||||
comment: (fg_muted, bg),
|
||||
keyword: (lightred, Color::Rgb(30, 14, 14)),
|
||||
stack_op: (lightblue, Color::Rgb(0, 16, 32)),
|
||||
operator: (cyan, Color::Rgb(20, 30, 28)),
|
||||
sound: (green, Color::Rgb(0, 24, 10)),
|
||||
param: (violet, Color::Rgb(24, 8, 24)),
|
||||
context: (orange, Color::Rgb(28, 16, 10)),
|
||||
note: (lightgreen, Color::Rgb(18, 30, 12)),
|
||||
interval: (Color::Rgb(130, 210, 80), Color::Rgb(14, 26, 8)),
|
||||
variable: (violet, Color::Rgb(24, 8, 24)),
|
||||
vary: (yellow, Color::Rgb(30, 30, 14)),
|
||||
generator: (cyan, Color::Rgb(20, 30, 28)),
|
||||
user_defined: (orange, Color::Rgb(30, 20, 10)),
|
||||
default: (fg_dim, bg),
|
||||
},
|
||||
table: TableColors {
|
||||
row_even: bg,
|
||||
row_odd: surface,
|
||||
},
|
||||
values: ValuesColors {
|
||||
tempo: lightblue,
|
||||
value: fg_dim,
|
||||
},
|
||||
hint: HintColors {
|
||||
key: lightblue,
|
||||
text: fg_muted,
|
||||
},
|
||||
view_badge: ViewBadgeColors { bg: fg, fg: bg },
|
||||
nav: NavColors {
|
||||
selected_bg: Color::Rgb(0, 20, 40),
|
||||
selected_fg: fg,
|
||||
unselected_bg: surface,
|
||||
unselected_fg: fg_muted,
|
||||
},
|
||||
editor_widget: EditorWidgetColors {
|
||||
cursor_bg: fg,
|
||||
cursor_fg: bg,
|
||||
selection_bg: Color::Rgb(0, 24, 48),
|
||||
completion_bg: surface,
|
||||
completion_fg: fg,
|
||||
completion_selected: lightblue,
|
||||
completion_example: cyan,
|
||||
},
|
||||
browser: BrowserColors {
|
||||
directory: lightblue,
|
||||
project_file: green,
|
||||
selected: cyan,
|
||||
file: fg,
|
||||
focused_border: lightblue,
|
||||
unfocused_border: fg_muted,
|
||||
root: fg,
|
||||
file_icon: fg_muted,
|
||||
folder_icon: lightblue,
|
||||
empty_text: fg_muted,
|
||||
},
|
||||
input: InputColors {
|
||||
text: lightblue,
|
||||
cursor: fg,
|
||||
hint: fg_muted,
|
||||
},
|
||||
search: SearchColors {
|
||||
active: lightblue,
|
||||
inactive: fg_muted,
|
||||
match_bg: yellow,
|
||||
match_fg: bg,
|
||||
},
|
||||
markdown: MarkdownColors {
|
||||
h1: lightblue,
|
||||
h2: green,
|
||||
h3: violet,
|
||||
code: lightgreen,
|
||||
code_border: Color::Rgb(36, 36, 36),
|
||||
link: cyan,
|
||||
link_url: brown,
|
||||
quote: fg_muted,
|
||||
text: fg,
|
||||
list: fg,
|
||||
},
|
||||
engine: EngineColors {
|
||||
header: lightblue,
|
||||
header_focused: cyan,
|
||||
divider: Color::Rgb(28, 28, 28),
|
||||
scroll_indicator: Color::Rgb(51, 51, 51),
|
||||
label: fg_dim,
|
||||
label_focused: fg,
|
||||
label_dim: fg_muted,
|
||||
value: fg,
|
||||
focused: cyan,
|
||||
normal: fg,
|
||||
dim: Color::Rgb(36, 36, 36),
|
||||
path: fg_dim,
|
||||
border_magenta: violet,
|
||||
border_green: green,
|
||||
border_cyan: cyan,
|
||||
separator: Color::Rgb(28, 28, 28),
|
||||
hint_active: lightblue,
|
||||
hint_inactive: Color::Rgb(28, 28, 28),
|
||||
},
|
||||
dict: DictColors {
|
||||
word_name: lightgreen,
|
||||
word_bg: Color::Rgb(10, 18, 6),
|
||||
alias: fg_muted,
|
||||
stack_sig: violet,
|
||||
description: fg,
|
||||
example: fg_dim,
|
||||
category_focused: cyan,
|
||||
category_selected: lightblue,
|
||||
category_normal: fg,
|
||||
category_dimmed: Color::Rgb(36, 36, 36),
|
||||
border_focused: cyan,
|
||||
border_normal: Color::Rgb(28, 28, 28),
|
||||
header_desc: fg_dim,
|
||||
},
|
||||
title: TitleColors {
|
||||
big_title: lightblue,
|
||||
author: green,
|
||||
link: cyan,
|
||||
license: yellow,
|
||||
prompt: fg_dim,
|
||||
subtitle: fg,
|
||||
},
|
||||
meter: MeterColors {
|
||||
low: green,
|
||||
mid: yellow,
|
||||
high: lightred,
|
||||
low_rgb: (0, 204, 85),
|
||||
mid_rgb: (238, 238, 119),
|
||||
high_rgb: (255, 119, 119),
|
||||
},
|
||||
sparkle: SparkleColors {
|
||||
colors: [
|
||||
(0, 136, 255),
|
||||
(170, 255, 238),
|
||||
(0, 204, 85),
|
||||
(238, 238, 119),
|
||||
(204, 68, 204),
|
||||
],
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: lightred,
|
||||
button_selected_bg: lightblue,
|
||||
button_selected_fg: bg,
|
||||
},
|
||||
}
|
||||
}
|
||||
283
crates/ratatui/src/theme/gruvbox_dark.rs
Normal file
283
crates/ratatui/src/theme/gruvbox_dark.rs
Normal file
@@ -0,0 +1,283 @@
|
||||
use super::*;
|
||||
use ratatui::style::Color;
|
||||
|
||||
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);
|
||||
|
||||
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,
|
||||
content_bg: Color::Rgb(57, 72, 62),
|
||||
inactive_bg: bg1,
|
||||
inactive_fg: fg3,
|
||||
active_selected_bg: Color::Rgb(85, 70, 60),
|
||||
active_in_range_bg: Color::Rgb(70, 65, 55),
|
||||
link_bright: [
|
||||
(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),
|
||||
],
|
||||
},
|
||||
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,
|
||||
},
|
||||
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,
|
||||
muted_bg: Color::Rgb(50, 50, 55),
|
||||
muted_fg: fg4,
|
||||
soloed_bg: Color::Rgb(70, 65, 40),
|
||||
soloed_fg: yellow,
|
||||
},
|
||||
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)),
|
||||
user_defined: (orange, Color::Rgb(60, 45, 30)),
|
||||
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),
|
||||
],
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: orange,
|
||||
button_selected_bg: orange,
|
||||
button_selected_fg: bg0,
|
||||
},
|
||||
}
|
||||
}
|
||||
278
crates/ratatui/src/theme/hot_dog_stand.rs
Normal file
278
crates/ratatui/src/theme/hot_dog_stand.rs
Normal file
@@ -0,0 +1,278 @@
|
||||
use super::*;
|
||||
use ratatui::style::Color;
|
||||
|
||||
pub fn theme() -> ThemeColors {
|
||||
let red = Color::Rgb(255, 0, 0);
|
||||
let dark_red = Color::Rgb(215, 0, 0);
|
||||
let darker_red = Color::Rgb(175, 0, 0);
|
||||
let yellow = Color::Rgb(255, 255, 0);
|
||||
let light_yellow = Color::Rgb(255, 255, 95);
|
||||
let gold = Color::Rgb(255, 215, 0);
|
||||
let black = Color::Rgb(0, 0, 0);
|
||||
let white = Color::Rgb(255, 255, 255);
|
||||
|
||||
let dim_yellow = Color::Rgb(180, 180, 0);
|
||||
let muted_red = Color::Rgb(140, 40, 40);
|
||||
|
||||
ThemeColors {
|
||||
ui: UiColors {
|
||||
bg: red,
|
||||
bg_rgb: (255, 0, 0),
|
||||
text_primary: yellow,
|
||||
text_muted: light_yellow,
|
||||
text_dim: gold,
|
||||
border: yellow,
|
||||
header: yellow,
|
||||
unfocused: gold,
|
||||
accent: yellow,
|
||||
surface: dark_red,
|
||||
},
|
||||
status: StatusColors {
|
||||
playing_bg: Color::Rgb(180, 180, 0),
|
||||
playing_fg: black,
|
||||
stopped_bg: darker_red,
|
||||
stopped_fg: yellow,
|
||||
fill_on: yellow,
|
||||
fill_off: gold,
|
||||
fill_bg: dark_red,
|
||||
},
|
||||
selection: SelectionColors {
|
||||
cursor_bg: yellow,
|
||||
cursor_fg: red,
|
||||
selected_bg: Color::Rgb(200, 200, 0),
|
||||
selected_fg: black,
|
||||
in_range_bg: Color::Rgb(170, 100, 0),
|
||||
in_range_fg: yellow,
|
||||
cursor: yellow,
|
||||
selected: Color::Rgb(200, 200, 0),
|
||||
in_range: Color::Rgb(170, 100, 0),
|
||||
},
|
||||
tile: TileColors {
|
||||
playing_active_bg: Color::Rgb(200, 200, 0),
|
||||
playing_active_fg: black,
|
||||
playing_inactive_bg: Color::Rgb(180, 180, 0),
|
||||
playing_inactive_fg: black,
|
||||
active_bg: Color::Rgb(200, 50, 50),
|
||||
active_fg: yellow,
|
||||
content_bg: Color::Rgb(210, 60, 60),
|
||||
inactive_bg: dark_red,
|
||||
inactive_fg: gold,
|
||||
active_selected_bg: Color::Rgb(200, 200, 0),
|
||||
active_in_range_bg: Color::Rgb(170, 100, 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),
|
||||
],
|
||||
},
|
||||
header: HeaderColors {
|
||||
tempo_bg: Color::Rgb(180, 180, 0),
|
||||
tempo_fg: black,
|
||||
bank_bg: darker_red,
|
||||
bank_fg: yellow,
|
||||
pattern_bg: Color::Rgb(200, 200, 0),
|
||||
pattern_fg: black,
|
||||
stats_bg: dark_red,
|
||||
stats_fg: yellow,
|
||||
},
|
||||
modal: ModalColors {
|
||||
border: yellow,
|
||||
border_accent: white,
|
||||
border_warn: gold,
|
||||
border_dim: dim_yellow,
|
||||
confirm: gold,
|
||||
rename: light_yellow,
|
||||
input: yellow,
|
||||
editor: yellow,
|
||||
preview: gold,
|
||||
},
|
||||
flash: FlashColors {
|
||||
error_bg: black,
|
||||
error_fg: yellow,
|
||||
success_bg: Color::Rgb(180, 180, 0),
|
||||
success_fg: black,
|
||||
info_bg: dark_red,
|
||||
info_fg: yellow,
|
||||
},
|
||||
list: ListColors {
|
||||
playing_bg: Color::Rgb(180, 180, 0),
|
||||
playing_fg: black,
|
||||
staged_play_bg: Color::Rgb(200, 200, 0),
|
||||
staged_play_fg: black,
|
||||
staged_stop_bg: darker_red,
|
||||
staged_stop_fg: yellow,
|
||||
edit_bg: Color::Rgb(200, 50, 50),
|
||||
edit_fg: yellow,
|
||||
hover_bg: Color::Rgb(230, 50, 50),
|
||||
hover_fg: yellow,
|
||||
muted_bg: darker_red,
|
||||
muted_fg: dim_yellow,
|
||||
soloed_bg: Color::Rgb(200, 200, 0),
|
||||
soloed_fg: black,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: white,
|
||||
connected: yellow,
|
||||
listening: gold,
|
||||
},
|
||||
syntax: SyntaxColors {
|
||||
gap_bg: darker_red,
|
||||
executed_bg: Color::Rgb(200, 50, 50),
|
||||
selected_bg: Color::Rgb(180, 180, 0),
|
||||
emit: (yellow, muted_red),
|
||||
number: (white, muted_red),
|
||||
string: (gold, muted_red),
|
||||
comment: (dim_yellow, darker_red),
|
||||
keyword: (light_yellow, muted_red),
|
||||
stack_op: (yellow, muted_red),
|
||||
operator: (light_yellow, muted_red),
|
||||
sound: (yellow, muted_red),
|
||||
param: (gold, muted_red),
|
||||
context: (gold, muted_red),
|
||||
note: (white, muted_red),
|
||||
interval: (Color::Rgb(255, 240, 150), muted_red),
|
||||
variable: (white, muted_red),
|
||||
vary: (gold, muted_red),
|
||||
generator: (yellow, muted_red),
|
||||
user_defined: (gold, Color::Rgb(100, 70, 0)),
|
||||
default: (light_yellow, darker_red),
|
||||
},
|
||||
table: TableColors {
|
||||
row_even: darker_red,
|
||||
row_odd: red,
|
||||
},
|
||||
values: ValuesColors {
|
||||
tempo: gold,
|
||||
value: light_yellow,
|
||||
},
|
||||
hint: HintColors {
|
||||
key: white,
|
||||
text: gold,
|
||||
},
|
||||
view_badge: ViewBadgeColors { bg: yellow, fg: red },
|
||||
nav: NavColors {
|
||||
selected_bg: Color::Rgb(200, 200, 0),
|
||||
selected_fg: black,
|
||||
unselected_bg: dark_red,
|
||||
unselected_fg: gold,
|
||||
},
|
||||
editor_widget: EditorWidgetColors {
|
||||
cursor_bg: yellow,
|
||||
cursor_fg: red,
|
||||
selection_bg: Color::Rgb(180, 180, 0),
|
||||
completion_bg: dark_red,
|
||||
completion_fg: yellow,
|
||||
completion_selected: white,
|
||||
completion_example: gold,
|
||||
},
|
||||
browser: BrowserColors {
|
||||
directory: yellow,
|
||||
project_file: white,
|
||||
selected: gold,
|
||||
file: light_yellow,
|
||||
focused_border: white,
|
||||
unfocused_border: gold,
|
||||
root: yellow,
|
||||
file_icon: gold,
|
||||
folder_icon: yellow,
|
||||
empty_text: gold,
|
||||
},
|
||||
input: InputColors {
|
||||
text: yellow,
|
||||
cursor: white,
|
||||
hint: gold,
|
||||
},
|
||||
search: SearchColors {
|
||||
active: white,
|
||||
inactive: gold,
|
||||
match_bg: yellow,
|
||||
match_fg: red,
|
||||
},
|
||||
markdown: MarkdownColors {
|
||||
h1: yellow,
|
||||
h2: white,
|
||||
h3: gold,
|
||||
code: light_yellow,
|
||||
code_border: dim_yellow,
|
||||
link: white,
|
||||
link_url: gold,
|
||||
quote: dim_yellow,
|
||||
text: yellow,
|
||||
list: yellow,
|
||||
},
|
||||
engine: EngineColors {
|
||||
header: yellow,
|
||||
header_focused: white,
|
||||
divider: dim_yellow,
|
||||
scroll_indicator: gold,
|
||||
label: light_yellow,
|
||||
label_focused: white,
|
||||
label_dim: dim_yellow,
|
||||
value: yellow,
|
||||
focused: white,
|
||||
normal: yellow,
|
||||
dim: dim_yellow,
|
||||
path: gold,
|
||||
border_magenta: gold,
|
||||
border_green: yellow,
|
||||
border_cyan: white,
|
||||
separator: dim_yellow,
|
||||
hint_active: white,
|
||||
hint_inactive: dim_yellow,
|
||||
},
|
||||
dict: DictColors {
|
||||
word_name: yellow,
|
||||
word_bg: darker_red,
|
||||
alias: gold,
|
||||
stack_sig: white,
|
||||
description: yellow,
|
||||
example: gold,
|
||||
category_focused: white,
|
||||
category_selected: yellow,
|
||||
category_normal: light_yellow,
|
||||
category_dimmed: dim_yellow,
|
||||
border_focused: white,
|
||||
border_normal: dim_yellow,
|
||||
header_desc: gold,
|
||||
},
|
||||
title: TitleColors {
|
||||
big_title: yellow,
|
||||
author: white,
|
||||
link: gold,
|
||||
license: light_yellow,
|
||||
prompt: gold,
|
||||
subtitle: yellow,
|
||||
},
|
||||
meter: MeterColors {
|
||||
low: yellow,
|
||||
mid: gold,
|
||||
high: white,
|
||||
low_rgb: (255, 255, 0),
|
||||
mid_rgb: (255, 215, 0),
|
||||
high_rgb: (255, 255, 255),
|
||||
},
|
||||
sparkle: SparkleColors {
|
||||
colors: [
|
||||
(255, 255, 0),
|
||||
(255, 255, 255),
|
||||
(255, 215, 0),
|
||||
(255, 255, 95),
|
||||
(255, 255, 0),
|
||||
],
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: white,
|
||||
button_selected_bg: yellow,
|
||||
button_selected_fg: red,
|
||||
},
|
||||
}
|
||||
}
|
||||
283
crates/ratatui/src/theme/kanagawa.rs
Normal file
283
crates/ratatui/src/theme/kanagawa.rs
Normal file
@@ -0,0 +1,283 @@
|
||||
use super::*;
|
||||
use ratatui::style::Color;
|
||||
|
||||
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(228, 104, 118);
|
||||
let sakura_pink = Color::Rgb(210, 126, 153);
|
||||
|
||||
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,
|
||||
content_bg: Color::Rgb(52, 62, 77),
|
||||
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),
|
||||
link_bright: [
|
||||
(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),
|
||||
],
|
||||
},
|
||||
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,
|
||||
},
|
||||
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,
|
||||
muted_bg: Color::Rgb(38, 38, 48),
|
||||
muted_fg: comment,
|
||||
soloed_bg: Color::Rgb(60, 55, 45),
|
||||
soloed_fg: carp_yellow,
|
||||
},
|
||||
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)),
|
||||
user_defined: (sakura_pink, Color::Rgb(55, 40, 50)),
|
||||
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: (228, 104, 118),
|
||||
},
|
||||
sparkle: SparkleColors {
|
||||
colors: [
|
||||
(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,
|
||||
},
|
||||
}
|
||||
}
|
||||
283
crates/ratatui/src/theme/letz_light.rs
Normal file
283
crates/ratatui/src/theme/letz_light.rs
Normal file
@@ -0,0 +1,283 @@
|
||||
use super::*;
|
||||
use ratatui::style::Color;
|
||||
|
||||
pub fn theme() -> ThemeColors {
|
||||
let bg = Color::Rgb(255, 255, 255);
|
||||
let off_white = Color::Rgb(245, 245, 247);
|
||||
let surface = Color::Rgb(235, 235, 240);
|
||||
let border = Color::Rgb(210, 210, 215);
|
||||
let text = Color::Rgb(29, 29, 31);
|
||||
let text_dim = Color::Rgb(110, 110, 115);
|
||||
let text_muted = Color::Rgb(160, 160, 165);
|
||||
|
||||
let keyword = Color::Rgb(173, 61, 164);
|
||||
let string = Color::Rgb(209, 47, 27);
|
||||
let comment = Color::Rgb(112, 127, 52);
|
||||
let number = Color::Rgb(39, 42, 216);
|
||||
let types = Color::Rgb(112, 61, 170);
|
||||
let function = Color::Rgb(62, 128, 135);
|
||||
let preproc = Color::Rgb(120, 73, 42);
|
||||
let accent = Color::Rgb(0, 112, 243);
|
||||
|
||||
ThemeColors {
|
||||
ui: UiColors {
|
||||
bg,
|
||||
bg_rgb: (255, 255, 255),
|
||||
text_primary: text,
|
||||
text_muted: text_dim,
|
||||
text_dim: text_muted,
|
||||
border,
|
||||
header: accent,
|
||||
unfocused: text_muted,
|
||||
accent,
|
||||
surface,
|
||||
},
|
||||
status: StatusColors {
|
||||
playing_bg: Color::Rgb(220, 240, 220),
|
||||
playing_fg: comment,
|
||||
stopped_bg: Color::Rgb(245, 220, 220),
|
||||
stopped_fg: string,
|
||||
fill_on: comment,
|
||||
fill_off: text_muted,
|
||||
fill_bg: surface,
|
||||
},
|
||||
selection: SelectionColors {
|
||||
cursor_bg: accent,
|
||||
cursor_fg: bg,
|
||||
selected_bg: Color::Rgb(200, 220, 250),
|
||||
selected_fg: accent,
|
||||
in_range_bg: Color::Rgb(220, 233, 250),
|
||||
in_range_fg: text,
|
||||
cursor: accent,
|
||||
selected: Color::Rgb(200, 220, 250),
|
||||
in_range: Color::Rgb(220, 233, 250),
|
||||
},
|
||||
tile: TileColors {
|
||||
playing_active_bg: Color::Rgb(250, 225, 210),
|
||||
playing_active_fg: preproc,
|
||||
playing_inactive_bg: Color::Rgb(250, 240, 200),
|
||||
playing_inactive_fg: Color::Rgb(180, 140, 20),
|
||||
active_bg: Color::Rgb(210, 235, 240),
|
||||
active_fg: function,
|
||||
content_bg: Color::Rgb(195, 225, 230),
|
||||
inactive_bg: surface,
|
||||
inactive_fg: text_dim,
|
||||
active_selected_bg: Color::Rgb(210, 215, 245),
|
||||
active_in_range_bg: Color::Rgb(220, 230, 245),
|
||||
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),
|
||||
],
|
||||
},
|
||||
header: HeaderColors {
|
||||
tempo_bg: Color::Rgb(225, 215, 240),
|
||||
tempo_fg: types,
|
||||
bank_bg: Color::Rgb(210, 230, 250),
|
||||
bank_fg: accent,
|
||||
pattern_bg: Color::Rgb(210, 235, 235),
|
||||
pattern_fg: function,
|
||||
stats_bg: surface,
|
||||
stats_fg: text_dim,
|
||||
},
|
||||
modal: ModalColors {
|
||||
border: accent,
|
||||
border_accent: keyword,
|
||||
border_warn: preproc,
|
||||
border_dim: text_muted,
|
||||
confirm: preproc,
|
||||
rename: keyword,
|
||||
input: accent,
|
||||
editor: accent,
|
||||
preview: text_muted,
|
||||
},
|
||||
flash: FlashColors {
|
||||
error_bg: Color::Rgb(250, 215, 215),
|
||||
error_fg: string,
|
||||
success_bg: Color::Rgb(215, 240, 215),
|
||||
success_fg: comment,
|
||||
info_bg: surface,
|
||||
info_fg: text,
|
||||
},
|
||||
list: ListColors {
|
||||
playing_bg: Color::Rgb(215, 240, 220),
|
||||
playing_fg: comment,
|
||||
staged_play_bg: Color::Rgb(230, 215, 240),
|
||||
staged_play_fg: keyword,
|
||||
staged_stop_bg: Color::Rgb(245, 215, 220),
|
||||
staged_stop_fg: string,
|
||||
edit_bg: Color::Rgb(210, 235, 240),
|
||||
edit_fg: function,
|
||||
hover_bg: Color::Rgb(240, 240, 242),
|
||||
hover_fg: text,
|
||||
muted_bg: Color::Rgb(230, 230, 235),
|
||||
muted_fg: text_muted,
|
||||
soloed_bg: Color::Rgb(250, 240, 200),
|
||||
soloed_fg: Color::Rgb(170, 130, 10),
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: string,
|
||||
connected: comment,
|
||||
listening: Color::Rgb(180, 140, 20),
|
||||
},
|
||||
syntax: SyntaxColors {
|
||||
gap_bg: off_white,
|
||||
executed_bg: Color::Rgb(230, 225, 245),
|
||||
selected_bg: Color::Rgb(250, 240, 210),
|
||||
emit: (text, Color::Rgb(250, 220, 215)),
|
||||
number: (number, Color::Rgb(220, 220, 250)),
|
||||
string: (string, Color::Rgb(250, 225, 220)),
|
||||
comment: (Color::Rgb(130, 145, 75), off_white),
|
||||
keyword: (keyword, Color::Rgb(240, 225, 240)),
|
||||
stack_op: (accent, Color::Rgb(220, 235, 250)),
|
||||
operator: (preproc, Color::Rgb(240, 230, 215)),
|
||||
sound: (function, Color::Rgb(215, 240, 240)),
|
||||
param: (types, Color::Rgb(230, 220, 240)),
|
||||
context: (preproc, Color::Rgb(240, 230, 215)),
|
||||
note: (comment, Color::Rgb(225, 240, 220)),
|
||||
interval: (Color::Rgb(90, 110, 40), Color::Rgb(225, 240, 215)),
|
||||
variable: (keyword, Color::Rgb(240, 225, 240)),
|
||||
vary: (Color::Rgb(180, 140, 20), Color::Rgb(250, 240, 210)),
|
||||
generator: (function, Color::Rgb(215, 240, 235)),
|
||||
user_defined: (preproc, Color::Rgb(240, 230, 215)),
|
||||
default: (text_dim, off_white),
|
||||
},
|
||||
table: TableColors {
|
||||
row_even: off_white,
|
||||
row_odd: bg,
|
||||
},
|
||||
values: ValuesColors {
|
||||
tempo: preproc,
|
||||
value: text_dim,
|
||||
},
|
||||
hint: HintColors {
|
||||
key: accent,
|
||||
text: text_muted,
|
||||
},
|
||||
view_badge: ViewBadgeColors { bg: text, fg: bg },
|
||||
nav: NavColors {
|
||||
selected_bg: Color::Rgb(210, 225, 250),
|
||||
selected_fg: text,
|
||||
unselected_bg: surface,
|
||||
unselected_fg: text_muted,
|
||||
},
|
||||
editor_widget: EditorWidgetColors {
|
||||
cursor_bg: text,
|
||||
cursor_fg: bg,
|
||||
selection_bg: Color::Rgb(200, 220, 250),
|
||||
completion_bg: surface,
|
||||
completion_fg: text,
|
||||
completion_selected: accent,
|
||||
completion_example: function,
|
||||
},
|
||||
browser: BrowserColors {
|
||||
directory: accent,
|
||||
project_file: keyword,
|
||||
selected: preproc,
|
||||
file: text,
|
||||
focused_border: accent,
|
||||
unfocused_border: text_muted,
|
||||
root: text,
|
||||
file_icon: text_muted,
|
||||
folder_icon: accent,
|
||||
empty_text: text_muted,
|
||||
},
|
||||
input: InputColors {
|
||||
text: accent,
|
||||
cursor: text,
|
||||
hint: text_muted,
|
||||
},
|
||||
search: SearchColors {
|
||||
active: accent,
|
||||
inactive: text_muted,
|
||||
match_bg: Color::Rgb(255, 230, 80),
|
||||
match_fg: text,
|
||||
},
|
||||
markdown: MarkdownColors {
|
||||
h1: accent,
|
||||
h2: preproc,
|
||||
h3: keyword,
|
||||
code: comment,
|
||||
code_border: Color::Rgb(200, 200, 205),
|
||||
link: function,
|
||||
link_url: Color::Rgb(150, 150, 155),
|
||||
quote: text_muted,
|
||||
text,
|
||||
list: text,
|
||||
},
|
||||
engine: EngineColors {
|
||||
header: accent,
|
||||
header_focused: preproc,
|
||||
divider: Color::Rgb(200, 200, 205),
|
||||
scroll_indicator: Color::Rgb(180, 180, 185),
|
||||
label: Color::Rgb(120, 120, 125),
|
||||
label_focused: Color::Rgb(80, 80, 85),
|
||||
label_dim: Color::Rgb(150, 150, 155),
|
||||
value: Color::Rgb(60, 60, 65),
|
||||
focused: preproc,
|
||||
normal: text,
|
||||
dim: Color::Rgb(180, 180, 185),
|
||||
path: Color::Rgb(120, 120, 125),
|
||||
border_magenta: keyword,
|
||||
border_green: comment,
|
||||
border_cyan: function,
|
||||
separator: Color::Rgb(200, 200, 210),
|
||||
hint_active: preproc,
|
||||
hint_inactive: Color::Rgb(200, 200, 210),
|
||||
},
|
||||
dict: DictColors {
|
||||
word_name: function,
|
||||
word_bg: Color::Rgb(215, 235, 240),
|
||||
alias: text_muted,
|
||||
stack_sig: keyword,
|
||||
description: text,
|
||||
example: Color::Rgb(110, 110, 120),
|
||||
category_focused: preproc,
|
||||
category_selected: accent,
|
||||
category_normal: text,
|
||||
category_dimmed: Color::Rgb(180, 180, 185),
|
||||
border_focused: preproc,
|
||||
border_normal: Color::Rgb(200, 200, 205),
|
||||
header_desc: Color::Rgb(100, 100, 110),
|
||||
},
|
||||
title: TitleColors {
|
||||
big_title: accent,
|
||||
author: types,
|
||||
link: function,
|
||||
license: preproc,
|
||||
prompt: Color::Rgb(100, 100, 110),
|
||||
subtitle: text,
|
||||
},
|
||||
meter: MeterColors {
|
||||
low: comment,
|
||||
mid: Color::Rgb(200, 150, 20),
|
||||
high: string,
|
||||
low_rgb: (112, 127, 52),
|
||||
mid_rgb: (200, 150, 20),
|
||||
high_rgb: (209, 47, 27),
|
||||
},
|
||||
sparkle: SparkleColors {
|
||||
colors: [
|
||||
(0, 112, 243),
|
||||
(173, 61, 164),
|
||||
(112, 127, 52),
|
||||
(62, 128, 135),
|
||||
(120, 73, 42),
|
||||
],
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: accent,
|
||||
button_selected_bg: accent,
|
||||
button_selected_fg: bg,
|
||||
},
|
||||
}
|
||||
}
|
||||
391
crates/ratatui/src/theme/mod.rs
Normal file
391
crates/ratatui/src/theme/mod.rs
Normal file
@@ -0,0 +1,391 @@
|
||||
//! Centralized color definitions for Cagire TUI.
|
||||
//! Supports multiple color schemes with runtime switching.
|
||||
|
||||
mod catppuccin_latte;
|
||||
mod catppuccin_mocha;
|
||||
mod dracula;
|
||||
mod eden;
|
||||
mod ember;
|
||||
mod georges;
|
||||
mod fairyfloss;
|
||||
mod gruvbox_dark;
|
||||
mod hot_dog_stand;
|
||||
mod kanagawa;
|
||||
mod letz_light;
|
||||
mod monochrome_black;
|
||||
mod monochrome_white;
|
||||
mod monokai;
|
||||
mod nord;
|
||||
mod pitch_black;
|
||||
mod rose_pine;
|
||||
mod tokyo_night;
|
||||
pub mod transform;
|
||||
|
||||
use ratatui::style::Color;
|
||||
use std::cell::RefCell;
|
||||
|
||||
pub struct ThemeEntry {
|
||||
pub id: &'static str,
|
||||
pub label: &'static str,
|
||||
pub colors: fn() -> ThemeColors,
|
||||
}
|
||||
|
||||
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: "Fairyfloss", label: "Fairyfloss", colors: fairyfloss::theme },
|
||||
ThemeEntry { id: "HotDogStand", label: "Hot Dog Stand", colors: hot_dog_stand::theme },
|
||||
ThemeEntry { id: "LetzLight", label: "Letz Light", colors: letz_light::theme },
|
||||
ThemeEntry { id: "Ember", label: "Ember", colors: ember::theme },
|
||||
ThemeEntry { id: "Eden", label: "Eden", colors: eden::theme },
|
||||
ThemeEntry { id: "Georges", label: "Georges", colors: georges::theme },
|
||||
];
|
||||
|
||||
thread_local! {
|
||||
static CURRENT_THEME: RefCell<ThemeColors> = RefCell::new((THEMES[0].colors)());
|
||||
}
|
||||
|
||||
pub fn get() -> ThemeColors {
|
||||
CURRENT_THEME.with(|t| t.borrow().clone())
|
||||
}
|
||||
|
||||
pub fn set(theme: ThemeColors) {
|
||||
CURRENT_THEME.with(|t| *t.borrow_mut() = theme);
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ThemeColors {
|
||||
pub ui: UiColors,
|
||||
pub status: StatusColors,
|
||||
pub selection: SelectionColors,
|
||||
pub tile: TileColors,
|
||||
pub header: HeaderColors,
|
||||
pub modal: ModalColors,
|
||||
pub flash: FlashColors,
|
||||
pub list: ListColors,
|
||||
pub link_status: LinkStatusColors,
|
||||
pub syntax: SyntaxColors,
|
||||
pub table: TableColors,
|
||||
pub values: ValuesColors,
|
||||
pub hint: HintColors,
|
||||
pub view_badge: ViewBadgeColors,
|
||||
pub nav: NavColors,
|
||||
pub editor_widget: EditorWidgetColors,
|
||||
pub browser: BrowserColors,
|
||||
pub input: InputColors,
|
||||
pub search: SearchColors,
|
||||
pub markdown: MarkdownColors,
|
||||
pub engine: EngineColors,
|
||||
pub dict: DictColors,
|
||||
pub title: TitleColors,
|
||||
pub meter: MeterColors,
|
||||
pub sparkle: SparkleColors,
|
||||
pub confirm: ConfirmColors,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UiColors {
|
||||
pub bg: Color,
|
||||
pub bg_rgb: (u8, u8, u8),
|
||||
pub text_primary: Color,
|
||||
pub text_muted: Color,
|
||||
pub text_dim: Color,
|
||||
pub border: Color,
|
||||
pub header: Color,
|
||||
pub unfocused: Color,
|
||||
pub accent: Color,
|
||||
pub surface: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct StatusColors {
|
||||
pub playing_bg: Color,
|
||||
pub playing_fg: Color,
|
||||
pub stopped_bg: Color,
|
||||
pub stopped_fg: Color,
|
||||
pub fill_on: Color,
|
||||
pub fill_off: Color,
|
||||
pub fill_bg: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SelectionColors {
|
||||
pub cursor_bg: Color,
|
||||
pub cursor_fg: Color,
|
||||
pub selected_bg: Color,
|
||||
pub selected_fg: Color,
|
||||
pub in_range_bg: Color,
|
||||
pub in_range_fg: Color,
|
||||
pub cursor: Color,
|
||||
pub selected: Color,
|
||||
pub in_range: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TileColors {
|
||||
pub playing_active_bg: Color,
|
||||
pub playing_active_fg: Color,
|
||||
pub playing_inactive_bg: Color,
|
||||
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,
|
||||
pub active_in_range_bg: Color,
|
||||
pub link_bright: [(u8, u8, u8); 5],
|
||||
pub link_dim: [(u8, u8, u8); 5],
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HeaderColors {
|
||||
pub tempo_bg: Color,
|
||||
pub tempo_fg: Color,
|
||||
pub bank_bg: Color,
|
||||
pub bank_fg: Color,
|
||||
pub pattern_bg: Color,
|
||||
pub pattern_fg: Color,
|
||||
pub stats_bg: Color,
|
||||
pub stats_fg: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ModalColors {
|
||||
pub border: Color,
|
||||
pub border_accent: Color,
|
||||
pub border_warn: Color,
|
||||
pub border_dim: Color,
|
||||
pub confirm: Color,
|
||||
pub rename: Color,
|
||||
pub input: Color,
|
||||
pub editor: Color,
|
||||
pub preview: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FlashColors {
|
||||
pub error_bg: Color,
|
||||
pub error_fg: Color,
|
||||
pub success_bg: Color,
|
||||
pub success_fg: Color,
|
||||
pub info_bg: Color,
|
||||
pub info_fg: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ListColors {
|
||||
pub playing_bg: Color,
|
||||
pub playing_fg: Color,
|
||||
pub staged_play_bg: Color,
|
||||
pub staged_play_fg: Color,
|
||||
pub staged_stop_bg: Color,
|
||||
pub staged_stop_fg: Color,
|
||||
pub edit_bg: Color,
|
||||
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,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LinkStatusColors {
|
||||
pub disabled: Color,
|
||||
pub connected: Color,
|
||||
pub listening: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SyntaxColors {
|
||||
pub gap_bg: Color,
|
||||
pub executed_bg: Color,
|
||||
pub selected_bg: Color,
|
||||
pub emit: (Color, Color),
|
||||
pub number: (Color, Color),
|
||||
pub string: (Color, Color),
|
||||
pub comment: (Color, Color),
|
||||
pub keyword: (Color, Color),
|
||||
pub stack_op: (Color, Color),
|
||||
pub operator: (Color, Color),
|
||||
pub sound: (Color, Color),
|
||||
pub param: (Color, Color),
|
||||
pub context: (Color, Color),
|
||||
pub note: (Color, Color),
|
||||
pub interval: (Color, Color),
|
||||
pub variable: (Color, Color),
|
||||
pub vary: (Color, Color),
|
||||
pub generator: (Color, Color),
|
||||
pub user_defined: (Color, Color),
|
||||
pub default: (Color, Color),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TableColors {
|
||||
pub row_even: Color,
|
||||
pub row_odd: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ValuesColors {
|
||||
pub tempo: Color,
|
||||
pub value: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HintColors {
|
||||
pub key: Color,
|
||||
pub text: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ViewBadgeColors {
|
||||
pub bg: Color,
|
||||
pub fg: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NavColors {
|
||||
pub selected_bg: Color,
|
||||
pub selected_fg: Color,
|
||||
pub unselected_bg: Color,
|
||||
pub unselected_fg: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EditorWidgetColors {
|
||||
pub cursor_bg: Color,
|
||||
pub cursor_fg: Color,
|
||||
pub selection_bg: Color,
|
||||
pub completion_bg: Color,
|
||||
pub completion_fg: Color,
|
||||
pub completion_selected: Color,
|
||||
pub completion_example: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BrowserColors {
|
||||
pub directory: Color,
|
||||
pub project_file: Color,
|
||||
pub selected: Color,
|
||||
pub file: Color,
|
||||
pub focused_border: Color,
|
||||
pub unfocused_border: Color,
|
||||
pub root: Color,
|
||||
pub file_icon: Color,
|
||||
pub folder_icon: Color,
|
||||
pub empty_text: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct InputColors {
|
||||
pub text: Color,
|
||||
pub cursor: Color,
|
||||
pub hint: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SearchColors {
|
||||
pub active: Color,
|
||||
pub inactive: Color,
|
||||
pub match_bg: Color,
|
||||
pub match_fg: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MarkdownColors {
|
||||
pub h1: Color,
|
||||
pub h2: Color,
|
||||
pub h3: Color,
|
||||
pub code: Color,
|
||||
pub code_border: Color,
|
||||
pub link: Color,
|
||||
pub link_url: Color,
|
||||
pub quote: Color,
|
||||
pub text: Color,
|
||||
pub list: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EngineColors {
|
||||
pub header: Color,
|
||||
pub header_focused: Color,
|
||||
pub divider: Color,
|
||||
pub scroll_indicator: Color,
|
||||
pub label: Color,
|
||||
pub label_focused: Color,
|
||||
pub label_dim: Color,
|
||||
pub value: Color,
|
||||
pub focused: Color,
|
||||
pub normal: Color,
|
||||
pub dim: Color,
|
||||
pub path: Color,
|
||||
pub border_magenta: Color,
|
||||
pub border_green: Color,
|
||||
pub border_cyan: Color,
|
||||
pub separator: Color,
|
||||
pub hint_active: Color,
|
||||
pub hint_inactive: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DictColors {
|
||||
pub word_name: Color,
|
||||
pub word_bg: Color,
|
||||
pub alias: Color,
|
||||
pub stack_sig: Color,
|
||||
pub description: Color,
|
||||
pub example: Color,
|
||||
pub category_focused: Color,
|
||||
pub category_selected: Color,
|
||||
pub category_normal: Color,
|
||||
pub category_dimmed: Color,
|
||||
pub border_focused: Color,
|
||||
pub border_normal: Color,
|
||||
pub header_desc: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TitleColors {
|
||||
pub big_title: Color,
|
||||
pub author: Color,
|
||||
pub link: Color,
|
||||
pub license: Color,
|
||||
pub prompt: Color,
|
||||
pub subtitle: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MeterColors {
|
||||
pub low: Color,
|
||||
pub mid: Color,
|
||||
pub high: Color,
|
||||
pub low_rgb: (u8, u8, u8),
|
||||
pub mid_rgb: (u8, u8, u8),
|
||||
pub high_rgb: (u8, u8, u8),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SparkleColors {
|
||||
pub colors: [(u8, u8, u8); 5],
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ConfirmColors {
|
||||
pub border: Color,
|
||||
pub button_selected_bg: Color,
|
||||
pub button_selected_fg: Color,
|
||||
}
|
||||
|
||||
280
crates/ratatui/src/theme/monochrome_black.rs
Normal file
280
crates/ratatui/src/theme/monochrome_black.rs
Normal file
@@ -0,0 +1,280 @@
|
||||
use super::*;
|
||||
use ratatui::style::Color;
|
||||
|
||||
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);
|
||||
|
||||
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,
|
||||
content_bg: Color::Rgb(55, 55, 55),
|
||||
inactive_bg: surface,
|
||||
inactive_fg: fg_dim,
|
||||
active_selected_bg: Color::Rgb(80, 80, 80),
|
||||
active_in_range_bg: Color::Rgb(55, 55, 55),
|
||||
link_bright: [
|
||||
(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),
|
||||
],
|
||||
},
|
||||
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,
|
||||
},
|
||||
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,
|
||||
muted_bg: Color::Rgb(22, 22, 22),
|
||||
muted_fg: dark,
|
||||
soloed_bg: Color::Rgb(60, 60, 60),
|
||||
soloed_fg: bright,
|
||||
},
|
||||
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)),
|
||||
user_defined: (medium, Color::Rgb(35, 35, 35)),
|
||||
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),
|
||||
],
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: bright,
|
||||
button_selected_bg: bright,
|
||||
button_selected_fg: bg,
|
||||
},
|
||||
}
|
||||
}
|
||||
280
crates/ratatui/src/theme/monochrome_white.rs
Normal file
280
crates/ratatui/src/theme/monochrome_white.rs
Normal file
@@ -0,0 +1,280 @@
|
||||
use super::*;
|
||||
use ratatui::style::Color;
|
||||
|
||||
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);
|
||||
|
||||
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,
|
||||
content_bg: Color::Rgb(195, 195, 195),
|
||||
inactive_bg: surface,
|
||||
inactive_fg: fg_dim,
|
||||
active_selected_bg: Color::Rgb(170, 170, 170),
|
||||
active_in_range_bg: Color::Rgb(195, 195, 195),
|
||||
link_bright: [
|
||||
(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),
|
||||
],
|
||||
},
|
||||
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,
|
||||
},
|
||||
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,
|
||||
muted_bg: Color::Rgb(235, 235, 235),
|
||||
muted_fg: light,
|
||||
soloed_bg: Color::Rgb(190, 190, 190),
|
||||
soloed_fg: dark,
|
||||
},
|
||||
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)),
|
||||
user_defined: (medium, Color::Rgb(225, 225, 225)),
|
||||
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),
|
||||
],
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: dark,
|
||||
button_selected_bg: dark,
|
||||
button_selected_fg: bg,
|
||||
},
|
||||
}
|
||||
}
|
||||
281
crates/ratatui/src/theme/monokai.rs
Normal file
281
crates/ratatui/src/theme/monokai.rs
Normal file
@@ -0,0 +1,281 @@
|
||||
use super::*;
|
||||
use ratatui::style::Color;
|
||||
|
||||
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);
|
||||
|
||||
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,
|
||||
content_bg: Color::Rgb(62, 82, 77),
|
||||
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),
|
||||
link_bright: [
|
||||
(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),
|
||||
],
|
||||
},
|
||||
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,
|
||||
},
|
||||
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,
|
||||
muted_bg: Color::Rgb(48, 50, 45),
|
||||
muted_fg: comment,
|
||||
soloed_bg: Color::Rgb(70, 65, 45),
|
||||
soloed_fg: yellow,
|
||||
},
|
||||
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)),
|
||||
user_defined: (orange, Color::Rgb(60, 50, 30)),
|
||||
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),
|
||||
],
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: orange,
|
||||
button_selected_bg: orange,
|
||||
button_selected_fg: bg,
|
||||
},
|
||||
}
|
||||
}
|
||||
284
crates/ratatui/src/theme/nord.rs
Normal file
284
crates/ratatui/src/theme/nord.rs
Normal file
@@ -0,0 +1,284 @@
|
||||
use super::*;
|
||||
use ratatui::style::Color;
|
||||
|
||||
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);
|
||||
|
||||
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,
|
||||
content_bg: Color::Rgb(57, 72, 72),
|
||||
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),
|
||||
link_bright: [
|
||||
(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),
|
||||
],
|
||||
},
|
||||
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,
|
||||
},
|
||||
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,
|
||||
muted_bg: Color::Rgb(55, 60, 70),
|
||||
muted_fg: polar_night3,
|
||||
soloed_bg: Color::Rgb(70, 65, 50),
|
||||
soloed_fg: aurora_yellow,
|
||||
},
|
||||
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)),
|
||||
user_defined: (aurora_orange, Color::Rgb(60, 50, 45)),
|
||||
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),
|
||||
],
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: aurora_orange,
|
||||
button_selected_bg: aurora_orange,
|
||||
button_selected_fg: polar_night0,
|
||||
},
|
||||
}
|
||||
}
|
||||
282
crates/ratatui/src/theme/pitch_black.rs
Normal file
282
crates/ratatui/src/theme/pitch_black.rs
Normal file
@@ -0,0 +1,282 @@
|
||||
use super::*;
|
||||
use ratatui::style::Color;
|
||||
|
||||
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);
|
||||
|
||||
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,
|
||||
content_bg: Color::Rgb(22, 47, 47),
|
||||
inactive_bg: surface,
|
||||
inactive_fg: fg_dim,
|
||||
active_selected_bg: Color::Rgb(45, 40, 55),
|
||||
active_in_range_bg: Color::Rgb(30, 35, 45),
|
||||
link_bright: [
|
||||
(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),
|
||||
],
|
||||
},
|
||||
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,
|
||||
},
|
||||
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,
|
||||
muted_bg: Color::Rgb(15, 15, 15),
|
||||
muted_fg: fg_muted,
|
||||
soloed_bg: Color::Rgb(45, 40, 15),
|
||||
soloed_fg: yellow,
|
||||
},
|
||||
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)),
|
||||
user_defined: (orange, Color::Rgb(40, 25, 10)),
|
||||
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),
|
||||
],
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: orange,
|
||||
button_selected_bg: orange,
|
||||
button_selected_fg: bg,
|
||||
},
|
||||
}
|
||||
}
|
||||
282
crates/ratatui/src/theme/rose_pine.rs
Normal file
282
crates/ratatui/src/theme/rose_pine.rs
Normal file
@@ -0,0 +1,282 @@
|
||||
use super::*;
|
||||
use ratatui::style::Color;
|
||||
|
||||
pub fn theme() -> ThemeColors {
|
||||
let bg = Color::Rgb(25, 23, 36);
|
||||
let bg_light = Color::Rgb(33, 32, 46);
|
||||
let bg_lighter = Color::Rgb(42, 39, 63);
|
||||
let fg = Color::Rgb(224, 222, 244);
|
||||
let fg_dim = Color::Rgb(144, 140, 170);
|
||||
let muted = Color::Rgb(110, 106, 134);
|
||||
let rose = Color::Rgb(235, 188, 186);
|
||||
let gold = Color::Rgb(246, 193, 119);
|
||||
let foam = Color::Rgb(156, 207, 216);
|
||||
let iris = Color::Rgb(196, 167, 231);
|
||||
let pine = Color::Rgb(49, 116, 143);
|
||||
let subtle = Color::Rgb(235, 188, 186);
|
||||
let love = Color::Rgb(235, 111, 146);
|
||||
|
||||
let darker_bg = Color::Rgb(21, 19, 30);
|
||||
|
||||
ThemeColors {
|
||||
ui: UiColors {
|
||||
bg,
|
||||
bg_rgb: (25, 23, 36),
|
||||
text_primary: fg,
|
||||
text_muted: fg_dim,
|
||||
text_dim: muted,
|
||||
border: bg_lighter,
|
||||
header: foam,
|
||||
unfocused: muted,
|
||||
accent: rose,
|
||||
surface: bg_light,
|
||||
},
|
||||
status: StatusColors {
|
||||
playing_bg: Color::Rgb(35, 50, 55),
|
||||
playing_fg: foam,
|
||||
stopped_bg: Color::Rgb(55, 40, 50),
|
||||
stopped_fg: love,
|
||||
fill_on: foam,
|
||||
fill_off: muted,
|
||||
fill_bg: bg_light,
|
||||
},
|
||||
selection: SelectionColors {
|
||||
cursor_bg: rose,
|
||||
cursor_fg: bg,
|
||||
selected_bg: Color::Rgb(60, 50, 70),
|
||||
selected_fg: rose,
|
||||
in_range_bg: Color::Rgb(50, 45, 60),
|
||||
in_range_fg: fg,
|
||||
cursor: rose,
|
||||
selected: Color::Rgb(60, 50, 70),
|
||||
in_range: Color::Rgb(50, 45, 60),
|
||||
},
|
||||
tile: TileColors {
|
||||
playing_active_bg: Color::Rgb(65, 55, 50),
|
||||
playing_active_fg: gold,
|
||||
playing_inactive_bg: Color::Rgb(55, 55, 55),
|
||||
playing_inactive_fg: subtle,
|
||||
active_bg: Color::Rgb(35, 50, 60),
|
||||
active_fg: foam,
|
||||
content_bg: Color::Rgb(42, 57, 67),
|
||||
inactive_bg: bg_light,
|
||||
inactive_fg: fg_dim,
|
||||
active_selected_bg: Color::Rgb(60, 50, 70),
|
||||
active_in_range_bg: Color::Rgb(50, 45, 60),
|
||||
link_bright: [
|
||||
(235, 111, 146),
|
||||
(196, 167, 231),
|
||||
(246, 193, 119),
|
||||
(156, 207, 216),
|
||||
(49, 116, 143),
|
||||
],
|
||||
link_dim: [
|
||||
(75, 45, 55),
|
||||
(60, 50, 75),
|
||||
(75, 60, 45),
|
||||
(50, 65, 70),
|
||||
(30, 50, 55),
|
||||
],
|
||||
},
|
||||
header: HeaderColors {
|
||||
tempo_bg: Color::Rgb(60, 45, 60),
|
||||
tempo_fg: iris,
|
||||
bank_bg: Color::Rgb(35, 50, 60),
|
||||
bank_fg: foam,
|
||||
pattern_bg: Color::Rgb(35, 55, 60),
|
||||
pattern_fg: pine,
|
||||
stats_bg: bg_light,
|
||||
stats_fg: fg_dim,
|
||||
},
|
||||
modal: ModalColors {
|
||||
border: foam,
|
||||
border_accent: rose,
|
||||
border_warn: gold,
|
||||
border_dim: muted,
|
||||
confirm: gold,
|
||||
rename: iris,
|
||||
input: foam,
|
||||
editor: foam,
|
||||
preview: muted,
|
||||
},
|
||||
flash: FlashColors {
|
||||
error_bg: Color::Rgb(60, 40, 50),
|
||||
error_fg: love,
|
||||
success_bg: Color::Rgb(35, 55, 55),
|
||||
success_fg: foam,
|
||||
info_bg: bg_light,
|
||||
info_fg: fg,
|
||||
},
|
||||
list: ListColors {
|
||||
playing_bg: Color::Rgb(35, 55, 55),
|
||||
playing_fg: foam,
|
||||
staged_play_bg: Color::Rgb(55, 50, 70),
|
||||
staged_play_fg: iris,
|
||||
staged_stop_bg: Color::Rgb(60, 45, 55),
|
||||
staged_stop_fg: love,
|
||||
edit_bg: Color::Rgb(35, 50, 60),
|
||||
edit_fg: foam,
|
||||
hover_bg: bg_lighter,
|
||||
hover_fg: fg,
|
||||
muted_bg: Color::Rgb(32, 30, 42),
|
||||
muted_fg: muted,
|
||||
soloed_bg: Color::Rgb(60, 50, 40),
|
||||
soloed_fg: gold,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: love,
|
||||
connected: foam,
|
||||
listening: gold,
|
||||
},
|
||||
syntax: SyntaxColors {
|
||||
gap_bg: darker_bg,
|
||||
executed_bg: Color::Rgb(40, 40, 55),
|
||||
selected_bg: Color::Rgb(65, 55, 50),
|
||||
emit: (fg, Color::Rgb(60, 45, 60)),
|
||||
number: (iris, Color::Rgb(55, 50, 70)),
|
||||
string: (gold, Color::Rgb(65, 55, 45)),
|
||||
comment: (muted, darker_bg),
|
||||
keyword: (rose, Color::Rgb(60, 45, 55)),
|
||||
stack_op: (foam, Color::Rgb(40, 55, 60)),
|
||||
operator: (love, Color::Rgb(60, 45, 55)),
|
||||
sound: (foam, Color::Rgb(40, 55, 60)),
|
||||
param: (gold, Color::Rgb(65, 55, 45)),
|
||||
context: (gold, Color::Rgb(65, 55, 45)),
|
||||
note: (pine, Color::Rgb(35, 50, 55)),
|
||||
interval: (Color::Rgb(100, 160, 180), Color::Rgb(35, 55, 60)),
|
||||
variable: (pine, Color::Rgb(35, 50, 55)),
|
||||
vary: (subtle, Color::Rgb(60, 55, 55)),
|
||||
generator: (foam, Color::Rgb(40, 55, 60)),
|
||||
user_defined: (love, Color::Rgb(55, 35, 45)),
|
||||
default: (fg_dim, darker_bg),
|
||||
},
|
||||
table: TableColors {
|
||||
row_even: darker_bg,
|
||||
row_odd: bg,
|
||||
},
|
||||
values: ValuesColors {
|
||||
tempo: gold,
|
||||
value: fg_dim,
|
||||
},
|
||||
hint: HintColors {
|
||||
key: gold,
|
||||
text: muted,
|
||||
},
|
||||
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: muted,
|
||||
},
|
||||
editor_widget: EditorWidgetColors {
|
||||
cursor_bg: fg,
|
||||
cursor_fg: bg,
|
||||
selection_bg: Color::Rgb(55, 50, 70),
|
||||
completion_bg: bg_light,
|
||||
completion_fg: fg,
|
||||
completion_selected: gold,
|
||||
completion_example: foam,
|
||||
},
|
||||
browser: BrowserColors {
|
||||
directory: foam,
|
||||
project_file: iris,
|
||||
selected: gold,
|
||||
file: fg,
|
||||
focused_border: gold,
|
||||
unfocused_border: muted,
|
||||
root: fg,
|
||||
file_icon: muted,
|
||||
folder_icon: foam,
|
||||
empty_text: muted,
|
||||
},
|
||||
input: InputColors {
|
||||
text: foam,
|
||||
cursor: fg,
|
||||
hint: muted,
|
||||
},
|
||||
search: SearchColors {
|
||||
active: gold,
|
||||
inactive: muted,
|
||||
match_bg: gold,
|
||||
match_fg: bg,
|
||||
},
|
||||
markdown: MarkdownColors {
|
||||
h1: foam,
|
||||
h2: gold,
|
||||
h3: iris,
|
||||
code: pine,
|
||||
code_border: Color::Rgb(60, 55, 75),
|
||||
link: rose,
|
||||
link_url: Color::Rgb(100, 95, 120),
|
||||
quote: muted,
|
||||
text: fg,
|
||||
list: fg,
|
||||
},
|
||||
engine: EngineColors {
|
||||
header: foam,
|
||||
header_focused: gold,
|
||||
divider: Color::Rgb(55, 52, 70),
|
||||
scroll_indicator: Color::Rgb(70, 65, 90),
|
||||
label: Color::Rgb(130, 125, 155),
|
||||
label_focused: Color::Rgb(160, 155, 185),
|
||||
label_dim: Color::Rgb(100, 95, 125),
|
||||
value: Color::Rgb(200, 195, 220),
|
||||
focused: gold,
|
||||
normal: fg,
|
||||
dim: Color::Rgb(70, 65, 90),
|
||||
path: Color::Rgb(130, 125, 155),
|
||||
border_magenta: iris,
|
||||
border_green: foam,
|
||||
border_cyan: pine,
|
||||
separator: Color::Rgb(55, 52, 70),
|
||||
hint_active: Color::Rgb(230, 180, 110),
|
||||
hint_inactive: Color::Rgb(55, 52, 70),
|
||||
},
|
||||
dict: DictColors {
|
||||
word_name: pine,
|
||||
word_bg: Color::Rgb(40, 50, 55),
|
||||
alias: muted,
|
||||
stack_sig: iris,
|
||||
description: fg,
|
||||
example: Color::Rgb(130, 125, 155),
|
||||
category_focused: gold,
|
||||
category_selected: foam,
|
||||
category_normal: fg,
|
||||
category_dimmed: Color::Rgb(70, 65, 90),
|
||||
border_focused: gold,
|
||||
border_normal: Color::Rgb(55, 52, 70),
|
||||
header_desc: Color::Rgb(150, 145, 175),
|
||||
},
|
||||
title: TitleColors {
|
||||
big_title: rose,
|
||||
author: foam,
|
||||
link: pine,
|
||||
license: gold,
|
||||
prompt: Color::Rgb(150, 145, 175),
|
||||
subtitle: fg,
|
||||
},
|
||||
meter: MeterColors {
|
||||
low: foam,
|
||||
mid: gold,
|
||||
high: love,
|
||||
low_rgb: (156, 207, 216),
|
||||
mid_rgb: (246, 193, 119),
|
||||
high_rgb: (235, 111, 146),
|
||||
},
|
||||
sparkle: SparkleColors {
|
||||
colors: [
|
||||
(156, 207, 216),
|
||||
(246, 193, 119),
|
||||
(49, 116, 143),
|
||||
(235, 111, 146),
|
||||
(196, 167, 231),
|
||||
],
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: gold,
|
||||
button_selected_bg: gold,
|
||||
button_selected_fg: bg,
|
||||
},
|
||||
}
|
||||
}
|
||||
282
crates/ratatui/src/theme/tokyo_night.rs
Normal file
282
crates/ratatui/src/theme/tokyo_night.rs
Normal file
@@ -0,0 +1,282 @@
|
||||
use super::*;
|
||||
use ratatui::style::Color;
|
||||
|
||||
pub fn theme() -> ThemeColors {
|
||||
let bg = Color::Rgb(26, 27, 38);
|
||||
let bg_light = Color::Rgb(36, 40, 59);
|
||||
let bg_lighter = Color::Rgb(52, 59, 88);
|
||||
let fg = Color::Rgb(169, 177, 214);
|
||||
let fg_dim = Color::Rgb(130, 140, 180);
|
||||
let comment = Color::Rgb(86, 95, 137);
|
||||
let blue = Color::Rgb(122, 162, 247);
|
||||
let purple = Color::Rgb(187, 154, 247);
|
||||
let green = Color::Rgb(158, 206, 106);
|
||||
let red = Color::Rgb(247, 118, 142);
|
||||
let orange = Color::Rgb(224, 175, 104);
|
||||
let cyan = Color::Rgb(125, 207, 255);
|
||||
let yellow = Color::Rgb(224, 175, 104);
|
||||
|
||||
let darker_bg = Color::Rgb(22, 23, 32);
|
||||
|
||||
ThemeColors {
|
||||
ui: UiColors {
|
||||
bg,
|
||||
bg_rgb: (26, 27, 38),
|
||||
text_primary: fg,
|
||||
text_muted: fg_dim,
|
||||
text_dim: comment,
|
||||
border: bg_lighter,
|
||||
header: blue,
|
||||
unfocused: comment,
|
||||
accent: purple,
|
||||
surface: bg_light,
|
||||
},
|
||||
status: StatusColors {
|
||||
playing_bg: Color::Rgb(45, 60, 50),
|
||||
playing_fg: green,
|
||||
stopped_bg: Color::Rgb(60, 40, 50),
|
||||
stopped_fg: red,
|
||||
fill_on: green,
|
||||
fill_off: comment,
|
||||
fill_bg: bg_light,
|
||||
},
|
||||
selection: SelectionColors {
|
||||
cursor_bg: purple,
|
||||
cursor_fg: bg,
|
||||
selected_bg: Color::Rgb(70, 60, 90),
|
||||
selected_fg: purple,
|
||||
in_range_bg: Color::Rgb(55, 55, 75),
|
||||
in_range_fg: fg,
|
||||
cursor: purple,
|
||||
selected: Color::Rgb(70, 60, 90),
|
||||
in_range: Color::Rgb(55, 55, 75),
|
||||
},
|
||||
tile: TileColors {
|
||||
playing_active_bg: Color::Rgb(70, 60, 45),
|
||||
playing_active_fg: orange,
|
||||
playing_inactive_bg: Color::Rgb(60, 60, 50),
|
||||
playing_inactive_fg: yellow,
|
||||
active_bg: Color::Rgb(45, 60, 75),
|
||||
active_fg: blue,
|
||||
content_bg: Color::Rgb(52, 67, 82),
|
||||
inactive_bg: bg_light,
|
||||
inactive_fg: fg_dim,
|
||||
active_selected_bg: Color::Rgb(70, 55, 85),
|
||||
active_in_range_bg: Color::Rgb(55, 55, 75),
|
||||
link_bright: [
|
||||
(247, 118, 142),
|
||||
(187, 154, 247),
|
||||
(224, 175, 104),
|
||||
(125, 207, 255),
|
||||
(158, 206, 106),
|
||||
],
|
||||
link_dim: [
|
||||
(80, 45, 55),
|
||||
(65, 55, 85),
|
||||
(75, 60, 40),
|
||||
(45, 70, 85),
|
||||
(55, 70, 45),
|
||||
],
|
||||
},
|
||||
header: HeaderColors {
|
||||
tempo_bg: Color::Rgb(65, 50, 70),
|
||||
tempo_fg: purple,
|
||||
bank_bg: Color::Rgb(45, 55, 75),
|
||||
bank_fg: blue,
|
||||
pattern_bg: Color::Rgb(50, 65, 50),
|
||||
pattern_fg: green,
|
||||
stats_bg: bg_light,
|
||||
stats_fg: fg_dim,
|
||||
},
|
||||
modal: ModalColors {
|
||||
border: blue,
|
||||
border_accent: purple,
|
||||
border_warn: orange,
|
||||
border_dim: comment,
|
||||
confirm: orange,
|
||||
rename: purple,
|
||||
input: blue,
|
||||
editor: blue,
|
||||
preview: comment,
|
||||
},
|
||||
flash: FlashColors {
|
||||
error_bg: Color::Rgb(65, 40, 50),
|
||||
error_fg: red,
|
||||
success_bg: Color::Rgb(45, 60, 45),
|
||||
success_fg: green,
|
||||
info_bg: bg_light,
|
||||
info_fg: fg,
|
||||
},
|
||||
list: ListColors {
|
||||
playing_bg: Color::Rgb(45, 60, 45),
|
||||
playing_fg: green,
|
||||
staged_play_bg: Color::Rgb(60, 50, 75),
|
||||
staged_play_fg: purple,
|
||||
staged_stop_bg: Color::Rgb(70, 45, 55),
|
||||
staged_stop_fg: red,
|
||||
edit_bg: Color::Rgb(45, 55, 70),
|
||||
edit_fg: blue,
|
||||
hover_bg: bg_lighter,
|
||||
hover_fg: fg,
|
||||
muted_bg: Color::Rgb(35, 38, 50),
|
||||
muted_fg: comment,
|
||||
soloed_bg: Color::Rgb(60, 55, 40),
|
||||
soloed_fg: yellow,
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: red,
|
||||
connected: green,
|
||||
listening: yellow,
|
||||
},
|
||||
syntax: SyntaxColors {
|
||||
gap_bg: darker_bg,
|
||||
executed_bg: Color::Rgb(45, 45, 60),
|
||||
selected_bg: Color::Rgb(70, 60, 50),
|
||||
emit: (fg, Color::Rgb(70, 50, 65)),
|
||||
number: (purple, Color::Rgb(55, 50, 70)),
|
||||
string: (green, Color::Rgb(50, 60, 50)),
|
||||
comment: (comment, darker_bg),
|
||||
keyword: (purple, Color::Rgb(60, 50, 70)),
|
||||
stack_op: (cyan, Color::Rgb(45, 60, 75)),
|
||||
operator: (red, Color::Rgb(65, 45, 55)),
|
||||
sound: (blue, Color::Rgb(45, 55, 70)),
|
||||
param: (orange, Color::Rgb(70, 55, 45)),
|
||||
context: (orange, Color::Rgb(70, 55, 45)),
|
||||
note: (green, Color::Rgb(50, 60, 45)),
|
||||
interval: (Color::Rgb(180, 220, 130), Color::Rgb(50, 65, 45)),
|
||||
variable: (green, Color::Rgb(50, 60, 45)),
|
||||
vary: (yellow, Color::Rgb(70, 60, 45)),
|
||||
generator: (cyan, Color::Rgb(45, 60, 75)),
|
||||
user_defined: (orange, Color::Rgb(60, 50, 35)),
|
||||
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(65, 55, 80),
|
||||
selected_fg: fg,
|
||||
unselected_bg: bg_light,
|
||||
unselected_fg: comment,
|
||||
},
|
||||
editor_widget: EditorWidgetColors {
|
||||
cursor_bg: fg,
|
||||
cursor_fg: bg,
|
||||
selection_bg: Color::Rgb(60, 60, 80),
|
||||
completion_bg: bg_light,
|
||||
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: 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(70, 75, 95),
|
||||
link: red,
|
||||
link_url: Color::Rgb(110, 120, 160),
|
||||
quote: comment,
|
||||
text: fg,
|
||||
list: fg,
|
||||
},
|
||||
engine: EngineColors {
|
||||
header: blue,
|
||||
header_focused: yellow,
|
||||
divider: Color::Rgb(65, 70, 90),
|
||||
scroll_indicator: Color::Rgb(80, 85, 110),
|
||||
label: Color::Rgb(130, 140, 175),
|
||||
label_focused: Color::Rgb(160, 170, 200),
|
||||
label_dim: Color::Rgb(100, 110, 145),
|
||||
value: Color::Rgb(190, 195, 220),
|
||||
focused: yellow,
|
||||
normal: fg,
|
||||
dim: Color::Rgb(80, 85, 110),
|
||||
path: Color::Rgb(130, 140, 175),
|
||||
border_magenta: purple,
|
||||
border_green: green,
|
||||
border_cyan: cyan,
|
||||
separator: Color::Rgb(65, 70, 90),
|
||||
hint_active: Color::Rgb(210, 180, 100),
|
||||
hint_inactive: Color::Rgb(65, 70, 90),
|
||||
},
|
||||
dict: DictColors {
|
||||
word_name: green,
|
||||
word_bg: Color::Rgb(45, 55, 60),
|
||||
alias: comment,
|
||||
stack_sig: purple,
|
||||
description: fg,
|
||||
example: Color::Rgb(130, 140, 175),
|
||||
category_focused: yellow,
|
||||
category_selected: blue,
|
||||
category_normal: fg,
|
||||
category_dimmed: Color::Rgb(80, 85, 110),
|
||||
border_focused: yellow,
|
||||
border_normal: Color::Rgb(65, 70, 90),
|
||||
header_desc: Color::Rgb(150, 160, 190),
|
||||
},
|
||||
title: TitleColors {
|
||||
big_title: purple,
|
||||
author: blue,
|
||||
link: green,
|
||||
license: orange,
|
||||
prompt: Color::Rgb(150, 160, 190),
|
||||
subtitle: fg,
|
||||
},
|
||||
meter: MeterColors {
|
||||
low: green,
|
||||
mid: yellow,
|
||||
high: red,
|
||||
low_rgb: (158, 206, 106),
|
||||
mid_rgb: (224, 175, 104),
|
||||
high_rgb: (247, 118, 142),
|
||||
},
|
||||
sparkle: SparkleColors {
|
||||
colors: [
|
||||
(125, 207, 255),
|
||||
(224, 175, 104),
|
||||
(158, 206, 106),
|
||||
(247, 118, 142),
|
||||
(187, 154, 247),
|
||||
],
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: orange,
|
||||
button_selected_bg: orange,
|
||||
button_selected_fg: bg,
|
||||
},
|
||||
}
|
||||
}
|
||||
350
crates/ratatui/src/theme/transform.rs
Normal file
350
crates/ratatui/src/theme/transform.rs
Normal file
@@ -0,0 +1,350 @@
|
||||
use ratatui::style::Color;
|
||||
use super::*;
|
||||
|
||||
fn rgb_to_hsv(r: u8, g: u8, b: u8) -> (f32, f32, f32) {
|
||||
let r = r as f32 / 255.0;
|
||||
let g = g as f32 / 255.0;
|
||||
let b = b as f32 / 255.0;
|
||||
|
||||
let max = r.max(g).max(b);
|
||||
let min = r.min(g).min(b);
|
||||
let delta = max - min;
|
||||
|
||||
let h = if delta == 0.0 {
|
||||
0.0
|
||||
} else if max == r {
|
||||
60.0 * (((g - b) / delta) % 6.0)
|
||||
} else if max == g {
|
||||
60.0 * (((b - r) / delta) + 2.0)
|
||||
} else {
|
||||
60.0 * (((r - g) / delta) + 4.0)
|
||||
};
|
||||
|
||||
let h = if h < 0.0 { h + 360.0 } else { h };
|
||||
let s = if max == 0.0 { 0.0 } else { delta / max };
|
||||
let v = max;
|
||||
|
||||
(h, s, v)
|
||||
}
|
||||
|
||||
fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (u8, u8, u8) {
|
||||
let c = v * s;
|
||||
let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
|
||||
let m = v - c;
|
||||
|
||||
let (r, g, b) = if h < 60.0 {
|
||||
(c, x, 0.0)
|
||||
} else if h < 120.0 {
|
||||
(x, c, 0.0)
|
||||
} else if h < 180.0 {
|
||||
(0.0, c, x)
|
||||
} else if h < 240.0 {
|
||||
(0.0, x, c)
|
||||
} else if h < 300.0 {
|
||||
(x, 0.0, c)
|
||||
} else {
|
||||
(c, 0.0, x)
|
||||
};
|
||||
|
||||
(
|
||||
((r + m) * 255.0) as u8,
|
||||
((g + m) * 255.0) as u8,
|
||||
((b + m) * 255.0) as u8,
|
||||
)
|
||||
}
|
||||
|
||||
fn rotate_hue_rgb(r: u8, g: u8, b: u8, degrees: f32) -> (u8, u8, u8) {
|
||||
let (h, s, v) = rgb_to_hsv(r, g, b);
|
||||
let new_h = (h + degrees) % 360.0;
|
||||
let new_h = if new_h < 0.0 { new_h + 360.0 } else { new_h };
|
||||
hsv_to_rgb(new_h, s, v)
|
||||
}
|
||||
|
||||
fn rotate_color(color: Color, degrees: f32) -> Color {
|
||||
match color {
|
||||
Color::Rgb(r, g, b) => {
|
||||
let (nr, ng, nb) = rotate_hue_rgb(r, g, b, degrees);
|
||||
Color::Rgb(nr, ng, nb)
|
||||
}
|
||||
_ => color,
|
||||
}
|
||||
}
|
||||
|
||||
fn rotate_tuple(tuple: (u8, u8, u8), degrees: f32) -> (u8, u8, u8) {
|
||||
rotate_hue_rgb(tuple.0, tuple.1, tuple.2, degrees)
|
||||
}
|
||||
|
||||
fn rotate_color_pair(pair: (Color, Color), degrees: f32) -> (Color, Color) {
|
||||
(rotate_color(pair.0, degrees), rotate_color(pair.1, degrees))
|
||||
}
|
||||
|
||||
pub fn rotate_theme(theme: ThemeColors, degrees: f32) -> ThemeColors {
|
||||
if degrees == 0.0 {
|
||||
return theme;
|
||||
}
|
||||
|
||||
ThemeColors {
|
||||
ui: UiColors {
|
||||
bg: rotate_color(theme.ui.bg, degrees),
|
||||
bg_rgb: rotate_tuple(theme.ui.bg_rgb, degrees),
|
||||
text_primary: rotate_color(theme.ui.text_primary, degrees),
|
||||
text_muted: rotate_color(theme.ui.text_muted, degrees),
|
||||
text_dim: rotate_color(theme.ui.text_dim, degrees),
|
||||
border: rotate_color(theme.ui.border, degrees),
|
||||
header: rotate_color(theme.ui.header, degrees),
|
||||
unfocused: rotate_color(theme.ui.unfocused, degrees),
|
||||
accent: rotate_color(theme.ui.accent, degrees),
|
||||
surface: rotate_color(theme.ui.surface, degrees),
|
||||
},
|
||||
status: StatusColors {
|
||||
playing_bg: rotate_color(theme.status.playing_bg, degrees),
|
||||
playing_fg: rotate_color(theme.status.playing_fg, degrees),
|
||||
stopped_bg: rotate_color(theme.status.stopped_bg, degrees),
|
||||
stopped_fg: rotate_color(theme.status.stopped_fg, degrees),
|
||||
fill_on: rotate_color(theme.status.fill_on, degrees),
|
||||
fill_off: rotate_color(theme.status.fill_off, degrees),
|
||||
fill_bg: rotate_color(theme.status.fill_bg, degrees),
|
||||
},
|
||||
selection: SelectionColors {
|
||||
cursor_bg: rotate_color(theme.selection.cursor_bg, degrees),
|
||||
cursor_fg: rotate_color(theme.selection.cursor_fg, degrees),
|
||||
selected_bg: rotate_color(theme.selection.selected_bg, degrees),
|
||||
selected_fg: rotate_color(theme.selection.selected_fg, degrees),
|
||||
in_range_bg: rotate_color(theme.selection.in_range_bg, degrees),
|
||||
in_range_fg: rotate_color(theme.selection.in_range_fg, degrees),
|
||||
cursor: rotate_color(theme.selection.cursor, degrees),
|
||||
selected: rotate_color(theme.selection.selected, degrees),
|
||||
in_range: rotate_color(theme.selection.in_range, degrees),
|
||||
},
|
||||
tile: TileColors {
|
||||
playing_active_bg: rotate_color(theme.tile.playing_active_bg, degrees),
|
||||
playing_active_fg: rotate_color(theme.tile.playing_active_fg, degrees),
|
||||
playing_inactive_bg: rotate_color(theme.tile.playing_inactive_bg, degrees),
|
||||
playing_inactive_fg: rotate_color(theme.tile.playing_inactive_fg, degrees),
|
||||
active_bg: rotate_color(theme.tile.active_bg, degrees),
|
||||
active_fg: rotate_color(theme.tile.active_fg, degrees),
|
||||
content_bg: rotate_color(theme.tile.content_bg, degrees),
|
||||
inactive_bg: rotate_color(theme.tile.inactive_bg, degrees),
|
||||
inactive_fg: rotate_color(theme.tile.inactive_fg, degrees),
|
||||
active_selected_bg: rotate_color(theme.tile.active_selected_bg, degrees),
|
||||
active_in_range_bg: rotate_color(theme.tile.active_in_range_bg, degrees),
|
||||
link_bright: [
|
||||
rotate_tuple(theme.tile.link_bright[0], degrees),
|
||||
rotate_tuple(theme.tile.link_bright[1], degrees),
|
||||
rotate_tuple(theme.tile.link_bright[2], degrees),
|
||||
rotate_tuple(theme.tile.link_bright[3], degrees),
|
||||
rotate_tuple(theme.tile.link_bright[4], degrees),
|
||||
],
|
||||
link_dim: [
|
||||
rotate_tuple(theme.tile.link_dim[0], degrees),
|
||||
rotate_tuple(theme.tile.link_dim[1], degrees),
|
||||
rotate_tuple(theme.tile.link_dim[2], degrees),
|
||||
rotate_tuple(theme.tile.link_dim[3], degrees),
|
||||
rotate_tuple(theme.tile.link_dim[4], degrees),
|
||||
],
|
||||
},
|
||||
header: HeaderColors {
|
||||
tempo_bg: rotate_color(theme.header.tempo_bg, degrees),
|
||||
tempo_fg: rotate_color(theme.header.tempo_fg, degrees),
|
||||
bank_bg: rotate_color(theme.header.bank_bg, degrees),
|
||||
bank_fg: rotate_color(theme.header.bank_fg, degrees),
|
||||
pattern_bg: rotate_color(theme.header.pattern_bg, degrees),
|
||||
pattern_fg: rotate_color(theme.header.pattern_fg, degrees),
|
||||
stats_bg: rotate_color(theme.header.stats_bg, degrees),
|
||||
stats_fg: rotate_color(theme.header.stats_fg, degrees),
|
||||
},
|
||||
modal: ModalColors {
|
||||
border: rotate_color(theme.modal.border, degrees),
|
||||
border_accent: rotate_color(theme.modal.border_accent, degrees),
|
||||
border_warn: rotate_color(theme.modal.border_warn, degrees),
|
||||
border_dim: rotate_color(theme.modal.border_dim, degrees),
|
||||
confirm: rotate_color(theme.modal.confirm, degrees),
|
||||
rename: rotate_color(theme.modal.rename, degrees),
|
||||
input: rotate_color(theme.modal.input, degrees),
|
||||
editor: rotate_color(theme.modal.editor, degrees),
|
||||
preview: rotate_color(theme.modal.preview, degrees),
|
||||
},
|
||||
flash: FlashColors {
|
||||
error_bg: rotate_color(theme.flash.error_bg, degrees),
|
||||
error_fg: rotate_color(theme.flash.error_fg, degrees),
|
||||
success_bg: rotate_color(theme.flash.success_bg, degrees),
|
||||
success_fg: rotate_color(theme.flash.success_fg, degrees),
|
||||
info_bg: rotate_color(theme.flash.info_bg, degrees),
|
||||
info_fg: rotate_color(theme.flash.info_fg, degrees),
|
||||
},
|
||||
list: ListColors {
|
||||
playing_bg: rotate_color(theme.list.playing_bg, degrees),
|
||||
playing_fg: rotate_color(theme.list.playing_fg, degrees),
|
||||
staged_play_bg: rotate_color(theme.list.staged_play_bg, degrees),
|
||||
staged_play_fg: rotate_color(theme.list.staged_play_fg, degrees),
|
||||
staged_stop_bg: rotate_color(theme.list.staged_stop_bg, degrees),
|
||||
staged_stop_fg: rotate_color(theme.list.staged_stop_fg, degrees),
|
||||
edit_bg: rotate_color(theme.list.edit_bg, degrees),
|
||||
edit_fg: rotate_color(theme.list.edit_fg, degrees),
|
||||
hover_bg: rotate_color(theme.list.hover_bg, degrees),
|
||||
hover_fg: rotate_color(theme.list.hover_fg, degrees),
|
||||
muted_bg: rotate_color(theme.list.muted_bg, degrees),
|
||||
muted_fg: rotate_color(theme.list.muted_fg, degrees),
|
||||
soloed_bg: rotate_color(theme.list.soloed_bg, degrees),
|
||||
soloed_fg: rotate_color(theme.list.soloed_fg, degrees),
|
||||
},
|
||||
link_status: LinkStatusColors {
|
||||
disabled: rotate_color(theme.link_status.disabled, degrees),
|
||||
connected: rotate_color(theme.link_status.connected, degrees),
|
||||
listening: rotate_color(theme.link_status.listening, degrees),
|
||||
},
|
||||
syntax: SyntaxColors {
|
||||
gap_bg: rotate_color(theme.syntax.gap_bg, degrees),
|
||||
executed_bg: rotate_color(theme.syntax.executed_bg, degrees),
|
||||
selected_bg: rotate_color(theme.syntax.selected_bg, degrees),
|
||||
emit: rotate_color_pair(theme.syntax.emit, degrees),
|
||||
number: rotate_color_pair(theme.syntax.number, degrees),
|
||||
string: rotate_color_pair(theme.syntax.string, degrees),
|
||||
comment: rotate_color_pair(theme.syntax.comment, degrees),
|
||||
keyword: rotate_color_pair(theme.syntax.keyword, degrees),
|
||||
stack_op: rotate_color_pair(theme.syntax.stack_op, degrees),
|
||||
operator: rotate_color_pair(theme.syntax.operator, degrees),
|
||||
sound: rotate_color_pair(theme.syntax.sound, degrees),
|
||||
param: rotate_color_pair(theme.syntax.param, degrees),
|
||||
context: rotate_color_pair(theme.syntax.context, degrees),
|
||||
note: rotate_color_pair(theme.syntax.note, degrees),
|
||||
interval: rotate_color_pair(theme.syntax.interval, degrees),
|
||||
variable: rotate_color_pair(theme.syntax.variable, degrees),
|
||||
vary: rotate_color_pair(theme.syntax.vary, degrees),
|
||||
generator: rotate_color_pair(theme.syntax.generator, degrees),
|
||||
user_defined: rotate_color_pair(theme.syntax.user_defined, degrees),
|
||||
default: rotate_color_pair(theme.syntax.default, degrees),
|
||||
},
|
||||
table: TableColors {
|
||||
row_even: rotate_color(theme.table.row_even, degrees),
|
||||
row_odd: rotate_color(theme.table.row_odd, degrees),
|
||||
},
|
||||
values: ValuesColors {
|
||||
tempo: rotate_color(theme.values.tempo, degrees),
|
||||
value: rotate_color(theme.values.value, degrees),
|
||||
},
|
||||
hint: HintColors {
|
||||
key: rotate_color(theme.hint.key, degrees),
|
||||
text: rotate_color(theme.hint.text, degrees),
|
||||
},
|
||||
view_badge: ViewBadgeColors {
|
||||
bg: rotate_color(theme.view_badge.bg, degrees),
|
||||
fg: rotate_color(theme.view_badge.fg, degrees),
|
||||
},
|
||||
nav: NavColors {
|
||||
selected_bg: rotate_color(theme.nav.selected_bg, degrees),
|
||||
selected_fg: rotate_color(theme.nav.selected_fg, degrees),
|
||||
unselected_bg: rotate_color(theme.nav.unselected_bg, degrees),
|
||||
unselected_fg: rotate_color(theme.nav.unselected_fg, degrees),
|
||||
},
|
||||
editor_widget: EditorWidgetColors {
|
||||
cursor_bg: rotate_color(theme.editor_widget.cursor_bg, degrees),
|
||||
cursor_fg: rotate_color(theme.editor_widget.cursor_fg, degrees),
|
||||
selection_bg: rotate_color(theme.editor_widget.selection_bg, degrees),
|
||||
completion_bg: rotate_color(theme.editor_widget.completion_bg, degrees),
|
||||
completion_fg: rotate_color(theme.editor_widget.completion_fg, degrees),
|
||||
completion_selected: rotate_color(theme.editor_widget.completion_selected, degrees),
|
||||
completion_example: rotate_color(theme.editor_widget.completion_example, degrees),
|
||||
},
|
||||
browser: BrowserColors {
|
||||
directory: rotate_color(theme.browser.directory, degrees),
|
||||
project_file: rotate_color(theme.browser.project_file, degrees),
|
||||
selected: rotate_color(theme.browser.selected, degrees),
|
||||
file: rotate_color(theme.browser.file, degrees),
|
||||
focused_border: rotate_color(theme.browser.focused_border, degrees),
|
||||
unfocused_border: rotate_color(theme.browser.unfocused_border, degrees),
|
||||
root: rotate_color(theme.browser.root, degrees),
|
||||
file_icon: rotate_color(theme.browser.file_icon, degrees),
|
||||
folder_icon: rotate_color(theme.browser.folder_icon, degrees),
|
||||
empty_text: rotate_color(theme.browser.empty_text, degrees),
|
||||
},
|
||||
input: InputColors {
|
||||
text: rotate_color(theme.input.text, degrees),
|
||||
cursor: rotate_color(theme.input.cursor, degrees),
|
||||
hint: rotate_color(theme.input.hint, degrees),
|
||||
},
|
||||
search: SearchColors {
|
||||
active: rotate_color(theme.search.active, degrees),
|
||||
inactive: rotate_color(theme.search.inactive, degrees),
|
||||
match_bg: rotate_color(theme.search.match_bg, degrees),
|
||||
match_fg: rotate_color(theme.search.match_fg, degrees),
|
||||
},
|
||||
markdown: MarkdownColors {
|
||||
h1: rotate_color(theme.markdown.h1, degrees),
|
||||
h2: rotate_color(theme.markdown.h2, degrees),
|
||||
h3: rotate_color(theme.markdown.h3, degrees),
|
||||
code: rotate_color(theme.markdown.code, degrees),
|
||||
code_border: rotate_color(theme.markdown.code_border, degrees),
|
||||
link: rotate_color(theme.markdown.link, degrees),
|
||||
link_url: rotate_color(theme.markdown.link_url, degrees),
|
||||
quote: rotate_color(theme.markdown.quote, degrees),
|
||||
text: rotate_color(theme.markdown.text, degrees),
|
||||
list: rotate_color(theme.markdown.list, degrees),
|
||||
},
|
||||
engine: EngineColors {
|
||||
header: rotate_color(theme.engine.header, degrees),
|
||||
header_focused: rotate_color(theme.engine.header_focused, degrees),
|
||||
divider: rotate_color(theme.engine.divider, degrees),
|
||||
scroll_indicator: rotate_color(theme.engine.scroll_indicator, degrees),
|
||||
label: rotate_color(theme.engine.label, degrees),
|
||||
label_focused: rotate_color(theme.engine.label_focused, degrees),
|
||||
label_dim: rotate_color(theme.engine.label_dim, degrees),
|
||||
value: rotate_color(theme.engine.value, degrees),
|
||||
focused: rotate_color(theme.engine.focused, degrees),
|
||||
normal: rotate_color(theme.engine.normal, degrees),
|
||||
dim: rotate_color(theme.engine.dim, degrees),
|
||||
path: rotate_color(theme.engine.path, degrees),
|
||||
border_magenta: rotate_color(theme.engine.border_magenta, degrees),
|
||||
border_green: rotate_color(theme.engine.border_green, degrees),
|
||||
border_cyan: rotate_color(theme.engine.border_cyan, degrees),
|
||||
separator: rotate_color(theme.engine.separator, degrees),
|
||||
hint_active: rotate_color(theme.engine.hint_active, degrees),
|
||||
hint_inactive: rotate_color(theme.engine.hint_inactive, degrees),
|
||||
},
|
||||
dict: DictColors {
|
||||
word_name: rotate_color(theme.dict.word_name, degrees),
|
||||
word_bg: rotate_color(theme.dict.word_bg, degrees),
|
||||
alias: rotate_color(theme.dict.alias, degrees),
|
||||
stack_sig: rotate_color(theme.dict.stack_sig, degrees),
|
||||
description: rotate_color(theme.dict.description, degrees),
|
||||
example: rotate_color(theme.dict.example, degrees),
|
||||
category_focused: rotate_color(theme.dict.category_focused, degrees),
|
||||
category_selected: rotate_color(theme.dict.category_selected, degrees),
|
||||
category_normal: rotate_color(theme.dict.category_normal, degrees),
|
||||
category_dimmed: rotate_color(theme.dict.category_dimmed, degrees),
|
||||
border_focused: rotate_color(theme.dict.border_focused, degrees),
|
||||
border_normal: rotate_color(theme.dict.border_normal, degrees),
|
||||
header_desc: rotate_color(theme.dict.header_desc, degrees),
|
||||
},
|
||||
title: TitleColors {
|
||||
big_title: rotate_color(theme.title.big_title, degrees),
|
||||
author: rotate_color(theme.title.author, degrees),
|
||||
link: rotate_color(theme.title.link, degrees),
|
||||
license: rotate_color(theme.title.license, degrees),
|
||||
prompt: rotate_color(theme.title.prompt, degrees),
|
||||
subtitle: rotate_color(theme.title.subtitle, degrees),
|
||||
},
|
||||
meter: MeterColors {
|
||||
low: rotate_color(theme.meter.low, degrees),
|
||||
mid: rotate_color(theme.meter.mid, degrees),
|
||||
high: rotate_color(theme.meter.high, degrees),
|
||||
low_rgb: rotate_tuple(theme.meter.low_rgb, degrees),
|
||||
mid_rgb: rotate_tuple(theme.meter.mid_rgb, degrees),
|
||||
high_rgb: rotate_tuple(theme.meter.high_rgb, degrees),
|
||||
},
|
||||
sparkle: SparkleColors {
|
||||
colors: [
|
||||
rotate_tuple(theme.sparkle.colors[0], degrees),
|
||||
rotate_tuple(theme.sparkle.colors[1], degrees),
|
||||
rotate_tuple(theme.sparkle.colors[2], degrees),
|
||||
rotate_tuple(theme.sparkle.colors[3], degrees),
|
||||
rotate_tuple(theme.sparkle.colors[4], degrees),
|
||||
],
|
||||
},
|
||||
confirm: ConfirmColors {
|
||||
border: rotate_color(theme.confirm.border, degrees),
|
||||
button_selected_bg: rotate_color(theme.confirm.button_selected_bg, degrees),
|
||||
button_selected_fg: rotate_color(theme.confirm.button_selected_fg, degrees),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::theme;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Color;
|
||||
@@ -29,13 +30,13 @@ impl VuMeter {
|
||||
(db - DB_MIN) / DB_RANGE
|
||||
}
|
||||
|
||||
fn row_to_color(row_position: f32) -> Color {
|
||||
fn row_to_color(row_position: f32, colors: &theme::ThemeColors) -> Color {
|
||||
if row_position > 0.9 {
|
||||
Color::Red
|
||||
colors.meter.high
|
||||
} else if row_position > 0.75 {
|
||||
Color::Yellow
|
||||
colors.meter.mid
|
||||
} else {
|
||||
Color::Green
|
||||
colors.meter.low
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,6 +47,7 @@ impl Widget for VuMeter {
|
||||
return;
|
||||
}
|
||||
|
||||
let colors = theme::get();
|
||||
let height = area.height as usize;
|
||||
let half_width = area.width / 2;
|
||||
let gap = 1u16;
|
||||
@@ -61,7 +63,7 @@ impl Widget for VuMeter {
|
||||
for row in 0..height {
|
||||
let y = area.y + area.height - 1 - row as u16;
|
||||
let row_position = (row as f32 + 0.5) / height as f32;
|
||||
let color = Self::row_to_color(row_position);
|
||||
let color = Self::row_to_color(row_position, &colors);
|
||||
|
||||
for col in 0..half_width.saturating_sub(gap) {
|
||||
let x = area.x + col;
|
||||
197
crates/ratatui/src/waveform.rs
Normal file
197
crates/ratatui/src/waveform.rs
Normal file
@@ -0,0 +1,197 @@
|
||||
use crate::scope::Orientation;
|
||||
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()) };
|
||||
}
|
||||
|
||||
pub struct Waveform<'a> {
|
||||
data: &'a [f32],
|
||||
orientation: Orientation,
|
||||
color: Option<Color>,
|
||||
gain: f32,
|
||||
}
|
||||
|
||||
impl<'a> Waveform<'a> {
|
||||
pub fn new(data: &'a [f32]) -> Self {
|
||||
Self {
|
||||
data,
|
||||
orientation: Orientation::Horizontal,
|
||||
color: None,
|
||||
gain: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn orientation(mut self, o: Orientation) -> Self {
|
||||
self.orientation = o;
|
||||
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 Waveform<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
if area.width == 0 || area.height == 0 || self.data.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let color = self.color.unwrap_or_else(|| theme::get().meter.low);
|
||||
|
||||
match self.orientation {
|
||||
Orientation::Horizontal => {
|
||||
render_horizontal(self.data, area, buf, color, self.gain)
|
||||
}
|
||||
Orientation::Vertical => render_vertical(self.data, area, buf, color, self.gain),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 render_horizontal(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gain: f32) {
|
||||
let width = area.width as usize;
|
||||
let height = area.height as usize;
|
||||
let fine_width = width * 2;
|
||||
let fine_height = height * 4;
|
||||
let len = data.len();
|
||||
|
||||
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();
|
||||
patterns.clear();
|
||||
patterns.resize(width * height, 0);
|
||||
|
||||
for fine_x in 0..fine_width {
|
||||
let start = fine_x * len / fine_width;
|
||||
let end = ((fine_x + 1) * len / fine_width).max(start + 1).min(len);
|
||||
let slice = &data[start..end];
|
||||
|
||||
let mut min_s = f32::MAX;
|
||||
let mut max_s = f32::MIN;
|
||||
for &s in slice {
|
||||
let s = (s * auto_gain).clamp(-1.0, 1.0);
|
||||
if s < min_s {
|
||||
min_s = s;
|
||||
}
|
||||
if s > max_s {
|
||||
max_s = s;
|
||||
}
|
||||
}
|
||||
|
||||
let fy_top = ((1.0 - max_s) * 0.5 * (fine_height - 1) as f32).round() as usize;
|
||||
let fy_bot = ((1.0 - min_s) * 0.5 * (fine_height - 1) as f32).round() as usize;
|
||||
let fy_top = fy_top.min(fine_height - 1);
|
||||
let fy_bot = fy_bot.min(fine_height - 1);
|
||||
|
||||
let char_x = fine_x / 2;
|
||||
let dot_x = fine_x % 2;
|
||||
|
||||
for fy in fy_top..=fy_bot {
|
||||
let char_y = fy / 4;
|
||||
let dot_y = fy % 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_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gain: f32) {
|
||||
let width = area.width as usize;
|
||||
let height = area.height as usize;
|
||||
let fine_width = width * 2;
|
||||
let fine_height = height * 4;
|
||||
let len = data.len();
|
||||
|
||||
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();
|
||||
patterns.clear();
|
||||
patterns.resize(width * height, 0);
|
||||
|
||||
for fine_y in 0..fine_height {
|
||||
let start = fine_y * len / fine_height;
|
||||
let end = ((fine_y + 1) * len / fine_height).max(start + 1).min(len);
|
||||
let slice = &data[start..end];
|
||||
|
||||
let mut min_s = f32::MAX;
|
||||
let mut max_s = f32::MIN;
|
||||
for &s in slice {
|
||||
let s = (s * auto_gain).clamp(-1.0, 1.0);
|
||||
if s < min_s {
|
||||
min_s = s;
|
||||
}
|
||||
if s > max_s {
|
||||
max_s = s;
|
||||
}
|
||||
}
|
||||
|
||||
let fx_left = ((min_s + 1.0) * 0.5 * (fine_width - 1) as f32).round() as usize;
|
||||
let fx_right = ((max_s + 1.0) * 0.5 * (fine_width - 1) as f32).round() as usize;
|
||||
let fx_left = fx_left.min(fine_width - 1);
|
||||
let fx_right = fx_right.min(fine_width - 1);
|
||||
|
||||
let char_y = fine_y / 4;
|
||||
let dot_y = fine_y % 4;
|
||||
|
||||
for fx in fx_left..=fx_right {
|
||||
let char_x = fx / 2;
|
||||
let dot_x = fx % 2;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
115
docs/about_forth.md
Normal file
115
docs/about_forth.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# About Forth
|
||||
|
||||
Forth is a _stack-based_ programming language created by Charles H. Moore in the early 1970s. It was designed with simplicity, directness, and interactive exploration in mind. Forth has been used for many years to do scientific work and program embedded systems: it was used to control telescopes and was running on some devices used in space missions among other things. Forth quickly evolved into multiple implementations targetting various computer architectures. None of them really took off and became popular. Nonetheless, the ideas behind Forth continue to garner the interest of many different people in very different (often unrelated) fields. Nowadays, Forth languages are used by hackers and artists for their peculiarity. Forth is simple, direct and beautiful to implement. Forth is an elegant and minimal language to learn. It is easy to understand, to extend and to apply to a specific task. The Forth we use in Cagire is specialized in making live music. We think of it as a DSL: a _Domain Specific Language_.
|
||||
|
||||
## Why Forth?
|
||||
|
||||
Most programming languages nowadays use a complex syntax made of `variables`, `expressions` and `statements` like `x = 3 + 4`. Forth works differently. It is way more simple than that, has almost no syntax and performs computations in a quite unique way. You push values onto a `stack` and apply `words` that transform them:
|
||||
|
||||
```forth
|
||||
3 4 +
|
||||
```
|
||||
|
||||
This program leaves the number `7` on the stack. There are no variables, no parentheses, no syntax to remember. You just end up with words and numbers separated by spaces. For live coding music, this directness is quite exciting. All you do is think in terms of transformations and add things to the stack: take a note, shift it up, add reverb, play it.
|
||||
|
||||
## The Stack
|
||||
|
||||
The stack is where values live. When you type a number, it goes on the stack. When you type a word, it usually takes values off and puts new ones back.
|
||||
|
||||
```forth
|
||||
3 ( stack: 3 )
|
||||
4 ( stack: 3 4 )
|
||||
+ ( stack: 7 )
|
||||
```
|
||||
|
||||
The stack is `last-in, first-out`. The most recent value is always on top. This means that its often better to read Forth programs from the end to the beginning: from right to left, from the bottom to the top.
|
||||
|
||||
## Words
|
||||
|
||||
Everything in Forth is either a `number` or a `word`. Words are like functions but conceptually simpler. They have no arguments or return values in the traditional sense. They just manipulate the stack directly.
|
||||
|
||||
```forth
|
||||
dup ( duplicate the top value )
|
||||
drop ( discard the top value )
|
||||
swap ( swap the top two values )
|
||||
```
|
||||
|
||||
Words compose naturally on the stack. To double a number:
|
||||
|
||||
```forth
|
||||
3 dup + ( 3 3 +)
|
||||
```
|
||||
|
||||
There are a lot of words in a Forth and thus, Cagire has a `Dictionary` embedded directly into the application. You can also create your own words. They will work just like the already existing words. There are good reasons to create new words on-the-fly:
|
||||
|
||||
- To make synth definitions.
|
||||
- To abstract _some piece of code_ that you use frequently.
|
||||
- To share data and processes between different steps.
|
||||
|
||||
## Values
|
||||
|
||||
Four types of values can live on the stack:
|
||||
|
||||
- **Integers**: `42`, `-7`, `0`
|
||||
- **Floats**: `0.5`, `3.14`, `-1.0`
|
||||
- **Strings**: `"kick"`, `"hello"`
|
||||
- **Quotations**: `{ dup + }` (code as data)
|
||||
|
||||
Quotations are special. They let you pass code around as a value. This is how conditionals and loops work. Think nothing of it for now, you will learn more about how to use it later on.
|
||||
|
||||
## Stack Notation
|
||||
|
||||
Documentation uses a notation to show what words do:
|
||||
|
||||
```
|
||||
( before -- after )
|
||||
```
|
||||
|
||||
For example, `+` has the signature `( a b -- sum )`. It takes two values and leaves one.
|
||||
|
||||
## The Command Register
|
||||
|
||||
Traditional Forth programs print text to a terminal. Cagire's Forth builds sound commands instead. This happens through an invisible accumulator called the command register. The command register has two parts:
|
||||
- a **sound name** (what instrument to play)
|
||||
- a list of **parameters** (how to play it)
|
||||
|
||||
Three types of words interact with it:
|
||||
|
||||
```forth
|
||||
kick sound ;; sets the sound name
|
||||
0.5 gain ;; adds a parameter
|
||||
. ;; emits the command and clears the register
|
||||
```
|
||||
|
||||
The word `sound` (or its shorthand `s`) sets what sound to play. Parameter words like `gain`, `freq`, `decay`, or `verb` add key-value pairs to the register. Nothing happens until you emit with `.` (dot). At that moment, the register is packaged into a command and sent to the audio engine.
|
||||
|
||||
This design lets you build sounds incrementally:
|
||||
|
||||
```forth
|
||||
"sine" sound
|
||||
c4 note
|
||||
0.5 gain
|
||||
0.3 decay
|
||||
0.4 verb
|
||||
.
|
||||
```
|
||||
|
||||
Each line adds something to the register. The final `.` triggers the sound. You can also write it all on one line:
|
||||
|
||||
```forth
|
||||
"sine" s c4 note 0.5 gain 0.3 decay 0.4 verb .
|
||||
```
|
||||
|
||||
The order of parameters does not matter. You can even emit multiple times in a single step. If you need to discard the register without emitting, use `clear`:
|
||||
|
||||
```forth
|
||||
"kick" s 0.5 gain clear ;; nothing plays, register is emptied
|
||||
"hat" s . ;; only the hat plays
|
||||
```
|
||||
|
||||
This is useful when conditionals might cancel a sound before it emits.
|
||||
|
||||
## More details
|
||||
|
||||
- Each step has its own stack and independant runtime.
|
||||
- Word definitions and variable definitions are shared by all steps.
|
||||
51
docs/banks_patterns.md
Normal file
51
docs/banks_patterns.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Banks & Patterns
|
||||
|
||||
Cagire organizes all your patterns and data following a strict hierarchy:
|
||||
|
||||
- **Projects** contain **Banks**.
|
||||
- **Banks** contain **Patterns**.
|
||||
- **Patterns** contain **Steps**.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
Project
|
||||
└── 32 Banks
|
||||
└── 32 Patterns (per bank)
|
||||
└── 1024 Steps (per pattern)
|
||||
```
|
||||
|
||||
A single project gives you 32 banks, each holding 32 patterns. You get 1024 patterns in each project, ~1.048.000 steps.
|
||||
|
||||
## Patterns
|
||||
|
||||
Each pattern is an independent sequence of steps with its own properties:
|
||||
|
||||
| Property | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| Length | Steps before the pattern loops (`1`-`1024`) | `16` |
|
||||
| Speed | Playback rate (`1/8x` to `8x`) | `1x` |
|
||||
| Quantization | When the pattern launches | `Bar` |
|
||||
| Sync Mode | Reset or Phase-Lock on re-trigger | `Reset` |
|
||||
|
||||
Press `e` in the patterns view to edit these settings.
|
||||
|
||||
## Patterns View
|
||||
|
||||
Access the patterns view with `Ctrl+Up` from the sequencer. The view shows all banks and patterns in a grid. Indicators show pattern state:
|
||||
|
||||
- `>` Currently playing
|
||||
- `+` Staged to play
|
||||
- `-` Staged to stop
|
||||
|
||||
### Keybindings
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `Arrows` | Navigate banks and patterns |
|
||||
| `Enter` | Select and return to sequencer |
|
||||
| `Space` | Toggle pattern playback |
|
||||
| `e` | Edit pattern properties |
|
||||
| `r` | Rename bank or pattern |
|
||||
| `c` / `v` | Copy / Paste |
|
||||
| `Delete` | Reset to empty pattern |
|
||||
185
docs/control_flow.md
Normal file
185
docs/control_flow.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# Control Flow
|
||||
|
||||
A drum pattern that plays the same sound on every step is not very interesting. You want kicks on the downbeats, snares on the backbeats, hats filling the gaps. Control flow is how you make decisions inside a step.
|
||||
|
||||
## if / else / then
|
||||
|
||||
The simplest branch. Push a condition, then `if`:
|
||||
|
||||
```forth
|
||||
step 4 mod 0 = if kick s . then
|
||||
```
|
||||
|
||||
Every fourth step gets a kick. The rest do nothing. Add `else` for a two-way split:
|
||||
|
||||
```forth
|
||||
step 2 mod 0 = if
|
||||
kick s 0.8 gain .
|
||||
else
|
||||
hat s 0.3 gain .
|
||||
then
|
||||
```
|
||||
|
||||
These are compiler syntax -- you won't find them in the dictionary. Think nothing of it.
|
||||
|
||||
## ? and !?
|
||||
|
||||
When you already have a quotation, `?` executes it if the condition is truthy:
|
||||
|
||||
```forth
|
||||
{ snare s . } coin ?
|
||||
```
|
||||
|
||||
`!?` is the opposite -- executes when falsy:
|
||||
|
||||
```forth
|
||||
{ hat s 0.2 gain . } coin !?
|
||||
```
|
||||
|
||||
These pair well with `chance`, `prob`, and the other probability words:
|
||||
|
||||
```forth
|
||||
{ rim s . } fill ? ;; rim only during fills
|
||||
{ 0.5 verb } 0.3 chance ? ;; occasional reverb wash
|
||||
```
|
||||
|
||||
## ifelse
|
||||
|
||||
Two quotations, one condition. The true branch comes first:
|
||||
|
||||
```forth
|
||||
{ kick s . } { hat s . } step 2 mod 0 = ifelse
|
||||
```
|
||||
|
||||
Reads naturally: "kick or hat, depending on whether it's an even step."
|
||||
|
||||
```forth
|
||||
{ c3 note } { c4 note } coin ifelse
|
||||
saw s 0.6 gain . ;; bass or lead, coin flip
|
||||
```
|
||||
|
||||
## pick
|
||||
|
||||
Choose the nth option from a list of quotations:
|
||||
|
||||
```forth
|
||||
{ kick s . } { snare s . } { hat s . } step 3 mod pick
|
||||
```
|
||||
|
||||
Step 0 plays kick, step 1 plays snare, step 2 plays hat. The index is 0-based.
|
||||
|
||||
```forth
|
||||
{ c4 } { e4 } { g4 } { b4 } step 4 mod pick
|
||||
note sine s 0.5 decay .
|
||||
```
|
||||
|
||||
Four notes cycling through a major seventh chord, one per step.
|
||||
|
||||
## case / of / endof / endcase
|
||||
|
||||
For matching a value against several options. Cleaner than a chain of `if`s when you have more than two branches:
|
||||
|
||||
```forth
|
||||
step 8 mod case
|
||||
0 of kick s . endof
|
||||
4 of snare s . endof
|
||||
endcase
|
||||
```
|
||||
|
||||
Steps 0 and 4 get sounds. Everything else falls through to `endcase` and nothing happens.
|
||||
|
||||
A fuller pattern:
|
||||
|
||||
```forth
|
||||
step 8 mod case
|
||||
0 of kick s 0.9 gain . endof
|
||||
2 of hat s 0.3 gain . endof
|
||||
4 of snare s 0.7 gain . endof
|
||||
6 of hat s 0.3 gain . endof
|
||||
hat s 0.15 gain .
|
||||
endcase
|
||||
```
|
||||
|
||||
The last line before `endcase` is the default -- it runs when no `of` matched. Here it gives unmatched steps a ghost hat.
|
||||
|
||||
The `of` value can be any expression:
|
||||
|
||||
```forth
|
||||
step 16 mod case
|
||||
0 of kick s . endof
|
||||
3 1 + of snare s . endof
|
||||
2 4 * of kick s . snare s . endof
|
||||
endcase
|
||||
```
|
||||
|
||||
## times
|
||||
|
||||
Repeat a quotation n times. `@i` holds the current iteration (starting from 0):
|
||||
|
||||
```forth
|
||||
4 { @i 4 / at hat s . } times ;; four hats, evenly spaced
|
||||
```
|
||||
|
||||
Build chords:
|
||||
|
||||
```forth
|
||||
3 { c4 @i 4 * + note } times
|
||||
sine s 0.4 gain 0.5 verb . ;; c4, e4, g#4
|
||||
```
|
||||
|
||||
Subdivide and accent:
|
||||
|
||||
```forth
|
||||
8 {
|
||||
@i 8 / at
|
||||
@i 4 mod 0 = if 0.7 else 0.2 then gain
|
||||
hat s .
|
||||
} times
|
||||
```
|
||||
|
||||
Eight hats per step. Every fourth one louder.
|
||||
|
||||
## Putting It Together
|
||||
|
||||
A basic drum pattern using `case`:
|
||||
|
||||
```forth
|
||||
step 8 mod case
|
||||
0 of kick s . endof
|
||||
2 of { hat s . } often endof
|
||||
4 of snare s . endof
|
||||
6 of { rim s . } sometimes endof
|
||||
{ hat s 0.15 gain . } coin ?
|
||||
endcase
|
||||
```
|
||||
|
||||
Kicks and snares on the strong beats. Hats and rims show up probabilistically. The default sprinkles ghost hats.
|
||||
|
||||
A melodic step that picks a scale degree and adds micro-timing:
|
||||
|
||||
```forth
|
||||
{ c4 } { d4 } { e4 } { g4 } { a4 } step 5 mod pick
|
||||
note
|
||||
|
||||
step 3 mod 0 = if
|
||||
0 0.33 0.66 at ;; triplet feel on every third step
|
||||
then
|
||||
|
||||
saw s 0.4 gain 0.3 decay 0.2 verb .
|
||||
```
|
||||
|
||||
A `times` loop paired with `case` for a drum machine in one step:
|
||||
|
||||
```forth
|
||||
4 {
|
||||
@i case
|
||||
0 of kick s . endof
|
||||
1 of hat s 0.3 gain . endof
|
||||
2 of snare s . endof
|
||||
3 of { rim s . } 0.5 chance endof
|
||||
endcase
|
||||
@i 4 / at
|
||||
} times
|
||||
```
|
||||
|
||||
Four voices, four sub-positions, one step.
|
||||
95
docs/definitions.md
Normal file
95
docs/definitions.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# Creating Words
|
||||
|
||||
One of Forth's most powerful features is the ability to define new words. A word definition gives a name to a sequence of operations. Once defined, you can use the new word just like any built-in word.
|
||||
|
||||
## The Syntax
|
||||
|
||||
Use `:` to start a definition and `;` to end it:
|
||||
|
||||
```forth
|
||||
: double dup + ;
|
||||
```
|
||||
|
||||
This creates a word called `double` that duplicates the top value and adds it to itself. Now you can use it:
|
||||
|
||||
```forth
|
||||
3 double ;; leaves 6 on the stack
|
||||
5 double ;; leaves 10 on the stack
|
||||
```
|
||||
|
||||
The definition is simple: everything between `:` and `;` becomes the body of the word.
|
||||
|
||||
## Definitions Are Shared
|
||||
|
||||
When you define a word in one step, it becomes available to all other steps. This is how you share code across your pattern. Define your synths, rhythms, and utilities once, then use them everywhere.
|
||||
|
||||
Step 0:
|
||||
```forth
|
||||
: bass "saw" s 0.8 gain 800 lpf ;
|
||||
```
|
||||
|
||||
Step 4:
|
||||
```forth
|
||||
c2 note bass .
|
||||
```
|
||||
|
||||
Step 8:
|
||||
```forth
|
||||
g2 note bass .
|
||||
```
|
||||
|
||||
The `bass` word carries the sound design. Each step just adds a note and plays.
|
||||
|
||||
## Redefining Words
|
||||
|
||||
You can redefine any word, including built-in ones:
|
||||
|
||||
```forth
|
||||
: dup drop ;
|
||||
```
|
||||
|
||||
Now `dup` does the opposite of what it used to do. This is powerful but dangerous. Redefining core words can break things in subtle ways.
|
||||
|
||||
You can even redefine numbers:
|
||||
|
||||
```forth
|
||||
: 2 4 ;
|
||||
```
|
||||
|
||||
Now `2` pushes `4` onto the stack. The number two no longer exists in your session. This is a classic Forth demonstration: nothing is sacred, everything can be redefined.
|
||||
|
||||
## Practical Uses
|
||||
|
||||
**Synth definitions** save you from repeating sound design:
|
||||
|
||||
```forth
|
||||
: pad "sine" s 0.3 gain 2 attack 0.5 verb ;
|
||||
```
|
||||
|
||||
**Transpositions** and musical helpers:
|
||||
|
||||
```forth
|
||||
: octup 12 + ;
|
||||
: octdn 12 - ;
|
||||
```
|
||||
|
||||
## Words That Emit
|
||||
|
||||
A word can contain `.` to emit sounds directly:
|
||||
|
||||
```forth
|
||||
: kick "kick" s . ;
|
||||
: hat "hat" s 0.4 gain . ;
|
||||
```
|
||||
|
||||
Then a step becomes trivial:
|
||||
|
||||
```forth
|
||||
kick hat
|
||||
```
|
||||
|
||||
Two sounds, two words, no clutter.
|
||||
|
||||
## Stack Effects
|
||||
|
||||
When you create a word, think about what it expects on the stack and what it leaves behind. The word `double` expects one number and leaves one number. The word `kick` expects nothing and leaves nothing (it emits a sound as a side effect). Well-designed words have clear stack effects. This makes them easy to combine.
|
||||
38
docs/dictionary.md
Normal file
38
docs/dictionary.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# The Dictionary
|
||||
|
||||
Cagire includes a built-in dictionary of all the internal Forth words. Press `Ctrl+Up` to reach the **Dict** view.
|
||||
|
||||
## Using the Dictionary
|
||||
|
||||
The dictionary shows every available word organized by category:
|
||||
|
||||
- **Stack**: Manipulation words like `dup`, `swap`, `drop`.
|
||||
- **Arithmetic**: Math operations.
|
||||
- **Sound**: Sound sources and emission.
|
||||
- **Filter**, **Envelope**, **Effects**: Sound shaping.
|
||||
- **MIDI**: External MIDI control (`chan`, `cc`, `emit`, `clock`, etc.).
|
||||
- **Context**: Sequencer state like `step`, `beat`, `tempo`.
|
||||
- And many more...
|
||||
|
||||
This tutorial will not teach you how to use all words. The syntax is very uniform and you can quickly learn a new word when necessary. We encourage you to explore as you play, this is the best way to learn. The tutorial will remain focused on various topics that require you to apply knowledge to a given task or specific context.
|
||||
|
||||
## Navigation
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `Tab` | Switch between categories and words |
|
||||
| `↑/↓` or `j/k` | Navigate items |
|
||||
| `PgUp/PgDn` | Page through lists |
|
||||
| `/` or `Ctrl+F` | Search |
|
||||
| `Esc` | Clear search |
|
||||
|
||||
Each word entry shows:
|
||||
|
||||
- **Name** and aliases
|
||||
- **Stack effect**: `( before -- after )`
|
||||
- **Description**: What the word does
|
||||
- **Example**: How to use it
|
||||
|
||||
Press `/` to search across all words. The search matches word names, aliases, and descriptions. Press `Esc` to clear and return to browsing.
|
||||
|
||||
Use the dictionary while writing scripts to check stack effects and study their behavior. Some words also come with shorter aliases (e.g., `sound` → `s`). You will learn aliases quite naturally, because aliases are usually reserved for very common words.
|
||||
62
docs/editing.md
Normal file
62
docs/editing.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Editing a Step
|
||||
|
||||
Each step in Cagire contains a Forth script. When the sequencer reaches that step, it runs the script to produce sound. This is where you write your music. Press `Enter` when hovering over any step to open the `code editor`. The editor appears as a modal overlay with the step number in the title bar. If the step is a linked step (shown with an arrow like `→05`), pressing `Enter` navigates to the source step instead.
|
||||
|
||||
## Writing Scripts
|
||||
|
||||
Scripts are written in Forth. Type words and numbers separated by spaces. The simplest script that makes sound is:
|
||||
|
||||
```forth
|
||||
sine sound .
|
||||
```
|
||||
|
||||
Add parameters before words to modify them:
|
||||
|
||||
```forth
|
||||
c4 note 0.75 decay sine sound .
|
||||
```
|
||||
|
||||
Writing long lines is not recommended because it can become quite unmanageable. Instead, break them into multiple lines for clarity:
|
||||
|
||||
```forth
|
||||
c4 note
|
||||
0.75 decay
|
||||
sine sound
|
||||
0.4 verb
|
||||
.
|
||||
```
|
||||
|
||||
## Saving
|
||||
|
||||
- `Esc` - Save and close the editor
|
||||
- `Ctrl+E` - Save without closing
|
||||
|
||||
When you save, the script is compiled and sent to the sequencer. If there's an error, a message appears briefly at the bottom of the screen. You will also receive visual feedback in the form of a flashing window when saving / evaluating a script.
|
||||
|
||||
## Completion
|
||||
|
||||
As you type, the editor suggests matching Forth words. The completion list shows all built-in words that start with your current input. Press `Tab` to insert the selected suggestion, or `Esc` to dismiss the list. Use arrow keys to navigate between suggestions.
|
||||
|
||||
Completion helps you discover words without memorizing them all. Type a few letters and browse what's available. For example, typing `ver` will suggest `verb` (reverb), typing `fil` will show filter-related words.
|
||||
|
||||
## Debugging
|
||||
|
||||
Press `Ctrl+S` to toggle the stack display. This shows the stack state evaluated up to the cursor line, useful for understanding how values flow through your script. Press `Ctrl+R` to execute the script immediately without waiting for the sequencer to reach the step.
|
||||
|
||||
## Keybindings
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `Esc` | Save and close |
|
||||
| `Ctrl+E` | Save without closing |
|
||||
| `Ctrl+R` | Execute script once |
|
||||
| `Ctrl+S` | Toggle stack display |
|
||||
| `Ctrl+F` | Search |
|
||||
| `Ctrl+N` | Find next |
|
||||
| `Ctrl+P` | Find previous |
|
||||
| `Ctrl+A` | Select all |
|
||||
| `Ctrl+C` | Copy |
|
||||
| `Ctrl+X` | Cut |
|
||||
| `Ctrl+V` | Paste |
|
||||
| `Shift+Arrows` | Extend selection |
|
||||
| `Tab` | Accept completion |
|
||||
82
docs/engine_audio_modulation.md
Normal file
82
docs/engine_audio_modulation.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Audio-Rate Modulation
|
||||
|
||||
Any parameter can be modulated continuously using modulation words. Instead of a fixed value, these words produce a modulation string that the engine interprets as a moving signal.
|
||||
|
||||
All time values are in **steps**, just like `attack`, `decay`, and `release`. At 120 BPM with speed 1, one step is 0.125 seconds. Writing `4 lfo` means a 4-step period.
|
||||
|
||||
## LFOs
|
||||
|
||||
Oscillate a parameter between two values.
|
||||
|
||||
```forth
|
||||
saw s 200 4000 4 lfo lpf . ( sweep filter over 4 steps )
|
||||
saw s 0.3 0.7 2 tlfo pan . ( triangle pan over 2 steps )
|
||||
```
|
||||
|
||||
| Word | Shape | Output |
|
||||
|------|-------|--------|
|
||||
| `lfo` | Sine | `min~max:period` |
|
||||
| `tlfo` | Triangle | `min~max:periodt` |
|
||||
| `wlfo` | Sawtooth | `min~max:periodw` |
|
||||
| `qlfo` | Square | `min~max:periodq` |
|
||||
|
||||
Stack effect: `( min max period -- str )`
|
||||
|
||||
## Slides
|
||||
|
||||
Transition from one value to another over a duration.
|
||||
|
||||
```forth
|
||||
saw s 0 1 0.5 slide gain . ( fade in over half a step )
|
||||
saw s 200 4000 8 sslide lpf . ( smooth sweep over 8 steps )
|
||||
```
|
||||
|
||||
| Word | Curve | Output |
|
||||
|------|-------|--------|
|
||||
| `slide` | Linear | `start>end:dur` |
|
||||
| `expslide` | Exponential | `start>end:dure` |
|
||||
| `sslide` | Smooth (S-curve) | `start>end:durs` |
|
||||
|
||||
Stack effect: `( start end dur -- str )`
|
||||
|
||||
## Random
|
||||
|
||||
Randomize a parameter within a range, retriggering at a given period.
|
||||
|
||||
```forth
|
||||
saw s 200 4000 2 jit lpf . ( new random value every 2 steps )
|
||||
saw s 200 4000 2 sjit lpf . ( same but smoothly interpolated )
|
||||
saw s 200 4000 1 drunk lpf . ( random walk, each step )
|
||||
```
|
||||
|
||||
| Word | Behavior | Output |
|
||||
|------|----------|--------|
|
||||
| `jit` | Sample & hold | `min?max:period` |
|
||||
| `sjit` | Smooth interpolation | `min?max:periods` |
|
||||
| `drunk` | Random walk | `min?max:periodd` |
|
||||
|
||||
Stack effect: `( min max period -- str )`
|
||||
|
||||
## Envelopes
|
||||
|
||||
Define a multi-segment envelope for a parameter. Provide a start value, then pairs of target and duration.
|
||||
|
||||
```forth
|
||||
saw s 0 1 0.1 0.7 0.5 0 8 env gain .
|
||||
```
|
||||
|
||||
This creates: start at `0`, rise to `1` in `0.1` steps, drop to `0.7` in `0.5` steps, fall to `0` in `8` steps.
|
||||
|
||||
Stack effect: `( start target1 dur1 [target2 dur2 ...] -- str )`
|
||||
|
||||
## Combining
|
||||
|
||||
Modulation words return strings, so they compose naturally with the rest of the language. Use them anywhere a parameter value is expected.
|
||||
|
||||
```forth
|
||||
saw s
|
||||
200 4000 4 lfo lpf
|
||||
0.3 0.7 8 tlfo pan
|
||||
0 1 0.1 0.7 0.5 0 8 env gain
|
||||
.
|
||||
```
|
||||
65
docs/engine_distortion.md
Normal file
65
docs/engine_distortion.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Distortion
|
||||
|
||||
Distortion effects add harmonics by nonlinearly shaping the waveform.
|
||||
|
||||
## Saturation
|
||||
|
||||
Soft saturation using the transfer function `x / (1 + k|x|)`.
|
||||
|
||||
```forth
|
||||
saw 2 distort .
|
||||
saw 8 distort 0.5 distortvol . ( with volume compensation )
|
||||
```
|
||||
|
||||
| Parameter | Range | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `distort` | 0+ | Saturation amount |
|
||||
| `distortvol` | 0-1 | Output volume |
|
||||
|
||||
## Wavefolding
|
||||
|
||||
Wavefolding reflects the signal when it exceeds ±1, using `sin(x × amount × π/2)`.
|
||||
|
||||
```forth
|
||||
sine 4 fold .
|
||||
```
|
||||
|
||||
| Parameter | Range | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `fold` | 0+ | Fold amount |
|
||||
|
||||
## Wavewrapping
|
||||
|
||||
Wavewrapping applies modulo to wrap the signal into the -1 to 1 range.
|
||||
|
||||
```forth
|
||||
saw 3 wrap .
|
||||
```
|
||||
|
||||
| Parameter | Range | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `wrap` | 0+ | Number of wraps |
|
||||
|
||||
## Bit Crushing
|
||||
|
||||
Bit crushing quantizes the signal to fewer amplitude levels.
|
||||
|
||||
```forth
|
||||
snare 6 crush . ( 6-bit = 32 levels )
|
||||
```
|
||||
|
||||
| Parameter | Range | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `crush` | 1-16 | Bit depth |
|
||||
|
||||
## Sample Rate Reduction
|
||||
|
||||
Sample rate reduction holds each sample for multiple output samples.
|
||||
|
||||
```forth
|
||||
hat 4 coarse . ( 1/4 effective sample rate )
|
||||
```
|
||||
|
||||
| Parameter | Range | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `coarse` | 1+ | Reduction factor (1 = bypass) |
|
||||
131
docs/engine_filters.md
Normal file
131
docs/engine_filters.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# Filters
|
||||
|
||||
Filters attenuate frequencies above or below a cutoff point.
|
||||
|
||||
## Lowpass Filter
|
||||
|
||||
The lowpass filter (`lpf`) attenuates frequencies above the cutoff.
|
||||
|
||||
```forth
|
||||
saw 1000 lpf . ( cut above 1000 Hz )
|
||||
saw 500 lpf 0.8 lpq . ( with resonance )
|
||||
```
|
||||
|
||||
| Parameter | Range | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `lpf` | Hz | Cutoff frequency |
|
||||
| `lpq` | 0-1 | Resonance (peak at cutoff) |
|
||||
|
||||
## Highpass Filter
|
||||
|
||||
The highpass filter (`hpf`) attenuates frequencies below the cutoff.
|
||||
|
||||
```forth
|
||||
kick 200 hpf . ( cut below 200 Hz )
|
||||
pad 400 hpf 0.3 hpq . ( with resonance )
|
||||
```
|
||||
|
||||
| Parameter | Range | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `hpf` | Hz | Cutoff frequency |
|
||||
| `hpq` | 0-1 | Resonance |
|
||||
|
||||
## Bandpass Filter
|
||||
|
||||
The bandpass filter (`bpf`) attenuates frequencies outside a band around the center frequency.
|
||||
|
||||
```forth
|
||||
noise 1000 bpf 0.7 bpq . ( narrow band around 1000 Hz )
|
||||
```
|
||||
|
||||
| Parameter | Range | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `bpf` | Hz | Center frequency |
|
||||
| `bpq` | 0-1 | Resonance (narrower band) |
|
||||
|
||||
## Filter Slope
|
||||
|
||||
The `ftype` parameter sets the filter slope (rolloff steepness).
|
||||
|
||||
| Value | Slope |
|
||||
|-------|-------|
|
||||
| `1` | 12 dB/octave |
|
||||
| `2` | 24 dB/octave (default) |
|
||||
| `3` | 48 dB/octave |
|
||||
|
||||
```forth
|
||||
saw 800 lpf 3 ftype . ( 48 dB/oct lowpass )
|
||||
```
|
||||
|
||||
## Filter Envelope
|
||||
|
||||
Filters can be modulated by an ADSR envelope. The envelope multiplies the base cutoff:
|
||||
|
||||
```
|
||||
final_cutoff = lpf + (lpe × envelope × lpf)
|
||||
```
|
||||
|
||||
When the envelope is at 1.0 and `lpe` is 1.0, the cutoff doubles. When the envelope is at 0, the cutoff equals `lpf`.
|
||||
|
||||
```forth
|
||||
saw 200 lpf 2 lpe 0.01 lpa 0.3 lpd . ( cutoff sweeps from 600 Hz down to 200 Hz )
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
|-----------|-------------|
|
||||
| `lpe` | Envelope depth (multiplier, 1.0 = double cutoff at peak) |
|
||||
| `lpa` | Attack time in seconds |
|
||||
| `lpd` | Decay time in seconds |
|
||||
| `lps` | Sustain level (0-1) |
|
||||
| `lpr` | Release time in seconds |
|
||||
|
||||
The same pattern works for highpass (`hpe`, `hpa`, etc.) and bandpass (`bpe`, `bpa`, etc.).
|
||||
|
||||
## Ladder Filters
|
||||
|
||||
Ladder filters use a different algorithm (Moog-style) with self-oscillation at high resonance.
|
||||
|
||||
```forth
|
||||
saw 800 llpf 0.7 llpq . ( ladder lowpass )
|
||||
saw 300 lhpf 0.5 lhpq . ( ladder highpass )
|
||||
saw 1000 lbpf 0.8 lbpq . ( ladder bandpass )
|
||||
```
|
||||
|
||||
| Parameter | Range | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `llpf` | Hz | Ladder lowpass cutoff |
|
||||
| `llpq` | 0-1 | Ladder lowpass resonance |
|
||||
| `lhpf` | Hz | Ladder highpass cutoff |
|
||||
| `lhpq` | 0-1 | Ladder highpass resonance |
|
||||
| `lbpf` | Hz | Ladder bandpass cutoff |
|
||||
| `lbpq` | 0-1 | Ladder bandpass resonance |
|
||||
|
||||
Ladder filters share the lowpass envelope parameters (`lpe`, `lpa`, etc.).
|
||||
|
||||
## EQ
|
||||
|
||||
The 3-band EQ applies shelf and peak filters at fixed frequencies.
|
||||
|
||||
```forth
|
||||
kick 3 eqlo -2 eqhi . ( +3 dB at 200 Hz, -2 dB at 5000 Hz )
|
||||
snare 2 eqmid . ( +2 dB at 1000 Hz )
|
||||
```
|
||||
|
||||
| Parameter | Frequency | Type |
|
||||
|-----------|-----------|------|
|
||||
| `eqlo` | 200 Hz | Low shelf (dB) |
|
||||
| `eqmid` | 1000 Hz | Peak (dB) |
|
||||
| `eqhi` | 5000 Hz | High shelf (dB) |
|
||||
|
||||
## Tilt EQ
|
||||
|
||||
Tilt EQ applies a high shelf at 800 Hz with up to ±6 dB gain.
|
||||
|
||||
```forth
|
||||
pad -0.5 tilt . ( -3 dB above 800 Hz )
|
||||
hat 0.5 tilt . ( +3 dB above 800 Hz )
|
||||
```
|
||||
|
||||
| Parameter | Range | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `tilt` | -1 to 1 | High shelf gain (-1 = -6 dB, 0 = flat, 1 = +6 dB) |
|
||||
55
docs/engine_intro.md
Normal file
55
docs/engine_intro.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Introduction
|
||||
|
||||
Cagire includes an audio engine called `Doux`. No external software is needed to make sound. `Doux` is an opinionated, semi-modular synthesizer. It was designed for live coding environments and works by receiving command strings that describe sounds. Despite its fixed architecture,`Doux` is extremely versatile and will likely cover most of the audio needs of a live coder.
|
||||
|
||||
## How It Works
|
||||
|
||||
When you write a Forth script and emit (`.`), the script produces a command string. This command travels to the audio engine, which interprets it and creates a voice. The voice plays until its envelope finishes or until it is killed by another voice. You can also spawn infinite voices, but you will need to manage their lifecycle manually, otherwise they will never stop.
|
||||
|
||||
```forth
|
||||
saw s c4 note 0.8 gain 0.3 verb .
|
||||
```
|
||||
|
||||
## Voices
|
||||
|
||||
Each `emit` (`.`) creates or manages a voice by sending parameters. Voices are independent sound generators with their own oscillator, envelope, and effects. The engine can run many voices at once (up to `128`, default `32`). When you exceed the voice limit, the oldest voice is stolen (a process called _round robin scheduling_). You can monitor voice usage on the Engine page:
|
||||
|
||||
- **Active voices**: how many are playing right now.
|
||||
- **Peak voices**: the maximum reached since last reset.
|
||||
|
||||
Press `r` on the Engine page to reset the peak counter.
|
||||
|
||||
## Parameters
|
||||
|
||||
After selecting a sound source, you add parameters. Each parameter word takes a value from the stack and stores it in the command register:
|
||||
|
||||
```forth
|
||||
saw s
|
||||
c4 note ;; pitch
|
||||
0.5 gain ;; volume
|
||||
0.1 attack ;; envelope attack time
|
||||
2000 lpf ;; lowpass filter at 2kHz
|
||||
0.3 verb ;; reverb mix
|
||||
.
|
||||
```
|
||||
|
||||
Parameters can appear in any order. They accumulate until you emit. You can clear the register using the `clear` word.
|
||||
|
||||
## Controlling Existing Voices
|
||||
|
||||
You can emit without a sound name. In this case, no new voice is created. Instead, the parameters are sent to control an existing voice. Use `voice` with an ID to target a specific voice:
|
||||
|
||||
```forth
|
||||
0 voice 500 freq . ;; change frequency on voice 0
|
||||
```
|
||||
|
||||
This is useful for modulating long-running or infinite voices. Set up a drone on one step with a known voice ID, then tweak its parameters from other steps.
|
||||
|
||||
## Hush and Panic
|
||||
|
||||
Two emergency controls exist on the Engine page:
|
||||
|
||||
- `h` - **Hush**: gracefully fade out all voices
|
||||
- `p` - **Panic**: immediately kill all voices
|
||||
|
||||
Use hush when things get too loud. Use panic when things go wrong.
|
||||
132
docs/engine_modulation.md
Normal file
132
docs/engine_modulation.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Modulation
|
||||
|
||||
Modulation effects vary parameters over time using LFOs or envelopes.
|
||||
|
||||
## Vibrato
|
||||
|
||||
Vibrato modulates pitch with an LFO.
|
||||
|
||||
```forth
|
||||
saw 5 vib 0.5 vibmod . ( 5 Hz, 0.5 semitone depth )
|
||||
```
|
||||
|
||||
| Parameter | Range | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `vib` | Hz | LFO rate |
|
||||
| `vibmod` | semitones | Modulation depth |
|
||||
| `vibshape` | shape | LFO waveform (sine, tri, saw, square) |
|
||||
|
||||
## Pitch Envelope
|
||||
|
||||
The pitch envelope applies an ADSR to the oscillator frequency.
|
||||
|
||||
```forth
|
||||
sine 100 freq 24 penv 0.001 patt 0.1 pdec .
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
|-----------|-------------|
|
||||
| `penv` | Envelope depth in semitones |
|
||||
| `patt` | Attack time in seconds |
|
||||
| `pdec` | Decay time in seconds |
|
||||
| `psus` | Sustain level (0-1) |
|
||||
| `prel` | Release time in seconds |
|
||||
|
||||
## Glide
|
||||
|
||||
Glide interpolates between pitch changes over time.
|
||||
|
||||
```forth
|
||||
saw c4 0.1 glide . ( 100ms glide )
|
||||
```
|
||||
|
||||
| Parameter | Range | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `glide` | seconds | Glide time |
|
||||
|
||||
## FM Synthesis
|
||||
|
||||
FM modulates the carrier frequency with a modulator oscillator.
|
||||
|
||||
```forth
|
||||
sine 440 freq 2 fm 2 fmh . ( modulator at 2× carrier frequency )
|
||||
```
|
||||
|
||||
| Parameter | Range | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `fm` | 0+ | Modulation index |
|
||||
| `fmh` | ratio | Harmonic ratio (modulator / carrier) |
|
||||
| `fmshape` | shape | Modulator waveform |
|
||||
|
||||
FM has its own envelope (`fme`, `fma`, `fmd`, `fms`, `fmr`).
|
||||
|
||||
## Amplitude Modulation
|
||||
|
||||
AM multiplies the signal by an LFO.
|
||||
|
||||
```forth
|
||||
pad 4 am 0.5 amdepth . ( 4 Hz tremolo )
|
||||
```
|
||||
|
||||
| Parameter | Range | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `am` | Hz | LFO rate |
|
||||
| `amdepth` | 0-1 | Modulation depth |
|
||||
| `amshape` | shape | LFO waveform |
|
||||
|
||||
## Ring Modulation
|
||||
|
||||
Ring modulation multiplies two signals, producing sum and difference frequencies.
|
||||
|
||||
```forth
|
||||
saw 150 rm 0.8 rmdepth . ( ring mod at 150 Hz )
|
||||
```
|
||||
|
||||
| Parameter | Range | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `rm` | Hz | Modulator frequency |
|
||||
| `rmdepth` | 0-1 | Modulation depth |
|
||||
| `rmshape` | shape | Modulator waveform |
|
||||
|
||||
## Phaser
|
||||
|
||||
Phaser sweeps notches through the frequency spectrum using allpass filters.
|
||||
|
||||
```forth
|
||||
pad 0.5 phaser 0.6 phaserdepth .
|
||||
```
|
||||
|
||||
| Parameter | Range | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `phaser` | Hz | Sweep rate |
|
||||
| `phaserdepth` | 0-1 | Sweep depth |
|
||||
| `phasersweep` | cents | Sweep range |
|
||||
| `phasercenter` | Hz | Center frequency |
|
||||
|
||||
## Flanger
|
||||
|
||||
Flanger mixes the signal with a short modulated delay (0.5-10ms).
|
||||
|
||||
```forth
|
||||
pad 0.3 flanger 0.7 flangerdepth 0.5 flangerfeedback .
|
||||
```
|
||||
|
||||
| Parameter | Range | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `flanger` | Hz | Modulation rate |
|
||||
| `flangerdepth` | 0-1 | Modulation depth |
|
||||
| `flangerfeedback` | 0-0.95 | Feedback amount |
|
||||
|
||||
## Chorus
|
||||
|
||||
Chorus uses multiple modulated delay lines with 120° phase offset for stereo width.
|
||||
|
||||
```forth
|
||||
pad 1 chorus 0.4 chorusdepth 20 chorusdelay .
|
||||
```
|
||||
|
||||
| Parameter | Range | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `chorus` | Hz | Modulation rate |
|
||||
| `chorusdepth` | 0-1 | Modulation depth |
|
||||
| `chorusdelay` | ms | Base delay time |
|
||||
126
docs/engine_samples.md
Normal file
126
docs/engine_samples.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# Samples
|
||||
|
||||
The `sample` source plays audio files from disk with pitch tracking.
|
||||
|
||||
## Loading Samples
|
||||
|
||||
There are two ways to load samples:
|
||||
|
||||
* **From the app:** Navigate to the Engine view and find the Samples section. Press `A` to open a file browser, then select a folder containing your samples. Press `D` to remove the last added path.
|
||||
|
||||
* **From the command line:** Use the `-s` flag when launching Cagire:
|
||||
|
||||
```
|
||||
cagire -s ~/samples -s ~/more-samples
|
||||
```
|
||||
|
||||
The engine scans these directories and builds a registry of available samples. Samples load in the background without blocking audio. Supported file formats are `.wav`, `.mp3`, `.ogg`, `.flac` and `.aiff`.
|
||||
|
||||
## Folder Organization
|
||||
|
||||
```
|
||||
samples/
|
||||
├── kick.wav → "kick"
|
||||
├── snare.wav → "snare"
|
||||
└── hats/
|
||||
├── closed.wav → "hats" n 0
|
||||
├── open.wav → "hats" n 1
|
||||
└── pedal.wav → "hats" n 2
|
||||
```
|
||||
|
||||
Folders at the root of your directory are used as the name of a sample bank. Folders create sample banks where each file gets an index. Files are sorted alphabetically and assigned indices starting from `0`.
|
||||
|
||||
## Playing Samples
|
||||
|
||||
```forth
|
||||
kick sound . ( play kick sample )
|
||||
hats sound 2 n . ( play third hat sample )
|
||||
snare sound 0.5 speed . ( play snare at half speed )
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Range | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `n` | 0+ | Sample index within a folder (wraps around) |
|
||||
| `begin` | 0-1 | Playback start position |
|
||||
| `end` | 0-1 | Playback end position |
|
||||
| `speed` | any | Playback speed multiplier |
|
||||
| `freq` | Hz | Base frequency for pitch tracking |
|
||||
| `fit` | seconds | Stretch/compress sample to fit duration |
|
||||
| `cut` | 0+ | Choke group |
|
||||
|
||||
## Slicing with Begin/End
|
||||
|
||||
The `begin` and `end` parameters define what portion of the sample plays. Values are normalized from 0 (start) to 1 (end).
|
||||
|
||||
```forth
|
||||
kick sound 0.25 begin 0.75 end . ( play middle half )
|
||||
kick sound 0.5 begin . ( play second half )
|
||||
kick sound 0.5 end . ( play first half )
|
||||
```
|
||||
|
||||
If begin is greater than end, they swap automatically.
|
||||
|
||||
## Speed and Pitch
|
||||
|
||||
The `speed` parameter affects both tempo and pitch. A speed of 2 plays twice as fast and an octave higher.
|
||||
|
||||
```forth
|
||||
snare sound 2 speed . ( double speed, octave up )
|
||||
snare sound 0.5 speed . ( half speed, octave down )
|
||||
snare sound -1 speed . ( play in reverse )
|
||||
```
|
||||
|
||||
For pitched playback, use `freq` or note names. The sample's base frequency defaults to middle C (261.626 Hz).
|
||||
|
||||
```forth
|
||||
kick sound 440 freq . ( play at A4 )
|
||||
kick sound c4 . ( play at C4 )
|
||||
```
|
||||
|
||||
Negative speed will reverse the sample and play it backwards.
|
||||
|
||||
```forth
|
||||
crow sound -1 speed . ( play backwards at nominal speed )
|
||||
crow sound -4 speed . ( play backwards, 4 times faster )
|
||||
```
|
||||
|
||||
## Fitting to Duration
|
||||
|
||||
The `fit` parameter stretches or compresses a sample to match a target duration in seconds. This adjusts speed automatically.
|
||||
|
||||
```forth
|
||||
kick sound 0.25 fit . ( fit kick into quarter second )
|
||||
snare sound beat fit . ( fit snare to one beat )
|
||||
```
|
||||
|
||||
## Choke Groups
|
||||
|
||||
The `cut` parameter assigns a sample to a choke group. When a new sample with the same cut value plays, it kills any currently playing samples in that group.
|
||||
|
||||
```forth
|
||||
hihat_closed sound 1 cut . ( choke group 1 )
|
||||
hihat_open sound 1 cut . ( kills closed hat, starts open )
|
||||
```
|
||||
|
||||
This is essential for realistic hi-hat behavior where open and closed hats shouldn't overlap.
|
||||
|
||||
## Bank Variations
|
||||
|
||||
Add `_suffix` to sample names to create variations that share the same base name.
|
||||
|
||||
```
|
||||
samples/
|
||||
├── kick.wav
|
||||
├── kick_hard.wav
|
||||
├── kick_soft.wav
|
||||
```
|
||||
|
||||
Select variations with the `bank` parameter:
|
||||
|
||||
```forth
|
||||
kick sound . ( plays kick.wav )
|
||||
kick sound hard bank . ( plays kick_hard.wav )
|
||||
kick sound soft bank . ( plays kick_soft.wav )
|
||||
```
|
||||
125
docs/engine_settings.md
Normal file
125
docs/engine_settings.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Settings
|
||||
|
||||
The audio engine can be configured through the Engine page or via command-line arguments. Settings are saved and restored between sessions.
|
||||
|
||||
## Engine Page
|
||||
|
||||
Press `Ctrl+Right` until you reach the Engine page. Here you can see the engine status and adjust settings.
|
||||
|
||||
### Display
|
||||
|
||||
The right side of the page shows visualizations:
|
||||
|
||||
- **Scope**: oscilloscope showing the audio waveform
|
||||
- **Spectrum**: 32-band frequency analyzer
|
||||
|
||||
### Settings
|
||||
|
||||
Navigate with arrow keys, adjust values with left/right:
|
||||
|
||||
- **Output Device**: where sound goes (speakers, headphones, interface).
|
||||
- **Input Device**: what audio input source to use (microphone, line-in, etc.).
|
||||
- **Channels**: number of output channels (2 for stereo).
|
||||
- **Buffer Size**: audio buffer in samples (64-4096).
|
||||
- **Max Voices**: polyphony limit (1-128, default 32).
|
||||
|
||||
### Buffer Size
|
||||
|
||||
Smaller buffers mean lower latency but higher CPU load. Larger buffers are safer but feel sluggish.
|
||||
|
||||
| Buffer | Latency at 44.1kHz |
|
||||
|--------|-------------------|
|
||||
| 64 | ~1.5ms |
|
||||
| 128 | ~3ms |
|
||||
| 256 | ~6ms |
|
||||
| 512 | ~12ms |
|
||||
| 1024 | ~23ms |
|
||||
|
||||
Start with 512. Lower it if you need tighter timing. Raise it if you hear glitches.
|
||||
|
||||
## Samples
|
||||
|
||||
The engine indexes audio files from your sample directories. Add directories with `A`, remove with `D`. The sample count shows how many files are indexed.
|
||||
|
||||
### Supported Formats
|
||||
|
||||
- WAV (.wav)
|
||||
- MP3 (.mp3)
|
||||
- OGG Vorbis (.ogg)
|
||||
- FLAC (.flac)
|
||||
- AIFF (.aiff, .aif)
|
||||
- AAC (.aac)
|
||||
- M4A (.m4a)
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
Samples are not loaded into memory at startup. They are decoded on demand when first played. This means you can have thousands of samples indexed without using much RAM until you actually use them.
|
||||
|
||||
### Folder Organization
|
||||
|
||||
The scanner looks at top-level files and one level of subdirectories:
|
||||
|
||||
```
|
||||
samples/
|
||||
├── kick.wav -> "kick"
|
||||
├── snare.wav -> "snare"
|
||||
├── hats/
|
||||
│ ├── closed.wav -> "hats" n 0
|
||||
│ ├── open.wav -> "hats" n 1
|
||||
│ └── pedal.wav -> "hats" n 2
|
||||
└── breaks/
|
||||
├── amen.wav -> "breaks" n 0
|
||||
└── think.wav -> "breaks" n 1
|
||||
```
|
||||
|
||||
Top-level files are named by their filename (without extension). Files inside folders are sorted alphabetically and numbered starting from 0.
|
||||
|
||||
### Playing Samples
|
||||
|
||||
Reference samples by name:
|
||||
|
||||
```forth
|
||||
kick s . ;; play kick.wav
|
||||
snare s 0.5 gain . ;; play snare at half volume
|
||||
```
|
||||
|
||||
For samples in folders, use `n` to select which one:
|
||||
|
||||
```forth
|
||||
hats s 0 n . ;; play hats/closed.wav (index 0)
|
||||
hats s 1 n . ;; play hats/open.wav (index 1)
|
||||
hats s 2 n . ;; play hats/pedal.wav (index 2)
|
||||
```
|
||||
|
||||
The index wraps around. If you have 3 samples and request `5 n`, you get index 2 (because 5 % 3 = 2).
|
||||
|
||||
### Sample Variations with Bank
|
||||
|
||||
The `bank` parameter lets you organize variations:
|
||||
|
||||
```
|
||||
samples/
|
||||
├── kick.wav -> default
|
||||
├── kick_a.wav -> bank "a"
|
||||
├── kick_b.wav -> bank "b"
|
||||
└── kick_hard.wav -> bank "hard"
|
||||
```
|
||||
|
||||
```forth
|
||||
kick s . ;; plays kick.wav
|
||||
kick s a bank . ;; plays kick_a.wav
|
||||
kick s hard bank . ;; plays kick_hard.wav
|
||||
```
|
||||
|
||||
If the banked version does not exist, it falls back to the default.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
* **No sound**: Check output device selection.
|
||||
* Try the test sound (`t`) on Engine page).
|
||||
|
||||
* **Glitches/crackling**: Increase buffer size, restart the Engine.
|
||||
|
||||
* **High CPU**: Reduce max voices. Disable scope/spectrum. Increase buffer size.
|
||||
|
||||
* **Samples not found**: Check sample directories on Engine page. Filenames are case-sensitive on some systems.
|
||||
92
docs/engine_sources.md
Normal file
92
docs/engine_sources.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Sources
|
||||
|
||||
The audio engine provides a variety of sound sources. Use the `sound` word (or `s` for short) to select one.
|
||||
|
||||
## Basic Oscillators
|
||||
|
||||
| Name | Description |
|
||||
|------|-------------|
|
||||
| `sine` | Pure sinusoid, smooth and mellow |
|
||||
| `tri` | Triangle wave, warmer than sine, naturally band-limited |
|
||||
| `saw` | Bright sawtooth with anti-aliasing, rich in harmonics |
|
||||
| `zaw` | Raw sawtooth without anti-aliasing, lo-fi character |
|
||||
| `pulse`, `square` | Variable-width pulse wave with anti-aliasing |
|
||||
| `pulze`, `zquare` | Raw pulse without anti-aliasing, 8-bit feel |
|
||||
|
||||
`pulse` and `pulze` respond to the `pw` parameter (0.0-1.0) for pulse width. At 0.5 you get a square wave.
|
||||
|
||||
### Phase Shaping
|
||||
|
||||
All oscillators support phase shaping for timbral variation:
|
||||
|
||||
| Parameter | Range | Effect |
|
||||
|-----------|-------|--------|
|
||||
| `size` | 0-256 | Phase quantization (lo-fi, chiptune). |
|
||||
| `mult` | 0.25-16 | Phase multiplier (harmonic overtones). |
|
||||
| `warp` | -1 to 1 | Power curve asymmetry. |
|
||||
| `mirror` | 0-1 | Phase reflection point. |
|
||||
|
||||
These are super useful to get the most out of your oscillators.
|
||||
|
||||
### Sub Oscillator
|
||||
|
||||
Add a sub oscillator layer to any basic oscillator:
|
||||
|
||||
| Parameter | Range | Effect |
|
||||
|-----------|-------|--------|
|
||||
| `sub` | 0-1 | Mix level |
|
||||
| `suboct` | 1-3 | Octaves below main. |
|
||||
| `subwave` | tri/sine/square | Sub waveform. |
|
||||
|
||||
## Noise
|
||||
|
||||
| Name | Description |
|
||||
|------|-------------|
|
||||
| `white` | Equal energy across all frequencies, bright and hissy. |
|
||||
| `pink` | -3dB/octave rolloff, equal energy per octave, natural. |
|
||||
| `brown` | -6dB/octave rolloff, deep rumbling, random walk. |
|
||||
|
||||
Noise sources ignore pitch. Use filters to shape the spectrum.
|
||||
|
||||
## Live Input
|
||||
|
||||
| Name | Description |
|
||||
|------|-------------|
|
||||
| `live`, `livein`, `mic` | Live audio input from microphone or line-in |
|
||||
|
||||
All filter and effect parameters apply to the input signal.
|
||||
|
||||
## Plaits Engines
|
||||
|
||||
The Plaits engines come from Mutable Instruments and provide a range of synthesis methods. Beware, these sources can be quite CPU hungry. All share three control parameters (`0.0`-`1.0`):
|
||||
|
||||
| Parameter | Controls |
|
||||
|-----------|----------|
|
||||
| `harmonics` | Harmonic content, structure, detuning. |
|
||||
| `timbre` | Brightness, tonal color. |
|
||||
| `morph` | Smooth transitions between variations. |
|
||||
|
||||
### Pitched
|
||||
|
||||
| Name | Description |
|
||||
|------|-------------|
|
||||
| `modal` | Struck/plucked resonant bodies (strings, plates, tubes). |
|
||||
| `va`, `analog` | Virtual analog with waveform sync and crossfading. |
|
||||
| `ws`, `waveshape` | Waveshaper and wavefolder. |
|
||||
| `fm2` | Two-operator FM synthesis with feedback. |
|
||||
| `grain` | Granular formant oscillator (vowel-like). |
|
||||
| `additive` | Harmonic additive synthesis. |
|
||||
| `wavetable` | Built-in Plaits wavetables (four 8x8 banks). |
|
||||
| `chord` | Four-note chord generator. |
|
||||
| `swarm` | Granular cloud of enveloped sawtooths. |
|
||||
| `pnoise` | Clocked noise through multimode filter. |
|
||||
|
||||
### Percussion
|
||||
|
||||
| Name | Description |
|
||||
|------|-------------|
|
||||
| `kick`, `bass` | 808-style bass drum. |
|
||||
| `snare` | Analog snare drum with tone/noise balance. |
|
||||
| `hihat`, `hat` | Metallic 808-style hi-hat. |
|
||||
|
||||
Percussions are super hard to use correctly, because you need to tweak their envelope correctly.
|
||||
118
docs/engine_space.md
Normal file
118
docs/engine_space.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# Space & Time
|
||||
|
||||
Spatial effects position sounds in the stereo field and add depth through delays and reverbs.
|
||||
|
||||
## Pan
|
||||
|
||||
Pan positions a sound in the stereo field.
|
||||
|
||||
```forth
|
||||
hat -0.5 pan . ( slightly left )
|
||||
perc 1 pan . ( hard right )
|
||||
kick 0 pan . ( center )
|
||||
```
|
||||
|
||||
| Parameter | Range | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `pan` | -1 to 1 | Stereo position (-1 = left, 0 = center, 1 = right) |
|
||||
|
||||
## Width
|
||||
|
||||
Width controls the stereo spread using mid-side processing.
|
||||
|
||||
```forth
|
||||
pad 1.5 width . ( wider stereo )
|
||||
pad 0 width . ( mono )
|
||||
```
|
||||
|
||||
| Parameter | Range | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `width` | 0+ | Stereo width (0 = mono, 1 = unchanged, 2 = exaggerated) |
|
||||
|
||||
## Haas Effect
|
||||
|
||||
The Haas effect delays one channel slightly, creating a sense of stereo width and spatial placement.
|
||||
|
||||
```forth
|
||||
snare 15 haas . ( 15ms delay on right channel )
|
||||
```
|
||||
|
||||
| Parameter | Range | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `haas` | ms | Delay time (1-10ms = subtle width, 10-35ms = distinct echo) |
|
||||
|
||||
## Delay
|
||||
|
||||
Delay is a send effect that creates echoes. The `delay` parameter sets how much signal is sent to the delay bus.
|
||||
|
||||
```forth
|
||||
snare 0.3 delay 0.25 delaytime 0.5 delayfeedback .
|
||||
```
|
||||
|
||||
| Parameter | Range | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `delay` | 0-1 | Send level |
|
||||
| `delaytime` | seconds | Delay time |
|
||||
| `delayfeedback` | 0-0.95 | Feedback amount |
|
||||
| `delaytype` | type | Delay algorithm |
|
||||
|
||||
### Delay Types
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `standard` | Clean digital repeats |
|
||||
| `pingpong` | Bounces between left and right |
|
||||
| `tape` | Each repeat gets darker (analog warmth) |
|
||||
| `multitap` | 4 taps with swing control via feedback |
|
||||
|
||||
```forth
|
||||
snare 0.4 delay pingpong delaytype .
|
||||
pad 0.3 delay tape delaytype .
|
||||
```
|
||||
|
||||
## Reverb
|
||||
|
||||
Reverb is a send effect that simulates acoustic spaces. The `verb` parameter sets the send level.
|
||||
|
||||
```forth
|
||||
snare 0.2 verb 2 verbdecay .
|
||||
pad 0.4 verb 4 verbdecay 0.7 verbdamp .
|
||||
```
|
||||
|
||||
| Parameter | Range | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `verb` | 0-1 | Send level |
|
||||
| `verbdecay` | seconds | Reverb tail length |
|
||||
| `verbdamp` | 0-1 | High frequency damping |
|
||||
| `verbpredelay` | ms | Initial delay before reverb |
|
||||
| `verbdiff` | 0-1 | Diffusion (smears transients) |
|
||||
| `verbtype` | type | Reverb algorithm |
|
||||
|
||||
### Reverb Types
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `dattorro` | Plate reverb, bright and metallic shimmer |
|
||||
| `fdn` | Hall reverb, dense and smooth |
|
||||
|
||||
```forth
|
||||
snare 0.3 verb dattorro verbtype . ( plate )
|
||||
pad 0.5 verb fdn verbtype . ( hall )
|
||||
```
|
||||
|
||||
## Comb Filter
|
||||
|
||||
The comb filter creates resonant pitched delays, useful for Karplus-Strong string synthesis and metallic tones.
|
||||
|
||||
```forth
|
||||
white 0.5 comb 220 combfreq 0.9 combfeedback . ( plucked string )
|
||||
```
|
||||
|
||||
| Parameter | Range | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `comb` | 0-1 | Send level |
|
||||
| `combfreq` | Hz | Resonant frequency |
|
||||
| `combfeedback` | 0-0.99 | Feedback (higher = longer decay) |
|
||||
| `combdamp` | 0-1 | High frequency damping |
|
||||
|
||||
Higher feedback creates longer, ringing tones. Add damping for more natural string-like decay.
|
||||
87
docs/engine_wavetable.md
Normal file
87
docs/engine_wavetable.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Wavetables
|
||||
|
||||
Any sample can be played as a wavetable. When you use the `scan` parameter, the sample automatically becomes a pitched oscillator that can morph between cycles.
|
||||
|
||||
## What is a Wavetable?
|
||||
|
||||
A wavetable is a series of single-cycle waveforms stored end-to-end in an audio file. Each cycle is a complete waveform that starts and ends at zero crossing, allowing it to loop seamlessly at any pitch.
|
||||
|
||||
```
|
||||
Sample: [cycle 0][cycle 1][cycle 2][cycle 3]
|
||||
↑ ↑
|
||||
scan 0 scan 1
|
||||
```
|
||||
|
||||
The oscillator reads through one cycle at audio rate (determining pitch), while `scan` selects which cycle to play. Values between cycles crossfade smoothly, creating timbral morphing.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
Just add `scan` to any sample and it becomes a wavetable:
|
||||
|
||||
```forth
|
||||
pad 0 scan . ( play pad as wavetable, first cycle )
|
||||
pad 0.5 scan . ( blend to middle cycles )
|
||||
pad 440 freq 0 scan . ( play at A4 )
|
||||
```
|
||||
|
||||
Without `scan`, the sample plays normally. With `scan`, it becomes a looping wavetable oscillator.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Range | Description |
|
||||
|-----------|-------|-------------|
|
||||
| `scan` | 0-1 | Position in wavetable (0 = first cycle, 1 = last) |
|
||||
| `wtlen` | samples | Cycle length in samples (0 = entire sample) |
|
||||
| `scanlfo` | Hz | LFO rate for scan modulation |
|
||||
| `scandepth` | 0-1 | LFO modulation depth |
|
||||
| `scanshape` | shape | LFO waveform |
|
||||
|
||||
## Cycle Length
|
||||
|
||||
The `wtlen` parameter tells the engine how many samples make up one cycle. This must match how the wavetable was created, otherwise you'll hear the wrong pitch or glitchy artifacts.
|
||||
|
||||
```forth
|
||||
pad 0 scan 2048 wtlen . ( Serum-style 2048-sample cycles )
|
||||
pad 0 scan 1024 wtlen . ( 1024-sample cycles )
|
||||
```
|
||||
|
||||
Common cycle lengths are powers of two: 256, 512, 1024, 2048. Serum uses 2048 samples per cycle. The number of cycles in a wavetable is the total sample length divided by `wtlen`. If `wtlen` is 0 (default), the entire sample is treated as one cycle. The sample still loops as a pitched oscillator, but `scan` has no morphing effect since there's only one cycle.
|
||||
|
||||
## Scanning
|
||||
|
||||
The `scan` parameter selects which cycle to play:
|
||||
|
||||
```forth
|
||||
pad 0 scan . ( first cycle only )
|
||||
pad 0.5 scan . ( blend between middle cycles )
|
||||
pad 1 scan . ( last cycle only )
|
||||
```
|
||||
|
||||
## LFO Modulation
|
||||
|
||||
Automate the scan position with a built-in LFO:
|
||||
|
||||
```forth
|
||||
pad 0 scan 2 scanlfo 0.3 scandepth . ( 2 Hz modulation, 30% depth )
|
||||
```
|
||||
|
||||
Available LFO shapes:
|
||||
|
||||
| Shape | Description |
|
||||
|-------|-------------|
|
||||
| `sine` | Smooth oscillation (default) |
|
||||
| `tri` | Triangle wave |
|
||||
| `saw` | Sawtooth, ramps up |
|
||||
| `square` | Alternates between extremes |
|
||||
| `sh` | Sample and hold, random steps |
|
||||
|
||||
## Creating Wavetables
|
||||
|
||||
A proper wavetable file:
|
||||
|
||||
- Contains multiple single-cycle waveforms of identical length
|
||||
- Each cycle starts and ends at zero crossing for seamless looping
|
||||
- Uses power-of-two cycle lengths (256, 512, 1024, 2048)
|
||||
- Has cycles that morph smoothly from one to the next
|
||||
|
||||
You can find wavetable packs online or create your own in tools like Serum, WaveEdit, or Audacity (using zero-crossing snap). Single-cycle waveforms also work. With `wtlen` set to 0, a single-cycle sample becomes a basic pitched oscillator.
|
||||
43
docs/engine_words.md
Normal file
43
docs/engine_words.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Words & Sounds
|
||||
|
||||
Word definitions let you abstract sound design into reusable units.
|
||||
|
||||
## Defining Sounds
|
||||
|
||||
```forth
|
||||
: lead "saw" s 0.3 gain 1200 lpf ;
|
||||
```
|
||||
|
||||
Use it with different notes:
|
||||
|
||||
```forth
|
||||
c4 note lead .
|
||||
e4 note lead .
|
||||
```
|
||||
|
||||
## Self-Contained Words
|
||||
|
||||
Include the emit to make the word play directly:
|
||||
|
||||
```forth
|
||||
: kk "kick" s 1 decay . ;
|
||||
: hh "hihat" s 0.5 gain 0.5 decay . ;
|
||||
```
|
||||
|
||||
Steps become simple:
|
||||
|
||||
```forth
|
||||
kk
|
||||
0.5 at hh
|
||||
```
|
||||
|
||||
## Effect Presets
|
||||
|
||||
```forth
|
||||
: dark 800 lpf 0.6 lpq ;
|
||||
: wet 0.7 verb 8 verbdiff ;
|
||||
```
|
||||
|
||||
```forth
|
||||
c4 note saw s dark wet .
|
||||
```
|
||||
52
docs/grid.md
Normal file
52
docs/grid.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# The Sequencer Grid
|
||||
|
||||
The sequencer grid is the main view of Cagire. This is the one you see when you open the application. On this view, you can see the step sequencer grid and edit each step using the `code editor`. At the top, you can optionally display an oscilloscope and a spectrum analyzer.
|
||||
|
||||
## Navigation
|
||||
|
||||
Use arrow keys to move between steps. The grid wraps around at pattern boundaries. You can move in any direction.
|
||||
|
||||
## Preview
|
||||
|
||||
Press `P` to enter preview mode. In preview mode, a view-only code editor opens so that you can see the script of the currently playing step. While in preview mode, you can still move around the grid. Press `Esc` to exit preview mode.
|
||||
|
||||
## Selection
|
||||
|
||||
Hold `Shift` while pressing arrow keys to select multiple steps. Press `Esc` to clear the selection.
|
||||
|
||||
## Editing Steps
|
||||
|
||||
- `Enter` - Open the script editor.
|
||||
- `t` - Toggle step active/inactive.
|
||||
- `r` - Rename a step.
|
||||
- `Del` - Delete selected steps.
|
||||
|
||||
## Copy & Paste
|
||||
|
||||
- `Ctrl+C` - Copy selected steps.
|
||||
- `Ctrl+V` - Paste as copies.
|
||||
- `Ctrl+B` - Paste as linked steps.
|
||||
- `Ctrl+D` - Duplicate selection.
|
||||
- `Ctrl+H` - Harden links (convert to copies).
|
||||
|
||||
`Linked steps` share the same script as their source. When you edit the source, all linked steps update automatically. This is an extremely important and powerful feature. It allows you to create complex patterns with minimal effort. `Ctrl+H` is your best friend to manage linked steps and convert them to real steps.
|
||||
|
||||
## Pattern Controls
|
||||
|
||||
- `<` / `>` - Decrease/increase pattern length
|
||||
- `[` / `]` - Decrease/increase pattern speed
|
||||
- `L` - Set length directly
|
||||
- `S` - Set speed directly
|
||||
|
||||
## Playback
|
||||
|
||||
- `Space` - Toggle play/stop
|
||||
- `+` / `-` - Adjust tempo
|
||||
- `T` - Set tempo directly
|
||||
- `Ctrl+R` - Run current step once (preview)
|
||||
|
||||
## Visual Indicators
|
||||
|
||||
- **Highlighted cell** - Currently playing step
|
||||
- **Colored backgrounds** - Linked steps share colors by source
|
||||
- **Arrow prefix** (`→05`) - Step is linked to step 05
|
||||
58
docs/how_it_works.md
Normal file
58
docs/how_it_works.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# How Does It Work?
|
||||
|
||||
Cagire is a step sequencer where each step contains a **Forth script** instead of the typical note data. When the sequencer reaches a step, it runs the script. A script _can do whatever it is programed to do_, such as producing sound commands sent to an internal audio engine. Everything else is similar to a step sequencer: you can `toggle` / `untoggle`, `copy` / `paste` any step or group of steps, etc. You are completely free to define what your scripts will do. It can be as simple as playing a note, or as complex as triggering random audio samples with complex effects. Scripts can also share code and data with each other.
|
||||
|
||||
## Project / session organization
|
||||
|
||||
Cagire can run multiple patterns concurrently. Each pattern contains a given number of steps. Every session / project is organized hierarchically:
|
||||
|
||||
- **32 Banks**
|
||||
- **32 Patterns** per bank
|
||||
- **1024 Steps** per pattern
|
||||
|
||||
That's over 1,000,000 possible steps per project. Most of my sessions use 15-20 at best.
|
||||
|
||||
## What does a script look like?
|
||||
|
||||
Forth is a stack-based programming language. It is very minimalistic and emphasizes simplicity and readability. Using Forth doesn't feel like programming at all. It feels more like juggling with words and numbers or writing bad computer poetry. There is pretty much no syntax to learn, just a few rules to follow. Forth is ancient, powerful, flexible, and... super fun to live code with! Here is a minimal program that will play a middle C note using a sine wave:
|
||||
|
||||
```forth
|
||||
c4 note sine sound .
|
||||
```
|
||||
|
||||
Read the program backwards and you will understand what it does instantly:
|
||||
|
||||
- `.`: we want to play a sound.
|
||||
- `sine sound`: the sound is a sinewave.
|
||||
- `c4 note`: the pitch is C4 (middle-C).
|
||||
|
||||
Scripts can be simple one-liners or complex programs with conditionals, loops, and randomness. They tend to look like an accumulation of words and numbers. Use space and line returns to your advantage. The Forth language can be learned... on the spot. You just need to understand the following basic rules:
|
||||
|
||||
- there are `words` and `numbers`.
|
||||
- they are delimited by spaces.
|
||||
- everything piles up on the `stack`.
|
||||
|
||||
Obviously you will need to understand what the **stack** is, but it will take you five minutes. That's it. See the **Forth** section for details.
|
||||
|
||||
## The Audio Engine
|
||||
|
||||
Cagire includes a complete synthesis engine. No external software is required to play music. It comes with a large number of sound sources and sound shaping tools: oscillators, sample players, effects, filters, and more. The audio engine is quite capable and versatile, and can accomodate a vast array of genres / styles. Here are a few examples :
|
||||
|
||||
```forth
|
||||
;; sawtooth wave with lowpass filter, chorus and reverb
|
||||
saw sound 1200 lpf 0.2 chorus 0.8 verb .
|
||||
```
|
||||
|
||||
```forth
|
||||
;; pure sine wave with vibrato and bit crushing
|
||||
0.5 vibmod 4 vib sine sound 8 crush 0.8 gain .
|
||||
```
|
||||
|
||||
```forth
|
||||
;; very loud and pitched-down kick drum using an audio sample
|
||||
kkick sound 1.5 distort 0.9 postgain 0.8 speed .
|
||||
```
|
||||
|
||||
## Timing & Synchronization
|
||||
|
||||
Cagire uses **Ableton Link** to manage timing and synchronization. This means that all devices using the same protocol can be synchronized to the same tempo. Most commercial softwares support this protocol. The playback speed is defined as a BPM (beats per minute) value. Patterns can run at different speeds relative to the master tempo. Most of the durations in Cagire are defined in terms of beats.
|
||||
@@ -1,58 +0,0 @@
|
||||
# Keybindings
|
||||
|
||||
## Navigation
|
||||
|
||||
- **Ctrl+Left/Right**: Switch between pages (Main, Audio, Doc)
|
||||
- **q**: Quit (with confirmation)
|
||||
|
||||
## Main Page - Sequencer Focus
|
||||
|
||||
- **Arrow keys**: Navigate steps in pattern
|
||||
- **Enter**: Toggle step active/inactive
|
||||
- **Tab**: Switch focus to editor
|
||||
- **Space**: Play/pause
|
||||
|
||||
### Pattern Controls
|
||||
|
||||
- **< / >**: Decrease/increase pattern length
|
||||
- **[ / ]**: Decrease/increase pattern speed
|
||||
- **p**: Open pattern picker
|
||||
- **b**: Open bank picker
|
||||
|
||||
### Slots
|
||||
|
||||
- **1-8**: Toggle slot on/off
|
||||
- **g**: Queue current pattern to first free slot
|
||||
- **G**: Queue removal of current pattern from its slot
|
||||
|
||||
### Files
|
||||
|
||||
- **s**: Save project
|
||||
- **l**: Load project
|
||||
- **Ctrl+C**: Copy step script
|
||||
- **Ctrl+V**: Paste step script
|
||||
|
||||
### Tempo
|
||||
|
||||
- **+ / =**: Increase tempo
|
||||
- **-**: Decrease tempo
|
||||
|
||||
## Main Page - Editor Focus
|
||||
|
||||
- **Tab / Esc**: Return to sequencer focus
|
||||
- **Ctrl+E**: Compile current step script
|
||||
|
||||
## Audio Page
|
||||
|
||||
- **h**: Hush (stop all sounds gracefully)
|
||||
- **p**: Panic (kill all sounds immediately)
|
||||
- **r**: Reset peak voice counter
|
||||
- **t**: Test sound (plays 440Hz sine)
|
||||
- **Space**: Play/pause
|
||||
|
||||
## Doc Page
|
||||
|
||||
- **j / Down**: Next topic
|
||||
- **k / Up**: Previous topic
|
||||
- **PgDn**: Scroll content down
|
||||
- **PgUp**: Scroll content up
|
||||
68
docs/midi_input.md
Normal file
68
docs/midi_input.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# MIDI Input
|
||||
|
||||
Read incoming MIDI control change values with the `ccval` word. This lets you use hardware controllers to modulate parameters in your scripts.
|
||||
|
||||
## Reading CC Values
|
||||
|
||||
The `ccval` word takes a CC number and channel from the stack, and returns the last received value:
|
||||
|
||||
```forth
|
||||
1 1 ccval ;; read CC 1 (mod wheel) on channel 1
|
||||
```
|
||||
|
||||
Stack effect: `(cc chan -- val)`
|
||||
|
||||
The returned value is `0`-`127`. If no message has been received for that CC/channel combination, the value is `0`.
|
||||
|
||||
## Device Selection
|
||||
|
||||
Use `dev` to select which input device slot to read from:
|
||||
|
||||
```forth
|
||||
1 dev 1 1 ccval ;; read from device slot 1
|
||||
```
|
||||
|
||||
Device defaults to `0` if not specified.
|
||||
|
||||
## Practical Examples
|
||||
|
||||
Map a controller knob to filter cutoff:
|
||||
|
||||
```forth
|
||||
74 1 ccval 127 / 200 2740 range lpf
|
||||
```
|
||||
|
||||
Use mod wheel for vibrato depth:
|
||||
|
||||
```forth
|
||||
1 1 ccval 127 / 0 0.5 range vibdepth
|
||||
```
|
||||
|
||||
Crossfade between two sounds:
|
||||
|
||||
```forth
|
||||
1 1 ccval 127 / ;; normalize to 0.0-1.0
|
||||
dup saw s swap gain .
|
||||
1 swap - tri s swap gain .
|
||||
```
|
||||
|
||||
## Scaling Values
|
||||
|
||||
CC values are integers `0`-`127`. Normalize to `0.0`-`1.0` first, then use `range` to scale:
|
||||
|
||||
```forth
|
||||
;; normalize to 0.0-1.0
|
||||
74 1 ccval 127 /
|
||||
|
||||
;; scale to custom range (e.g., 200-4000)
|
||||
74 1 ccval 127 / 200 4000 range
|
||||
|
||||
;; bipolar range (-1.0 to 1.0)
|
||||
74 1 ccval 127 / -1 1 range
|
||||
```
|
||||
|
||||
The `range` word takes a normalized value (`0.0`-`1.0`) and scales it to your target range: `(val min max -- scaled)`.
|
||||
|
||||
## Latency
|
||||
|
||||
CC values are sampled at the start of each step. Changes during a step take effect on the next step. For smoothest results, turn knobs slowly or use higher step rates.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user