This commit is contained in:
72
src/app.rs
72
src/app.rs
@@ -18,8 +18,8 @@ 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, FlashKind, LiveKeyState, Metrics, Modal,
|
||||
MuteState, OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav,
|
||||
AudioSettings, CyclicEnum, EditorContext, EditorTarget, FlashKind, LiveKeyState, Metrics,
|
||||
Modal, MuteState, OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav,
|
||||
PlaybackState, ProjectState, StagedChange, StagedPropChange, UiState,
|
||||
};
|
||||
|
||||
@@ -164,8 +164,12 @@ impl App {
|
||||
self.project_state.mark_all_dirty();
|
||||
}
|
||||
|
||||
pub fn toggle_playing(&mut self) {
|
||||
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) {
|
||||
@@ -338,6 +342,58 @@ impl App {
|
||||
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<String> = if prelude.is_empty() {
|
||||
vec![String::new()]
|
||||
} else {
|
||||
prelude.lines().map(String::from).collect()
|
||||
};
|
||||
self.editor_ctx.editor.set_content(lines);
|
||||
let candidates = model::WORDS
|
||||
.iter()
|
||||
.map(|w| cagire_ratatui::CompletionCandidate {
|
||||
name: w.name.to_string(),
|
||||
signature: w.stack.to_string(),
|
||||
description: w.desc.to_string(),
|
||||
example: w.example.to_string(),
|
||||
})
|
||||
.collect();
|
||||
self.editor_ctx.editor.set_candidates(candidates);
|
||||
self.editor_ctx
|
||||
.editor
|
||||
.set_completion_enabled(self.ui.show_completion);
|
||||
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,
|
||||
@@ -614,6 +670,8 @@ impl App {
|
||||
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 },
|
||||
@@ -862,7 +920,7 @@ impl App {
|
||||
pub fn dispatch(&mut self, cmd: AppCommand, link: &LinkState, snapshot: &SequencerSnapshot) {
|
||||
match cmd {
|
||||
// Playback
|
||||
AppCommand::TogglePlaying => self.toggle_playing(),
|
||||
AppCommand::TogglePlaying => self.toggle_playing(link),
|
||||
AppCommand::TempoUp => self.tempo_up(link),
|
||||
AppCommand::TempoDown => self.tempo_down(link),
|
||||
|
||||
@@ -1278,6 +1336,12 @@ impl App {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -221,4 +221,10 @@ pub enum AppCommand {
|
||||
steps: usize,
|
||||
rotation: usize,
|
||||
},
|
||||
|
||||
// Prelude
|
||||
OpenPreludeEditor,
|
||||
SavePrelude,
|
||||
EvaluatePrelude,
|
||||
ClosePreludeEditor,
|
||||
}
|
||||
|
||||
@@ -269,6 +269,8 @@ pub struct SequencerConfig {
|
||||
pub audio_sample_pos: Arc<AtomicU64>,
|
||||
pub sample_rate: Arc<std::sync::atomic::AtomicU32>,
|
||||
pub cc_access: Option<Arc<dyn CcAccess>>,
|
||||
pub variables: Variables,
|
||||
pub dict: Dictionary,
|
||||
#[cfg(feature = "desktop")]
|
||||
pub mouse_x: Arc<AtomicU32>,
|
||||
#[cfg(feature = "desktop")]
|
||||
@@ -301,6 +303,8 @@ pub fn spawn_sequencer(
|
||||
let shared_state = Arc::new(ArcSwap::from_pointee(SharedSequencerState::default()));
|
||||
let shared_state_clone = Arc::clone(&shared_state);
|
||||
|
||||
let variables = config.variables;
|
||||
let dict = config.dict;
|
||||
#[cfg(feature = "desktop")]
|
||||
let mouse_x = config.mouse_x;
|
||||
#[cfg(feature = "desktop")]
|
||||
@@ -335,6 +339,8 @@ pub fn spawn_sequencer(
|
||||
config.audio_sample_pos,
|
||||
config.sample_rate,
|
||||
config.cc_access,
|
||||
variables,
|
||||
dict,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_x,
|
||||
#[cfg(feature = "desktop")]
|
||||
@@ -529,6 +535,7 @@ pub(crate) struct SequencerState {
|
||||
event_count: usize,
|
||||
script_engine: ScriptEngine,
|
||||
variables: Variables,
|
||||
dict: Dictionary,
|
||||
speed_overrides: HashMap<(usize, usize), f64>,
|
||||
key_cache: KeyCache,
|
||||
buf_audio_commands: Vec<TimestampedCommand>,
|
||||
@@ -547,7 +554,7 @@ impl SequencerState {
|
||||
rng: Rng,
|
||||
cc_access: Option<Arc<dyn CcAccess>>,
|
||||
) -> Self {
|
||||
let script_engine = ScriptEngine::new(Arc::clone(&variables), dict, rng);
|
||||
let script_engine = ScriptEngine::new(Arc::clone(&variables), Arc::clone(&dict), rng);
|
||||
Self {
|
||||
audio_state: AudioState::new(),
|
||||
pattern_cache: PatternCache::new(),
|
||||
@@ -557,6 +564,7 @@ impl SequencerState {
|
||||
event_count: 0,
|
||||
script_engine,
|
||||
variables,
|
||||
dict,
|
||||
speed_overrides: HashMap::with_capacity(MAX_PATTERNS),
|
||||
key_cache: KeyCache::new(),
|
||||
buf_audio_commands: Vec::with_capacity(32),
|
||||
@@ -663,12 +671,9 @@ impl SequencerState {
|
||||
self.audio_state.flush_midi_notes = true;
|
||||
}
|
||||
SeqCommand::ResetScriptState => {
|
||||
let variables: Variables = Arc::new(ArcSwap::from_pointee(HashMap::new()));
|
||||
let dict: Dictionary = Arc::new(Mutex::new(HashMap::new()));
|
||||
let rng: Rng = Arc::new(Mutex::new(StdRng::seed_from_u64(0)));
|
||||
self.script_engine =
|
||||
ScriptEngine::new(Arc::clone(&variables), dict, rng);
|
||||
self.variables = variables;
|
||||
// Clear shared state instead of replacing - preserves sharing with app
|
||||
self.variables.store(Arc::new(HashMap::new()));
|
||||
self.dict.lock().clear();
|
||||
self.speed_overrides.clear();
|
||||
}
|
||||
SeqCommand::Shutdown => {}
|
||||
@@ -1070,6 +1075,8 @@ fn sequencer_loop(
|
||||
audio_sample_pos: Arc<AtomicU64>,
|
||||
sample_rate: Arc<std::sync::atomic::AtomicU32>,
|
||||
cc_access: Option<Arc<dyn CcAccess>>,
|
||||
variables: Variables,
|
||||
dict: Dictionary,
|
||||
#[cfg(feature = "desktop")] mouse_x: Arc<AtomicU32>,
|
||||
#[cfg(feature = "desktop")] mouse_y: Arc<AtomicU32>,
|
||||
#[cfg(feature = "desktop")] mouse_down: Arc<AtomicU32>,
|
||||
@@ -1078,8 +1085,6 @@ fn sequencer_loop(
|
||||
|
||||
set_realtime_priority();
|
||||
|
||||
let variables: Variables = Arc::new(ArcSwap::from_pointee(HashMap::new()));
|
||||
let dict: Dictionary = Arc::new(Mutex::new(HashMap::new()));
|
||||
let rng: Rng = Arc::new(Mutex::new(StdRng::seed_from_u64(0)));
|
||||
let mut seq_state = SequencerState::new(variables, dict, rng, cc_access);
|
||||
|
||||
|
||||
@@ -133,6 +133,8 @@ pub fn init(args: InitArgs) -> Init {
|
||||
audio_sample_pos: Arc::clone(&audio_sample_pos),
|
||||
sample_rate: Arc::clone(&sample_rate_shared),
|
||||
cc_access: Some(Arc::new(app.midi.cc_memory.clone()) as Arc<dyn model::CcAccess>),
|
||||
variables: Arc::clone(&app.variables),
|
||||
dict: Arc::clone(&app.dict),
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_x: Arc::clone(&mouse_x),
|
||||
#[cfg(feature = "desktop")]
|
||||
|
||||
35
src/input.rs
35
src/input.rs
@@ -11,8 +11,8 @@ use crate::engine::{AudioCommand, LinkState, SeqCommand, SequencerSnapshot};
|
||||
use crate::model::PatternSpeed;
|
||||
use crate::page::Page;
|
||||
use crate::state::{
|
||||
CyclicEnum, DeviceKind, EngineSection, EuclideanField, Modal, OptionsFocus, PanelFocus,
|
||||
PatternField, PatternPropsField, SampleBrowserState, SettingKind, SidePanel,
|
||||
CyclicEnum, DeviceKind, EditorTarget, EngineSection, EuclideanField, Modal, OptionsFocus,
|
||||
PanelFocus, PatternField, PatternPropsField, SampleBrowserState, SettingKind, SidePanel,
|
||||
};
|
||||
|
||||
pub enum InputResult {
|
||||
@@ -488,14 +488,31 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
} else if editor.completion_active() {
|
||||
editor.dismiss_completion();
|
||||
} else {
|
||||
ctx.dispatch(AppCommand::SaveEditorToStep);
|
||||
ctx.dispatch(AppCommand::CompileCurrentStep);
|
||||
match ctx.app.editor_ctx.target {
|
||||
EditorTarget::Step => {
|
||||
ctx.dispatch(AppCommand::SaveEditorToStep);
|
||||
ctx.dispatch(AppCommand::CompileCurrentStep);
|
||||
}
|
||||
EditorTarget::Prelude => {
|
||||
ctx.dispatch(AppCommand::SavePrelude);
|
||||
ctx.dispatch(AppCommand::EvaluatePrelude);
|
||||
ctx.dispatch(AppCommand::ClosePreludeEditor);
|
||||
}
|
||||
}
|
||||
ctx.dispatch(AppCommand::CloseModal);
|
||||
}
|
||||
}
|
||||
KeyCode::Char('e') if ctrl => {
|
||||
ctx.dispatch(AppCommand::SaveEditorToStep);
|
||||
ctx.dispatch(AppCommand::CompileCurrentStep);
|
||||
match ctx.app.editor_ctx.target {
|
||||
EditorTarget::Step => {
|
||||
ctx.dispatch(AppCommand::SaveEditorToStep);
|
||||
ctx.dispatch(AppCommand::CompileCurrentStep);
|
||||
}
|
||||
EditorTarget::Prelude => {
|
||||
ctx.dispatch(AppCommand::SavePrelude);
|
||||
ctx.dispatch(AppCommand::EvaluatePrelude);
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Char('f') if ctrl => {
|
||||
editor.activate_search();
|
||||
@@ -1082,6 +1099,12 @@ fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputR
|
||||
ctx.dispatch(AppCommand::ClearSolos);
|
||||
ctx.app.send_mute_state(ctx.seq_cmd_tx);
|
||||
}
|
||||
KeyCode::Char('d') => {
|
||||
ctx.dispatch(AppCommand::OpenPreludeEditor);
|
||||
}
|
||||
KeyCode::Char('D') => {
|
||||
ctx.dispatch(AppCommand::EvaluatePrelude);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
InputResult::Continue
|
||||
|
||||
@@ -27,6 +27,7 @@ pub const DOCS: &[DocEntry] = &[
|
||||
Topic("The Dictionary", include_str!("../../docs/dictionary.md")),
|
||||
Topic("The Stack", include_str!("../../docs/stack.md")),
|
||||
Topic("Creating Words", include_str!("../../docs/definitions.md")),
|
||||
Topic("The Prelude", include_str!("../../docs/prelude.md")),
|
||||
Topic("Oddities", include_str!("../../docs/oddities.md")),
|
||||
// Audio Engine
|
||||
Section("Audio Engine"),
|
||||
|
||||
@@ -3,6 +3,13 @@ use std::ops::RangeInclusive;
|
||||
|
||||
use cagire_ratatui::Editor;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum EditorTarget {
|
||||
#[default]
|
||||
Step,
|
||||
Prelude,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PatternField {
|
||||
Length,
|
||||
@@ -76,6 +83,7 @@ pub struct EditorContext {
|
||||
pub copied_steps: Option<CopiedSteps>,
|
||||
pub show_stack: bool,
|
||||
pub stack_cache: RefCell<Option<StackCache>>,
|
||||
pub target: EditorTarget,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -125,6 +133,7 @@ impl Default for EditorContext {
|
||||
copied_steps: None,
|
||||
show_stack: false,
|
||||
stack_cache: RefCell::new(None),
|
||||
target: EditorTarget::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ pub mod ui;
|
||||
pub use audio::{AudioSettings, DeviceKind, EngineSection, MainLayout, Metrics, SettingKind};
|
||||
pub use color_scheme::ColorScheme;
|
||||
pub use editor::{
|
||||
CopiedStepData, CopiedSteps, EditorContext, EuclideanField, PatternField, PatternPropsField,
|
||||
StackCache,
|
||||
CopiedStepData, CopiedSteps, EditorContext, EditorTarget, EuclideanField, PatternField,
|
||||
PatternPropsField, StackCache,
|
||||
};
|
||||
pub use live_keys::LiveKeyState;
|
||||
pub use modal::Modal;
|
||||
|
||||
@@ -37,6 +37,12 @@ pub fn bindings_for(page: Page) -> Vec<(&'static str, &'static str, &'static str
|
||||
bindings.push(("r", "Rename", "Rename current step"));
|
||||
bindings.push(("Ctrl+R", "Run", "Run step script immediately"));
|
||||
bindings.push(("e", "Euclidean", "Distribute linked steps using Euclidean rhythm"));
|
||||
bindings.push(("m", "Mute", "Stage mute for current pattern"));
|
||||
bindings.push(("x", "Solo", "Stage solo for current pattern"));
|
||||
bindings.push(("M", "Clear mutes", "Clear all mutes"));
|
||||
bindings.push(("X", "Clear solos", "Clear all solos"));
|
||||
bindings.push(("d", "Prelude", "Edit prelude script"));
|
||||
bindings.push(("D", "Eval prelude", "Re-evaluate prelude without editing"));
|
||||
}
|
||||
Page::Patterns => {
|
||||
bindings.push(("←→↑↓", "Navigate", "Move between banks/patterns"));
|
||||
@@ -46,6 +52,10 @@ pub fn bindings_for(page: Page) -> Vec<(&'static str, &'static str, &'static str
|
||||
bindings.push(("c", "Commit", "Commit staged changes"));
|
||||
bindings.push(("r", "Rename", "Rename bank/pattern"));
|
||||
bindings.push(("e", "Properties", "Edit pattern properties"));
|
||||
bindings.push(("m", "Mute", "Stage mute for pattern"));
|
||||
bindings.push(("x", "Solo", "Stage solo for pattern"));
|
||||
bindings.push(("M", "Clear mutes", "Clear all mutes"));
|
||||
bindings.push(("X", "Clear solos", "Clear all solos"));
|
||||
bindings.push(("Ctrl+C", "Copy", "Copy bank/pattern"));
|
||||
bindings.push(("Ctrl+V", "Paste", "Paste bank/pattern"));
|
||||
bindings.push(("Del", "Reset", "Reset bank/pattern"));
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::engine::{LinkState, SequencerSnapshot};
|
||||
use crate::model::SourceSpan;
|
||||
use crate::page::Page;
|
||||
use crate::state::{
|
||||
EuclideanField, FlashKind, Modal, PanelFocus, PatternField, SidePanel,
|
||||
EditorTarget, EuclideanField, FlashKind, Modal, PanelFocus, PatternField, SidePanel,
|
||||
};
|
||||
use crate::theme;
|
||||
use crate::views::highlight::{self, highlight_line, highlight_line_with_runtime};
|
||||
@@ -629,8 +629,6 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
||||
Modal::Editor => {
|
||||
let width = (term.width * 80 / 100).max(40);
|
||||
let height = (term.height * 60 / 100).max(10);
|
||||
let step_num = app.editor_ctx.step + 1;
|
||||
let step = app.current_edit_pattern().step(app.editor_ctx.step);
|
||||
|
||||
let flash_kind = app.ui.flash_kind();
|
||||
let border_color = match flash_kind {
|
||||
@@ -640,10 +638,17 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
||||
None => theme.modal.editor,
|
||||
};
|
||||
|
||||
let title = if let Some(ref name) = step.and_then(|s| s.name.as_ref()) {
|
||||
format!("Step {step_num:02}: {name}")
|
||||
} else {
|
||||
format!("Step {step_num:02} Script")
|
||||
let title = match app.editor_ctx.target {
|
||||
EditorTarget::Prelude => "Prelude".to_string(),
|
||||
EditorTarget::Step => {
|
||||
let step_num = app.editor_ctx.step + 1;
|
||||
let step = app.current_edit_pattern().step(app.editor_ctx.step);
|
||||
if let Some(ref name) = step.and_then(|s| s.name.as_ref()) {
|
||||
format!("Step {step_num:02}: {name}")
|
||||
} else {
|
||||
format!("Step {step_num:02} Script")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let inner = ModalFrame::new(&title)
|
||||
@@ -652,7 +657,10 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
||||
.border_color(border_color)
|
||||
.render_centered(frame, term);
|
||||
|
||||
let trace = if app.ui.runtime_highlight && app.playback.playing {
|
||||
let trace = if app.ui.runtime_highlight
|
||||
&& app.playback.playing
|
||||
&& app.editor_ctx.target == EditorTarget::Step
|
||||
{
|
||||
let source = app
|
||||
.current_edit_pattern()
|
||||
.resolve_source(app.editor_ctx.step);
|
||||
|
||||
Reference in New Issue
Block a user