From 3d552ec072e5833ca39de18eb53835c8f73927f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Sun, 22 Feb 2026 13:28:03 +0100 Subject: [PATCH] Feat: cleanup --- crates/forth/src/compiler.rs | 2 + crates/forth/src/lib.rs | 2 + crates/forth/src/ops.rs | 2 + crates/forth/src/types.rs | 2 + crates/forth/src/vm.rs | 4 +- crates/project/src/lib.rs | 2 + crates/project/src/project.rs | 2 + crates/ratatui/src/lib.rs | 2 + src/app/clipboard.rs | 62 +++++++++++++++----------- src/app/dispatch.rs | 10 ++--- src/app/mod.rs | 8 ++-- src/app/staging.rs | 17 +++---- src/app/undo.rs | 12 +++-- src/commands.rs | 2 + src/engine/audio.rs | 8 ++-- src/engine/mod.rs | 23 ++++++---- src/engine/sequencer.rs | 4 +- src/init.rs | 1 + src/input/mod.rs | 2 + src/input/modal.rs | 27 +++++++----- src/model/mod.rs | 4 ++ src/services/clipboard.rs | 12 ++--- src/state/modal.rs | 4 +- src/views/highlight.rs | 14 +++--- src/views/main_view.rs | 83 +++++++++++------------------------ src/views/render.rs | 83 +++++++++++++++++++++-------------- 26 files changed, 213 insertions(+), 181 deletions(-) diff --git a/crates/forth/src/compiler.rs b/crates/forth/src/compiler.rs index acc144d..1ab4bc2 100644 --- a/crates/forth/src/compiler.rs +++ b/crates/forth/src/compiler.rs @@ -1,3 +1,5 @@ +//! Single-pass compiler from Forth source text to Op sequences. + use std::borrow::Cow; use std::sync::Arc; diff --git a/crates/forth/src/lib.rs b/crates/forth/src/lib.rs index 1f37c50..2f72020 100644 --- a/crates/forth/src/lib.rs +++ b/crates/forth/src/lib.rs @@ -1,3 +1,5 @@ +//! Forth virtual machine for the Cagire music sequencer. + mod compiler; mod ops; mod theory; diff --git a/crates/forth/src/ops.rs b/crates/forth/src/ops.rs index c9e6a27..df9251e 100644 --- a/crates/forth/src/ops.rs +++ b/crates/forth/src/ops.rs @@ -1,3 +1,5 @@ +//! Compiled operation variants for the Forth VM instruction set. + use std::sync::Arc; use super::types::SourceSpan; diff --git a/crates/forth/src/types.rs b/crates/forth/src/types.rs index 27b79a1..82003dc 100644 --- a/crates/forth/src/types.rs +++ b/crates/forth/src/types.rs @@ -1,3 +1,5 @@ +//! Core types for the Forth VM: values, execution context, and shared state. + use arc_swap::ArcSwap; use parking_lot::Mutex; use rand::rngs::StdRng; diff --git a/crates/forth/src/vm.rs b/crates/forth/src/vm.rs index 03e1bc6..7aa2fb8 100644 --- a/crates/forth/src/vm.rs +++ b/crates/forth/src/vm.rs @@ -1,3 +1,5 @@ +//! Stack-based Forth interpreter with audio command generation. + use parking_lot::Mutex; use rand::rngs::StdRng; use rand::{Rng as RngTrait, SeedableRng}; @@ -29,12 +31,10 @@ impl Forth { } } - #[allow(dead_code)] pub fn stack(&self) -> Vec { self.stack.lock().clone() } - #[allow(dead_code)] pub fn clear_stack(&self) { self.stack.lock().clear(); } diff --git a/crates/project/src/lib.rs b/crates/project/src/lib.rs index d4d4228..2667e9f 100644 --- a/crates/project/src/lib.rs +++ b/crates/project/src/lib.rs @@ -1,3 +1,5 @@ +//! Project data model: banks, patterns, and steps for the Cagire sequencer. + mod file; mod project; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 819405c..4e71c4d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1,3 +1,5 @@ +//! Project, Bank, Pattern, and Step structs with serialization. + use std::path::PathBuf; use serde::{Deserialize, Deserializer, Serialize, Serializer}; diff --git a/crates/ratatui/src/lib.rs b/crates/ratatui/src/lib.rs index 4e8cb95..487bfc6 100644 --- a/crates/ratatui/src/lib.rs +++ b/crates/ratatui/src/lib.rs @@ -1,3 +1,5 @@ +//! Reusable TUI widgets for the Cagire sequencer interface. + mod category_list; mod confirm; mod editor; diff --git a/src/app/clipboard.rs b/src/app/clipboard.rs index e9c5c8e..cc1bb05 100644 --- a/src/app/clipboard.rs +++ b/src/app/clipboard.rs @@ -15,14 +15,16 @@ impl App { } 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(); + if let Some(patterns) = self.copied_patterns.take() { + if let Some(src) = patterns.first() { + 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); } - self.ui.flash("Pattern pasted", 150, FlashKind::Success); + self.copied_patterns = Some(patterns); } } @@ -41,13 +43,14 @@ impl App { } pub fn paste_patterns(&mut self, bank: usize, start: usize) { - if let Some(sources) = self.copied_patterns.clone() { + if let Some(sources) = self.copied_patterns.take() { let count = clipboard::paste_patterns( &mut self.project_state.project, bank, start, &sources, ); + self.copied_patterns = Some(sources); for i in 0..count { self.project_state.mark_dirty(bank, start + i); } @@ -111,17 +114,19 @@ impl App { } 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 let Some(banks) = self.copied_banks.take() { + if let Some(src) = banks.first() { + 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); } - if self.editor_ctx.bank == bank { - self.load_step_to_editor(); - } - self.ui.flash("Bank pasted", 150, FlashKind::Success); + self.copied_banks = Some(banks); } } @@ -139,12 +144,13 @@ impl App { } pub fn paste_banks(&mut self, start: usize) { - if let Some(sources) = self.copied_banks.clone() { + if let Some(sources) = self.copied_banks.take() { let count = clipboard::paste_banks( &mut self.project_state.project, start, &sources, ); + self.copied_banks = Some(sources); for i in 0..count { let bank = start + i; for pattern in 0..model::MAX_PATTERNS { @@ -184,23 +190,24 @@ impl App { pub fn copy_steps(&mut self) { let (bank, pattern) = self.current_bank_pattern(); let indices = self.selected_steps(); - let (copied, scripts) = clipboard::copy_steps( + let copied = 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")); + let text: String = copied.steps.iter().map(|s| s.script.as_str()).collect::>().join("\n"); + let _ = clip.set_text(text); } + self.editor_ctx.copied_steps = Some(copied); self.ui .flash(&format!("Copied {count} steps"), 150, FlashKind::Info); } pub fn paste_steps(&mut self, link: &crate::engine::LinkState) { - let Some(copied) = self.editor_ctx.copied_steps.clone() else { + let Some(copied) = self.editor_ctx.copied_steps.take() else { self.ui.set_status("Nothing copied".to_string()); return; }; @@ -213,6 +220,7 @@ impl App { cursor, &copied, ); + self.editor_ctx.copied_steps = Some(copied); self.project_state.mark_dirty(bank, pattern); self.load_step_to_editor(); for &target in &result.compile_targets { @@ -230,19 +238,21 @@ impl App { } pub fn link_paste_steps(&mut self) { - let Some(copied) = self.editor_ctx.copied_steps.clone() else { + let Some(copied) = self.editor_ctx.copied_steps.take() 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( + let result = clipboard::link_paste_steps( &mut self.project_state.project, bank, pattern, cursor, &copied, - ) { + ); + self.editor_ctx.copied_steps = Some(copied); + match result { None => { self.ui .set_status("Can only link within same pattern".to_string()); diff --git a/src/app/dispatch.rs b/src/app/dispatch.rs index 29b0740..87a3a6e 100644 --- a/src/app/dispatch.rs +++ b/src/app/dispatch.rs @@ -1,5 +1,8 @@ +//! Routes `AppCommand` variants to the appropriate `App` methods. + use crate::commands::AppCommand; use crate::engine::{LinkState, SequencerSnapshot}; +use crate::model::bp_label; use crate::services::{dict_nav, euclidean, help_nav, pattern_editor}; use crate::state::{undo::UndoEntry, FlashKind, Modal, StagedPropChange}; @@ -193,11 +196,8 @@ impl App { follow_up, }, ); - self.ui.set_status(format!( - "B{:02}:P{:02} props staged", - bank + 1, - pattern + 1 - )); + self.ui + .set_status(format!("{} props staged", bp_label(bank, pattern))); } // Page navigation diff --git a/src/app/mod.rs b/src/app/mod.rs index 3fd2ab4..a64b2dd 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,3 +1,5 @@ +//! Application state: owns the project, editor context, and all UI/playback state. + mod clipboard; mod dispatch; mod editing; @@ -54,8 +56,8 @@ pub struct App { pub script_engine: ScriptEngine, pub variables: Variables, pub dict: Dictionary, - #[allow(dead_code)] - pub rng: Rng, + // Held to keep the Arc alive (shared with ScriptEngine). + pub _rng: Rng, pub live_keys: Arc, pub clipboard: Option, pub copied_patterns: Option>, @@ -108,7 +110,7 @@ impl App { metrics: Metrics::default(), variables, dict, - rng, + _rng: rng, live_keys, script_engine, clipboard: arboard::Clipboard::new().ok(), diff --git a/src/app/staging.rs b/src/app/staging.rs index 0d635b3..0f890f2 100644 --- a/src/app/staging.rs +++ b/src/app/staging.rs @@ -1,4 +1,5 @@ use crate::engine::{PatternChange, SequencerSnapshot}; +use crate::model::bp_label; use crate::state::StagedChange; use super::App; @@ -20,29 +21,23 @@ impl App { 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)); + .set_status(format!("{} unstaged", bp_label(bank, pattern))); } 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 - )); + self.ui + .set_status(format!("{} staged to stop", bp_label(bank, pattern))); } 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 - )); + self.ui + .set_status(format!("{} staged to play", bp_label(bank, pattern))); } } diff --git a/src/app/undo.rs b/src/app/undo.rs index 6f8967b..ea46033 100644 --- a/src/app/undo.rs +++ b/src/app/undo.rs @@ -64,15 +64,19 @@ impl App { let cursor = (self.editor_ctx.bank, self.editor_ctx.pattern, self.editor_ctx.step); let reverse_scope = match entry.scope { UndoScope::Pattern { bank, pattern, data } => { - let current = self.project_state.project.pattern_at(bank, pattern).clone(); - *self.project_state.project.pattern_at_mut(bank, pattern) = data; + let current = std::mem::replace( + self.project_state.project.pattern_at_mut(bank, pattern), + data, + ); self.project_state.mark_dirty(bank, pattern); UndoScope::Pattern { bank, pattern, data: current } } UndoScope::Bank { bank, data } => { - let current = self.project_state.project.banks[bank].clone(); + let current = std::mem::replace( + &mut self.project_state.project.banks[bank], + data, + ); let pat_count = current.patterns.len(); - self.project_state.project.banks[bank] = data; for p in 0..pat_count { self.project_state.mark_dirty(bank, p); } diff --git a/src/commands.rs b/src/commands.rs index d1af466..a70a140 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,3 +1,5 @@ +//! All user actions expressed as the `AppCommand` enum, dispatched by `App::dispatch()`. + use std::path::PathBuf; use crate::model::{FollowUp, LaunchQuantization, PatternSpeed, SyncMode}; diff --git a/src/engine/audio.rs b/src/engine/audio.rs index 80e6f50..139df71 100644 --- a/src/engine/audio.rs +++ b/src/engine/audio.rs @@ -1,3 +1,5 @@ +//! Audio output stream (cpal) and FFT spectrum analysis. + use ringbuf::{traits::*, HeapRb}; use rustfft::{num_complex::Complex, FftPlanner}; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; @@ -173,8 +175,8 @@ impl SpectrumAnalyzer { pub struct AnalysisHandle { running: Arc, - #[allow(dead_code)] - thread: Option>, + // Held to keep the thread alive until this handle is dropped. + _thread: Option>, } impl Drop for AnalysisHandle { @@ -202,7 +204,7 @@ pub fn spawn_analysis_thread( let handle = AnalysisHandle { running, - thread: Some(thread), + _thread: Some(thread), }; (producer, handle) diff --git a/src/engine/mod.rs b/src/engine/mod.rs index 8877c3c..d968490 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -7,20 +7,27 @@ mod timing; pub use timing::{substeps_in_window, StepTiming, SyncTime}; -// Used by plugin and desktop crates via the lib; not by the terminal binary directly. +pub use audio::{preload_sample_heads, AnalysisHandle, ScopeBuffer, SpectrumBuffer}; + +// Re-exported for the plugin crate (not used by the terminal binary). #[allow(unused_imports)] -pub use audio::{ - preload_sample_heads, spawn_analysis_thread, AnalysisHandle, ScopeBuffer, SpectrumBuffer, -}; +pub use audio::spawn_analysis_thread; +#[cfg(feature = "cli")] +pub use audio::{build_stream, AudioStreamConfig}; #[cfg(feature = "cli")] #[allow(unused_imports)] -pub use audio::{build_stream, AudioStreamConfig, AudioStreamInfo}; +pub use audio::AudioStreamInfo; pub use link::LinkState; +pub use sequencer::{ + spawn_sequencer, AudioCommand, MidiCommand, PatternChange, PatternSnapshot, SeqCommand, + SequencerConfig, SequencerHandle, SequencerSnapshot, StepSnapshot, +}; + +// Re-exported for the plugin crate (not used by the terminal binary). #[allow(unused_imports)] pub use sequencer::{ - parse_midi_command, spawn_sequencer, AudioCommand, MidiCommand, PatternChange, PatternSnapshot, - SeqCommand, SequencerConfig, SequencerHandle, SequencerSnapshot, SequencerState, - SharedSequencerState, StepSnapshot, TickInput, TickOutput, TimestampedCommand, + parse_midi_command, SequencerState, SharedSequencerState, TickInput, TickOutput, + TimestampedCommand, }; diff --git a/src/engine/sequencer.rs b/src/engine/sequencer.rs index f464c3f..15cfc6c 100644 --- a/src/engine/sequencer.rs +++ b/src/engine/sequencer.rs @@ -1,3 +1,5 @@ +//! Real-time pattern sequencer: evaluates Forth scripts per step and produces audio/MIDI commands. + use arc_swap::ArcSwap; use crossbeam_channel::{bounded, unbounded, Receiver, Sender}; use parking_lot::Mutex; @@ -132,6 +134,7 @@ pub struct PatternSnapshot { pub speed: crate::model::PatternSpeed, pub length: usize, pub steps: Vec, + #[allow(dead_code)] pub quantization: LaunchQuantization, pub sync_mode: SyncMode, pub follow_up: FollowUp, @@ -164,7 +167,6 @@ pub struct SharedSequencerState { pub beat: f64, } -#[allow(dead_code)] pub struct SequencerSnapshot { pub active_patterns: Vec, step_traces: Arc, diff --git a/src/init.rs b/src/init.rs index 7e4e075..cdef963 100644 --- a/src/init.rs +++ b/src/init.rs @@ -26,6 +26,7 @@ pub struct InitArgs { pub buffer: Option, } +// Fields destructured in main.rs (cli) and plugin crate — all are used. #[allow(dead_code)] pub struct Init { pub app: App, diff --git a/src/input/mod.rs b/src/input/mod.rs index 75a5370..a6fc3be 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -1,3 +1,5 @@ +//! Keyboard and mouse input handling — dispatches events to page-specific or modal handlers. + pub(crate) mod engine_page; mod help_page; mod main_page; diff --git a/src/input/modal.rs b/src/input/modal.rs index 9d59817..24eb4e7 100644 --- a/src/input/modal.rs +++ b/src/input/modal.rs @@ -12,9 +12,12 @@ use crate::state::{ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult { match &mut ctx.app.ui.modal { Modal::Confirm { action, selected } => { - let (action, confirmed) = (action.clone(), *selected); + let confirmed = *selected; match key.code { - KeyCode::Char('y') | KeyCode::Char('Y') => return execute_confirm(ctx, &action), + KeyCode::Char('y') | KeyCode::Char('Y') => { + let action = action.clone(); + return execute_confirm(ctx, &action); + } KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => { ctx.dispatch(AppCommand::CloseModal); } @@ -25,6 +28,7 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input } KeyCode::Enter => { if confirmed { + let action = action.clone(); return execute_confirm(ctx, &action); } ctx.dispatch(AppCommand::CloseModal); @@ -35,17 +39,16 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input Modal::FileBrowser(state) => match key.code { KeyCode::Enter => { use crate::state::file_browser::FileBrowserMode; - let mode = state.mode.clone(); + let is_save = matches!(state.mode, FileBrowserMode::Save); if let Some(path) = state.confirm() { ctx.dispatch(AppCommand::CloseModal); - match mode { - FileBrowserMode::Save => ctx.dispatch(AppCommand::Save(path)), - FileBrowserMode::Load => { - let _ = ctx.seq_cmd_tx.send(SeqCommand::StopAll); - let _ = ctx.seq_cmd_tx.send(SeqCommand::ResetScriptState); - ctx.dispatch(AppCommand::Load(path)); - super::load_project_samples(ctx); - } + if is_save { + ctx.dispatch(AppCommand::Save(path)); + } else { + let _ = ctx.seq_cmd_tx.send(SeqCommand::StopAll); + let _ = ctx.seq_cmd_tx.send(SeqCommand::ResetScriptState); + ctx.dispatch(AppCommand::Load(path)); + super::load_project_samples(ctx); } } } @@ -63,7 +66,6 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input _ => {} }, Modal::Rename { target, name } => { - let target = target.clone(); match key.code { KeyCode::Enter => { let new_name = if name.trim().is_empty() { @@ -71,6 +73,7 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input } else { Some(name.clone()) }; + let target = target.clone(); ctx.dispatch(rename_command(&target, new_name)); ctx.dispatch(AppCommand::CloseModal); } diff --git a/src/model/mod.rs b/src/model/mod.rs index b2c309f..841282d 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -12,3 +12,7 @@ pub use cagire_project::{ MAX_BANKS, MAX_PATTERNS, }; pub use script::ScriptEngine; + +pub fn bp_label(bank: usize, pattern: usize) -> String { + format!("B{:02}:P{:02}", bank + 1, pattern + 1) +} diff --git a/src/services/clipboard.rs b/src/services/clipboard.rs index bf3c930..19ff65b 100644 --- a/src/services/clipboard.rs +++ b/src/services/clipboard.rs @@ -117,17 +117,14 @@ pub fn copy_steps( bank: usize, pattern: usize, indices: &[usize], -) -> (CopiedSteps, Vec) { +) -> CopiedSteps { let pat = project.pattern_at(bank, pattern); let mut steps = Vec::new(); - let mut scripts = Vec::new(); for &idx in indices { if let Some(step) = pat.step(idx) { - let resolved = pat.resolve_script(idx).unwrap_or("").to_string(); - scripts.push(resolved.clone()); steps.push(CopiedStepData { - script: resolved, + script: pat.resolve_script(idx).unwrap_or("").to_string(), active: step.active, source: step.source, original_index: idx, @@ -136,12 +133,11 @@ pub fn copy_steps( } } - let copied = CopiedSteps { + CopiedSteps { bank, pattern, steps, - }; - (copied, scripts) + } } pub struct PasteResult { diff --git a/src/state/modal.rs b/src/state/modal.rs index d1471a9..3910701 100644 --- a/src/state/modal.rs +++ b/src/state/modal.rs @@ -1,4 +1,4 @@ -use crate::model::{FollowUp, LaunchQuantization, PatternSpeed, SyncMode}; +use crate::model::{self, FollowUp, LaunchQuantization, PatternSpeed, SyncMode}; use crate::state::editor::{EuclideanField, PatternField, PatternPropsField}; use crate::state::file_browser::FileBrowserState; @@ -41,7 +41,7 @@ impl RenameTarget { pub fn title(&self) -> String { match self { Self::Bank { bank } => format!("Rename Bank {:02}", bank + 1), - Self::Pattern { bank, pattern } => format!("Rename B{:02}:P{:02}", bank + 1, pattern + 1), + Self::Pattern { bank, pattern } => format!("Rename {}", model::bp_label(*bank, *pattern)), Self::Step { step, .. } => format!("Name Step {:02}", step + 1), } } diff --git a/src/views/highlight.rs b/src/views/highlight.rs index 0f86110..1251e0c 100644 --- a/src/views/highlight.rs +++ b/src/views/highlight.rs @@ -9,7 +9,7 @@ use crate::theme; static EMPTY_SET: LazyLock> = LazyLock::new(HashSet::new); #[derive(Clone, Copy, PartialEq, Eq)] -pub enum TokenKind { +enum TokenKind { Number, String, Comment, @@ -65,11 +65,11 @@ impl TokenKind { } } -pub struct Token { - pub start: usize, - pub end: usize, - pub kind: TokenKind, - pub varargs: bool, +struct Token { + start: usize, + end: usize, + kind: TokenKind, + varargs: bool, } fn lookup_word_kind(word: &str) -> Option<(TokenKind, bool)> { @@ -121,7 +121,7 @@ const INTERVALS: &[&str] = &[ "M14", "P15", ]; -pub fn tokenize_line(line: &str, user_words: &HashSet) -> Vec { +fn tokenize_line(line: &str, user_words: &HashSet) -> Vec { let mut tokens = Vec::new(); let mut chars = line.char_indices().peekable(); diff --git a/src/views/main_view.rs b/src/views/main_view.rs index 411f0cf..3a86248 100644 --- a/src/views/main_view.rs +++ b/src/views/main_view.rs @@ -1,18 +1,17 @@ +//! Main page view — sequencer grid, visualizations (scope/spectrum), script previews. + use std::collections::HashSet; use ratatui::layout::{Alignment, Constraint, Layout, Rect}; use ratatui::style::{Color, Modifier, Style}; -use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Borders, Paragraph}; use ratatui::Frame; use crate::app::App; use crate::engine::SequencerSnapshot; -use crate::model::SourceSpan; use crate::state::MainLayout; use crate::theme; -use crate::views::highlight::highlight_line_with_runtime; -use crate::views::render::{adjust_resolved_for_line, adjust_spans_for_line}; +use crate::views::render::highlight_script_lines; use crate::widgets::{Orientation, Scope, Spectrum, VuMeter}; pub fn layout(area: Rect) -> [Rect; 3] { @@ -70,14 +69,15 @@ fn render_top_layout( idx += 1; } if has_preview { + let user_words: HashSet = app.dict.lock().keys().cloned().collect(); let has_prelude = !app.project_state.project.prelude.trim().is_empty(); if has_prelude { let [script_area, prelude_area] = Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)]).areas(areas[idx]); - render_script_preview(frame, app, snapshot, script_area); - render_prelude_preview(frame, app, prelude_area); + render_script_preview(frame, app, snapshot, &user_words, script_area); + render_prelude_preview(frame, app, &user_words, prelude_area); } else { - render_script_preview(frame, app, snapshot, areas[idx]); + render_script_preview(frame, app, snapshot, &user_words, areas[idx]); } idx += 1; } @@ -163,11 +163,18 @@ fn render_viz_area( Orientation::Horizontal }; + let user_words_once: Option> = if panels.iter().any(|p| matches!(p, VizPanel::Preview)) { + Some(app.dict.lock().keys().cloned().collect()) + } else { + None + }; + for (panel, panel_area) in panels.iter().zip(areas.iter()) { match panel { VizPanel::Scope => render_scope(frame, app, *panel_area, orientation), VizPanel::Spectrum => render_spectrum(frame, app, *panel_area), VizPanel::Preview => { + let user_words = user_words_once.as_ref().unwrap(); let has_prelude = !app.project_state.project.prelude.trim().is_empty(); if has_prelude { let [script_area, prelude_area] = if is_vertical_layout { @@ -177,10 +184,10 @@ fn render_viz_area( Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)]) .areas(*panel_area) }; - render_script_preview(frame, app, snapshot, script_area); - render_prelude_preview(frame, app, prelude_area); + render_script_preview(frame, app, snapshot, user_words, script_area); + render_prelude_preview(frame, app, user_words, prelude_area); } else { - render_script_preview(frame, app, snapshot, *panel_area); + render_script_preview(frame, app, snapshot, user_words, *panel_area); } } } @@ -488,10 +495,10 @@ fn render_script_preview( frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, + user_words: &HashSet, area: Rect, ) { let theme = theme::get(); - let user_words: HashSet = app.dict.lock().keys().cloned().collect(); let pattern = app.current_edit_pattern(); let step_idx = app.editor_ctx.step; @@ -534,43 +541,17 @@ fn render_script_preview( None }; - let resolved_display: Vec<(SourceSpan, String)> = trace - .map(|t| { - t.resolved - .iter() - .map(|(s, v)| (*s, v.display())) - .collect() - }) - .unwrap_or_default(); - - let mut line_start = 0usize; - let lines: Vec = script - .lines() - .take(inner.height as usize) - .map(|line_str| { - let tokens = if let Some(t) = trace { - let exec = adjust_spans_for_line(&t.executed_spans, line_start, line_str.len()); - let sel = adjust_spans_for_line(&t.selected_spans, line_start, line_str.len()); - let res = adjust_resolved_for_line(&resolved_display, line_start, line_str.len()); - highlight_line_with_runtime(line_str, &exec, &sel, &res, &user_words) - } else { - highlight_line_with_runtime(line_str, &[], &[], &[], &user_words) - }; - line_start += line_str.len() + 1; - let spans: Vec = tokens - .into_iter() - .map(|(style, text, _)| Span::styled(text, style)) - .collect(); - Line::from(spans) - }) - .collect(); - + let lines = highlight_script_lines(script, trace, user_words, inner.height as usize); frame.render_widget(Paragraph::new(lines), inner); } -fn render_prelude_preview(frame: &mut Frame, app: &App, area: Rect) { +fn render_prelude_preview( + frame: &mut Frame, + app: &App, + user_words: &HashSet, + area: Rect, +) { let theme = theme::get(); - let user_words: HashSet = app.dict.lock().keys().cloned().collect(); let prelude = &app.project_state.project.prelude; let block = Block::default() @@ -580,19 +561,7 @@ fn render_prelude_preview(frame: &mut Frame, app: &App, area: Rect) { let inner = block.inner(area); frame.render_widget(block, area); - let lines: Vec = prelude - .lines() - .take(inner.height as usize) - .map(|line_str| { - let tokens = highlight_line_with_runtime(line_str, &[], &[], &[], &user_words); - let spans: Vec = tokens - .into_iter() - .map(|(style, text, _)| Span::styled(text, style)) - .collect(); - Line::from(spans) - }) - .collect(); - + let lines = highlight_script_lines(prelude, None, user_words, inner.height as usize); frame.render_widget(Paragraph::new(lines), inner); } diff --git a/src/views/render.rs b/src/views/render.rs index 8ddc0ea..9179d05 100644 --- a/src/views/render.rs +++ b/src/views/render.rs @@ -1,3 +1,5 @@ +//! Top-level render dispatch — composes header, page views, modals, and effects each frame. + use std::collections::HashSet; use std::time::Duration; @@ -9,7 +11,7 @@ use ratatui::Frame; use crate::app::App; use crate::engine::{LinkState, SequencerSnapshot}; -use crate::model::SourceSpan; +use crate::model::{ExecutionTrace, SourceSpan}; use crate::page::Page; use crate::state::{ EditorTarget, EuclideanField, FlashKind, Modal, PanelFocus, PatternField, RenameTarget, @@ -62,6 +64,44 @@ pub fn adjust_resolved_for_line( .collect() } +pub fn highlight_script_lines( + script: &str, + trace: Option<&ExecutionTrace>, + user_words: &HashSet, + max_lines: usize, +) -> Vec> { + let resolved_display: Vec<(SourceSpan, String)> = trace + .map(|t| { + t.resolved + .iter() + .map(|(s, v)| (*s, v.display())) + .collect() + }) + .unwrap_or_default(); + + let mut line_start = 0usize; + script + .lines() + .take(max_lines) + .map(|line_str| { + let tokens = if let Some(t) = trace { + let exec = adjust_spans_for_line(&t.executed_spans, line_start, line_str.len()); + let sel = adjust_spans_for_line(&t.selected_spans, line_start, line_str.len()); + let res = adjust_resolved_for_line(&resolved_display, line_start, line_str.len()); + highlight_line_with_runtime(line_str, &exec, &sel, &res, user_words) + } else { + highlight_line_with_runtime(line_str, &[], &[], &[], user_words) + }; + line_start += line_str.len() + 1; + let spans: Vec = tokens + .into_iter() + .map(|(style, text, _)| Span::styled(text, style)) + .collect(); + Line::from(spans) + }) + .collect() +} + pub fn horizontal_padding(width: u16) -> u16 { if width >= 120 { 4 @@ -522,7 +562,6 @@ fn render_modal( term: Rect, ) -> Option { let theme = theme::get(); - let user_words: HashSet = app.dict.lock().keys().cloned().collect(); let inner = match &app.ui.modal { Modal::None => return None, Modal::Confirm { action, selected } => { @@ -598,8 +637,14 @@ fn render_modal( .height(18) .render_centered(frame, term) } - Modal::Preview => render_modal_preview(frame, app, snapshot, &user_words, term), - Modal::Editor => render_modal_editor(frame, app, snapshot, &user_words, term), + Modal::Preview => { + let user_words: HashSet = app.dict.lock().keys().cloned().collect(); + render_modal_preview(frame, app, snapshot, &user_words, term) + } + Modal::Editor => { + let user_words: HashSet = app.dict.lock().keys().cloned().collect(); + render_modal_editor(frame, app, snapshot, &user_words, term) + } Modal::PatternProps { bank, pattern, @@ -859,34 +904,8 @@ fn render_modal_preview( None }; - let resolved_display: Vec<(SourceSpan, String)> = trace - .map(|t| t.resolved.iter().map(|(s, v)| (*s, v.display())).collect()) - .unwrap_or_default(); - - let mut line_start = 0usize; - let lines: Vec = script - .lines() - .map(|line_str| { - let tokens = if let Some(t) = trace { - let exec = adjust_spans_for_line(&t.executed_spans, line_start, line_str.len()); - let sel = adjust_spans_for_line(&t.selected_spans, line_start, line_str.len()); - let res = - adjust_resolved_for_line(&resolved_display, line_start, line_str.len()); - highlight_line_with_runtime(line_str, &exec, &sel, &res, user_words) - } else { - highlight_line_with_runtime(line_str, &[], &[], &[], user_words) - }; - line_start += line_str.len() + 1; - let spans: Vec = tokens - .into_iter() - .map(|(style, text, _)| Span::styled(text, style)) - .collect(); - Line::from(spans) - }) - .collect(); - - let paragraph = Paragraph::new(lines); - frame.render_widget(paragraph, inner); + let lines = highlight_script_lines(script, trace, user_words, usize::MAX); + frame.render_widget(Paragraph::new(lines), inner); } inner