This commit is contained in:
2026-03-05 22:14:28 +01:00
parent 77364dddae
commit 04b68850d0
25 changed files with 374 additions and 8447 deletions

View File

@@ -469,6 +469,13 @@ impl App {
AppCommand::SavePrelude => self.save_prelude(),
AppCommand::EvaluatePrelude => self.evaluate_prelude(link),
AppCommand::ClosePreludeEditor => self.close_prelude_editor(),
AppCommand::OpenBankPreludeEditor => self.open_bank_prelude_editor(),
AppCommand::SaveBankPrelude => self.save_bank_prelude(),
AppCommand::EvaluateBankPrelude => {
let bank = self.editor_ctx.bank;
self.evaluate_bank_prelude(bank, link);
}
AppCommand::CloseBankPreludeEditor => self.close_bank_prelude_editor(),
// Periodic script
AppCommand::OpenScriptModal(field) => self.open_script_modal(field),

View File

@@ -109,24 +109,101 @@ impl App {
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;
/// Switch the editor to the current bank's prelude script.
pub fn open_bank_prelude_editor(&mut self) {
let bank = self.editor_ctx.bank;
let prelude = &self.project_state.project.banks[bank].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::BankPrelude;
self.ui.modal = Modal::Editor;
}
pub fn save_bank_prelude(&mut self) {
let bank = self.editor_ctx.bank;
let text = self.editor_ctx.editor.content();
self.project_state.project.banks[bank].prelude = text;
}
pub fn close_bank_prelude_editor(&mut self) {
self.editor_ctx.target = EditorTarget::Step;
self.load_step_to_editor();
}
/// Evaluate a single bank's prelude.
pub fn evaluate_bank_prelude(&mut self, bank: usize, link: &LinkState) {
let prelude = &self.project_state.project.banks[bank].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);
}
Ok(_) => {}
Err(e) => {
self.ui
.flash(&format!("Prelude error: {e}"), 300, FlashKind::Error);
let fallback = format!("Bank {}", bank + 1);
let bank_name = self.project_state.project.banks[bank]
.name
.as_deref()
.unwrap_or(&fallback);
self.ui.flash(
&format!("{bank_name} prelude error: {e}"),
300,
FlashKind::Error,
);
}
}
}
/// Evaluate the project prelude and all bank preludes.
pub fn evaluate_prelude(&mut self, link: &LinkState) {
let project_prelude = &self.project_state.project.prelude;
if !project_prelude.trim().is_empty() {
let ctx = self.create_step_context(0, link);
match self.script_engine.evaluate(project_prelude, &ctx) {
Ok(_) => {}
Err(e) => {
self.ui
.flash(&format!("Project prelude error: {e}"), 300, FlashKind::Error);
return;
}
}
}
for bank_idx in 0..self.project_state.project.banks.len() {
let prelude = &self.project_state.project.banks[bank_idx].prelude;
if prelude.trim().is_empty() {
continue;
}
let ctx = self.create_step_context(0, link);
match self.script_engine.evaluate(prelude, &ctx) {
Ok(_) => {}
Err(e) => {
let bank_name = self.project_state.project.banks[bank_idx]
.name
.as_deref()
.map(String::from)
.unwrap_or_else(|| format!("Bank {}", bank_idx + 1));
self.ui.flash(
&format!("{bank_name} prelude error: {e}"),
300,
FlashKind::Error,
);
return;
}
}
}
self.ui.flash("Preludes evaluated", 150, FlashKind::Info);
}
/// Evaluate a script and immediately send its audio commands.
/// Returns collected `print` output, if any.
pub fn execute_script_oneshot(

View File

@@ -42,6 +42,11 @@ impl App {
let data = self.project_state.project.pattern_at(*bank, *pattern).clone();
Some(UndoScope::Pattern { bank: *bank, pattern: *pattern, data })
}
AppCommand::SaveBankPrelude => {
let bank = self.editor_ctx.bank;
let data = self.project_state.project.banks[bank].clone();
Some(UndoScope::Bank { bank, data })
}
AppCommand::ResetBank { bank }
| AppCommand::PasteBank { bank }
| AppCommand::ImportBank { bank } => {

View File

@@ -301,6 +301,10 @@ pub enum AppCommand {
SavePrelude,
EvaluatePrelude,
ClosePreludeEditor,
OpenBankPreludeEditor,
SaveBankPrelude,
EvaluateBankPrelude,
CloseBankPreludeEditor,
// Onboarding
DismissOnboarding,

View File

@@ -82,7 +82,8 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
KeyCode::Char(']') => ctx.dispatch(AppCommand::SpeedIncrease),
KeyCode::Char('L') => ctx.dispatch(AppCommand::OpenPatternModal(PatternField::Length)),
KeyCode::Char('S') => ctx.dispatch(AppCommand::OpenPatternModal(PatternField::Speed)),
KeyCode::Char('p') => ctx.dispatch(AppCommand::OpenPreludeEditor),
KeyCode::Char('p') => ctx.dispatch(AppCommand::OpenBankPreludeEditor),
KeyCode::Char('P') => ctx.dispatch(AppCommand::OpenPreludeEditor),
KeyCode::Delete | KeyCode::Backspace => {
let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern);
if let Some(range) = ctx.app.editor_ctx.selection_range() {

View File

@@ -351,6 +351,11 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
ctx.dispatch(AppCommand::EvaluatePrelude);
ctx.dispatch(AppCommand::ClosePreludeEditor);
}
EditorTarget::BankPrelude => {
ctx.dispatch(AppCommand::SaveBankPrelude);
ctx.dispatch(AppCommand::EvaluateBankPrelude);
ctx.dispatch(AppCommand::CloseBankPreludeEditor);
}
}
ctx.dispatch(AppCommand::CloseModal);
}
@@ -364,6 +369,10 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
ctx.dispatch(AppCommand::SavePrelude);
ctx.dispatch(AppCommand::EvaluatePrelude);
}
EditorTarget::BankPrelude => {
ctx.dispatch(AppCommand::SaveBankPrelude);
ctx.dispatch(AppCommand::EvaluateBankPrelude);
}
},
KeyCode::Char('b') if ctrl => {
editor.activate_sample_finder();

View File

@@ -941,6 +941,11 @@ fn handle_modal_click(ctx: &mut InputContext, col: u16, row: u16, term: Rect) {
ctx.dispatch(AppCommand::EvaluatePrelude);
ctx.dispatch(AppCommand::ClosePreludeEditor);
}
EditorTarget::BankPrelude => {
ctx.dispatch(AppCommand::SaveBankPrelude);
ctx.dispatch(AppCommand::EvaluateBankPrelude);
ctx.dispatch(AppCommand::CloseBankPreludeEditor);
}
}
ctx.dispatch(AppCommand::CloseModal);
}

View File

@@ -70,7 +70,7 @@ pub const DOCS: &[DocEntry] = &[
),
Topic("Brackets", include_str!("../../docs/forth/brackets.md")),
Topic("Cycling", include_str!("../../docs/forth/cycling.md")),
Topic("The Prelude", include_str!("../../docs/forth/prelude.md")),
Topic("Preludes", include_str!("../../docs/forth/prelude.md")),
Topic(
"Cagire vs Classic",
include_str!("../../docs/forth/oddities.md"),

View File

@@ -766,17 +766,26 @@ pub static COMMANDS: LazyLock<Vec<CommandEntry>> = LazyLock::new(|| {
// === Prelude ===
CommandEntry {
name: "Edit Prelude",
description: "Open prelude editor",
name: "Edit Bank Prelude",
description: "Open current bank's prelude editor",
category: "Prelude",
keybinding: "p",
pages: &[Main],
normal_mode: false,
action: Some(PaletteAction::Resolve(|_| Some(AppCommand::OpenBankPreludeEditor))),
},
CommandEntry {
name: "Edit Project Prelude",
description: "Open project-wide prelude editor",
category: "Prelude",
keybinding: "P",
pages: &[Main],
normal_mode: false,
action: Some(PaletteAction::Resolve(|_| Some(AppCommand::OpenPreludeEditor))),
},
CommandEntry {
name: "Evaluate Prelude",
description: "Re-evaluate prelude script",
name: "Evaluate All Preludes",
description: "Re-evaluate project and all bank preludes",
category: "Prelude",
keybinding: "d",
pages: &[Main],

View File

@@ -8,6 +8,7 @@ pub enum EditorTarget {
#[default]
Step,
Prelude,
BankPrelude,
}
#[derive(Clone, Copy, PartialEq, Eq)]

View File

@@ -76,7 +76,11 @@ fn render_top_layout(
}
if has_preview {
let user_words: HashSet<String> = app.dict.lock().keys().cloned().collect();
let has_prelude = !app.project_state.project.prelude.trim().is_empty();
let has_prelude = !app.project_state.project.prelude.trim().is_empty()
|| !app.project_state.project.banks[app.editor_ctx.bank]
.prelude
.trim()
.is_empty();
if has_prelude {
let [script_area, prelude_area] =
Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)]).areas(areas[idx]);
@@ -195,7 +199,11 @@ fn render_viz_area(
VizPanel::Lissajous => render_lissajous(frame, app, *panel_area),
VizPanel::Preview => {
let user_words = user_words_once.as_ref().expect("user_words initialized");
let has_prelude = !app.project_state.project.prelude.trim().is_empty();
let has_prelude = !app.project_state.project.prelude.trim().is_empty()
|| !app.project_state.project.banks[app.editor_ctx.bank]
.prelude
.trim()
.is_empty();
if has_prelude {
let [script_area, prelude_area] = if is_vertical_layout {
Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)])
@@ -655,11 +663,20 @@ pub(crate) fn render_prelude_preview(
area: Rect,
) {
let theme = theme::get();
let prelude = &app.project_state.project.prelude;
let bank_prelude = &app.project_state.project.banks[app.editor_ctx.bank].prelude;
let (prelude, title) = if !bank_prelude.trim().is_empty() {
let bank_name = app.project_state.project.banks[app.editor_ctx.bank]
.name
.as_deref()
.unwrap_or("Bank");
(bank_prelude.as_str(), format!(" {bank_name} Prelude "))
} else {
(app.project_state.project.prelude.as_str(), " Prelude ".to_string())
};
let block = Block::default()
.borders(Borders::ALL)
.title(" Prelude ")
.title(title)
.border_style(Style::new().fg(theme.ui.border));
let inner = block.inner(area);
frame.render_widget(block, area);

View File

@@ -911,7 +911,13 @@ fn render_modal_editor(
};
let title = match app.editor_ctx.target {
EditorTarget::Prelude => "Prelude".to_string(),
EditorTarget::Prelude => "Project Prelude".to_string(),
EditorTarget::BankPrelude => {
let bank = &app.project_state.project.banks[app.editor_ctx.bank];
let fallback = format!("Bank {}", app.editor_ctx.bank + 1);
let bank_name = bank.name.as_deref().unwrap_or(&fallback);
format!("{bank_name} Prelude")
}
EditorTarget::Step => {
let step_num = app.editor_ctx.step + 1;
let step = app.current_edit_pattern().step(app.editor_ctx.step);

View File

@@ -114,7 +114,11 @@ fn render_sidebar(frame: &mut Frame, app: &App, area: Rect) {
if app.audio.config.show_lissajous {
constraints.push(Constraint::Fill(1));
}
let has_prelude = !app.project_state.project.prelude.trim().is_empty();
let has_prelude = !app.project_state.project.prelude.trim().is_empty()
|| !app.project_state.project.banks[app.editor_ctx.bank]
.prelude
.trim()
.is_empty();
if has_prelude {
constraints.push(Constraint::Fill(1));
}