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
|
### Added
|
||||||
- TachyonFX based animations
|
- 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
|
### 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.
|
- Decoupled script runtime state between UI and sequencer threads, eliminating shared mutexes on the RT path.
|
||||||
|
|
||||||
### Fixed
|
### 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).
|
- 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.
|
- 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.
|
- PatternProps and EuclideanDistribution modals now use the global theme background instead of the terminal default.
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ pub enum Op {
|
|||||||
Drop2,
|
Drop2,
|
||||||
Swap2,
|
Swap2,
|
||||||
Over2,
|
Over2,
|
||||||
|
Rev,
|
||||||
|
Shuffle,
|
||||||
|
Sort,
|
||||||
|
RSort,
|
||||||
|
Sum,
|
||||||
|
Prod,
|
||||||
Forget,
|
Forget,
|
||||||
Add,
|
Add,
|
||||||
Sub,
|
Sub,
|
||||||
@@ -91,6 +97,7 @@ pub enum Op {
|
|||||||
SetSpeed,
|
SetSpeed,
|
||||||
At,
|
At,
|
||||||
IntRange,
|
IntRange,
|
||||||
|
StepRange,
|
||||||
Generate,
|
Generate,
|
||||||
GeomRange,
|
GeomRange,
|
||||||
Times,
|
Times,
|
||||||
|
|||||||
@@ -345,6 +345,77 @@ impl Forth {
|
|||||||
stack.push(a);
|
stack.push(a);
|
||||||
stack.push(b);
|
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::Add => binary_op(stack, |a, b| a + b)?,
|
||||||
Op::Sub => 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 => {
|
Op::Generate => {
|
||||||
let count = stack.pop().ok_or("stack underflow")?.as_int()?;
|
let count = stack.pop().ok_or("stack underflow")?.as_int()?;
|
||||||
let quot = stack.pop().ok_or("stack underflow")?;
|
let quot = stack.pop().ok_or("stack underflow")?;
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
|||||||
"2drop" => Op::Drop2,
|
"2drop" => Op::Drop2,
|
||||||
"2swap" => Op::Swap2,
|
"2swap" => Op::Swap2,
|
||||||
"2over" => Op::Over2,
|
"2over" => Op::Over2,
|
||||||
|
"rev" => Op::Rev,
|
||||||
|
"shuffle" => Op::Shuffle,
|
||||||
|
"sort" => Op::Sort,
|
||||||
|
"rsort" => Op::RSort,
|
||||||
|
"sum" => Op::Sum,
|
||||||
|
"prod" => Op::Prod,
|
||||||
"+" => Op::Add,
|
"+" => Op::Add,
|
||||||
"-" => Op::Sub,
|
"-" => Op::Sub,
|
||||||
"*" => Op::Mul,
|
"*" => Op::Mul,
|
||||||
@@ -83,6 +89,7 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
|||||||
"oct" => Op::Oct,
|
"oct" => Op::Oct,
|
||||||
"clear" => Op::ClearCmd,
|
"clear" => Op::ClearCmd,
|
||||||
".." => Op::IntRange,
|
".." => Op::IntRange,
|
||||||
|
".," => Op::StepRange,
|
||||||
"gen" => Op::Generate,
|
"gen" => Op::Generate,
|
||||||
"geom.." => Op::GeomRange,
|
"geom.." => Op::GeomRange,
|
||||||
"times" => Op::Times,
|
"times" => Op::Times,
|
||||||
|
|||||||
@@ -123,6 +123,66 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
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
|
// Arithmetic
|
||||||
Word {
|
Word {
|
||||||
name: "+",
|
name: "+",
|
||||||
|
|||||||
@@ -390,6 +390,16 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
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 {
|
Word {
|
||||||
name: "gen",
|
name: "gen",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ struct ProjectFile {
|
|||||||
tempo: f64,
|
tempo: f64,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
playing_patterns: Vec<(usize, usize)>,
|
playing_patterns: Vec<(usize, usize)>,
|
||||||
|
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||||
|
prelude: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_tempo() -> f64 {
|
fn default_tempo() -> f64 {
|
||||||
@@ -41,6 +43,7 @@ impl From<&Project> for ProjectFile {
|
|||||||
sample_paths: project.sample_paths.clone(),
|
sample_paths: project.sample_paths.clone(),
|
||||||
tempo: project.tempo,
|
tempo: project.tempo,
|
||||||
playing_patterns: project.playing_patterns.clone(),
|
playing_patterns: project.playing_patterns.clone(),
|
||||||
|
prelude: project.prelude.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,6 +55,7 @@ impl From<ProjectFile> for Project {
|
|||||||
sample_paths: file.sample_paths,
|
sample_paths: file.sample_paths,
|
||||||
tempo: file.tempo,
|
tempo: file.tempo,
|
||||||
playing_patterns: file.playing_patterns,
|
playing_patterns: file.playing_patterns,
|
||||||
|
prelude: file.prelude,
|
||||||
};
|
};
|
||||||
project.normalize();
|
project.normalize();
|
||||||
project
|
project
|
||||||
|
|||||||
@@ -452,6 +452,8 @@ pub struct Project {
|
|||||||
pub tempo: f64,
|
pub tempo: f64,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub playing_patterns: Vec<(usize, usize)>,
|
pub playing_patterns: Vec<(usize, usize)>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub prelude: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_tempo() -> f64 {
|
fn default_tempo() -> f64 {
|
||||||
@@ -465,6 +467,7 @@ impl Default for Project {
|
|||||||
sample_paths: Vec::new(),
|
sample_paths: Vec::new(),
|
||||||
tempo: default_tempo(),
|
tempo: default_tempo(),
|
||||||
playing_patterns: Vec::new(),
|
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::services::{clipboard, dict_nav, euclidean, help_nav, pattern_editor};
|
||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
AudioSettings, CyclicEnum, EditorContext, FlashKind, LiveKeyState, Metrics, Modal,
|
AudioSettings, CyclicEnum, EditorContext, EditorTarget, FlashKind, LiveKeyState, Metrics,
|
||||||
MuteState, OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav,
|
Modal, MuteState, OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav,
|
||||||
PlaybackState, ProjectState, StagedChange, StagedPropChange, UiState,
|
PlaybackState, ProjectState, StagedChange, StagedPropChange, UiState,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -164,8 +164,12 @@ impl App {
|
|||||||
self.project_state.mark_all_dirty();
|
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();
|
self.playback.toggle();
|
||||||
|
if !was_playing && self.playback.playing {
|
||||||
|
self.evaluate_prelude(link);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tempo_up(&self, link: &LinkState) {
|
pub fn tempo_up(&self, link: &LinkState) {
|
||||||
@@ -338,6 +342,58 @@ impl App {
|
|||||||
self.project_state.mark_dirty(change.bank, change.pattern);
|
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(
|
pub fn execute_script_oneshot(
|
||||||
&self,
|
&self,
|
||||||
script: &str,
|
script: &str,
|
||||||
@@ -614,6 +670,8 @@ impl App {
|
|||||||
self.variables.store(Arc::new(HashMap::new()));
|
self.variables.store(Arc::new(HashMap::new()));
|
||||||
self.dict.lock().clear();
|
self.dict.lock().clear();
|
||||||
|
|
||||||
|
self.evaluate_prelude(link);
|
||||||
|
|
||||||
for (bank, pattern) in playing {
|
for (bank, pattern) in playing {
|
||||||
self.playback.queued_changes.push(StagedChange {
|
self.playback.queued_changes.push(StagedChange {
|
||||||
change: PatternChange::Start { bank, pattern },
|
change: PatternChange::Start { bank, pattern },
|
||||||
@@ -862,7 +920,7 @@ impl App {
|
|||||||
pub fn dispatch(&mut self, cmd: AppCommand, link: &LinkState, snapshot: &SequencerSnapshot) {
|
pub fn dispatch(&mut self, cmd: AppCommand, link: &LinkState, snapshot: &SequencerSnapshot) {
|
||||||
match cmd {
|
match cmd {
|
||||||
// Playback
|
// Playback
|
||||||
AppCommand::TogglePlaying => self.toggle_playing(),
|
AppCommand::TogglePlaying => self.toggle_playing(link),
|
||||||
AppCommand::TempoUp => self.tempo_up(link),
|
AppCommand::TempoUp => self.tempo_up(link),
|
||||||
AppCommand::TempoDown => self.tempo_down(link),
|
AppCommand::TempoDown => self.tempo_down(link),
|
||||||
|
|
||||||
@@ -1278,6 +1336,12 @@ impl App {
|
|||||||
FlashKind::Success,
|
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,
|
steps: usize,
|
||||||
rotation: usize,
|
rotation: usize,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Prelude
|
||||||
|
OpenPreludeEditor,
|
||||||
|
SavePrelude,
|
||||||
|
EvaluatePrelude,
|
||||||
|
ClosePreludeEditor,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -269,6 +269,8 @@ pub struct SequencerConfig {
|
|||||||
pub audio_sample_pos: Arc<AtomicU64>,
|
pub audio_sample_pos: Arc<AtomicU64>,
|
||||||
pub sample_rate: Arc<std::sync::atomic::AtomicU32>,
|
pub sample_rate: Arc<std::sync::atomic::AtomicU32>,
|
||||||
pub cc_access: Option<Arc<dyn CcAccess>>,
|
pub cc_access: Option<Arc<dyn CcAccess>>,
|
||||||
|
pub variables: Variables,
|
||||||
|
pub dict: Dictionary,
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
pub mouse_x: Arc<AtomicU32>,
|
pub mouse_x: Arc<AtomicU32>,
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
@@ -301,6 +303,8 @@ pub fn spawn_sequencer(
|
|||||||
let shared_state = Arc::new(ArcSwap::from_pointee(SharedSequencerState::default()));
|
let shared_state = Arc::new(ArcSwap::from_pointee(SharedSequencerState::default()));
|
||||||
let shared_state_clone = Arc::clone(&shared_state);
|
let shared_state_clone = Arc::clone(&shared_state);
|
||||||
|
|
||||||
|
let variables = config.variables;
|
||||||
|
let dict = config.dict;
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
let mouse_x = config.mouse_x;
|
let mouse_x = config.mouse_x;
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
@@ -335,6 +339,8 @@ pub fn spawn_sequencer(
|
|||||||
config.audio_sample_pos,
|
config.audio_sample_pos,
|
||||||
config.sample_rate,
|
config.sample_rate,
|
||||||
config.cc_access,
|
config.cc_access,
|
||||||
|
variables,
|
||||||
|
dict,
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
mouse_x,
|
mouse_x,
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
@@ -529,6 +535,7 @@ pub(crate) struct SequencerState {
|
|||||||
event_count: usize,
|
event_count: usize,
|
||||||
script_engine: ScriptEngine,
|
script_engine: ScriptEngine,
|
||||||
variables: Variables,
|
variables: Variables,
|
||||||
|
dict: Dictionary,
|
||||||
speed_overrides: HashMap<(usize, usize), f64>,
|
speed_overrides: HashMap<(usize, usize), f64>,
|
||||||
key_cache: KeyCache,
|
key_cache: KeyCache,
|
||||||
buf_audio_commands: Vec<TimestampedCommand>,
|
buf_audio_commands: Vec<TimestampedCommand>,
|
||||||
@@ -547,7 +554,7 @@ impl SequencerState {
|
|||||||
rng: Rng,
|
rng: Rng,
|
||||||
cc_access: Option<Arc<dyn CcAccess>>,
|
cc_access: Option<Arc<dyn CcAccess>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let script_engine = ScriptEngine::new(Arc::clone(&variables), dict, rng);
|
let script_engine = ScriptEngine::new(Arc::clone(&variables), Arc::clone(&dict), rng);
|
||||||
Self {
|
Self {
|
||||||
audio_state: AudioState::new(),
|
audio_state: AudioState::new(),
|
||||||
pattern_cache: PatternCache::new(),
|
pattern_cache: PatternCache::new(),
|
||||||
@@ -557,6 +564,7 @@ impl SequencerState {
|
|||||||
event_count: 0,
|
event_count: 0,
|
||||||
script_engine,
|
script_engine,
|
||||||
variables,
|
variables,
|
||||||
|
dict,
|
||||||
speed_overrides: HashMap::with_capacity(MAX_PATTERNS),
|
speed_overrides: HashMap::with_capacity(MAX_PATTERNS),
|
||||||
key_cache: KeyCache::new(),
|
key_cache: KeyCache::new(),
|
||||||
buf_audio_commands: Vec::with_capacity(32),
|
buf_audio_commands: Vec::with_capacity(32),
|
||||||
@@ -663,12 +671,9 @@ impl SequencerState {
|
|||||||
self.audio_state.flush_midi_notes = true;
|
self.audio_state.flush_midi_notes = true;
|
||||||
}
|
}
|
||||||
SeqCommand::ResetScriptState => {
|
SeqCommand::ResetScriptState => {
|
||||||
let variables: Variables = Arc::new(ArcSwap::from_pointee(HashMap::new()));
|
// Clear shared state instead of replacing - preserves sharing with app
|
||||||
let dict: Dictionary = Arc::new(Mutex::new(HashMap::new()));
|
self.variables.store(Arc::new(HashMap::new()));
|
||||||
let rng: Rng = Arc::new(Mutex::new(StdRng::seed_from_u64(0)));
|
self.dict.lock().clear();
|
||||||
self.script_engine =
|
|
||||||
ScriptEngine::new(Arc::clone(&variables), dict, rng);
|
|
||||||
self.variables = variables;
|
|
||||||
self.speed_overrides.clear();
|
self.speed_overrides.clear();
|
||||||
}
|
}
|
||||||
SeqCommand::Shutdown => {}
|
SeqCommand::Shutdown => {}
|
||||||
@@ -1070,6 +1075,8 @@ fn sequencer_loop(
|
|||||||
audio_sample_pos: Arc<AtomicU64>,
|
audio_sample_pos: Arc<AtomicU64>,
|
||||||
sample_rate: Arc<std::sync::atomic::AtomicU32>,
|
sample_rate: Arc<std::sync::atomic::AtomicU32>,
|
||||||
cc_access: Option<Arc<dyn CcAccess>>,
|
cc_access: Option<Arc<dyn CcAccess>>,
|
||||||
|
variables: Variables,
|
||||||
|
dict: Dictionary,
|
||||||
#[cfg(feature = "desktop")] mouse_x: Arc<AtomicU32>,
|
#[cfg(feature = "desktop")] mouse_x: Arc<AtomicU32>,
|
||||||
#[cfg(feature = "desktop")] mouse_y: Arc<AtomicU32>,
|
#[cfg(feature = "desktop")] mouse_y: Arc<AtomicU32>,
|
||||||
#[cfg(feature = "desktop")] mouse_down: Arc<AtomicU32>,
|
#[cfg(feature = "desktop")] mouse_down: Arc<AtomicU32>,
|
||||||
@@ -1078,8 +1085,6 @@ fn sequencer_loop(
|
|||||||
|
|
||||||
set_realtime_priority();
|
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 rng: Rng = Arc::new(Mutex::new(StdRng::seed_from_u64(0)));
|
||||||
let mut seq_state = SequencerState::new(variables, dict, rng, cc_access);
|
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),
|
audio_sample_pos: Arc::clone(&audio_sample_pos),
|
||||||
sample_rate: Arc::clone(&sample_rate_shared),
|
sample_rate: Arc::clone(&sample_rate_shared),
|
||||||
cc_access: Some(Arc::new(app.midi.cc_memory.clone()) as Arc<dyn model::CcAccess>),
|
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")]
|
#[cfg(feature = "desktop")]
|
||||||
mouse_x: Arc::clone(&mouse_x),
|
mouse_x: Arc::clone(&mouse_x),
|
||||||
#[cfg(feature = "desktop")]
|
#[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::model::PatternSpeed;
|
||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
CyclicEnum, DeviceKind, EngineSection, EuclideanField, Modal, OptionsFocus, PanelFocus,
|
CyclicEnum, DeviceKind, EditorTarget, EngineSection, EuclideanField, Modal, OptionsFocus,
|
||||||
PatternField, PatternPropsField, SampleBrowserState, SettingKind, SidePanel,
|
PanelFocus, PatternField, PatternPropsField, SampleBrowserState, SettingKind, SidePanel,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub enum InputResult {
|
pub enum InputResult {
|
||||||
@@ -488,15 +488,32 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
} else if editor.completion_active() {
|
} else if editor.completion_active() {
|
||||||
editor.dismiss_completion();
|
editor.dismiss_completion();
|
||||||
} else {
|
} else {
|
||||||
|
match ctx.app.editor_ctx.target {
|
||||||
|
EditorTarget::Step => {
|
||||||
ctx.dispatch(AppCommand::SaveEditorToStep);
|
ctx.dispatch(AppCommand::SaveEditorToStep);
|
||||||
ctx.dispatch(AppCommand::CompileCurrentStep);
|
ctx.dispatch(AppCommand::CompileCurrentStep);
|
||||||
|
}
|
||||||
|
EditorTarget::Prelude => {
|
||||||
|
ctx.dispatch(AppCommand::SavePrelude);
|
||||||
|
ctx.dispatch(AppCommand::EvaluatePrelude);
|
||||||
|
ctx.dispatch(AppCommand::ClosePreludeEditor);
|
||||||
|
}
|
||||||
|
}
|
||||||
ctx.dispatch(AppCommand::CloseModal);
|
ctx.dispatch(AppCommand::CloseModal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Char('e') if ctrl => {
|
KeyCode::Char('e') if ctrl => {
|
||||||
|
match ctx.app.editor_ctx.target {
|
||||||
|
EditorTarget::Step => {
|
||||||
ctx.dispatch(AppCommand::SaveEditorToStep);
|
ctx.dispatch(AppCommand::SaveEditorToStep);
|
||||||
ctx.dispatch(AppCommand::CompileCurrentStep);
|
ctx.dispatch(AppCommand::CompileCurrentStep);
|
||||||
}
|
}
|
||||||
|
EditorTarget::Prelude => {
|
||||||
|
ctx.dispatch(AppCommand::SavePrelude);
|
||||||
|
ctx.dispatch(AppCommand::EvaluatePrelude);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
KeyCode::Char('f') if ctrl => {
|
KeyCode::Char('f') if ctrl => {
|
||||||
editor.activate_search();
|
editor.activate_search();
|
||||||
}
|
}
|
||||||
@@ -1082,6 +1099,12 @@ fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputR
|
|||||||
ctx.dispatch(AppCommand::ClearSolos);
|
ctx.dispatch(AppCommand::ClearSolos);
|
||||||
ctx.app.send_mute_state(ctx.seq_cmd_tx);
|
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
|
InputResult::Continue
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ pub const DOCS: &[DocEntry] = &[
|
|||||||
Topic("The Dictionary", include_str!("../../docs/dictionary.md")),
|
Topic("The Dictionary", include_str!("../../docs/dictionary.md")),
|
||||||
Topic("The Stack", include_str!("../../docs/stack.md")),
|
Topic("The Stack", include_str!("../../docs/stack.md")),
|
||||||
Topic("Creating Words", include_str!("../../docs/definitions.md")),
|
Topic("Creating Words", include_str!("../../docs/definitions.md")),
|
||||||
|
Topic("The Prelude", include_str!("../../docs/prelude.md")),
|
||||||
Topic("Oddities", include_str!("../../docs/oddities.md")),
|
Topic("Oddities", include_str!("../../docs/oddities.md")),
|
||||||
// Audio Engine
|
// Audio Engine
|
||||||
Section("Audio Engine"),
|
Section("Audio Engine"),
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ use std::ops::RangeInclusive;
|
|||||||
|
|
||||||
use cagire_ratatui::Editor;
|
use cagire_ratatui::Editor;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum EditorTarget {
|
||||||
|
#[default]
|
||||||
|
Step,
|
||||||
|
Prelude,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum PatternField {
|
pub enum PatternField {
|
||||||
Length,
|
Length,
|
||||||
@@ -76,6 +83,7 @@ pub struct EditorContext {
|
|||||||
pub copied_steps: Option<CopiedSteps>,
|
pub copied_steps: Option<CopiedSteps>,
|
||||||
pub show_stack: bool,
|
pub show_stack: bool,
|
||||||
pub stack_cache: RefCell<Option<StackCache>>,
|
pub stack_cache: RefCell<Option<StackCache>>,
|
||||||
|
pub target: EditorTarget,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -125,6 +133,7 @@ impl Default for EditorContext {
|
|||||||
copied_steps: None,
|
copied_steps: None,
|
||||||
show_stack: false,
|
show_stack: false,
|
||||||
stack_cache: RefCell::new(None),
|
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 audio::{AudioSettings, DeviceKind, EngineSection, MainLayout, Metrics, SettingKind};
|
||||||
pub use color_scheme::ColorScheme;
|
pub use color_scheme::ColorScheme;
|
||||||
pub use editor::{
|
pub use editor::{
|
||||||
CopiedStepData, CopiedSteps, EditorContext, EuclideanField, PatternField, PatternPropsField,
|
CopiedStepData, CopiedSteps, EditorContext, EditorTarget, EuclideanField, PatternField,
|
||||||
StackCache,
|
PatternPropsField, StackCache,
|
||||||
};
|
};
|
||||||
pub use live_keys::LiveKeyState;
|
pub use live_keys::LiveKeyState;
|
||||||
pub use modal::Modal;
|
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(("r", "Rename", "Rename current step"));
|
||||||
bindings.push(("Ctrl+R", "Run", "Run step script immediately"));
|
bindings.push(("Ctrl+R", "Run", "Run step script immediately"));
|
||||||
bindings.push(("e", "Euclidean", "Distribute linked steps using Euclidean rhythm"));
|
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 => {
|
Page::Patterns => {
|
||||||
bindings.push(("←→↑↓", "Navigate", "Move between banks/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(("c", "Commit", "Commit staged changes"));
|
||||||
bindings.push(("r", "Rename", "Rename bank/pattern"));
|
bindings.push(("r", "Rename", "Rename bank/pattern"));
|
||||||
bindings.push(("e", "Properties", "Edit pattern properties"));
|
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+C", "Copy", "Copy bank/pattern"));
|
||||||
bindings.push(("Ctrl+V", "Paste", "Paste bank/pattern"));
|
bindings.push(("Ctrl+V", "Paste", "Paste bank/pattern"));
|
||||||
bindings.push(("Del", "Reset", "Reset bank/pattern"));
|
bindings.push(("Del", "Reset", "Reset bank/pattern"));
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use crate::engine::{LinkState, SequencerSnapshot};
|
|||||||
use crate::model::SourceSpan;
|
use crate::model::SourceSpan;
|
||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
EuclideanField, FlashKind, Modal, PanelFocus, PatternField, SidePanel,
|
EditorTarget, EuclideanField, FlashKind, Modal, PanelFocus, PatternField, SidePanel,
|
||||||
};
|
};
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use crate::views::highlight::{self, highlight_line, highlight_line_with_runtime};
|
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 => {
|
Modal::Editor => {
|
||||||
let width = (term.width * 80 / 100).max(40);
|
let width = (term.width * 80 / 100).max(40);
|
||||||
let height = (term.height * 60 / 100).max(10);
|
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 flash_kind = app.ui.flash_kind();
|
||||||
let border_color = match 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,
|
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}")
|
format!("Step {step_num:02}: {name}")
|
||||||
} else {
|
} else {
|
||||||
format!("Step {step_num:02} Script")
|
format!("Step {step_num:02} Script")
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let inner = ModalFrame::new(&title)
|
let inner = ModalFrame::new(&title)
|
||||||
@@ -652,7 +657,10 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
|||||||
.border_color(border_color)
|
.border_color(border_color)
|
||||||
.render_centered(frame, term);
|
.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
|
let source = app
|
||||||
.current_edit_pattern()
|
.current_edit_pattern()
|
||||||
.resolve_source(app.editor_ctx.step);
|
.resolve_source(app.editor_ctx.step);
|
||||||
|
|||||||
@@ -102,3 +102,43 @@ fn geom_zero_count() {
|
|||||||
fn geom_underflow() {
|
fn geom_underflow() {
|
||||||
expect_error("1 2 geom..", "stack 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();
|
f.clear_stack();
|
||||||
assert!(f.stack().is_empty());
|
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