180 Commits

Author SHA1 Message Date
4a8396670f Feat: lissajous
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s
2026-02-23 22:06:09 +01:00
77b7fa1f9e Feat: fixing stderr catching and scope not drawing completely 2026-02-23 21:53:53 +01:00
979b7639ac Feat: new harmony / melodic words and demo
Some checks failed
Deploy Website / deploy (push) Failing after 4m49s
2026-02-23 02:25:32 +01:00
2a2b3c5651 Feat: fixes and demo
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s
2026-02-23 01:18:43 +01:00
f6c7438886 Fix: revert optimizations 2026-02-23 00:51:01 +01:00
057ba5b2f3 Feat: demo songs 2026-02-22 23:50:35 +01:00
40e69b66da Feat: script execution performance optimization
Some checks failed
Deploy Website / deploy (push) Failing after 4m52s
2026-02-22 14:16:38 +01:00
1ce5b8597a Feat: cleanup
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s
2026-02-22 13:28:03 +01:00
789dbb186b Feat: CHANGELOG updates 2026-02-22 12:55:58 +01:00
8ba98e8f3b Feat: introduce follow up actions
Some checks failed
Deploy Website / deploy (push) Failing after 4m49s
2026-02-22 03:59:09 +01:00
003ee0518e Feat: WIP pattern view redesign
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s
2026-02-22 03:26:48 +01:00
52406c7374 Feat: add wave word for drum synthesis
Some checks failed
Deploy Website / deploy (push) Failing after 4m51s
2026-02-21 22:03:07 +01:00
0b78f15ef1 Feat: fixing some errors in the documentation 2026-02-21 18:23:31 +01:00
302f40c4ac Feat: better UI in the main view
Some checks failed
Deploy Website / deploy (push) Failing after 4m49s
2026-02-21 16:21:29 +01:00
79a4c3b6e2 Feat: saving screen during perfs 2026-02-21 15:56:52 +01:00
12b90bc99b Feat: update CHANGELOG
Some checks failed
Deploy Website / deploy (push) Failing after 4m47s
2026-02-21 15:07:03 +01:00
a1190af494 Feat: clean the codebase as much as possible
Some checks failed
Deploy Website / deploy (push) Failing after 4m51s
2026-02-21 14:46:53 +01:00
f85a20d9a7 Feat: make some stuff optional for the CLAP/VST version 2026-02-21 13:23:43 +01:00
baa2aba381 Clean plugins
Some checks failed
Deploy Website / deploy (push) Failing after 4m48s
2026-02-21 01:27:32 +01:00
75a8fd4401 Trying to clena the mess opened by plugins
Some checks failed
Deploy Website / deploy (push) Failing after 4m53s
2026-02-21 01:03:55 +01:00
ac0ddc7fb9 WIP: rename to cagire-plugins 2026-02-20 22:31:13 +01:00
07e95d5b6f WIP: fix VST3 version 2026-02-20 22:26:35 +01:00
00d6eb2f1f WIP: clap 2026-02-20 22:14:21 +01:00
12752e0167 Cargo to github
Some checks failed
Deploy Website / deploy (push) Failing after 4m52s
2026-02-19 16:51:39 +01:00
3b41a06d5e Feat: continue to improve documentation
Some checks failed
Deploy Website / deploy (push) Failing after 4m48s
2026-02-17 00:51:56 +01:00
f258358c8f Feat: collapsible help 2026-02-16 23:43:25 +01:00
2d8abe4af9 Feat: documentation
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s
2026-02-16 23:19:06 +01:00
37f5f74ec1 Feat: refactoring codebase 2026-02-16 16:26:57 +01:00
58624b64cf Feat: refactoring codebase 2026-02-16 16:00:57 +01:00
5385bf675a Feat: fixing ratatui big-text and UX
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s
2026-02-16 15:43:22 +01:00
211e71f5a9 Feat: UI / UX 2026-02-16 01:22:40 +01:00
23c7abb145 Feat: improving MIDI 2026-02-15 19:06:49 +01:00
670ae0b6b6 Feat: lots of things, preparing for live gig
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s
2026-02-15 11:23:11 +01:00
10ca567ac5 Feat: early mouse support 2026-02-14 16:26:29 +01:00
b2871ac251 Feat: F1 F2 F3
Some checks failed
Deploy Website / deploy (push) Failing after 4m53s
2026-02-14 15:13:21 +01:00
8ba89f91a0 Fixes 2026-02-10 23:51:17 +01:00
7d670dacb9 Re-update cargo 2026-02-10 21:42:24 +01:00
1de8c068f6 Feat: all engine params use varargs and can eat the stack, document it as such 2026-02-10 19:41:59 +01:00
d792f011ee Feat: rescale spectrum 2026-02-10 19:32:51 +01:00
897f1a776e Feat: reverb words 2026-02-10 19:27:11 +01:00
869d3af244 Feat: entretien de la codebase 2026-02-09 21:12:49 +01:00
a5f17687f1 chore: Release 2026-02-08 13:57:52 +01:00
5b851751e5 Feat: update the CHANGELOG.md correctly 2026-02-08 13:57:25 +01:00
bc5d12e53a Feat: lots of improvements
Some checks failed
Deploy Website / deploy (push) Failing after 4m49s
2026-02-08 13:52:40 +01:00
d6bbae173b Feat: improve website
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s
2026-02-08 02:57:41 +01:00
1f339f1503 Small corrections
Some checks failed
Deploy Website / deploy (push) Failing after 4m51s
2026-02-08 01:33:50 +01:00
8ffe2c22c7 Feat: comfort features 2026-02-08 00:46:56 +01:00
20c32ce0d8 Prepare v0.0.8 release
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s
2026-02-07 13:14:14 +01:00
a326d58d30 Feat: restore Cargo.toml to git version 2026-02-07 13:07:56 +01:00
c72733bac8 WIP: prepare the ground for audio rate modulation 2026-02-07 12:08:11 +01:00
5758b18d58 Feat: trying to get rid of some sequencer bugs 2026-02-07 01:24:38 +01:00
52cc890a67 Feat: website WIP and new words
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s
2026-02-06 16:19:09 +01:00
0f9d750069 Feat: trying to improve bundling and compilation 2026-02-06 00:46:40 +01:00
66ee2e28ff Words and universal macOS installer
Some checks failed
Deploy Website / deploy (push) Failing after 4m48s
2026-02-06 00:37:08 +01:00
6ec3a86568 New themes 2026-02-06 00:19:16 +01:00
51f52be4ce Feat: optimizations 2026-02-05 23:15:46 +01:00
2c98a915fa Space on all views
Some checks failed
Deploy Website / deploy (push) Failing after 4m47s
2026-02-05 18:57:09 +01:00
e42476dd4d Feat: rework audio sample library viewer 2026-02-05 18:37:32 +01:00
3e364a6622 chore: Release
Some checks failed
Deploy Website / deploy (push) Failing after 4m47s
2026-02-05 15:56:52 +01:00
1248f74b25 Feat: update CHANGELOG.md 2026-02-05 15:56:27 +01:00
fc2ab0757b Feat: update CHANGELOG.md
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s
2026-02-05 14:36:12 +01:00
10ed5a629a Feat: background head-preload for sample libraries 2026-02-05 14:35:26 +01:00
88c2b51720 Feat: introduce Forth words for 3-OP Fm synthesis (with feedback)
Some checks failed
Deploy Website / deploy (push) Failing after 4m52s
2026-02-05 12:00:00 +01:00
5cda1a8f95 chore: Release
Some checks failed
Deploy Website / deploy (push) Failing after 4m48s
2026-02-05 01:40:51 +01:00
200832f230 Feat: update CHANGELOG.md before release 2026-02-05 01:40:06 +01:00
91bc9011b2 Feat: new euclidean words and sugar for floating point numbers
Some checks failed
Deploy Website / deploy (push) Failing after 4m47s
2026-02-05 01:30:34 +01:00
de56598fca Feat: prelude and new words
Some checks failed
Deploy Website / deploy (push) Failing after 4m48s
2026-02-05 00:58:53 +01:00
abafea8ddf Feat: refactoring by breaking words in multiple files
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s
2026-02-04 23:50:38 +01:00
e6f776bdf4 Feat: tri is now triangle (disambiguation) 2026-02-04 20:34:37 +01:00
d40d713649 Feat: really good lookahead mechanism for scheduling
Some checks failed
Deploy Website / deploy (push) Failing after 4m50s
2026-02-04 20:28:42 +01:00
767575b25d Removing lookahead concept 2026-02-04 20:01:17 +01:00
82b0668bcf Some kind of refactoring 2026-02-04 19:35:30 +01:00
6cf9d2eec1 Ungoing refactoring 2026-02-04 18:47:40 +01:00
2097997372 Feat: tweak and fix from last night workshop
Some checks failed
Deploy Website / deploy (push) Failing after 4m46s
2026-02-04 09:37:29 +01:00
5579708f69 Feat: add tachyonFX animations 2026-02-04 00:40:15 +01:00
1b01491e87 Fix: prevent 0 division error when loading project 2026-02-03 23:41:27 +01:00
5581ba1881 chore: Release 2026-02-03 17:03:58 +01:00
8983b3f21c Fix: dict popup in editor is less intrusive
Some checks failed
Deploy Website / deploy (push) Failing after 4m46s
2026-02-03 17:02:07 +01:00
4a7ae83019 Fix: desktop build
Some checks failed
Deploy Website / deploy (push) Failing after 4m47s
2026-02-03 16:00:26 +01:00
61a6d7aad0 Fix: simpler scheduling
Some checks failed
Deploy Website / deploy (push) Has been cancelled
2026-02-03 15:55:43 +01:00
1b01e3b805 WIP: improve Linux audio support
Some checks failed
Deploy Website / deploy (push) Failing after 4m46s
2026-02-03 14:42:03 +01:00
2a57cc415b Fix: JACK stuff
Some checks failed
Deploy Website / deploy (push) Failing after 4m46s
2026-02-03 14:23:24 +01:00
7c76bdb8d6 clamp audio options
Some checks failed
Deploy Website / deploy (push) Failing after 4m48s
2026-02-03 14:14:28 +01:00
1facc72a67 Fix Linux audio: enable JACK support and RT priority for audio callback
Some checks failed
Deploy Website / deploy (push) Failing after 4m47s
2026-02-03 14:04:34 +01:00
726ea16e92 Wip 2026-02-03 13:52:36 +01:00
154cac6547 Again 2026-02-03 03:25:31 +01:00
3380e454df Again 2026-02-03 03:08:13 +01:00
660f48216a Still searching... 2026-02-03 02:53:34 +01:00
fb1f73ebd6 WIP: not sure 2026-02-03 02:31:55 +01:00
cd223592a7 Insane linux fixes
Some checks failed
Deploy Website / deploy (push) Failing after 4m45s
2026-02-03 01:15:07 +01:00
af81c94207 WIP: even more crazy linux optimizations
Some checks failed
Deploy Website / deploy (push) Failing after 4m46s
2026-02-03 00:38:46 +01:00
b53e4a76ab WIP: optimizations for linux
Some checks failed
Deploy Website / deploy (push) Failing after 4m48s
2026-02-03 00:16:31 +01:00
8c31ed4196 Another round of optimization
Some checks failed
Deploy Website / deploy (push) Failing after 4m47s
2026-02-02 22:16:00 +01:00
8024c18bb0 Less memory allocations at runtime 2026-02-02 21:55:10 +01:00
194030d953 fixing linux stuff
Some checks failed
Deploy Website / deploy (push) Failing after 4m47s
2026-02-02 19:26:01 +01:00
e4799c1f42 Merge branch 'main' of github.com:Bubobubobubobubo/cagire
Some checks failed
Deploy Website / deploy (push) Failing after 4m49s
2026-02-02 19:12:37 +01:00
636129688d lookahead 2026-02-02 19:12:32 +01:00
a2ee0e5a50 Fix: Copy register handling for cagire-desktop (Linux) 2026-02-02 18:25:02 +01:00
96ed74c6fe Fix: CPAL version mismatch 2026-02-02 18:08:55 +01:00
a67d982fcd Pattern mute and so on 2026-02-02 16:27:11 +01:00
c9ab7a4f0b chore: Release 2026-02-02 13:44:47 +01:00
772d21a8ed Feat: update CHANGELOG.md
Some checks failed
Deploy Website / deploy (push) Failing after 4m47s
2026-02-02 13:42:42 +01:00
4396147a8b Euclidean + hue rotation
Some checks failed
Deploy Website / deploy (push) Has been cancelled
2026-02-02 13:25:27 +01:00
c396c39b6b Fix layout 2026-02-02 12:18:22 +01:00
f6b43cb021 Add double-stack words (2dup, 2drop, 2swap, 2over) and forget
Some checks failed
Deploy Website / deploy (push) Failing after 4m49s
2026-02-02 07:46:39 +01:00
60d1d7ca74 Feat: update website to prevent ugliness
Some checks failed
Deploy Website / deploy (push) Failing after 4m48s
2026-02-02 01:38:21 +01:00
9864cc6d61 Update changelog for v0.0.3 2026-02-02 01:12:49 +01:00
985ab687d7 chore: Release
Some checks failed
Deploy Website / deploy (push) Failing after 4m46s
CI / build (cagire-linux-x86_64, ubuntu-latest, x86_64-unknown-linux-gnu) (push) Failing after 12m15s
CI / build (cagire-macos-aarch64, macos-14, aarch64-apple-darwin) (push) Has been cancelled
CI / build (cagire-macos-x86_64, macos-15-intel, x86_64-apple-darwin) (push) Has been cancelled
CI / build (cagire-windows-x86_64, windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
CI / release (push) Has been cancelled
2026-02-02 01:09:13 +01:00
9b925d881e Feat: update changelog
Some checks failed
Deploy Website / deploy (push) Has been cancelled
2026-02-02 01:08:33 +01:00
71146c7cea Feat: more predictable projet load behavior
Some checks failed
Deploy Website / deploy (push) Failing after 4m48s
2026-02-02 01:01:01 +01:00
6b95f31afd Feat: polyphony + iterator reset
Some checks failed
Deploy Website / deploy (push) Failing after 4m48s
2026-02-02 00:33:46 +01:00
adee8d0d57 Feat: adding some basic music theory
Some checks failed
Deploy Website / deploy (push) Failing after 4m49s
2026-02-01 16:15:09 +01:00
f9c284effd Feat: adding logrand and exprand 2026-02-01 15:16:20 +01:00
57fd51be3e Fix release.toml format
Some checks failed
Deploy Website / deploy (push) Failing after 4m47s
CI / build (cagire-linux-x86_64, ubuntu-latest, x86_64-unknown-linux-gnu) (push) Failing after 12m21s
CI / build (cagire-macos-aarch64, macos-14, aarch64-apple-darwin) (push) Has been cancelled
CI / build (cagire-macos-x86_64, macos-15-intel, x86_64-apple-darwin) (push) Has been cancelled
CI / build (cagire-windows-x86_64, windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
CI / release (push) Has been cancelled
2026-02-01 14:05:55 +01:00
ce70251057 Feat: work on metadata and packaging
Some checks failed
Deploy Website / deploy (push) Failing after 4m48s
2026-02-01 14:00:10 +01:00
b47c789612 Feat: continue refactoring
Some checks failed
Deploy Website / deploy (push) Failing after 4m48s
2026-02-01 13:39:25 +01:00
dd853b8e1b Feat: begin slight refactoring
Some checks failed
Deploy Website / deploy (push) Failing after 4m46s
2026-02-01 12:38:48 +01:00
a0585b0814 MIDI Documentation and optional mouse event support
Some checks failed
Deploy Website / deploy (push) Failing after 4m45s
2026-02-01 00:51:56 +01:00
2100b82dad More robust midi implementation
Some checks failed
Deploy Website / deploy (push) Failing after 4m58s
2026-01-31 23:58:57 +01:00
15a4300db5 better quality midi 2026-01-31 23:23:36 +01:00
fed39c01e8 Lots + MIDI implementation 2026-01-31 23:13:51 +01:00
0a4f1419eb Fix: continue to fix release build and CI 2026-01-31 19:58:21 +01:00
793c83e18c Fix: again CI breaks 2026-01-31 18:04:11 +01:00
20bc0ffcb4 Fixing builds and workflows 2026-01-31 17:52:44 +01:00
8e09fd106e Remove emit_n tests (feature not implemented) 2026-01-31 17:37:00 +01:00
73ca0ff096 Add Windows/Linux desktop bundles to CI 2026-01-31 17:24:41 +01:00
425f1c8627 CI build versions 2026-01-31 16:35:38 +01:00
730332cfb0 Work on documentation
Some checks failed
Deploy Website / deploy (push) Failing after 6s
2026-01-31 15:03:20 +01:00
1d70a83759 Work on documentation 2026-01-31 14:31:44 +01:00
0299012725 Work on documentation 2026-01-31 13:46:43 +01:00
08029ec604 Working on internal documentation
Some checks failed
Deploy Website / deploy (push) Failing after 7s
2026-01-31 02:41:05 +01:00
4f9b1f39f9 Write some amount of documentation 2026-01-31 01:46:18 +01:00
4772b02f77 Feat: fix scope / spectrum / vumeter
Some checks failed
Deploy Website / deploy (push) Failing after 6s
2026-01-30 21:50:00 +01:00
4049c7787c Feat: extend CI to cover desktop 2026-01-30 21:19:48 +01:00
4c635500dd Feat: extend CI to cover desktop 2026-01-30 20:34:34 +01:00
d0e37e13e6 Feat: README update
Some checks failed
Deploy Website / deploy (push) Failing after 6s
2026-01-30 20:28:43 +01:00
7658cf9d51 Feat: add icon and reorganize desktop.rs
Some checks failed
Deploy Website / deploy (push) Failing after 7s
2026-01-30 20:27:08 +01:00
584dbb6aad Fixing color schemes 2026-01-30 20:15:43 +01:00
2731eea037 Monster commit: native version 2026-01-30 15:03:49 +01:00
22ee5f97e6 More robust workflows for website deployment 2026-01-30 12:39:09 +01:00
5fb059ea20 Corrections
Some checks failed
Deploy Website / deploy (push) Failing after 30s
CI / build (cagire-linux-x86_64, ubuntu-latest, x86_64-unknown-linux-gnu) (push) Failing after 24m12s
CI / build (cagire-macos-aarch64, macos-14, aarch64-apple-darwin) (push) Has been cancelled
CI / build (cagire-macos-x86_64, macos-15-intel, x86_64-apple-darwin) (push) Has been cancelled
CI / build (cagire-windows-x86_64, windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
CI / release (push) Has been cancelled
2026-01-30 12:27:27 +01:00
705d93702b Deplyment 2026-01-30 12:13:38 +01:00
77a6aa9eb7 Feat: ability to rename steps 2026-01-30 11:58:16 +01:00
d25b1317fc WIP: words for wavetable synthesis 2026-01-30 01:55:40 +01:00
2851785e0d WIP: consolidate sampling 2026-01-30 00:04:25 +01:00
a72772c8cc WIP: better precision? 2026-01-29 18:50:54 +01:00
4d22bd5d2b Remi 2026-01-29 12:17:09 +01:00
495bfb3bdc Try to optimize 2026-01-29 11:53:47 +01:00
73db616139 WIP simplify 2026-01-29 09:38:41 +01:00
8efafffaff Cleaning old temporal model 2026-01-29 01:28:57 +01:00
48f5920fed Cleaning language 2026-01-29 01:10:53 +01:00
d106711708 Before going crazy 2026-01-28 18:05:50 +01:00
2be15d11f4 Mixed bag of things 2026-01-28 17:39:41 +01:00
5952807240 wip 2026-01-28 13:54:29 +01:00
0beed16c31 Help modal 2026-01-28 13:22:51 +01:00
c6860105a6 vastly improved selection system 2026-01-28 02:29:17 +01:00
f4eafdf5b2 A ton of bug fixes 2026-01-28 01:09:23 +01:00
935df84920 ok 2026-01-27 15:23:04 +01:00
a3a39ea28e Fixing subtle bugs 2026-01-27 13:40:52 +01:00
574625735b Feat: parameter duration scaling 2026-01-27 12:17:23 +01:00
40c509e295 cleaning 2026-01-27 12:00:34 +01:00
61daa9d79d big commit 2026-01-27 01:04:08 +01:00
9e597258e4 WIP 2026-01-26 12:22:44 +01:00
223679acf8 So much better 2026-01-26 02:24:04 +01:00
2235a4b0a1 Basic search mechanism in editor 2026-01-26 01:25:40 +01:00
2453b78237 Looks better now 2026-01-26 01:02:18 +01:00
fcb6adb6af ok 2026-01-26 00:24:17 +01:00
ce98acacd0 Wip: refacto 2026-01-25 22:17:08 +01:00
d2d6ef5b06 broken 2026-01-25 21:44:08 +01:00
6efcabd32d WIP: menu 2026-01-25 21:37:53 +01:00
250e359fc5 scales 2026-01-25 20:43:12 +01:00
cf5994e604 Loop word 2026-01-24 12:47:19 +01:00
e1aff189cd Flash 2026-01-24 02:16:18 +01:00
b3c56bc56c WIP: half broken 2026-01-24 01:59:51 +01:00
3bb19cbda8 chain word and better save/load UI 2026-01-23 23:36:23 +01:00
42ad77d9ae Reorganize repository 2026-01-23 20:29:44 +01:00
e853e67492 Break down forth implementation properly 2026-01-23 19:36:40 +01:00
f7e6f96cbf words definition 2026-01-23 11:15:15 +01:00
8af64fc4e2 trace 2026-01-23 10:37:48 +01:00
183dd5b516 spectrum 2026-01-23 01:42:07 +01:00
268 changed files with 10812 additions and 17416 deletions

View File

@@ -1,10 +0,0 @@
[env]
MACOSX_DEPLOYMENT_TARGET = "12.0"
[alias]
xtask = "run --package xtask --release --"
[target.x86_64-pc-windows-gnu]
rustflags = [
"-C", "link-args=-Wl,-Bstatic -lstdc++ -lgcc -lgcc_eh -lpthread -Wl,-Bdynamic -lmingwex -lmsvcrt -lws2_32 -liphlpapi -lwinmm -lole32 -loleaut32 -luuid -lkernel32",
]

View File

@@ -1,135 +0,0 @@
name: Assemble macOS Universal
on:
workflow_call:
jobs:
assemble:
runs-on: macos-14
timeout-minutes: 10
steps:
- name: Download macOS artifacts
uses: actions/download-artifact@v4
with:
pattern: cagire-macos-*
path: artifacts
- name: Create universal CLI binary
run: |
lipo -create \
artifacts/cagire-macos-x86_64/cagire \
artifacts/cagire-macos-aarch64/cagire \
-output cagire
chmod +x cagire
lipo -info cagire
- name: Create universal app bundle
run: |
cd artifacts/cagire-macos-aarch64-desktop
unzip Cagire.app.zip
cd ../cagire-macos-x86_64-desktop
unzip Cagire.app.zip
cd ../..
cp -R artifacts/cagire-macos-aarch64-desktop/Cagire.app Cagire.app
lipo -create \
artifacts/cagire-macos-x86_64-desktop/Cagire.app/Contents/MacOS/cagire-desktop \
artifacts/cagire-macos-aarch64-desktop/Cagire.app/Contents/MacOS/cagire-desktop \
-output Cagire.app/Contents/MacOS/cagire-desktop
lipo -info Cagire.app/Contents/MacOS/cagire-desktop
zip -r Cagire.app.zip Cagire.app
- name: Create universal CLAP plugin
run: |
mkdir -p cagire-plugins.clap/Contents/MacOS
cp artifacts/cagire-macos-aarch64-clap/cagire-plugins.clap/Contents/Info.plist \
cagire-plugins.clap/Contents/ 2>/dev/null || true
cp artifacts/cagire-macos-aarch64-clap/cagire-plugins.clap/Contents/PkgInfo \
cagire-plugins.clap/Contents/ 2>/dev/null || true
lipo -create \
artifacts/cagire-macos-x86_64-clap/cagire-plugins.clap/Contents/MacOS/cagire-plugins \
artifacts/cagire-macos-aarch64-clap/cagire-plugins.clap/Contents/MacOS/cagire-plugins \
-output cagire-plugins.clap/Contents/MacOS/cagire-plugins
lipo -info cagire-plugins.clap/Contents/MacOS/cagire-plugins
- name: Create universal VST3 plugin
run: |
mkdir -p cagire-plugins.vst3/Contents/MacOS
cp -R artifacts/cagire-macos-aarch64-vst3/cagire-plugins.vst3/Contents/Info.plist \
cagire-plugins.vst3/Contents/ 2>/dev/null || true
cp artifacts/cagire-macos-aarch64-vst3/cagire-plugins.vst3/Contents/PkgInfo \
cagire-plugins.vst3/Contents/ 2>/dev/null || true
cp -R artifacts/cagire-macos-aarch64-vst3/cagire-plugins.vst3/Contents/Resources \
cagire-plugins.vst3/Contents/ 2>/dev/null || true
lipo -create \
artifacts/cagire-macos-x86_64-vst3/cagire-plugins.vst3/Contents/MacOS/cagire-plugins \
artifacts/cagire-macos-aarch64-vst3/cagire-plugins.vst3/Contents/MacOS/cagire-plugins \
-output cagire-plugins.vst3/Contents/MacOS/cagire-plugins
lipo -info cagire-plugins.vst3/Contents/MacOS/cagire-plugins
- uses: actions/checkout@v4
with:
sparse-checkout: |
assets/DMG-README.txt
scripts/make-dmg.sh
clean: false
- name: Create DMG
run: |
chmod +x scripts/make-dmg.sh
scripts/make-dmg.sh Cagire.app .
- name: Build .pkg installer
run: |
VERSION="${GITHUB_REF_NAME#v}"
mkdir -p pkg-root/Applications pkg-root/usr/local/bin
cp -R Cagire.app pkg-root/Applications/
cp cagire pkg-root/usr/local/bin/
pkgbuild --analyze --root pkg-root component.plist
plutil -replace BundleIsRelocatable -bool NO component.plist
pkgbuild --root pkg-root --identifier com.sova.cagire \
--version "$VERSION" --install-location / \
--component-plist component.plist \
"Cagire-${VERSION}-universal.pkg"
- name: Upload universal CLI
uses: actions/upload-artifact@v4
with:
name: cagire-macos-universal
path: cagire
- name: Upload universal app bundle
uses: actions/upload-artifact@v4
with:
name: cagire-macos-universal-desktop
path: Cagire.app.zip
- name: Prepare universal plugin staging
run: |
mkdir -p staging/clap staging/vst3
cp -R cagire-plugins.clap staging/clap/
cp -R cagire-plugins.vst3 staging/vst3/
- name: Upload universal CLAP plugin
uses: actions/upload-artifact@v4
with:
name: cagire-macos-universal-clap
path: staging/clap/
- name: Upload universal VST3 plugin
uses: actions/upload-artifact@v4
with:
name: cagire-macos-universal-vst3
path: staging/vst3/
- name: Upload DMG
uses: actions/upload-artifact@v4
with:
name: cagire-macos-universal-dmg
path: Cagire-*.dmg
- name: Upload .pkg installer
uses: actions/upload-artifact@v4
with:
name: cagire-macos-universal-pkg
path: Cagire-*-universal.pkg

View File

@@ -1,49 +0,0 @@
name: Build Cross (Linux ARM64)
on:
workflow_call:
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-unknown-linux-gnu
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
key: aarch64-unknown-linux-gnu
- name: Install cross
run: cargo install cross --git https://github.com/cross-rs/cross
- name: Build
run: cross build --release --target aarch64-unknown-linux-gnu
- name: Build desktop
run: cross build --release --features desktop --bin cagire-desktop --target aarch64-unknown-linux-gnu
- name: Upload CLI artifact
uses: actions/upload-artifact@v4
with:
name: cagire-linux-aarch64
path: target/aarch64-unknown-linux-gnu/release/cagire
- name: Upload desktop artifact
uses: actions/upload-artifact@v4
with:
name: cagire-linux-aarch64-desktop
path: target/aarch64-unknown-linux-gnu/release/cagire-desktop

View File

@@ -1,131 +0,0 @@
name: Build Linux
on:
workflow_call:
inputs:
run-tests:
type: boolean
default: false
run-clippy:
type: boolean
default: false
build-packages:
type: boolean
default: false
workflow_dispatch:
inputs:
run-tests:
type: boolean
default: true
run-clippy:
type: boolean
default: true
build-packages:
type: boolean
default: false
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-unknown-linux-gnu
components: clippy
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
key: x86_64-unknown-linux-gnu
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake pkg-config libasound2-dev libclang-dev libjack-dev \
libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev libgl1-mesa-dev \
libx11-dev libx11-xcb-dev libxcursor-dev libxrandr-dev libxi-dev libwayland-dev
- name: Build
run: cargo build --release --target x86_64-unknown-linux-gnu
- name: Build desktop
run: cargo build --release --features desktop --bin cagire-desktop --target x86_64-unknown-linux-gnu
- name: Test
if: inputs.run-tests
run: cargo test --target x86_64-unknown-linux-gnu
- name: Clippy
if: inputs.run-clippy
run: cargo clippy --target x86_64-unknown-linux-gnu -- -D warnings
- name: Install cargo-bundle
if: inputs.build-packages
run: cargo install cargo-bundle
- name: Bundle desktop app
if: inputs.build-packages
run: cargo bundle --release --features desktop --bin cagire-desktop --target x86_64-unknown-linux-gnu
- name: Build AppImages
if: inputs.build-packages
run: |
mkdir -p target/releases
scripts/make-appimage.sh target/x86_64-unknown-linux-gnu/release/cagire x86_64 target/releases
scripts/make-appimage.sh target/x86_64-unknown-linux-gnu/release/cagire-desktop x86_64 target/releases
- name: Bundle CLAP plugin
if: inputs.build-packages
run: cargo xtask bundle cagire-plugins --release --target x86_64-unknown-linux-gnu
- name: Upload CLI artifact
if: inputs.build-packages
uses: actions/upload-artifact@v4
with:
name: cagire-linux-x86_64
path: target/x86_64-unknown-linux-gnu/release/cagire
- name: Upload desktop artifact
if: inputs.build-packages
uses: actions/upload-artifact@v4
with:
name: cagire-linux-x86_64-desktop
path: target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
- name: Upload AppImage artifacts
if: inputs.build-packages
uses: actions/upload-artifact@v4
with:
name: cagire-linux-x86_64-appimage
path: target/releases/*.AppImage
- name: Prepare plugin artifacts
if: inputs.build-packages
run: |
mkdir -p staging/clap staging/vst3
cp -R target/bundled/cagire-plugins.clap staging/clap/
cp -R target/bundled/cagire-plugins.vst3 staging/vst3/
- name: Upload CLAP artifact
if: inputs.build-packages
uses: actions/upload-artifact@v4
with:
name: cagire-linux-x86_64-clap
path: staging/clap/
- name: Upload VST3 artifact
if: inputs.build-packages
uses: actions/upload-artifact@v4
with:
name: cagire-linux-x86_64-vst3
path: staging/vst3/

View File

@@ -1,127 +0,0 @@
name: Build macOS
on:
workflow_call:
inputs:
run-tests:
type: boolean
default: false
run-clippy:
type: boolean
default: false
build-packages:
type: boolean
default: false
matrix:
type: string
default: '[{"os":"macos-14","target":"aarch64-apple-darwin","artifact":"cagire-macos-aarch64"}]'
workflow_dispatch:
inputs:
run-tests:
type: boolean
default: true
run-clippy:
type: boolean
default: true
build-packages:
type: boolean
default: false
matrix:
type: string
default: '[{"os":"macos-14","target":"aarch64-apple-darwin","artifact":"cagire-macos-aarch64"}]'
env:
CARGO_TERM_COLOR: always
MACOSX_DEPLOYMENT_TARGET: "12.0"
jobs:
build:
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(inputs.matrix) }}
runs-on: ${{ matrix.os }}
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
components: clippy
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}
- name: Install dependencies
run: brew list cmake &>/dev/null || brew install cmake
- name: Build
run: cargo build --release --target ${{ matrix.target }}
- name: Build desktop
run: cargo build --release --features desktop --bin cagire-desktop --target ${{ matrix.target }}
- name: Test
if: inputs.run-tests
run: cargo test --target ${{ matrix.target }}
- name: Clippy
if: inputs.run-clippy
run: cargo clippy --target ${{ matrix.target }} -- -D warnings
- name: Bundle desktop app
if: inputs.build-packages
run: scripts/make-app-bundle.sh ${{ matrix.target }}
- name: Bundle CLAP plugin
if: inputs.build-packages
run: cargo xtask bundle cagire-plugins --release --target ${{ matrix.target }}
- name: Zip macOS app bundle
if: inputs.build-packages
run: |
cd target/${{ matrix.target }}/release/bundle/osx
zip -r Cagire.app.zip Cagire.app
- name: Upload CLI artifact
if: inputs.build-packages
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: target/${{ matrix.target }}/release/cagire
- name: Upload desktop artifact
if: inputs.build-packages
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-desktop
path: target/${{ matrix.target }}/release/bundle/osx/Cagire.app.zip
- name: Prepare plugin artifacts
if: inputs.build-packages
run: |
mkdir -p staging/clap staging/vst3
cp -R target/bundled/cagire-plugins.clap staging/clap/
cp -R target/bundled/cagire-plugins.vst3 staging/vst3/
- name: Upload CLAP artifact
if: inputs.build-packages
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-clap
path: staging/clap/
- name: Upload VST3 artifact
if: inputs.build-packages
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-vst3
path: staging/vst3/

View File

@@ -1,56 +0,0 @@
name: Build Plugins Linux
on:
workflow_call:
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-unknown-linux-gnu
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
key: x86_64-unknown-linux-gnu-plugins
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake pkg-config libasound2-dev libclang-dev libjack-dev \
libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev libgl1-mesa-dev \
libx11-dev libx11-xcb-dev libxcursor-dev libxrandr-dev libxi-dev libwayland-dev
- name: Build plugins
run: cargo xtask bundle cagire-plugins --release --target x86_64-unknown-linux-gnu
- name: Prepare plugin artifacts
run: |
mkdir -p staging/clap staging/vst3
cp -R target/bundled/cagire-plugins.clap staging/clap/
cp -R target/bundled/cagire-plugins.vst3 staging/vst3/
- name: Upload CLAP artifact
uses: actions/upload-artifact@v4
with:
name: plugins-linux-x86_64-clap
path: staging/clap/
- name: Upload VST3 artifact
uses: actions/upload-artifact@v4
with:
name: plugins-linux-x86_64-vst3
path: staging/vst3/

View File

@@ -1,66 +0,0 @@
name: Build Plugins macOS
on:
workflow_call:
inputs:
matrix:
type: string
default: '[{"os":"macos-14","target":"aarch64-apple-darwin","artifact":"plugins-macos-aarch64"},{"os":"macos-15-intel","target":"x86_64-apple-darwin","artifact":"plugins-macos-x86_64"}]'
workflow_dispatch:
inputs:
matrix:
type: string
default: '[{"os":"macos-14","target":"aarch64-apple-darwin","artifact":"plugins-macos-aarch64"}]'
env:
CARGO_TERM_COLOR: always
MACOSX_DEPLOYMENT_TARGET: "12.0"
jobs:
build:
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(inputs.matrix) }}
runs-on: ${{ matrix.os }}
timeout-minutes: 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 }}-plugins
- name: Install dependencies
run: brew list cmake &>/dev/null || brew install cmake
- name: Build plugins
run: cargo xtask bundle cagire-plugins --release --target ${{ matrix.target }}
- name: Prepare plugin artifacts
run: |
mkdir -p staging/clap staging/vst3
cp -R target/bundled/cagire-plugins.clap staging/clap/
cp -R target/bundled/cagire-plugins.vst3 staging/vst3/
- name: Upload CLAP artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-clap
path: staging/clap/
- name: Upload VST3 artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-vst3
path: staging/vst3/

View File

@@ -1,59 +0,0 @@
name: Build Plugins RPi
on:
workflow_call:
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-unknown-linux-gnu
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
key: aarch64-unknown-linux-gnu-plugins
- name: Install cross
run: cargo install cross --git https://github.com/cross-rs/cross
- name: Build plugins
run: cross build --release -p cagire-plugins --target aarch64-unknown-linux-gnu
- name: Prepare plugin artifacts
run: |
mkdir -p target/bundled
# CLAP: single .so renamed to .clap
cp target/aarch64-unknown-linux-gnu/release/libcagire_plugins.so target/bundled/cagire-plugins.clap
# VST3: correct directory structure
mkdir -p "target/bundled/cagire-plugins.vst3/Contents/aarch64-linux"
cp target/aarch64-unknown-linux-gnu/release/libcagire_plugins.so "target/bundled/cagire-plugins.vst3/Contents/aarch64-linux/cagire-plugins.so"
mkdir -p staging/clap staging/vst3
cp -R target/bundled/cagire-plugins.clap staging/clap/
cp -R target/bundled/cagire-plugins.vst3 staging/vst3/
- name: Upload CLAP artifact
uses: actions/upload-artifact@v4
with:
name: plugins-linux-aarch64-clap
path: staging/clap/
- name: Upload VST3 artifact
uses: actions/upload-artifact@v4
with:
name: plugins-linux-aarch64-vst3
path: staging/vst3/

View File

@@ -1,59 +0,0 @@
name: Build Plugins Windows
on:
workflow_call:
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: windows-latest
timeout-minutes: 30
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
key: x86_64-pc-windows-msvc-plugins
- name: Install dependencies
shell: pwsh
run: |
choco install cmake --installargs 'ADD_CMAKE_TO_PATH=System'
echo "C:\Program Files\CMake\bin" >> $env:GITHUB_PATH
- name: Build plugins
run: cargo xtask bundle cagire-plugins --release --target x86_64-pc-windows-msvc
- name: Prepare plugin artifacts
run: |
mkdir -p staging/clap staging/vst3
cp -R target/bundled/cagire-plugins.clap staging/clap/
cp -R target/bundled/cagire-plugins.vst3 staging/vst3/
- name: Upload CLAP artifact
uses: actions/upload-artifact@v4
with:
name: plugins-windows-x86_64-clap
path: staging/clap/
- name: Upload VST3 artifact
uses: actions/upload-artifact@v4
with:
name: plugins-windows-x86_64-vst3
path: staging/vst3/

View File

@@ -1,18 +0,0 @@
name: Build Plugins
on:
workflow_call:
workflow_dispatch:
jobs:
linux:
uses: ./.github/workflows/build-plugins-linux.yml
macos:
uses: ./.github/workflows/build-plugins-macos.yml
windows:
uses: ./.github/workflows/build-plugins-windows.yml
rpi:
uses: ./.github/workflows/build-plugins-rpi.yml

View File

@@ -1,122 +0,0 @@
name: Build Windows
on:
workflow_call:
inputs:
run-tests:
type: boolean
default: false
run-clippy:
type: boolean
default: false
build-packages:
type: boolean
default: false
workflow_dispatch:
inputs:
run-tests:
type: boolean
default: true
run-clippy:
type: boolean
default: true
build-packages:
type: boolean
default: true
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: windows-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
components: clippy
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
key: x86_64-pc-windows-msvc
- name: Install dependencies
run: |
choco install cmake --installargs 'ADD_CMAKE_TO_PATH=System'
echo "C:\Program Files\CMake\bin" >> $env:GITHUB_PATH
- name: Build
run: cargo build --release --target x86_64-pc-windows-msvc
- name: Build desktop
run: cargo build --release --features desktop --bin cagire-desktop --target x86_64-pc-windows-msvc
- name: Test
if: inputs.run-tests
run: cargo test --target x86_64-pc-windows-msvc
- name: Clippy
if: inputs.run-clippy
run: cargo clippy --target x86_64-pc-windows-msvc -- -D warnings
- name: Bundle CLAP plugin
if: inputs.build-packages
run: cargo xtask bundle cagire-plugins --release --target x86_64-pc-windows-msvc
- name: Install cargo-wix
if: inputs.build-packages
run: cargo install cargo-wix
- name: Build MSI installer
if: inputs.build-packages
run: cargo wix --no-build --nocapture --package cagire -C -arch -C x64
- name: Upload CLI artifact
if: inputs.build-packages
uses: actions/upload-artifact@v4
with:
name: cagire-windows-x86_64
path: target/x86_64-pc-windows-msvc/release/cagire.exe
- name: Upload desktop artifact
if: inputs.build-packages
uses: actions/upload-artifact@v4
with:
name: cagire-windows-x86_64-desktop
path: target/x86_64-pc-windows-msvc/release/cagire-desktop.exe
- name: Upload MSI artifact
if: inputs.build-packages
uses: actions/upload-artifact@v4
with:
name: cagire-windows-x86_64-msi
path: target/wix/*.msi
- name: Prepare plugin artifacts
if: inputs.build-packages
run: |
mkdir -p staging/clap staging/vst3
cp -R target/bundled/cagire-plugins.clap staging/clap/
cp -R target/bundled/cagire-plugins.vst3 staging/vst3/
- name: Upload CLAP artifact
if: inputs.build-packages
uses: actions/upload-artifact@v4
with:
name: cagire-windows-x86_64-clap
path: staging/clap/
- name: Upload VST3 artifact
if: inputs.build-packages
uses: actions/upload-artifact@v4
with:
name: cagire-windows-x86_64-vst3
path: staging/vst3/

View File

@@ -1,28 +1,305 @@
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:
linux:
uses: ./.github/workflows/build-linux.yml
with:
run-tests: true
run-clippy: true
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
macos:
uses: ./.github/workflows/build-macos.yml
with:
run-tests: true
run-clippy: true
runs-on: ${{ matrix.os }}
timeout-minutes: 30
windows:
uses: ./.github/workflows/build-windows.yml
with:
run-tests: true
run-clippy: true
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: Bundle CLAP plugin
run: cargo xtask bundle cagire-plugins --release --target ${{ matrix.target }}
- name: Zip macOS app bundle
if: runner.os == 'macOS'
run: |
cd target/${{ matrix.target }}/release/bundle/osx
zip -r Cagire.app.zip Cagire.app
- name: Upload artifact (Unix)
if: runner.os != 'Windows'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: target/${{ matrix.target }}/release/cagire
- name: Upload artifact (Windows)
if: runner.os == 'Windows'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: target/${{ matrix.target }}/release/cagire.exe
- name: Upload desktop artifact (Linux deb)
if: runner.os == 'Linux'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-desktop
path: target/${{ matrix.target }}/release/bundle/deb/*.deb
- name: Upload desktop artifact (macOS app bundle)
if: runner.os == 'macOS'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-desktop
path: target/${{ matrix.target }}/release/bundle/osx/Cagire.app.zip
- name: Upload desktop artifact (Windows exe)
if: runner.os == 'Windows'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-desktop
path: target/${{ matrix.target }}/release/cagire-desktop.exe
- name: Upload CLAP artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-clap
path: target/bundled/cagire-plugins.clap
- name: Upload VST3 artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-vst3
path: target/bundled/cagire-plugins.vst3
universal-macos:
needs: build
if: startsWith(github.ref, 'refs/tags/v')
runs-on: macos-14
timeout-minutes: 10
steps:
- name: Download macOS artifacts
uses: actions/download-artifact@v4
with:
pattern: cagire-macos-*
path: artifacts
- name: Create universal CLI binary
run: |
lipo -create \
artifacts/cagire-macos-x86_64/cagire \
artifacts/cagire-macos-aarch64/cagire \
-output cagire
chmod +x cagire
lipo -info cagire
- name: Create universal app bundle
run: |
cd artifacts/cagire-macos-aarch64-desktop
unzip Cagire.app.zip
cd ../cagire-macos-x86_64-desktop
unzip Cagire.app.zip
cd ../..
cp -R artifacts/cagire-macos-aarch64-desktop/Cagire.app Cagire.app
lipo -create \
artifacts/cagire-macos-x86_64-desktop/Cagire.app/Contents/MacOS/cagire-desktop \
artifacts/cagire-macos-aarch64-desktop/Cagire.app/Contents/MacOS/cagire-desktop \
-output Cagire.app/Contents/MacOS/cagire-desktop
lipo -info Cagire.app/Contents/MacOS/cagire-desktop
zip -r Cagire.app.zip Cagire.app
- name: Create universal CLAP plugin
run: |
mkdir -p cagire-plugins.clap/Contents/MacOS
cp artifacts/cagire-macos-aarch64-clap/cagire-plugins.clap/Contents/Info.plist \
cagire-plugins.clap/Contents/ 2>/dev/null || true
cp artifacts/cagire-macos-aarch64-clap/cagire-plugins.clap/Contents/PkgInfo \
cagire-plugins.clap/Contents/ 2>/dev/null || true
lipo -create \
artifacts/cagire-macos-x86_64-clap/cagire-plugins.clap/Contents/MacOS/cagire-plugins \
artifacts/cagire-macos-aarch64-clap/cagire-plugins.clap/Contents/MacOS/cagire-plugins \
-output cagire-plugins.clap/Contents/MacOS/cagire-plugins
lipo -info cagire-plugins.clap/Contents/MacOS/cagire-plugins
- name: Create universal VST3 plugin
run: |
mkdir -p cagire-plugins.vst3/Contents/MacOS
cp -R artifacts/cagire-macos-aarch64-vst3/cagire-plugins.vst3/Contents/Info.plist \
cagire-plugins.vst3/Contents/ 2>/dev/null || true
cp artifacts/cagire-macos-aarch64-vst3/cagire-plugins.vst3/Contents/PkgInfo \
cagire-plugins.vst3/Contents/ 2>/dev/null || true
cp -R artifacts/cagire-macos-aarch64-vst3/cagire-plugins.vst3/Contents/Resources \
cagire-plugins.vst3/Contents/ 2>/dev/null || true
lipo -create \
artifacts/cagire-macos-x86_64-vst3/cagire-plugins.vst3/Contents/MacOS/cagire-plugins \
artifacts/cagire-macos-aarch64-vst3/cagire-plugins.vst3/Contents/MacOS/cagire-plugins \
-output cagire-plugins.vst3/Contents/MacOS/cagire-plugins
lipo -info cagire-plugins.vst3/Contents/MacOS/cagire-plugins
- name: Build .pkg installer
run: |
VERSION="${GITHUB_REF_NAME#v}"
mkdir -p pkg-root/Applications pkg-root/usr/local/bin
cp -R Cagire.app pkg-root/Applications/
cp cagire pkg-root/usr/local/bin/
pkgbuild --analyze --root pkg-root component.plist
plutil -replace BundleIsRelocatable -bool NO component.plist
pkgbuild --root pkg-root --identifier com.sova.cagire \
--version "$VERSION" --install-location / \
--component-plist component.plist \
"Cagire-${VERSION}-universal.pkg"
- name: Upload universal CLI
uses: actions/upload-artifact@v4
with:
name: cagire-macos-universal
path: cagire
- name: Upload universal app bundle
uses: actions/upload-artifact@v4
with:
name: cagire-macos-universal-desktop
path: Cagire.app.zip
- name: Upload universal CLAP plugin
uses: actions/upload-artifact@v4
with:
name: cagire-macos-universal-clap
path: cagire-plugins.clap
- name: Upload universal VST3 plugin
uses: actions/upload-artifact@v4
with:
name: cagire-macos-universal-vst3
path: cagire-plugins.vst3
- name: Upload .pkg installer
uses: actions/upload-artifact@v4
with:
name: cagire-macos-universal-pkg
path: Cagire-*-universal.pkg
release:
needs: [build, universal-macos]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Prepare release files
run: |
mkdir -p release
for dir in artifacts/*/; do
name=$(basename "$dir")
if [[ "$name" == "cagire-macos-universal-pkg" ]]; then
cp "$dir"/*.pkg release/
elif [[ "$name" == "cagire-macos-universal-desktop" ]]; then
cp "$dir/Cagire.app.zip" "release/cagire-macos-universal-desktop.app.zip"
elif [[ "$name" == "cagire-macos-universal" ]]; then
cp "$dir/cagire" "release/cagire-macos-universal"
elif [[ "$name" == "cagire-macos-universal-clap" ]]; then
cd "$dir" && zip -r "../../release/cagire-macos-universal-clap.zip" cagire-plugins.clap && cd ../..
elif [[ "$name" == "cagire-macos-universal-vst3" ]]; then
cd "$dir" && zip -r "../../release/cagire-macos-universal-vst3.zip" cagire-plugins.vst3 && cd ../..
elif [[ "$name" == *-clap ]]; then
base="${name%-clap}"
cd "$dir" && zip -r "../../release/${base}-clap.zip" cagire-plugins.clap && cd ../..
elif [[ "$name" == *-vst3 ]]; then
base="${name%-vst3}"
cd "$dir" && zip -r "../../release/${base}-vst3.zip" cagire-plugins.vst3 && cd ../..
elif [[ "$name" == *-desktop ]]; then
base="${name%-desktop}"
if ls "$dir"/*.deb 1>/dev/null 2>&1; then
cp "$dir"/*.deb "release/${base}-desktop.deb"
elif [ -f "$dir/Cagire.app.zip" ]; then
cp "$dir/Cagire.app.zip" "release/${base}-desktop.app.zip"
elif [ -f "$dir/cagire-desktop.exe" ]; then
cp "$dir/cagire-desktop.exe" "release/${base}-desktop.exe"
fi
else
if [ -f "$dir/cagire.exe" ]; then
cp "$dir/cagire.exe" "release/${name}.exe"
elif [ -f "$dir/cagire" ]; then
cp "$dir/cagire" "release/${name}"
fi
fi
done
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: release/*
generate_release_notes: true

View File

@@ -16,7 +16,6 @@ concurrency:
jobs:
deploy:
if: github.server_url == 'https://github.com'
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}

View File

@@ -1,107 +0,0 @@
name: Release
on:
workflow_dispatch:
push:
tags: ['v*']
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
linux:
if: github.server_url == 'https://github.com'
uses: ./.github/workflows/build-linux.yml
with:
build-packages: true
macos:
if: github.server_url == 'https://github.com'
uses: ./.github/workflows/build-macos.yml
with:
build-packages: true
matrix: >-
[
{"os":"macos-14","target":"aarch64-apple-darwin","artifact":"cagire-macos-aarch64"},
{"os":"macos-15-intel","target":"x86_64-apple-darwin","artifact":"cagire-macos-x86_64"}
]
windows:
if: github.server_url == 'https://github.com'
uses: ./.github/workflows/build-windows.yml
with:
build-packages: true
cross:
if: github.server_url == 'https://github.com'
uses: ./.github/workflows/build-cross.yml
assemble-macos:
needs: macos
uses: ./.github/workflows/assemble-macos.yml
release:
needs: [linux, macos, windows, cross, assemble-macos]
if: startsWith(github.ref, 'refs/tags/v') && github.server_url == 'https://github.com'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Prepare release files
run: |
mkdir -p release
for dir in artifacts/*/; do
name=$(basename "$dir")
if [[ "$name" == "cagire-macos-universal-dmg" ]]; then
cp "$dir"/*.dmg release/
elif [[ "$name" == "cagire-macos-universal-pkg" ]]; then
cp "$dir"/*.pkg release/
elif [[ "$name" == "cagire-macos-universal-desktop" ]]; then
cp "$dir/Cagire.app.zip" "release/cagire-macos-universal-desktop.app.zip"
elif [[ "$name" == "cagire-macos-universal" ]]; then
cp "$dir/cagire" "release/cagire-macos-universal"
elif [[ "$name" == "cagire-macos-universal-clap" ]]; then
cd "$dir" && zip -r "../../release/cagire-macos-universal-clap.zip" cagire-plugins.clap && cd ../..
elif [[ "$name" == "cagire-macos-universal-vst3" ]]; then
cd "$dir" && zip -r "../../release/cagire-macos-universal-vst3.zip" cagire-plugins.vst3 && cd ../..
elif [[ "$name" == *-clap ]]; then
base="${name%-clap}"
cd "$dir" && zip -r "../../release/${base}-clap.zip" cagire-plugins.clap && cd ../..
elif [[ "$name" == *-vst3 ]]; then
base="${name%-vst3}"
cd "$dir" && zip -r "../../release/${base}-vst3.zip" cagire-plugins.vst3 && cd ../..
elif [[ "$name" == *-msi ]]; then
cp "$dir"/*.msi release/
elif [[ "$name" == *-appimage ]]; then
cp "$dir"/*.AppImage release/
elif [[ "$name" == *-desktop ]]; then
base="${name%-desktop}"
if ls "$dir"/*.deb 1>/dev/null 2>&1; then
cp "$dir"/*.deb "release/${base}-desktop.deb"
elif [ -f "$dir/Cagire.app.zip" ]; then
cp "$dir/Cagire.app.zip" "release/${base}-desktop.app.zip"
elif [ -f "$dir/cagire-desktop.exe" ]; then
cp "$dir/cagire-desktop.exe" "release/${base}-desktop.exe"
fi
else
if [ -f "$dir/cagire.exe" ]; then
cp "$dir/cagire.exe" "release/${name}.exe"
elif [ -f "$dir/cagire" ]; then
cp "$dir/cagire" "release/${name}"
fi
fi
done
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: release/*
generate_release_notes: true

7
.gitignore vendored
View File

@@ -1,11 +1,10 @@
/target
/.cache
Cargo.lock
*.prof
.DS_Store
releases/
# Local cargo overrides (doux path patch)
.cargo/config.local.toml
# Cargo config
.cargo/config.toml
# Claude
.claude/

View File

@@ -1,187 +0,0 @@
# Building Cagire
## Quick Start
```bash
git clone https://github.com/Bubobubobubobubo/cagire
cd cagire
cargo build --release
```
The `doux` audio engine is fetched automatically from git. No local path setup needed.
## Prerequisites
**Rust** (stable toolchain): https://rustup.rs
## System Dependencies
### macOS
```bash
brew install cmake
```
cmake is required by `rusty_link` (Ableton Link C++ bindings). Xcode Command Line Tools provide the C++ compiler. CoreAudio and CoreMIDI are built-in. The desktop build needs no additional dependencies on macOS (Cocoa/Metal are provided by the system).
### Linux (Debian/Ubuntu)
```bash
sudo apt install cmake g++ pkg-config libasound2-dev libjack-jackd2-dev
```
For the desktop build (egui/eframe), also install:
```bash
sudo apt install libgl-dev libxkbcommon-dev libx11-dev libxcursor-dev libxrandr-dev libxi-dev libwayland-dev
```
### Linux (Arch)
```bash
sudo pacman -S cmake gcc pkgconf alsa-lib jack2
```
For the desktop build:
```bash
sudo pacman -S libxkbcommon libx11 libxcursor libxrandr libxi wayland mesa
```
### Linux (Fedora)
```bash
sudo dnf install cmake gcc-c++ pkgconf-pkg-config alsa-lib-devel jack-audio-connection-kit-devel
```
For the desktop build:
```bash
sudo dnf install libxkbcommon-devel libX11-devel libXcursor-devel libXrandr-devel libXi-devel wayland-devel mesa-libGL-devel
```
### Windows
Install Visual Studio Build Tools (MSVC) and CMake. Everything else is provided by the Windows SDK.
## Build
Terminal (default):
```bash
cargo build --release
```
Desktop (egui window):
```bash
cargo build --release --features desktop --bin cagire-desktop
```
Plugins (CLAP/VST3):
```bash
cargo xtask bundle cagire-plugins --release
```
The xtask alias is defined in `.cargo/config.toml` (committed). Plugin bundles are output to `target/bundled/`.
## Run
Terminal (default):
```bash
cargo run --release -- [OPTIONS]
```
Desktop (egui window):
```bash
cargo run --release --features desktop --bin cagire-desktop
```
| Flag | Description |
|------|-------------|
| `-s, --samples <path>` | Sample directory (repeatable) |
| `-o, --output <device>` | Output audio device |
| `-i, --input <device>` | Input audio device |
| `-c, --channels <n>` | Output channel count |
| `-b, --buffer <size>` | Audio buffer size |
## Cross-Compilation
[cross](https://github.com/cross-rs/cross) uses Docker to build for other platforms without installing their toolchains locally. It works on any OS that runs Docker.
### Targets
| Target | Method | Binaries |
|--------|--------|----------|
| aarch64-apple-darwin | Native (macOS ARM only) | `cagire`, `cagire-desktop` |
| x86_64-apple-darwin | Native (macOS only) | `cagire`, `cagire-desktop` |
| x86_64-unknown-linux-gnu | `cross build` | `cagire`, `cagire-desktop` |
| aarch64-unknown-linux-gnu (RPi 64-bit) | `cross build` | `cagire`, `cagire-desktop` |
| x86_64-pc-windows-gnu | `cross build` | `cagire`, `cagire-desktop` |
macOS targets can only be built on macOS — Apple does not support cross-compilation to macOS from other platforms. Linux and Windows targets can be cross-compiled from any OS. The aarch64-unknown-linux-gnu target covers Raspberry Pi (64-bit OS).
### Windows ABI
CI produces `x86_64-pc-windows-msvc` binaries (native Windows build, better compatibility). Local cross-compilation from non-Windows hosts produces `x86_64-pc-windows-gnu` binaries (MinGW via Docker). Both work; MSVC is preferred for releases.
### Prerequisites
1. **Docker**: https://docs.docker.com/get-docker/
2. **cross**: `cargo install cross --git https://github.com/cross-rs/cross`
3. On macOS, add the Intel target: `rustup target add x86_64-apple-darwin`
Docker must be running before invoking `cross` or `scripts/build-all.sh`.
### Building Individual Targets
```bash
# Linux x86_64
cross build --release --target x86_64-unknown-linux-gnu
cross build --release --features desktop --bin cagire-desktop --target x86_64-unknown-linux-gnu
# Linux aarch64
cross build --release --target aarch64-unknown-linux-gnu
cross build --release --features desktop --bin cagire-desktop --target aarch64-unknown-linux-gnu
# Windows x86_64
cross build --release --target x86_64-pc-windows-gnu
cross build --release --features desktop --bin cagire-desktop --target x86_64-pc-windows-gnu
```
### Building All Targets (macOS only)
```bash
# Interactive (prompts for platform/target selection):
scripts/build-all.sh
# Non-interactive:
scripts/build-all.sh --platforms macos-arm64,linux-x86_64 --targets cli,desktop --yes
scripts/build-all.sh --all --yes
```
Builds selected targets, producing binaries in `releases/`.
Platform aliases: `macos-arm64`, `macos-x86_64`, `linux-x86_64`, `linux-aarch64`, `windows-x86_64`.
Target aliases: `cli`, `desktop`, `plugins`.
### Linux AppImage Packaging
Linux releases ship as AppImages — self-contained executables that bundle all shared library dependencies (ALSA, JACK, X11, OpenGL). No runtime dependencies required.
After building a Linux target, produce an AppImage with:
```bash
scripts/make-appimage.sh target/x86_64-unknown-linux-gnu/release/cagire x86_64 releases
```
`scripts/build-all.sh` does this automatically for every Linux target selected. The CI pipeline produces AppImages for the x86_64 Linux build. Cross-arch AppImage building (e.g. aarch64 on x86_64) is not supported — run on a matching host or in CI.
### Notes
- Custom Dockerfiles in `cross/` install the native libraries Cagire depends on (ALSA, JACK, X11, cmake, libclang, etc.). `Cross.toml` maps each target to its Dockerfile.
- The first build per target downloads Docker base images and installs packages. Subsequent builds use cached layers.
- Cross-architecture Docker builds (e.g. aarch64 on x86_64 or vice versa) run under QEMU emulation and are significantly slower.

View File

@@ -2,144 +2,56 @@
All notable changes to this project will be documented in this file.
## [0.1.1]
### Forth Language
- `map` word: apply a quotation to each stack element (`1 2 3 ( 10 * ) map => 10 20 30`).
- `loop` fix: now operates in steps instead of beats, uses `step_duration()` for correct timing.
### Fixed
- Crash on missing sample directories: sample path scanning now validates directories exist before scanning.
- Audio channel minimum enforced to 2, preventing crash on devices reporting fewer channels.
- Audio device disconnect: automatic stream restart when device is lost (terminal and desktop).
- Live keys (e.g. `f` for fill) no longer trigger while searching in dictionary or help views.
- Side panel always uses horizontal layout (removed broken vertical fallback for narrow terminals).
### Changed
- Runtime highlight enabled by default.
### Packaging
- Modular CI: split monolithic release workflow into per-platform builds (Linux, macOS, Windows, cross-compilation).
- Separate CI workflows for CLAP/VST plugin builds (Linux, macOS, Windows, Raspberry Pi).
- Windows MSI installer workflow fixes.
- Website download matrix updated.
## [0.1.0]
### Breaking
- **Quotation syntax changed from `{ }` to `( )`** — all deferred code blocks now use parentheses.
### Forth Language
**Syntax:**
- `[ v1 v2 v3 ]` bracket lists with implicit count.
- `( ... )` quotation syntax (replaces `{ }`).
- `,varname` assignment syntax (SetKeep): assign without consuming.
- `case/of/endof/endcase` control flow.
- `print` — debug word, outputs top-of-stack as text.
- Arithmetic and unary ops now lift over ArpList and CycleList element-wise.
**New words:**
- `index` — select item at explicit index (wraps with modulo).
- `slice` / `pick` — sample slicing: divide a sample into N equal parts and select which slice to play.
- `wave` / `waveform` — set drum synthesis waveform (0=sine, 0.5=triangle, 1=saw).
- `pbounce` — ping-pong cycle keyed by pattern iteration.
- `except` — inverse of `every`.
- `every+` / `except+` — phase-offset variants.
- `bjork` / `pbjork` — euclidean rhythm gates using quotations.
- `arp` — arpeggio list type (spreads notes across time).
- `all` / `noall` — apply params globally to all emitted sounds.
- `linmap` / `expmap` — linear and exponential range mapping.
- `rec` / `overdub` (`dub`) — record/overdub master audio to a named sample.
- `orec` / `odub` — record/overdub a single orbit.
**Harmony and voicing:**
- `key!` — set tonal center.
- `triad` / `seventh` — diatonic chord from scale degree.
- `inv` / `dinv` — chord inversion / down inversion.
- `drop2` / `drop3` — drop voicings.
- `tp` — transpose all ints on stack by N semitones.
**New chord types:**
- `pwr`, `augmaj7`, `7sus4`, `9sus4`, `maj69`, `min69`, `maj11`, `maj13`, `min13`, `dom7s11`.
**Effect parameters:**
- Ducking compressor: `comp`, `compattack`/`cattack`, `comprelease`/`crelease`, `comporbit`/`corbit`.
- Smear effect: `smear`, `smearfreq`, `smearfb`.
- Reverb: `verbtype`, `verbchorus`, `verbchorusfreq`, `verbprelow`, `verbprehigh`, `verblowcut`, `verbhighcut`, `verblowgain`.
**Behavior changes:**
- All parameter words now accept varargs (100+ words updated to consume the full stack).
- `every` reworked to accept quotations.
- Removed `chain` word (replaced by pattern-level Follow Up setting).
### Engine
- SF2 soundfont support: auto-scans sample directories for `.sf2` files.
- Follow-up actions: patterns have configurable follow-up (Loop, Stop, Chain). Replaces the `chain` word with a declarative UI setting (`e` key).
- Delta-time MIDI scheduling for tighter timing.
- Audio stream errors surfaced as flash messages.
- Prelude script evaluated on application startup (not only on play).
- Global periodic script: a hidden script page runs alongside all patterns at its own speed/length.
- RestartAll command: reset all active patterns to step 0 and clear state.
- Tempo and current beat exposed in sequencer snapshot.
- Spectrum analyzer rescaling.
### UI / UX
- **Engine page redesign**: responsive narrow/wide layout, Link/MIDI/device settings moved here from Options.
- **Patterns view redesign**: banks column with pattern counts, expandable detail rows, bottom preview strip with mini step grid.
- **Mouse support**: click navigation on header/grid/panels/modals, text selection in code editor (click+drag), double-click on scope/spectrum/lissajous to cycle display modes.
- Smooth playback progress bar interpolated between steps.
- Dynamic step grid sizing adapts to terminal height.
- Lissajous XY scope with Braille rendering and thermal trail mode.
- Gain boost (1x16x) and normalize toggle for scope/lissajous/spectrum.
- Pattern description field: editable via `d`, shown in pattern list and properties.
- Bank/pattern import and export via clipboard (base64 serialization for sharing).
- Mute/solo on main page now apply immediately (no staging).
- Step name automatically cleared when deleting a step.
- F1F6 page navigation across the 3×2 page grid.
- Collapsible help sections with code block copy.
- Onboarding system for first-time users.
- Show/hide preview pane toggle and zoom factor setting.
- Reduced UI lag: sequencer snapshot moved after render call.
- 10 bundled demo projects loaded on fresh startup (togglable in Options).
- Options page: each option shows a description line below when focused.
- Dictionary page: word list uses full page height (removed description box).
### Themes
- 5 new themes: Iceberg, Everforest, Fauve, Tropicalia, Jaipur.
- Palette-based generation: all 18 themes derived from a 14-field Palette via Oklab color space (definitions reduced from ~300 to ~20 lines each).
### Desktop (egui)
- Fixed Alt/Option key on macOS (dead-key composition now works).
- Fixed multi-character text paste.
- Extended function key support (F13F20).
- No console window on Windows desktop build.
### Packaging
- macOS: `.dmg` disk image with `.app` bundle (Intel + Apple Silicon fat binaries via `lipo`).
- Windows: `.msi` installer via WiX.
- Linux: improved AppImage build scripts and Docker cross-compilation.
### UI / UX (breaking cosmetic changes)
- **Options page**: Each option now shows a short description line below when focused, replacing the static header box.
- **Dictionary page**: Removed the Forth description box at the top. The word list now uses the full page height.
### CLAP Plugin (experimental)
- Early CLAP plugin support via nih-plug, baseview, and egui. Feature-gated builds separate CLI from plugin targets.
### Forth Language
- Removed `chain` word (replaced by pattern-level Follow Up setting).
- `case/of/endof/endcase` control flow for pattern-matching dispatch.
- `bjork` / `pbjork` — euclidean rhythm gates using quotations: execute a block only on Bjorklund-distributed hits.
- `arp` — arpeggio list type that spreads notes across time positions instead of stacking them simultaneously.
- `,varname` assignment syntax (SetKeep): assign to a variable without consuming the value from the stack.
- `every` reworked to accept quotations for cleaner conditional step logic.
- All parameter words now accept varargs — over 100 words updated to consume the full stack.
- Reverb parameter words added.
### Engine
- Follow-up actions: patterns now have a configurable follow-up behavior (Loop, Stop, or Chain to another pattern). Replaces the Forth `chain` word with a declarative setting in the Pattern Properties modal (`e` key). Chain targets specify bank and pattern via UI fields.
- Delta-time MIDI scheduling for tighter, sample-accurate timing.
- Tempo and current beat exposed in sequencer snapshot.
- Spectrum analyzer rescaling.
### UI / UX
- Patterns view redesign: new layout with banks column (showing pattern counts), expandable detail rows for the focused pattern (quantization, sync mode, progress bar), and a bottom preview strip with mini step grid and pattern properties.
- Smooth playback progress: playing patterns display a real-time progress bar interpolated between steps.
- Dynamic step grid sizing: `steps_per_page` adapts to terminal height instead of using a fixed constant.
- Mouse support: click navigation on the pattern grid, panels, and modals.
- F1F6 page navigation across the 3×2 page grid.
- Collapsible help sections with code block copy.
- Onboarding system for first-time users.
- New reusable widgets: CategoryList, HintBar, PropsForm, ScrollIndicators, SearchBar, SectionHeader.
- Show/hide preview pane toggle and zoom factor setting.
### Documentation
- Complete reorganization into `docs/` subdirectories.
- 10 getting-started guides, 5 interactive tutorials.
- New tutorials: Recording, Soundfonts, Sharing (import/export).
- New topics: control flow, generators, harmony, randomness, variables, timing, bracket syntax.
- Crate-level READMEs for forth, markdown, project, ratatui.
- New topics: control flow, generators, harmony, randomness, variables, timing.
### Fixed
- CycleList + ArpList index collision: arp uses timing index, cycle uses polyphony slot.
- Scope widget not drawing completely in some terminal sizes.
### Theme System
- Palette-based generation: all 18 themes now derived from a 14-field Palette via Oklab color space.
- Theme definitions reduced from ~300 lines each to ~20 lines.
### Codebase
- `src/app.rs` split into 10 focused modules.
- `src/app.rs` split into 10 focused modules (dispatch, clipboard, editing, navigation, persistence, scripting, sequencer, staging, undo).
- `src/input.rs` split into 8 page-specific handlers.
- Undo/redo system with scope-based tracking.
- Feature-gated CLI vs plugin builds.
- New reusable widgets: CategoryList, HintBar, PropsForm, ScrollIndicators, SearchBar, SectionHeader.
## [0.0.9]

View File

@@ -10,7 +10,6 @@ Contributions are welcome. There are many ways to contribute beyond code:
## Prerequisites
- **Rust** (stable toolchain) - [rustup.rs](https://rustup.rs/)
- **System libraries** - See [BUILDING.md](BUILDING.md) for platform-specific packages (cmake, ALSA, etc.)
## Quick start

7363
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
members = ["crates/forth", "crates/markdown", "crates/project", "crates/ratatui", "plugins/cagire-plugins", "plugins/baseview", "plugins/egui-baseview", "plugins/nih-plug-egui", "xtask"]
[workspace.package]
version = "0.1.1"
version = "0.0.9"
edition = "2021"
authors = ["Raphaël Forment <raphael.forment@gmail.com>"]
license = "AGPL-3.0"
@@ -51,11 +51,11 @@ cagire-forth = { path = "crates/forth" }
cagire-markdown = { path = "crates/markdown" }
cagire-project = { path = "crates/project" }
cagire-ratatui = { path = "crates/ratatui" }
doux = { git = "https://github.com/sova-org/doux", features = ["native", "soundfont"] }
doux = { path = "/Users/bubo/doux", features = ["native"] }
rusty_link = "0.4"
ratatui = "0.30"
crossterm = "0.29"
cpal = { version = "0.17", optional = true }
cpal = { version = "0.17", features = ["jack"], optional = true }
clap = { version = "4", features = ["derive"], optional = true }
rand = "0.8"
serde = { version = "1", features = ["derive"] }
@@ -83,9 +83,6 @@ rustc-hash = { version = "2", optional = true }
image = { version = "0.25", default-features = false, features = ["png"], optional = true }
[target.'cfg(target_os = "linux")'.dependencies]
cpal = { version = "0.17", optional = true, features = ["jack"] }
[target.'cfg(windows)'.build-dependencies]
winres = "0.1"
@@ -112,4 +109,3 @@ icon = ["assets/Cagire.icns", "assets/Cagire.ico", "assets/Cagire.png"]
copyright = "Copyright (c) 2025 Raphaël Forment"
category = "Music"
short_description = "Forth-based music sequencer"
minimum_system_version = "12.0"

View File

@@ -1,8 +0,0 @@
[target.aarch64-unknown-linux-gnu]
dockerfile = "./scripts/cross/aarch64-linux.Dockerfile"
[target.x86_64-unknown-linux-gnu]
dockerfile = "./scripts/cross/x86_64-linux.Dockerfile"
[target.x86_64-pc-windows-gnu]
dockerfile = "./scripts/cross/x86_64-windows.Dockerfile"

View File

@@ -1,84 +1,37 @@
<h1 align="center">Cagire</h1>
<p align="center"><em>A Forth-based live coding sequencer</em></p>
<p align="center"><em>A Forth Music Sequencer</em></p>
<p align="center">
<img src="assets/Cagire.png" alt="Cagire" width="256">
<img src="cagire_pixel.png" alt="Cagire" width="256">
</p>
<p align="center">
<a href="https://cagire.raphaelforment.fr">Website</a> &middot;
<a href="https://github.com/Bubobubobubobubo/cagire">GitHub</a> &middot;
AGPL-3.0
</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.
Cagire is a terminal based step sequencer and live coding platform. Each step in a sequence is represented by a **Forth** script. It ships with a self-contained audio engine. No external software is needed, Cagire is a fully autonomous musical instrument that provides everything you need to perform.
## Build
### Examples
A filtered sawtooth with reverb:
```forth
saw sound
200 199 freq
400 lpf
.8 lpq .3 verb
.
Terminal version:
```
cargo build --release
```
A generative pattern using randomness, scales, and effects:
```forth
sine sound 2 fm 0.5 fmh
0 7 rand minor 50 + note
.1 .8 rrand cutoff
1 4 irand 10 * delay .5 delayfb
.
Desktop version (with egui window):
```
cargo build --release --features desktop --bin cagire-desktop
```
### Features
## Run
- **Cagire's Forth**: a stack-based language made for live coding
- Forth has almost no syntax, only words, numbers and spaces. Very easy to learn for beginners, quite deep for experienced programmers.
- Nondeterminism and generative: randomness, probabilities, patterns thought as first-class features.
- Quotations: code blocks `( ... )` that compose with probability, cycling, euclidean, and conditional words.
- User-defined words: extend (or redefine) the language on the fly with `:name ... ;` definitions.
- Interactive documentation: built-in tutorials with runnable examples.
- **Audio engine** (powered by [Doux](https://doux.livecoding.fr)):
- Synthesis: classic waveforms (saw, pulse, tri, sine), additive, FM (2-op, 3 algorithms), additive synthesis, wavetables, 7-voice spread, Mutable Instruments Plaits models: modal, granular, waveshaping, chord, swarm, etc.
- Drum models: seven drum models with timbral morphing.
- Sampling: disk-loaded samples with slicing, looping, pitch tracking, wavetable mode, and live recording from engine output or line input.
- Filters: biquad LP/HP/BP and ladder filters, each with independent envelope. Filters can be modulated, stacked, etc.
- Effects: phaser, flanger, chorus, smear, distortion, wavefolder, wavewrapper, bitcrusher, sample-rate reduction, 3-band EQ, tilt EQ, Haas stereo.
- Bus effects: delay (standard, ping-pong, tape, multitap), two reverb engines (Dattorro plate, Vital Space), comb filter, feedback delay with LFO, sidechain compressor.
- Modulation: vibrato, AM, ring mod, pitch envelope, FM envelope, glide — all with selectable LFO shapes (sine, tri, saw, square, sample & hold).
- **Sequencing**: probabilities, patterns, euclidean structures, sub-step timing, pattern chaining and a lot more.
- **MIDI**: receive or send MIDI messages across up to 4 inputs and 4 outputs.
- **Ableton Link**: tempo and phase sync with any Link-enabled software or hardware.
- **Cross-platform**: terminal and desktop interfaces on macOS, Linux, and Windows.
- **Plugins**: run Cagire as a CLAP or VST3 plugin inside your DAW (separate version).
Terminal version:
```
cargo run --release
```
### Getting started
Desktop version:
```
cargo run --release --features desktop --bin cagire-desktop
```
Download the latest release for your platform from the [website](https://cagire.raphaelforment.fr).
## License
To build from source instead, see [BUILDING.md](BUILDING.md).
### Documentation
Cagire includes interactive documentation with runnable code examples. Press **F1** in the application to open it.
- [Website](https://cagire.raphaelforment.fr)
- [BUILDING.md](BUILDING.md) — build instructions and CLI flags
- [CHANGELOG.md](CHANGELOG.md)
### Credits
Cagire is developed by [BuboBubo](https://raphaelforment.fr) (Raphael Forment).
- **[Doux](https://doux.livecoding.fr)** (audio engine) — Rust port of Dough, originally written in C by Felix Roos
- **mi-plaits-dsp-rs** — Rust port of Mutable Instruments Plaits DSP by Oliver Rockstedt, original code by Emilie Gillet
### License
[AGPL-3.0](LICENSE)
AGPL-3.0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -1,18 +0,0 @@
# Cagire - A Forth-based music sequencer
## Installation
Drag Cagire.app into the Applications folder.
## Unquarantine
Since this app is not signed with an Apple Developer certificate,
macOS will block it from running. Thanks Apple! To fix this, open
Terminal and run:
xattr -cr /Applications/Cagire.app
## Support
If you enjoy this software, consider supporting development:
https://ko-fi.com/raphaelbubo

View File

@@ -1,7 +0,0 @@
[Desktop Entry]
Type=Application
Name=Cagire
Comment=Forth-based music sequencer
Exec=cagire
Icon=cagire
Categories=Audio;Music;AudioVideo;

View File

@@ -1,18 +1,4 @@
//! Build script — embeds Windows application resources (icon, metadata).
fn main() {
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
if target_os == "windows" {
// C++ runtime (stdc++, gcc, gcc_eh, pthread) linked statically via .cargo/config.toml
// using -Wl,-Bstatic. Only Windows system DLLs go here.
println!("cargo:rustc-link-lib=ws2_32");
println!("cargo:rustc-link-lib=iphlpapi");
println!("cargo:rustc-link-lib=winmm");
println!("cargo:rustc-link-lib=ole32");
println!("cargo:rustc-link-lib=oleaut32");
}
#[cfg(windows)]
{
let mut res = winres::WindowsResource::new();

View File

@@ -1,22 +0,0 @@
# cagire-forth
Stack-based Forth VM for the Cagire sequencer. Tokenizes, compiles, and executes step scripts to produce audio and MIDI commands.
## Modules
| Module | Description |
|--------|-------------|
| `vm` | Interpreter loop, `Forth::evaluate()` entry point |
| `compiler` | Tokenization (with source spans) and single-pass compilation to ops |
| `ops` | `Op` enum (~90 variants) |
| `types` | `Value`, `StepContext`, shared state types |
| `words/` | Built-in word definitions: `core`, `sound`, `music`, `midi`, `effects`, `sequencing`, `compile` |
| `theory/` | Music theory lookups: `scales` (~200 patterns), `chords` (interval arrays) |
## Key Types
- **`Forth`** — VM instance, holds stacks and compilation state
- **`Value`** — Stack value (int, float, string, list, quotation, ...)
- **`StepContext`** — Per-step evaluation context (step index, tempo, variables, ...)
- **`Op`** — Compiled operation; nondeterministic variants carry `Option<SourceSpan>` for tracing
- **`ExecutionTrace`** — Records executed/selected spans and resolved values during evaluation

View File

@@ -15,7 +15,6 @@ enum Token {
Word(String, SourceSpan),
}
/// Compile Forth source text into an executable Op sequence.
pub(super) fn compile_script(input: &str, dict: &Dictionary) -> Result<Vec<Op>, String> {
let tokens = tokenize(input);
compile(&tokens, dict)
@@ -31,7 +30,7 @@ fn tokenize(input: &str) -> Vec<Token> {
continue;
}
if c == '{' || c == '}' {
if c == '(' || c == ')' {
chars.next();
continue;
}
@@ -133,7 +132,7 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
Token::Str(s, span) => ops.push(Op::PushStr(Arc::from(s.as_str()), Some(*span))),
Token::Word(w, span) => {
let word = w.as_str();
if word == "(" {
if word == "{" {
let (quote_ops, consumed, end_span) =
compile_quotation(&tokens[i + 1..], dict)?;
i += consumed;
@@ -142,21 +141,8 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
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 (bracket_ops, consumed, end_span) =
compile_bracket(&tokens[i + 1..], dict)?;
i += consumed;
ops.push(Op::Mark);
ops.extend(bracket_ops);
let count_span = SourceSpan {
start: span.start,
end: end_span.end,
};
ops.push(Op::Count(Some(count_span)));
} else if word == "]" {
return Err("unexpected ]".into());
} else if word == "}" {
return Err("unexpected }".into());
} else if word == ":" {
let (consumed, name, body) = compile_colon_def(&tokens[i + 1..], dict)?;
i += consumed;
@@ -203,8 +189,8 @@ fn compile_quotation(
for (i, tok) in tokens.iter().enumerate() {
if let Token::Word(w, _) = tok {
match w.as_str() {
"(" => depth += 1,
")" => {
"{" => depth += 1,
"}" => {
depth -= 1;
if depth == 0 {
end_idx = Some(i);
@@ -216,7 +202,7 @@ fn compile_quotation(
}
}
let end_idx = end_idx.ok_or("missing )")?;
let end_idx = end_idx.ok_or("missing }")?;
let end_span = match &tokens[end_idx] {
Token::Word(_, span) => *span,
_ => unreachable!(),
@@ -225,38 +211,6 @@ fn compile_quotation(
Ok((quote_ops, end_idx + 1, end_span))
}
fn compile_bracket(
tokens: &[Token],
dict: &Dictionary,
) -> Result<(Vec<Op>, usize, SourceSpan), String> {
let mut depth = 1;
let mut end_idx = None;
for (i, tok) in tokens.iter().enumerate() {
if let Token::Word(w, _) = tok {
match w.as_str() {
"[" => depth += 1,
"]" => {
depth -= 1;
if depth == 0 {
end_idx = Some(i);
break;
}
}
_ => {}
}
}
}
let end_idx = end_idx.ok_or("missing ]")?;
let end_span = match &tokens[end_idx] {
Token::Word(_, span) => *span,
_ => unreachable!(),
};
let body_ops = compile(&tokens[..end_idx], dict)?;
Ok((body_ops, end_idx + 1, end_span))
}
fn token_span(tok: &Token) -> Option<SourceSpan> {
match tok {
Token::Int(_, s) | Token::Float(_, s) | Token::Str(_, s) | Token::Word(_, s) => Some(*s),

View File

@@ -4,7 +4,6 @@ use std::sync::Arc;
use super::types::SourceSpan;
/// Single VM instruction produced by the compiler.
#[derive(Clone, Debug, PartialEq)]
pub enum Op {
PushInt(i64, Option<SourceSpan>),
@@ -65,7 +64,6 @@ pub enum Op {
NewCmd,
SetParam(&'static str),
Emit,
Print,
Get,
Set,
SetKeep,
@@ -78,7 +76,6 @@ pub enum Op {
PCycle(Option<SourceSpan>),
Choose(Option<SourceSpan>),
Bounce(Option<SourceSpan>),
PBounce(Option<SourceSpan>),
WChoose(Option<SourceSpan>),
ChanceExec(Option<SourceSpan>),
ProbExec(Option<SourceSpan>),
@@ -87,9 +84,6 @@ pub enum Op {
Ftom,
SetTempo,
Every(Option<SourceSpan>),
Except(Option<SourceSpan>),
EveryOffset(Option<SourceSpan>),
ExceptOffset(Option<SourceSpan>),
Bjork(Option<SourceSpan>),
PBjork(Option<SourceSpan>),
Quotation(Arc<[Op]>, Option<SourceSpan>),
@@ -118,7 +112,6 @@ pub enum Op {
Euclid,
EuclidRot,
Times,
Map,
Chord(&'static [i64]),
Transpose,
Invert,
@@ -133,9 +126,6 @@ pub enum Op {
ModSlide(u8),
ModRnd(u8),
ModEnv,
// Global params
EmitAll,
ClearGlobal,
// MIDI
MidiEmit,
GetMidiCC,
@@ -143,13 +133,4 @@ pub enum Op {
MidiStart,
MidiStop,
MidiContinue,
// Recording
Rec,
Overdub,
Orec,
Odub,
// Bracket syntax (mark/count for auto-counting)
Mark,
Count(Option<SourceSpan>),
Index(Option<SourceSpan>),
}

View File

@@ -1,12 +1,8 @@
//! Chord definitions as semitone interval arrays.
/// Named chord with its interval pattern.
pub struct Chord {
pub name: &'static str,
pub intervals: &'static [i64],
}
/// All built-in chord types.
pub static CHORDS: &[Chord] = &[
// Triads
Chord {
@@ -173,7 +169,6 @@ pub static CHORDS: &[Chord] = &[
},
];
/// Find a chord's intervals by name.
pub fn lookup(name: &str) -> Option<&'static [i64]> {
CHORDS.iter().find(|c| c.name == name).map(|c| c.intervals)
}

View File

@@ -1,5 +1,3 @@
//! Music theory data — chord and scale lookup tables.
pub mod chords;
mod scales;

View File

@@ -1,12 +1,8 @@
//! Scale definitions as semitone offset arrays.
/// Named scale with its semitone pattern.
pub struct Scale {
pub name: &'static str,
pub pattern: &'static [i64],
}
/// All built-in scale types.
pub static SCALES: &[Scale] = &[
Scale {
name: "major",
@@ -129,7 +125,6 @@ pub static SCALES: &[Scale] = &[
},
];
/// Find a scale's pattern by name.
pub fn lookup(name: &str) -> Option<&'static [i64]> {
SCALES.iter().find(|s| s.name == name).map(|s| s.pattern)
}

View File

@@ -14,14 +14,12 @@ pub trait CcAccess: Send + Sync {
fn get_cc(&self, device: usize, channel: usize, cc: usize) -> u8;
}
/// Byte range in source text.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct SourceSpan {
pub start: u32,
pub end: u32,
}
/// Concrete value resolved from a nondeterministic op, used for trace annotations.
#[derive(Clone, Debug)]
pub enum ResolvedValue {
Int(i64),
@@ -41,7 +39,6 @@ impl ResolvedValue {
}
}
/// Spans and resolved values collected during a single evaluation, used for UI highlighting.
#[derive(Clone, Debug, Default)]
pub struct ExecutionTrace {
pub executed_spans: Vec<SourceSpan>,
@@ -49,7 +46,6 @@ pub struct ExecutionTrace {
pub resolved: Vec<(SourceSpan, ResolvedValue)>,
}
/// Per-step sequencer state passed into the VM.
pub struct StepContext<'a> {
pub step: usize,
pub beat: f64,
@@ -76,18 +72,13 @@ impl StepContext<'_> {
}
}
/// Underlying map for user-defined variables.
pub type VariablesMap = HashMap<String, Value>;
/// Shared variable store, swapped atomically after each step.
pub type Variables = Arc<ArcSwap<VariablesMap>>;
/// Shared user-defined word dictionary.
pub type Dictionary = Arc<Mutex<HashMap<String, Vec<Op>>>>;
/// Shared random number generator.
pub type Rng = Arc<Mutex<StdRng>>;
pub type Stack = Mutex<Vec<Value>>;
pub(super) type CmdSnapshot<'a> = (Option<&'a Value>, &'a [(&'static str, Value)]);
/// Stack value in the Forth VM.
#[derive(Clone, Debug)]
pub enum Value {
Int(i64, Option<SourceSpan>),
@@ -169,7 +160,6 @@ pub(super) struct CmdRegister {
sound: Option<Value>,
params: Vec<(&'static str, Value)>,
deltas: Vec<Value>,
global_params: Vec<(&'static str, Value)>,
}
impl CmdRegister {
@@ -178,7 +168,6 @@ impl CmdRegister {
sound: None,
params: Vec::with_capacity(16),
deltas: Vec::with_capacity(4),
global_params: Vec::new(),
}
}
@@ -214,28 +203,6 @@ impl CmdRegister {
}
}
pub(super) fn global_params(&self) -> &[(&'static str, Value)] {
&self.global_params
}
pub(super) fn commit_global(&mut self) {
self.global_params.append(&mut self.params);
self.sound = None;
self.deltas.clear();
}
pub(super) fn clear_global(&mut self) {
self.global_params.clear();
}
pub fn set_global(&mut self, params: Vec<(&'static str, Value)>) {
self.global_params = params;
}
pub fn take_global(&mut self) -> Vec<(&'static str, Value)> {
std::mem::take(&mut self.global_params)
}
pub(super) fn clear(&mut self) {
self.sound = None;
self.params.clear();

View File

@@ -14,13 +14,11 @@ use super::types::{
Value, Variables, VariablesMap,
};
/// Forth VM instance. Holds the stack, variables, dictionary, and RNG.
pub struct Forth {
stack: Stack,
vars: Variables,
dict: Dictionary,
rng: Rng,
global_params: Mutex<Vec<(&'static str, Value)>>,
}
impl Forth {
@@ -30,7 +28,6 @@ impl Forth {
vars,
dict,
rng,
global_params: Mutex::new(Vec::new()),
}
}
@@ -42,18 +39,12 @@ impl Forth {
self.stack.lock().clear();
}
pub fn clear_global_params(&self) {
self.global_params.lock().clear();
}
/// Evaluate a Forth script and return audio command strings.
pub fn evaluate(&self, script: &str, ctx: &StepContext) -> Result<Vec<String>, String> {
let (outputs, var_writes) = self.evaluate_impl(script, ctx, None)?;
self.apply_var_writes(var_writes);
Ok(outputs)
}
/// Evaluate and collect an execution trace for UI highlighting.
pub fn evaluate_with_trace(
&self,
script: &str,
@@ -65,7 +56,6 @@ impl Forth {
Ok(outputs)
}
/// Evaluate and return both outputs and pending variable writes (without applying them).
pub fn evaluate_raw(
&self,
script: &str,
@@ -112,8 +102,6 @@ impl Forth {
let vars_snapshot = self.vars.load_full();
let mut var_writes: HashMap<String, Value> = HashMap::new();
cmd.set_global(self.global_params.lock().clone());
self.execute_ops(
ops,
ctx,
@@ -125,8 +113,6 @@ impl Forth {
&mut var_writes,
)?;
*self.global_params.lock() = cmd.take_global();
Ok((outputs, var_writes))
}
@@ -144,7 +130,6 @@ impl Forth {
var_writes: &mut HashMap<String, Value>,
) -> Result<(), String> {
let mut pc = 0;
let mut marks: Vec<usize> = Vec::new();
let trace_cell = std::cell::RefCell::new(trace);
let var_writes_cell = std::cell::RefCell::new(Some(var_writes));
@@ -229,9 +214,8 @@ impl Forth {
_ => 1,
};
let param_max = cmd
.global_params()
.params()
.iter()
.chain(cmd.params().iter())
.map(|(_, v)| match v {
Value::CycleList(items) => items.len(),
_ => 1,
@@ -243,8 +227,7 @@ impl Forth {
let has_arp_list = |cmd: &CmdRegister| -> bool {
matches!(cmd.sound(), Some(Value::ArpList(_)))
|| cmd.global_params().iter().chain(cmd.params().iter())
.any(|(_, v)| matches!(v, Value::ArpList(_)))
|| cmd.params().iter().any(|(_, v)| matches!(v, Value::ArpList(_)))
};
let compute_arp_count = |cmd: &CmdRegister| -> usize {
@@ -270,21 +253,15 @@ impl Forth {
delta_secs: f64,
outputs: &mut Vec<String>|
-> Result<Option<Value>, String> {
let has_sound = cmd.sound().is_some();
let has_params = !cmd.params().is_empty();
let has_global = !cmd.global_params().is_empty();
if !has_sound && !has_params && !has_global {
return Err("nothing to emit".into());
}
let (sound_opt, params) = cmd.snapshot().ok_or("nothing to emit")?;
let resolved_sound_val =
cmd.sound().map(|sv| resolve_value(sv, arp_idx, poly_idx));
sound_opt.map(|sv| resolve_value(sv, arp_idx, poly_idx));
let sound_str = match &resolved_sound_val {
Some(v) => Some(v.as_str()?.to_string()),
None => None,
};
let resolved_params: Vec<(&str, String)> = cmd.global_params()
let resolved_params: Vec<(&str, String)> = params
.iter()
.chain(cmd.params().iter())
.map(|(k, v)| {
let resolved = resolve_value(v, arp_idx, poly_idx);
if let Value::CycleList(_) | Value::ArpList(_) = v {
@@ -315,7 +292,7 @@ impl Forth {
Op::Dup => {
ensure(stack, 1)?;
let v = stack.last().expect("stack non-empty after ensure").clone();
let v = stack.last().unwrap().clone();
stack.push(v);
}
Op::Dupn => {
@@ -328,16 +305,6 @@ impl Forth {
Op::Drop => {
pop(stack)?;
}
Op::Print => {
let val = pop(stack)?;
let text = match &val {
Value::Int(n, _) => n.to_string(),
Value::Float(f, _) => format!("{f}"),
Value::Str(s, _) => s.to_string(),
_ => format!("{val:?}"),
};
outputs.push(format!("print:{text}"));
}
Op::Swap => {
ensure(stack, 2)?;
let len = stack.len();
@@ -568,10 +535,7 @@ impl Forth {
Op::NewCmd => {
ensure(stack, 1)?;
let values = drain_skip_quotations(stack);
if values.is_empty() {
return Err("expected sound name".into());
}
let values = std::mem::take(stack);
let val = if values.len() == 1 {
values.into_iter().next().unwrap()
} else {
@@ -581,10 +545,7 @@ impl Forth {
}
Op::SetParam(param) => {
ensure(stack, 1)?;
let values = drain_skip_quotations(stack);
if values.is_empty() {
return Err("expected parameter value".into());
}
let values = std::mem::take(stack);
let val = if values.len() == 1 {
values.into_iter().next().unwrap()
} else {
@@ -819,20 +780,16 @@ impl Forth {
drain_select_run(count, idx, stack, outputs, cmd)?;
}
Op::Bounce(word_span) | Op::PBounce(word_span) => {
Op::Bounce(word_span) => {
let count = pop_int(stack)? as usize;
if count == 0 {
return Err("bounce count must be > 0".into());
}
let counter = match &ops[pc] {
Op::Bounce(_) => ctx.runs,
_ => ctx.iter,
};
let idx = if count == 1 {
0
} else {
let period = 2 * (count - 1);
let raw = counter % period;
let raw = ctx.runs % period;
if raw < count { raw } else { period - raw }
};
if let Some(span) = word_span {
@@ -919,47 +876,6 @@ impl Forth {
}
}
Op::Except(word_span) => {
let n = pop_int(stack)?;
let quot = pop(stack)?;
if n <= 0 {
return Err("except count must be > 0".into());
}
let result = ctx.iter as i64 % n != 0;
record_resolved(&trace_cell, *word_span, ResolvedValue::Bool(result));
if result {
run_quotation(quot, stack, outputs, cmd)?;
}
}
Op::EveryOffset(word_span) => {
let offset = pop_int(stack)?;
let n = pop_int(stack)?;
let quot = pop(stack)?;
if n <= 0 {
return Err("every+ count must be > 0".into());
}
let result = ctx.iter as i64 % n == offset.rem_euclid(n);
record_resolved(&trace_cell, *word_span, ResolvedValue::Bool(result));
if result {
run_quotation(quot, stack, outputs, cmd)?;
}
}
Op::ExceptOffset(word_span) => {
let offset = pop_int(stack)?;
let n = pop_int(stack)?;
let quot = pop(stack)?;
if n <= 0 {
return Err("except+ count must be > 0".into());
}
let result = ctx.iter as i64 % n != offset.rem_euclid(n);
record_resolved(&trace_cell, *word_span, ResolvedValue::Bool(result));
if result {
run_quotation(quot, stack, outputs, cmd)?;
}
}
Op::Bjork(word_span) | Op::PBjork(word_span) => {
let n = pop_int(stack)?;
let k = pop_int(stack)?;
@@ -1180,11 +1096,11 @@ impl Forth {
}
Op::Loop => {
let steps = pop_float(stack)?;
let beats = pop_float(stack)?;
if ctx.tempo == 0.0 || ctx.speed == 0.0 {
return Err("tempo and speed must be non-zero".into());
}
let dur = steps * ctx.step_duration();
let dur = beats * 60.0 / ctx.tempo / ctx.speed;
cmd.set_param("fit", Value::Float(dur, None));
cmd.set_param("dur", Value::Float(dur, None));
}
@@ -1278,37 +1194,6 @@ impl Forth {
cmd.clear();
}
Op::EmitAll => {
// Retroactive: patch existing sound outputs with current params
if !cmd.params().is_empty() {
let step_duration = ctx.step_duration();
for output in outputs.iter_mut() {
if output.starts_with("/sound/") {
use std::fmt::Write;
for (k, v) in cmd.params() {
let val_str = v.to_param_string();
if !output.ends_with('/') {
output.push('/');
}
if is_tempo_scaled_param(k) {
if let Ok(val) = val_str.parse::<f64>() {
let _ = write!(output, "{k}/{}", val * step_duration);
continue;
}
}
let _ = write!(output, "{k}/{val_str}");
}
}
}
}
// Prospective: store for future emits
cmd.commit_global();
}
Op::ClearGlobal => {
cmd.clear_global();
}
Op::IntRange => {
let end = pop_int(stack)?;
let start = pop_int(stack)?;
@@ -1374,15 +1259,6 @@ impl Forth {
}
}
Op::Map => {
let quot = pop(stack)?;
let items = std::mem::take(stack);
for item in items {
stack.push(item);
run_quotation(quot.clone(), stack, outputs, cmd)?;
}
}
Op::GeomRange => {
let count = pop_int(stack)?;
let ratio = pop_float(stack)?;
@@ -1616,47 +1492,6 @@ impl Forth {
.unwrap_or(0);
stack.push(Value::Int(val as i64, None));
}
Op::Mark => {
marks.push(stack.len());
}
Op::Count(span) => {
let mark = marks.pop().ok_or("count without mark")?;
stack.push(Value::Int((stack.len() - mark) as i64, *span));
}
Op::Index(word_span) => {
let idx = pop_int(stack)?;
let count = pop_int(stack)? as usize;
if count == 0 {
return Err("index count must be > 0".into());
}
let resolved_idx = ((idx % count as i64 + count as i64) % count as i64) as usize;
if let Some(span) = word_span {
if stack.len() >= count {
let start = stack.len() - count;
let selected = &stack[start + resolved_idx];
record_resolved_from_value(&trace_cell, Some(*span), selected);
}
}
drain_select_run(count, resolved_idx, stack, outputs, cmd)?;
}
Op::Rec => {
let name = pop(stack)?;
outputs.push(format!("/doux/rec/sound/{}", name.as_str()?));
}
Op::Overdub => {
let name = pop(stack)?;
outputs.push(format!("/doux/rec/sound/{}/overdub/1", name.as_str()?));
}
Op::Orec => {
let orbit = pop(stack)?.as_int()?;
let name = pop(stack)?;
outputs.push(format!("/doux/rec/sound/{}/orbit/{}", name.as_str()?, orbit));
}
Op::Odub => {
let orbit = pop(stack)?.as_int()?;
let name = pop(stack)?;
outputs.push(format!("/doux/rec/sound/{}/overdub/1/orbit/{}", name.as_str()?, orbit));
}
Op::Forget => {
let name = pop(stack)?;
self.dict.lock().remove(name.as_str()?);
@@ -1829,8 +1664,8 @@ fn euclidean_rhythm(k: usize, n: usize, rotation: usize) -> Vec<i64> {
groups.into_iter().partition(|g| g[0]);
for _ in 0..min_count {
let mut one = ones.pop().expect("ones sufficient for min_count");
one.extend(zeros.pop().expect("zeros sufficient for min_count"));
let mut one = ones.pop().unwrap();
one.extend(zeros.pop().unwrap());
new_groups.push(one);
}
new_groups.extend(ones);
@@ -1891,21 +1726,6 @@ fn pop_bool(stack: &mut Vec<Value>) -> Result<bool, String> {
Ok(pop(stack)?.is_truthy())
}
/// Drain the stack, returning non-quotation values.
/// Quotations are pushed back onto the stack (transparent).
fn drain_skip_quotations(stack: &mut Vec<Value>) -> Vec<Value> {
let values = std::mem::take(stack);
let mut result = Vec::new();
for v in values {
if matches!(v, Value::Quotation(..)) {
stack.push(v);
} else {
result.push(v);
}
}
result
}
fn ensure(stack: &[Value], n: usize) -> Result<(), String> {
if stack.len() < n {
return Err("stack underflow".into());
@@ -1923,67 +1743,23 @@ fn float_to_value(result: f64) -> Value {
fn lift_unary<F>(val: Value, f: F) -> Result<Value, String>
where
F: Fn(f64) -> f64 + Copy,
F: Fn(f64) -> f64,
{
match val {
Value::ArpList(items) => {
let mapped: Result<Vec<_>, _> = items.iter().map(|x| lift_unary(x.clone(), f)).collect();
Ok(Value::ArpList(Arc::from(mapped?)))
}
Value::CycleList(items) => {
let mapped: Result<Vec<_>, _> = items.iter().map(|x| lift_unary(x.clone(), f)).collect();
Ok(Value::CycleList(Arc::from(mapped?)))
}
v => Ok(float_to_value(f(v.as_float()?))),
}
Ok(float_to_value(f(val.as_float()?)))
}
fn lift_unary_int<F>(val: Value, f: F) -> Result<Value, String>
where
F: Fn(i64) -> i64 + Copy,
F: Fn(i64) -> i64,
{
match val {
Value::ArpList(items) => {
let mapped: Result<Vec<_>, _> =
items.iter().map(|x| lift_unary_int(x.clone(), f)).collect();
Ok(Value::ArpList(Arc::from(mapped?)))
}
Value::CycleList(items) => {
let mapped: Result<Vec<_>, _> =
items.iter().map(|x| lift_unary_int(x.clone(), f)).collect();
Ok(Value::CycleList(Arc::from(mapped?)))
}
v => Ok(Value::Int(f(v.as_int()?), None)),
}
Ok(Value::Int(f(val.as_int()?), None))
}
fn lift_binary<F>(a: Value, b: Value, f: F) -> Result<Value, String>
where
F: Fn(f64, f64) -> f64 + Copy,
F: Fn(f64, f64) -> f64,
{
match (a, b) {
(Value::ArpList(items), b) => {
let mapped: Result<Vec<_>, _> =
items.iter().map(|x| lift_binary(x.clone(), b.clone(), f)).collect();
Ok(Value::ArpList(Arc::from(mapped?)))
}
(a, Value::ArpList(items)) => {
let mapped: Result<Vec<_>, _> =
items.iter().map(|x| lift_binary(a.clone(), x.clone(), f)).collect();
Ok(Value::ArpList(Arc::from(mapped?)))
}
(Value::CycleList(items), b) => {
let mapped: Result<Vec<_>, _> =
items.iter().map(|x| lift_binary(x.clone(), b.clone(), f)).collect();
Ok(Value::CycleList(Arc::from(mapped?)))
}
(a, Value::CycleList(items)) => {
let mapped: Result<Vec<_>, _> =
items.iter().map(|x| lift_binary(a.clone(), x.clone(), f)).collect();
Ok(Value::CycleList(Arc::from(mapped?)))
}
(a, b) => Ok(float_to_value(f(a.as_float()?, b.as_float()?))),
}
Ok(float_to_value(f(a.as_float()?, b.as_float()?)))
}
fn binary_op<F>(stack: &mut Vec<Value>, f: F) -> Result<(), String>

View File

@@ -1,5 +1,3 @@
//! Word-to-Op translation: maps Forth word names to compiled instructions.
use std::sync::Arc;
use crate::ops::Op;
@@ -13,7 +11,6 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
"dup" => Op::Dup,
"dupn" => Op::Dupn,
"drop" => Op::Drop,
"print" => Op::Print,
"swap" => Op::Swap,
"over" => Op::Over,
"rot" => Op::Rot,
@@ -59,7 +56,7 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
"nand" => Op::Nand,
"nor" => Op::Nor,
"ifelse" => Op::IfElse,
"select" => Op::Pick,
"pick" => Op::Pick,
"sound" => Op::NewCmd,
"." => Op::Emit,
"rand" => Op::Rand(None),
@@ -70,12 +67,8 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
"pcycle" => Op::PCycle(None),
"choose" => Op::Choose(None),
"bounce" => Op::Bounce(None),
"pbounce" => Op::PBounce(None),
"wchoose" => Op::WChoose(None),
"every" => Op::Every(None),
"except" => Op::Except(None),
"every+" => Op::EveryOffset(None),
"except+" => Op::ExceptOffset(None),
"bjork" => Op::Bjork(None),
"pbjork" => Op::PBjork(None),
"chance" => Op::ChanceExec(None),
@@ -101,8 +94,6 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
"loop" => Op::Loop,
"oct" => Op::Oct,
"clear" => Op::ClearCmd,
"all" => Op::EmitAll,
"noall" => Op::ClearGlobal,
".." => Op::IntRange,
".," => Op::StepRange,
"gen" => Op::Generate,
@@ -110,19 +101,13 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
"euclid" => Op::Euclid,
"euclidrot" => Op::EuclidRot,
"times" => Op::Times,
"map" => Op::Map,
"m." => Op::MidiEmit,
"ccval" => Op::GetMidiCC,
"mclock" => Op::MidiClock,
"mstart" => Op::MidiStart,
"mstop" => Op::MidiStop,
"mcont" => Op::MidiContinue,
"rec" => Op::Rec,
"overdub" | "dub" => Op::Overdub,
"orec" => Op::Orec,
"odub" => Op::Odub,
"forget" => Op::Forget,
"index" => Op::Index(None),
"key!" => Op::SetKey,
"tp" => Op::Transpose,
"inv" => Op::Invert,
@@ -216,10 +201,9 @@ fn attach_span(op: &mut Op, span: SourceSpan) {
match op {
Op::Rand(s) | Op::ExpRand(s) | Op::LogRand(s) | Op::Coin(s)
| Op::Choose(s) | Op::WChoose(s) | Op::Cycle(s) | Op::PCycle(s)
| Op::Bounce(s) | Op::PBounce(s) | Op::ChanceExec(s) | Op::ProbExec(s)
| Op::Every(s) | Op::Except(s) | Op::EveryOffset(s) | Op::ExceptOffset(s)
| Op::Bjork(s) | Op::PBjork(s)
| Op::Count(s) | Op::Index(s) => *s = Some(span),
| Op::Bounce(s) | Op::ChanceExec(s) | Op::ProbExec(s)
| Op::Every(s)
| Op::Bjork(s) | Op::PBjork(s) => *s = Some(span),
_ => {}
}
}

View File

@@ -1,6 +1,6 @@
//! Word metadata for core language primitives (stack, arithmetic, logic, variables, definitions).
use super::{Word, WordCompile::*};
// Stack, Arithmetic, Comparison, Logic, Control, Variables, Definitions
pub(super) const WORDS: &[Word] = &[
// Stack manipulation
Word {
@@ -33,16 +33,6 @@ pub(super) const WORDS: &[Word] = &[
compile: Simple,
varargs: false,
},
Word {
name: "print",
aliases: &[],
category: "Stack",
stack: "(x --)",
desc: "Print top of stack to footer bar",
example: "42 print",
compile: Simple,
varargs: false,
},
Word {
name: "swap",
aliases: &[],
@@ -512,17 +502,17 @@ pub(super) const WORDS: &[Word] = &[
category: "Logic",
stack: "(true-quot false-quot bool --)",
desc: "Execute true-quot if true, else false-quot",
example: "( 1 ) ( 2 ) coin ifelse",
example: "{ 1 } { 2 } coin ifelse",
compile: Simple,
varargs: false,
},
Word {
name: "select",
name: "pick",
aliases: &[],
category: "Logic",
stack: "(..quots n --)",
desc: "Execute nth quotation (0-indexed)",
example: "( 1 ) ( 2 ) ( 3 ) 2 select => 3",
example: "{ 1 } { 2 } { 3 } 2 pick => 3",
compile: Simple,
varargs: true,
},
@@ -532,7 +522,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Logic",
stack: "(quot bool --)",
desc: "Execute quotation if true",
example: "( 2 distort ) 0.5 chance ?",
example: "{ 2 distort } 0.5 chance ?",
compile: Simple,
varargs: false,
},
@@ -542,7 +532,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Logic",
stack: "(quot bool --)",
desc: "Execute quotation if false",
example: "( 1 distort ) 0.5 chance !?",
example: "{ 1 distort } 0.5 chance !?",
compile: Simple,
varargs: false,
},
@@ -552,7 +542,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Logic",
stack: "(quot --)",
desc: "Execute quotation unconditionally",
example: "( 2 * ) apply",
example: "{ 2 * } apply",
compile: Simple,
varargs: false,
},
@@ -563,17 +553,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Control",
stack: "(n quot --)",
desc: "Execute quotation n times, @i holds current index",
example: "4 ( @i . ) times => 0 1 2 3",
compile: Simple,
varargs: false,
},
Word {
name: "map",
aliases: &[],
category: "Control",
stack: "(..vals quot -- ..results)",
desc: "Apply quotation to each stack element",
example: "1 2 3 ( 10 * ) map => 10 20 30",
example: "4 { @i . } times => 0 1 2 3",
compile: Simple,
varargs: false,
},

View File

@@ -1,6 +1,6 @@
//! Word metadata for audio effect parameters (filter, envelope, reverb, delay, lo-fi, stereo, mod FX).
use super::{Word, WordCompile::*};
// Filter, Envelope, Reverb, Delay, Lo-fi, Stereo, Mod FX
pub(super) const WORDS: &[Word] = &[
// Envelope
Word {
@@ -959,45 +959,4 @@ pub(super) const WORDS: &[Word] = &[
compile: Param,
varargs: true,
},
// Compressor
Word {
name: "comp",
aliases: &[],
category: "Compressor",
stack: "(v.. --)",
desc: "Set sidechain duck amount (0-1)",
example: "0.8 comp",
compile: Param,
varargs: true,
},
Word {
name: "compattack",
aliases: &["cattack"],
category: "Compressor",
stack: "(v.. --)",
desc: "Set compressor attack time in seconds",
example: "0.01 compattack",
compile: Param,
varargs: true,
},
Word {
name: "comprelease",
aliases: &["crelease"],
category: "Compressor",
stack: "(v.. --)",
desc: "Set compressor release time in seconds",
example: "0.15 comprelease",
compile: Param,
varargs: true,
},
Word {
name: "comporbit",
aliases: &["corbit"],
category: "Compressor",
stack: "(v.. --)",
desc: "Set sidechain source orbit",
example: "0 comporbit",
compile: Param,
varargs: true,
},
];

View File

@@ -1,7 +1,6 @@
//! MIDI word definitions: channel, CC, pitch bend, transport, and device routing.
use super::{Word, WordCompile::*};
// MIDI
pub(super) const WORDS: &[Word] = &[
Word {
name: "chan",

View File

@@ -1,5 +1,3 @@
//! Built-in word definitions and lookup for the Forth VM.
mod compile;
mod core;
mod effects;
@@ -13,7 +11,6 @@ use std::sync::LazyLock;
pub(crate) use compile::compile_word;
/// How a word is compiled into ops.
#[derive(Clone, Copy)]
pub enum WordCompile {
Simple,
@@ -22,7 +19,6 @@ pub enum WordCompile {
Probability(f64),
}
/// Metadata for a built-in Forth word.
#[derive(Clone, Copy)]
pub struct Word {
pub name: &'static str,
@@ -35,7 +31,6 @@ pub struct Word {
pub varargs: bool,
}
/// All built-in words, aggregated from every category module.
pub static WORDS: LazyLock<Vec<Word>> = LazyLock::new(|| {
let mut words = Vec::new();
words.extend_from_slice(self::core::WORDS);
@@ -47,7 +42,6 @@ pub static WORDS: LazyLock<Vec<Word>> = LazyLock::new(|| {
words
});
/// Index mapping word names and aliases to their definitions.
static WORD_MAP: LazyLock<HashMap<&'static str, &'static Word>> = LazyLock::new(|| {
let mut map = HashMap::with_capacity(WORDS.len() * 2);
for word in WORDS.iter() {
@@ -59,7 +53,6 @@ static WORD_MAP: LazyLock<HashMap<&'static str, &'static Word>> = LazyLock::new(
map
});
/// Find a word by name or alias.
pub fn lookup_word(name: &str) -> Option<&'static Word> {
WORD_MAP.get(name).copied()
}

View File

@@ -1,7 +1,6 @@
//! Word definitions for music theory, harmony, and chord construction.
use super::{Word, WordCompile::*};
// Music, Chord
pub(super) const WORDS: &[Word] = &[
// Music
Word {

View File

@@ -1,7 +1,6 @@
//! Word metadata for sequencing: probability, timing, context queries, generators.
use super::{Word, WordCompile::*};
// Time, Context, Probability, Generator, Desktop
pub(super) const WORDS: &[Word] = &[
// Probability
Word {
@@ -60,7 +59,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Probability",
stack: "(quot prob --)",
desc: "Execute quotation with probability (0.0-1.0)",
example: "( 2 distort ) 0.75 chance",
example: "{ 2 distort } 0.75 chance",
compile: Simple,
varargs: false,
},
@@ -70,7 +69,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Probability",
stack: "(quot pct --)",
desc: "Execute quotation with probability (0-100)",
example: "( 2 distort ) 75 prob",
example: "{ 2 distort } 75 prob",
compile: Simple,
varargs: false,
},
@@ -114,26 +113,6 @@ pub(super) const WORDS: &[Word] = &[
compile: Simple,
varargs: true,
},
Word {
name: "pbounce",
aliases: &[],
category: "Probability",
stack: "(v1..vn n -- selected)",
desc: "Ping-pong cycle through n items by pattern iteration",
example: "60 64 67 72 4 pbounce",
compile: Simple,
varargs: true,
},
Word {
name: "index",
aliases: &[],
category: "Probability",
stack: "(v1..vn n idx -- selected)",
desc: "Select item at explicit index",
example: "[ c4 e4 g4 ] step index",
compile: Simple,
varargs: true,
},
Word {
name: "wchoose",
aliases: &[],
@@ -150,7 +129,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Probability",
stack: "(quot --)",
desc: "Always execute quotation",
example: "( 2 distort ) always",
example: "{ 2 distort } always",
compile: Probability(1.0),
varargs: false,
},
@@ -160,7 +139,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Probability",
stack: "(quot --)",
desc: "Never execute quotation",
example: "( 2 distort ) never",
example: "{ 2 distort } never",
compile: Probability(0.0),
varargs: false,
},
@@ -170,7 +149,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Probability",
stack: "(quot --)",
desc: "Execute quotation 75% of the time",
example: "( 2 distort ) often",
example: "{ 2 distort } often",
compile: Probability(0.75),
varargs: false,
},
@@ -180,7 +159,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Probability",
stack: "(quot --)",
desc: "Execute quotation 50% of the time",
example: "( 2 distort ) sometimes",
example: "{ 2 distort } sometimes",
compile: Probability(0.5),
varargs: false,
},
@@ -190,7 +169,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Probability",
stack: "(quot --)",
desc: "Execute quotation 25% of the time",
example: "( 2 distort ) rarely",
example: "{ 2 distort } rarely",
compile: Probability(0.25),
varargs: false,
},
@@ -200,7 +179,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Probability",
stack: "(quot --)",
desc: "Execute quotation 10% of the time",
example: "( 2 distort ) almostNever",
example: "{ 2 distort } almostNever",
compile: Probability(0.1),
varargs: false,
},
@@ -210,7 +189,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Probability",
stack: "(quot --)",
desc: "Execute quotation 90% of the time",
example: "( 2 distort ) almostAlways",
example: "{ 2 distort } almostAlways",
compile: Probability(0.9),
varargs: false,
},
@@ -221,37 +200,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Time",
stack: "(quot n --)",
desc: "Execute quotation every nth iteration",
example: "( 2 distort ) 4 every",
compile: Simple,
varargs: false,
},
Word {
name: "except",
aliases: &[],
category: "Time",
stack: "(quot n --)",
desc: "Execute quotation on all iterations except every nth",
example: "( 2 distort ) 4 except",
compile: Simple,
varargs: false,
},
Word {
name: "every+",
aliases: &[],
category: "Time",
stack: "(quot n offset --)",
desc: "Execute quotation every nth iteration with phase offset",
example: "( snare ) 4 2 every+ => fires at iter 2, 6, 10...",
compile: Simple,
varargs: false,
},
Word {
name: "except+",
aliases: &[],
category: "Time",
stack: "(quot n offset --)",
desc: "Skip quotation every nth iteration with phase offset",
example: "( snare ) 4 2 except+ => skips at iter 2, 6, 10...",
example: "{ 2 distort } 4 every",
compile: Simple,
varargs: false,
},
@@ -261,7 +210,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Time",
stack: "(quot k n --)",
desc: "Execute quotation using Euclidean distribution over step runs",
example: "( 2 distort ) 3 8 bjork",
example: "{ 2 distort } 3 8 bjork",
compile: Simple,
varargs: false,
},
@@ -271,7 +220,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Time",
stack: "(quot k n --)",
desc: "Execute quotation using Euclidean distribution over pattern iterations",
example: "( 2 distort ) 3 8 pbjork",
example: "{ 2 distort } 3 8 pbjork",
compile: Simple,
varargs: false,
},
@@ -280,8 +229,8 @@ pub(super) const WORDS: &[Word] = &[
aliases: &[],
category: "Time",
stack: "(n --)",
desc: "Fit sample to n steps",
example: "\"break\" s 16 loop @",
desc: "Fit sample to n beats",
example: "\"break\" s 4 loop @",
compile: Simple,
varargs: false,
},
@@ -456,7 +405,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Desktop",
stack: "(-- bool)",
desc: "1 when mouse button held, 0 otherwise",
example: "mdown ( \"crash\" s . ) ?",
example: "mdown { \"crash\" s . } ?",
compile: Context("mdown"),
varargs: false,
},
@@ -487,7 +436,7 @@ pub(super) const WORDS: &[Word] = &[
category: "Generator",
stack: "(quot n -- results...)",
desc: "Execute quotation n times, push all results",
example: "( 1 6 rand ) 4 gen => 4 random values",
example: "{ 1 6 rand } 4 gen => 4 random values",
compile: Simple,
varargs: true,
},

View File

@@ -1,7 +1,6 @@
//! Word metadata for sound commands, sample/oscillator params, FM, modulation, and LFO.
use super::{Word, WordCompile::*};
// Sound, Oscillator, Sample, Wavetable, FM, Modulation, LFO
pub(super) const WORDS: &[Word] = &[
// Sound
Word {
@@ -44,67 +43,6 @@ pub(super) const WORDS: &[Word] = &[
compile: Simple,
varargs: false,
},
Word {
name: "all",
aliases: &[],
category: "Sound",
stack: "(--)",
desc: "Apply current params to all sounds",
example: "500 lpf 0.5 verb all",
compile: Simple,
varargs: false,
},
Word {
name: "noall",
aliases: &[],
category: "Sound",
stack: "(--)",
desc: "Clear global params",
example: "noall",
compile: Simple,
varargs: false,
},
// Recording
Word {
name: "rec",
aliases: &[],
category: "Sound",
stack: "(name --)",
desc: "Toggle recording audio output to named sample",
example: "\"loop1\" rec",
compile: Simple,
varargs: false,
},
Word {
name: "overdub",
aliases: &["dub"],
category: "Sound",
stack: "(name --)",
desc: "Toggle overdub recording onto existing named sample",
example: "\"loop1\" overdub",
compile: Simple,
varargs: false,
},
Word {
name: "orec",
aliases: &[],
category: "Sound",
stack: "(name orbit --)",
desc: "Toggle recording a single orbit into named sample",
example: "\"drums\" 0 orec",
compile: Simple,
varargs: false,
},
Word {
name: "odub",
aliases: &[],
category: "Sound",
stack: "(name orbit --)",
desc: "Toggle overdub recording a single orbit onto named sample",
example: "\"drums\" 0 odub",
compile: Simple,
varargs: false,
},
// Sample
Word {
name: "bank",
@@ -186,26 +124,6 @@ pub(super) const WORDS: &[Word] = &[
compile: Param,
varargs: true,
},
Word {
name: "slice",
aliases: &[],
category: "Sample",
stack: "(v.. --)",
desc: "Divide sample into N equal slices",
example: r#""break" s 8 slice 3 pick ."#,
compile: Param,
varargs: true,
},
Word {
name: "pick",
aliases: &[],
category: "Sample",
stack: "(v.. --)",
desc: "Select which slice to play (0-indexed, wraps)",
example: r#""break" s 8 slice 3 pick ."#,
compile: Param,
varargs: true,
},
Word {
name: "voice",
aliases: &[],

View File

@@ -1,15 +0,0 @@
# cagire-markdown
Markdown parser and renderer that produces ratatui-styled lines. Used for the built-in help/documentation views.
## Modules
| Module | Description |
|--------|-------------|
| `parser` | Markdown-to-styled-lines conversion |
| `highlighter` | `CodeHighlighter` trait for syntax highlighting in fenced code blocks |
| `theme` | Color mappings for markdown elements |
## Key Trait
- **`CodeHighlighter`** — Implement to provide language-specific syntax highlighting. Returns `Vec<(Style, String)>` per line.

View File

@@ -1,13 +1,9 @@
//! Syntax highlighting trait for fenced code blocks in markdown.
use ratatui::style::Style;
/// Produce styled spans from a single line of source code.
pub trait CodeHighlighter {
fn highlight(&self, line: &str) -> Vec<(Style, String)>;
}
/// Pass-through highlighter that applies no styling.
pub struct NoHighlight;
impl CodeHighlighter for NoHighlight {

View File

@@ -1,5 +1,3 @@
//! Parse markdown into styled ratatui lines with pluggable syntax highlighting.
mod highlighter;
mod parser;
mod theme;

View File

@@ -1,5 +1,3 @@
//! Parse markdown text into styled ratatui lines with syntax-highlighted code blocks.
use minimad::{Composite, CompositeStyle, Compound, Line, TableRow};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line as RLine, Span};
@@ -7,20 +5,17 @@ use ratatui::text::{Line as RLine, Span};
use crate::highlighter::CodeHighlighter;
use crate::theme::MarkdownTheme;
/// Span of lines within a parsed document that form a fenced code block.
pub struct CodeBlock {
pub start_line: usize,
pub end_line: usize,
pub source: String,
}
/// Result of parsing a markdown string: styled lines and extracted code blocks.
pub struct ParsedMarkdown {
pub lines: Vec<RLine<'static>>,
pub code_blocks: Vec<CodeBlock>,
}
/// Parse markdown text into themed, syntax-highlighted ratatui lines.
pub fn parse<T: MarkdownTheme, H: CodeHighlighter>(
md: &str,
theme: &T,
@@ -49,7 +44,7 @@ pub fn parse<T: MarkdownTheme, H: CodeHighlighter>(
let close_block = |start: Option<usize>,
source: &mut Vec<String>,
blocks: &mut Vec<CodeBlock>,
lines: &[RLine<'static>]| {
lines: &Vec<RLine<'static>>| {
if let Some(start) = start {
blocks.push(CodeBlock {
start_line: start,
@@ -123,7 +118,7 @@ pub fn parse<T: MarkdownTheme, H: CodeHighlighter>(
ParsedMarkdown { lines, code_blocks }
}
fn preprocess_markdown(md: &str) -> String {
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);
@@ -167,7 +162,7 @@ fn preprocess_markdown(md: &str) -> String {
out
}
fn convert_dash_lists(line: &str) -> String {
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();

View File

@@ -1,8 +1,5 @@
//! Style provider trait for markdown rendering.
use ratatui::style::{Color, Modifier, Style};
/// Style provider for each markdown element type.
pub trait MarkdownTheme {
fn h1(&self) -> Style;
fn h2(&self) -> Style;
@@ -19,7 +16,6 @@ pub trait MarkdownTheme {
fn table_row_odd(&self) -> Color;
}
/// Fallback theme with hardcoded terminal colors, used in tests.
pub struct DefaultTheme;
impl MarkdownTheme for DefaultTheme {

View File

@@ -10,9 +10,3 @@ description = "Project data structures for cagire sequencer"
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
rmp-serde = "1"
brotli = "7"
base64 = "0.22"
[dev-dependencies]
flate2 = "1"

View File

@@ -1,22 +0,0 @@
# cagire-project
Project data model and persistence for Cagire.
## Modules
| Module | Description |
|--------|-------------|
| `project` | `Project`, `Bank`, `Pattern`, `Step` structs and constants |
| `file` | File I/O (save/load) |
| `share` | Project sharing/export |
## Key Types
- **`Project`** — Top-level container: banks of patterns
- **`Bank`** — Collection of patterns
- **`Pattern`** — Sequence of steps with metadata
- **`Step`** — Single step holding a Forth script
## Constants
`MAX_BANKS=32`, `MAX_PATTERNS=32`, `MAX_STEPS=1024`

View File

@@ -1,17 +1,15 @@
//! JSON-based project file persistence with versioned format.
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::project::{Bank, PatternSpeed, Project};
use crate::project::{Bank, Project};
const VERSION: u8 = 1;
const EXTENSION: &str = "cagire";
pub const EXTENSION: &str = "cagire";
fn ensure_extension(path: &Path) -> PathBuf {
pub fn ensure_extension(path: &Path) -> PathBuf {
if path.extension().map(|e| e == EXTENSION).unwrap_or(false) {
path.to_path_buf()
} else {
@@ -31,24 +29,6 @@ struct ProjectFile {
playing_patterns: Vec<(usize, usize)>,
#[serde(default, skip_serializing_if = "String::is_empty")]
prelude: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
script: String,
#[serde(default, skip_serializing_if = "is_default_speed")]
script_speed: PatternSpeed,
#[serde(default = "default_script_length", skip_serializing_if = "is_default_script_length")]
script_length: usize,
}
fn is_default_speed(s: &PatternSpeed) -> bool {
*s == PatternSpeed::default()
}
fn default_script_length() -> usize {
16
}
fn is_default_script_length(n: &usize) -> bool {
*n == default_script_length()
}
fn default_tempo() -> f64 {
@@ -64,9 +44,6 @@ impl From<&Project> for ProjectFile {
tempo: project.tempo,
playing_patterns: project.playing_patterns.clone(),
prelude: project.prelude.clone(),
script: project.script.clone(),
script_speed: project.script_speed,
script_length: project.script_length,
}
}
}
@@ -79,16 +56,12 @@ impl From<ProjectFile> for Project {
tempo: file.tempo,
playing_patterns: file.playing_patterns,
prelude: file.prelude,
script: file.script,
script_speed: file.script_speed,
script_length: file.script_length,
};
project.normalize();
project
}
}
/// Error returned by project save/load operations.
#[derive(Debug)]
pub enum FileError {
Io(io::Error),
@@ -118,7 +91,6 @@ impl From<serde_json::Error> for FileError {
}
}
/// Write a project to disk as pretty-printed JSON, returning the final path.
pub fn save(project: &Project, path: &Path) -> Result<PathBuf, FileError> {
let path = ensure_extension(path);
let file = ProjectFile::from(project);
@@ -127,13 +99,11 @@ pub fn save(project: &Project, path: &Path) -> Result<PathBuf, FileError> {
Ok(path)
}
/// Read a project from a `.cagire` file on disk.
pub fn load(path: &Path) -> Result<Project, FileError> {
let json = fs::read_to_string(path)?;
load_str(&json)
}
/// Parse a project from a JSON string.
pub fn load_str(json: &str) -> Result<Project, FileError> {
let file: ProjectFile = serde_json::from_str(json)?;
if file.version > VERSION {

View File

@@ -2,15 +2,10 @@
mod file;
mod project;
pub mod share;
/// Maximum number of banks in a project.
pub const MAX_BANKS: usize = 32;
/// Maximum number of patterns per bank.
pub const MAX_PATTERNS: usize = 32;
/// Maximum number of steps per pattern.
pub const MAX_STEPS: usize = 1024;
/// Default pattern length in steps.
pub const DEFAULT_LENGTH: usize = 16;
pub use file::{load, load_str, save, FileError};

View File

@@ -6,7 +6,6 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::{DEFAULT_LENGTH, MAX_BANKS, MAX_PATTERNS, MAX_STEPS};
/// Speed multiplier for a pattern, expressed as a rational fraction.
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct PatternSpeed {
pub num: u8,
@@ -38,12 +37,10 @@ impl PatternSpeed {
Self::OCTO,
];
/// Return the speed as a floating-point multiplier.
pub fn multiplier(&self) -> f64 {
self.num as f64 / self.denom as f64
}
/// Format as a human-readable label (e.g. "2x", "1/4x").
pub fn label(&self) -> String {
if self.denom == 1 {
format!("{}x", self.num)
@@ -52,7 +49,6 @@ impl PatternSpeed {
}
}
/// Return the next faster preset, or self if already at maximum.
pub fn next(&self) -> Self {
let current = self.multiplier();
Self::PRESETS
@@ -62,7 +58,6 @@ impl PatternSpeed {
.unwrap_or(*self)
}
/// Return the next slower preset, or self if already at minimum.
pub fn prev(&self) -> Self {
let current = self.multiplier();
Self::PRESETS
@@ -73,7 +68,6 @@ impl PatternSpeed {
.unwrap_or(*self)
}
/// Parse a speed label like "2x" or "1/4x" into a `PatternSpeed`.
pub fn from_label(s: &str) -> Option<Self> {
let s = s.trim().trim_end_matches('x');
if let Some((num, denom)) = s.split_once('/') {
@@ -145,7 +139,6 @@ impl<'de> Deserialize<'de> for PatternSpeed {
}
}
/// Quantization grid for launching patterns.
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
pub enum LaunchQuantization {
Immediate,
@@ -158,7 +151,6 @@ pub enum LaunchQuantization {
}
impl LaunchQuantization {
/// Human-readable label for display.
pub fn label(&self) -> &'static str {
match self {
Self::Immediate => "Immediate",
@@ -170,7 +162,6 @@ impl LaunchQuantization {
}
}
/// Cycle to the next longer quantization, clamped at `Bars8`.
pub fn next(&self) -> Self {
match self {
Self::Immediate => Self::Beat,
@@ -182,7 +173,6 @@ impl LaunchQuantization {
}
}
/// Cycle to the next shorter quantization, clamped at `Immediate`.
pub fn prev(&self) -> Self {
match self {
Self::Immediate => Self::Immediate,
@@ -195,7 +185,6 @@ impl LaunchQuantization {
}
}
/// How a pattern synchronizes when launched: restart or phase-lock.
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
pub enum SyncMode {
#[default]
@@ -204,7 +193,6 @@ pub enum SyncMode {
}
impl SyncMode {
/// Human-readable label for display.
pub fn label(&self) -> &'static str {
match self {
Self::Reset => "Reset",
@@ -212,7 +200,6 @@ impl SyncMode {
}
}
/// Toggle between Reset and PhaseLock.
pub fn toggle(&self) -> Self {
match self {
Self::Reset => Self::PhaseLock,
@@ -221,7 +208,6 @@ impl SyncMode {
}
}
/// What happens when a pattern finishes: loop, stop, or chain to another.
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
pub enum FollowUp {
#[default]
@@ -231,7 +217,6 @@ pub enum FollowUp {
}
impl FollowUp {
/// Human-readable label for display.
pub fn label(&self) -> &'static str {
match self {
Self::Loop => "Loop",
@@ -240,7 +225,6 @@ impl FollowUp {
}
}
/// Cycle forward through follow-up modes.
pub fn next_mode(&self) -> Self {
match self {
Self::Loop => Self::Stop,
@@ -249,7 +233,6 @@ impl FollowUp {
}
}
/// Cycle backward through follow-up modes.
pub fn prev_mode(&self) -> Self {
match self {
Self::Loop => Self::Chain { bank: 0, pattern: 0 },
@@ -263,7 +246,6 @@ fn is_default_follow_up(f: &FollowUp) -> bool {
*f == FollowUp::default()
}
/// Single step in a pattern, holding a Forth script and optional metadata.
#[derive(Clone, Serialize, Deserialize)]
pub struct Step {
pub active: bool,
@@ -275,12 +257,10 @@ pub struct Step {
}
impl Step {
/// True if all fields are at their default values.
pub fn is_default(&self) -> bool {
self.active && self.script.is_empty() && self.source.is_none() && self.name.is_none()
}
/// True if the script is non-empty.
pub fn has_content(&self) -> bool {
!self.script.is_empty()
}
@@ -297,14 +277,12 @@ impl Default for Step {
}
}
/// Sequence of steps with playback settings (speed, quantization, sync, follow-up).
#[derive(Clone)]
pub struct Pattern {
pub steps: Vec<Step>,
pub length: usize,
pub speed: PatternSpeed,
pub name: Option<String>,
pub description: Option<String>,
pub quantization: LaunchQuantization,
pub sync_mode: SyncMode,
pub follow_up: FollowUp,
@@ -339,8 +317,6 @@ struct SparsePattern {
speed: PatternSpeed,
#[serde(default, skip_serializing_if = "Option::is_none")]
name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(default, skip_serializing_if = "is_default_quantization")]
quantization: LaunchQuantization,
#[serde(default, skip_serializing_if = "is_default_sync_mode")]
@@ -366,8 +342,6 @@ struct LegacyPattern {
#[serde(default)]
name: Option<String>,
#[serde(default)]
description: Option<String>,
#[serde(default)]
quantization: LaunchQuantization,
#[serde(default)]
sync_mode: SyncMode,
@@ -396,7 +370,6 @@ impl Serialize for Pattern {
length: self.length,
speed: self.speed,
name: self.name.clone(),
description: self.description.clone(),
quantization: self.quantization,
sync_mode: self.sync_mode,
follow_up: self.follow_up,
@@ -432,7 +405,6 @@ impl<'de> Deserialize<'de> for Pattern {
length: sparse.length,
speed: sparse.speed,
name: sparse.name,
description: sparse.description,
quantization: sparse.quantization,
sync_mode: sparse.sync_mode,
follow_up: sparse.follow_up,
@@ -443,7 +415,6 @@ impl<'de> Deserialize<'de> for Pattern {
length: legacy.length,
speed: legacy.speed,
name: legacy.name,
description: legacy.description,
quantization: legacy.quantization,
sync_mode: legacy.sync_mode,
follow_up: legacy.follow_up,
@@ -459,7 +430,6 @@ impl Default for Pattern {
length: DEFAULT_LENGTH,
speed: PatternSpeed::default(),
name: None,
description: None,
quantization: LaunchQuantization::default(),
sync_mode: SyncMode::default(),
follow_up: FollowUp::default(),
@@ -468,17 +438,14 @@ impl Default for Pattern {
}
impl Pattern {
/// Borrow a step by index.
pub fn step(&self, index: usize) -> Option<&Step> {
self.steps.get(index)
}
/// Mutably borrow a step by index.
pub fn step_mut(&mut self, index: usize) -> Option<&mut Step> {
self.steps.get_mut(index)
}
/// Set the active length, clamped to `[1, MAX_STEPS]`.
pub fn set_length(&mut self, length: usize) {
let length = length.clamp(1, MAX_STEPS);
while self.steps.len() < length {
@@ -487,7 +454,6 @@ impl Pattern {
self.length = length;
}
/// Follow the source chain from `index` to find the originating step.
pub fn resolve_source(&self, index: usize) -> usize {
let mut current = index;
for _ in 0..self.steps.len() {
@@ -504,22 +470,20 @@ impl Pattern {
index
}
/// Return the script at the resolved source of `index`.
pub fn resolve_script(&self, index: usize) -> Option<&str> {
let source_idx = self.resolve_source(index);
self.steps.get(source_idx).map(|s| s.script.as_str())
}
/// Count active-length steps that have a script or a source reference.
pub fn content_step_count(&self) -> usize {
self.steps[..self.length]
.iter()
.filter(|s| s.has_content() || s.source.is_some())
.count()
}
}
/// Collection of patterns forming a bank.
#[derive(Clone, Serialize, Deserialize)]
pub struct Bank {
pub patterns: Vec<Pattern>,
@@ -528,7 +492,6 @@ pub struct Bank {
}
impl Bank {
/// Count patterns that contain at least one non-empty step.
pub fn content_pattern_count(&self) -> usize {
self.patterns
.iter()
@@ -546,7 +509,6 @@ impl Default for Bank {
}
}
/// Top-level project: banks, tempo, sample paths, and prelude script.
#[derive(Clone, Serialize, Deserialize)]
pub struct Project {
pub banks: Vec<Bank>,
@@ -558,22 +520,12 @@ pub struct Project {
pub playing_patterns: Vec<(usize, usize)>,
#[serde(default)]
pub prelude: String,
#[serde(default)]
pub script: String,
#[serde(default)]
pub script_speed: PatternSpeed,
#[serde(default = "default_script_length")]
pub script_length: usize,
}
fn default_tempo() -> f64 {
120.0
}
fn default_script_length() -> usize {
16
}
impl Default for Project {
fn default() -> Self {
Self {
@@ -582,25 +534,19 @@ impl Default for Project {
tempo: default_tempo(),
playing_patterns: Vec::new(),
prelude: String::new(),
script: String::new(),
script_speed: PatternSpeed::default(),
script_length: default_script_length(),
}
}
}
impl Project {
/// Borrow a pattern by bank and pattern index.
pub fn pattern_at(&self, bank: usize, pattern: usize) -> &Pattern {
&self.banks[bank].patterns[pattern]
}
/// Mutably borrow a pattern by bank and pattern index.
pub fn pattern_at_mut(&mut self, bank: usize, pattern: usize) -> &mut Pattern {
&mut self.banks[bank].patterns[pattern]
}
/// Pad banks, patterns, and steps to their maximum sizes after deserialization.
pub fn normalize(&mut self) {
self.banks.resize_with(MAX_BANKS, Bank::default);
for bank in &mut self.banks {

View File

@@ -1,214 +0,0 @@
//! Pattern and project sharing via compact text strings.
//!
//! Export: data → MessagePack → Brotli → base64 URL-safe → prefix
//! Import: strip prefix → base64 decode → Brotli decompress → MessagePack → data
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use crate::{Bank, Pattern};
const PATTERN_PREFIX: &str = "cgr:";
const BANK_PREFIX: &str = "cgrb:";
/// Error during pattern or bank import/export.
#[derive(Debug)]
pub enum ShareError {
InvalidPrefix,
Base64(base64::DecodeError),
Decompress(std::io::Error),
Deserialize(rmp_serde::decode::Error),
Serialize(rmp_serde::encode::Error),
Compress(std::io::Error),
}
impl std::fmt::Display for ShareError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidPrefix => write!(f, "missing cgr:/cgrb: prefix"),
Self::Base64(e) => write!(f, "base64: {e}"),
Self::Decompress(e) => write!(f, "decompress: {e}"),
Self::Deserialize(e) => write!(f, "deserialize: {e}"),
Self::Serialize(e) => write!(f, "serialize: {e}"),
Self::Compress(e) => write!(f, "compress: {e}"),
}
}
}
fn compress(data: &[u8]) -> Result<Vec<u8>, ShareError> {
let mut output = Vec::new();
let params = brotli::enc::BrotliEncoderParams {
quality: 11,
lgwin: 22,
lgblock: 0,
..Default::default()
};
brotli::BrotliCompress(&mut &data[..], &mut output, &params).map_err(ShareError::Compress)?;
Ok(output)
}
fn decompress(data: &[u8]) -> Result<Vec<u8>, ShareError> {
let mut output = Vec::new();
brotli::BrotliDecompress(&mut &data[..], &mut output).map_err(ShareError::Decompress)?;
Ok(output)
}
fn encode<T: serde::Serialize>(value: &T, prefix: &str) -> Result<String, ShareError> {
let packed = rmp_serde::to_vec_named(value).map_err(ShareError::Serialize)?;
let compressed = compress(&packed)?;
let encoded = URL_SAFE_NO_PAD.encode(&compressed);
Ok(format!("{prefix}{encoded}"))
}
fn decode<T: serde::de::DeserializeOwned>(text: &str, prefix: &str) -> Result<T, ShareError> {
let text = text.trim();
let payload = text.strip_prefix(prefix).ok_or(ShareError::InvalidPrefix)?;
let compressed = URL_SAFE_NO_PAD.decode(payload).map_err(ShareError::Base64)?;
let packed = decompress(&compressed)?;
rmp_serde::from_slice(&packed).map_err(ShareError::Deserialize)
}
/// Encode a pattern as a shareable `cgr:` string.
pub fn export(pattern: &Pattern) -> Result<String, ShareError> {
encode(pattern, PATTERN_PREFIX)
}
/// Decode a `cgr:` string back into a pattern.
pub fn import(text: &str) -> Result<Pattern, ShareError> {
decode(text, PATTERN_PREFIX)
}
/// Encode a bank as a shareable `cgrb:` string.
pub fn export_bank(bank: &Bank) -> Result<String, ShareError> {
encode(bank, BANK_PREFIX)
}
/// Decode a `cgrb:` string back into a bank.
pub fn import_bank(text: &str) -> Result<Bank, ShareError> {
decode(text, BANK_PREFIX)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Step;
#[test]
fn roundtrip_empty() {
let pattern = Pattern::default();
let encoded = export(&pattern).expect("export pattern");
assert!(encoded.starts_with("cgr:"));
let decoded = import(&encoded).expect("import pattern");
assert_eq!(decoded.length, pattern.length);
assert_eq!(decoded.steps.len(), pattern.steps.len());
}
#[test]
fn roundtrip_with_steps() {
let mut pattern = Pattern::default();
pattern.steps[0] = Step {
active: true,
script: "kick 60 note".to_string(),
source: None,
name: Some("kick".to_string()),
};
pattern.steps[1] = Step {
active: false,
script: "snare".to_string(),
source: None,
name: None,
};
pattern.steps[3] = Step {
active: true,
script: String::new(),
source: Some(0),
name: None,
};
pattern.length = 8;
pattern.name = Some("Test".to_string());
let encoded = export(&pattern).expect("export pattern");
let decoded = import(&encoded).expect("import pattern");
assert_eq!(decoded.length, 8);
assert_eq!(decoded.name.as_deref(), Some("Test"));
assert_eq!(decoded.steps[0].script, "kick 60 note");
assert_eq!(decoded.steps[0].name.as_deref(), Some("kick"));
assert!(!decoded.steps[1].active);
assert_eq!(decoded.steps[1].script, "snare");
assert_eq!(decoded.steps[3].source, Some(0));
}
#[test]
fn bad_prefix() {
assert!(matches!(import("xxx:abc"), Err(ShareError::InvalidPrefix)));
}
#[test]
fn bad_base64() {
assert!(matches!(import("cgr:!!!"), Err(ShareError::Base64(_))));
}
#[test]
fn whitespace_trimming() {
let pattern = Pattern::default();
let encoded = export(&pattern).expect("export pattern");
let padded = format!(" {encoded} \n");
let decoded = import(&padded).expect("import padded pattern");
assert_eq!(decoded.length, pattern.length);
}
#[test]
fn msgpack_brotli_smaller_than_json_deflate() {
let mut pattern = Pattern::default();
for i in 0..16 {
pattern.steps[i] = Step {
active: true,
script: format!("kick {i} note 0.5 dur"),
source: None,
name: Some(format!("step_{i}")),
};
}
pattern.length = 16;
// Current (msgpack+brotli)
let new_encoded = export(&pattern).expect("export pattern");
// Old pipeline (json+deflate) for comparison
use std::io::Write;
let json = serde_json::to_vec(&pattern).expect("serialize json");
let mut encoder =
flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::best());
encoder.write_all(&json).expect("write to encoder");
let old_compressed = encoder.finish().expect("finish encoder");
let old_encoded = format!("cgr:{}", URL_SAFE_NO_PAD.encode(&old_compressed));
assert!(
new_encoded.len() < old_encoded.len(),
"msgpack+brotli ({}) should be smaller than json+deflate ({})",
new_encoded.len(),
old_encoded.len()
);
}
#[test]
fn roundtrip_bank() {
let mut bank = Bank::default();
bank.patterns[0].steps[0] = Step {
active: true,
script: "kick 60 note".to_string(),
source: None,
name: Some("kick".to_string()),
};
bank.patterns[0].length = 8;
bank.name = Some("Drums".to_string());
let encoded = export_bank(&bank).expect("export bank");
assert!(encoded.starts_with("cgrb:"));
let decoded = import_bank(&encoded).expect("import bank");
assert_eq!(decoded.name.as_deref(), Some("Drums"));
assert_eq!(decoded.patterns[0].length, 8);
assert_eq!(decoded.patterns[0].steps[0].script, "kick 60 note");
}
}

View File

@@ -11,4 +11,4 @@ description = "TUI components for cagire sequencer"
rand = "0.8"
ratatui = "0.30"
regex = "1"
tui-textarea = { git = "https://github.com/phsym/tui-textarea", rev = "e2ec4d3", features = ["search"] }
tui-textarea = { git = "https://github.com/phsym/tui-textarea", branch = "main", features = ["search"] }

View File

@@ -1,25 +0,0 @@
# cagire-ratatui
TUI widget library and theme system for Cagire.
## Widgets
`category_list`, `confirm`, `editor`, `file_browser`, `hint_bar`, `lissajous`, `list_select`, `modal`, `nav_minimap`, `props_form`, `sample_browser`, `scope`, `scroll_indicators`, `search_bar`, `section_header`, `sparkles`, `spectrum`, `text_input`, `vu_meter`, `waveform`
## Theme System
The `theme/` module provides a palette-based theming system using Oklab color space.
| Module | Description |
|--------|-------------|
| `mod` | `THEMES` array, `CURRENT_THEME` thread-local, `get()`/`set()` |
| `palette` | `Palette` (14 fields), color manipulation helpers (`shift`, `mix`, `tint_bg`, ...) |
| `build` | Derives ~190 `ThemeColors` fields from a `Palette` |
| `transform` | HSV-based hue rotation for generated palettes |
25 built-in themes.
## Key Types
- **`Palette`** — 14-field color definition, input to theme generation
- **`ThemeColors`** — ~190 derived semantic colors used throughout the UI

View File

@@ -1,5 +1,3 @@
//! Collapsible categorized list widget with section headers.
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::widgets::{Block, Borders, List, ListItem};
@@ -7,20 +5,17 @@ use ratatui::Frame;
use crate::theme;
/// Entry in a category list: either a section header or a leaf item.
pub struct CategoryItem<'a> {
pub label: &'a str,
pub is_section: bool,
pub collapsed: bool,
}
/// What is currently selected: a leaf item or a section header.
pub enum Selection {
Item(usize),
Section(usize),
}
/// Scrollable list with collapsible section headers.
pub struct CategoryList<'a> {
items: &'a [CategoryItem<'a>],
selection: Selection,

View File

@@ -1,5 +1,3 @@
//! Yes/No confirmation dialog widget.
use crate::theme;
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
use ratatui::style::Style;
@@ -9,7 +7,6 @@ use ratatui::Frame;
use super::ModalFrame;
/// Modal dialog with Yes/No buttons.
pub struct ConfirmModal<'a> {
title: &'a str,
message: &'a str,

View File

@@ -1,5 +1,3 @@
//! Script editor widget with completion, search, and sample finder popups.
use std::cell::Cell;
use crate::theme;
@@ -12,10 +10,8 @@ use ratatui::{
};
use tui_textarea::TextArea;
/// Callback that syntax-highlights a single line, returning styled spans (bool = annotation).
pub type Highlighter<'a> = &'a dyn Fn(usize, &str) -> Vec<(Style, String, bool)>;
/// Metadata for a single autocomplete entry.
#[derive(Clone)]
pub struct CompletionCandidate {
pub name: String,
@@ -82,7 +78,6 @@ impl SearchState {
}
}
/// Multi-line text editor backed by tui_textarea.
pub struct Editor {
text: TextArea<'static>,
completion: CompletionState,
@@ -104,14 +99,6 @@ impl Editor {
self.text.is_selecting()
}
pub fn move_cursor_to(&mut self, row: u16, col: u16) {
self.text.move_cursor(tui_textarea::CursorMove::Jump(row, col));
}
pub fn scroll_offset(&self) -> u16 {
self.scroll_offset.get()
}
pub fn copy(&mut self) {
self.text.copy();
}
@@ -124,14 +111,6 @@ impl Editor {
self.text.paste()
}
pub fn yank_text(&self) -> String {
self.text.yank_text()
}
pub fn set_yank_text(&mut self, text: impl Into<String>) {
self.text.set_yank_text(text);
}
pub fn select_all(&mut self) {
self.text.select_all();
}
@@ -159,11 +138,7 @@ impl Editor {
}
pub fn set_content(&mut self, lines: Vec<String>) {
let yank = self.text.yank_text();
self.text = TextArea::new(lines);
if !yank.is_empty() {
self.text.set_yank_text(yank);
}
self.completion.active = false;
self.sample_finder.active = false;
self.search.query.clear();
@@ -487,7 +462,7 @@ impl Editor {
if is_cursor {
cursor_style
} else if is_selected {
base_style.bg(selection_style.bg.expect("selection style has bg"))
base_style.bg(selection_style.bg.unwrap())
} else {
base_style
}
@@ -707,7 +682,6 @@ impl Editor {
}
}
/// Score a fuzzy match of `query` against `target`. Lower is better; `None` if no match.
pub fn fuzzy_match(query: &str, target: &str) -> Option<usize> {
let target_lower: Vec<char> = target.to_lowercase().chars().collect();
let query_lower: Vec<char> = query.to_lowercase().chars().collect();

View File

@@ -1,5 +1,3 @@
//! File/directory browser modal widget.
use crate::theme;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Color, Style};
@@ -9,7 +7,6 @@ use ratatui::Frame;
use super::ModalFrame;
/// Modal listing files and directories with a filter input line.
pub struct FileBrowserModal<'a> {
title: &'a str,
input: &'a str,

View File

@@ -1,11 +1,8 @@
//! Bottom-bar keyboard hint renderer.
use ratatui::text::{Line, Span};
use ratatui::style::Style;
use crate::theme;
/// Build a styled line of key/action pairs for the hint bar.
pub fn hint_line(pairs: &[(&str, &str)]) -> Line<'static> {
let theme = theme::get();
let key_style = Style::default().fg(theme.hint.key);

View File

@@ -38,7 +38,7 @@ 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, SpectrumStyle};
pub use spectrum::Spectrum;
pub use text_input::TextInputModal;
pub use vu_meter::VuMeter;
pub use waveform::Waveform;

View File

@@ -1,5 +1,3 @@
//! Lissajous XY oscilloscope widget using braille characters.
use crate::theme;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
@@ -9,22 +7,12 @@ use std::cell::RefCell;
thread_local! {
static PATTERNS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
static TRAIL: RefCell<TrailState> = const { RefCell::new(TrailState { fine_w: 0, fine_h: 0, heat: Vec::new() }) };
}
struct TrailState {
fine_w: usize,
fine_h: usize,
heat: Vec<f32>,
}
/// XY oscilloscope plotting left vs right channels as a Lissajous curve.
pub struct Lissajous<'a> {
left: &'a [f32],
right: &'a [f32],
color: Option<Color>,
gain: f32,
trails: bool,
}
impl<'a> Lissajous<'a> {
@@ -33,25 +21,13 @@ impl<'a> Lissajous<'a> {
left,
right,
color: None,
gain: 1.0,
trails: false,
}
}
pub fn trails(mut self, enabled: bool) -> Self {
self.trails = enabled;
self
}
pub fn color(mut self, c: Color) -> Self {
self.color = Some(c);
self
}
pub fn gain(mut self, g: f32) -> Self {
self.gain = g;
self
}
}
impl Widget for Lissajous<'_> {
@@ -60,16 +36,6 @@ impl Widget for Lissajous<'_> {
return;
}
if self.trails {
self.render_trails(area, buf);
} else {
self.render_normal(area, buf);
}
}
}
impl Lissajous<'_> {
fn render_normal(self, area: Rect, buf: &mut Buffer) {
let color = self.color.unwrap_or_else(|| theme::get().meter.low);
let width = area.width as usize;
let height = area.height as usize;
@@ -77,6 +43,14 @@ impl Lissajous<'_> {
let fine_height = height * 4;
let len = self.left.len().min(self.right.len());
let peak = self
.left
.iter()
.chain(self.right.iter())
.map(|s| s.abs())
.fold(0.0f32, f32::max);
let gain = if peak > 0.001 { 1.0 / peak } else { 1.0 };
PATTERNS.with(|p| {
let mut patterns = p.borrow_mut();
let size = width * height;
@@ -84,9 +58,10 @@ impl Lissajous<'_> {
patterns.resize(size, 0);
for i in 0..len {
let l = (self.left[i] * self.gain).clamp(-1.0, 1.0);
let r = (self.right[i] * self.gain).clamp(-1.0, 1.0);
let l = (self.left[i] * gain).clamp(-1.0, 1.0);
let r = (self.right[i] * gain).clamp(-1.0, 1.0);
// X = right channel, Y = left channel (inverted so up = positive)
let fine_x = ((r + 1.0) * 0.5 * (fine_width - 1) as f32).round() as usize;
let fine_y = ((1.0 - l) * 0.5 * (fine_height - 1) as f32).round() as usize;
let fine_x = fine_x.min(fine_width - 1);
@@ -97,7 +72,19 @@ impl Lissajous<'_> {
let dot_x = fine_x % 2;
let dot_y = fine_y % 4;
patterns[char_y * width + char_x] |= braille_bit(dot_x, dot_y);
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 {
@@ -113,122 +100,4 @@ impl Lissajous<'_> {
}
});
}
fn render_trails(self, area: Rect, buf: &mut Buffer) {
let theme = theme::get();
let width = area.width as usize;
let height = area.height as usize;
let fine_w = width * 2;
let fine_h = height * 4;
let len = self.left.len().min(self.right.len());
TRAIL.with(|t| {
let mut trail = t.borrow_mut();
// Reset if dimensions changed
if trail.fine_w != fine_w || trail.fine_h != fine_h {
trail.fine_w = fine_w;
trail.fine_h = fine_h;
trail.heat.clear();
trail.heat.resize(fine_w * fine_h, 0.0);
}
// Decay existing heat
for h in trail.heat.iter_mut() {
*h *= 0.85;
}
// Plot new sample points
for i in 0..len {
let l = (self.left[i] * self.gain).clamp(-1.0, 1.0);
let r = (self.right[i] * self.gain).clamp(-1.0, 1.0);
let fx = ((r + 1.0) * 0.5 * (fine_w - 1) as f32).round() as usize;
let fy = ((1.0 - l) * 0.5 * (fine_h - 1) as f32).round() as usize;
let fx = fx.min(fine_w - 1);
let fy = fy.min(fine_h - 1);
trail.heat[fy * fine_w + fx] = 1.0;
}
// Convert heat map to braille
PATTERNS.with(|p| {
let mut patterns = p.borrow_mut();
patterns.clear();
patterns.resize(width * height, 0);
// Track brightest color per cell
let mut colors: Vec<Option<Color>> = vec![None; width * height];
for fy in 0..fine_h {
for fx in 0..fine_w {
let h = trail.heat[fy * fine_w + fx];
if h < 0.05 {
continue;
}
let cx = fx / 2;
let cy = fy / 4;
let dx = fx % 2;
let dy = fy % 4;
let idx = cy * width + cx;
patterns[idx] |= braille_bit(dx, dy);
let dot_color = if h > 0.7 {
theme.meter.high
} else if h > 0.25 {
theme.meter.mid
} else {
theme.meter.low
};
let replace = match colors[idx] {
None => true,
Some(cur) => {
rank_color(dot_color, &theme) > rank_color(cur, &theme)
}
};
if replace {
colors[idx] = Some(dot_color);
}
}
}
for cy in 0..height {
for cx in 0..width {
let idx = cy * width + cx;
let pattern = patterns[idx];
if pattern != 0 {
let ch = char::from_u32(0x2800 + pattern as u32).unwrap_or(' ');
let color = colors[idx].unwrap_or(theme.meter.low);
buf[(area.x + cx as u16, area.y + cy as u16)]
.set_char(ch)
.set_fg(color);
}
}
}
});
});
}
}
fn braille_bit(dot_x: usize, dot_y: usize) -> u8 {
match (dot_x, dot_y) {
(0, 0) => 0x01,
(0, 1) => 0x02,
(0, 2) => 0x04,
(0, 3) => 0x40,
(1, 0) => 0x08,
(1, 1) => 0x10,
(1, 2) => 0x20,
(1, 3) => 0x80,
_ => unreachable!(),
}
}
fn rank_color(c: Color, theme: &crate::theme::ThemeColors) -> u8 {
if c == theme.meter.high { 2 }
else if c == theme.meter.mid { 1 }
else { 0 }
}

View File

@@ -1,5 +1,3 @@
//! Scrollable single-select list widget with cursor highlight.
use crate::theme;
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
@@ -7,7 +5,6 @@ use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use ratatui::Frame;
/// Scrollable list with a highlighted cursor and selected-item marker.
pub struct ListSelect<'a> {
items: &'a [String],
selected: usize,

View File

@@ -1,12 +1,9 @@
//! Centered modal frame with border and title.
use crate::theme;
use ratatui::layout::Rect;
use ratatui::style::{Color, Style};
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
use ratatui::Frame;
/// Centered modal overlay with titled border.
pub struct ModalFrame<'a> {
title: &'a str,
width: u16,

View File

@@ -1,5 +1,3 @@
//! Page navigation minimap showing a 3x2 grid of tiles.
use crate::theme;
use ratatui::layout::{Alignment, Rect};
use ratatui::style::Style;

View File

@@ -1,5 +1,3 @@
//! Vertical label/value property form renderer.
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::widgets::Paragraph;
@@ -7,7 +5,6 @@ use ratatui::Frame;
use crate::theme;
/// Render a vertical list of label/value pairs with selection highlight.
pub fn render_props_form(frame: &mut Frame, area: Rect, fields: &[(&str, &str, bool)]) {
let theme = theme::get();

View File

@@ -1,13 +1,10 @@
//! Tree-view sample browser with search filtering.
use crate::theme;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
use ratatui::widgets::{Block, Borders, Paragraph};
use ratatui::Frame;
/// Node type in the sample tree.
#[derive(Clone, Copy)]
pub enum TreeLineKind {
Root { expanded: bool },
@@ -15,7 +12,6 @@ pub enum TreeLineKind {
File,
}
/// A single row in the sample browser tree.
#[derive(Clone)]
pub struct TreeLine {
pub depth: u8,
@@ -25,7 +21,6 @@ pub struct TreeLine {
pub index: usize,
}
/// Tree-view browser for navigating sample folders.
pub struct SampleBrowser<'a> {
entries: &'a [TreeLine],
cursor: usize,
@@ -116,13 +111,13 @@ impl<'a> SampleBrowser<'a> {
fn render_tree(&self, frame: &mut Frame, area: Rect, colors: &theme::ThemeColors) {
let height = area.height as usize;
if self.entries.is_empty() {
if self.search_query.is_empty() {
self.render_empty_guide(frame, area, colors);
let msg = if self.search_query.is_empty() {
"No samples loaded"
} else {
let line =
Line::from(Span::styled("No matches", Style::new().fg(colors.browser.empty_text)));
frame.render_widget(Paragraph::new(vec![line]), area);
}
"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;
}
@@ -179,47 +174,4 @@ impl<'a> SampleBrowser<'a> {
frame.render_widget(Paragraph::new(lines), area);
}
fn render_empty_guide(&self, frame: &mut Frame, area: Rect, colors: &theme::ThemeColors) {
let muted = Style::new().fg(colors.browser.empty_text);
let heading = Style::new().fg(colors.ui.text_primary);
let key = Style::new().fg(colors.hint.key);
let desc = Style::new().fg(colors.hint.text);
let code = Style::new().fg(colors.ui.accent);
let lines = vec![
Line::from(Span::styled(" No samples loaded.", muted)),
Line::from(""),
Line::from(Span::styled(" Load from the Engine page:", heading)),
Line::from(""),
Line::from(vec![
Span::styled(" F6 ", key),
Span::styled("Go to Engine page", desc),
]),
Line::from(vec![
Span::styled(" A ", key),
Span::styled("Add a sample folder", desc),
]),
Line::from(""),
Line::from(Span::styled(" Organize samples like this:", heading)),
Line::from(""),
Line::from(Span::styled(" samples/", code)),
Line::from(Span::styled(" \u{251C}\u{2500}\u{2500} kick/", code)),
Line::from(Span::styled(" \u{2502} \u{2514}\u{2500}\u{2500} kick.wav", code)),
Line::from(Span::styled(" \u{251C}\u{2500}\u{2500} snare/", code)),
Line::from(Span::styled(" \u{2502} \u{2514}\u{2500}\u{2500} snare.wav", code)),
Line::from(Span::styled(" \u{2514}\u{2500}\u{2500} hats/", code)),
Line::from(Span::styled(" \u{251C}\u{2500}\u{2500} closed.wav", code)),
Line::from(Span::styled(" \u{251C}\u{2500}\u{2500} open.wav", code)),
Line::from(Span::styled(" \u{2514}\u{2500}\u{2500} pedal.wav", code)),
Line::from(""),
Line::from(Span::styled(" Folders become Forth words:", heading)),
Line::from(""),
Line::from(Span::styled(" kick sound .", code)),
Line::from(Span::styled(" hats sound 2 n .", code)),
Line::from(Span::styled(" snare sound 0.5 speed .", code)),
];
frame.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), area);
}
}

View File

@@ -1,5 +1,3 @@
//! Oscilloscope waveform widget using braille characters.
use crate::theme;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
@@ -11,14 +9,12 @@ thread_local! {
static PATTERNS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
}
/// Rendering direction for the oscilloscope.
#[derive(Clone, Copy)]
pub enum Orientation {
Horizontal,
Vertical,
}
/// Single-channel oscilloscope using braille dot plotting.
pub struct Scope<'a> {
data: &'a [f32],
orientation: Orientation,
@@ -45,11 +41,6 @@ impl<'a> Scope<'a> {
self.color = Some(c);
self
}
pub fn gain(mut self, g: f32) -> Self {
self.gain = g;
self
}
}
impl Widget for Scope<'_> {
@@ -75,6 +66,9 @@ fn render_horizontal(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, g
let fine_width = width * 2;
let fine_height = height * 4;
let peak = data.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
let auto_gain = if peak > 0.001 { gain / peak } else { gain };
PATTERNS.with(|p| {
let mut patterns = p.borrow_mut();
let size = width * height;
@@ -83,7 +77,7 @@ fn render_horizontal(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, g
for fine_x in 0..fine_width {
let sample_idx = (fine_x * data.len()) / fine_width;
let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * gain).clamp(-1.0, 1.0);
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);
@@ -128,6 +122,9 @@ fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gai
let fine_width = width * 2;
let fine_height = height * 4;
let peak = data.iter().map(|s| s.abs()).fold(0.0f32, f32::max);
let auto_gain = if peak > 0.001 { gain / peak } else { gain };
PATTERNS.with(|p| {
let mut patterns = p.borrow_mut();
let size = width * height;
@@ -136,7 +133,7 @@ fn render_vertical(data: &[f32], area: Rect, buf: &mut Buffer, color: Color, gai
for fine_y in 0..fine_height {
let sample_idx = (fine_y * data.len()) / fine_height;
let sample = (data.get(sample_idx).copied().unwrap_or(0.0) * gain).clamp(-1.0, 1.0);
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);

View File

@@ -1,17 +1,13 @@
//! Up/down arrow scroll indicators for bounded lists.
use ratatui::layout::Rect;
use ratatui::style::{Color, Style};
use ratatui::widgets::Paragraph;
use ratatui::Frame;
/// Horizontal alignment for scroll indicators.
pub enum IndicatorAlign {
Center,
Right,
}
/// Render up/down scroll arrows when content overflows.
pub fn render_scroll_indicators(
frame: &mut Frame,
area: Rect,

View File

@@ -1,5 +1,3 @@
//! Inline search bar with active/inactive styling.
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::text::{Line, Span};
@@ -8,7 +6,6 @@ use ratatui::Frame;
use crate::theme;
/// Render a `/query` search bar.
pub fn render_search_bar(frame: &mut Frame, area: Rect, query: &str, active: bool) {
let theme = theme::get();
let style = if active {

View File

@@ -1,5 +1,3 @@
//! Section header with horizontal divider for engine-view panels.
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::widgets::Paragraph;
@@ -7,7 +5,6 @@ use ratatui::Frame;
use crate::theme;
/// Render a section title with a horizontal divider below it.
pub fn render_section_header(frame: &mut Frame, title: &str, focused: bool, area: Rect) {
let theme = theme::get();
let [header_area, divider_area] =

View File

@@ -1,5 +1,3 @@
//! Decorative particle effect using random Unicode glyphs.
use crate::theme;
use rand::Rng;
use ratatui::buffer::Buffer;
@@ -16,7 +14,6 @@ struct Sparkle {
life: u8,
}
/// Animated sparkle particles for visual flair.
#[derive(Default)]
pub struct Sparkles {
sparkles: Vec<Sparkle>,

View File

@@ -1,58 +1,18 @@
//! 32-band frequency spectrum display with optional peak hold.
use crate::theme;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Color;
use ratatui::widgets::Widget;
use std::cell::RefCell;
const BLOCKS: [char; 8] = ['\u{2581}', '\u{2582}', '\u{2583}', '\u{2584}', '\u{2585}', '\u{2586}', '\u{2587}', '\u{2588}'];
#[derive(Clone, Copy, PartialEq, Eq, Default)]
pub enum SpectrumStyle {
#[default]
Bars,
Line,
Filled,
}
thread_local! {
static PEAKS: RefCell<[f32; 32]> = const { RefCell::new([0.0; 32]) };
static PATTERNS: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
}
/// 32-band spectrum analyzer using block characters.
pub struct Spectrum<'a> {
data: &'a [f32; 32],
gain: f32,
style: SpectrumStyle,
peaks: bool,
}
impl<'a> Spectrum<'a> {
pub fn new(data: &'a [f32; 32]) -> Self {
Self {
data,
gain: 1.0,
style: SpectrumStyle::Bars,
peaks: false,
}
}
pub fn gain(mut self, g: f32) -> Self {
self.gain = g;
self
}
pub fn style(mut self, s: SpectrumStyle) -> Self {
self.style = s;
self
}
pub fn peaks(mut self, enabled: bool) -> Self {
self.peaks = enabled;
self
Self { data }
}
}
@@ -62,177 +22,45 @@ impl Widget for Spectrum<'_> {
return;
}
// Update peak hold state
let peak_values = if self.peaks {
Some(PEAKS.with(|p| {
let mut peaks = p.borrow_mut();
for (i, &mag) in self.data.iter().enumerate() {
let v = (mag * self.gain).min(1.0);
if v >= peaks[i] {
peaks[i] = v;
} else {
peaks[i] = (peaks[i] - 0.02).max(v);
}
}
*peaks
}))
} else {
None
};
match self.style {
SpectrumStyle::Bars => render_bars(self.data, area, buf, self.gain, peak_values.as_ref()),
SpectrumStyle::Line => render_braille(self.data, area, buf, self.gain, false, peak_values.as_ref()),
SpectrumStyle::Filled => render_braille(self.data, area, buf, self.gain, true, peak_values.as_ref()),
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;
}
}
}
fn band_color(ratio: f32, colors: &theme::ThemeColors) -> Color {
if ratio < 0.33 {
Color::Rgb(colors.meter.low_rgb.0, colors.meter.low_rgb.1, colors.meter.low_rgb.2)
} else if ratio < 0.66 {
Color::Rgb(colors.meter.mid_rgb.0, colors.meter.mid_rgb.1, colors.meter.mid_rgb.2)
} else {
Color::Rgb(colors.meter.high_rgb.0, colors.meter.high_rgb.1, colors.meter.high_rgb.2)
}
}
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;
fn render_bars(data: &[f32; 32], area: Rect, buf: &mut Buffer, gain: f32, peaks: Option<&[f32; 32]>) {
let colors = theme::get();
let height = area.height as f32;
let base = area.width as usize / 32;
let remainder = area.width as usize % 32;
if base == 0 && remainder == 0 {
return;
}
let mut x_start = area.x;
for (band, &mag) in data.iter().enumerate() {
let w = base + if band < remainder { 1 } else { 0 };
if w == 0 {
continue;
}
let bar_height = (mag * gain).min(1.0) * height;
let full_cells = bar_height as usize;
let frac = bar_height - full_cells as f32;
let frac_idx = (frac * 8.0) as usize;
// Peak hold row
let peak_row = peaks.map(|p| {
let ph = p[band] * height;
let row = (height - ph).max(0.0) as usize;
row.min(area.height as usize - 1)
});
for row in 0..area.height as usize {
let y = area.y + area.height - 1 - row as u16;
let ratio = row as f32 / area.height as f32;
let color = band_color(ratio, &colors);
for dx in 0..w as u16 {
let x = x_start + dx;
if row < full_cells {
buf[(x, y)].set_char(BLOCKS[7]).set_fg(color);
} else if row == full_cells && frac_idx > 0 {
buf[(x, y)].set_char(BLOCKS[frac_idx - 1]).set_fg(color);
} else if let Some(pr) = peak_row {
// peak_row is from top (0 = top), row is from bottom
let from_top = area.height as usize - 1 - row;
if from_top == pr {
buf[(x, y)].set_char('─').set_fg(colors.meter.high);
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;
}
x_start += w as u16;
}
}
fn render_braille(
data: &[f32; 32],
area: Rect,
buf: &mut Buffer,
gain: f32,
filled: bool,
peaks: Option<&[f32; 32]>,
) {
let colors = theme::get();
let width = area.width as usize;
let height = area.height as usize;
let fine_w = width * 2;
let fine_h = height * 4;
PATTERNS.with(|p| {
let mut patterns = p.borrow_mut();
patterns.clear();
patterns.resize(width * height, 0);
// Interpolate 32 bands across fine_w columns
for fx in 0..fine_w {
let band_f = fx as f32 * 31.0 / (fine_w - 1).max(1) as f32;
let lo = band_f as usize;
let hi = (lo + 1).min(31);
let t = band_f - lo as f32;
let mag = ((data[lo] * (1.0 - t) + data[hi] * t) * gain).min(1.0);
let fy = ((1.0 - mag) * (fine_h - 1) as f32).round() as usize;
let fy = fy.min(fine_h - 1);
if filled {
for y in fy..fine_h {
let cy = y / 4;
let dy = y % 4;
let cx = fx / 2;
let dx = fx % 2;
patterns[cy * width + cx] |= braille_bit(dx, dy);
}
} else {
let cy = fy / 4;
let dy = fy % 4;
let cx = fx / 2;
let dx = fx % 2;
patterns[cy * width + cx] |= braille_bit(dx, dy);
}
// Peak dots
if let Some(pk) = peaks {
let pv = (pk[lo] * (1.0 - t) + pk[hi] * t).min(1.0);
let py = ((1.0 - pv) * (fine_h - 1) as f32).round() as usize;
let py = py.min(fine_h - 1);
let cy = py / 4;
let dy = py % 4;
let cx = fx / 2;
let dx = fx % 2;
patterns[cy * width + cx] |= braille_bit(dx, dy);
}
}
for cy in 0..height {
for cx in 0..width {
let pattern = patterns[cy * width + cx];
if pattern != 0 {
let ratio = 1.0 - (cy as f32 / height as f32);
let color = band_color(ratio, &colors);
let ch = char::from_u32(0x2800 + pattern as u32).unwrap_or(' ');
buf[(area.x + cx as u16, area.y + cy as u16)]
.set_char(ch)
.set_fg(color);
}
}
}
});
}
fn braille_bit(dot_x: usize, dot_y: usize) -> u8 {
match (dot_x, dot_y) {
(0, 0) => 0x01,
(0, 1) => 0x02,
(0, 2) => 0x04,
(0, 3) => 0x40,
(1, 0) => 0x08,
(1, 1) => 0x10,
(1, 2) => 0x20,
(1, 3) => 0x80,
_ => unreachable!(),
}
}

View File

@@ -1,5 +1,3 @@
//! Single-line text input modal with optional hint.
use crate::theme;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::style::{Color, Style};
@@ -9,7 +7,6 @@ use ratatui::Frame;
use super::ModalFrame;
/// Modal dialog with a single-line text input.
pub struct TextInputModal<'a> {
title: &'a str,
input: &'a str,

View File

@@ -1,9 +1,6 @@
//! Derive [`ThemeColors`] from a [`Palette`].
use super::*;
use super::palette::{Palette, Rgb, darken, mid, rgb, tint};
/// Build a complete [`ThemeColors`] from a [`Palette`].
pub fn build(p: &Palette) -> ThemeColors {
let darker_bg = darken(p.bg, 0.15);

View File

@@ -1,5 +1,3 @@
//! Catppuccin Latte palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,5 +1,3 @@
//! Catppuccin Mocha palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,5 +1,3 @@
//! Dracula palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,5 +1,3 @@
//! Eden palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,5 +1,3 @@
//! Ember palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,41 +0,0 @@
//! Everforest palette.
use super::palette::Palette;
pub fn palette() -> Palette {
Palette {
bg: (45, 53, 59),
surface: (52, 62, 68),
surface2: (68, 80, 86),
fg: (211, 198, 170),
fg_dim: (135, 131, 116),
fg_muted: (80, 80, 68),
accent: (167, 192, 128),
red: (230, 126, 128),
green: (167, 192, 128),
yellow: (219, 188, 127),
blue: (127, 187, 179),
purple: (214, 153, 182),
cyan: (131, 192, 146),
orange: (230, 152, 117),
tempo_color: (214, 153, 182),
bank_color: (127, 187, 179),
pattern_color: (131, 192, 146),
title_accent: (167, 192, 128),
title_author: (127, 187, 179),
secondary: (230, 152, 117),
link_bright: [
(167, 192, 128), (214, 153, 182), (230, 152, 117),
(127, 187, 179), (219, 188, 127),
],
link_dim: [
(56, 66, 46), (70, 52, 62), (72, 52, 42),
(44, 64, 60), (70, 62, 44),
],
sparkle: [
(167, 192, 128), (230, 152, 117), (131, 192, 146),
(214, 153, 182), (219, 188, 127),
],
meter: [(148, 172, 110), (200, 170, 108), (210, 108, 110)],
}
}

View File

@@ -1,5 +1,3 @@
//! Fairyfloss palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,41 +0,0 @@
//! Fauve palette.
use super::palette::Palette;
pub fn palette() -> Palette {
Palette {
bg: (28, 22, 18),
surface: (42, 33, 26),
surface2: (58, 46, 36),
fg: (240, 228, 210),
fg_dim: (170, 150, 130),
fg_muted: (100, 82, 66),
accent: (230, 60, 20),
red: (220, 38, 32),
green: (30, 170, 80),
yellow: (255, 210, 0),
blue: (20, 80, 200),
purple: (170, 40, 150),
cyan: (0, 150, 180),
orange: (240, 120, 0),
tempo_color: (230, 60, 20),
bank_color: (20, 80, 200),
pattern_color: (0, 150, 180),
title_accent: (230, 60, 20),
title_author: (20, 80, 200),
secondary: (170, 40, 150),
link_bright: [
(230, 60, 20), (20, 80, 200), (240, 120, 0),
(0, 150, 180), (30, 170, 80),
],
link_dim: [
(72, 24, 10), (10, 28, 65), (76, 40, 6),
(6, 48, 58), (14, 54, 28),
],
sparkle: [
(230, 60, 20), (255, 210, 0), (30, 170, 80),
(20, 80, 200), (170, 40, 150),
],
meter: [(26, 152, 72), (235, 190, 0), (200, 34, 28)],
}
}

View File

@@ -1,7 +1,6 @@
//! Georges palette (C64 colors on black).
use super::palette::Palette;
// C64 palette on pure black
pub fn palette() -> Palette {
Palette {
bg: (0, 0, 0),

View File

@@ -1,5 +1,3 @@
//! Gruvbox Dark palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,5 +1,3 @@
//! Hot Dog Stand palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,41 +0,0 @@
//! Iceberg palette.
use super::palette::Palette;
pub fn palette() -> Palette {
Palette {
bg: (22, 24, 33),
surface: (30, 33, 46),
surface2: (45, 48, 64),
fg: (198, 200, 209),
fg_dim: (109, 112, 126),
fg_muted: (64, 66, 78),
accent: (132, 160, 198),
red: (226, 120, 120),
green: (180, 190, 130),
yellow: (226, 164, 120),
blue: (132, 160, 198),
purple: (160, 147, 199),
cyan: (137, 184, 194),
orange: (226, 164, 120),
tempo_color: (160, 147, 199),
bank_color: (132, 160, 198),
pattern_color: (137, 184, 194),
title_accent: (132, 160, 198),
title_author: (160, 147, 199),
secondary: (226, 164, 120),
link_bright: [
(132, 160, 198), (160, 147, 199), (226, 164, 120),
(137, 184, 194), (180, 190, 130),
],
link_dim: [
(45, 55, 70), (55, 50, 68), (70, 55, 42),
(46, 62, 66), (58, 62, 44),
],
sparkle: [
(132, 160, 198), (226, 164, 120), (180, 190, 130),
(160, 147, 199), (226, 120, 120),
],
meter: [(160, 175, 115), (210, 150, 105), (200, 105, 105)],
}
}

View File

@@ -1,41 +0,0 @@
//! Jaipur palette.
use super::palette::Palette;
pub fn palette() -> Palette {
Palette {
bg: (30, 24, 22),
surface: (44, 36, 32),
surface2: (60, 48, 42),
fg: (238, 222, 200),
fg_dim: (165, 145, 125),
fg_muted: (95, 78, 65),
accent: (210, 90, 100),
red: (200, 44, 52),
green: (30, 160, 120),
yellow: (240, 180, 20),
blue: (60, 60, 180),
purple: (150, 50, 120),
cyan: (0, 155, 155),
orange: (220, 120, 50),
tempo_color: (210, 90, 100),
bank_color: (60, 60, 180),
pattern_color: (0, 155, 155),
title_accent: (210, 90, 100),
title_author: (60, 60, 180),
secondary: (220, 120, 50),
link_bright: [
(210, 90, 100), (60, 60, 180), (220, 120, 50),
(0, 155, 155), (30, 160, 120),
],
link_dim: [
(66, 30, 34), (22, 22, 58), (70, 40, 18),
(6, 48, 48), (12, 50, 38),
],
sparkle: [
(210, 90, 100), (240, 180, 20), (30, 160, 120),
(60, 60, 180), (150, 50, 120),
],
meter: [(26, 144, 106), (222, 164, 18), (184, 40, 46)],
}
}

View File

@@ -1,5 +1,3 @@
//! Kanagawa palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,5 +1,3 @@
//! Letz Light palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -8,22 +8,17 @@ mod catppuccin_mocha;
mod dracula;
mod eden;
mod ember;
mod everforest;
mod georges;
mod fairyfloss;
mod gruvbox_dark;
mod hot_dog_stand;
mod iceberg;
mod jaipur;
mod kanagawa;
mod letz_light;
mod monochrome_black;
mod monochrome_white;
mod monokai;
mod nord;
mod fauve;
mod pitch_black;
mod tropicalia;
mod rose_pine;
mod tokyo_night;
pub mod transform;
@@ -31,14 +26,12 @@ pub mod transform;
use ratatui::style::Color;
use std::cell::RefCell;
/// Entry in the theme registry: id, display label, and palette constructor.
pub struct ThemeEntry {
pub id: &'static str,
pub label: &'static str,
pub palette: fn() -> palette::Palette,
}
/// All available themes.
pub const THEMES: &[ThemeEntry] = &[
ThemeEntry { id: "CatppuccinMocha", label: "Catppuccin Mocha", palette: catppuccin_mocha::palette },
ThemeEntry { id: "CatppuccinLatte", label: "Catppuccin Latte", palette: catppuccin_latte::palette },
@@ -58,28 +51,20 @@ pub const THEMES: &[ThemeEntry] = &[
ThemeEntry { id: "Ember", label: "Ember", palette: ember::palette },
ThemeEntry { id: "Eden", label: "Eden", palette: eden::palette },
ThemeEntry { id: "Georges", label: "Georges", palette: georges::palette },
ThemeEntry { id: "Iceberg", label: "Iceberg", palette: iceberg::palette },
ThemeEntry { id: "Everforest", label: "Everforest", palette: everforest::palette },
ThemeEntry { id: "Fauve", label: "Fauve", palette: fauve::palette },
ThemeEntry { id: "Tropicalia", label: "Tropicalia", palette: tropicalia::palette },
ThemeEntry { id: "Jaipur", label: "Jaipur", palette: jaipur::palette },
];
thread_local! {
static CURRENT_THEME: RefCell<ThemeColors> = RefCell::new(build::build(&(THEMES[0].palette)()));
}
/// Return the current thread-local theme.
pub fn get() -> ThemeColors {
CURRENT_THEME.with(|t| t.borrow().clone())
}
/// Set the current thread-local theme.
pub fn set(theme: ThemeColors) {
CURRENT_THEME.with(|t| *t.borrow_mut() = theme);
}
/// Complete set of resolved colors for all UI components.
#[derive(Clone)]
pub struct ThemeColors {
pub ui: UiColors,
@@ -110,7 +95,6 @@ pub struct ThemeColors {
pub confirm: ConfirmColors,
}
/// Core UI colors: background, text, borders.
#[derive(Clone)]
pub struct UiColors {
pub bg: Color,
@@ -125,7 +109,6 @@ pub struct UiColors {
pub surface: Color,
}
/// Playback status bar colors.
#[derive(Clone)]
pub struct StatusColors {
pub playing_bg: Color,
@@ -137,7 +120,6 @@ pub struct StatusColors {
pub fill_bg: Color,
}
/// Step grid selection and cursor colors.
#[derive(Clone)]
pub struct SelectionColors {
pub cursor_bg: Color,
@@ -151,7 +133,6 @@ pub struct SelectionColors {
pub in_range: Color,
}
/// Step tile colors for various states.
#[derive(Clone)]
pub struct TileColors {
pub playing_active_bg: Color,
@@ -169,7 +150,6 @@ pub struct TileColors {
pub link_dim: [(u8, u8, u8); 5],
}
/// Top header bar segment colors.
#[derive(Clone)]
pub struct HeaderColors {
pub tempo_bg: Color,
@@ -182,7 +162,6 @@ pub struct HeaderColors {
pub stats_fg: Color,
}
/// Modal dialog border colors.
#[derive(Clone)]
pub struct ModalColors {
pub border: Color,
@@ -196,7 +175,6 @@ pub struct ModalColors {
pub preview: Color,
}
/// Flash notification colors.
#[derive(Clone)]
pub struct FlashColors {
pub error_bg: Color,
@@ -207,7 +185,6 @@ pub struct FlashColors {
pub info_fg: Color,
}
/// Pattern list row state colors.
#[derive(Clone)]
pub struct ListColors {
pub playing_bg: Color,
@@ -226,7 +203,6 @@ pub struct ListColors {
pub soloed_fg: Color,
}
/// Ableton Link status indicator colors.
#[derive(Clone)]
pub struct LinkStatusColors {
pub disabled: Color,
@@ -234,7 +210,6 @@ pub struct LinkStatusColors {
pub listening: Color,
}
/// Syntax highlighting (fg, bg) pairs per token category.
#[derive(Clone)]
pub struct SyntaxColors {
pub gap_bg: Color,
@@ -259,35 +234,30 @@ pub struct SyntaxColors {
pub default: (Color, Color),
}
/// Alternating table row colors.
#[derive(Clone)]
pub struct TableColors {
pub row_even: Color,
pub row_odd: Color,
}
/// Value display colors.
#[derive(Clone)]
pub struct ValuesColors {
pub tempo: Color,
pub value: Color,
}
/// Keyboard hint key/text colors.
#[derive(Clone)]
pub struct HintColors {
pub key: Color,
pub text: Color,
}
/// View badge pill colors.
#[derive(Clone)]
pub struct ViewBadgeColors {
pub bg: Color,
pub fg: Color,
}
/// Navigation minimap tile colors.
#[derive(Clone)]
pub struct NavColors {
pub selected_bg: Color,
@@ -296,7 +266,6 @@ pub struct NavColors {
pub unselected_fg: Color,
}
/// Script editor colors.
#[derive(Clone)]
pub struct EditorWidgetColors {
pub cursor_bg: Color,
@@ -308,7 +277,6 @@ pub struct EditorWidgetColors {
pub completion_example: Color,
}
/// File and sample browser colors.
#[derive(Clone)]
pub struct BrowserColors {
pub directory: Color,
@@ -323,7 +291,6 @@ pub struct BrowserColors {
pub empty_text: Color,
}
/// Text input field colors.
#[derive(Clone)]
pub struct InputColors {
pub text: Color,
@@ -331,7 +298,6 @@ pub struct InputColors {
pub hint: Color,
}
/// Search bar and match highlight colors.
#[derive(Clone)]
pub struct SearchColors {
pub active: Color,
@@ -340,7 +306,6 @@ pub struct SearchColors {
pub match_fg: Color,
}
/// Markdown renderer colors.
#[derive(Clone)]
pub struct MarkdownColors {
pub h1: Color,
@@ -355,7 +320,6 @@ pub struct MarkdownColors {
pub list: Color,
}
/// Engine view panel colors.
#[derive(Clone)]
pub struct EngineColors {
pub header: Color,
@@ -378,7 +342,6 @@ pub struct EngineColors {
pub hint_inactive: Color,
}
/// Dictionary view colors.
#[derive(Clone)]
pub struct DictColors {
pub word_name: Color,
@@ -396,7 +359,6 @@ pub struct DictColors {
pub header_desc: Color,
}
/// Title screen colors.
#[derive(Clone)]
pub struct TitleColors {
pub big_title: Color,
@@ -407,7 +369,6 @@ pub struct TitleColors {
pub subtitle: Color,
}
/// VU meter and spectrum level colors.
#[derive(Clone)]
pub struct MeterColors {
pub low: Color,
@@ -418,13 +379,11 @@ pub struct MeterColors {
pub high_rgb: (u8, u8, u8),
}
/// Sparkle particle colors.
#[derive(Clone)]
pub struct SparkleColors {
pub colors: [(u8, u8, u8); 5],
}
/// Confirm dialog colors.
#[derive(Clone)]
pub struct ConfirmColors {
pub border: Color,

View File

@@ -1,5 +1,3 @@
//! Monochrome (black background) palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,5 +1,3 @@
//! Monochrome (white background) palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,5 +1,3 @@
//! Monokai palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,5 +1,3 @@
//! Nord palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,11 +1,7 @@
//! Palette definition and color mixing utilities.
use ratatui::style::Color;
/// RGB color triple.
pub type Rgb = (u8, u8, u8);
/// Base color palette that themes are derived from.
pub struct Palette {
// Core
pub bg: Rgb,
@@ -37,12 +33,10 @@ pub struct Palette {
pub meter: [Rgb; 3],
}
/// Convert an RGB triple to a ratatui [`Color`].
pub fn rgb(c: Rgb) -> Color {
Color::Rgb(c.0, c.1, c.2)
}
/// Blend `bg` toward `accent` by `amount` (0.01.0).
pub fn tint(bg: Rgb, accent: Rgb, amount: f32) -> Rgb {
let mix = |b: u8, a: u8| -> u8 {
let v = b as f32 + (a as f32 - b as f32) * amount;
@@ -51,12 +45,10 @@ pub fn tint(bg: Rgb, accent: Rgb, amount: f32) -> Rgb {
(mix(bg.0, accent.0), mix(bg.1, accent.1), mix(bg.2, accent.2))
}
/// Linearly interpolate between two colors.
pub fn mid(a: Rgb, b: Rgb, t: f32) -> Rgb {
tint(a, b, t)
}
/// Darken a color by reducing brightness.
pub fn darken(c: Rgb, amount: f32) -> Rgb {
let d = |v: u8| -> u8 { (v as f32 * (1.0 - amount)).clamp(0.0, 255.0) as u8 };
(d(c.0), d(c.1), d(c.2))

View File

@@ -1,5 +1,3 @@
//! Pitch Black palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,5 +1,3 @@
//! Rose Pine palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,5 +1,3 @@
//! Tokyo Night palette.
use super::palette::Palette;
pub fn palette() -> Palette {

View File

@@ -1,5 +1,3 @@
//! Hue rotation for palette-wide color transforms.
use super::palette::{Palette, Rgb};
use super::build::build;
use super::ThemeColors;
@@ -64,7 +62,6 @@ fn rotate3(arr: [Rgb; 3], d: f32) -> [Rgb; 3] {
[rotate(arr[0], d), rotate(arr[1], d), rotate(arr[2], d)]
}
/// Build a [`ThemeColors`] with all palette hues rotated by `degrees`.
pub fn rotate_palette(palette: &Palette, degrees: f32) -> ThemeColors {
if degrees == 0.0 {
return build(palette);

Some files were not shown because too many files have changed in this diff Show More