Files
Cagire/src/app/scripting.rs

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);
}
}
}