use rand::rngs::StdRng; use rand::SeedableRng; use std::collections::HashMap; use std::path::PathBuf; use std::sync::{Arc, Mutex}; use crossbeam_channel::Sender; use crate::commands::AppCommand; use crate::engine::{ LinkState, PatternChange, PatternSnapshot, SeqCommand, SequencerSnapshot, StepSnapshot, }; use crate::model::{self, Bank, Dictionary, Pattern, Rng, ScriptEngine, StepContext, Variables}; use crate::page::Page; use crate::services::pattern_editor; use crate::settings::Settings; use crate::state::{ AudioSettings, EditorContext, Focus, LiveKeyState, Metrics, Modal, PatternField, PatternsNav, PlaybackState, ProjectState, UiState, }; use crate::views::doc_view; const STEPS_PER_PAGE: usize = 32; pub struct App { pub project_state: ProjectState, pub ui: UiState, pub playback: PlaybackState, pub page: Page, pub editor_ctx: EditorContext, pub patterns_nav: PatternsNav, pub metrics: Metrics, pub script_engine: ScriptEngine, pub variables: Variables, pub dict: Dictionary, pub rng: Rng, pub live_keys: Arc, pub clipboard: Option, pub copied_pattern: Option, pub copied_bank: Option, pub audio: AudioSettings, } impl App { pub fn new() -> Self { let variables = Arc::new(Mutex::new(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(), 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_pattern: None, copied_bank: None, audio: AudioSettings::default(), } } 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, }, 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, }, link: crate::settings::LinkSettings { enabled: link.is_enabled(), tempo: link.tempo(), quantum: link.quantum(), }, }; settings.save(); } fn current_bank_pattern(&self) -> (usize, usize) { (self.editor_ctx.bank, self.editor_ctx.pattern) } pub fn mark_all_patterns_dirty(&mut self) { self.project_state.mark_all_dirty(); } pub fn toggle_playing(&mut self) { self.playback.toggle(); } 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 toggle_focus(&mut self, link: &LinkState) { match self.editor_ctx.focus { Focus::Sequencer => { self.editor_ctx.focus = Focus::Editor; self.load_step_to_editor(); } Focus::Editor => { self.save_editor_to_step(); self.compile_current_step(link); self.editor_ctx.focus = Focus::Sequencer; } } } 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_step(&mut self) { let (bank, pattern) = self.current_bank_pattern(); let change = pattern_editor::toggle_step( &mut self.project_state.project, bank, pattern, self.editor_ctx.step, ); self.project_state.mark_dirty(change.bank, change.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 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.text = tui_textarea::TextArea::new(lines); } } pub fn save_editor_to_step(&mut self) { let text = self.editor_ctx.text.lines().join("\n"); 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 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() { if let Some(step) = self .project_state .project .pattern_at_mut(bank, pattern) .step_mut(step_idx) { step.command = None; } return; } let speed = self .project_state .project .pattern_at(bank, pattern) .speed .multiplier(); let ctx = StepContext { step: step_idx, beat: link.beat(), pattern, tempo: link.tempo(), phase: link.phase(), slot: 0, runs: 0, iter: 0, speed, fill: false, }; match self.script_engine.evaluate(&script, &ctx) { Ok(cmds) => { if let Some(step) = self .project_state .project .pattern_at_mut(bank, pattern) .step_mut(step_idx) { step.command = if cmds.is_empty() { None } else { Some(cmds.join("\n")) }; } self.ui.flash("Script compiled", 150); } Err(e) => { if let Some(step) = self .project_state .project .pattern_at_mut(bank, pattern) .step_mut(step_idx) { step.command = None; } self.ui.set_status(format!("Script error: {e}")); } } } 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() { if let Some(step) = self .project_state .project .pattern_at_mut(bank, pattern) .step_mut(step_idx) { step.command = None; } continue; } let speed = self .project_state .project .pattern_at(bank, pattern) .speed .multiplier(); let ctx = StepContext { step: step_idx, beat: 0.0, pattern, tempo: link.tempo(), phase: 0.0, slot: 0, runs: 0, iter: 0, speed, fill: false, }; if let Ok(cmds) = self.script_engine.evaluate(&script, &ctx) { if let Some(step) = self .project_state .project .pattern_at_mut(bank, pattern) .step_mut(step_idx) { step.command = if cmds.is_empty() { None } else { Some(cmds.join("\n")) }; } } } } pub fn toggle_pattern_playback( &mut self, bank: usize, pattern: usize, snapshot: &SequencerSnapshot, ) { let is_playing = snapshot.is_playing(bank, pattern); let pending = self .playback .queued_changes .iter() .position(|c| c.pattern_id().bank == bank && c.pattern_id().pattern == pattern); if let Some(idx) = pending { self.playback.queued_changes.remove(idx); self.ui.set_status(format!( "B{:02}:P{:02} change cancelled", bank + 1, pattern + 1 )); } else if is_playing { self.playback .queued_changes .push(PatternChange::Stop { bank, pattern }); self.ui.set_status(format!( "B{:02}:P{:02} queued to stop", bank + 1, pattern + 1 )); } else { self.playback .queued_changes .push(PatternChange::Start { bank, pattern }); self.ui.set_status(format!( "B{:02}:P{:02} queued to play", bank + 1, pattern + 1 )); } } 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) { self.save_editor_to_step(); self.project_state.project.sample_paths = self.audio.config.sample_paths.clone(); self.project_state.project.tempo = link.tempo(); match model::save(&self.project_state.project, &path) { Ok(()) => { self.ui.set_status(format!("Saved: {}", path.display())); self.project_state.file_path = Some(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; 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.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 copy_step(&mut self) { let (bank, pattern) = self.current_bank_pattern(); let step = self.editor_ctx.step; let script = pattern_editor::get_step_script(&self.project_state.project, bank, pattern, step); if let Some(script) = script { if let Some(clip) = &mut self.clipboard { if clip.set_text(&script).is_ok() { self.editor_ctx.copied_step = Some(crate::state::CopiedStep { bank, pattern, step, }); self.ui.set_status("Copied".to_string()); } } } } pub fn delete_step(&mut self, bank: usize, pattern: usize, step: usize) { let pat = self.project_state.project.pattern_at_mut(bank, pattern); for s in &mut pat.steps { if s.source == Some(step) { s.source = None; s.script.clear(); s.command = None; } } let change = pattern_editor::set_step_script( &mut self.project_state.project, bank, pattern, step, String::new(), ); if let Some(s) = self .project_state .project .pattern_at_mut(bank, pattern) .step_mut(step) { s.command = None; s.source = None; } self.project_state.mark_dirty(change.bank, change.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); } pub fn reset_pattern(&mut self, bank: usize, pattern: usize) { self.project_state.project.banks[bank].patterns[pattern] = Pattern::default(); 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 reset", 150); } pub fn reset_bank(&mut self, bank: usize) { self.project_state.project.banks[bank] = Bank::default(); for pattern in 0..self.project_state.project.banks[bank].patterns.len() { self.project_state.mark_dirty(bank, pattern); } if self.editor_ctx.bank == bank { self.load_step_to_editor(); } self.ui.flash("Bank reset", 150); } pub fn copy_pattern(&mut self, bank: usize, pattern: usize) { let pat = self.project_state.project.banks[bank].patterns[pattern].clone(); self.copied_pattern = Some(pat); self.ui.flash("Pattern copied", 150); } pub fn paste_pattern(&mut self, bank: usize, pattern: usize) { if let Some(src) = &self.copied_pattern { let mut pat = src.clone(); pat.name = match &src.name { Some(name) if !name.ends_with(" (copy)") => Some(format!("{name} (copy)")), Some(name) => Some(name.clone()), None => Some("(copy)".to_string()), }; self.project_state.project.banks[bank].patterns[pattern] = pat; 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); } } pub fn copy_bank(&mut self, bank: usize) { let b = self.project_state.project.banks[bank].clone(); self.copied_bank = Some(b); self.ui.flash("Bank copied", 150); } pub fn paste_bank(&mut self, bank: usize) { if let Some(src) = &self.copied_bank { let mut b = src.clone(); b.name = match &src.name { Some(name) if !name.ends_with(" (copy)") => Some(format!("{name} (copy)")), Some(name) => Some(name.clone()), None => Some("(copy)".to_string()), }; self.project_state.project.banks[bank] = b; for pattern in 0..self.project_state.project.banks[bank].patterns.len() { self.project_state.mark_dirty(bank, pattern); } if self.editor_ctx.bank == bank { self.load_step_to_editor(); } self.ui.flash("Bank pasted", 150); } } pub fn paste_step(&mut self, link: &LinkState) { let text = self .clipboard .as_mut() .and_then(|clip| clip.get_text().ok()); if let Some(text) = text { 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); self.load_step_to_editor(); self.compile_current_step(link); } } pub fn link_paste_step(&mut self) { let Some(copied) = self.editor_ctx.copied_step else { self.ui.set_status("Nothing copied".to_string()); return; }; let (bank, pattern) = self.current_bank_pattern(); let step = self.editor_ctx.step; if copied.bank != bank || copied.pattern != pattern { self.ui .set_status("Can only link within same pattern".to_string()); return; } if copied.step == step { self.ui.set_status("Cannot link step to itself".to_string()); return; } let source_step = self .project_state .project .pattern_at(bank, pattern) .step(copied.step); if source_step.map(|s| s.source.is_some()).unwrap_or(false) { self.ui .set_status("Cannot link to a linked step".to_string()); return; } if let Some(s) = self .project_state .project .pattern_at_mut(bank, pattern) .step_mut(step) { s.source = Some(copied.step); s.script.clear(); s.command = None; } self.project_state.mark_dirty(bank, pattern); self.load_step_to_editor(); self.ui .flash(&format!("Linked to step {:02}", copied.step + 1), 150); } pub fn harden_step(&mut self) { let (bank, pattern) = self.current_bank_pattern(); let step = self.editor_ctx.step; let resolved_script = self .project_state .project .pattern_at(bank, pattern) .resolve_script(step) .map(|s| s.to_string()); let Some(script) = resolved_script else { return; }; if let Some(s) = self .project_state .project .pattern_at_mut(bank, pattern) .step_mut(step) { if s.source.is_none() { self.ui.set_status("Step is not linked".to_string()); return; } s.source = None; s.script = script; } self.project_state.mark_dirty(bank, pattern); self.load_step_to_editor(); self.ui.flash("Step hardened", 150); } 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 dispatch(&mut self, cmd: AppCommand, link: &LinkState, snapshot: &SequencerSnapshot) { match cmd { // Playback AppCommand::TogglePlaying => self.toggle_playing(), 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(), AppCommand::ToggleFocus => self.toggle_focus(link), AppCommand::SelectEditBank(bank) => self.select_edit_bank(bank), AppCommand::SelectEditPattern(pattern) => self.select_edit_pattern(pattern), // Pattern editing AppCommand::ToggleStep => self.toggle_step(), 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::CompileAllSteps => self.compile_all_steps(link), AppCommand::DeleteStep { bank, pattern, step, } => { self.delete_step(bank, pattern, step); } 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::CopyBank { bank } => { self.copy_bank(bank); } AppCommand::PasteBank { bank } => { self.paste_bank(bank); } // Clipboard AppCommand::CopyStep => self.copy_step(), AppCommand::PasteStep => self.paste_step(link), AppCommand::LinkPasteStep => self.link_paste_step(), AppCommand::HardenStep => self.harden_step(), // Pattern playback AppCommand::QueuePatternChange(change) => { self.playback.queued_changes.push(change); } AppCommand::TogglePatternPlayback { bank, pattern } => { self.toggle_pattern_playback(bank, pattern, snapshot); } // 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::Save(path) => self.save(path, link), AppCommand::Load(path) => self.load(path, link), // UI AppCommand::SetStatus(msg) => self.ui.set_status(msg), AppCommand::ClearStatus => self.ui.clear_status(), AppCommand::Flash { message, duration_ms, } => self.ui.flash(&message, duration_ms), 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.steps[self.editor_ctx.step].source { self.editor_ctx.step = source; } self.load_step_to_editor(); } self.ui.modal = modal; } AppCommand::CloseModal => self.ui.modal = Modal::None, AppCommand::OpenPatternModal(field) => self.open_pattern_modal(field), // Page navigation AppCommand::PageLeft => self.page.left(), AppCommand::PageRight => self.page.right(), AppCommand::PageUp => self.page.up(), AppCommand::PageDown => self.page.down(), // Doc navigation AppCommand::DocNextTopic => { self.ui.doc_topic = (self.ui.doc_topic + 1) % doc_view::topic_count(); self.ui.doc_scroll = 0; self.ui.doc_category = 0; } AppCommand::DocPrevTopic => { let count = doc_view::topic_count(); self.ui.doc_topic = (self.ui.doc_topic + count - 1) % count; self.ui.doc_scroll = 0; self.ui.doc_category = 0; } AppCommand::DocScrollDown(n) => { self.ui.doc_scroll = self.ui.doc_scroll.saturating_add(n); } AppCommand::DocScrollUp(n) => { self.ui.doc_scroll = self.ui.doc_scroll.saturating_sub(n); } AppCommand::DocNextCategory => { let count = doc_view::category_count(); self.ui.doc_category = (self.ui.doc_category + 1) % count; self.ui.doc_scroll = 0; } AppCommand::DocPrevCategory => { let count = doc_view::category_count(); self.ui.doc_category = (self.ui.doc_category + count - 1) % count; self.ui.doc_scroll = 0; } // 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(); } AppCommand::PatternsTogglePlay => { let bank = self.patterns_nav.selected_bank(); let pattern = self.patterns_nav.selected_pattern(); self.toggle_pattern_playback(bank, pattern, snapshot); } } } pub fn flush_queued_changes(&mut self, cmd_tx: &Sender) { for change in self.playback.queued_changes.drain(..) { match change { PatternChange::Start { bank, pattern } => { let _ = cmd_tx.send(SeqCommand::PatternStart { bank, pattern }); } PatternChange::Stop { bank, pattern } => { let _ = cmd_tx.send(SeqCommand::PatternStop { bank, pattern }); } } } } 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(), }; let _ = cmd_tx.send(SeqCommand::PatternUpdate { bank, pattern, data: snapshot, }); } } }