Feat: prelude and new words
This commit is contained in:
@@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
### Added
|
||||
- TachyonFX based animations
|
||||
- Prelude: project-level Forth script for persistent word definitions. Press `d` to edit, `D` to re-evaluate. Runs automatically on playback start and project load.
|
||||
- Varargs stack words: `rev`, `shuffle`, `sort` (ascending), `rsort` (descending), `sum`, `prod`. All take a count and operate on the top n items.
|
||||
|
||||
|
||||
### Changed
|
||||
@@ -23,6 +25,7 @@ All notable changes to this project will be documented in this file.
|
||||
- Decoupled script runtime state between UI and sequencer threads, eliminating shared mutexes on the RT path.
|
||||
|
||||
### Fixed
|
||||
- Prelude content no longer leaks into step editor. Closing the prelude editor now restores the current step's content to the buffer.
|
||||
- Desktop binary now loads color theme and connects MIDI devices on startup (was missing).
|
||||
- Audio commands no longer silently dropped when channel is full; switched to unbounded channel matching MIDI dispatch pattern.
|
||||
- PatternProps and EuclideanDistribution modals now use the global theme background instead of the terminal default.
|
||||
|
||||
@@ -19,6 +19,12 @@ pub enum Op {
|
||||
Drop2,
|
||||
Swap2,
|
||||
Over2,
|
||||
Rev,
|
||||
Shuffle,
|
||||
Sort,
|
||||
RSort,
|
||||
Sum,
|
||||
Prod,
|
||||
Forget,
|
||||
Add,
|
||||
Sub,
|
||||
@@ -91,6 +97,7 @@ pub enum Op {
|
||||
SetSpeed,
|
||||
At,
|
||||
IntRange,
|
||||
StepRange,
|
||||
Generate,
|
||||
GeomRange,
|
||||
Times,
|
||||
|
||||
@@ -345,6 +345,77 @@ impl Forth {
|
||||
stack.push(a);
|
||||
stack.push(b);
|
||||
}
|
||||
Op::Rev => {
|
||||
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||
if count > stack.len() {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
let start = stack.len() - count;
|
||||
stack[start..].reverse();
|
||||
}
|
||||
Op::Shuffle => {
|
||||
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||
if count > stack.len() {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
let start = stack.len() - count;
|
||||
let slice = &mut stack[start..];
|
||||
let mut rng = self.rng.lock();
|
||||
for i in (1..slice.len()).rev() {
|
||||
let j = rng.gen_range(0..=i);
|
||||
slice.swap(i, j);
|
||||
}
|
||||
}
|
||||
Op::Sort => {
|
||||
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||
if count > stack.len() {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
let start = stack.len() - count;
|
||||
stack[start..].sort_by(|a, b| {
|
||||
a.as_float()
|
||||
.unwrap_or(0.0)
|
||||
.partial_cmp(&b.as_float().unwrap_or(0.0))
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
}
|
||||
Op::RSort => {
|
||||
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||
if count > stack.len() {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
let start = stack.len() - count;
|
||||
stack[start..].sort_by(|a, b| {
|
||||
b.as_float()
|
||||
.unwrap_or(0.0)
|
||||
.partial_cmp(&a.as_float().unwrap_or(0.0))
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
}
|
||||
Op::Sum => {
|
||||
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||
if count > stack.len() {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
let start = stack.len() - count;
|
||||
let total: f64 = stack
|
||||
.drain(start..)
|
||||
.map(|v| v.as_float().unwrap_or(0.0))
|
||||
.sum();
|
||||
stack.push(float_to_value(total));
|
||||
}
|
||||
Op::Prod => {
|
||||
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||
if count > stack.len() {
|
||||
return Err("stack underflow".into());
|
||||
}
|
||||
let start = stack.len() - count;
|
||||
let product: f64 = stack
|
||||
.drain(start..)
|
||||
.map(|v| v.as_float().unwrap_or(1.0))
|
||||
.product();
|
||||
stack.push(float_to_value(product));
|
||||
}
|
||||
|
||||
Op::Add => binary_op(stack, |a, b| a + b)?,
|
||||
Op::Sub => binary_op(stack, |a, b| a - b)?,
|
||||
@@ -889,6 +960,25 @@ impl Forth {
|
||||
}
|
||||
}
|
||||
|
||||
Op::StepRange => {
|
||||
let step = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let end = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
let start = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||
if step == 0.0 {
|
||||
return Err("step cannot be zero".into());
|
||||
}
|
||||
let ascending = step > 0.0;
|
||||
let mut val = start;
|
||||
loop {
|
||||
let done = if ascending { val > end } else { val < end };
|
||||
if done {
|
||||
break;
|
||||
}
|
||||
stack.push(float_to_value(val));
|
||||
val += step;
|
||||
}
|
||||
}
|
||||
|
||||
Op::Generate => {
|
||||
let count = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||
let quot = stack.pop().ok_or("stack underflow")?;
|
||||
|
||||
@@ -20,6 +20,12 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
||||
"2drop" => Op::Drop2,
|
||||
"2swap" => Op::Swap2,
|
||||
"2over" => Op::Over2,
|
||||
"rev" => Op::Rev,
|
||||
"shuffle" => Op::Shuffle,
|
||||
"sort" => Op::Sort,
|
||||
"rsort" => Op::RSort,
|
||||
"sum" => Op::Sum,
|
||||
"prod" => Op::Prod,
|
||||
"+" => Op::Add,
|
||||
"-" => Op::Sub,
|
||||
"*" => Op::Mul,
|
||||
@@ -83,6 +89,7 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
||||
"oct" => Op::Oct,
|
||||
"clear" => Op::ClearCmd,
|
||||
".." => Op::IntRange,
|
||||
".," => Op::StepRange,
|
||||
"gen" => Op::Generate,
|
||||
"geom.." => Op::GeomRange,
|
||||
"times" => Op::Times,
|
||||
|
||||
@@ -123,6 +123,66 @@ pub(super) const WORDS: &[Word] = &[
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "rev",
|
||||
aliases: &[],
|
||||
category: "Stack",
|
||||
stack: "(..n n -- ..n)",
|
||||
desc: "Reverse top n items",
|
||||
example: "1 2 3 3 rev => 3 2 1",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "shuffle",
|
||||
aliases: &[],
|
||||
category: "Stack",
|
||||
stack: "(..n n -- ..n)",
|
||||
desc: "Randomly shuffle top n items",
|
||||
example: "1 2 3 3 shuffle",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "sort",
|
||||
aliases: &[],
|
||||
category: "Stack",
|
||||
stack: "(..n n -- ..n)",
|
||||
desc: "Sort top n items ascending",
|
||||
example: "3 1 2 3 sort => 1 2 3",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "rsort",
|
||||
aliases: &[],
|
||||
category: "Stack",
|
||||
stack: "(..n n -- ..n)",
|
||||
desc: "Sort top n items descending",
|
||||
example: "1 2 3 3 rsort => 3 2 1",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "sum",
|
||||
aliases: &[],
|
||||
category: "Stack",
|
||||
stack: "(..n n -- total)",
|
||||
desc: "Sum top n items",
|
||||
example: "1 2 3 3 sum => 6",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
Word {
|
||||
name: "prod",
|
||||
aliases: &[],
|
||||
category: "Stack",
|
||||
stack: "(..n n -- product)",
|
||||
desc: "Multiply top n items",
|
||||
example: "2 3 4 3 prod => 24",
|
||||
compile: Simple,
|
||||
varargs: true,
|
||||
},
|
||||
// Arithmetic
|
||||
Word {
|
||||
name: "+",
|
||||
|
||||
@@ -390,6 +390,16 @@ pub(super) const WORDS: &[Word] = &[
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: ".,",
|
||||
aliases: &[],
|
||||
category: "Generator",
|
||||
stack: "(start end step -- start start+step ...)",
|
||||
desc: "Push arithmetic sequence with custom step",
|
||||
example: "0 1 0.25 ., => 0 0.25 0.5 0.75 1",
|
||||
compile: Simple,
|
||||
varargs: false,
|
||||
},
|
||||
Word {
|
||||
name: "gen",
|
||||
aliases: &[],
|
||||
|
||||
@@ -27,6 +27,8 @@ struct ProjectFile {
|
||||
tempo: f64,
|
||||
#[serde(default)]
|
||||
playing_patterns: Vec<(usize, usize)>,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
prelude: String,
|
||||
}
|
||||
|
||||
fn default_tempo() -> f64 {
|
||||
@@ -41,6 +43,7 @@ impl From<&Project> for ProjectFile {
|
||||
sample_paths: project.sample_paths.clone(),
|
||||
tempo: project.tempo,
|
||||
playing_patterns: project.playing_patterns.clone(),
|
||||
prelude: project.prelude.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,6 +55,7 @@ impl From<ProjectFile> for Project {
|
||||
sample_paths: file.sample_paths,
|
||||
tempo: file.tempo,
|
||||
playing_patterns: file.playing_patterns,
|
||||
prelude: file.prelude,
|
||||
};
|
||||
project.normalize();
|
||||
project
|
||||
|
||||
@@ -452,6 +452,8 @@ pub struct Project {
|
||||
pub tempo: f64,
|
||||
#[serde(default)]
|
||||
pub playing_patterns: Vec<(usize, usize)>,
|
||||
#[serde(default)]
|
||||
pub prelude: String,
|
||||
}
|
||||
|
||||
fn default_tempo() -> f64 {
|
||||
@@ -465,6 +467,7 @@ impl Default for Project {
|
||||
sample_paths: Vec::new(),
|
||||
tempo: default_tempo(),
|
||||
playing_patterns: Vec::new(),
|
||||
prelude: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
51
docs/prelude.md
Normal file
51
docs/prelude.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# The Prelude
|
||||
|
||||
When you define a word in a step, it becomes available to all steps. But when you close and reopen the project, the dictionary is empty again. Words defined in steps only exist after those steps run.
|
||||
|
||||
The **prelude** solves this. It's a project-wide script that runs automatically when playback starts and when you load a project.
|
||||
|
||||
## Accessing the Prelude
|
||||
|
||||
Press `d` to open the prelude editor. Press `Esc` to save and evaluate. Press `D` (Shift+d) to re-evaluate the prelude without opening the editor.
|
||||
|
||||
## What It's For
|
||||
|
||||
Define words that should be available everywhere, always:
|
||||
|
||||
```forth
|
||||
: kick "kick" s 0.9 gain . ;
|
||||
: hat "hat" s 0.4 gain . ;
|
||||
: bass "saw" s 0.7 gain 200 lpf . ;
|
||||
```
|
||||
|
||||
Now every step in your project can use `kick`, `hat`, and `bass` from the first beat.
|
||||
|
||||
## When It Runs
|
||||
|
||||
The prelude evaluates:
|
||||
|
||||
1. When you press Space to start playback (if stopped)
|
||||
2. When you load a project
|
||||
3. When you press `D` manually
|
||||
|
||||
It does not run on every step, only once at these moments. This makes it ideal for setup code: word definitions, initial variable values, seed resets.
|
||||
|
||||
## Practical Example
|
||||
|
||||
A prelude for a techno project:
|
||||
|
||||
```forth
|
||||
: k "kick" s 1.2 attack . ;
|
||||
: sn "snare" s 0.6 gain 0.02 attack . ;
|
||||
: hh "hat" s 0.3 gain 8000 hpf . ;
|
||||
: sub "sine" s 0.8 gain 150 lpf . ;
|
||||
0 seed
|
||||
```
|
||||
|
||||
Step scripts become trivial:
|
||||
|
||||
```forth
|
||||
c1 note k sub
|
||||
```
|
||||
|
||||
The sound design lives in the prelude. Steps focus on rhythm and melody.
|
||||
72
src/app.rs
72
src/app.rs
@@ -18,8 +18,8 @@ use crate::page::Page;
|
||||
use crate::services::{clipboard, dict_nav, euclidean, help_nav, pattern_editor};
|
||||
use crate::settings::Settings;
|
||||
use crate::state::{
|
||||
AudioSettings, CyclicEnum, EditorContext, FlashKind, LiveKeyState, Metrics, Modal,
|
||||
MuteState, OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav,
|
||||
AudioSettings, CyclicEnum, EditorContext, EditorTarget, FlashKind, LiveKeyState, Metrics,
|
||||
Modal, MuteState, OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav,
|
||||
PlaybackState, ProjectState, StagedChange, StagedPropChange, UiState,
|
||||
};
|
||||
|
||||
@@ -164,8 +164,12 @@ impl App {
|
||||
self.project_state.mark_all_dirty();
|
||||
}
|
||||
|
||||
pub fn toggle_playing(&mut self) {
|
||||
pub fn toggle_playing(&mut self, link: &LinkState) {
|
||||
let was_playing = self.playback.playing;
|
||||
self.playback.toggle();
|
||||
if !was_playing && self.playback.playing {
|
||||
self.evaluate_prelude(link);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tempo_up(&self, link: &LinkState) {
|
||||
@@ -338,6 +342,58 @@ impl App {
|
||||
self.project_state.mark_dirty(change.bank, change.pattern);
|
||||
}
|
||||
|
||||
pub fn open_prelude_editor(&mut self) {
|
||||
let prelude = &self.project_state.project.prelude;
|
||||
let lines: Vec<String> = if prelude.is_empty() {
|
||||
vec![String::new()]
|
||||
} else {
|
||||
prelude.lines().map(String::from).collect()
|
||||
};
|
||||
self.editor_ctx.editor.set_content(lines);
|
||||
let candidates = model::WORDS
|
||||
.iter()
|
||||
.map(|w| cagire_ratatui::CompletionCandidate {
|
||||
name: w.name.to_string(),
|
||||
signature: w.stack.to_string(),
|
||||
description: w.desc.to_string(),
|
||||
example: w.example.to_string(),
|
||||
})
|
||||
.collect();
|
||||
self.editor_ctx.editor.set_candidates(candidates);
|
||||
self.editor_ctx
|
||||
.editor
|
||||
.set_completion_enabled(self.ui.show_completion);
|
||||
self.editor_ctx.target = EditorTarget::Prelude;
|
||||
self.ui.modal = Modal::Editor;
|
||||
}
|
||||
|
||||
pub fn save_prelude(&mut self) {
|
||||
let text = self.editor_ctx.editor.content();
|
||||
self.project_state.project.prelude = text;
|
||||
}
|
||||
|
||||
pub fn close_prelude_editor(&mut self) {
|
||||
self.editor_ctx.target = EditorTarget::Step;
|
||||
self.load_step_to_editor();
|
||||
}
|
||||
|
||||
pub fn evaluate_prelude(&mut self, link: &LinkState) {
|
||||
let prelude = &self.project_state.project.prelude;
|
||||
if prelude.trim().is_empty() {
|
||||
return;
|
||||
}
|
||||
let ctx = self.create_step_context(0, link);
|
||||
match self.script_engine.evaluate(prelude, &ctx) {
|
||||
Ok(_) => {
|
||||
self.ui.flash("Prelude evaluated", 150, FlashKind::Info);
|
||||
}
|
||||
Err(e) => {
|
||||
self.ui
|
||||
.flash(&format!("Prelude error: {e}"), 300, FlashKind::Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute_script_oneshot(
|
||||
&self,
|
||||
script: &str,
|
||||
@@ -614,6 +670,8 @@ impl App {
|
||||
self.variables.store(Arc::new(HashMap::new()));
|
||||
self.dict.lock().clear();
|
||||
|
||||
self.evaluate_prelude(link);
|
||||
|
||||
for (bank, pattern) in playing {
|
||||
self.playback.queued_changes.push(StagedChange {
|
||||
change: PatternChange::Start { bank, pattern },
|
||||
@@ -862,7 +920,7 @@ impl App {
|
||||
pub fn dispatch(&mut self, cmd: AppCommand, link: &LinkState, snapshot: &SequencerSnapshot) {
|
||||
match cmd {
|
||||
// Playback
|
||||
AppCommand::TogglePlaying => self.toggle_playing(),
|
||||
AppCommand::TogglePlaying => self.toggle_playing(link),
|
||||
AppCommand::TempoUp => self.tempo_up(link),
|
||||
AppCommand::TempoDown => self.tempo_down(link),
|
||||
|
||||
@@ -1278,6 +1336,12 @@ impl App {
|
||||
FlashKind::Success,
|
||||
);
|
||||
}
|
||||
|
||||
// Prelude
|
||||
AppCommand::OpenPreludeEditor => self.open_prelude_editor(),
|
||||
AppCommand::SavePrelude => self.save_prelude(),
|
||||
AppCommand::EvaluatePrelude => self.evaluate_prelude(link),
|
||||
AppCommand::ClosePreludeEditor => self.close_prelude_editor(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -221,4 +221,10 @@ pub enum AppCommand {
|
||||
steps: usize,
|
||||
rotation: usize,
|
||||
},
|
||||
|
||||
// Prelude
|
||||
OpenPreludeEditor,
|
||||
SavePrelude,
|
||||
EvaluatePrelude,
|
||||
ClosePreludeEditor,
|
||||
}
|
||||
|
||||
@@ -269,6 +269,8 @@ pub struct SequencerConfig {
|
||||
pub audio_sample_pos: Arc<AtomicU64>,
|
||||
pub sample_rate: Arc<std::sync::atomic::AtomicU32>,
|
||||
pub cc_access: Option<Arc<dyn CcAccess>>,
|
||||
pub variables: Variables,
|
||||
pub dict: Dictionary,
|
||||
#[cfg(feature = "desktop")]
|
||||
pub mouse_x: Arc<AtomicU32>,
|
||||
#[cfg(feature = "desktop")]
|
||||
@@ -301,6 +303,8 @@ pub fn spawn_sequencer(
|
||||
let shared_state = Arc::new(ArcSwap::from_pointee(SharedSequencerState::default()));
|
||||
let shared_state_clone = Arc::clone(&shared_state);
|
||||
|
||||
let variables = config.variables;
|
||||
let dict = config.dict;
|
||||
#[cfg(feature = "desktop")]
|
||||
let mouse_x = config.mouse_x;
|
||||
#[cfg(feature = "desktop")]
|
||||
@@ -335,6 +339,8 @@ pub fn spawn_sequencer(
|
||||
config.audio_sample_pos,
|
||||
config.sample_rate,
|
||||
config.cc_access,
|
||||
variables,
|
||||
dict,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_x,
|
||||
#[cfg(feature = "desktop")]
|
||||
@@ -529,6 +535,7 @@ pub(crate) struct SequencerState {
|
||||
event_count: usize,
|
||||
script_engine: ScriptEngine,
|
||||
variables: Variables,
|
||||
dict: Dictionary,
|
||||
speed_overrides: HashMap<(usize, usize), f64>,
|
||||
key_cache: KeyCache,
|
||||
buf_audio_commands: Vec<TimestampedCommand>,
|
||||
@@ -547,7 +554,7 @@ impl SequencerState {
|
||||
rng: Rng,
|
||||
cc_access: Option<Arc<dyn CcAccess>>,
|
||||
) -> Self {
|
||||
let script_engine = ScriptEngine::new(Arc::clone(&variables), dict, rng);
|
||||
let script_engine = ScriptEngine::new(Arc::clone(&variables), Arc::clone(&dict), rng);
|
||||
Self {
|
||||
audio_state: AudioState::new(),
|
||||
pattern_cache: PatternCache::new(),
|
||||
@@ -557,6 +564,7 @@ impl SequencerState {
|
||||
event_count: 0,
|
||||
script_engine,
|
||||
variables,
|
||||
dict,
|
||||
speed_overrides: HashMap::with_capacity(MAX_PATTERNS),
|
||||
key_cache: KeyCache::new(),
|
||||
buf_audio_commands: Vec::with_capacity(32),
|
||||
@@ -663,12 +671,9 @@ impl SequencerState {
|
||||
self.audio_state.flush_midi_notes = true;
|
||||
}
|
||||
SeqCommand::ResetScriptState => {
|
||||
let variables: Variables = Arc::new(ArcSwap::from_pointee(HashMap::new()));
|
||||
let dict: Dictionary = Arc::new(Mutex::new(HashMap::new()));
|
||||
let rng: Rng = Arc::new(Mutex::new(StdRng::seed_from_u64(0)));
|
||||
self.script_engine =
|
||||
ScriptEngine::new(Arc::clone(&variables), dict, rng);
|
||||
self.variables = variables;
|
||||
// Clear shared state instead of replacing - preserves sharing with app
|
||||
self.variables.store(Arc::new(HashMap::new()));
|
||||
self.dict.lock().clear();
|
||||
self.speed_overrides.clear();
|
||||
}
|
||||
SeqCommand::Shutdown => {}
|
||||
@@ -1070,6 +1075,8 @@ fn sequencer_loop(
|
||||
audio_sample_pos: Arc<AtomicU64>,
|
||||
sample_rate: Arc<std::sync::atomic::AtomicU32>,
|
||||
cc_access: Option<Arc<dyn CcAccess>>,
|
||||
variables: Variables,
|
||||
dict: Dictionary,
|
||||
#[cfg(feature = "desktop")] mouse_x: Arc<AtomicU32>,
|
||||
#[cfg(feature = "desktop")] mouse_y: Arc<AtomicU32>,
|
||||
#[cfg(feature = "desktop")] mouse_down: Arc<AtomicU32>,
|
||||
@@ -1078,8 +1085,6 @@ fn sequencer_loop(
|
||||
|
||||
set_realtime_priority();
|
||||
|
||||
let variables: Variables = Arc::new(ArcSwap::from_pointee(HashMap::new()));
|
||||
let dict: Dictionary = Arc::new(Mutex::new(HashMap::new()));
|
||||
let rng: Rng = Arc::new(Mutex::new(StdRng::seed_from_u64(0)));
|
||||
let mut seq_state = SequencerState::new(variables, dict, rng, cc_access);
|
||||
|
||||
|
||||
@@ -133,6 +133,8 @@ pub fn init(args: InitArgs) -> Init {
|
||||
audio_sample_pos: Arc::clone(&audio_sample_pos),
|
||||
sample_rate: Arc::clone(&sample_rate_shared),
|
||||
cc_access: Some(Arc::new(app.midi.cc_memory.clone()) as Arc<dyn model::CcAccess>),
|
||||
variables: Arc::clone(&app.variables),
|
||||
dict: Arc::clone(&app.dict),
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_x: Arc::clone(&mouse_x),
|
||||
#[cfg(feature = "desktop")]
|
||||
|
||||
27
src/input.rs
27
src/input.rs
@@ -11,8 +11,8 @@ use crate::engine::{AudioCommand, LinkState, SeqCommand, SequencerSnapshot};
|
||||
use crate::model::PatternSpeed;
|
||||
use crate::page::Page;
|
||||
use crate::state::{
|
||||
CyclicEnum, DeviceKind, EngineSection, EuclideanField, Modal, OptionsFocus, PanelFocus,
|
||||
PatternField, PatternPropsField, SampleBrowserState, SettingKind, SidePanel,
|
||||
CyclicEnum, DeviceKind, EditorTarget, EngineSection, EuclideanField, Modal, OptionsFocus,
|
||||
PanelFocus, PatternField, PatternPropsField, SampleBrowserState, SettingKind, SidePanel,
|
||||
};
|
||||
|
||||
pub enum InputResult {
|
||||
@@ -488,15 +488,32 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
} else if editor.completion_active() {
|
||||
editor.dismiss_completion();
|
||||
} else {
|
||||
match ctx.app.editor_ctx.target {
|
||||
EditorTarget::Step => {
|
||||
ctx.dispatch(AppCommand::SaveEditorToStep);
|
||||
ctx.dispatch(AppCommand::CompileCurrentStep);
|
||||
}
|
||||
EditorTarget::Prelude => {
|
||||
ctx.dispatch(AppCommand::SavePrelude);
|
||||
ctx.dispatch(AppCommand::EvaluatePrelude);
|
||||
ctx.dispatch(AppCommand::ClosePreludeEditor);
|
||||
}
|
||||
}
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
}
|
||||
KeyCode::Char('e') if ctrl => {
|
||||
match ctx.app.editor_ctx.target {
|
||||
EditorTarget::Step => {
|
||||
ctx.dispatch(AppCommand::SaveEditorToStep);
|
||||
ctx.dispatch(AppCommand::CompileCurrentStep);
|
||||
}
|
||||
EditorTarget::Prelude => {
|
||||
ctx.dispatch(AppCommand::SavePrelude);
|
||||
ctx.dispatch(AppCommand::EvaluatePrelude);
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Char('f') if ctrl => {
|
||||
editor.activate_search();
|
||||
}
|
||||
@@ -1082,6 +1099,12 @@ fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputR
|
||||
ctx.dispatch(AppCommand::ClearSolos);
|
||||
ctx.app.send_mute_state(ctx.seq_cmd_tx);
|
||||
}
|
||||
KeyCode::Char('d') => {
|
||||
ctx.dispatch(AppCommand::OpenPreludeEditor);
|
||||
}
|
||||
KeyCode::Char('D') => {
|
||||
ctx.dispatch(AppCommand::EvaluatePrelude);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
InputResult::Continue
|
||||
|
||||
@@ -27,6 +27,7 @@ pub const DOCS: &[DocEntry] = &[
|
||||
Topic("The Dictionary", include_str!("../../docs/dictionary.md")),
|
||||
Topic("The Stack", include_str!("../../docs/stack.md")),
|
||||
Topic("Creating Words", include_str!("../../docs/definitions.md")),
|
||||
Topic("The Prelude", include_str!("../../docs/prelude.md")),
|
||||
Topic("Oddities", include_str!("../../docs/oddities.md")),
|
||||
// Audio Engine
|
||||
Section("Audio Engine"),
|
||||
|
||||
@@ -3,6 +3,13 @@ use std::ops::RangeInclusive;
|
||||
|
||||
use cagire_ratatui::Editor;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum EditorTarget {
|
||||
#[default]
|
||||
Step,
|
||||
Prelude,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PatternField {
|
||||
Length,
|
||||
@@ -76,6 +83,7 @@ pub struct EditorContext {
|
||||
pub copied_steps: Option<CopiedSteps>,
|
||||
pub show_stack: bool,
|
||||
pub stack_cache: RefCell<Option<StackCache>>,
|
||||
pub target: EditorTarget,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -125,6 +133,7 @@ impl Default for EditorContext {
|
||||
copied_steps: None,
|
||||
show_stack: false,
|
||||
stack_cache: RefCell::new(None),
|
||||
target: EditorTarget::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ pub mod ui;
|
||||
pub use audio::{AudioSettings, DeviceKind, EngineSection, MainLayout, Metrics, SettingKind};
|
||||
pub use color_scheme::ColorScheme;
|
||||
pub use editor::{
|
||||
CopiedStepData, CopiedSteps, EditorContext, EuclideanField, PatternField, PatternPropsField,
|
||||
StackCache,
|
||||
CopiedStepData, CopiedSteps, EditorContext, EditorTarget, EuclideanField, PatternField,
|
||||
PatternPropsField, StackCache,
|
||||
};
|
||||
pub use live_keys::LiveKeyState;
|
||||
pub use modal::Modal;
|
||||
|
||||
@@ -37,6 +37,12 @@ pub fn bindings_for(page: Page) -> Vec<(&'static str, &'static str, &'static str
|
||||
bindings.push(("r", "Rename", "Rename current step"));
|
||||
bindings.push(("Ctrl+R", "Run", "Run step script immediately"));
|
||||
bindings.push(("e", "Euclidean", "Distribute linked steps using Euclidean rhythm"));
|
||||
bindings.push(("m", "Mute", "Stage mute for current pattern"));
|
||||
bindings.push(("x", "Solo", "Stage solo for current pattern"));
|
||||
bindings.push(("M", "Clear mutes", "Clear all mutes"));
|
||||
bindings.push(("X", "Clear solos", "Clear all solos"));
|
||||
bindings.push(("d", "Prelude", "Edit prelude script"));
|
||||
bindings.push(("D", "Eval prelude", "Re-evaluate prelude without editing"));
|
||||
}
|
||||
Page::Patterns => {
|
||||
bindings.push(("←→↑↓", "Navigate", "Move between banks/patterns"));
|
||||
@@ -46,6 +52,10 @@ pub fn bindings_for(page: Page) -> Vec<(&'static str, &'static str, &'static str
|
||||
bindings.push(("c", "Commit", "Commit staged changes"));
|
||||
bindings.push(("r", "Rename", "Rename bank/pattern"));
|
||||
bindings.push(("e", "Properties", "Edit pattern properties"));
|
||||
bindings.push(("m", "Mute", "Stage mute for pattern"));
|
||||
bindings.push(("x", "Solo", "Stage solo for pattern"));
|
||||
bindings.push(("M", "Clear mutes", "Clear all mutes"));
|
||||
bindings.push(("X", "Clear solos", "Clear all solos"));
|
||||
bindings.push(("Ctrl+C", "Copy", "Copy bank/pattern"));
|
||||
bindings.push(("Ctrl+V", "Paste", "Paste bank/pattern"));
|
||||
bindings.push(("Del", "Reset", "Reset bank/pattern"));
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::engine::{LinkState, SequencerSnapshot};
|
||||
use crate::model::SourceSpan;
|
||||
use crate::page::Page;
|
||||
use crate::state::{
|
||||
EuclideanField, FlashKind, Modal, PanelFocus, PatternField, SidePanel,
|
||||
EditorTarget, EuclideanField, FlashKind, Modal, PanelFocus, PatternField, SidePanel,
|
||||
};
|
||||
use crate::theme;
|
||||
use crate::views::highlight::{self, highlight_line, highlight_line_with_runtime};
|
||||
@@ -629,8 +629,6 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
||||
Modal::Editor => {
|
||||
let width = (term.width * 80 / 100).max(40);
|
||||
let height = (term.height * 60 / 100).max(10);
|
||||
let step_num = app.editor_ctx.step + 1;
|
||||
let step = app.current_edit_pattern().step(app.editor_ctx.step);
|
||||
|
||||
let flash_kind = app.ui.flash_kind();
|
||||
let border_color = match flash_kind {
|
||||
@@ -640,10 +638,17 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
||||
None => theme.modal.editor,
|
||||
};
|
||||
|
||||
let title = if let Some(ref name) = step.and_then(|s| s.name.as_ref()) {
|
||||
let title = match app.editor_ctx.target {
|
||||
EditorTarget::Prelude => "Prelude".to_string(),
|
||||
EditorTarget::Step => {
|
||||
let step_num = app.editor_ctx.step + 1;
|
||||
let step = app.current_edit_pattern().step(app.editor_ctx.step);
|
||||
if let Some(ref name) = step.and_then(|s| s.name.as_ref()) {
|
||||
format!("Step {step_num:02}: {name}")
|
||||
} else {
|
||||
format!("Step {step_num:02} Script")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let inner = ModalFrame::new(&title)
|
||||
@@ -652,7 +657,10 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
||||
.border_color(border_color)
|
||||
.render_centered(frame, term);
|
||||
|
||||
let trace = if app.ui.runtime_highlight && app.playback.playing {
|
||||
let trace = if app.ui.runtime_highlight
|
||||
&& app.playback.playing
|
||||
&& app.editor_ctx.target == EditorTarget::Step
|
||||
{
|
||||
let source = app
|
||||
.current_edit_pattern()
|
||||
.resolve_source(app.editor_ctx.step);
|
||||
|
||||
@@ -102,3 +102,43 @@ fn geom_zero_count() {
|
||||
fn geom_underflow() {
|
||||
expect_error("1 2 geom..", "stack underflow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_range_ascending() {
|
||||
expect_stack("0 1 0.25 .,", &[
|
||||
int(0),
|
||||
Value::Float(0.25, None),
|
||||
Value::Float(0.5, None),
|
||||
Value::Float(0.75, None),
|
||||
int(1),
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_range_descending() {
|
||||
expect_stack("1 0 -0.5 .,", &[
|
||||
int(1),
|
||||
Value::Float(0.5, None),
|
||||
int(0),
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_range_integer_step() {
|
||||
expect_stack("0 6 2 .,", &[int(0), int(2), int(4), int(6)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_range_single() {
|
||||
expect_stack("5 5 1 .,", &[int(5)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_range_zero_step_error() {
|
||||
expect_error("0 10 0 .,", "step cannot be zero");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn step_range_underflow() {
|
||||
expect_error("0 1 .,", "stack underflow");
|
||||
}
|
||||
|
||||
@@ -152,3 +152,104 @@ fn clear_stack() {
|
||||
f.clear_stack();
|
||||
assert!(f.stack().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rev() {
|
||||
expect_stack("1 2 3 3 rev", &[int(3), int(2), int(1)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rev_partial() {
|
||||
expect_stack("1 2 3 4 2 rev", &[int(1), int(2), int(4), int(3)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rev_one() {
|
||||
expect_stack("1 2 3 1 rev", &[int(1), int(2), int(3)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rev_zero() {
|
||||
expect_stack("1 2 3 0 rev", &[int(1), int(2), int(3)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rev_underflow() {
|
||||
expect_error("1 2 5 rev", "stack underflow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shuffle_preserves_elements() {
|
||||
let f = forth();
|
||||
f.evaluate("1 2 3 4 4 shuffle", &default_ctx()).unwrap();
|
||||
let mut stack: Vec<i64> = f
|
||||
.stack()
|
||||
.iter()
|
||||
.map(|v| match v {
|
||||
Value::Int(n, _) => *n,
|
||||
_ => panic!("expected int"),
|
||||
})
|
||||
.collect();
|
||||
stack.sort();
|
||||
assert_eq!(stack, vec![1, 2, 3, 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shuffle_underflow() {
|
||||
expect_error("1 2 5 shuffle", "stack underflow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sort() {
|
||||
expect_stack("3 1 4 1 5 5 sort", &[int(1), int(1), int(3), int(4), int(5)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sort_partial() {
|
||||
expect_stack("9 3 1 2 3 sort", &[int(9), int(1), int(2), int(3)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sort_underflow() {
|
||||
expect_error("1 2 5 sort", "stack underflow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rsort() {
|
||||
expect_stack("1 3 2 3 rsort", &[int(3), int(2), int(1)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sum() {
|
||||
expect_stack("1 2 3 4 4 sum", &[int(10)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sum_single() {
|
||||
expect_stack("42 1 sum", &[int(42)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sum_zero() {
|
||||
expect_stack("1 2 3 0 sum", &[int(1), int(2), int(3), int(0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sum_underflow() {
|
||||
expect_error("1 2 5 sum", "stack underflow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prod() {
|
||||
expect_stack("2 3 4 3 prod", &[int(24)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prod_single() {
|
||||
expect_stack("7 1 prod", &[int(7)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prod_zero_count() {
|
||||
expect_stack("1 2 3 0 prod", &[int(1), int(2), int(3), int(1)]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user