11 KiB
11 KiB
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.rsandsrc/bin/desktop.rsduplicate long initialization sequences (settings, audio config, MIDI discovery, samples, sequencer startup). This creates drift and implicit coupling.
Refactor:
- Create a
bootstrapmodule (e.g.,src/bootstrap.rs) that returns a fully configuredApp,SequencerHandle, and any shared runtime state. - Both binaries call into this module with minimal differences (e.g., terminal vs desktop display setup).
Checklist:
- 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).
- In
- Define a shared config struct
- Create a
BootstrapArgsstruct with fields:samples,output,input,channels,buffer. - This mirrors CLI args from both binaries.
- Create a
- Extract app init
- Create
fn build_app(settings: &Settings, args: &BootstrapArgs) -> Appin 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)
- Create
- Extract audio/sample init
- Create
fn load_samples(paths: &[PathBuf]) -> (Vec<SampleEntry>, usize)returning the sample list and count. - Replace inline
scan_samples_dirloops in both binaries.
- Create
- Extract sequencer init
- Create
fn build_sequencer(app: &App, settings: &Settings, link: Arc<LinkState>, ...) -> SequencerHandlethat:- Builds
SequencerConfig - Calls
spawn_sequencer - Returns the handle + receivers needed for MIDI/audio.
- Builds
- Create
- Extract stream init
- Create
fn build_audio_stream(config: &AudioStreamConfig, ...) -> Result<(Stream, StreamInfo, AnalysisHandle), Error> - Use it in both binaries to set
app.audiofields.
- Create
- Replace duplicated code
- Update
src/main.rsandsrc/bin/desktop.rsto call the bootstrap functions.
- Update
- Keep env-specific parts in binary
- Terminal UI init (
crosstermsetup) remains insrc/main.rs. - Desktop font/egui setup remains in
src/bin/desktop.rs.
- Terminal UI init (
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::defaultdepends 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.rsorsrc/data/help_topics.rs. UiState::defaultshould depend only on state/data modules, not views.
Checklist:
- Identify view-level sources
src/views/help_view.rsdefines topics usingTopic(...)list.src/views/dict_view.rsdefines category list andcategory_count().
- 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.
- Add a new module (e.g.,
- Update views to depend on data module
help_view::topic_count()should returnHELP_TOPIC_COUNT.dict_view::category_count()should returnDICT_CATEGORY_COUNT.
- Update
UiState::default- Replace calls to view modules with
datamodule constants insrc/state/ui.rs.
- Replace calls to view modules with
- Confirm no other state files import
views- Use
rg "views::" src/stateto verify.
- Use
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::dispatchis 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:
- 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.
- Playback:
- Introduce handler modules
- Create
src/controllers/(orsrc/handlers/) withplayback.rs,project.rs,ui.rs,audio.rs,midi.rs.
- Create
- Move code in thin slices
- Start with the smallest domain (UI-only commands).
- Move those match arms into
UiController::handleand call fromApp::dispatch. - Repeat for each domain.
- Define minimal interfaces
- Each controller takes only the state it needs (e.g.,
&mut UiState, not&mut App).
- Each controller takes only the state it needs (e.g.,
- Keep
App::dispatchas a router- Single match that forwards to controller or returns early.
- Reduce
Appfield exposure- Make fields private where possible and provide accessors for controllers.
- 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_displayruns 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
UiStateor a new cache state. - Renderers become pure read-only views of state.
Checklist:
- Locate render-time work
src/views/render.rs→compute_stack_displaybuilds a Forth engine and evaluates script.
- Introduce cache state
- Add a
StackPreviewstruct tosrc/state/editor.rsorsrc/state/ui.rs:last_cursor_line,lines_hash,result.
- Add a
- Define update points
- Update cache when editor content changes or cursor moves.
- Likely locations:
input.rswhen editor receives input.app.rsfunctions that mutate editor state.
- Move compute logic to controller/service
- Create
src/services/stack_preview.rsthat computes preview from editor state. - Call it from update points, store in state.
- Create
- Make render pure
- Replace
compute_stack_displayusage with cached state fromUiState/EditorContext.
- Replace
- Verify consistency
- Ensure preview updates on paste and modal editor inputs (see
Event::Pastehandling insrc/main.rs).
- Ensure preview updates on paste and modal editor inputs (see
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,rngare shared between UI and sequencer threads viaArc<Mutex>andArcSwap. 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:
- Identify shared state
App::newconstructsvariables,dict,rng,script_engine.- These are passed into
spawn_sequencerinsrc/main.rs.
- Define a runtime command channel
- New enum
ScriptRuntimeCommand(e.g.,Reset,UpdateVar,ClearDict, etc.). - Add channel to sequencer config and main loop.
- New enum
- Move ownership into sequencer thread
spawn_sequencercreates its ownvariables/dict/rngandScriptEngine.- Sequencer thread consumes
ScriptRuntimeCommands and applies changes.
- Update UI interaction points
- Anywhere UI currently mutates
variablesordictshould send a runtime command instead. - Example:
App::dispatchbranch that doesself.variables.store(...)/self.dict.lock().clear().
- Anywhere UI currently mutates
- Replace direct shared references
- Remove
ArcSwapandMutexfromAppfor script runtime.
- Remove
- 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.rsis 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:
- Identify heavy imports
use crate::model::{...}insrc/app.rs,src/input.rs,src/views/*.
- Create small facade modules
- e.g.,
src/model/project_facade.rsexporting onlyProject,Pattern,Bank, etc. - e.g.,
src/model/script_facade.rsfor script-related types.
- e.g.,
- Replace
pub useinmodel/mod.rs- Narrow to explicit re-exports or remove when not needed.
- Update call sites
- Replace
crate::model::*with focused imports from facade modules.
- Replace
- 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.
UiStatecompiles without importing anything fromviews.App::dispatchreduced 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.