Some kind of refactoring
This commit is contained in:
@@ -9,9 +9,13 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- Flattened model re-export indirection; `script.rs` now exports only `ScriptEngine`.
|
||||||
- Hue rotation step size increased from 1° to 5° for faster adjustment.
|
- Hue rotation step size increased from 1° to 5° for faster adjustment.
|
||||||
- Moved catalog data (DOCS, CATEGORIES) from views to `src/model/`, eliminating state-to-view layer inversion.
|
- Moved catalog data (DOCS, CATEGORIES) from views to `src/model/`, eliminating state-to-view layer inversion.
|
||||||
- Extracted shared initialization into `src/init.rs`, deduplicating ~140 lines between terminal and desktop binaries.
|
- Extracted shared initialization into `src/init.rs`, deduplicating ~140 lines between terminal and desktop binaries.
|
||||||
|
- Split App dispatch into focused service modules (`help_nav`, `dict_nav`, `euclidean`, `clipboard`, extended `pattern_editor`), reducing `app.rs` by ~310 lines.
|
||||||
|
- Moved stack preview computation from render path to input time, making editor rendering pure.
|
||||||
|
- Decoupled script runtime state between UI and sequencer threads, eliminating shared mutexes on the RT path.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Desktop binary now loads color theme and connects MIDI devices on startup (was missing).
|
- Desktop binary now loads color theme and connects MIDI devices on startup (was missing).
|
||||||
|
|||||||
227
refacto.md
227
refacto.md
@@ -1,227 +0,0 @@
|
|||||||
# Refactoring Plan: Isolation and Lower Connascence
|
|
||||||
|
|
||||||
This document outlines practical, incremental refactoring tasks to improve architectural isolation and reduce connascence in this codebase. The goal is to make changes safe and reversible, while reducing coupling between UI, engine, and state.
|
|
||||||
|
|
||||||
Principles:
|
|
||||||
- Preserve behavior. Refactor in small steps with clear boundaries.
|
|
||||||
- Reduce connascence by making dependencies explicit and localized.
|
|
||||||
- Make render paths pure views over state.
|
|
||||||
- Centralize bootstrapping and cross-cutting wiring.
|
|
||||||
|
|
||||||
## Task 1: Extract Shared Bootstrap (Detailed Checklist)
|
|
||||||
Problem:
|
|
||||||
- `src/main.rs` and `src/bin/desktop.rs` duplicate long initialization sequences (settings, audio config, MIDI discovery, samples, sequencer startup). This creates drift and implicit coupling.
|
|
||||||
|
|
||||||
Refactor:
|
|
||||||
- Create a `bootstrap` module (e.g., `src/bootstrap.rs`) that returns a fully configured `App`, `SequencerHandle`, and any shared runtime state.
|
|
||||||
- Both binaries call into this module with minimal differences (e.g., terminal vs desktop display setup).
|
|
||||||
|
|
||||||
Checklist:
|
|
||||||
1. Inventory duplicated blocks
|
|
||||||
- In `src/main.rs`, locate: settings load, link setup, `App::new`, audio config assignment, MIDI load, sample scan, sequencer spawn, `build_stream`.
|
|
||||||
- In `src/bin/desktop.rs`, locate the analogous blocks (very similar ordering).
|
|
||||||
2. Define a shared config struct
|
|
||||||
- Create a `BootstrapArgs` struct with fields: `samples`, `output`, `input`, `channels`, `buffer`.
|
|
||||||
- This mirrors CLI args from both binaries.
|
|
||||||
3. Extract app init
|
|
||||||
- Create `fn build_app(settings: &Settings, args: &BootstrapArgs) -> App` in a new module (e.g., `src/bootstrap.rs`).
|
|
||||||
- Move these from both binaries:
|
|
||||||
- `App::new()`
|
|
||||||
- app playback queued changes setup
|
|
||||||
- audio config assignments from settings/args
|
|
||||||
- UI settings (runtime_highlight, show_scope, show_spectrum, completion, color scheme/hue in terminal path)
|
|
||||||
- MIDI device selection (output/input device selection based on stored names)
|
|
||||||
4. Extract audio/sample init
|
|
||||||
- Create `fn load_samples(paths: &[PathBuf]) -> (Vec<SampleEntry>, usize)` returning the sample list and count.
|
|
||||||
- Replace inline `scan_samples_dir` loops in both binaries.
|
|
||||||
5. Extract sequencer init
|
|
||||||
- Create `fn build_sequencer(app: &App, settings: &Settings, link: Arc<LinkState>, ...) -> SequencerHandle` that:
|
|
||||||
- Builds `SequencerConfig`
|
|
||||||
- Calls `spawn_sequencer`
|
|
||||||
- Returns the handle + receivers needed for MIDI/audio.
|
|
||||||
6. Extract stream init
|
|
||||||
- Create `fn build_audio_stream(config: &AudioStreamConfig, ...) -> Result<(Stream, StreamInfo, AnalysisHandle), Error>`
|
|
||||||
- Use it in both binaries to set `app.audio` fields.
|
|
||||||
7. Replace duplicated code
|
|
||||||
- Update `src/main.rs` and `src/bin/desktop.rs` to call the bootstrap functions.
|
|
||||||
8. Keep env-specific parts in binary
|
|
||||||
- Terminal UI init (`crossterm` setup) remains in `src/main.rs`.
|
|
||||||
- Desktop font/egui setup remains in `src/bin/desktop.rs`.
|
|
||||||
|
|
||||||
Outcome:
|
|
||||||
- One place to reason about initialization and one place to fix config drift.
|
|
||||||
|
|
||||||
Connascence reduced:
|
|
||||||
- Reduced connascence of meaning and position across two binary entry points.
|
|
||||||
|
|
||||||
## Task 2: Remove State-to-View Layer Inversion (Detailed Checklist)
|
|
||||||
Problem:
|
|
||||||
- `UiState::default` depends on view modules (`dict_view::category_count()`, `help_view::topic_count()`). This inverts layering and creates brittle dependencies.
|
|
||||||
|
|
||||||
Refactor:
|
|
||||||
- Move those counts into a pure data module, for example `src/state/ui_defaults.rs` or `src/data/help_topics.rs`.
|
|
||||||
- `UiState::default` should depend only on state/data modules, not views.
|
|
||||||
|
|
||||||
Checklist:
|
|
||||||
1. Identify view-level sources
|
|
||||||
- `src/views/help_view.rs` defines topics using `Topic(...)` list.
|
|
||||||
- `src/views/dict_view.rs` defines category list and `category_count()`.
|
|
||||||
2. Create a pure data module
|
|
||||||
- Add a new module (e.g., `src/data/help_dict.rs`) with:
|
|
||||||
- `pub const HELP_TOPIC_COUNT: usize = ...`
|
|
||||||
- `pub const DICT_CATEGORY_COUNT: usize = ...`
|
|
||||||
- If the counts are derived from arrays, move the arrays into this module and re-export.
|
|
||||||
3. Update views to depend on data module
|
|
||||||
- `help_view::topic_count()` should return `HELP_TOPIC_COUNT`.
|
|
||||||
- `dict_view::category_count()` should return `DICT_CATEGORY_COUNT`.
|
|
||||||
4. Update `UiState::default`
|
|
||||||
- Replace calls to view modules with `data` module constants in `src/state/ui.rs`.
|
|
||||||
5. Confirm no other state files import `views`
|
|
||||||
- Use `rg "views::" src/state` to verify.
|
|
||||||
|
|
||||||
Outcome:
|
|
||||||
- UI state is decoupled from rendering code.
|
|
||||||
|
|
||||||
Connascence reduced:
|
|
||||||
- Reduced connascence of type (views no longer required by state).
|
|
||||||
|
|
||||||
## Task 3: Split App Dispatch into Subsystem Handlers (Detailed Checklist)
|
|
||||||
Problem:
|
|
||||||
- `App::dispatch` is a very large match that mutates many unrelated parts of state. This makes the App a God object and introduces high connascence across domains.
|
|
||||||
|
|
||||||
Refactor:
|
|
||||||
- Introduce subsystem handlers: `ProjectController`, `PlaybackController`, `UiController`, `AudioController`, `MidiController`.
|
|
||||||
- Dispatch routes to a handler based on command domain.
|
|
||||||
|
|
||||||
Checklist:
|
|
||||||
1. Partition commands by domain
|
|
||||||
- Playback: `TogglePlaying`, `TempoUp`, `TempoDown`, staging changes.
|
|
||||||
- Project edits: rename/save/load, pattern/bank changes.
|
|
||||||
- UI-only: modals, status, help/dict navigation.
|
|
||||||
- Audio settings: device list navigation, buffer/channels settings.
|
|
||||||
- MIDI: connect/disconnect, CC, etc.
|
|
||||||
2. Introduce handler modules
|
|
||||||
- Create `src/controllers/` (or `src/handlers/`) with `playback.rs`, `project.rs`, `ui.rs`, `audio.rs`, `midi.rs`.
|
|
||||||
3. Move code in thin slices
|
|
||||||
- Start with the smallest domain (UI-only commands).
|
|
||||||
- Move those match arms into `UiController::handle` and call from `App::dispatch`.
|
|
||||||
- Repeat for each domain.
|
|
||||||
4. Define minimal interfaces
|
|
||||||
- Each controller takes only the state it needs (e.g., `&mut UiState`, not `&mut App`).
|
|
||||||
5. Keep `App::dispatch` as a router
|
|
||||||
- Single match that forwards to controller or returns early.
|
|
||||||
6. Reduce `App` field exposure
|
|
||||||
- Make fields private where possible and provide accessors for controllers.
|
|
||||||
7. Update tests (if/when added)
|
|
||||||
- New controllers can be unit-tested without UI/engine wiring.
|
|
||||||
|
|
||||||
Outcome:
|
|
||||||
- Clear separation of concerns. Smaller, testable units.
|
|
||||||
|
|
||||||
Connascence reduced:
|
|
||||||
- Reduced connascence of execution and meaning inside one massive switch.
|
|
||||||
|
|
||||||
## Task 4: Make Rendering Pure (No Side Effects / Heavy Work) (Detailed Checklist)
|
|
||||||
Problem:
|
|
||||||
- Rendering does real work (`compute_stack_display` runs a Forth evaluation each frame). This couples rendering to runtime behavior and risks UI jank.
|
|
||||||
|
|
||||||
Refactor:
|
|
||||||
- Compute stack previews in a controller step when editor state changes, store result in `UiState` or a new cache state.
|
|
||||||
- Renderers become pure read-only views of state.
|
|
||||||
|
|
||||||
Checklist:
|
|
||||||
1. Locate render-time work
|
|
||||||
- `src/views/render.rs` → `compute_stack_display` builds a Forth engine and evaluates script.
|
|
||||||
2. Introduce cache state
|
|
||||||
- Add a `StackPreview` struct to `src/state/editor.rs` or `src/state/ui.rs`:
|
|
||||||
- `last_cursor_line`, `lines_hash`, `result`.
|
|
||||||
3. Define update points
|
|
||||||
- Update cache when editor content changes or cursor moves.
|
|
||||||
- Likely locations:
|
|
||||||
- `input.rs` when editor receives input.
|
|
||||||
- `app.rs` functions that mutate editor state.
|
|
||||||
4. Move compute logic to controller/service
|
|
||||||
- Create `src/services/stack_preview.rs` that computes preview from editor state.
|
|
||||||
- Call it from update points, store in state.
|
|
||||||
5. Make render pure
|
|
||||||
- Replace `compute_stack_display` usage with cached state from `UiState`/`EditorContext`.
|
|
||||||
6. Verify consistency
|
|
||||||
- Ensure preview updates on paste and modal editor inputs (see `Event::Paste` handling in `src/main.rs`).
|
|
||||||
|
|
||||||
Outcome:
|
|
||||||
- Rendering becomes deterministic and cheap.
|
|
||||||
|
|
||||||
Connascence reduced:
|
|
||||||
- Reduced connascence of timing (render cycle no longer coupled to evaluation).
|
|
||||||
|
|
||||||
## Task 5: Concurrency Boundary for Script Runtime (Detailed Checklist)
|
|
||||||
Problem:
|
|
||||||
- `variables`, `dict`, `rng` are shared between UI and sequencer threads via `Arc<Mutex>` and `ArcSwap`. This is a hidden concurrency coupling and risks contention in RT threads.
|
|
||||||
|
|
||||||
Refactor:
|
|
||||||
- Introduce a message-based interface: UI posts commands to a dedicated script runtime in the sequencer thread.
|
|
||||||
- UI no longer directly mutates shared `dict`/`vars`.
|
|
||||||
|
|
||||||
Checklist:
|
|
||||||
1. Identify shared state
|
|
||||||
- `App::new` constructs `variables`, `dict`, `rng`, `script_engine`.
|
|
||||||
- These are passed into `spawn_sequencer` in `src/main.rs`.
|
|
||||||
2. Define a runtime command channel
|
|
||||||
- New enum `ScriptRuntimeCommand` (e.g., `Reset`, `UpdateVar`, `ClearDict`, etc.).
|
|
||||||
- Add channel to sequencer config and main loop.
|
|
||||||
3. Move ownership into sequencer thread
|
|
||||||
- `spawn_sequencer` creates its own `variables/dict/rng` and `ScriptEngine`.
|
|
||||||
- Sequencer thread consumes `ScriptRuntimeCommand`s and applies changes.
|
|
||||||
4. Update UI interaction points
|
|
||||||
- Anywhere UI currently mutates `variables` or `dict` should send a runtime command instead.
|
|
||||||
- Example: `App::dispatch` branch that does `self.variables.store(...)` / `self.dict.lock().clear()`.
|
|
||||||
5. Replace direct shared references
|
|
||||||
- Remove `ArcSwap` and `Mutex` from `App` for script runtime.
|
|
||||||
6. Verify audio thread safety
|
|
||||||
- Ensure no locks are taken in RT critical sections.
|
|
||||||
|
|
||||||
Outcome:
|
|
||||||
- Clear ownership and fewer concurrency hazards.
|
|
||||||
|
|
||||||
Connascence reduced:
|
|
||||||
- Reduced connascence of timing and execution across threads.
|
|
||||||
|
|
||||||
## Task 6: Isolate Model from Re-Exports (Detailed Checklist)
|
|
||||||
Problem:
|
|
||||||
- `src/model/mod.rs` is mostly re-exports. This blurs boundaries and encourages wide imports.
|
|
||||||
|
|
||||||
Refactor:
|
|
||||||
- Introduce a narrower domain API layer (e.g., `model::ProjectFacade`) that exposes only the subset the app needs.
|
|
||||||
- Limit broad re-exports.
|
|
||||||
|
|
||||||
Checklist:
|
|
||||||
1. Identify heavy imports
|
|
||||||
- `use crate::model::{...}` in `src/app.rs`, `src/input.rs`, `src/views/*`.
|
|
||||||
2. Create small facade modules
|
|
||||||
- e.g., `src/model/project_facade.rs` exporting only `Project`, `Pattern`, `Bank`, etc.
|
|
||||||
- e.g., `src/model/script_facade.rs` for script-related types.
|
|
||||||
3. Replace `pub use` in `model/mod.rs`
|
|
||||||
- Narrow to explicit re-exports or remove when not needed.
|
|
||||||
4. Update call sites
|
|
||||||
- Replace `crate::model::*` with focused imports from facade modules.
|
|
||||||
5. Document boundary
|
|
||||||
- Add brief module docs explaining what is safe to use externally.
|
|
||||||
|
|
||||||
Outcome:
|
|
||||||
- Clearer ownership and lower coupling to external crate internals.
|
|
||||||
|
|
||||||
Connascence reduced:
|
|
||||||
- Reduced connascence of meaning across module boundaries.
|
|
||||||
|
|
||||||
## Suggested Order
|
|
||||||
- Task 1 (bootstrap) and Task 2 (state/view decoupling) first. These are low risk and improve clarity.
|
|
||||||
- Task 3 (split dispatch) next to make the codebase easier to change.
|
|
||||||
- Task 4 (pure rendering) and Task 5 (runtime boundary) after the above to avoid broad ripple effects.
|
|
||||||
- Task 6 (model isolation) last, because it benefits from clearer boundaries established earlier.
|
|
||||||
|
|
||||||
## Success Criteria
|
|
||||||
- Startup is defined in one module and reused by both binaries.
|
|
||||||
- `UiState` compiles without importing anything from `views`.
|
|
||||||
- `App::dispatch` reduced by at least 50% and sub-handlers exist.
|
|
||||||
- Render path is a pure function of state (no evaluation, no locks).
|
|
||||||
- Script runtime state is owned by the sequencer thread.
|
|
||||||
507
src/app.rs
507
src/app.rs
@@ -15,14 +15,13 @@ use crate::engine::{
|
|||||||
use crate::midi::MidiState;
|
use crate::midi::MidiState;
|
||||||
use crate::model::{self, Bank, Dictionary, Pattern, Rng, ScriptEngine, StepContext, Variables};
|
use crate::model::{self, Bank, Dictionary, Pattern, Rng, ScriptEngine, StepContext, Variables};
|
||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
use crate::services::pattern_editor;
|
use crate::services::{clipboard, dict_nav, euclidean, help_nav, pattern_editor};
|
||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
AudioSettings, CyclicEnum, DictFocus, EditorContext, FlashKind, LiveKeyState, Metrics, Modal,
|
AudioSettings, CyclicEnum, EditorContext, FlashKind, LiveKeyState, Metrics, Modal,
|
||||||
MuteState, OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav,
|
MuteState, OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav,
|
||||||
PlaybackState, ProjectState, StagedChange, StagedPropChange, UiState,
|
PlaybackState, ProjectState, StagedChange, StagedPropChange, UiState,
|
||||||
};
|
};
|
||||||
use crate::model::{categories, docs};
|
|
||||||
|
|
||||||
const STEPS_PER_PAGE: usize = 32;
|
const STEPS_PER_PAGE: usize = 32;
|
||||||
|
|
||||||
@@ -41,6 +40,7 @@ pub struct App {
|
|||||||
pub script_engine: ScriptEngine,
|
pub script_engine: ScriptEngine,
|
||||||
pub variables: Variables,
|
pub variables: Variables,
|
||||||
pub dict: Dictionary,
|
pub dict: Dictionary,
|
||||||
|
#[allow(dead_code)] // kept alive for ScriptEngine's Arc clone
|
||||||
pub rng: Rng,
|
pub rng: Rng,
|
||||||
pub live_keys: Arc<LiveKeyState>,
|
pub live_keys: Arc<LiveKeyState>,
|
||||||
pub clipboard: Option<arboard::Clipboard>,
|
pub clipboard: Option<arboard::Clipboard>,
|
||||||
@@ -161,14 +161,6 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn annotate_copy_name(name: &Option<String>) -> Option<String> {
|
|
||||||
match name {
|
|
||||||
Some(n) if !n.ends_with(" (copy)") => Some(format!("{n} (copy)")),
|
|
||||||
Some(n) => Some(n.clone()),
|
|
||||||
None => Some("(copy)".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mark_all_patterns_dirty(&mut self) {
|
pub fn mark_all_patterns_dirty(&mut self) {
|
||||||
self.project_state.mark_all_dirty();
|
self.project_state.mark_all_dirty();
|
||||||
}
|
}
|
||||||
@@ -328,6 +320,9 @@ impl App {
|
|||||||
self.editor_ctx
|
self.editor_ctx
|
||||||
.editor
|
.editor
|
||||||
.set_completion_enabled(self.ui.show_completion);
|
.set_completion_enabled(self.ui.show_completion);
|
||||||
|
if self.editor_ctx.show_stack {
|
||||||
|
crate::services::stack_preview::update_cache(&self.editor_ctx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -638,32 +633,8 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_step(&mut self, bank: usize, pattern: usize, step: usize) {
|
pub fn delete_step(&mut self, bank: usize, pattern: usize, step: usize) {
|
||||||
let pat = self.project_state.project.pattern_at_mut(bank, pattern);
|
let edit = pattern_editor::delete_step(&mut self.project_state.project, bank, pattern, step);
|
||||||
for s in &mut pat.steps {
|
self.project_state.mark_dirty(edit.bank, edit.pattern);
|
||||||
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
|
if self.editor_ctx.bank == bank
|
||||||
&& self.editor_ctx.pattern == pattern
|
&& self.editor_ctx.pattern == pattern
|
||||||
&& self.editor_ctx.step == step
|
&& self.editor_ctx.step == step
|
||||||
@@ -674,34 +645,8 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_steps(&mut self, bank: usize, pattern: usize, steps: &[usize]) {
|
pub fn delete_steps(&mut self, bank: usize, pattern: usize, steps: &[usize]) {
|
||||||
for &step in steps {
|
let edit = pattern_editor::delete_steps(&mut self.project_state.project, bank, pattern, steps);
|
||||||
let pat = self.project_state.project.pattern_at_mut(bank, pattern);
|
self.project_state.mark_dirty(edit.bank, edit.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 {
|
if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern {
|
||||||
self.load_step_to_editor();
|
self.load_step_to_editor();
|
||||||
}
|
}
|
||||||
@@ -714,8 +659,8 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset_pattern(&mut self, bank: usize, pattern: usize) {
|
pub fn reset_pattern(&mut self, bank: usize, pattern: usize) {
|
||||||
self.project_state.project.banks[bank].patterns[pattern] = Pattern::default();
|
let edit = pattern_editor::reset_pattern(&mut self.project_state.project, bank, pattern);
|
||||||
self.project_state.mark_dirty(bank, pattern);
|
self.project_state.mark_dirty(edit.bank, edit.pattern);
|
||||||
if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern {
|
if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern {
|
||||||
self.load_step_to_editor();
|
self.load_step_to_editor();
|
||||||
}
|
}
|
||||||
@@ -723,8 +668,8 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset_bank(&mut self, bank: usize) {
|
pub fn reset_bank(&mut self, bank: usize) {
|
||||||
self.project_state.project.banks[bank] = Bank::default();
|
let pat_count = pattern_editor::reset_bank(&mut self.project_state.project, bank);
|
||||||
for pattern in 0..self.project_state.project.banks[bank].patterns.len() {
|
for pattern in 0..pat_count {
|
||||||
self.project_state.mark_dirty(bank, pattern);
|
self.project_state.mark_dirty(bank, pattern);
|
||||||
}
|
}
|
||||||
if self.editor_ctx.bank == bank {
|
if self.editor_ctx.bank == bank {
|
||||||
@@ -734,16 +679,13 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn copy_pattern(&mut self, bank: usize, pattern: usize) {
|
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(clipboard::copy_pattern(&self.project_state.project, bank, pattern));
|
||||||
self.copied_pattern = Some(pat);
|
|
||||||
self.ui.flash("Pattern copied", 150, FlashKind::Success);
|
self.ui.flash("Pattern copied", 150, FlashKind::Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn paste_pattern(&mut self, bank: usize, pattern: usize) {
|
pub fn paste_pattern(&mut self, bank: usize, pattern: usize) {
|
||||||
if let Some(src) = &self.copied_pattern {
|
if let Some(src) = self.copied_pattern.clone() {
|
||||||
let mut pat = src.clone();
|
clipboard::paste_pattern(&mut self.project_state.project, bank, pattern, &src);
|
||||||
pat.name = Self::annotate_copy_name(&src.name);
|
|
||||||
self.project_state.project.banks[bank].patterns[pattern] = pat;
|
|
||||||
self.project_state.mark_dirty(bank, pattern);
|
self.project_state.mark_dirty(bank, pattern);
|
||||||
if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern {
|
if self.editor_ctx.bank == bank && self.editor_ctx.pattern == pattern {
|
||||||
self.load_step_to_editor();
|
self.load_step_to_editor();
|
||||||
@@ -753,17 +695,14 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn copy_bank(&mut self, bank: usize) {
|
pub fn copy_bank(&mut self, bank: usize) {
|
||||||
let b = self.project_state.project.banks[bank].clone();
|
self.copied_bank = Some(clipboard::copy_bank(&self.project_state.project, bank));
|
||||||
self.copied_bank = Some(b);
|
|
||||||
self.ui.flash("Bank copied", 150, FlashKind::Success);
|
self.ui.flash("Bank copied", 150, FlashKind::Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn paste_bank(&mut self, bank: usize) {
|
pub fn paste_bank(&mut self, bank: usize) {
|
||||||
if let Some(src) = &self.copied_bank {
|
if let Some(src) = self.copied_bank.clone() {
|
||||||
let mut b = src.clone();
|
let pat_count = clipboard::paste_bank(&mut self.project_state.project, bank, &src);
|
||||||
b.name = Self::annotate_copy_name(&src.name);
|
for pattern in 0..pat_count {
|
||||||
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);
|
self.project_state.mark_dirty(bank, pattern);
|
||||||
}
|
}
|
||||||
if self.editor_ctx.bank == bank {
|
if self.editor_ctx.bank == bank {
|
||||||
@@ -776,36 +715,11 @@ impl App {
|
|||||||
pub fn harden_steps(&mut self) {
|
pub fn harden_steps(&mut self) {
|
||||||
let (bank, pattern) = self.current_bank_pattern();
|
let (bank, pattern) = self.current_bank_pattern();
|
||||||
let indices = self.selected_steps();
|
let indices = self.selected_steps();
|
||||||
|
let count = clipboard::harden_steps(&mut self.project_state.project, bank, pattern, &indices);
|
||||||
let pat = self.project_state.project.pattern_at(bank, pattern);
|
if count == 0 {
|
||||||
let resolutions: Vec<(usize, String)> = indices
|
|
||||||
.iter()
|
|
||||||
.filter_map(|&idx| {
|
|
||||||
let step = pat.step(idx)?;
|
|
||||||
step.source?;
|
|
||||||
let script = pat.resolve_script(idx)?.to_string();
|
|
||||||
Some((idx, script))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if resolutions.is_empty() {
|
|
||||||
self.ui.set_status("No linked steps to harden".to_string());
|
self.ui.set_status("No linked steps to harden".to_string());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let count = resolutions.len();
|
|
||||||
for (idx, script) in resolutions {
|
|
||||||
if let Some(s) = self
|
|
||||||
.project_state
|
|
||||||
.project
|
|
||||||
.pattern_at_mut(bank, pattern)
|
|
||||||
.step_mut(idx)
|
|
||||||
{
|
|
||||||
s.source = None;
|
|
||||||
s.script = script;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.project_state.mark_dirty(bank, pattern);
|
self.project_state.mark_dirty(bank, pattern);
|
||||||
self.load_step_to_editor();
|
self.load_step_to_editor();
|
||||||
self.editor_ctx.clear_selection();
|
self.editor_ctx.clear_selection();
|
||||||
@@ -819,36 +733,18 @@ impl App {
|
|||||||
|
|
||||||
pub fn copy_steps(&mut self) {
|
pub fn copy_steps(&mut self) {
|
||||||
let (bank, pattern) = self.current_bank_pattern();
|
let (bank, pattern) = self.current_bank_pattern();
|
||||||
let pat = self.project_state.project.pattern_at(bank, pattern);
|
|
||||||
let indices = self.selected_steps();
|
let indices = self.selected_steps();
|
||||||
|
let (copied, scripts) = clipboard::copy_steps(
|
||||||
let mut steps = Vec::new();
|
&self.project_state.project,
|
||||||
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(crate::state::CopiedStepData {
|
|
||||||
script: resolved,
|
|
||||||
active: step.active,
|
|
||||||
source: step.source,
|
|
||||||
original_index: idx,
|
|
||||||
name: step.name.clone(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let count = steps.len();
|
|
||||||
self.editor_ctx.copied_steps = Some(crate::state::CopiedSteps {
|
|
||||||
bank,
|
bank,
|
||||||
pattern,
|
pattern,
|
||||||
steps,
|
&indices,
|
||||||
});
|
);
|
||||||
|
let count = copied.steps.len();
|
||||||
|
self.editor_ctx.copied_steps = Some(copied);
|
||||||
if let Some(clip) = &mut self.clipboard {
|
if let Some(clip) = &mut self.clipboard {
|
||||||
let _ = clip.set_text(scripts.join("\n"));
|
let _ = clip.set_text(scripts.join("\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.ui
|
self.ui
|
||||||
.flash(&format!("Copied {count} steps"), 150, FlashKind::Info);
|
.flash(&format!("Copied {count} steps"), 150, FlashKind::Info);
|
||||||
}
|
}
|
||||||
@@ -858,54 +754,26 @@ impl App {
|
|||||||
self.ui.set_status("Nothing copied".to_string());
|
self.ui.set_status("Nothing copied".to_string());
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let (bank, pattern) = self.current_bank_pattern();
|
let (bank, pattern) = self.current_bank_pattern();
|
||||||
let pat_len = self.project_state.project.pattern_at(bank, pattern).length;
|
|
||||||
let cursor = self.editor_ctx.step;
|
let cursor = self.editor_ctx.step;
|
||||||
|
let result = clipboard::paste_steps(
|
||||||
let same_pattern = copied.bank == bank && copied.pattern == pattern;
|
&mut self.project_state.project,
|
||||||
for (i, data) in copied.steps.iter().enumerate() {
|
bank,
|
||||||
let target = cursor + i;
|
pattern,
|
||||||
if target >= pat_len {
|
cursor,
|
||||||
break;
|
&copied,
|
||||||
}
|
);
|
||||||
if let Some(step) = self
|
|
||||||
.project_state
|
|
||||||
.project
|
|
||||||
.pattern_at_mut(bank, pattern)
|
|
||||||
.step_mut(target)
|
|
||||||
{
|
|
||||||
let source = if same_pattern { data.source } else { None };
|
|
||||||
step.active = data.active;
|
|
||||||
step.source = source;
|
|
||||||
step.name = data.name.clone();
|
|
||||||
if source.is_some() {
|
|
||||||
step.script.clear();
|
|
||||||
step.command = None;
|
|
||||||
} else {
|
|
||||||
step.script = data.script.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.project_state.mark_dirty(bank, pattern);
|
self.project_state.mark_dirty(bank, pattern);
|
||||||
self.load_step_to_editor();
|
self.load_step_to_editor();
|
||||||
|
for &target in &result.compile_targets {
|
||||||
// Compile affected steps
|
let saved = self.editor_ctx.step;
|
||||||
for i in 0..copied.steps.len() {
|
|
||||||
let target = cursor + i;
|
|
||||||
if target >= pat_len {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let saved_step = self.editor_ctx.step;
|
|
||||||
self.editor_ctx.step = target;
|
self.editor_ctx.step = target;
|
||||||
self.compile_current_step(link);
|
self.compile_current_step(link);
|
||||||
self.editor_ctx.step = saved_step;
|
self.editor_ctx.step = saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.editor_ctx.clear_selection();
|
self.editor_ctx.clear_selection();
|
||||||
self.ui.flash(
|
self.ui.flash(
|
||||||
&format!("Pasted {} steps", copied.steps.len()),
|
&format!("Pasted {} steps", result.count),
|
||||||
150,
|
150,
|
||||||
FlashKind::Success,
|
FlashKind::Success,
|
||||||
);
|
);
|
||||||
@@ -916,111 +784,52 @@ impl App {
|
|||||||
self.ui.set_status("Nothing copied".to_string());
|
self.ui.set_status("Nothing copied".to_string());
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let (bank, pattern) = self.current_bank_pattern();
|
let (bank, pattern) = self.current_bank_pattern();
|
||||||
|
|
||||||
if copied.bank != bank || copied.pattern != pattern {
|
|
||||||
self.ui
|
|
||||||
.set_status("Can only link within same pattern".to_string());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let pat_len = self.project_state.project.pattern_at(bank, pattern).length;
|
|
||||||
let cursor = self.editor_ctx.step;
|
let cursor = self.editor_ctx.step;
|
||||||
|
match clipboard::link_paste_steps(
|
||||||
for (i, data) in copied.steps.iter().enumerate() {
|
&mut self.project_state.project,
|
||||||
let target = cursor + i;
|
bank,
|
||||||
if target >= pat_len {
|
pattern,
|
||||||
break;
|
cursor,
|
||||||
|
&copied,
|
||||||
|
) {
|
||||||
|
None => {
|
||||||
|
self.ui
|
||||||
|
.set_status("Can only link within same pattern".to_string());
|
||||||
}
|
}
|
||||||
let source_idx = if data.source.is_some() {
|
Some(count) => {
|
||||||
// Original was linked, link to same source
|
self.project_state.mark_dirty(bank, pattern);
|
||||||
data.source
|
self.load_step_to_editor();
|
||||||
} else {
|
self.editor_ctx.clear_selection();
|
||||||
Some(data.original_index)
|
self.ui.flash(
|
||||||
};
|
&format!("Linked {count} steps"),
|
||||||
if source_idx == Some(target) {
|
150,
|
||||||
continue;
|
FlashKind::Success,
|
||||||
}
|
);
|
||||||
if let Some(step) = self
|
|
||||||
.project_state
|
|
||||||
.project
|
|
||||||
.pattern_at_mut(bank, pattern)
|
|
||||||
.step_mut(target)
|
|
||||||
{
|
|
||||||
step.source = source_idx;
|
|
||||||
step.script.clear();
|
|
||||||
step.command = None;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.project_state.mark_dirty(bank, pattern);
|
|
||||||
self.load_step_to_editor();
|
|
||||||
self.editor_ctx.clear_selection();
|
|
||||||
self.ui.flash(
|
|
||||||
&format!("Linked {} steps", copied.steps.len()),
|
|
||||||
150,
|
|
||||||
FlashKind::Success,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn duplicate_steps(&mut self, link: &LinkState) {
|
pub fn duplicate_steps(&mut self, link: &LinkState) {
|
||||||
let (bank, pattern) = self.current_bank_pattern();
|
let (bank, pattern) = self.current_bank_pattern();
|
||||||
let pat = self.project_state.project.pattern_at(bank, pattern);
|
|
||||||
let pat_len = pat.length;
|
|
||||||
let indices = self.selected_steps();
|
let indices = self.selected_steps();
|
||||||
let count = indices.len();
|
let result = clipboard::duplicate_steps(
|
||||||
let paste_at = *indices.last().unwrap() + 1;
|
&mut self.project_state.project,
|
||||||
|
bank,
|
||||||
let dupe_data: Vec<(bool, String, Option<usize>)> = indices
|
pattern,
|
||||||
.iter()
|
&indices,
|
||||||
.filter_map(|&idx| {
|
);
|
||||||
let step = pat.step(idx)?;
|
|
||||||
let script = pat.resolve_script(idx).unwrap_or("").to_string();
|
|
||||||
let source = step.source;
|
|
||||||
Some((step.active, script, source))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut pasted = 0;
|
|
||||||
for (i, (active, script, source)) in dupe_data.into_iter().enumerate() {
|
|
||||||
let target = paste_at + i;
|
|
||||||
if target >= pat_len {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if let Some(step) = self
|
|
||||||
.project_state
|
|
||||||
.project
|
|
||||||
.pattern_at_mut(bank, pattern)
|
|
||||||
.step_mut(target)
|
|
||||||
{
|
|
||||||
step.active = active;
|
|
||||||
step.source = source;
|
|
||||||
if source.is_some() {
|
|
||||||
step.script.clear();
|
|
||||||
step.command = None;
|
|
||||||
} else {
|
|
||||||
step.script = script;
|
|
||||||
step.command = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pasted += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.project_state.mark_dirty(bank, pattern);
|
self.project_state.mark_dirty(bank, pattern);
|
||||||
self.load_step_to_editor();
|
self.load_step_to_editor();
|
||||||
|
for &target in &result.compile_targets {
|
||||||
for i in 0..pasted {
|
|
||||||
let target = paste_at + i;
|
|
||||||
let saved = self.editor_ctx.step;
|
let saved = self.editor_ctx.step;
|
||||||
self.editor_ctx.step = target;
|
self.editor_ctx.step = target;
|
||||||
self.compile_current_step(link);
|
self.compile_current_step(link);
|
||||||
self.editor_ctx.step = saved;
|
self.editor_ctx.step = saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.editor_ctx.clear_selection();
|
self.editor_ctx.clear_selection();
|
||||||
self.ui.flash(
|
self.ui.flash(
|
||||||
&format!("Duplicated {count} steps"),
|
&format!("Duplicated {} steps", result.count),
|
||||||
150,
|
150,
|
||||||
FlashKind::Success,
|
FlashKind::Success,
|
||||||
);
|
);
|
||||||
@@ -1232,100 +1041,28 @@ impl App {
|
|||||||
AppCommand::PageDown => self.page.down(),
|
AppCommand::PageDown => self.page.down(),
|
||||||
|
|
||||||
// Help navigation
|
// Help navigation
|
||||||
AppCommand::HelpToggleFocus => {
|
AppCommand::HelpToggleFocus => help_nav::toggle_focus(&mut self.ui),
|
||||||
use crate::state::HelpFocus;
|
AppCommand::HelpNextTopic(n) => help_nav::next_topic(&mut self.ui, n),
|
||||||
self.ui.help_focus = match self.ui.help_focus {
|
AppCommand::HelpPrevTopic(n) => help_nav::prev_topic(&mut self.ui, n),
|
||||||
HelpFocus::Topics => HelpFocus::Content,
|
AppCommand::HelpScrollDown(n) => help_nav::scroll_down(&mut self.ui, n),
|
||||||
HelpFocus::Content => HelpFocus::Topics,
|
AppCommand::HelpScrollUp(n) => help_nav::scroll_up(&mut self.ui, n),
|
||||||
};
|
AppCommand::HelpActivateSearch => help_nav::activate_search(&mut self.ui),
|
||||||
}
|
AppCommand::HelpClearSearch => help_nav::clear_search(&mut self.ui),
|
||||||
AppCommand::HelpNextTopic(n) => {
|
AppCommand::HelpSearchInput(c) => help_nav::search_input(&mut self.ui, c),
|
||||||
let count = docs::topic_count();
|
AppCommand::HelpSearchBackspace => help_nav::search_backspace(&mut self.ui),
|
||||||
self.ui.help_topic = (self.ui.help_topic + n) % count;
|
AppCommand::HelpSearchConfirm => help_nav::search_confirm(&mut self.ui),
|
||||||
}
|
|
||||||
AppCommand::HelpPrevTopic(n) => {
|
|
||||||
let count = docs::topic_count();
|
|
||||||
self.ui.help_topic = (self.ui.help_topic + count - (n % count)) % count;
|
|
||||||
}
|
|
||||||
AppCommand::HelpScrollDown(n) => {
|
|
||||||
let s = self.ui.help_scroll_mut();
|
|
||||||
*s = s.saturating_add(n);
|
|
||||||
}
|
|
||||||
AppCommand::HelpScrollUp(n) => {
|
|
||||||
let s = self.ui.help_scroll_mut();
|
|
||||||
*s = s.saturating_sub(n);
|
|
||||||
}
|
|
||||||
AppCommand::HelpActivateSearch => {
|
|
||||||
self.ui.help_search_active = true;
|
|
||||||
}
|
|
||||||
AppCommand::HelpClearSearch => {
|
|
||||||
self.ui.help_search_query.clear();
|
|
||||||
self.ui.help_search_active = false;
|
|
||||||
}
|
|
||||||
AppCommand::HelpSearchInput(c) => {
|
|
||||||
self.ui.help_search_query.push(c);
|
|
||||||
if let Some((topic, line)) = docs::find_match(&self.ui.help_search_query) {
|
|
||||||
self.ui.help_topic = topic;
|
|
||||||
self.ui.help_scrolls[topic] = line;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AppCommand::HelpSearchBackspace => {
|
|
||||||
self.ui.help_search_query.pop();
|
|
||||||
if self.ui.help_search_query.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if let Some((topic, line)) = docs::find_match(&self.ui.help_search_query) {
|
|
||||||
self.ui.help_topic = topic;
|
|
||||||
self.ui.help_scrolls[topic] = line;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AppCommand::HelpSearchConfirm => {
|
|
||||||
self.ui.help_search_active = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dictionary navigation
|
// Dictionary navigation
|
||||||
AppCommand::DictToggleFocus => {
|
AppCommand::DictToggleFocus => dict_nav::toggle_focus(&mut self.ui),
|
||||||
self.ui.dict_focus = match self.ui.dict_focus {
|
AppCommand::DictNextCategory => dict_nav::next_category(&mut self.ui),
|
||||||
DictFocus::Categories => DictFocus::Words,
|
AppCommand::DictPrevCategory => dict_nav::prev_category(&mut self.ui),
|
||||||
DictFocus::Words => DictFocus::Categories,
|
AppCommand::DictScrollDown(n) => dict_nav::scroll_down(&mut self.ui, n),
|
||||||
};
|
AppCommand::DictScrollUp(n) => dict_nav::scroll_up(&mut self.ui, n),
|
||||||
}
|
AppCommand::DictActivateSearch => dict_nav::activate_search(&mut self.ui),
|
||||||
AppCommand::DictNextCategory => {
|
AppCommand::DictClearSearch => dict_nav::clear_search(&mut self.ui),
|
||||||
let count = categories::category_count();
|
AppCommand::DictSearchInput(c) => dict_nav::search_input(&mut self.ui, c),
|
||||||
self.ui.dict_category = (self.ui.dict_category + 1) % count;
|
AppCommand::DictSearchBackspace => dict_nav::search_backspace(&mut self.ui),
|
||||||
}
|
AppCommand::DictSearchConfirm => dict_nav::search_confirm(&mut self.ui),
|
||||||
AppCommand::DictPrevCategory => {
|
|
||||||
let count = categories::category_count();
|
|
||||||
self.ui.dict_category = (self.ui.dict_category + count - 1) % count;
|
|
||||||
}
|
|
||||||
AppCommand::DictScrollDown(n) => {
|
|
||||||
let s = self.ui.dict_scroll_mut();
|
|
||||||
*s = s.saturating_add(n);
|
|
||||||
}
|
|
||||||
AppCommand::DictScrollUp(n) => {
|
|
||||||
let s = self.ui.dict_scroll_mut();
|
|
||||||
*s = s.saturating_sub(n);
|
|
||||||
}
|
|
||||||
AppCommand::DictActivateSearch => {
|
|
||||||
self.ui.dict_search_active = true;
|
|
||||||
self.ui.dict_focus = DictFocus::Words;
|
|
||||||
}
|
|
||||||
AppCommand::DictClearSearch => {
|
|
||||||
self.ui.dict_search_query.clear();
|
|
||||||
self.ui.dict_search_active = false;
|
|
||||||
*self.ui.dict_scroll_mut() = 0;
|
|
||||||
}
|
|
||||||
AppCommand::DictSearchInput(c) => {
|
|
||||||
self.ui.dict_search_query.push(c);
|
|
||||||
*self.ui.dict_scroll_mut() = 0;
|
|
||||||
}
|
|
||||||
AppCommand::DictSearchBackspace => {
|
|
||||||
self.ui.dict_search_query.pop();
|
|
||||||
*self.ui.dict_scroll_mut() = 0;
|
|
||||||
}
|
|
||||||
AppCommand::DictSearchConfirm => {
|
|
||||||
self.ui.dict_search_active = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Patterns view
|
// Patterns view
|
||||||
AppCommand::PatternsCursorLeft => {
|
AppCommand::PatternsCursorLeft => {
|
||||||
@@ -1381,6 +1118,9 @@ impl App {
|
|||||||
}
|
}
|
||||||
AppCommand::ToggleEditorStack => {
|
AppCommand::ToggleEditorStack => {
|
||||||
self.editor_ctx.show_stack = !self.editor_ctx.show_stack;
|
self.editor_ctx.show_stack = !self.editor_ctx.show_stack;
|
||||||
|
if self.editor_ctx.show_stack {
|
||||||
|
crate::services::stack_preview::update_cache(&self.editor_ctx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
AppCommand::SetColorScheme(scheme) => {
|
AppCommand::SetColorScheme(scheme) => {
|
||||||
self.ui.color_scheme = scheme;
|
self.ui.color_scheme = scheme;
|
||||||
@@ -1516,51 +1256,26 @@ impl App {
|
|||||||
steps,
|
steps,
|
||||||
rotation,
|
rotation,
|
||||||
} => {
|
} => {
|
||||||
let pat_len = self.project_state.project.pattern_at(bank, pattern).length;
|
let targets = euclidean::apply_distribution(
|
||||||
let rhythm = euclidean_rhythm(pulses, steps, rotation);
|
&mut self.project_state.project,
|
||||||
|
bank,
|
||||||
let mut created_count = 0;
|
pattern,
|
||||||
for (i, &is_hit) in rhythm.iter().enumerate() {
|
source_step,
|
||||||
if !is_hit {
|
pulses,
|
||||||
continue;
|
steps,
|
||||||
}
|
rotation,
|
||||||
|
);
|
||||||
let target = (source_step + i) % pat_len;
|
let created_count = targets.len();
|
||||||
|
|
||||||
if target == source_step {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(step) = self
|
|
||||||
.project_state
|
|
||||||
.project
|
|
||||||
.pattern_at_mut(bank, pattern)
|
|
||||||
.step_mut(target)
|
|
||||||
{
|
|
||||||
step.source = Some(source_step);
|
|
||||||
step.script.clear();
|
|
||||||
step.command = None;
|
|
||||||
step.active = true;
|
|
||||||
}
|
|
||||||
created_count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.project_state.mark_dirty(bank, pattern);
|
self.project_state.mark_dirty(bank, pattern);
|
||||||
|
for &target in &targets {
|
||||||
for (i, &is_hit) in rhythm.iter().enumerate() {
|
|
||||||
if !is_hit || i == 0 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let target = (source_step + i) % pat_len;
|
|
||||||
let saved = self.editor_ctx.step;
|
let saved = self.editor_ctx.step;
|
||||||
self.editor_ctx.step = target;
|
self.editor_ctx.step = target;
|
||||||
self.compile_current_step(link);
|
self.compile_current_step(link);
|
||||||
self.editor_ctx.step = saved;
|
self.editor_ctx.step = saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.load_step_to_editor();
|
self.load_step_to_editor();
|
||||||
self.ui.flash(
|
self.ui.flash(
|
||||||
&format!("Created {} linked steps (E({pulses},{steps}))", created_count),
|
&format!("Created {created_count} linked steps (E({pulses},{steps}))"),
|
||||||
200,
|
200,
|
||||||
FlashKind::Success,
|
FlashKind::Success,
|
||||||
);
|
);
|
||||||
@@ -1624,21 +1339,3 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn euclidean_rhythm(pulses: usize, steps: usize, rotation: usize) -> Vec<bool> {
|
|
||||||
if pulses == 0 || steps == 0 || pulses > steps {
|
|
||||||
return vec![false; steps];
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut pattern = vec![false; steps];
|
|
||||||
for i in 0..pulses {
|
|
||||||
let pos = (i * steps) / pulses;
|
|
||||||
pattern[pos] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if rotation > 0 {
|
|
||||||
pattern.rotate_left(rotation % steps);
|
|
||||||
}
|
|
||||||
|
|
||||||
pattern
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
use crossbeam_channel::{bounded, unbounded, Receiver, Sender};
|
use crossbeam_channel::{bounded, unbounded, Receiver, Sender};
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use rand::rngs::StdRng;
|
||||||
|
use rand::SeedableRng;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
use std::sync::atomic::AtomicU32;
|
use std::sync::atomic::AtomicU32;
|
||||||
@@ -120,6 +123,7 @@ pub enum SeqCommand {
|
|||||||
soloed: std::collections::HashSet<(usize, usize)>,
|
soloed: std::collections::HashSet<(usize, usize)>,
|
||||||
},
|
},
|
||||||
StopAll,
|
StopAll,
|
||||||
|
ResetScriptState,
|
||||||
Shutdown,
|
Shutdown,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,13 +278,9 @@ pub struct SequencerConfig {
|
|||||||
pub mouse_down: Arc<AtomicU32>,
|
pub mouse_down: Arc<AtomicU32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn spawn_sequencer(
|
pub fn spawn_sequencer(
|
||||||
link: Arc<LinkState>,
|
link: Arc<LinkState>,
|
||||||
playing: Arc<std::sync::atomic::AtomicBool>,
|
playing: Arc<std::sync::atomic::AtomicBool>,
|
||||||
variables: Variables,
|
|
||||||
dict: Dictionary,
|
|
||||||
rng: Rng,
|
|
||||||
quantum: f64,
|
quantum: f64,
|
||||||
live_keys: Arc<LiveKeyState>,
|
live_keys: Arc<LiveKeyState>,
|
||||||
nudge_us: Arc<AtomicI64>,
|
nudge_us: Arc<AtomicI64>,
|
||||||
@@ -329,9 +329,6 @@ pub fn spawn_sequencer(
|
|||||||
sequencer_audio_tx,
|
sequencer_audio_tx,
|
||||||
link,
|
link,
|
||||||
playing,
|
playing,
|
||||||
variables,
|
|
||||||
dict,
|
|
||||||
rng,
|
|
||||||
quantum,
|
quantum,
|
||||||
shared_state_clone,
|
shared_state_clone,
|
||||||
live_keys,
|
live_keys,
|
||||||
@@ -667,6 +664,15 @@ impl SequencerState {
|
|||||||
self.runs_counter.counts.clear();
|
self.runs_counter.counts.clear();
|
||||||
self.audio_state.flush_midi_notes = true;
|
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;
|
||||||
|
self.speed_overrides.clear();
|
||||||
|
}
|
||||||
SeqCommand::Shutdown => {}
|
SeqCommand::Shutdown => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1063,9 +1069,6 @@ fn sequencer_loop(
|
|||||||
audio_tx: Arc<ArcSwap<Sender<AudioCommand>>>,
|
audio_tx: Arc<ArcSwap<Sender<AudioCommand>>>,
|
||||||
link: Arc<LinkState>,
|
link: Arc<LinkState>,
|
||||||
playing: Arc<std::sync::atomic::AtomicBool>,
|
playing: Arc<std::sync::atomic::AtomicBool>,
|
||||||
variables: Variables,
|
|
||||||
dict: Dictionary,
|
|
||||||
rng: Rng,
|
|
||||||
quantum: f64,
|
quantum: f64,
|
||||||
shared_state: Arc<ArcSwap<SharedSequencerState>>,
|
shared_state: Arc<ArcSwap<SharedSequencerState>>,
|
||||||
live_keys: Arc<LiveKeyState>,
|
live_keys: Arc<LiveKeyState>,
|
||||||
@@ -1093,6 +1096,9 @@ fn sequencer_loop(
|
|||||||
eprintln!("[cagire] Then log out and back in.");
|
eprintln!("[cagire] Then log out and back in.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
let mut seq_state = SequencerState::new(variables, dict, rng, cc_access);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
|||||||
@@ -149,9 +149,6 @@ pub fn init(args: InitArgs) -> Init {
|
|||||||
let (sequencer, initial_audio_rx, midi_rx) = spawn_sequencer(
|
let (sequencer, initial_audio_rx, midi_rx) = spawn_sequencer(
|
||||||
Arc::clone(&link),
|
Arc::clone(&link),
|
||||||
Arc::clone(&playing),
|
Arc::clone(&playing),
|
||||||
Arc::clone(&app.variables),
|
|
||||||
Arc::clone(&app.dict),
|
|
||||||
Arc::clone(&app.rng),
|
|
||||||
settings.link.quantum,
|
settings.link.quantum,
|
||||||
Arc::clone(&app.live_keys),
|
Arc::clone(&app.live_keys),
|
||||||
Arc::clone(&nudge_us),
|
Arc::clone(&nudge_us),
|
||||||
|
|||||||
@@ -259,6 +259,7 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
FileBrowserMode::Save => ctx.dispatch(AppCommand::Save(path)),
|
FileBrowserMode::Save => ctx.dispatch(AppCommand::Save(path)),
|
||||||
FileBrowserMode::Load => {
|
FileBrowserMode::Load => {
|
||||||
let _ = ctx.seq_cmd_tx.send(SeqCommand::StopAll);
|
let _ = ctx.seq_cmd_tx.send(SeqCommand::StopAll);
|
||||||
|
let _ = ctx.seq_cmd_tx.send(SeqCommand::ResetScriptState);
|
||||||
ctx.dispatch(AppCommand::Load(path));
|
ctx.dispatch(AppCommand::Load(path));
|
||||||
load_project_samples(ctx);
|
load_project_samples(ctx);
|
||||||
}
|
}
|
||||||
@@ -556,6 +557,10 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
|||||||
editor.input(Event::Key(key));
|
editor.input(Event::Key(key));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ctx.app.editor_ctx.show_stack {
|
||||||
|
crate::services::stack_preview::update_cache(&ctx.app.editor_ctx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Modal::Preview => match key.code {
|
Modal::Preview => match key.code {
|
||||||
KeyCode::Esc | KeyCode::Char('p') => ctx.dispatch(AppCommand::CloseModal),
|
KeyCode::Esc | KeyCode::Char('p') => ctx.dispatch(AppCommand::CloseModal),
|
||||||
|
|||||||
@@ -242,6 +242,9 @@ fn main() -> io::Result<()> {
|
|||||||
Event::Paste(text) => {
|
Event::Paste(text) => {
|
||||||
if matches!(app.ui.modal, state::Modal::Editor) {
|
if matches!(app.ui.modal, state::Modal::Editor) {
|
||||||
app.editor_ctx.editor.insert_str(&text);
|
app.editor_ctx.editor.insert_str(&text);
|
||||||
|
if app.editor_ctx.show_stack {
|
||||||
|
services::stack_preview::update_cache(&app.editor_ctx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use midir::{MidiInput, MidiOutput};
|
use midir::{MidiInput, MidiOutput};
|
||||||
|
|
||||||
use cagire_forth::CcAccess;
|
use crate::model::CcAccess;
|
||||||
|
|
||||||
pub const MAX_MIDI_OUTPUTS: usize = 4;
|
pub const MAX_MIDI_OUTPUTS: usize = 4;
|
||||||
pub const MAX_MIDI_INPUTS: usize = 4;
|
pub const MAX_MIDI_INPUTS: usize = 4;
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ pub mod categories;
|
|||||||
pub mod docs;
|
pub mod docs;
|
||||||
mod script;
|
mod script;
|
||||||
|
|
||||||
pub use cagire_forth::{lookup_word, Word, WordCompile, WORDS};
|
pub use cagire_forth::{
|
||||||
|
lookup_word, CcAccess, Dictionary, ExecutionTrace, Rng, SourceSpan, StepContext, Value,
|
||||||
|
Variables, Word, WordCompile, WORDS,
|
||||||
|
};
|
||||||
pub use cagire_project::{
|
pub use cagire_project::{
|
||||||
load, save, Bank, LaunchQuantization, Pattern, PatternSpeed, Project, SyncMode, MAX_BANKS,
|
load, save, Bank, LaunchQuantization, Pattern, PatternSpeed, Project, SyncMode, MAX_BANKS,
|
||||||
MAX_PATTERNS,
|
MAX_PATTERNS,
|
||||||
};
|
};
|
||||||
pub use script::{
|
pub use script::ScriptEngine;
|
||||||
CcAccess, Dictionary, ExecutionTrace, Rng, ScriptEngine, SourceSpan, StepContext, Value,
|
|
||||||
Variables,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
use cagire_forth::Forth;
|
use cagire_forth::{Dictionary, ExecutionTrace, Forth, Rng, StepContext, Value, Variables};
|
||||||
|
|
||||||
pub use cagire_forth::{
|
|
||||||
CcAccess, Dictionary, ExecutionTrace, Rng, SourceSpan, StepContext, Value, Variables,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct ScriptEngine {
|
pub struct ScriptEngine {
|
||||||
forth: Forth,
|
forth: Forth,
|
||||||
@@ -27,4 +23,8 @@ impl ScriptEngine {
|
|||||||
) -> Result<Vec<String>, String> {
|
) -> Result<Vec<String>, String> {
|
||||||
self.forth.evaluate_with_trace(script, ctx, trace)
|
self.forth.evaluate_with_trace(script, ctx, trace)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn stack(&self) -> Vec<Value> {
|
||||||
|
self.forth.stack()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
220
src/services/clipboard.rs
Normal file
220
src/services/clipboard.rs
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
use crate::model::{Bank, Pattern, Project};
|
||||||
|
use crate::state::{CopiedStepData, CopiedSteps};
|
||||||
|
|
||||||
|
fn annotate_copy_name(name: &Option<String>) -> Option<String> {
|
||||||
|
match name {
|
||||||
|
Some(n) if !n.ends_with(" (copy)") => Some(format!("{n} (copy)")),
|
||||||
|
Some(n) => Some(n.clone()),
|
||||||
|
None => Some("(copy)".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy_pattern(project: &Project, bank: usize, pattern: usize) -> Pattern {
|
||||||
|
project.banks[bank].patterns[pattern].clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn paste_pattern(
|
||||||
|
project: &mut Project,
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
source: &Pattern,
|
||||||
|
) {
|
||||||
|
let mut pat = source.clone();
|
||||||
|
pat.name = annotate_copy_name(&source.name);
|
||||||
|
project.banks[bank].patterns[pattern] = pat;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy_bank(project: &Project, bank: usize) -> Bank {
|
||||||
|
project.banks[bank].clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn paste_bank(project: &mut Project, bank: usize, source: &Bank) -> usize {
|
||||||
|
let mut b = source.clone();
|
||||||
|
b.name = annotate_copy_name(&source.name);
|
||||||
|
project.banks[bank] = b;
|
||||||
|
project.banks[bank].patterns.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy_steps(
|
||||||
|
project: &Project,
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
indices: &[usize],
|
||||||
|
) -> (CopiedSteps, Vec<String>) {
|
||||||
|
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,
|
||||||
|
active: step.active,
|
||||||
|
source: step.source,
|
||||||
|
original_index: idx,
|
||||||
|
name: step.name.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let copied = CopiedSteps {
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
steps,
|
||||||
|
};
|
||||||
|
(copied, scripts)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PasteResult {
|
||||||
|
pub count: usize,
|
||||||
|
pub compile_targets: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn paste_steps(
|
||||||
|
project: &mut Project,
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
cursor: usize,
|
||||||
|
copied: &CopiedSteps,
|
||||||
|
) -> PasteResult {
|
||||||
|
let pat_len = project.pattern_at(bank, pattern).length;
|
||||||
|
let same_pattern = copied.bank == bank && copied.pattern == pattern;
|
||||||
|
let mut compile_targets = Vec::new();
|
||||||
|
|
||||||
|
for (i, data) in copied.steps.iter().enumerate() {
|
||||||
|
let target = cursor + i;
|
||||||
|
if target >= pat_len {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if let Some(step) = project.pattern_at_mut(bank, pattern).step_mut(target) {
|
||||||
|
let source = if same_pattern { data.source } else { None };
|
||||||
|
step.active = data.active;
|
||||||
|
step.source = source;
|
||||||
|
step.name = data.name.clone();
|
||||||
|
if source.is_some() {
|
||||||
|
step.script.clear();
|
||||||
|
step.command = None;
|
||||||
|
} else {
|
||||||
|
step.script = data.script.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compile_targets.push(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
PasteResult {
|
||||||
|
count: copied.steps.len(),
|
||||||
|
compile_targets,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn link_paste_steps(
|
||||||
|
project: &mut Project,
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
cursor: usize,
|
||||||
|
copied: &CopiedSteps,
|
||||||
|
) -> Option<usize> {
|
||||||
|
if copied.bank != bank || copied.pattern != pattern {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pat_len = project.pattern_at(bank, pattern).length;
|
||||||
|
|
||||||
|
for (i, data) in copied.steps.iter().enumerate() {
|
||||||
|
let target = cursor + i;
|
||||||
|
if target >= pat_len {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let source_idx = if data.source.is_some() {
|
||||||
|
data.source
|
||||||
|
} else {
|
||||||
|
Some(data.original_index)
|
||||||
|
};
|
||||||
|
if source_idx == Some(target) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(step) = project.pattern_at_mut(bank, pattern).step_mut(target) {
|
||||||
|
step.source = source_idx;
|
||||||
|
step.script.clear();
|
||||||
|
step.command = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(copied.steps.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn harden_steps(
|
||||||
|
project: &mut Project,
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
indices: &[usize],
|
||||||
|
) -> usize {
|
||||||
|
let pat = project.pattern_at(bank, pattern);
|
||||||
|
let resolutions: Vec<(usize, String)> = indices
|
||||||
|
.iter()
|
||||||
|
.filter_map(|&idx| {
|
||||||
|
let step = pat.step(idx)?;
|
||||||
|
step.source?;
|
||||||
|
let script = pat.resolve_script(idx)?.to_string();
|
||||||
|
Some((idx, script))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let count = resolutions.len();
|
||||||
|
for (idx, script) in resolutions {
|
||||||
|
if let Some(s) = project.pattern_at_mut(bank, pattern).step_mut(idx) {
|
||||||
|
s.source = None;
|
||||||
|
s.script = script;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
count
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn duplicate_steps(
|
||||||
|
project: &mut Project,
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
indices: &[usize],
|
||||||
|
) -> PasteResult {
|
||||||
|
let pat = project.pattern_at(bank, pattern);
|
||||||
|
let pat_len = pat.length;
|
||||||
|
let paste_at = *indices.last().unwrap() + 1;
|
||||||
|
|
||||||
|
let dupe_data: Vec<(bool, String, Option<usize>)> = indices
|
||||||
|
.iter()
|
||||||
|
.filter_map(|&idx| {
|
||||||
|
let step = pat.step(idx)?;
|
||||||
|
let script = pat.resolve_script(idx).unwrap_or("").to_string();
|
||||||
|
let source = step.source;
|
||||||
|
Some((step.active, script, source))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut compile_targets = Vec::new();
|
||||||
|
for (i, (active, script, source)) in dupe_data.into_iter().enumerate() {
|
||||||
|
let target = paste_at + i;
|
||||||
|
if target >= pat_len {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if let Some(step) = project.pattern_at_mut(bank, pattern).step_mut(target) {
|
||||||
|
step.active = active;
|
||||||
|
step.source = source;
|
||||||
|
if source.is_some() {
|
||||||
|
step.script.clear();
|
||||||
|
step.command = None;
|
||||||
|
} else {
|
||||||
|
step.script = script;
|
||||||
|
step.command = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compile_targets.push(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
PasteResult {
|
||||||
|
count: indices.len(),
|
||||||
|
compile_targets,
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/services/dict_nav.rs
Normal file
54
src/services/dict_nav.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
use crate::model::categories;
|
||||||
|
use crate::state::{DictFocus, UiState};
|
||||||
|
|
||||||
|
pub fn toggle_focus(ui: &mut UiState) {
|
||||||
|
ui.dict_focus = match ui.dict_focus {
|
||||||
|
DictFocus::Categories => DictFocus::Words,
|
||||||
|
DictFocus::Words => DictFocus::Categories,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_category(ui: &mut UiState) {
|
||||||
|
let count = categories::category_count();
|
||||||
|
ui.dict_category = (ui.dict_category + 1) % count;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prev_category(ui: &mut UiState) {
|
||||||
|
let count = categories::category_count();
|
||||||
|
ui.dict_category = (ui.dict_category + count - 1) % count;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scroll_down(ui: &mut UiState, n: usize) {
|
||||||
|
let s = ui.dict_scroll_mut();
|
||||||
|
*s = s.saturating_add(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scroll_up(ui: &mut UiState, n: usize) {
|
||||||
|
let s = ui.dict_scroll_mut();
|
||||||
|
*s = s.saturating_sub(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn activate_search(ui: &mut UiState) {
|
||||||
|
ui.dict_search_active = true;
|
||||||
|
ui.dict_focus = DictFocus::Words;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_search(ui: &mut UiState) {
|
||||||
|
ui.dict_search_query.clear();
|
||||||
|
ui.dict_search_active = false;
|
||||||
|
*ui.dict_scroll_mut() = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search_input(ui: &mut UiState, c: char) {
|
||||||
|
ui.dict_search_query.push(c);
|
||||||
|
*ui.dict_scroll_mut() = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search_backspace(ui: &mut UiState) {
|
||||||
|
ui.dict_search_query.pop();
|
||||||
|
*ui.dict_scroll_mut() = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search_confirm(ui: &mut UiState) {
|
||||||
|
ui.dict_search_active = false;
|
||||||
|
}
|
||||||
56
src/services/euclidean.rs
Normal file
56
src/services/euclidean.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
use crate::model::Project;
|
||||||
|
|
||||||
|
pub fn euclidean_rhythm(pulses: usize, steps: usize, rotation: usize) -> Vec<bool> {
|
||||||
|
if pulses == 0 || steps == 0 || pulses > steps {
|
||||||
|
return vec![false; steps];
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pattern = vec![false; steps];
|
||||||
|
for i in 0..pulses {
|
||||||
|
let pos = (i * steps) / pulses;
|
||||||
|
pattern[pos] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if rotation > 0 {
|
||||||
|
pattern.rotate_left(rotation % steps);
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies euclidean distribution as linked steps from a source step.
|
||||||
|
/// Returns the indices of steps that were created (for compilation).
|
||||||
|
pub fn apply_distribution(
|
||||||
|
project: &mut Project,
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
source_step: usize,
|
||||||
|
pulses: usize,
|
||||||
|
steps: usize,
|
||||||
|
rotation: usize,
|
||||||
|
) -> Vec<usize> {
|
||||||
|
let pat_len = project.pattern_at(bank, pattern).length;
|
||||||
|
let rhythm = euclidean_rhythm(pulses, steps, rotation);
|
||||||
|
|
||||||
|
let mut targets = Vec::new();
|
||||||
|
for (i, &is_hit) in rhythm.iter().enumerate() {
|
||||||
|
if !is_hit || i == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let target = (source_step + i) % pat_len;
|
||||||
|
if target == source_step {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(step) = project.pattern_at_mut(bank, pattern).step_mut(target) {
|
||||||
|
step.source = Some(source_step);
|
||||||
|
step.script.clear();
|
||||||
|
step.command = None;
|
||||||
|
step.active = true;
|
||||||
|
}
|
||||||
|
targets.push(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
targets
|
||||||
|
}
|
||||||
61
src/services/help_nav.rs
Normal file
61
src/services/help_nav.rs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
use crate::model::docs;
|
||||||
|
use crate::state::{HelpFocus, UiState};
|
||||||
|
|
||||||
|
pub fn toggle_focus(ui: &mut UiState) {
|
||||||
|
ui.help_focus = match ui.help_focus {
|
||||||
|
HelpFocus::Topics => HelpFocus::Content,
|
||||||
|
HelpFocus::Content => HelpFocus::Topics,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_topic(ui: &mut UiState, n: usize) {
|
||||||
|
let count = docs::topic_count();
|
||||||
|
ui.help_topic = (ui.help_topic + n) % count;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prev_topic(ui: &mut UiState, n: usize) {
|
||||||
|
let count = docs::topic_count();
|
||||||
|
ui.help_topic = (ui.help_topic + count - (n % count)) % count;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scroll_down(ui: &mut UiState, n: usize) {
|
||||||
|
let s = ui.help_scroll_mut();
|
||||||
|
*s = s.saturating_add(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scroll_up(ui: &mut UiState, n: usize) {
|
||||||
|
let s = ui.help_scroll_mut();
|
||||||
|
*s = s.saturating_sub(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn activate_search(ui: &mut UiState) {
|
||||||
|
ui.help_search_active = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_search(ui: &mut UiState) {
|
||||||
|
ui.help_search_query.clear();
|
||||||
|
ui.help_search_active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search_input(ui: &mut UiState, c: char) {
|
||||||
|
ui.help_search_query.push(c);
|
||||||
|
if let Some((topic, line)) = docs::find_match(&ui.help_search_query) {
|
||||||
|
ui.help_topic = topic;
|
||||||
|
ui.help_scrolls[topic] = line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search_backspace(ui: &mut UiState) {
|
||||||
|
ui.help_search_query.pop();
|
||||||
|
if ui.help_search_query.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some((topic, line)) = docs::find_match(&ui.help_search_query) {
|
||||||
|
ui.help_topic = topic;
|
||||||
|
ui.help_scrolls[topic] = line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search_confirm(ui: &mut UiState) {
|
||||||
|
ui.help_search_active = false;
|
||||||
|
}
|
||||||
@@ -1 +1,6 @@
|
|||||||
|
pub mod clipboard;
|
||||||
|
pub mod dict_nav;
|
||||||
|
pub mod euclidean;
|
||||||
|
pub mod help_nav;
|
||||||
pub mod pattern_editor;
|
pub mod pattern_editor;
|
||||||
|
pub mod stack_preview;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::model::{PatternSpeed, Project};
|
use crate::model::{Bank, Pattern, PatternSpeed, Project};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct PatternEdit {
|
pub struct PatternEdit {
|
||||||
@@ -90,3 +90,38 @@ pub fn get_step_script(
|
|||||||
.step(step)
|
.step(step)
|
||||||
.map(|s| s.script.clone())
|
.map(|s| s.script.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn delete_step(project: &mut Project, bank: usize, pattern: usize, step: usize) -> PatternEdit {
|
||||||
|
let pat = 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set_step_script(project, bank, pattern, step, String::new());
|
||||||
|
if let Some(s) = project.pattern_at_mut(bank, pattern).step_mut(step) {
|
||||||
|
s.command = None;
|
||||||
|
s.source = None;
|
||||||
|
}
|
||||||
|
PatternEdit::new(bank, pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_steps(project: &mut Project, bank: usize, pattern: usize, steps: &[usize]) -> PatternEdit {
|
||||||
|
for &step in steps {
|
||||||
|
delete_step(project, bank, pattern, step);
|
||||||
|
}
|
||||||
|
PatternEdit::new(bank, pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_pattern(project: &mut Project, bank: usize, pattern: usize) -> PatternEdit {
|
||||||
|
project.banks[bank].patterns[pattern] = Pattern::default();
|
||||||
|
PatternEdit::new(bank, pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_bank(project: &mut Project, bank: usize) -> usize {
|
||||||
|
project.banks[bank] = Bank::default();
|
||||||
|
project.banks[bank].patterns.len()
|
||||||
|
}
|
||||||
|
|||||||
106
src/services/stack_preview.rs
Normal file
106
src/services/stack_preview.rs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
use arc_swap::ArcSwap;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use std::collections::hash_map::DefaultHasher;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use rand::rngs::StdRng;
|
||||||
|
use rand::SeedableRng;
|
||||||
|
|
||||||
|
use crate::model::{ScriptEngine, StepContext, Value};
|
||||||
|
use crate::state::{EditorContext, StackCache};
|
||||||
|
|
||||||
|
pub fn update_cache(editor_ctx: &EditorContext) {
|
||||||
|
let lines = editor_ctx.editor.lines();
|
||||||
|
let cursor_line = editor_ctx.editor.cursor().0;
|
||||||
|
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
for (i, line) in lines.iter().enumerate() {
|
||||||
|
if i > cursor_line {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
line.hash(&mut hasher);
|
||||||
|
}
|
||||||
|
let lines_hash = hasher.finish();
|
||||||
|
|
||||||
|
if let Some(ref c) = *editor_ctx.stack_cache.borrow() {
|
||||||
|
if c.cursor_line == cursor_line && c.lines_hash == lines_hash {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let partial: Vec<&str> = lines
|
||||||
|
.iter()
|
||||||
|
.take(cursor_line + 1)
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.collect();
|
||||||
|
let script = partial.join("\n");
|
||||||
|
|
||||||
|
let result = if script.trim().is_empty() {
|
||||||
|
"Stack: []".to_string()
|
||||||
|
} else {
|
||||||
|
let vars = Arc::new(ArcSwap::from_pointee(HashMap::new()));
|
||||||
|
let dict = Arc::new(Mutex::new(HashMap::new()));
|
||||||
|
let rng = Arc::new(Mutex::new(StdRng::seed_from_u64(42)));
|
||||||
|
let engine = ScriptEngine::new(vars, dict, rng);
|
||||||
|
|
||||||
|
let ctx = StepContext {
|
||||||
|
step: 0,
|
||||||
|
beat: 0.0,
|
||||||
|
bank: 0,
|
||||||
|
pattern: 0,
|
||||||
|
tempo: 120.0,
|
||||||
|
phase: 0.0,
|
||||||
|
slot: 0,
|
||||||
|
runs: 0,
|
||||||
|
iter: 0,
|
||||||
|
speed: 1.0,
|
||||||
|
fill: false,
|
||||||
|
nudge_secs: 0.0,
|
||||||
|
cc_access: None,
|
||||||
|
speed_key: "",
|
||||||
|
chain_key: "",
|
||||||
|
#[cfg(feature = "desktop")]
|
||||||
|
mouse_x: 0.5,
|
||||||
|
#[cfg(feature = "desktop")]
|
||||||
|
mouse_y: 0.5,
|
||||||
|
#[cfg(feature = "desktop")]
|
||||||
|
mouse_down: 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
match engine.evaluate(&script, &ctx) {
|
||||||
|
Ok(_) => {
|
||||||
|
let stack = engine.stack();
|
||||||
|
let formatted: Vec<String> = stack.iter().map(format_value).collect();
|
||||||
|
format!("Stack: [{}]", formatted.join(" "))
|
||||||
|
}
|
||||||
|
Err(e) => format!("Error: {e}"),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
*editor_ctx.stack_cache.borrow_mut() = Some(StackCache {
|
||||||
|
cursor_line,
|
||||||
|
lines_hash,
|
||||||
|
result,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_value(v: &Value) -> String {
|
||||||
|
match v {
|
||||||
|
Value::Int(n, _) => n.to_string(),
|
||||||
|
Value::Float(f, _) => {
|
||||||
|
if f.fract() == 0.0 && f.abs() < 1_000_000.0 {
|
||||||
|
format!("{f:.1}")
|
||||||
|
} else {
|
||||||
|
format!("{f:.4}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::Str(s, _) => format!("\"{s}\""),
|
||||||
|
Value::Quotation(..) => "[...]".to_string(),
|
||||||
|
Value::CycleList(items) => {
|
||||||
|
let inner: Vec<String> = items.iter().map(format_value).collect();
|
||||||
|
format!("({})", inner.join(" "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,5 @@
|
|||||||
use arc_swap::ArcSwap;
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use std::collections::hash_map::DefaultHasher;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::hash::{Hash, Hasher};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use rand::rngs::StdRng;
|
|
||||||
use rand::SeedableRng;
|
|
||||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||||
use ratatui::style::{Modifier, Style};
|
use ratatui::style::{Modifier, Style};
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
@@ -16,121 +8,21 @@ use ratatui::Frame;
|
|||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::engine::{LinkState, SequencerSnapshot};
|
use crate::engine::{LinkState, SequencerSnapshot};
|
||||||
use crate::model::{SourceSpan, StepContext, Value};
|
use crate::model::SourceSpan;
|
||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
EuclideanField, FlashKind, Modal, PanelFocus, PatternField, SidePanel, StackCache,
|
EuclideanField, FlashKind, Modal, PanelFocus, PatternField, SidePanel,
|
||||||
};
|
};
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use crate::views::highlight::{self, highlight_line, highlight_line_with_runtime};
|
use crate::views::highlight::{self, highlight_line, highlight_line_with_runtime};
|
||||||
use crate::widgets::{
|
use crate::widgets::{
|
||||||
ConfirmModal, ModalFrame, NavMinimap, NavTile, SampleBrowser, TextInputModal,
|
ConfirmModal, ModalFrame, NavMinimap, NavTile, SampleBrowser, TextInputModal,
|
||||||
};
|
};
|
||||||
use cagire_forth::Forth;
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
dict_view, engine_view, help_view, main_view, options_view, patterns_view, title_view,
|
dict_view, engine_view, help_view, main_view, options_view, patterns_view, title_view,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn compute_stack_display(
|
|
||||||
lines: &[String],
|
|
||||||
editor: &cagire_ratatui::Editor,
|
|
||||||
cache: &std::cell::RefCell<Option<StackCache>>,
|
|
||||||
) -> String {
|
|
||||||
let cursor_line = editor.cursor().0;
|
|
||||||
|
|
||||||
let mut hasher = DefaultHasher::new();
|
|
||||||
for (i, line) in lines.iter().enumerate() {
|
|
||||||
if i > cursor_line {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
line.hash(&mut hasher);
|
|
||||||
}
|
|
||||||
let lines_hash = hasher.finish();
|
|
||||||
|
|
||||||
if let Some(ref c) = *cache.borrow() {
|
|
||||||
if c.cursor_line == cursor_line && c.lines_hash == lines_hash {
|
|
||||||
return c.result.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let partial: Vec<&str> = lines
|
|
||||||
.iter()
|
|
||||||
.take(cursor_line + 1)
|
|
||||||
.map(|s| s.as_str())
|
|
||||||
.collect();
|
|
||||||
let script = partial.join("\n");
|
|
||||||
|
|
||||||
let result = if script.trim().is_empty() {
|
|
||||||
"Stack: []".to_string()
|
|
||||||
} else {
|
|
||||||
let vars = Arc::new(ArcSwap::from_pointee(HashMap::new()));
|
|
||||||
let dict = Arc::new(Mutex::new(HashMap::new()));
|
|
||||||
let rng = Arc::new(Mutex::new(StdRng::seed_from_u64(42)));
|
|
||||||
let forth = Forth::new(vars, dict, rng);
|
|
||||||
|
|
||||||
let ctx = StepContext {
|
|
||||||
step: 0,
|
|
||||||
beat: 0.0,
|
|
||||||
bank: 0,
|
|
||||||
pattern: 0,
|
|
||||||
tempo: 120.0,
|
|
||||||
phase: 0.0,
|
|
||||||
slot: 0,
|
|
||||||
runs: 0,
|
|
||||||
iter: 0,
|
|
||||||
speed: 1.0,
|
|
||||||
fill: false,
|
|
||||||
nudge_secs: 0.0,
|
|
||||||
cc_access: None,
|
|
||||||
speed_key: "",
|
|
||||||
chain_key: "",
|
|
||||||
#[cfg(feature = "desktop")]
|
|
||||||
mouse_x: 0.5,
|
|
||||||
#[cfg(feature = "desktop")]
|
|
||||||
mouse_y: 0.5,
|
|
||||||
#[cfg(feature = "desktop")]
|
|
||||||
mouse_down: 0.0,
|
|
||||||
};
|
|
||||||
|
|
||||||
match forth.evaluate(&script, &ctx) {
|
|
||||||
Ok(_) => {
|
|
||||||
let stack = forth.stack();
|
|
||||||
let formatted: Vec<String> = stack.iter().map(format_value).collect();
|
|
||||||
format!("Stack: [{}]", formatted.join(" "))
|
|
||||||
}
|
|
||||||
Err(e) => format!("Error: {e}"),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
*cache.borrow_mut() = Some(StackCache {
|
|
||||||
cursor_line,
|
|
||||||
lines_hash,
|
|
||||||
result: result.clone(),
|
|
||||||
});
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_value(v: &Value) -> String {
|
|
||||||
match v {
|
|
||||||
Value::Int(n, _) => n.to_string(),
|
|
||||||
Value::Float(f, _) => {
|
|
||||||
if f.fract() == 0.0 && f.abs() < 1_000_000.0 {
|
|
||||||
format!("{f:.1}")
|
|
||||||
} else {
|
|
||||||
format!("{f:.4}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Value::Str(s, _) => format!("\"{s}\""),
|
|
||||||
Value::Quotation(..) => "[...]".to_string(),
|
|
||||||
Value::CycleList(items) => {
|
|
||||||
let inner: Vec<String> = items.iter().map(format_value).collect();
|
|
||||||
format!("({})", inner.join(" "))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn adjust_spans_for_line(
|
fn adjust_spans_for_line(
|
||||||
spans: &[SourceSpan],
|
spans: &[SourceSpan],
|
||||||
line_start: usize,
|
line_start: usize,
|
||||||
@@ -850,11 +742,13 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
|||||||
]);
|
]);
|
||||||
frame.render_widget(Paragraph::new(hint).alignment(Alignment::Right), hint_area);
|
frame.render_widget(Paragraph::new(hint).alignment(Alignment::Right), hint_area);
|
||||||
} else if app.editor_ctx.show_stack {
|
} else if app.editor_ctx.show_stack {
|
||||||
let stack_text = compute_stack_display(
|
let stack_text = app
|
||||||
text_lines,
|
.editor_ctx
|
||||||
&app.editor_ctx.editor,
|
.stack_cache
|
||||||
&app.editor_ctx.stack_cache,
|
.borrow()
|
||||||
);
|
.as_ref()
|
||||||
|
.map(|c| c.result.clone())
|
||||||
|
.unwrap_or_else(|| "Stack: []".to_string());
|
||||||
let hint = Line::from(vec![
|
let hint = Line::from(vec![
|
||||||
Span::styled("Esc", key),
|
Span::styled("Esc", key),
|
||||||
Span::styled(" save ", dim),
|
Span::styled(" save ", dim),
|
||||||
|
|||||||
Reference in New Issue
Block a user