use arc_swap::ArcSwap; use parking_lot::Mutex; use rand::rngs::StdRng; use rand::SeedableRng; use std::collections::HashMap; use std::path::PathBuf; use std::sync::{Arc, LazyLock}; use cagire_ratatui::CompletionCandidate; use crossbeam_channel::Sender; use crate::commands::AppCommand; use crate::engine::{ LinkState, PatternChange, PatternSnapshot, SeqCommand, SequencerSnapshot, StepSnapshot, }; use crate::midi::MidiState; use crate::model::{self, Bank, Dictionary, Pattern, Rng, ScriptEngine, StepContext, Variables}; use crate::page::Page; use crate::services::{clipboard, dict_nav, euclidean, help_nav, pattern_editor}; use crate::settings::Settings; use crate::state::{ AudioSettings, CyclicEnum, EditorContext, EditorTarget, FlashKind, LiveKeyState, Metrics, Modal, MuteState, OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav, PlaybackState, ProjectState, SampleTree, StagedChange, StagedPropChange, UiState, }; const STEPS_PER_PAGE: usize = 32; static COMPLETION_CANDIDATES: LazyLock> = LazyLock::new(|| { model::WORDS .iter() .map(|w| CompletionCandidate { name: w.name.to_string(), signature: w.stack.to_string(), description: w.desc.to_string(), example: w.example.to_string(), }) .collect() }); pub struct App { pub project_state: ProjectState, pub ui: UiState, pub playback: PlaybackState, pub mute: MuteState, pub page: Page, pub editor_ctx: EditorContext, pub patterns_nav: PatternsNav, pub metrics: Metrics, pub script_engine: ScriptEngine, pub variables: Variables, pub dict: Dictionary, #[allow(dead_code)] // kept alive for ScriptEngine's Arc clone pub rng: Rng, pub live_keys: Arc, pub clipboard: Option, pub copied_patterns: Option>, pub copied_banks: Option>, pub audio: AudioSettings, pub options: OptionsState, pub panel: PanelState, pub midi: MidiState, } impl Default for App { fn default() -> Self { Self::new() } } impl App { pub fn new() -> Self { let variables = Arc::new(ArcSwap::from_pointee(HashMap::new())); let dict = Arc::new(Mutex::new(HashMap::new())); let rng = Arc::new(Mutex::new(StdRng::seed_from_u64(0))); let script_engine = ScriptEngine::new(Arc::clone(&variables), Arc::clone(&dict), Arc::clone(&rng)); let live_keys = Arc::new(LiveKeyState::new()); Self { project_state: ProjectState::default(), ui: UiState::default(), playback: PlaybackState::default(), mute: MuteState::default(), page: Page::default(), editor_ctx: EditorContext::default(), patterns_nav: PatternsNav::default(), metrics: Metrics::default(), variables, dict, rng, live_keys, script_engine, clipboard: arboard::Clipboard::new().ok(), copied_patterns: None, copied_banks: None, audio: AudioSettings::default(), options: OptionsState::default(), panel: PanelState::default(), midi: MidiState::new(), } } pub fn save_settings(&self, link: &LinkState) { let settings = Settings { audio: crate::settings::AudioSettings { output_device: self.audio.config.output_device.clone(), input_device: self.audio.config.input_device.clone(), channels: self.audio.config.channels, buffer_size: self.audio.config.buffer_size, max_voices: self.audio.config.max_voices, }, display: crate::settings::DisplaySettings { fps: self.audio.config.refresh_rate.to_fps(), runtime_highlight: self.ui.runtime_highlight, show_scope: self.audio.config.show_scope, show_spectrum: self.audio.config.show_spectrum, show_completion: self.ui.show_completion, color_scheme: self.ui.color_scheme, layout: self.audio.config.layout, hue_rotation: self.ui.hue_rotation, ..Default::default() }, link: crate::settings::LinkSettings { enabled: link.is_enabled(), tempo: link.tempo(), quantum: link.quantum(), }, midi: crate::settings::MidiSettings { output_devices: { let outputs = crate::midi::list_midi_outputs(); self.midi .selected_outputs .iter() .map(|opt| { opt.and_then(|idx| outputs.get(idx).map(|d| d.name.clone())) .unwrap_or_default() }) .collect() }, input_devices: { let inputs = crate::midi::list_midi_inputs(); self.midi .selected_inputs .iter() .map(|opt| { opt.and_then(|idx| inputs.get(idx).map(|d| d.name.clone())) .unwrap_or_default() }) .collect() }, }, }; settings.save(); } fn current_bank_pattern(&self) -> (usize, usize) { (self.editor_ctx.bank, self.editor_ctx.pattern) } fn selected_steps(&self) -> Vec { match self.editor_ctx.selection_range() { Some(range) => range.collect(), None => vec![self.editor_ctx.step], } } pub fn mark_all_patterns_dirty(&mut self) { self.project_state.mark_all_dirty(); } pub fn toggle_playing(&mut self, link: &LinkState) { let was_playing = self.playback.playing; self.playback.toggle(); if !was_playing && self.playback.playing { self.evaluate_prelude(link); } } pub fn tempo_up(&self, link: &LinkState) { let current = link.tempo(); link.set_tempo((current + 1.0).min(300.0)); } pub fn tempo_down(&self, link: &LinkState) { let current = link.tempo(); link.set_tempo((current - 1.0).max(20.0)); } pub fn current_edit_pattern(&self) -> &Pattern { let (bank, pattern) = self.current_bank_pattern(); self.project_state.project.pattern_at(bank, pattern) } pub fn next_step(&mut self) { let len = self.current_edit_pattern().length; self.editor_ctx.step = (self.editor_ctx.step + 1) % len; self.load_step_to_editor(); } pub fn prev_step(&mut self) { let len = self.current_edit_pattern().length; self.editor_ctx.step = (self.editor_ctx.step + len - 1) % len; self.load_step_to_editor(); } pub fn step_up(&mut self) { let len = self.current_edit_pattern().length; let page_start = (self.editor_ctx.step / STEPS_PER_PAGE) * STEPS_PER_PAGE; let steps_on_page = (page_start + STEPS_PER_PAGE).min(len) - page_start; let num_rows = steps_on_page.div_ceil(8); let steps_per_row = steps_on_page.div_ceil(num_rows); if self.editor_ctx.step >= steps_per_row { self.editor_ctx.step -= steps_per_row; } else { self.editor_ctx.step = (self.editor_ctx.step + len - steps_per_row) % len; } self.load_step_to_editor(); } pub fn step_down(&mut self) { let len = self.current_edit_pattern().length; let page_start = (self.editor_ctx.step / STEPS_PER_PAGE) * STEPS_PER_PAGE; let steps_on_page = (page_start + STEPS_PER_PAGE).min(len) - page_start; let num_rows = steps_on_page.div_ceil(8); let steps_per_row = steps_on_page.div_ceil(num_rows); self.editor_ctx.step = (self.editor_ctx.step + steps_per_row) % len; self.load_step_to_editor(); } pub fn toggle_steps(&mut self) { let (bank, pattern) = self.current_bank_pattern(); for idx in self.selected_steps() { pattern_editor::toggle_step(&mut self.project_state.project, bank, pattern, idx); } self.project_state.mark_dirty(bank, pattern); } pub fn length_increase(&mut self) { let (bank, pattern) = self.current_bank_pattern(); let (change, _) = pattern_editor::increase_length(&mut self.project_state.project, bank, pattern); self.project_state.mark_dirty(change.bank, change.pattern); } pub fn length_decrease(&mut self) { let (bank, pattern) = self.current_bank_pattern(); let (change, new_len) = pattern_editor::decrease_length(&mut self.project_state.project, bank, pattern); if self.editor_ctx.step >= new_len { self.editor_ctx.step = new_len - 1; self.load_step_to_editor(); } self.project_state.mark_dirty(change.bank, change.pattern); } pub fn speed_increase(&mut self) { let (bank, pattern) = self.current_bank_pattern(); let change = pattern_editor::increase_speed(&mut self.project_state.project, bank, pattern); self.project_state.mark_dirty(change.bank, change.pattern); } pub fn speed_decrease(&mut self) { let (bank, pattern) = self.current_bank_pattern(); let change = pattern_editor::decrease_speed(&mut self.project_state.project, bank, pattern); self.project_state.mark_dirty(change.bank, change.pattern); } 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: "", chain_key: "", #[cfg(feature = "desktop")] mouse_x: 0.5, #[cfg(feature = "desktop")] mouse_y: 0.5, #[cfg(feature = "desktop")] mouse_down: 0.0, } } 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(COMPLETION_CANDIDATES.clone()); 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()); if self.editor_ctx.show_stack { crate::services::stack_preview::update_cache(&self.editor_ctx); } } } 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); } 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(COMPLETION_CANDIDATES.clone()); 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(); } 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( &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)?; for cmd in cmds { let _ = audio_tx .load() .send(crate::engine::AudioCommand::Evaluate { cmd, time: None }); } Ok(()) } 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); } } } 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); } } pub fn stage_pattern_toggle( &mut self, bank: usize, pattern: usize, snapshot: &SequencerSnapshot, ) { let is_playing = snapshot.is_playing(bank, pattern); let pattern_data = self.project_state.project.pattern_at(bank, pattern); let existing = self.playback.staged_changes.iter().position(|c| { c.change.pattern_id().bank == bank && c.change.pattern_id().pattern == pattern }); if let Some(idx) = existing { self.playback.staged_changes.remove(idx); self.ui .set_status(format!("B{:02}:P{:02} unstaged", bank + 1, pattern + 1)); } else if is_playing { self.playback.staged_changes.push(StagedChange { change: PatternChange::Stop { bank, pattern }, quantization: pattern_data.quantization, sync_mode: pattern_data.sync_mode, }); self.ui.set_status(format!( "B{:02}:P{:02} staged to stop", bank + 1, pattern + 1 )); } else { self.playback.staged_changes.push(StagedChange { change: PatternChange::Start { bank, pattern }, quantization: pattern_data.quantization, sync_mode: pattern_data.sync_mode, }); self.ui.set_status(format!( "B{:02}:P{:02} staged to play", bank + 1, pattern + 1 )); } } /// Commits staged pattern, mute/solo, and prop changes. /// Returns true if mute state changed (caller should send to sequencer). pub fn commit_staged_changes(&mut self) -> bool { let pattern_count = self.playback.staged_changes.len(); let mute_count = self.playback.staged_mute_changes.len(); let prop_count = self.playback.staged_prop_changes.len(); if pattern_count == 0 && mute_count == 0 && prop_count == 0 { self.ui.set_status("No changes to commit".to_string()); return false; } // Commit pattern changes (queued for quantization) if pattern_count > 0 { self.playback .queued_changes .append(&mut self.playback.staged_changes); } // Apply mute/solo changes immediately let mute_changed = mute_count > 0; for change in self.playback.staged_mute_changes.drain() { match change { crate::state::StagedMuteChange::ToggleMute { bank, pattern } => { self.mute.toggle_mute(bank, pattern); } crate::state::StagedMuteChange::ToggleSolo { bank, pattern } => { self.mute.toggle_solo(bank, pattern); } } } // Apply staged prop changes for ((bank, pattern), props) in self.playback.staged_prop_changes.drain() { let pat = self.project_state.project.pattern_at_mut(bank, pattern); pat.name = props.name; if let Some(len) = props.length { pat.set_length(len); } pat.speed = props.speed; pat.quantization = props.quantization; pat.sync_mode = props.sync_mode; self.project_state.mark_dirty(bank, pattern); } let total = pattern_count + mute_count + prop_count; let status = format!("Committed {total} changes"); self.ui.set_status(status); mute_changed } pub fn clear_staged_changes(&mut self) { let pattern_count = self.playback.staged_changes.len(); let mute_count = self.playback.staged_mute_changes.len(); let prop_count = self.playback.staged_prop_changes.len(); if pattern_count == 0 && mute_count == 0 && prop_count == 0 { return; } self.playback.staged_changes.clear(); self.playback.staged_mute_changes.clear(); self.playback.staged_prop_changes.clear(); let total = pattern_count + mute_count + prop_count; let status = format!("Cleared {total} staged changes"); self.ui.set_status(status); } pub fn select_edit_pattern(&mut self, pattern: usize) { self.editor_ctx.pattern = pattern; self.editor_ctx.step = 0; self.load_step_to_editor(); } pub fn select_edit_bank(&mut self, bank: usize) { self.editor_ctx.bank = bank; self.editor_ctx.pattern = 0; self.editor_ctx.step = 0; self.load_step_to_editor(); } pub fn save(&mut self, path: PathBuf, link: &LinkState, snapshot: &SequencerSnapshot) { self.save_editor_to_step(); self.project_state.project.sample_paths = self.audio.config.sample_paths.clone(); self.project_state.project.tempo = link.tempo(); self.project_state.project.playing_patterns = snapshot .active_patterns .iter() .map(|p| (p.bank, p.pattern)) .collect(); match model::save(&self.project_state.project, &path) { Ok(final_path) => { self.ui .set_status(format!("Saved: {}", final_path.display())); self.project_state.file_path = Some(final_path); } Err(e) => { self.ui.set_status(format!("Save error: {e}")); } } } pub fn load(&mut self, path: PathBuf, link: &LinkState) { match model::load(&path) { Ok(project) => { let tempo = project.tempo; let playing = project.playing_patterns.clone(); self.project_state.project = project; self.editor_ctx.step = 0; self.load_step_to_editor(); self.compile_all_steps(link); self.mark_all_patterns_dirty(); link.set_tempo(tempo); self.playback.clear_queues(); self.variables.store(Arc::new(HashMap::new())); self.dict.lock().clear(); self.evaluate_prelude(link); for (bank, pattern) in playing { self.playback.queued_changes.push(StagedChange { change: PatternChange::Start { bank, pattern }, quantization: crate::model::LaunchQuantization::Immediate, sync_mode: crate::model::SyncMode::PhaseLock, }); } self.ui.set_status(format!("Loaded: {}", path.display())); self.project_state.file_path = Some(path); } Err(e) => { self.ui.set_status(format!("Load error: {e}")); } } } pub fn delete_step(&mut self, bank: usize, pattern: usize, step: usize) { let edit = pattern_editor::delete_step(&mut self.project_state.project, bank, pattern, step); self.project_state.mark_dirty(edit.bank, edit.pattern); if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern && self.editor_ctx.step == step { self.load_step_to_editor(); } self.ui.flash("Step deleted", 150, FlashKind::Success); } pub fn delete_steps(&mut self, bank: usize, pattern: usize, steps: &[usize]) { let edit = pattern_editor::delete_steps(&mut self.project_state.project, bank, pattern, steps); self.project_state.mark_dirty(edit.bank, edit.pattern); if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern { self.load_step_to_editor(); } self.editor_ctx.clear_selection(); self.ui.flash( &format!("{} steps deleted", steps.len()), 150, FlashKind::Success, ); } pub fn reset_pattern(&mut self, bank: usize, pattern: usize) { let edit = pattern_editor::reset_pattern(&mut self.project_state.project, bank, pattern); self.project_state.mark_dirty(edit.bank, edit.pattern); if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern { self.load_step_to_editor(); } self.ui.flash("Pattern reset", 150, FlashKind::Success); } pub fn reset_bank(&mut self, bank: usize) { let pat_count = pattern_editor::reset_bank(&mut self.project_state.project, bank); for pattern in 0..pat_count { self.project_state.mark_dirty(bank, pattern); } if self.editor_ctx.bank == bank { self.load_step_to_editor(); } self.ui.flash("Bank reset", 150, FlashKind::Success); } pub fn copy_pattern(&mut self, bank: usize, pattern: usize) { self.copied_patterns = Some(vec![clipboard::copy_pattern( &self.project_state.project, bank, pattern, )]); self.ui.flash("Pattern copied", 150, FlashKind::Success); } pub fn paste_pattern(&mut self, bank: usize, pattern: usize) { if let Some(src) = self.copied_patterns.as_ref().and_then(|v| v.first()) { let src = src.clone(); clipboard::paste_pattern(&mut self.project_state.project, bank, pattern, &src); self.project_state.mark_dirty(bank, pattern); if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern { self.load_step_to_editor(); } self.ui.flash("Pattern pasted", 150, FlashKind::Success); } } pub fn copy_patterns(&mut self, bank: usize, patterns: &[usize]) { self.copied_patterns = Some(clipboard::copy_patterns( &self.project_state.project, bank, patterns, )); let n = patterns.len(); self.ui.flash( &format!("{n} pattern{} copied", if n == 1 { "" } else { "s" }), 150, FlashKind::Success, ); } pub fn paste_patterns(&mut self, bank: usize, start: usize) { if let Some(sources) = self.copied_patterns.clone() { let count = clipboard::paste_patterns( &mut self.project_state.project, bank, start, &sources, ); for i in 0..count { self.project_state.mark_dirty(bank, start + i); } if self.editor_ctx.bank == bank { self.load_step_to_editor(); } self.ui.flash( &format!("{count} pattern{} pasted", if count == 1 { "" } else { "s" }), 150, FlashKind::Success, ); } } pub fn copy_bank(&mut self, bank: usize) { self.copied_banks = Some(vec![clipboard::copy_bank( &self.project_state.project, bank, )]); self.ui.flash("Bank copied", 150, FlashKind::Success); } pub fn paste_bank(&mut self, bank: usize) { if let Some(src) = self.copied_banks.as_ref().and_then(|v| v.first()) { let src = src.clone(); let pat_count = clipboard::paste_bank(&mut self.project_state.project, bank, &src); for pattern in 0..pat_count { self.project_state.mark_dirty(bank, pattern); } if self.editor_ctx.bank == bank { self.load_step_to_editor(); } self.ui.flash("Bank pasted", 150, FlashKind::Success); } } pub fn copy_banks(&mut self, banks: &[usize]) { self.copied_banks = Some(clipboard::copy_banks( &self.project_state.project, banks, )); let n = banks.len(); self.ui.flash( &format!("{n} bank{} copied", if n == 1 { "" } else { "s" }), 150, FlashKind::Success, ); } pub fn paste_banks(&mut self, start: usize) { if let Some(sources) = self.copied_banks.clone() { let count = clipboard::paste_banks( &mut self.project_state.project, start, &sources, ); for i in 0..count { let bank = start + i; for pattern in 0..model::MAX_PATTERNS { self.project_state.mark_dirty(bank, pattern); } } if (start..start + count).contains(&self.editor_ctx.bank) { self.load_step_to_editor(); } self.ui.flash( &format!("{count} bank{} pasted", if count == 1 { "" } else { "s" }), 150, FlashKind::Success, ); } } pub fn reset_patterns(&mut self, bank: usize, patterns: &[usize]) { for &pattern in patterns { let edit = pattern_editor::reset_pattern(&mut self.project_state.project, bank, pattern); self.project_state.mark_dirty(edit.bank, edit.pattern); } if self.editor_ctx.bank == bank && patterns.contains(&self.editor_ctx.pattern) { self.load_step_to_editor(); } let n = patterns.len(); self.ui.flash( &format!("{n} pattern{} reset", if n == 1 { "" } else { "s" }), 150, FlashKind::Success, ); } pub fn reset_banks(&mut self, banks: &[usize]) { for &bank in banks { let pat_count = pattern_editor::reset_bank(&mut self.project_state.project, bank); for pattern in 0..pat_count { self.project_state.mark_dirty(bank, pattern); } } if banks.contains(&self.editor_ctx.bank) { self.load_step_to_editor(); } let n = banks.len(); self.ui.flash( &format!("{n} bank{} reset", if n == 1 { "" } else { "s" }), 150, FlashKind::Success, ); } pub fn harden_steps(&mut self) { let (bank, pattern) = self.current_bank_pattern(); let indices = self.selected_steps(); let count = clipboard::harden_steps(&mut self.project_state.project, bank, pattern, &indices); if count == 0 { self.ui.set_status("No linked steps to harden".to_string()); return; } self.project_state.mark_dirty(bank, pattern); self.load_step_to_editor(); self.editor_ctx.clear_selection(); if count == 1 { self.ui.flash("Step hardened", 150, FlashKind::Success); } else { self.ui .flash(&format!("{count} steps hardened"), 150, FlashKind::Success); } } pub fn copy_steps(&mut self) { let (bank, pattern) = self.current_bank_pattern(); let indices = self.selected_steps(); let (copied, scripts) = clipboard::copy_steps( &self.project_state.project, bank, pattern, &indices, ); let count = copied.steps.len(); self.editor_ctx.copied_steps = Some(copied); if let Some(clip) = &mut self.clipboard { let _ = clip.set_text(scripts.join("\n")); } self.ui .flash(&format!("Copied {count} steps"), 150, FlashKind::Info); } pub fn paste_steps(&mut self, link: &LinkState) { let Some(copied) = self.editor_ctx.copied_steps.clone() else { self.ui.set_status("Nothing copied".to_string()); return; }; let (bank, pattern) = self.current_bank_pattern(); let cursor = self.editor_ctx.step; let result = clipboard::paste_steps( &mut self.project_state.project, bank, pattern, cursor, &copied, ); self.project_state.mark_dirty(bank, pattern); self.load_step_to_editor(); for &target in &result.compile_targets { let saved = self.editor_ctx.step; self.editor_ctx.step = target; self.compile_current_step(link); self.editor_ctx.step = saved; } self.editor_ctx.clear_selection(); self.ui.flash( &format!("Pasted {} steps", result.count), 150, FlashKind::Success, ); } pub fn link_paste_steps(&mut self) { let Some(copied) = self.editor_ctx.copied_steps.clone() else { self.ui.set_status("Nothing copied".to_string()); return; }; let (bank, pattern) = self.current_bank_pattern(); let cursor = self.editor_ctx.step; match clipboard::link_paste_steps( &mut self.project_state.project, bank, pattern, cursor, &copied, ) { None => { self.ui .set_status("Can only link within same pattern".to_string()); } Some(count) => { self.project_state.mark_dirty(bank, pattern); self.load_step_to_editor(); self.editor_ctx.clear_selection(); self.ui.flash( &format!("Linked {count} steps"), 150, FlashKind::Success, ); } } } pub fn duplicate_steps(&mut self, link: &LinkState) { let (bank, pattern) = self.current_bank_pattern(); let indices = self.selected_steps(); let result = clipboard::duplicate_steps( &mut self.project_state.project, bank, pattern, &indices, ); self.project_state.mark_dirty(bank, pattern); self.load_step_to_editor(); for &target in &result.compile_targets { let saved = self.editor_ctx.step; self.editor_ctx.step = target; self.compile_current_step(link); self.editor_ctx.step = saved; } self.editor_ctx.clear_selection(); self.ui.flash( &format!("Duplicated {} steps", result.count), 150, FlashKind::Success, ); } pub fn open_pattern_modal(&mut self, field: PatternField) { let current = match field { PatternField::Length => self.current_edit_pattern().length.to_string(), PatternField::Speed => self.current_edit_pattern().speed.label().to_string(), }; self.ui.modal = Modal::SetPattern { field, input: current, }; } pub fn open_pattern_props_modal(&mut self, bank: usize, pattern: usize) { let pat = self.project_state.project.pattern_at(bank, pattern); self.ui.modal = Modal::PatternProps { bank, pattern, field: PatternPropsField::default(), name: pat.name.clone().unwrap_or_default(), length: pat.length.to_string(), speed: pat.speed, quantization: pat.quantization, sync_mode: pat.sync_mode, }; } pub fn dispatch(&mut self, cmd: AppCommand, link: &LinkState, snapshot: &SequencerSnapshot) { match cmd { // Playback AppCommand::TogglePlaying => self.toggle_playing(link), AppCommand::TempoUp => self.tempo_up(link), AppCommand::TempoDown => self.tempo_down(link), // Navigation AppCommand::NextStep => self.next_step(), AppCommand::PrevStep => self.prev_step(), AppCommand::StepUp => self.step_up(), AppCommand::StepDown => self.step_down(), // Pattern editing AppCommand::ToggleSteps => self.toggle_steps(), AppCommand::LengthIncrease => self.length_increase(), AppCommand::LengthDecrease => self.length_decrease(), AppCommand::SpeedIncrease => self.speed_increase(), AppCommand::SpeedDecrease => self.speed_decrease(), AppCommand::SetLength { bank, pattern, length, } => { let (change, new_len) = pattern_editor::set_length( &mut self.project_state.project, bank, pattern, length, ); if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern && self.editor_ctx.step >= new_len { self.editor_ctx.step = new_len - 1; } self.project_state.mark_dirty(change.bank, change.pattern); } AppCommand::SetSpeed { bank, pattern, speed, } => { let change = pattern_editor::set_speed( &mut self.project_state.project, bank, pattern, speed, ); self.project_state.mark_dirty(change.bank, change.pattern); } // Script editing AppCommand::SaveEditorToStep => self.save_editor_to_step(), AppCommand::CompileCurrentStep => self.compile_current_step(link), AppCommand::DeleteStep { bank, pattern, step, } => { self.delete_step(bank, pattern, step); } AppCommand::DeleteSteps { bank, pattern, steps, } => { self.delete_steps(bank, pattern, &steps); } AppCommand::ResetPattern { bank, pattern } => { self.reset_pattern(bank, pattern); } AppCommand::ResetBank { bank } => { self.reset_bank(bank); } AppCommand::CopyPattern { bank, pattern } => { self.copy_pattern(bank, pattern); } AppCommand::PastePattern { bank, pattern } => { self.paste_pattern(bank, pattern); } AppCommand::CopyPatterns { bank, patterns } => { self.copy_patterns(bank, &patterns); } AppCommand::PastePatterns { bank, start } => { self.paste_patterns(bank, start); } AppCommand::CopyBank { bank } => { self.copy_bank(bank); } AppCommand::PasteBank { bank } => { self.paste_bank(bank); } AppCommand::CopyBanks { banks } => { self.copy_banks(&banks); } AppCommand::PasteBanks { start } => { self.paste_banks(start); } AppCommand::ResetPatterns { bank, patterns } => { self.reset_patterns(bank, &patterns); } AppCommand::ResetBanks { banks } => { self.reset_banks(&banks); } // Clipboard AppCommand::HardenSteps => self.harden_steps(), AppCommand::CopySteps => self.copy_steps(), AppCommand::PasteSteps => self.paste_steps(link), AppCommand::LinkPasteSteps => self.link_paste_steps(), AppCommand::DuplicateSteps => self.duplicate_steps(link), // Pattern playback (staging) AppCommand::ClearStagedChanges => { self.clear_staged_changes(); } // Project AppCommand::RenameBank { bank, name } => { self.project_state.project.banks[bank].name = name; } AppCommand::RenamePattern { bank, pattern, name, } => { self.project_state.project.banks[bank].patterns[pattern].name = name; } AppCommand::RenameStep { bank, pattern, step, name, } => { if let Some(s) = self.project_state.project.banks[bank].patterns[pattern].step_mut(step) { s.name = name; } self.project_state.mark_dirty(bank, pattern); } AppCommand::Save(path) => self.save(path, link, snapshot), AppCommand::Load(path) => self.load(path, link), // UI AppCommand::SetStatus(msg) => self.ui.set_status(msg), AppCommand::ClearStatus => self.ui.clear_status(), AppCommand::OpenModal(modal) => { if matches!(modal, Modal::Editor) { // If current step is a shallow copy, navigate to source step let pattern = &self.project_state.project.banks[self.editor_ctx.bank].patterns [self.editor_ctx.pattern]; if let Some(source) = pattern.step(self.editor_ctx.step).and_then(|s| s.source) { self.editor_ctx.step = source as usize; } self.load_step_to_editor(); } self.ui.modal = modal; } AppCommand::CloseModal => self.ui.modal = Modal::None, AppCommand::OpenPatternModal(field) => self.open_pattern_modal(field), AppCommand::OpenPatternPropsModal { bank, pattern } => { self.open_pattern_props_modal(bank, pattern); } AppCommand::StagePatternProps { bank, pattern, name, length, speed, quantization, sync_mode, } => { self.playback.staged_prop_changes.insert( (bank, pattern), StagedPropChange { name, length, speed, quantization, sync_mode, }, ); self.ui.set_status(format!( "B{:02}:P{:02} props staged", bank + 1, pattern + 1 )); } // Page navigation AppCommand::PageLeft => self.page.left(), AppCommand::PageRight => self.page.right(), AppCommand::PageUp => self.page.up(), AppCommand::PageDown => self.page.down(), AppCommand::GoToPage(page) => self.page = page, // Help navigation AppCommand::HelpToggleFocus => help_nav::toggle_focus(&mut self.ui), AppCommand::HelpNextTopic(n) => help_nav::next_topic(&mut self.ui, n), AppCommand::HelpPrevTopic(n) => help_nav::prev_topic(&mut self.ui, n), AppCommand::HelpScrollDown(n) => help_nav::scroll_down(&mut self.ui, n), AppCommand::HelpScrollUp(n) => help_nav::scroll_up(&mut self.ui, n), AppCommand::HelpActivateSearch => help_nav::activate_search(&mut self.ui), AppCommand::HelpClearSearch => help_nav::clear_search(&mut self.ui), AppCommand::HelpSearchInput(c) => help_nav::search_input(&mut self.ui, c), AppCommand::HelpSearchBackspace => help_nav::search_backspace(&mut self.ui), AppCommand::HelpSearchConfirm => help_nav::search_confirm(&mut self.ui), // Dictionary navigation AppCommand::DictToggleFocus => dict_nav::toggle_focus(&mut self.ui), AppCommand::DictNextCategory => dict_nav::next_category(&mut self.ui), AppCommand::DictPrevCategory => dict_nav::prev_category(&mut self.ui), AppCommand::DictScrollDown(n) => dict_nav::scroll_down(&mut self.ui, n), AppCommand::DictScrollUp(n) => dict_nav::scroll_up(&mut self.ui, n), AppCommand::DictActivateSearch => dict_nav::activate_search(&mut self.ui), AppCommand::DictClearSearch => dict_nav::clear_search(&mut self.ui), AppCommand::DictSearchInput(c) => dict_nav::search_input(&mut self.ui, c), AppCommand::DictSearchBackspace => dict_nav::search_backspace(&mut self.ui), AppCommand::DictSearchConfirm => dict_nav::search_confirm(&mut self.ui), // Patterns view AppCommand::PatternsCursorLeft => { self.patterns_nav.move_left(); } AppCommand::PatternsCursorRight => { self.patterns_nav.move_right(); } AppCommand::PatternsCursorUp => { self.patterns_nav.move_up(); } AppCommand::PatternsCursorDown => { self.patterns_nav.move_down(); } AppCommand::PatternsEnter => { let bank = self.patterns_nav.selected_bank(); let pattern = self.patterns_nav.selected_pattern(); self.select_edit_bank(bank); self.select_edit_pattern(pattern); self.page.down(); } AppCommand::PatternsBack => { self.page.down(); } // Mute/Solo (staged) AppCommand::StageMute { bank, pattern } => { self.playback.stage_mute(bank, pattern); } AppCommand::StageSolo { bank, pattern } => { self.playback.stage_solo(bank, pattern); } AppCommand::ClearMutes => { self.playback.clear_staged_mutes(); self.mute.clear_mute(); } AppCommand::ClearSolos => { self.playback.clear_staged_solos(); self.mute.clear_solo(); } // UI state AppCommand::ClearMinimap => { self.ui.minimap_until = None; } AppCommand::HideTitle => { self.ui.show_title = false; } AppCommand::ToggleEditorStack => { self.editor_ctx.show_stack = !self.editor_ctx.show_stack; if self.editor_ctx.show_stack { crate::services::stack_preview::update_cache(&self.editor_ctx); } } AppCommand::SetColorScheme(scheme) => { self.ui.color_scheme = scheme; let base_theme = scheme.to_theme(); let rotated = cagire_ratatui::theme::transform::rotate_theme(base_theme, self.ui.hue_rotation); crate::theme::set(rotated); } AppCommand::SetHueRotation(degrees) => { self.ui.hue_rotation = degrees; let base_theme = self.ui.color_scheme.to_theme(); let rotated = cagire_ratatui::theme::transform::rotate_theme(base_theme, degrees); crate::theme::set(rotated); } AppCommand::ToggleRuntimeHighlight => { self.ui.runtime_highlight = !self.ui.runtime_highlight; } AppCommand::ToggleCompletion => { self.ui.show_completion = !self.ui.show_completion; self.editor_ctx .editor .set_completion_enabled(self.ui.show_completion); } // Live keys AppCommand::ToggleLiveKeysFill => { self.live_keys.flip_fill(); } // Panel AppCommand::ClosePanel => { self.panel.visible = false; self.panel.focus = crate::state::PanelFocus::Main; } // Selection AppCommand::SetSelectionAnchor(step) => { self.editor_ctx.selection_anchor = Some(step); } // Audio settings (engine page) AppCommand::AudioNextSection => { self.audio.next_section(); } AppCommand::AudioPrevSection => { self.audio.prev_section(); } AppCommand::AudioOutputListUp => { self.audio.output_list.move_up(); } AppCommand::AudioOutputListDown(count) => { self.audio.output_list.move_down(count); } AppCommand::AudioOutputPageUp => { self.audio.output_list.page_up(); } AppCommand::AudioOutputPageDown(count) => { self.audio.output_list.page_down(count); } AppCommand::AudioInputListUp => { self.audio.input_list.move_up(); } AppCommand::AudioInputListDown(count) => { self.audio.input_list.move_down(count); } AppCommand::AudioInputPageDown(count) => { self.audio.input_list.page_down(count); } AppCommand::AudioSettingNext => { self.audio.setting_kind = self.audio.setting_kind.next(); } AppCommand::AudioSettingPrev => { self.audio.setting_kind = self.audio.setting_kind.prev(); } AppCommand::SetOutputDevice(name) => { self.audio.config.output_device = Some(name); } AppCommand::SetInputDevice(name) => { self.audio.config.input_device = Some(name); } AppCommand::SetDeviceKind(kind) => { self.audio.device_kind = kind; } AppCommand::AdjustAudioSetting { setting, delta } => { use crate::state::SettingKind; match setting { SettingKind::Channels => self.audio.adjust_channels(delta as i16), SettingKind::BufferSize => self.audio.adjust_buffer_size(delta), SettingKind::Polyphony => self.audio.adjust_max_voices(delta), SettingKind::Nudge => { self.metrics.nudge_ms = (self.metrics.nudge_ms + delta as f64).clamp(-50.0, 50.0); } } } AppCommand::AudioTriggerRestart => { self.audio.trigger_restart(); } AppCommand::RemoveLastSamplePath => { self.audio.remove_last_sample_path(); } AppCommand::AudioRefreshDevices => { self.audio.refresh_devices(); } // Options page AppCommand::OptionsNextFocus => { self.options.next_focus(); } AppCommand::OptionsPrevFocus => { self.options.prev_focus(); } AppCommand::ToggleRefreshRate => { self.audio.toggle_refresh_rate(); } AppCommand::ToggleScope => { self.audio.config.show_scope = !self.audio.config.show_scope; } AppCommand::ToggleSpectrum => { self.audio.config.show_spectrum = !self.audio.config.show_spectrum; } // Metrics AppCommand::ResetPeakVoices => { self.metrics.peak_voices = 0; } // Euclidean distribution AppCommand::ApplyEuclideanDistribution { bank, pattern, source_step, pulses, steps, rotation, } => { let targets = euclidean::apply_distribution( &mut self.project_state.project, bank, pattern, source_step, pulses, steps, rotation, ); let created_count = targets.len(); self.project_state.mark_dirty(bank, pattern); for &target in &targets { let saved = self.editor_ctx.step; self.editor_ctx.step = target; self.compile_current_step(link); self.editor_ctx.step = saved; } self.load_step_to_editor(); self.ui.flash( &format!("Created {created_count} linked steps (E({pulses},{steps}))"), 200, 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(), } } pub fn flush_queued_changes(&mut self, cmd_tx: &Sender) { for staged in self.playback.queued_changes.drain(..) { match staged.change { PatternChange::Start { bank, pattern } => { let _ = cmd_tx.send(SeqCommand::PatternStart { bank, pattern, quantization: staged.quantization, sync_mode: staged.sync_mode, }); } PatternChange::Stop { bank, pattern } => { let _ = cmd_tx.send(SeqCommand::PatternStop { bank, pattern, quantization: staged.quantization, }); } } } } pub fn send_mute_state(&self, cmd_tx: &Sender) { let _ = cmd_tx.send(SeqCommand::SetMuteState { muted: self.mute.muted.clone(), soloed: self.mute.soloed.clone(), }); } pub fn flush_dirty_patterns(&mut self, cmd_tx: &Sender) { for (bank, pattern) in self.project_state.take_dirty() { let pat = self.project_state.project.pattern_at(bank, pattern); let snapshot = PatternSnapshot { speed: pat.speed, length: pat.length, steps: pat .steps .iter() .take(pat.length) .map(|s| StepSnapshot { active: s.active, script: s.script.clone(), source: s.source, }) .collect(), quantization: pat.quantization, sync_mode: pat.sync_mode, }; let _ = cmd_tx.send(SeqCommand::PatternUpdate { bank, pattern, data: snapshot, }); } } }