251 lines
8.6 KiB
Rust
251 lines
8.6 KiB
Rust
//! Forth script compilation, evaluation, and editor ↔ step synchronization.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use crossbeam_channel::Sender;
|
|
|
|
use crate::engine::LinkState;
|
|
use crate::model::StepContext;
|
|
use crate::services::pattern_editor;
|
|
use crate::state::{EditorTarget, FlashKind, Modal, SampleTree};
|
|
|
|
use super::{App, COMPLETION_CANDIDATES};
|
|
|
|
impl App {
|
|
/// Build a `StepContext` for evaluating a script outside the sequencer.
|
|
pub(super) fn create_step_context(&self, step_idx: usize, link: &LinkState) -> StepContext<'static> {
|
|
let (bank, pattern) = self.current_bank_pattern();
|
|
let speed = self
|
|
.project_state
|
|
.project
|
|
.pattern_at(bank, pattern)
|
|
.speed
|
|
.multiplier();
|
|
StepContext {
|
|
step: step_idx,
|
|
beat: link.beat(),
|
|
bank,
|
|
pattern,
|
|
tempo: link.tempo(),
|
|
phase: link.phase(),
|
|
slot: 0,
|
|
runs: 0,
|
|
iter: 0,
|
|
speed,
|
|
fill: false,
|
|
nudge_secs: 0.0,
|
|
cc_access: None,
|
|
speed_key: "",
|
|
mouse_x: 0.5,
|
|
mouse_y: 0.5,
|
|
mouse_down: 0.0,
|
|
}
|
|
}
|
|
|
|
/// Load the current step's script into the editor widget.
|
|
pub(super) fn load_step_to_editor(&mut self) {
|
|
let (bank, pattern) = self.current_bank_pattern();
|
|
if let Some(script) = pattern_editor::get_step_script(
|
|
&self.project_state.project,
|
|
bank,
|
|
pattern,
|
|
self.editor_ctx.step,
|
|
) {
|
|
let lines: Vec<String> = if script.is_empty() {
|
|
vec![String::new()]
|
|
} else {
|
|
script.lines().map(String::from).collect()
|
|
};
|
|
self.editor_ctx.editor.set_content(lines);
|
|
self.editor_ctx.editor.set_candidates(Arc::clone(&COMPLETION_CANDIDATES));
|
|
self.editor_ctx
|
|
.editor
|
|
.set_completion_enabled(self.ui.show_completion);
|
|
let tree = SampleTree::from_paths(&self.audio.config.sample_paths);
|
|
self.editor_ctx.editor.set_sample_folders(tree.all_folder_names());
|
|
}
|
|
}
|
|
|
|
/// Write the editor widget's content back to the current step.
|
|
pub fn save_editor_to_step(&mut self) {
|
|
let text = self.editor_ctx.editor.content();
|
|
let (bank, pattern) = self.current_bank_pattern();
|
|
let change = pattern_editor::set_step_script(
|
|
&mut self.project_state.project,
|
|
bank,
|
|
pattern,
|
|
self.editor_ctx.step,
|
|
text,
|
|
);
|
|
self.project_state.mark_dirty(change.bank, change.pattern);
|
|
}
|
|
|
|
/// Switch the editor to the project prelude script.
|
|
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);
|
|
self.editor_ctx.editor.set_candidates(Arc::clone(&COMPLETION_CANDIDATES));
|
|
self.editor_ctx
|
|
.editor
|
|
.set_completion_enabled(self.ui.show_completion);
|
|
let tree = SampleTree::from_paths(&self.audio.config.sample_paths);
|
|
self.editor_ctx.editor.set_sample_folders(tree.all_folder_names());
|
|
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();
|
|
}
|
|
|
|
/// Evaluate the project prelude to seed variables and definitions.
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Evaluate a script and immediately send its audio commands.
|
|
/// Returns collected `print` output, if any.
|
|
pub fn execute_script_oneshot(
|
|
&self,
|
|
script: &str,
|
|
link: &LinkState,
|
|
audio_tx: &arc_swap::ArcSwap<Sender<crate::engine::AudioCommand>>,
|
|
) -> Result<Option<String>, String> {
|
|
let ctx = self.create_step_context(self.editor_ctx.step, link);
|
|
let cmds = self.script_engine.evaluate(script, &ctx)?;
|
|
let mut print_output = String::new();
|
|
for cmd in cmds {
|
|
if let Some(text) = cmd.strip_prefix("print:") {
|
|
if !print_output.is_empty() {
|
|
print_output.push(' ');
|
|
}
|
|
print_output.push_str(text);
|
|
continue;
|
|
}
|
|
let _ = audio_tx
|
|
.load()
|
|
.send(crate::engine::AudioCommand::Evaluate { cmd, time: None });
|
|
}
|
|
Ok(if print_output.is_empty() {
|
|
None
|
|
} else {
|
|
Some(print_output)
|
|
})
|
|
}
|
|
|
|
/// Compile (evaluate) the current step's script to check for errors.
|
|
pub fn compile_current_step(&mut self, link: &LinkState) {
|
|
let step_idx = self.editor_ctx.step;
|
|
let (bank, pattern) = self.current_bank_pattern();
|
|
|
|
let script =
|
|
pattern_editor::get_step_script(&self.project_state.project, bank, pattern, step_idx)
|
|
.unwrap_or_default();
|
|
|
|
if script.trim().is_empty() {
|
|
return;
|
|
}
|
|
|
|
let ctx = self.create_step_context(step_idx, link);
|
|
|
|
match self.script_engine.evaluate(&script, &ctx) {
|
|
Ok(_) => {
|
|
self.ui.flash("Script compiled", 150, FlashKind::Info);
|
|
}
|
|
Err(e) => {
|
|
self.ui
|
|
.flash(&format!("Script error: {e}"), 300, FlashKind::Error);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Load the project's periodic script into the script editor.
|
|
pub fn load_script_to_editor(&mut self) {
|
|
let script = &self.project_state.project.script;
|
|
let lines: Vec<String> = if script.is_empty() {
|
|
vec![String::new()]
|
|
} else {
|
|
script.lines().map(String::from).collect()
|
|
};
|
|
self.script_editor.editor.set_content(lines);
|
|
self.script_editor.editor.set_candidates(Arc::clone(&COMPLETION_CANDIDATES));
|
|
self.script_editor
|
|
.editor
|
|
.set_completion_enabled(self.ui.show_completion);
|
|
let tree = SampleTree::from_paths(&self.audio.config.sample_paths);
|
|
self.script_editor.editor.set_sample_folders(tree.all_folder_names());
|
|
}
|
|
|
|
/// Write the script editor content back to the project.
|
|
pub fn save_script_from_editor(&mut self) {
|
|
let text = self.script_editor.editor.content();
|
|
self.project_state.project.script = text;
|
|
self.script_editor.dirty = true;
|
|
}
|
|
|
|
/// Evaluate the script page content to check for errors.
|
|
pub fn evaluate_script_page(&mut self, link: &LinkState) {
|
|
let script = self.script_editor.editor.content();
|
|
if script.trim().is_empty() {
|
|
return;
|
|
}
|
|
let ctx = self.create_step_context(0, link);
|
|
match self.script_engine.evaluate(&script, &ctx) {
|
|
Ok(_) => {
|
|
self.ui.flash("Script compiled", 150, FlashKind::Info);
|
|
}
|
|
Err(e) => {
|
|
self.ui
|
|
.flash(&format!("Script error: {e}"), 300, FlashKind::Error);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Compile all steps in the current pattern to warm up definitions.
|
|
pub fn compile_all_steps(&mut self, link: &LinkState) {
|
|
let pattern_len = self.current_edit_pattern().length;
|
|
let (bank, pattern) = self.current_bank_pattern();
|
|
|
|
for step_idx in 0..pattern_len {
|
|
let script = pattern_editor::get_step_script(
|
|
&self.project_state.project,
|
|
bank,
|
|
pattern,
|
|
step_idx,
|
|
)
|
|
.unwrap_or_default();
|
|
|
|
if script.trim().is_empty() {
|
|
continue;
|
|
}
|
|
|
|
let ctx = self.create_step_context(step_idx, link);
|
|
let _ = self.script_engine.evaluate(&script, &ctx);
|
|
}
|
|
}
|
|
}
|