//! 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 = 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 = 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>, ) -> Result, 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 = 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); } } }