Feat: cleanup
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
//! Single-pass compiler from Forth source text to Op sequences.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Forth virtual machine for the Cagire music sequencer.
|
||||
|
||||
mod compiler;
|
||||
mod ops;
|
||||
mod theory;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Compiled operation variants for the Forth VM instruction set.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::types::SourceSpan;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Value> {
|
||||
self.stack.lock().clone()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn clear_stack(&self) {
|
||||
self.stack.lock().clear();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Project data model: banks, patterns, and steps for the Cagire sequencer.
|
||||
|
||||
mod file;
|
||||
mod project;
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Project, Bank, Pattern, and Step structs with serialization.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Reusable TUI widgets for the Cagire sequencer interface.
|
||||
|
||||
mod category_list;
|
||||
mod confirm;
|
||||
mod editor;
|
||||
|
||||
@@ -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::<Vec<_>>().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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<LiveKeyState>,
|
||||
pub clipboard: Option<arboard::Clipboard>,
|
||||
pub copied_patterns: Option<Vec<Pattern>>,
|
||||
@@ -108,7 +110,7 @@ impl App {
|
||||
metrics: Metrics::default(),
|
||||
variables,
|
||||
dict,
|
||||
rng,
|
||||
_rng: rng,
|
||||
live_keys,
|
||||
script_engine,
|
||||
clipboard: arboard::Clipboard::new().ok(),
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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<AtomicBool>,
|
||||
#[allow(dead_code)]
|
||||
thread: Option<JoinHandle<()>>,
|
||||
// Held to keep the thread alive until this handle is dropped.
|
||||
_thread: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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<StepSnapshot>,
|
||||
#[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<ActivePatternState>,
|
||||
step_traces: Arc<StepTracesMap>,
|
||||
|
||||
@@ -26,6 +26,7 @@ pub struct InitArgs {
|
||||
pub buffer: Option<u32>,
|
||||
}
|
||||
|
||||
// Fields destructured in main.rs (cli) and plugin crate — all are used.
|
||||
#[allow(dead_code)]
|
||||
pub struct Init {
|
||||
pub app: App,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -117,17 +117,14 @@ pub fn copy_steps(
|
||||
bank: usize,
|
||||
pattern: usize,
|
||||
indices: &[usize],
|
||||
) -> (CopiedSteps, Vec<String>) {
|
||||
) -> 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 {
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::theme;
|
||||
static EMPTY_SET: LazyLock<HashSet<String>> = 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<String>) -> Vec<Token> {
|
||||
fn tokenize_line(line: &str, user_words: &HashSet<String>) -> Vec<Token> {
|
||||
let mut tokens = Vec::new();
|
||||
let mut chars = line.char_indices().peekable();
|
||||
|
||||
|
||||
@@ -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<String> = 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<HashSet<String>> = 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<String>,
|
||||
area: Rect,
|
||||
) {
|
||||
let theme = theme::get();
|
||||
let user_words: HashSet<String> = 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<Line> = 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<Span> = 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<String>,
|
||||
area: Rect,
|
||||
) {
|
||||
let theme = theme::get();
|
||||
let user_words: HashSet<String> = 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<Line> = prelude
|
||||
.lines()
|
||||
.take(inner.height as usize)
|
||||
.map(|line_str| {
|
||||
let tokens = highlight_line_with_runtime(line_str, &[], &[], &[], &user_words);
|
||||
let spans: Vec<Span> = 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String>,
|
||||
max_lines: usize,
|
||||
) -> Vec<Line<'static>> {
|
||||
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<Span> = 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<Rect> {
|
||||
let theme = theme::get();
|
||||
let user_words: HashSet<String> = 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<String> = app.dict.lock().keys().cloned().collect();
|
||||
render_modal_preview(frame, app, snapshot, &user_words, term)
|
||||
}
|
||||
Modal::Editor => {
|
||||
let user_words: HashSet<String> = 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<Line> = 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<Span> = 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
|
||||
|
||||
Reference in New Issue
Block a user