diff --git a/TODO.md b/TODO.md deleted file mode 100644 index f6a15d9..0000000 --- a/TODO.md +++ /dev/null @@ -1,263 +0,0 @@ -# Rustdoc & Cleanup Review - -## Workflow - -**Strictly one file at a time, in list order.** When the user says "review" (or -similar), process the next unchecked file — never skip ahead, never batch. - -1. Read the file. -2. Read any imports, callers, or sibling files needed to understand what the code - does and how it fits in the codebase. Gathering context is encouraged. -3. Apply the changes described below. -4. **`cargo build`** to confirm nothing broke. -5. Check the file off in this list. -6. Stop. Wait for the user before moving to the next file. - -## What to do - -1. **Add `//!` module doc** at the top if missing — one or two lines explaining what - the module does and its role in the crate. -2. **Add `///` on public items** (structs, enums, functions, traits, type aliases). - Keep it to one line when possible. Document struct fields only when the name alone - is not self-explanatory. -3. **Light cleanup** — remove dead code, fix misleading names, apply trivial - simplifications. No behavior changes. -4. **`cargo build`** after each file to confirm nothing broke. - -## What NOT to do - -- No comments on private internals unless truly obscure. -- No fluff ("This struct represents…"). Be direct. -- No feature changes, refactoring sprees, or reformatting of unrelated code. -- No over-commenting. If the code is clear, leave it alone. - -## Style - -Sparse, english, imperative where possible. Match the tone of the existing codebase. - ---- - -- [x] build.rs -- [x] crates/forth/src/compiler.rs -- [x] crates/forth/src/lib.rs -- [x] crates/forth/src/ops.rs -- [x] crates/forth/src/theory/chords.rs -- [x] crates/forth/src/theory/mod.rs -- [x] crates/forth/src/theory/scales.rs -- [x] crates/forth/src/types.rs -- [x] crates/forth/src/vm.rs -- [x] crates/forth/src/words/compile.rs -- [x] crates/forth/src/words/core.rs -- [x] crates/forth/src/words/effects.rs -- [x] crates/forth/src/words/midi.rs -- [x] crates/forth/src/words/mod.rs -- [x] crates/forth/src/words/music.rs -- [x] crates/forth/src/words/sequencing.rs -- [x] crates/forth/src/words/sound.rs -- [x] crates/markdown/src/highlighter.rs -- [x] crates/markdown/src/lib.rs -- [x] crates/markdown/src/parser.rs -- [x] crates/markdown/src/theme.rs -- [x] crates/project/src/file.rs -- [x] crates/project/src/lib.rs -- [x] crates/project/src/project.rs -- [x] crates/project/src/share.rs -- [x] crates/ratatui/src/category_list.rs -- [x] crates/ratatui/src/confirm.rs -- [x] crates/ratatui/src/editor.rs -- [x] crates/ratatui/src/file_browser.rs -- [x] crates/ratatui/src/hint_bar.rs -- [ ] crates/ratatui/src/lib.rs -- [ ] crates/ratatui/src/lissajous.rs -- [ ] crates/ratatui/src/list_select.rs -- [ ] crates/ratatui/src/modal.rs -- [ ] crates/ratatui/src/nav_minimap.rs -- [ ] crates/ratatui/src/props_form.rs -- [ ] crates/ratatui/src/sample_browser.rs -- [ ] crates/ratatui/src/scope.rs -- [ ] crates/ratatui/src/scroll_indicators.rs -- [ ] crates/ratatui/src/search_bar.rs -- [ ] crates/ratatui/src/section_header.rs -- [ ] crates/ratatui/src/sparkles.rs -- [ ] crates/ratatui/src/spectrum.rs -- [ ] crates/ratatui/src/text_input.rs -- [ ] crates/ratatui/src/theme/build.rs -- [ ] crates/ratatui/src/theme/catppuccin_latte.rs -- [ ] crates/ratatui/src/theme/catppuccin_mocha.rs -- [ ] crates/ratatui/src/theme/dracula.rs -- [ ] crates/ratatui/src/theme/eden.rs -- [ ] crates/ratatui/src/theme/ember.rs -- [ ] crates/ratatui/src/theme/everforest.rs -- [ ] crates/ratatui/src/theme/fairyfloss.rs -- [ ] crates/ratatui/src/theme/fauve.rs -- [ ] crates/ratatui/src/theme/georges.rs -- [ ] crates/ratatui/src/theme/gruvbox_dark.rs -- [ ] crates/ratatui/src/theme/hot_dog_stand.rs -- [ ] crates/ratatui/src/theme/iceberg.rs -- [ ] crates/ratatui/src/theme/jaipur.rs -- [ ] crates/ratatui/src/theme/kanagawa.rs -- [ ] crates/ratatui/src/theme/letz_light.rs -- [ ] crates/ratatui/src/theme/mod.rs -- [ ] crates/ratatui/src/theme/monochrome_black.rs -- [ ] crates/ratatui/src/theme/monochrome_white.rs -- [ ] crates/ratatui/src/theme/monokai.rs -- [ ] crates/ratatui/src/theme/nord.rs -- [ ] crates/ratatui/src/theme/palette.rs -- [ ] crates/ratatui/src/theme/pitch_black.rs -- [ ] crates/ratatui/src/theme/rose_pine.rs -- [ ] crates/ratatui/src/theme/tokyo_night.rs -- [ ] crates/ratatui/src/theme/transform.rs -- [ ] crates/ratatui/src/theme/tropicalia.rs -- [ ] crates/ratatui/src/vu_meter.rs -- [ ] crates/ratatui/src/waveform.rs -- [ ] plugins/baseview/src/clipboard.rs -- [ ] plugins/baseview/src/event.rs -- [ ] plugins/baseview/src/gl/macos.rs -- [ ] plugins/baseview/src/gl/mod.rs -- [ ] plugins/baseview/src/gl/win.rs -- [ ] plugins/baseview/src/gl/x11.rs -- [ ] plugins/baseview/src/gl/x11/errors.rs -- [ ] plugins/baseview/src/keyboard.rs -- [ ] plugins/baseview/src/lib.rs -- [ ] plugins/baseview/src/macos/keyboard.rs -- [ ] plugins/baseview/src/macos/mod.rs -- [ ] plugins/baseview/src/macos/view.rs -- [ ] plugins/baseview/src/macos/window.rs -- [ ] plugins/baseview/src/mouse_cursor.rs -- [ ] plugins/baseview/src/win/cursor.rs -- [ ] plugins/baseview/src/win/drop_target.rs -- [ ] plugins/baseview/src/win/hook.rs -- [ ] plugins/baseview/src/win/keyboard.rs -- [ ] plugins/baseview/src/win/mod.rs -- [ ] plugins/baseview/src/win/window.rs -- [ ] plugins/baseview/src/window_info.rs -- [ ] plugins/baseview/src/window_open_options.rs -- [ ] plugins/baseview/src/window.rs -- [ ] plugins/baseview/src/x11/cursor.rs -- [ ] plugins/baseview/src/x11/event_loop.rs -- [ ] plugins/baseview/src/x11/keyboard.rs -- [ ] plugins/baseview/src/x11/mod.rs -- [ ] plugins/baseview/src/x11/visual_info.rs -- [ ] plugins/baseview/src/x11/window.rs -- [ ] plugins/baseview/src/x11/xcb_connection.rs -- [ ] plugins/cagire-plugins/src/editor.rs -- [ ] plugins/cagire-plugins/src/lib.rs -- [ ] plugins/cagire-plugins/src/main.rs -- [ ] plugins/cagire-plugins/src/params.rs -- [ ] plugins/egui-baseview/src/lib.rs -- [ ] plugins/egui-baseview/src/renderer.rs -- [ ] plugins/egui-baseview/src/renderer/opengl.rs -- [ ] plugins/egui-baseview/src/renderer/opengl/renderer.rs -- [ ] plugins/egui-baseview/src/translate.rs -- [ ] plugins/egui-baseview/src/window.rs -- [ ] plugins/nih-plug-egui/src/editor.rs -- [ ] plugins/nih-plug-egui/src/lib.rs -- [ ] plugins/nih-plug-egui/src/resizable_window.rs -- [ ] plugins/nih-plug-egui/src/widgets.rs -- [ ] plugins/nih-plug-egui/src/widgets/generic_ui.rs -- [ ] plugins/nih-plug-egui/src/widgets/param_slider.rs -- [ ] plugins/nih-plug-egui/src/widgets/util.rs -- [ ] src/app/clipboard.rs -- [ ] src/app/dispatch.rs -- [ ] src/app/editing.rs -- [ ] src/app/mod.rs -- [ ] src/app/navigation.rs -- [ ] src/app/persistence.rs -- [ ] src/app/scripting.rs -- [ ] src/app/sequencer.rs -- [ ] src/app/staging.rs -- [ ] src/app/undo.rs -- [ ] src/bin/desktop/main.rs -- [ ] src/block_renderer.rs -- [ ] src/commands.rs -- [ ] src/engine/audio.rs -- [ ] src/engine/dispatcher.rs -- [ ] src/engine/link.rs -- [ ] src/engine/mod.rs -- [ ] src/engine/realtime.rs -- [ ] src/engine/sequencer.rs -- [ ] src/engine/timing.rs -- [ ] src/init.rs -- [ ] src/input_egui.rs -- [ ] src/input/engine_page.rs -- [ ] src/input/help_page.rs -- [ ] src/input/main_page.rs -- [ ] src/input/mod.rs -- [ ] src/input/modal.rs -- [ ] src/input/mouse.rs -- [ ] src/input/options_page.rs -- [ ] src/input/panel.rs -- [ ] src/input/patterns_page.rs -- [ ] src/lib.rs -- [ ] src/main.rs -- [ ] src/midi.rs -- [ ] src/model/categories.rs -- [ ] src/model/demos.rs -- [ ] src/model/docs.rs -- [ ] src/model/mod.rs -- [ ] src/model/onboarding.rs -- [ ] src/model/script.rs -- [ ] src/page.rs -- [ ] src/services/clipboard.rs -- [ ] src/services/dict_nav.rs -- [ ] src/services/euclidean.rs -- [ ] src/services/help_nav.rs -- [ ] src/services/mod.rs -- [ ] src/services/pattern_editor.rs -- [ ] src/services/stack_preview.rs -- [ ] src/settings.rs -- [ ] src/state/audio.rs -- [ ] src/state/color_scheme.rs -- [ ] src/state/editor.rs -- [ ] src/state/effects.rs -- [ ] src/state/file_browser.rs -- [ ] src/state/live_keys.rs -- [ ] src/state/mod.rs -- [ ] src/state/modal.rs -- [ ] src/state/mute.rs -- [ ] src/state/options.rs -- [ ] src/state/panel.rs -- [ ] src/state/patterns_nav.rs -- [ ] src/state/playback.rs -- [ ] src/state/project.rs -- [ ] src/state/sample_browser.rs -- [ ] src/state/ui.rs -- [ ] src/state/undo.rs -- [ ] src/theme.rs -- [ ] src/views/dict_view.rs -- [ ] src/views/engine_view.rs -- [ ] src/views/help_view.rs -- [ ] src/views/highlight.rs -- [ ] src/views/keybindings.rs -- [ ] src/views/main_view.rs -- [ ] src/views/mod.rs -- [ ] src/views/options_view.rs -- [ ] src/views/patterns_view.rs -- [ ] src/views/render.rs -- [ ] src/views/title_view.rs -- [ ] src/widgets/mod.rs -- [ ] tests/forth.rs -- [ ] tests/forth/arithmetic.rs -- [ ] tests/forth/case_statement.rs -- [ ] tests/forth/chords.rs -- [ ] tests/forth/comparison.rs -- [ ] tests/forth/context.rs -- [ ] tests/forth/control_flow.rs -- [ ] tests/forth/definitions.rs -- [ ] tests/forth/errors.rs -- [ ] tests/forth/euclidean.rs -- [ ] tests/forth/generator.rs -- [ ] tests/forth/harmony.rs -- [ ] tests/forth/harness.rs -- [ ] tests/forth/intervals.rs -- [ ] tests/forth/list_words.rs -- [ ] tests/forth/midi.rs -- [ ] tests/forth/notes.rs -- [ ] tests/forth/quotations.rs -- [ ] tests/forth/ramps.rs -- [ ] tests/forth/randomness.rs -- [ ] tests/forth/sound.rs -- [ ] tests/forth/stack.rs -- [ ] tests/forth/temporal.rs -- [ ] tests/forth/variables.rs -- [ ] xtask/src/main.rs diff --git a/crates/forth/src/vm.rs b/crates/forth/src/vm.rs index f0911a9..51087be 100644 --- a/crates/forth/src/vm.rs +++ b/crates/forth/src/vm.rs @@ -1883,23 +1883,67 @@ fn float_to_value(result: f64) -> Value { fn lift_unary(val: Value, f: F) -> Result where - F: Fn(f64) -> f64, + F: Fn(f64) -> f64 + Copy, { - Ok(float_to_value(f(val.as_float()?))) + match val { + Value::ArpList(items) => { + let mapped: Result, _> = items.iter().map(|x| lift_unary(x.clone(), f)).collect(); + Ok(Value::ArpList(Arc::from(mapped?))) + } + Value::CycleList(items) => { + let mapped: Result, _> = items.iter().map(|x| lift_unary(x.clone(), f)).collect(); + Ok(Value::CycleList(Arc::from(mapped?))) + } + v => Ok(float_to_value(f(v.as_float()?))), + } } fn lift_unary_int(val: Value, f: F) -> Result where - F: Fn(i64) -> i64, + F: Fn(i64) -> i64 + Copy, { - Ok(Value::Int(f(val.as_int()?), None)) + match val { + Value::ArpList(items) => { + let mapped: Result, _> = + items.iter().map(|x| lift_unary_int(x.clone(), f)).collect(); + Ok(Value::ArpList(Arc::from(mapped?))) + } + Value::CycleList(items) => { + let mapped: Result, _> = + items.iter().map(|x| lift_unary_int(x.clone(), f)).collect(); + Ok(Value::CycleList(Arc::from(mapped?))) + } + v => Ok(Value::Int(f(v.as_int()?), None)), + } } fn lift_binary(a: Value, b: Value, f: F) -> Result where - F: Fn(f64, f64) -> f64, + F: Fn(f64, f64) -> f64 + Copy, { - Ok(float_to_value(f(a.as_float()?, b.as_float()?))) + match (a, b) { + (Value::ArpList(items), b) => { + let mapped: Result, _> = + items.iter().map(|x| lift_binary(x.clone(), b.clone(), f)).collect(); + Ok(Value::ArpList(Arc::from(mapped?))) + } + (a, Value::ArpList(items)) => { + let mapped: Result, _> = + items.iter().map(|x| lift_binary(a.clone(), x.clone(), f)).collect(); + Ok(Value::ArpList(Arc::from(mapped?))) + } + (Value::CycleList(items), b) => { + let mapped: Result, _> = + items.iter().map(|x| lift_binary(x.clone(), b.clone(), f)).collect(); + Ok(Value::CycleList(Arc::from(mapped?))) + } + (a, Value::CycleList(items)) => { + let mapped: Result, _> = + items.iter().map(|x| lift_binary(a.clone(), x.clone(), f)).collect(); + Ok(Value::CycleList(Arc::from(mapped?))) + } + (a, b) => Ok(float_to_value(f(a.as_float()?, b.as_float()?))), + } } fn binary_op(stack: &mut Vec, f: F) -> Result<(), String> diff --git a/crates/project/src/file.rs b/crates/project/src/file.rs index 8069d0d..2d8fb92 100644 --- a/crates/project/src/file.rs +++ b/crates/project/src/file.rs @@ -6,7 +6,7 @@ use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; -use crate::project::{Bank, Project}; +use crate::project::{Bank, PatternSpeed, Project}; const VERSION: u8 = 1; const EXTENSION: &str = "cagire"; @@ -31,6 +31,24 @@ struct ProjectFile { playing_patterns: Vec<(usize, usize)>, #[serde(default, skip_serializing_if = "String::is_empty")] prelude: String, + #[serde(default, skip_serializing_if = "String::is_empty")] + script: String, + #[serde(default, skip_serializing_if = "is_default_speed")] + script_speed: PatternSpeed, + #[serde(default = "default_script_length", skip_serializing_if = "is_default_script_length")] + script_length: usize, +} + +fn is_default_speed(s: &PatternSpeed) -> bool { + *s == PatternSpeed::default() +} + +fn default_script_length() -> usize { + 16 +} + +fn is_default_script_length(n: &usize) -> bool { + *n == default_script_length() } fn default_tempo() -> f64 { @@ -46,6 +64,9 @@ impl From<&Project> for ProjectFile { tempo: project.tempo, playing_patterns: project.playing_patterns.clone(), prelude: project.prelude.clone(), + script: project.script.clone(), + script_speed: project.script_speed, + script_length: project.script_length, } } } @@ -58,6 +79,9 @@ impl From for Project { tempo: file.tempo, playing_patterns: file.playing_patterns, prelude: file.prelude, + script: file.script, + script_speed: file.script_speed, + script_length: file.script_length, }; project.normalize(); project diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index da9c2b2..06a84c3 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -558,12 +558,22 @@ pub struct Project { pub playing_patterns: Vec<(usize, usize)>, #[serde(default)] pub prelude: String, + #[serde(default)] + pub script: String, + #[serde(default)] + pub script_speed: PatternSpeed, + #[serde(default = "default_script_length")] + pub script_length: usize, } fn default_tempo() -> f64 { 120.0 } +fn default_script_length() -> usize { + 16 +} + impl Default for Project { fn default() -> Self { Self { @@ -572,6 +582,9 @@ impl Default for Project { tempo: default_tempo(), playing_patterns: Vec::new(), prelude: String::new(), + script: String::new(), + script_speed: PatternSpeed::default(), + script_length: default_script_length(), } } } diff --git a/crates/ratatui/src/theme/build.rs b/crates/ratatui/src/theme/build.rs index db9adea..47c51f4 100644 --- a/crates/ratatui/src/theme/build.rs +++ b/crates/ratatui/src/theme/build.rs @@ -1,6 +1,9 @@ +//! Derive [`ThemeColors`] from a [`Palette`]. + use super::*; use super::palette::{Palette, Rgb, darken, mid, rgb, tint}; +/// Build a complete [`ThemeColors`] from a [`Palette`]. pub fn build(p: &Palette) -> ThemeColors { let darker_bg = darken(p.bg, 0.15); diff --git a/crates/ratatui/src/theme/catppuccin_latte.rs b/crates/ratatui/src/theme/catppuccin_latte.rs index fa0b243..0d469c1 100644 --- a/crates/ratatui/src/theme/catppuccin_latte.rs +++ b/crates/ratatui/src/theme/catppuccin_latte.rs @@ -1,3 +1,5 @@ +//! Catppuccin Latte palette. + use super::palette::Palette; pub fn palette() -> Palette { diff --git a/crates/ratatui/src/theme/catppuccin_mocha.rs b/crates/ratatui/src/theme/catppuccin_mocha.rs index 5c9d5f0..1165163 100644 --- a/crates/ratatui/src/theme/catppuccin_mocha.rs +++ b/crates/ratatui/src/theme/catppuccin_mocha.rs @@ -1,3 +1,5 @@ +//! Catppuccin Mocha palette. + use super::palette::Palette; pub fn palette() -> Palette { diff --git a/crates/ratatui/src/theme/dracula.rs b/crates/ratatui/src/theme/dracula.rs index 8e7e4f8..3b73705 100644 --- a/crates/ratatui/src/theme/dracula.rs +++ b/crates/ratatui/src/theme/dracula.rs @@ -1,3 +1,5 @@ +//! Dracula palette. + use super::palette::Palette; pub fn palette() -> Palette { diff --git a/crates/ratatui/src/theme/eden.rs b/crates/ratatui/src/theme/eden.rs index fca5bde..ad63941 100644 --- a/crates/ratatui/src/theme/eden.rs +++ b/crates/ratatui/src/theme/eden.rs @@ -1,3 +1,5 @@ +//! Eden palette. + use super::palette::Palette; pub fn palette() -> Palette { diff --git a/crates/ratatui/src/theme/ember.rs b/crates/ratatui/src/theme/ember.rs index 48939ba..3ff26d9 100644 --- a/crates/ratatui/src/theme/ember.rs +++ b/crates/ratatui/src/theme/ember.rs @@ -1,3 +1,5 @@ +//! Ember palette. + use super::palette::Palette; pub fn palette() -> Palette { diff --git a/crates/ratatui/src/theme/everforest.rs b/crates/ratatui/src/theme/everforest.rs index fc4f5e3..3d62b64 100644 --- a/crates/ratatui/src/theme/everforest.rs +++ b/crates/ratatui/src/theme/everforest.rs @@ -1,3 +1,5 @@ +//! Everforest palette. + use super::palette::Palette; pub fn palette() -> Palette { diff --git a/crates/ratatui/src/theme/fairyfloss.rs b/crates/ratatui/src/theme/fairyfloss.rs index df359d1..8f8ee1d 100644 --- a/crates/ratatui/src/theme/fairyfloss.rs +++ b/crates/ratatui/src/theme/fairyfloss.rs @@ -1,3 +1,5 @@ +//! Fairyfloss palette. + use super::palette::Palette; pub fn palette() -> Palette { diff --git a/crates/ratatui/src/theme/fauve.rs b/crates/ratatui/src/theme/fauve.rs index d9d0b5b..0fd98ea 100644 --- a/crates/ratatui/src/theme/fauve.rs +++ b/crates/ratatui/src/theme/fauve.rs @@ -1,3 +1,5 @@ +//! Fauve palette. + use super::palette::Palette; pub fn palette() -> Palette { diff --git a/crates/ratatui/src/theme/georges.rs b/crates/ratatui/src/theme/georges.rs index f97ea9e..41aaa61 100644 --- a/crates/ratatui/src/theme/georges.rs +++ b/crates/ratatui/src/theme/georges.rs @@ -1,6 +1,7 @@ +//! Georges palette (C64 colors on black). + use super::palette::Palette; -// C64 palette on pure black pub fn palette() -> Palette { Palette { bg: (0, 0, 0), diff --git a/crates/ratatui/src/theme/gruvbox_dark.rs b/crates/ratatui/src/theme/gruvbox_dark.rs index aa44baa..15463d0 100644 --- a/crates/ratatui/src/theme/gruvbox_dark.rs +++ b/crates/ratatui/src/theme/gruvbox_dark.rs @@ -1,3 +1,5 @@ +//! Gruvbox Dark palette. + use super::palette::Palette; pub fn palette() -> Palette { diff --git a/crates/ratatui/src/theme/hot_dog_stand.rs b/crates/ratatui/src/theme/hot_dog_stand.rs index f627a54..c1193c0 100644 --- a/crates/ratatui/src/theme/hot_dog_stand.rs +++ b/crates/ratatui/src/theme/hot_dog_stand.rs @@ -1,3 +1,5 @@ +//! Hot Dog Stand palette. + use super::palette::Palette; pub fn palette() -> Palette { diff --git a/crates/ratatui/src/theme/iceberg.rs b/crates/ratatui/src/theme/iceberg.rs index 39122f4..ace1ccc 100644 --- a/crates/ratatui/src/theme/iceberg.rs +++ b/crates/ratatui/src/theme/iceberg.rs @@ -1,3 +1,5 @@ +//! Iceberg palette. + use super::palette::Palette; pub fn palette() -> Palette { diff --git a/crates/ratatui/src/theme/jaipur.rs b/crates/ratatui/src/theme/jaipur.rs index 6b348e0..25d1dae 100644 --- a/crates/ratatui/src/theme/jaipur.rs +++ b/crates/ratatui/src/theme/jaipur.rs @@ -1,3 +1,5 @@ +//! Jaipur palette. + use super::palette::Palette; pub fn palette() -> Palette { diff --git a/crates/ratatui/src/theme/kanagawa.rs b/crates/ratatui/src/theme/kanagawa.rs index f4792c3..22cd74c 100644 --- a/crates/ratatui/src/theme/kanagawa.rs +++ b/crates/ratatui/src/theme/kanagawa.rs @@ -1,3 +1,5 @@ +//! Kanagawa palette. + use super::palette::Palette; pub fn palette() -> Palette { diff --git a/crates/ratatui/src/theme/letz_light.rs b/crates/ratatui/src/theme/letz_light.rs index 0f07da5..77b52c3 100644 --- a/crates/ratatui/src/theme/letz_light.rs +++ b/crates/ratatui/src/theme/letz_light.rs @@ -1,3 +1,5 @@ +//! Letz Light palette. + use super::palette::Palette; pub fn palette() -> Palette { diff --git a/crates/ratatui/src/theme/mod.rs b/crates/ratatui/src/theme/mod.rs index 5241a04..75fa679 100644 --- a/crates/ratatui/src/theme/mod.rs +++ b/crates/ratatui/src/theme/mod.rs @@ -31,12 +31,14 @@ pub mod transform; use ratatui::style::Color; use std::cell::RefCell; +/// Entry in the theme registry: id, display label, and palette constructor. pub struct ThemeEntry { pub id: &'static str, pub label: &'static str, pub palette: fn() -> palette::Palette, } +/// All available themes. pub const THEMES: &[ThemeEntry] = &[ ThemeEntry { id: "CatppuccinMocha", label: "Catppuccin Mocha", palette: catppuccin_mocha::palette }, ThemeEntry { id: "CatppuccinLatte", label: "Catppuccin Latte", palette: catppuccin_latte::palette }, @@ -67,14 +69,17 @@ thread_local! { static CURRENT_THEME: RefCell = RefCell::new(build::build(&(THEMES[0].palette)())); } +/// Return the current thread-local theme. pub fn get() -> ThemeColors { CURRENT_THEME.with(|t| t.borrow().clone()) } +/// Set the current thread-local theme. pub fn set(theme: ThemeColors) { CURRENT_THEME.with(|t| *t.borrow_mut() = theme); } +/// Complete set of resolved colors for all UI components. #[derive(Clone)] pub struct ThemeColors { pub ui: UiColors, @@ -105,6 +110,7 @@ pub struct ThemeColors { pub confirm: ConfirmColors, } +/// Core UI colors: background, text, borders. #[derive(Clone)] pub struct UiColors { pub bg: Color, @@ -119,6 +125,7 @@ pub struct UiColors { pub surface: Color, } +/// Playback status bar colors. #[derive(Clone)] pub struct StatusColors { pub playing_bg: Color, @@ -130,6 +137,7 @@ pub struct StatusColors { pub fill_bg: Color, } +/// Step grid selection and cursor colors. #[derive(Clone)] pub struct SelectionColors { pub cursor_bg: Color, @@ -143,6 +151,7 @@ pub struct SelectionColors { pub in_range: Color, } +/// Step tile colors for various states. #[derive(Clone)] pub struct TileColors { pub playing_active_bg: Color, @@ -160,6 +169,7 @@ pub struct TileColors { pub link_dim: [(u8, u8, u8); 5], } +/// Top header bar segment colors. #[derive(Clone)] pub struct HeaderColors { pub tempo_bg: Color, @@ -172,6 +182,7 @@ pub struct HeaderColors { pub stats_fg: Color, } +/// Modal dialog border colors. #[derive(Clone)] pub struct ModalColors { pub border: Color, @@ -185,6 +196,7 @@ pub struct ModalColors { pub preview: Color, } +/// Flash notification colors. #[derive(Clone)] pub struct FlashColors { pub error_bg: Color, @@ -195,6 +207,7 @@ pub struct FlashColors { pub info_fg: Color, } +/// Pattern list row state colors. #[derive(Clone)] pub struct ListColors { pub playing_bg: Color, @@ -213,6 +226,7 @@ pub struct ListColors { pub soloed_fg: Color, } +/// Ableton Link status indicator colors. #[derive(Clone)] pub struct LinkStatusColors { pub disabled: Color, @@ -220,6 +234,7 @@ pub struct LinkStatusColors { pub listening: Color, } +/// Syntax highlighting (fg, bg) pairs per token category. #[derive(Clone)] pub struct SyntaxColors { pub gap_bg: Color, @@ -244,30 +259,35 @@ pub struct SyntaxColors { pub default: (Color, Color), } +/// Alternating table row colors. #[derive(Clone)] pub struct TableColors { pub row_even: Color, pub row_odd: Color, } +/// Value display colors. #[derive(Clone)] pub struct ValuesColors { pub tempo: Color, pub value: Color, } +/// Keyboard hint key/text colors. #[derive(Clone)] pub struct HintColors { pub key: Color, pub text: Color, } +/// View badge pill colors. #[derive(Clone)] pub struct ViewBadgeColors { pub bg: Color, pub fg: Color, } +/// Navigation minimap tile colors. #[derive(Clone)] pub struct NavColors { pub selected_bg: Color, @@ -276,6 +296,7 @@ pub struct NavColors { pub unselected_fg: Color, } +/// Script editor colors. #[derive(Clone)] pub struct EditorWidgetColors { pub cursor_bg: Color, @@ -287,6 +308,7 @@ pub struct EditorWidgetColors { pub completion_example: Color, } +/// File and sample browser colors. #[derive(Clone)] pub struct BrowserColors { pub directory: Color, @@ -301,6 +323,7 @@ pub struct BrowserColors { pub empty_text: Color, } +/// Text input field colors. #[derive(Clone)] pub struct InputColors { pub text: Color, @@ -308,6 +331,7 @@ pub struct InputColors { pub hint: Color, } +/// Search bar and match highlight colors. #[derive(Clone)] pub struct SearchColors { pub active: Color, @@ -316,6 +340,7 @@ pub struct SearchColors { pub match_fg: Color, } +/// Markdown renderer colors. #[derive(Clone)] pub struct MarkdownColors { pub h1: Color, @@ -330,6 +355,7 @@ pub struct MarkdownColors { pub list: Color, } +/// Engine view panel colors. #[derive(Clone)] pub struct EngineColors { pub header: Color, @@ -352,6 +378,7 @@ pub struct EngineColors { pub hint_inactive: Color, } +/// Dictionary view colors. #[derive(Clone)] pub struct DictColors { pub word_name: Color, @@ -369,6 +396,7 @@ pub struct DictColors { pub header_desc: Color, } +/// Title screen colors. #[derive(Clone)] pub struct TitleColors { pub big_title: Color, @@ -379,6 +407,7 @@ pub struct TitleColors { pub subtitle: Color, } +/// VU meter and spectrum level colors. #[derive(Clone)] pub struct MeterColors { pub low: Color, @@ -389,11 +418,13 @@ pub struct MeterColors { pub high_rgb: (u8, u8, u8), } +/// Sparkle particle colors. #[derive(Clone)] pub struct SparkleColors { pub colors: [(u8, u8, u8); 5], } +/// Confirm dialog colors. #[derive(Clone)] pub struct ConfirmColors { pub border: Color, diff --git a/crates/ratatui/src/theme/monochrome_black.rs b/crates/ratatui/src/theme/monochrome_black.rs index 4d723e5..272a890 100644 --- a/crates/ratatui/src/theme/monochrome_black.rs +++ b/crates/ratatui/src/theme/monochrome_black.rs @@ -1,3 +1,5 @@ +//! Monochrome (black background) palette. + use super::palette::Palette; pub fn palette() -> Palette { diff --git a/crates/ratatui/src/theme/monochrome_white.rs b/crates/ratatui/src/theme/monochrome_white.rs index 99a0ea4..2492022 100644 --- a/crates/ratatui/src/theme/monochrome_white.rs +++ b/crates/ratatui/src/theme/monochrome_white.rs @@ -1,3 +1,5 @@ +//! Monochrome (white background) palette. + use super::palette::Palette; pub fn palette() -> Palette { diff --git a/crates/ratatui/src/theme/monokai.rs b/crates/ratatui/src/theme/monokai.rs index cb444ca..9590947 100644 --- a/crates/ratatui/src/theme/monokai.rs +++ b/crates/ratatui/src/theme/monokai.rs @@ -1,3 +1,5 @@ +//! Monokai palette. + use super::palette::Palette; pub fn palette() -> Palette { diff --git a/crates/ratatui/src/theme/nord.rs b/crates/ratatui/src/theme/nord.rs index 10ae33b..3de228d 100644 --- a/crates/ratatui/src/theme/nord.rs +++ b/crates/ratatui/src/theme/nord.rs @@ -1,3 +1,5 @@ +//! Nord palette. + use super::palette::Palette; pub fn palette() -> Palette { diff --git a/crates/ratatui/src/theme/palette.rs b/crates/ratatui/src/theme/palette.rs index 746d575..f897198 100644 --- a/crates/ratatui/src/theme/palette.rs +++ b/crates/ratatui/src/theme/palette.rs @@ -1,7 +1,11 @@ +//! Palette definition and color mixing utilities. + use ratatui::style::Color; +/// RGB color triple. pub type Rgb = (u8, u8, u8); +/// Base color palette that themes are derived from. pub struct Palette { // Core pub bg: Rgb, @@ -33,10 +37,12 @@ pub struct Palette { pub meter: [Rgb; 3], } +/// Convert an RGB triple to a ratatui [`Color`]. pub fn rgb(c: Rgb) -> Color { Color::Rgb(c.0, c.1, c.2) } +/// Blend `bg` toward `accent` by `amount` (0.0–1.0). pub fn tint(bg: Rgb, accent: Rgb, amount: f32) -> Rgb { let mix = |b: u8, a: u8| -> u8 { let v = b as f32 + (a as f32 - b as f32) * amount; @@ -45,10 +51,12 @@ pub fn tint(bg: Rgb, accent: Rgb, amount: f32) -> Rgb { (mix(bg.0, accent.0), mix(bg.1, accent.1), mix(bg.2, accent.2)) } +/// Linearly interpolate between two colors. pub fn mid(a: Rgb, b: Rgb, t: f32) -> Rgb { tint(a, b, t) } +/// Darken a color by reducing brightness. pub fn darken(c: Rgb, amount: f32) -> Rgb { let d = |v: u8| -> u8 { (v as f32 * (1.0 - amount)).clamp(0.0, 255.0) as u8 }; (d(c.0), d(c.1), d(c.2)) diff --git a/crates/ratatui/src/theme/pitch_black.rs b/crates/ratatui/src/theme/pitch_black.rs index e33d226..5705357 100644 --- a/crates/ratatui/src/theme/pitch_black.rs +++ b/crates/ratatui/src/theme/pitch_black.rs @@ -1,3 +1,5 @@ +//! Pitch Black palette. + use super::palette::Palette; pub fn palette() -> Palette { diff --git a/crates/ratatui/src/theme/rose_pine.rs b/crates/ratatui/src/theme/rose_pine.rs index f5287cf..cdc6329 100644 --- a/crates/ratatui/src/theme/rose_pine.rs +++ b/crates/ratatui/src/theme/rose_pine.rs @@ -1,3 +1,5 @@ +//! Rose Pine palette. + use super::palette::Palette; pub fn palette() -> Palette { diff --git a/crates/ratatui/src/theme/tokyo_night.rs b/crates/ratatui/src/theme/tokyo_night.rs index d922864..834673b 100644 --- a/crates/ratatui/src/theme/tokyo_night.rs +++ b/crates/ratatui/src/theme/tokyo_night.rs @@ -1,3 +1,5 @@ +//! Tokyo Night palette. + use super::palette::Palette; pub fn palette() -> Palette { diff --git a/crates/ratatui/src/theme/transform.rs b/crates/ratatui/src/theme/transform.rs index 2a84466..ba52695 100644 --- a/crates/ratatui/src/theme/transform.rs +++ b/crates/ratatui/src/theme/transform.rs @@ -1,3 +1,5 @@ +//! Hue rotation for palette-wide color transforms. + use super::palette::{Palette, Rgb}; use super::build::build; use super::ThemeColors; @@ -62,6 +64,7 @@ fn rotate3(arr: [Rgb; 3], d: f32) -> [Rgb; 3] { [rotate(arr[0], d), rotate(arr[1], d), rotate(arr[2], d)] } +/// Build a [`ThemeColors`] with all palette hues rotated by `degrees`. pub fn rotate_palette(palette: &Palette, degrees: f32) -> ThemeColors { if degrees == 0.0 { return build(palette); diff --git a/crates/ratatui/src/theme/tropicalia.rs b/crates/ratatui/src/theme/tropicalia.rs index faf6a65..6071f40 100644 --- a/crates/ratatui/src/theme/tropicalia.rs +++ b/crates/ratatui/src/theme/tropicalia.rs @@ -1,3 +1,5 @@ +//! Tropicalia palette. + use super::palette::Palette; pub fn palette() -> Palette { diff --git a/plugins/baseview/src/clipboard.rs b/plugins/baseview/src/clipboard.rs index c4f7bc4..3e2d749 100644 --- a/plugins/baseview/src/clipboard.rs +++ b/plugins/baseview/src/clipboard.rs @@ -1,3 +1,5 @@ +//! Cross-platform clipboard abstraction. + #[cfg(target_os = "macos")] use crate::macos as platform; #[cfg(target_os = "windows")] @@ -5,6 +7,7 @@ use crate::win as platform; #[cfg(target_os = "linux")] use crate::x11 as platform; +/// Copy the given string to the system clipboard. pub fn copy_to_clipboard(data: &str) { platform::copy_to_clipboard(data) } diff --git a/plugins/baseview/src/event.rs b/plugins/baseview/src/event.rs index cfb48ee..e8e0b48 100644 --- a/plugins/baseview/src/event.rs +++ b/plugins/baseview/src/event.rs @@ -1,9 +1,12 @@ +//! Window event types for mouse, keyboard, and window lifecycle. + use std::path::PathBuf; use keyboard_types::{KeyboardEvent, Modifiers}; use crate::{Point, WindowInfo}; +/// A mouse button identifier. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum MouseButton { Left, @@ -34,6 +37,7 @@ pub enum ScrollDelta { }, } +/// A mouse input event. #[derive(Debug, Clone, PartialEq)] pub enum MouseEvent { /// The mouse cursor was moved @@ -116,6 +120,7 @@ pub enum MouseEvent { }, } +/// A window lifecycle event. #[derive(Debug, Clone)] pub enum WindowEvent { Resized(WindowInfo), @@ -124,6 +129,7 @@ pub enum WindowEvent { WillClose, } +/// Top-level input event dispatched to a [`WindowHandler`](crate::WindowHandler). #[derive(Debug, Clone)] pub enum Event { Mouse(MouseEvent), @@ -131,6 +137,7 @@ pub enum Event { Window(WindowEvent), } +/// The effect to apply when a drag-and-drop operation completes. #[derive(Debug, Clone, Copy, PartialEq)] pub enum DropEffect { Copy, @@ -139,6 +146,7 @@ pub enum DropEffect { Scroll, } +/// Data payload carried by a drag-and-drop operation. #[derive(Debug, Clone, PartialEq)] pub enum DropData { None, diff --git a/plugins/baseview/src/gl/macos.rs b/plugins/baseview/src/gl/macos.rs index f9954a4..26e2675 100644 --- a/plugins/baseview/src/gl/macos.rs +++ b/plugins/baseview/src/gl/macos.rs @@ -1,3 +1,5 @@ +//! macOS OpenGL context implementation via NSOpenGLView. + // This is required because the objc crate is causing a lot of warnings: https://github.com/SSheldon/rust-objc/issues/125 // Eventually we should migrate to the objc2 crate and remove this. #![allow(unexpected_cfgs)] diff --git a/plugins/baseview/src/gl/mod.rs b/plugins/baseview/src/gl/mod.rs index 488cfd7..e4c1c35 100644 --- a/plugins/baseview/src/gl/mod.rs +++ b/plugins/baseview/src/gl/mod.rs @@ -1,3 +1,5 @@ +//! OpenGL context creation and management, with platform-specific backends. + use std::ffi::c_void; use std::marker::PhantomData; @@ -21,6 +23,7 @@ mod macos; #[cfg(target_os = "macos")] use macos as platform; +/// OpenGL framebuffer and context configuration. #[derive(Clone, Debug)] pub struct GlConfig { pub version: (u8, u8), @@ -56,12 +59,14 @@ impl Default for GlConfig { } } +/// OpenGL profile to request. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Profile { Compatibility, Core, } +/// Error returned when creating an OpenGL context fails. #[derive(Debug)] pub enum GlError { InvalidWindowHandle, @@ -69,6 +74,7 @@ pub enum GlError { CreationFailed(platform::CreationFailedError), } +/// Platform-independent OpenGL context handle. pub struct GlContext { context: platform::GlContext, phantom: PhantomData<*mut ()>, @@ -91,18 +97,22 @@ impl GlContext { GlContext { context, phantom: PhantomData } } + /// Bind this context to the current thread. pub unsafe fn make_current(&self) { self.context.make_current(); } + /// Unbind this context from the current thread. pub unsafe fn make_not_current(&self) { self.context.make_not_current(); } + /// Look up an OpenGL function pointer by name. pub fn get_proc_address(&self, symbol: &str) -> *const c_void { self.context.get_proc_address(symbol) } + /// Swap the front and back framebuffers. pub fn swap_buffers(&self) { self.context.swap_buffers(); } diff --git a/plugins/baseview/src/gl/win.rs b/plugins/baseview/src/gl/win.rs index 097eb09..500b8db 100644 --- a/plugins/baseview/src/gl/win.rs +++ b/plugins/baseview/src/gl/win.rs @@ -1,3 +1,5 @@ +//! Windows OpenGL context implementation via WGL. + use std::ffi::{c_void, CString, OsStr}; use std::os::windows::ffi::OsStrExt; diff --git a/plugins/baseview/src/gl/x11.rs b/plugins/baseview/src/gl/x11.rs index 4a4de73..7ea717c 100644 --- a/plugins/baseview/src/gl/x11.rs +++ b/plugins/baseview/src/gl/x11.rs @@ -1,3 +1,5 @@ +//! X11 OpenGL context implementation via GLX. + use std::ffi::{c_void, CString}; use std::os::raw::{c_int, c_ulong}; diff --git a/plugins/baseview/src/gl/x11/errors.rs b/plugins/baseview/src/gl/x11/errors.rs index 184a625..d76e29f 100644 --- a/plugins/baseview/src/gl/x11/errors.rs +++ b/plugins/baseview/src/gl/x11/errors.rs @@ -1,3 +1,5 @@ +//! Safe X11 error handling for GLX context creation. + use std::ffi::CStr; use std::fmt::{Debug, Display, Formatter}; use x11::xlib; diff --git a/plugins/baseview/src/lib.rs b/plugins/baseview/src/lib.rs index 54d57dd..e3517f4 100644 --- a/plugins/baseview/src/lib.rs +++ b/plugins/baseview/src/lib.rs @@ -1,3 +1,5 @@ +//! Cross-platform windowing library for plugin UIs, with OpenGL support. + #[cfg(target_os = "macos")] mod macos; #[cfg(target_os = "windows")] diff --git a/plugins/baseview/src/macos/mod.rs b/plugins/baseview/src/macos/mod.rs index d5e7f59..2159800 100644 --- a/plugins/baseview/src/macos/mod.rs +++ b/plugins/baseview/src/macos/mod.rs @@ -1,3 +1,5 @@ +//! macOS platform backend (Cocoa / AppKit). + // This is required because the objc crate is causing a lot of warnings: https://github.com/SSheldon/rust-objc/issues/125 // Eventually we should migrate to the objc2 crate and remove this. #![allow(unexpected_cfgs)] diff --git a/plugins/baseview/src/macos/view.rs b/plugins/baseview/src/macos/view.rs index 393b00c..729ef73 100644 --- a/plugins/baseview/src/macos/view.rs +++ b/plugins/baseview/src/macos/view.rs @@ -1,3 +1,5 @@ +//! NSView subclass that handles input events and drag-and-drop. + use std::ffi::c_void; use cocoa::appkit::{NSEvent, NSFilenamesPboardType, NSView, NSWindow}; diff --git a/plugins/baseview/src/macos/window.rs b/plugins/baseview/src/macos/window.rs index 6a2a35b..2bfb1a8 100644 --- a/plugins/baseview/src/macos/window.rs +++ b/plugins/baseview/src/macos/window.rs @@ -1,3 +1,5 @@ +//! macOS window creation, lifecycle, and event dispatch. + use std::cell::{Cell, RefCell}; use std::collections::VecDeque; use std::ffi::c_void; diff --git a/plugins/baseview/src/mouse_cursor.rs b/plugins/baseview/src/mouse_cursor.rs index bf4cde0..3c5699d 100644 --- a/plugins/baseview/src/mouse_cursor.rs +++ b/plugins/baseview/src/mouse_cursor.rs @@ -1,3 +1,6 @@ +//! Mouse cursor icon definitions. + +/// System mouse cursor style. #[derive(Debug, Default, Eq, PartialEq, Clone, Copy, PartialOrd, Ord, Hash)] pub enum MouseCursor { #[default] diff --git a/plugins/baseview/src/win/cursor.rs b/plugins/baseview/src/win/cursor.rs index f9a04d3..8367e88 100644 --- a/plugins/baseview/src/win/cursor.rs +++ b/plugins/baseview/src/win/cursor.rs @@ -1,3 +1,5 @@ +//! MouseCursor to Win32 LPCWSTR cursor mapping. + use crate::MouseCursor; use winapi::{ shared::ntdef::LPCWSTR, diff --git a/plugins/baseview/src/win/drop_target.rs b/plugins/baseview/src/win/drop_target.rs index e530a43..83b1e35 100644 --- a/plugins/baseview/src/win/drop_target.rs +++ b/plugins/baseview/src/win/drop_target.rs @@ -1,3 +1,5 @@ +//! COM IDropTarget implementation for drag-and-drop support. + use std::ffi::OsString; use std::mem::transmute; use std::os::windows::prelude::OsStringExt; diff --git a/plugins/baseview/src/win/hook.rs b/plugins/baseview/src/win/hook.rs index 0f6a3e8..3e8bcb4 100644 --- a/plugins/baseview/src/win/hook.rs +++ b/plugins/baseview/src/win/hook.rs @@ -1,3 +1,5 @@ +//! Win32 keyboard hook to intercept key events in DAW-hosted plugin windows. + use std::{ collections::HashSet, ffi::c_int, diff --git a/plugins/baseview/src/win/mod.rs b/plugins/baseview/src/win/mod.rs index b914b08..d79690c 100644 --- a/plugins/baseview/src/win/mod.rs +++ b/plugins/baseview/src/win/mod.rs @@ -1,3 +1,5 @@ +//! Windows platform backend (Win32 / WinAPI). + mod cursor; mod drop_target; mod hook; diff --git a/plugins/baseview/src/win/window.rs b/plugins/baseview/src/win/window.rs index fce33ae..026774c 100644 --- a/plugins/baseview/src/win/window.rs +++ b/plugins/baseview/src/win/window.rs @@ -1,3 +1,5 @@ +//! Win32 window creation, message loop, and event dispatch. + use winapi::shared::guiddef::GUID; use winapi::shared::minwindef::{ATOM, FALSE, LOWORD, LPARAM, LRESULT, UINT, WPARAM}; use winapi::shared::windef::{HWND, POINT, RECT}; diff --git a/plugins/baseview/src/window.rs b/plugins/baseview/src/window.rs index 49d81aa..a99634f 100644 --- a/plugins/baseview/src/window.rs +++ b/plugins/baseview/src/window.rs @@ -1,3 +1,5 @@ +//! Platform-independent window API and handler trait. + use std::marker::PhantomData; use raw_window_handle::{ @@ -15,6 +17,7 @@ use crate::win as platform; #[cfg(target_os = "linux")] use crate::x11 as platform; +/// Opaque handle to an open window, used to close it or check liveness. pub struct WindowHandle { window_handle: platform::WindowHandle, // so that WindowHandle is !Send on all platforms @@ -44,11 +47,13 @@ unsafe impl HasRawWindowHandle for WindowHandle { } } +/// Trait implemented by the application to receive window events and frame callbacks. pub trait WindowHandler { fn on_frame(&mut self, window: &mut Window); fn on_event(&mut self, window: &mut Window, event: Event) -> EventStatus; } +/// A window that can be drawn to and receive events. pub struct Window<'a> { window: platform::Window<'a>, @@ -67,6 +72,7 @@ impl<'a> Window<'a> { Window { window, phantom: PhantomData } } + /// Open a window as a child of the given parent. pub fn open_parented(parent: &P, options: WindowOpenOptions, build: B) -> WindowHandle where P: HasRawWindowHandle, @@ -78,6 +84,7 @@ impl<'a> Window<'a> { WindowHandle::new(window_handle) } + /// Open a standalone window and block until it is closed. pub fn open_blocking(options: WindowOpenOptions, build: B) where H: WindowHandler + 'static, @@ -103,14 +110,17 @@ impl<'a> Window<'a> { self.window.resize(size); } + /// Set the mouse cursor icon. pub fn set_mouse_cursor(&mut self, cursor: MouseCursor) { self.window.set_mouse_cursor(cursor); } + /// Whether this window currently has keyboard focus. pub fn has_focus(&mut self) -> bool { self.window.has_focus() } + /// Request keyboard focus for this window. pub fn focus(&mut self) { self.window.focus() } diff --git a/plugins/baseview/src/window_info.rs b/plugins/baseview/src/window_info.rs index edb5701..7750756 100644 --- a/plugins/baseview/src/window_info.rs +++ b/plugins/baseview/src/window_info.rs @@ -1,3 +1,5 @@ +//! Window geometry types with logical/physical coordinate conversion. + /// The info about the window #[derive(Debug, Copy, Clone)] pub struct WindowInfo { diff --git a/plugins/baseview/src/window_open_options.rs b/plugins/baseview/src/window_open_options.rs index 7c5cd19..02641fd 100644 --- a/plugins/baseview/src/window_open_options.rs +++ b/plugins/baseview/src/window_open_options.rs @@ -1,3 +1,5 @@ +//! Window creation options. + use crate::Size; /// The dpi scaling policy of the window diff --git a/plugins/baseview/src/x11/cursor.rs b/plugins/baseview/src/x11/cursor.rs index 56ff0d2..4031366 100644 --- a/plugins/baseview/src/x11/cursor.rs +++ b/plugins/baseview/src/x11/cursor.rs @@ -1,3 +1,5 @@ +//! MouseCursor to X11 cursor name mapping. + use std::error::Error; use x11rb::connection::Connection; diff --git a/plugins/baseview/src/x11/event_loop.rs b/plugins/baseview/src/x11/event_loop.rs index 00ef1de..7ad1d64 100644 --- a/plugins/baseview/src/x11/event_loop.rs +++ b/plugins/baseview/src/x11/event_loop.rs @@ -1,3 +1,5 @@ +//! X11 event loop: polls XCB events and dispatches to the window handler. + use crate::x11::keyboard::{convert_key_press_event, convert_key_release_event, key_mods}; use crate::x11::{ParentHandle, Window, WindowInner}; use crate::{ diff --git a/plugins/baseview/src/x11/mod.rs b/plugins/baseview/src/x11/mod.rs index 149df0b..fdbd157 100644 --- a/plugins/baseview/src/x11/mod.rs +++ b/plugins/baseview/src/x11/mod.rs @@ -1,3 +1,5 @@ +//! X11 platform backend (XCB / Xlib). + mod xcb_connection; use xcb_connection::XcbConnection; diff --git a/plugins/baseview/src/x11/visual_info.rs b/plugins/baseview/src/x11/visual_info.rs index 3f1be38..9760438 100644 --- a/plugins/baseview/src/x11/visual_info.rs +++ b/plugins/baseview/src/x11/visual_info.rs @@ -1,3 +1,5 @@ +//! X11 visual selection for window creation. + use crate::x11::xcb_connection::XcbConnection; use std::error::Error; use x11rb::connection::Connection; diff --git a/plugins/baseview/src/x11/window.rs b/plugins/baseview/src/x11/window.rs index 7daf862..45e2d26 100644 --- a/plugins/baseview/src/x11/window.rs +++ b/plugins/baseview/src/x11/window.rs @@ -1,3 +1,5 @@ +//! X11 window creation, lifecycle, and event dispatch. + use std::cell::Cell; use std::error::Error; use std::ffi::c_void; diff --git a/plugins/baseview/src/x11/xcb_connection.rs b/plugins/baseview/src/x11/xcb_connection.rs index a5ea06d..62e8ccc 100644 --- a/plugins/baseview/src/x11/xcb_connection.rs +++ b/plugins/baseview/src/x11/xcb_connection.rs @@ -1,3 +1,5 @@ +//! Shared XCB/Xlib connection wrapper. + use std::cell::RefCell; use std::collections::hash_map::{Entry, HashMap}; use std::error::Error; diff --git a/plugins/cagire-plugins/src/editor.rs b/plugins/cagire-plugins/src/editor.rs index b8e8033..ff0f793 100644 --- a/plugins/cagire-plugins/src/editor.rs +++ b/plugins/cagire-plugins/src/editor.rs @@ -1,3 +1,5 @@ +//! Plugin editor: renders the ratatui TUI inside an egui surface. + use std::sync::atomic::{AtomicBool, AtomicI64}; use std::sync::Arc; use std::time::Instant; @@ -147,6 +149,7 @@ struct EditorState { unsafe impl Send for EditorState {} unsafe impl Sync for EditorState {} +/// Build the egui-based plugin editor with ratatui rendering. pub fn create_editor( params: Arc, egui_state: Arc, diff --git a/plugins/cagire-plugins/src/lib.rs b/plugins/cagire-plugins/src/lib.rs index 15ddbd7..cad61dd 100644 --- a/plugins/cagire-plugins/src/lib.rs +++ b/plugins/cagire-plugins/src/lib.rs @@ -1,3 +1,5 @@ +//! Cagire as a CLAP/VST3 plugin via NIH-plug. + mod editor; mod params; @@ -20,6 +22,7 @@ use cagire::engine::{ use cagire::model::{Dictionary, Rng, Variables}; use params::CagireParams; +/// Channel bridge between the plugin editor and the audio/sequencer threads. pub struct PluginBridge { pub cmd_tx: Sender, pub cmd_rx: Receiver, @@ -37,6 +40,7 @@ struct PendingNoteOff { note: u8, } +/// NIH-plug plugin implementing sequencer, synthesis, and MIDI I/O. pub struct CagirePlugin { params: Arc, seq_state: Option, diff --git a/plugins/cagire-plugins/src/main.rs b/plugins/cagire-plugins/src/main.rs index 7f1d8ff..b38a216 100644 --- a/plugins/cagire-plugins/src/main.rs +++ b/plugins/cagire-plugins/src/main.rs @@ -1,3 +1,5 @@ +//! Standalone entry point for the Cagire plugin. + use cagire_plugins::CagirePlugin; use nih_plug::prelude::*; diff --git a/plugins/cagire-plugins/src/params.rs b/plugins/cagire-plugins/src/params.rs index 9d15855..dddd45f 100644 --- a/plugins/cagire-plugins/src/params.rs +++ b/plugins/cagire-plugins/src/params.rs @@ -1,3 +1,5 @@ +//! Persisted plugin parameters exposed to the DAW. + use std::sync::Arc; use cagire_project::Project; @@ -5,6 +7,7 @@ use nih_plug::prelude::*; use nih_plug_egui::EguiState; use parking_lot::Mutex; +/// DAW-visible parameters and persisted editor/project state. #[derive(Params)] pub struct CagireParams { #[persist = "editor-state"] diff --git a/plugins/egui-baseview/src/lib.rs b/plugins/egui-baseview/src/lib.rs index c49e4aa..79218ce 100644 --- a/plugins/egui-baseview/src/lib.rs +++ b/plugins/egui-baseview/src/lib.rs @@ -1,3 +1,5 @@ +//! Egui integration layer for baseview windows. + mod renderer; mod translate; mod window; diff --git a/plugins/egui-baseview/src/renderer.rs b/plugins/egui-baseview/src/renderer.rs index 23a63ec..7ccfac6 100644 --- a/plugins/egui-baseview/src/renderer.rs +++ b/plugins/egui-baseview/src/renderer.rs @@ -1,3 +1,5 @@ +//! GPU renderer backend selection (currently OpenGL only). + #[cfg(feature = "opengl")] mod opengl; #[cfg(feature = "opengl")] diff --git a/plugins/egui-baseview/src/renderer/opengl.rs b/plugins/egui-baseview/src/renderer/opengl.rs index 5c91ac7..d61812e 100644 --- a/plugins/egui-baseview/src/renderer/opengl.rs +++ b/plugins/egui-baseview/src/renderer/opengl.rs @@ -1,8 +1,11 @@ +//! OpenGL renderer errors and submodule. + use egui_glow::PainterError; use thiserror::Error; pub mod renderer; +/// Errors from OpenGL context or painter initialization. #[derive(Error, Debug)] pub enum OpenGlError { #[error("Failed to get baseview's GL context")] diff --git a/plugins/egui-baseview/src/renderer/opengl/renderer.rs b/plugins/egui-baseview/src/renderer/opengl/renderer.rs index 00407f5..afbd28d 100644 --- a/plugins/egui-baseview/src/renderer/opengl/renderer.rs +++ b/plugins/egui-baseview/src/renderer/opengl/renderer.rs @@ -1,3 +1,5 @@ +//! Glow-based OpenGL renderer for egui inside a baseview window. + use baseview::{PhySize, Window}; use egui::FullOutput; use egui_glow::Painter; @@ -5,6 +7,7 @@ use std::sync::Arc; use super::OpenGlError; +/// OpenGL rendering options for the egui painter. #[derive(Debug, Clone)] pub struct GraphicsConfig { /// Controls whether to apply dithering to minimize banding artifacts. @@ -32,12 +35,14 @@ impl Default for GraphicsConfig { } } +/// Manages glow context and egui painter lifecycle. pub struct Renderer { glow_context: Arc, painter: Painter, } impl Renderer { + /// Create a renderer from the baseview window's GL context. pub fn new(window: &Window, config: GraphicsConfig) -> Result { let context = window.gl_context().ok_or(OpenGlError::NoContext)?; unsafe { @@ -71,6 +76,7 @@ impl Renderer { self.painter.max_texture_side() } + /// Render a completed egui frame to the window. pub fn render( &mut self, window: &Window, diff --git a/plugins/egui-baseview/src/translate.rs b/plugins/egui-baseview/src/translate.rs index 57a079a..bea24a6 100644 --- a/plugins/egui-baseview/src/translate.rs +++ b/plugins/egui-baseview/src/translate.rs @@ -1,3 +1,5 @@ +//! Baseview-to-egui event translation. + pub(crate) fn translate_mouse_button(button: baseview::MouseButton) -> Option { match button { baseview::MouseButton::Left => Some(egui::PointerButton::Primary), diff --git a/plugins/egui-baseview/src/window.rs b/plugins/egui-baseview/src/window.rs index b85d6dc..d4d1018 100644 --- a/plugins/egui-baseview/src/window.rs +++ b/plugins/egui-baseview/src/window.rs @@ -1,3 +1,5 @@ +//! Egui window wrapper over baseview, handling input, rendering, and clipboard. + use std::time::Instant; use baseview::{ @@ -14,6 +16,7 @@ use crate::{renderer::Renderer, GraphicsConfig}; #[cfg(feature = "tracing")] use tracing::{error, warn}; +/// Deferred command queue available during the update callback. pub struct Queue<'a> { bg_color: &'a mut Rgba, close_requested: &'a mut bool, diff --git a/plugins/nih-plug-egui/src/resizable_window.rs b/plugins/nih-plug-egui/src/resizable_window.rs index 6409397..9abd766 100644 --- a/plugins/nih-plug-egui/src/resizable_window.rs +++ b/plugins/nih-plug-egui/src/resizable_window.rs @@ -14,6 +14,7 @@ pub struct ResizableWindow { } impl ResizableWindow { + /// Create a resizable window with the given ID source. pub fn new(id_source: impl std::hash::Hash) -> Self { Self { id: Id::new(id_source), @@ -28,6 +29,7 @@ impl ResizableWindow { self } + /// Draw contents inside the resizable window, adding a drag corner. pub fn show( self, context: &Context, @@ -65,6 +67,7 @@ impl ResizableWindow { } } +/// Draw diagonal lines in the corner as a resize affordance. pub fn paint_resize_corner(ui: &Ui, response: &Response) { let stroke = ui.style().interact(response).fg_stroke; diff --git a/plugins/nih-plug-egui/src/widgets/param_slider.rs b/plugins/nih-plug-egui/src/widgets/param_slider.rs index 38b1214..51ffe34 100644 --- a/plugins/nih-plug-egui/src/widgets/param_slider.rs +++ b/plugins/nih-plug-egui/src/widgets/param_slider.rs @@ -1,3 +1,5 @@ +//! Horizontal parameter slider widget with drag, granular drag, and text entry. + use std::sync::{Arc, LazyLock}; use egui_baseview::egui::emath::GuiRounding; diff --git a/src/app/clipboard.rs b/src/app/clipboard.rs index 9758783..84d83db 100644 --- a/src/app/clipboard.rs +++ b/src/app/clipboard.rs @@ -1,3 +1,5 @@ +//! Clipboard operations on steps, patterns, and banks. + use crate::model; use crate::services::clipboard; use crate::state::FlashKind; @@ -273,6 +275,7 @@ impl App { } } + /// Convert linked steps into independent copies. pub fn harden_steps(&mut self) { let (bank, pattern) = self.current_bank_pattern(); let indices = self.selected_steps(); @@ -342,6 +345,7 @@ impl App { ); } + /// Paste steps as linked references to the originals. pub fn link_paste_steps(&mut self) { let Some(copied) = self.editor_ctx.copied_steps.take() else { self.ui.set_status("Nothing copied".to_string()); diff --git a/src/app/dispatch.rs b/src/app/dispatch.rs index 7f61519..916b598 100644 --- a/src/app/dispatch.rs +++ b/src/app/dispatch.rs @@ -3,6 +3,7 @@ use crate::commands::AppCommand; use crate::engine::{LinkState, SequencerSnapshot}; use crate::model::bp_label; +use crate::page::Page; use crate::services::{dict_nav, euclidean, help_nav, pattern_editor}; use crate::state::{undo::UndoEntry, FlashKind, Modal, StagedPropChange}; @@ -215,23 +216,33 @@ impl App { // Page navigation AppCommand::PageLeft => { + self.auto_save_script_on_leave(); self.page.left(); + self.auto_load_script_on_arrive(); self.maybe_show_onboarding(); } AppCommand::PageRight => { + self.auto_save_script_on_leave(); self.page.right(); + self.auto_load_script_on_arrive(); self.maybe_show_onboarding(); } AppCommand::PageUp => { + self.auto_save_script_on_leave(); self.page.up(); + self.auto_load_script_on_arrive(); self.maybe_show_onboarding(); } AppCommand::PageDown => { + self.auto_save_script_on_leave(); self.page.down(); + self.auto_load_script_on_arrive(); self.maybe_show_onboarding(); } AppCommand::GoToPage(page) => { + self.auto_save_script_on_leave(); self.page = page; + self.auto_load_script_on_arrive(); self.maybe_show_onboarding(); } @@ -464,6 +475,35 @@ impl App { AppCommand::SavePrelude => self.save_prelude(), AppCommand::EvaluatePrelude => self.evaluate_prelude(link), AppCommand::ClosePreludeEditor => self.close_prelude_editor(), + + // Periodic script + AppCommand::OpenScriptModal(field) => self.open_script_modal(field), + AppCommand::SetScriptSpeed(speed) => { + self.project_state.project.script_speed = speed; + self.script_editor.dirty = true; + } + AppCommand::SetScriptLength(len) => { + self.project_state.project.script_length = len.clamp(1, 256); + self.script_editor.dirty = true; + } + AppCommand::ScriptSave => self.save_script_from_editor(), + AppCommand::ScriptEvaluate => self.evaluate_script_page(link), + AppCommand::ToggleScriptStack => { + self.script_editor.show_stack = !self.script_editor.show_stack; + } + } + } + + fn auto_save_script_on_leave(&mut self) { + if self.page == Page::Script { + self.save_script_from_editor(); + self.script_editor.focused = false; + } + } + + fn auto_load_script_on_arrive(&mut self) { + if self.page == Page::Script { + self.load_script_to_editor(); } } } diff --git a/src/app/editing.rs b/src/app/editing.rs index b892088..ab8f873 100644 --- a/src/app/editing.rs +++ b/src/app/editing.rs @@ -1,3 +1,5 @@ +//! Pattern and step editing operations (toggle, length, speed, delete, reset). + use crate::services::pattern_editor; use crate::state::FlashKind; diff --git a/src/app/mod.rs b/src/app/mod.rs index b3fd056..93a7297 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -26,7 +26,7 @@ use crate::page::Page; use crate::state::{ undo::UndoHistory, AudioSettings, EditorContext, LiveKeyState, Metrics, Modal, MuteState, OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav, PlaybackState, - ProjectState, UiState, + ProjectState, ScriptEditorState, UiState, }; static COMPLETION_CANDIDATES: LazyLock> = LazyLock::new(|| { @@ -49,6 +49,7 @@ pub struct App { pub page: Page, pub editor_ctx: EditorContext, + pub script_editor: ScriptEditorState, pub patterns_nav: PatternsNav, @@ -104,6 +105,7 @@ impl App { page: Page::default(), editor_ctx: EditorContext::default(), + script_editor: ScriptEditorState::default(), patterns_nav: PatternsNav::default(), @@ -169,6 +171,18 @@ impl App { self.project_state.project.pattern_at(bank, pattern) } + pub fn open_script_modal(&mut self, field: crate::state::ScriptField) { + use crate::state::ScriptField; + let current = match field { + ScriptField::Speed => self.project_state.project.script_speed.label().to_string(), + ScriptField::Length => self.project_state.project.script_length.to_string(), + }; + self.ui.modal = Modal::SetScript { + field, + input: current, + }; + } + pub fn open_pattern_modal(&mut self, field: PatternField) { let current = match field { PatternField::Length => self.current_edit_pattern().length.to_string(), diff --git a/src/app/navigation.rs b/src/app/navigation.rs index fc79f55..3f042a2 100644 --- a/src/app/navigation.rs +++ b/src/app/navigation.rs @@ -1,3 +1,5 @@ +//! Step and bank/pattern cursor navigation. + use super::App; impl App { diff --git a/src/app/persistence.rs b/src/app/persistence.rs index 41d1f39..9c7f459 100644 --- a/src/app/persistence.rs +++ b/src/app/persistence.rs @@ -1,3 +1,5 @@ +//! Project and settings save/load. + use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; @@ -10,6 +12,7 @@ use crate::state::StagedChange; use super::App; impl App { + /// Persist user preferences (audio, display, link, MIDI) via confy. pub fn save_settings(&self, link: &LinkState) { let settings = Settings { audio: crate::settings::AudioSettings { @@ -72,8 +75,12 @@ impl App { settings.save(); } + /// Flush the editor, capture playing state, and write the project file. pub fn save(&mut self, path: PathBuf, link: &LinkState, snapshot: &SequencerSnapshot) { self.save_editor_to_step(); + if self.page == crate::page::Page::Script { + self.save_script_from_editor(); + } self.project_state.project.sample_paths = self.audio.config.sample_paths.clone(); self.project_state.project.tempo = link.tempo(); self.project_state.project.playing_patterns = snapshot @@ -105,6 +112,7 @@ impl App { } } + /// Replace the current project, reset undo/variables, recompile, and restore playing patterns. fn apply_project(&mut self, project: model::Project, label: String, link: &LinkState) { let tempo = project.tempo; let playing = project.playing_patterns.clone(); @@ -122,6 +130,8 @@ impl App { self.dict.lock().clear(); self.evaluate_prelude(link); + self.load_script_to_editor(); + self.script_editor.dirty = true; for (bank, pattern) in playing { self.playback.queued_changes.push(StagedChange { diff --git a/src/app/scripting.rs b/src/app/scripting.rs index 29b68c3..65aed77 100644 --- a/src/app/scripting.rs +++ b/src/app/scripting.rs @@ -1,3 +1,5 @@ +//! Forth script compilation, evaluation, and editor ↔ step synchronization. + use crossbeam_channel::Sender; use crate::engine::LinkState; @@ -8,6 +10,7 @@ use crate::state::{EditorTarget, FlashKind, Modal, SampleTree}; use super::{App, COMPLETION_CANDIDATES}; impl App { + /// Build a `StepContext` for evaluating a script outside the sequencer. pub(super) fn create_step_context(&self, step_idx: usize, link: &LinkState) -> StepContext<'static> { let (bank, pattern) = self.current_bank_pattern(); let speed = self @@ -37,6 +40,7 @@ impl App { } } + /// Load the current step's script into the editor widget. pub(super) fn load_step_to_editor(&mut self) { let (bank, pattern) = self.current_bank_pattern(); if let Some(script) = pattern_editor::get_step_script( @@ -63,6 +67,7 @@ impl App { } } + /// Write the editor widget's content back to the current step. pub fn save_editor_to_step(&mut self) { let text = self.editor_ctx.editor.content(); let (bank, pattern) = self.current_bank_pattern(); @@ -76,6 +81,7 @@ impl App { self.project_state.mark_dirty(change.bank, change.pattern); } + /// Switch the editor to the project prelude script. pub fn open_prelude_editor(&mut self) { let prelude = &self.project_state.project.prelude; let lines: Vec = if prelude.is_empty() { @@ -104,6 +110,7 @@ impl App { self.load_step_to_editor(); } + /// Evaluate the project prelude to seed variables and definitions. pub fn evaluate_prelude(&mut self, link: &LinkState) { let prelude = &self.project_state.project.prelude; if prelude.trim().is_empty() { @@ -121,6 +128,7 @@ impl App { } } + /// Evaluate a script and immediately send its audio commands. pub fn execute_script_oneshot( &self, script: &str, @@ -137,6 +145,7 @@ impl App { Ok(()) } + /// Compile (evaluate) the current step's script to check for errors. pub fn compile_current_step(&mut self, link: &LinkState) { let step_idx = self.editor_ctx.step; let (bank, pattern) = self.current_bank_pattern(); @@ -162,6 +171,49 @@ impl App { } } + /// Load the project's periodic script into the script editor. + pub fn load_script_to_editor(&mut self) { + let script = &self.project_state.project.script; + let lines: Vec = if script.is_empty() { + vec![String::new()] + } else { + script.lines().map(String::from).collect() + }; + self.script_editor.editor.set_content(lines); + self.script_editor.editor.set_candidates(COMPLETION_CANDIDATES.clone()); + self.script_editor + .editor + .set_completion_enabled(self.ui.show_completion); + let tree = SampleTree::from_paths(&self.audio.config.sample_paths); + self.script_editor.editor.set_sample_folders(tree.all_folder_names()); + } + + /// Write the script editor content back to the project. + pub fn save_script_from_editor(&mut self) { + let text = self.script_editor.editor.content(); + self.project_state.project.script = text; + self.script_editor.dirty = true; + } + + /// Evaluate the script page content to check for errors. + pub fn evaluate_script_page(&mut self, link: &LinkState) { + let script = self.script_editor.editor.content(); + if script.trim().is_empty() { + return; + } + let ctx = self.create_step_context(0, link); + match self.script_engine.evaluate(&script, &ctx) { + Ok(_) => { + self.ui.flash("Script compiled", 150, FlashKind::Info); + } + Err(e) => { + self.ui + .flash(&format!("Script error: {e}"), 300, FlashKind::Error); + } + } + } + + /// Compile all steps in the current pattern to warm up definitions. pub fn compile_all_steps(&mut self, link: &LinkState) { let pattern_len = self.current_edit_pattern().length; let (bank, pattern) = self.current_bank_pattern(); diff --git a/src/app/sequencer.rs b/src/app/sequencer.rs index cfd6937..d3901ed 100644 --- a/src/app/sequencer.rs +++ b/src/app/sequencer.rs @@ -1,3 +1,5 @@ +//! Sends pattern data, mute state, and queued start/stop changes to the sequencer thread. + use crossbeam_channel::Sender; use crate::engine::{PatternChange, PatternSnapshot, SeqCommand, StepSnapshot}; @@ -5,6 +7,7 @@ use crate::engine::{PatternChange, PatternSnapshot, SeqCommand, StepSnapshot}; use super::App; impl App { + /// Drain staged start/stop changes and send them to the sequencer. pub fn flush_queued_changes(&mut self, cmd_tx: &Sender) { for staged in self.playback.queued_changes.drain(..) { match staged.change { @@ -34,6 +37,19 @@ impl App { }); } + /// Send the periodic script to the sequencer if dirty. + pub fn flush_dirty_script(&mut self, cmd_tx: &Sender) { + if self.script_editor.dirty { + self.script_editor.dirty = false; + let _ = cmd_tx.send(SeqCommand::ScriptUpdate { + script: self.project_state.project.script.clone(), + speed: self.project_state.project.script_speed, + length: self.project_state.project.script_length, + }); + } + } + + /// Snapshot and send all dirty patterns to the sequencer. Returns true if any were sent. pub fn flush_dirty_patterns(&mut self, cmd_tx: &Sender) -> bool { let dirty = self.project_state.take_dirty(); let had_dirty = !dirty.is_empty(); diff --git a/src/app/staging.rs b/src/app/staging.rs index c7e68e0..b624a30 100644 --- a/src/app/staging.rs +++ b/src/app/staging.rs @@ -1,3 +1,5 @@ +//! Staging area for pattern start/stop, mute/solo, and prop changes before commit. + use crate::engine::{PatternChange, SequencerSnapshot}; use crate::model::bp_label; use crate::state::StagedChange; @@ -5,6 +7,7 @@ use crate::state::StagedChange; use super::App; impl App { + /// Toggle a pattern's staged state: unstage if already staged, else stage start or stop. pub fn stage_pattern_toggle( &mut self, bank: usize, diff --git a/src/commands.rs b/src/commands.rs index cf9e2ad..1ebe793 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use crate::model::{FollowUp, LaunchQuantization, PatternSpeed, SyncMode}; use crate::page::Page; -use crate::state::{ColorScheme, DeviceKind, EngineSection, Modal, OptionsFocus, PatternField, SettingKind}; +use crate::state::{ColorScheme, DeviceKind, EngineSection, Modal, OptionsFocus, PatternField, ScriptField, SettingKind}; pub enum AppCommand { // Undo/Redo @@ -302,4 +302,12 @@ pub enum AppCommand { DismissOnboarding, ResetOnboarding, GoToHelpTopic(usize), + + // Periodic script + OpenScriptModal(ScriptField), + SetScriptSpeed(PatternSpeed), + SetScriptLength(usize), + ScriptSave, + ScriptEvaluate, + ToggleScriptStack, } diff --git a/src/engine/sequencer.rs b/src/engine/sequencer.rs index 653fc28..da53494 100644 --- a/src/engine/sequencer.rs +++ b/src/engine/sequencer.rs @@ -125,6 +125,11 @@ pub enum SeqCommand { muted: std::collections::HashSet<(usize, usize)>, soloed: std::collections::HashSet<(usize, usize)>, }, + ScriptUpdate { + script: String, + speed: crate::model::PatternSpeed, + length: usize, + }, StopAll, ResetScriptState, Shutdown, @@ -166,6 +171,7 @@ pub struct SharedSequencerState { pub event_count: usize, pub tempo: f64, pub beat: f64, + pub script_trace: Option, } pub struct SequencerSnapshot { @@ -174,6 +180,7 @@ pub struct SequencerSnapshot { pub event_count: usize, pub tempo: f64, pub beat: f64, + script_trace: Option, } impl From<&SharedSequencerState> for SequencerSnapshot { @@ -184,6 +191,7 @@ impl From<&SharedSequencerState> for SequencerSnapshot { event_count: s.event_count, tempo: s.tempo, beat: s.beat, + script_trace: s.script_trace.clone(), } } } @@ -197,6 +205,7 @@ impl SequencerSnapshot { event_count: 0, tempo: 0.0, beat: 0.0, + script_trace: None, } } @@ -236,6 +245,10 @@ impl SequencerSnapshot { pub fn get_trace(&self, bank: usize, pattern: usize, step: usize) -> Option<&ExecutionTrace> { self.step_traces.get(&(bank, pattern, step)) } + + pub fn script_trace(&self) -> Option<&ExecutionTrace> { + self.script_trace.as_ref() + } } pub struct SequencerHandle { @@ -555,6 +568,12 @@ pub struct SequencerState { soloed: std::collections::HashSet<(usize, usize)>, last_tempo: f64, last_beat: f64, + script_text: String, + script_speed: crate::model::PatternSpeed, + script_length: usize, + script_frontier: f64, + script_step: usize, + script_trace: Option, } impl SequencerState { @@ -586,6 +605,12 @@ impl SequencerState { soloed: std::collections::HashSet::new(), last_tempo: 0.0, last_beat: 0.0, + script_text: String::new(), + script_speed: crate::model::PatternSpeed::default(), + script_length: 16, + script_frontier: -1.0, + script_step: 0, + script_trace: None, } } @@ -670,6 +695,11 @@ impl SequencerState { self.audio_state.flush_midi_notes = true; } } + SeqCommand::ScriptUpdate { script, speed, length } => { + self.script_text = script; + self.script_speed = speed; + self.script_length = length; + } SeqCommand::StopAll => { // Flush pending updates so cache stays current for future launches for ((bank, pattern), snapshot) in self.pending_updates.drain() { @@ -728,6 +758,20 @@ impl SequencerState { input.mouse_down, ); + self.execute_periodic_script( + input.beat, + frontier, + lookahead_end, + input.tempo, + input.quantum, + input.fill, + input.nudge_secs, + input.engine_time, + input.mouse_x, + input.mouse_y, + input.mouse_down, + ); + let new_tempo = self.read_tempo_variable(steps.any_step_fired); self.apply_follow_ups(); @@ -754,6 +798,9 @@ impl SequencerState { } } self.audio_state.prev_beat = -1.0; + self.script_frontier = -1.0; + self.script_step = 0; + self.script_trace = None; self.buf_audio_commands.clear(); let flush = std::mem::take(&mut self.audio_state.flush_midi_notes); TickOutput { @@ -985,6 +1032,87 @@ impl SequencerState { result } + #[allow(clippy::too_many_arguments)] + fn execute_periodic_script( + &mut self, + beat: f64, + frontier: f64, + lookahead_end: f64, + tempo: f64, + quantum: f64, + fill: bool, + nudge_secs: f64, + engine_time: f64, + mouse_x: f64, + mouse_y: f64, + mouse_down: f64, + ) { + if self.script_text.trim().is_empty() { + return; + } + + let script_frontier = if self.script_frontier < 0.0 { + frontier.max(0.0) + } else { + self.script_frontier + }; + + let speed_mult = self.script_speed.multiplier(); + let fire_beats = substeps_in_window(script_frontier, lookahead_end, speed_mult); + + for step_beat in fire_beats { + let beat_delta = step_beat - beat; + let time_delta = if tempo > 0.0 { + (beat_delta / tempo) * 60.0 + } else { + 0.0 + }; + let event_time = Some(engine_time + time_delta); + + let step_in_cycle = self.script_step % self.script_length; + + if step_in_cycle == 0 { + let ctx = StepContext { + step: 0, + beat: step_beat, + bank: 0, + pattern: 0, + tempo, + phase: step_beat % quantum, + slot: 0, + runs: self.script_step / self.script_length, + iter: self.script_step / self.script_length, + speed: speed_mult, + fill, + nudge_secs, + cc_access: self.cc_access.as_deref(), + speed_key: "", + mouse_x, + mouse_y, + mouse_down, + }; + + let mut trace = ExecutionTrace::default(); + if let Ok(cmds) = + self.script_engine + .evaluate_with_trace(&self.script_text, &ctx, &mut trace) + { + for cmd in cmds { + self.event_count += 1; + self.buf_audio_commands.push(TimestampedCommand { + cmd, + time: event_time, + }); + } + } + self.script_trace = Some(trace); + } + self.script_step += 1; + } + + self.script_frontier = lookahead_end; + } + fn read_tempo_variable(&self, any_step_fired: bool) -> Option { if !any_step_fired { return None; @@ -1056,6 +1184,7 @@ impl SequencerState { event_count: self.event_count, tempo: self.last_tempo, beat: self.last_beat, + script_trace: self.script_trace.clone(), } } } diff --git a/src/input/mod.rs b/src/input/mod.rs index 8b6f0a8..a52fbb0 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -8,6 +8,7 @@ mod mouse; pub(crate) mod options_page; mod panel; mod patterns_page; +mod script_page; use arc_swap::ArcSwap; use crossbeam_channel::Sender; @@ -85,6 +86,7 @@ pub fn handle_key(ctx: &mut InputContext, key: KeyEvent) -> InputResult { fn handle_live_keys(ctx: &mut InputContext, key: &KeyEvent) -> bool { match (key.code, key.kind) { _ if !matches!(ctx.app.ui.modal, Modal::None) => false, + _ if ctx.app.page == Page::Script && ctx.app.script_editor.focused => false, (KeyCode::Char('f'), KeyEventKind::Press) => { ctx.dispatch(AppCommand::ToggleLiveKeysFill); true @@ -134,6 +136,7 @@ fn handle_normal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult { KeyCode::F(4) => Some(Page::Help), KeyCode::F(5) => Some(Page::Main), KeyCode::F(6) => Some(Page::Engine), + KeyCode::F(7) => Some(Page::Script), _ => None, } { ctx.app.ui.minimap = MinimapMode::Timed(Instant::now() + Duration::from_millis(250)); @@ -148,6 +151,7 @@ fn handle_normal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult { Page::Options => options_page::handle_options_page(ctx, key), Page::Help => help_page::handle_help_page(ctx, key), Page::Dict => help_page::handle_dict_page(ctx, key), + Page::Script => script_page::handle_script_page(ctx, key), } } diff --git a/src/input/modal.rs b/src/input/modal.rs index 5dbfdd6..b281577 100644 --- a/src/input/modal.rs +++ b/src/input/modal.rs @@ -6,7 +6,7 @@ use crate::engine::SeqCommand; use crate::model::{FollowUp, PatternSpeed}; use crate::state::{ ConfirmAction, EditorTarget, EuclideanField, Modal, PatternField, - PatternPropsField, RenameTarget, + PatternPropsField, RenameTarget, ScriptField, }; pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult { @@ -141,6 +141,44 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input KeyCode::Char(c) => input.push(c), _ => {} }, + Modal::SetScript { field, input } => match key.code { + KeyCode::Enter => { + let field = *field; + match field { + ScriptField::Length => { + if let Ok(len) = input.parse::() { + ctx.dispatch(AppCommand::SetScriptLength(len)); + let new_len = ctx.app.project_state.project.script_length; + ctx.dispatch(AppCommand::SetStatus(format!( + "Script length set to {new_len}" + ))); + } else { + ctx.dispatch(AppCommand::SetStatus("Invalid length".to_string())); + } + } + ScriptField::Speed => { + if let Some(speed) = PatternSpeed::from_label(input) { + ctx.dispatch(AppCommand::SetScriptSpeed(speed)); + ctx.dispatch(AppCommand::SetStatus(format!( + "Script speed set to {}", + speed.label() + ))); + } else { + ctx.dispatch(AppCommand::SetStatus( + "Invalid speed (try 1/3, 2/5, 1x, 2x)".to_string(), + )); + } + } + } + ctx.dispatch(AppCommand::CloseModal); + } + KeyCode::Esc => ctx.dispatch(AppCommand::CloseModal), + KeyCode::Backspace => { + input.pop(); + } + KeyCode::Char(c) => input.push(c), + _ => {} + }, Modal::JumpToStep(input) => match key.code { KeyCode::Enter => { if let Ok(step) = input.parse::() { diff --git a/src/input/mouse.rs b/src/input/mouse.rs index 903dbef..22238cd 100644 --- a/src/input/mouse.rs +++ b/src/input/mouse.rs @@ -7,7 +7,7 @@ use crate::state::{ DeviceKind, DictFocus, EngineSection, HelpFocus, MinimapMode, Modal, OptionsFocus, PatternsColumn, SettingKind, }; -use crate::views::{dict_view, engine_view, help_view, main_view, patterns_view}; +use crate::views::{dict_view, engine_view, help_view, main_view, patterns_view, script_view}; use super::InputContext; @@ -28,9 +28,11 @@ pub fn handle_mouse(ctx: &mut InputContext, mouse: MouseEvent, term: Rect) { MouseEventKind::Down(MouseButton::Left) => handle_click(ctx, col, row, term), MouseEventKind::Drag(MouseButton::Left) | MouseEventKind::Moved => { handle_editor_drag(ctx, col, row, term); + handle_script_editor_drag(ctx, col, row, term); } MouseEventKind::Up(MouseButton::Left) => { ctx.app.editor_ctx.mouse_selecting = false; + ctx.app.script_editor.mouse_selecting = false; } MouseEventKind::ScrollUp => handle_scroll(ctx, col, row, term, true), MouseEventKind::ScrollDown => handle_scroll(ctx, col, row, term, false), @@ -176,6 +178,14 @@ fn handle_scroll(ctx: &mut InputContext, col: u16, row: u16, term: Rect, up: boo ctx.dispatch(AppCommand::StepDown); } } + Page::Script => { + let [editor_area, _] = script_view::layout(body); + if contains(editor_area, col, row) { + use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + let code = if up { KeyCode::Up } else { KeyCode::Down }; + ctx.app.script_editor.editor.input(KeyEvent::new(code, KeyModifiers::empty())); + } + } Page::Help => { let [topics_area, content_area] = help_view::layout(body); if contains(topics_area, col, row) { @@ -305,6 +315,7 @@ fn handle_footer_click(ctx: &mut InputContext, col: u16, row: u16, footer: Rect) Page::Options => " OPTIONS ", Page::Help => " HELP ", Page::Dict => " DICT ", + Page::Script => " SCRIPT ", }; let badge_end = block_inner.x + badge_text.len() as u16; if col < badge_end { @@ -345,6 +356,7 @@ fn handle_body_click(ctx: &mut InputContext, col: u16, row: u16, body: Rect) { Page::Dict => handle_dict_click(ctx, col, row, page_area), Page::Options => handle_options_click(ctx, col, row, page_area), Page::Engine => handle_engine_click(ctx, col, row, page_area), + Page::Script => handle_script_click(ctx, col, row, page_area), } } @@ -697,6 +709,84 @@ fn handle_options_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect) // --- Engine page --- +fn handle_script_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect) { + let [editor_area, _] = script_view::layout(area); + if contains(editor_area, col, row) { + ctx.app.script_editor.focused = true; + handle_script_editor_mouse(ctx, col, row, area, false); + } else { + ctx.app.script_editor.focused = false; + } +} + +fn script_editor_text_area(area: Rect) -> Rect { + let [editor_area, _] = script_view::layout(area); + // Block with borders → inner + let inner = Rect { + x: editor_area.x + 1, + y: editor_area.y + 1, + width: editor_area.width.saturating_sub(2), + height: editor_area.height.saturating_sub(2), + }; + // Editor takes all but last row (hint line) + let editor_height = inner.height.saturating_sub(1); + Rect::new(inner.x, inner.y, inner.width, editor_height) +} + +fn handle_script_editor_drag(ctx: &mut InputContext, col: u16, row: u16, term: Rect) { + if ctx.app.script_editor.mouse_selecting { + let padded = padded(term); + let (_header, body, _footer) = top_level_layout(padded); + let page_area = if ctx.app.panel.visible && ctx.app.panel.side.is_some() { + if body.width >= 120 { + let panel_width = body.width * 35 / 100; + Layout::horizontal([Constraint::Fill(1), Constraint::Length(panel_width)]) + .split(body)[0] + } else { + let panel_height = body.height * 40 / 100; + Layout::vertical([Constraint::Fill(1), Constraint::Length(panel_height)]) + .split(body)[0] + } + } else { + body + }; + handle_script_editor_mouse(ctx, col, row, page_area, true); + } +} + +fn handle_script_editor_mouse( + ctx: &mut InputContext, + col: u16, + row: u16, + area: Rect, + dragging: bool, +) { + let text_area = script_editor_text_area(area); + + if col < text_area.x + || col >= text_area.x + text_area.width + || row < text_area.y + || row >= text_area.y + text_area.height + { + return; + } + + let scroll = ctx.app.script_editor.editor.scroll_offset(); + let text_row = (row - text_area.y) + scroll; + let text_col = col - text_area.x; + + if dragging { + if !ctx.app.script_editor.editor.is_selecting() { + ctx.app.script_editor.editor.start_selection(); + } + } else { + ctx.app.script_editor.mouse_selecting = true; + ctx.app.script_editor.editor.cancel_selection(); + } + + ctx.app.script_editor.editor.move_cursor_to(text_row, text_col); +} + fn handle_engine_click(ctx: &mut InputContext, col: u16, row: u16, area: Rect) { let [left_col, _, _] = engine_view::layout(area); diff --git a/src/input/script_page.rs b/src/input/script_page.rs new file mode 100644 index 0000000..9be4a59 --- /dev/null +++ b/src/input/script_page.rs @@ -0,0 +1,69 @@ +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use std::sync::atomic::Ordering; + +use crate::commands::AppCommand; +use crate::state::{ConfirmAction, Modal, ScriptField}; + +use super::{InputContext, InputResult}; + +pub fn handle_script_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult { + if ctx.app.script_editor.focused { + handle_focused(ctx, key) + } else { + handle_unfocused(ctx, key) + } +} + +fn handle_focused(ctx: &mut InputContext, key: KeyEvent) -> InputResult { + let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); + + match (ctrl, key.code) { + (_, KeyCode::Esc) => { + ctx.dispatch(AppCommand::ScriptSave); + ctx.app.script_editor.focused = false; + } + (true, KeyCode::Char('e')) => { + ctx.dispatch(AppCommand::ScriptSave); + ctx.dispatch(AppCommand::ScriptEvaluate); + } + (true, KeyCode::Char('s')) => { + ctx.dispatch(AppCommand::ToggleScriptStack); + } + _ => { + ctx.app.script_editor.editor.input(key); + } + } + InputResult::Continue +} + +fn handle_unfocused(ctx: &mut InputContext, key: KeyEvent) -> InputResult { + match key.code { + KeyCode::Enter => { + ctx.app.script_editor.focused = true; + } + KeyCode::Char('q') if !ctx.app.plugin_mode => { + ctx.dispatch(AppCommand::OpenModal(Modal::Confirm { + action: ConfirmAction::Quit, + selected: false, + })); + } + KeyCode::Char(' ') if !ctx.app.plugin_mode => { + ctx.dispatch(AppCommand::TogglePlaying); + ctx.playing + .store(ctx.app.playback.playing, Ordering::Relaxed); + } + KeyCode::Char('s') => super::open_save(ctx), + KeyCode::Char('l') => super::open_load(ctx), + KeyCode::Char('?') => { + ctx.dispatch(AppCommand::OpenModal(Modal::KeybindingsHelp { scroll: 0 })); + } + KeyCode::Char('L') => { + ctx.dispatch(AppCommand::OpenScriptModal(ScriptField::Length)); + } + KeyCode::Char('S') => { + ctx.dispatch(AppCommand::OpenScriptModal(ScriptField::Speed)); + } + _ => {} + } + InputResult::Continue +} diff --git a/src/main.rs b/src/main.rs index 77bb330..9e07a95 100644 --- a/src/main.rs +++ b/src/main.rs @@ -281,6 +281,7 @@ fn main() -> io::Result<()> { app.metrics.event_count = seq_snapshot.event_count; app.flush_dirty_patterns(&sequencer.cmd_tx); + app.flush_dirty_script(&sequencer.cmd_tx); app.flush_queued_changes(&sequencer.cmd_tx); let had_event = event::poll(Duration::from_millis( @@ -322,6 +323,8 @@ fn main() -> io::Result<()> { if app.editor_ctx.show_stack { services::stack_preview::update_cache(&app.editor_ctx); } + } else if app.page == page::Page::Script && app.script_editor.focused { + app.script_editor.editor.insert_str(&text); } } _ => {} diff --git a/src/model/onboarding.rs b/src/model/onboarding.rs index 1a4f85f..5aebee0 100644 --- a/src/model/onboarding.rs +++ b/src/model/onboarding.rs @@ -92,5 +92,14 @@ pub fn for_page(page: Page) -> &'static [(&'static str, &'static [(&'static str, ("?", "all keys"), ], )], + Page::Script => &[( + "Write a Forth script that runs periodically during playback, independent of the step sequencer. Use this for autonomous sound generation, drones, generative sequences, or anything that doesn't fit a fixed step grid.", + &[ + ("Esc", "save & back"), + ("Ctrl+E", "evaluate script"), + ("[ ]", "adjust speed"), + ("Ctrl+S", "toggle stack preview"), + ], + )], } } diff --git a/src/page.rs b/src/page.rs index 49a0aff..aad40ab 100644 --- a/src/page.rs +++ b/src/page.rs @@ -7,10 +7,11 @@ pub enum Page { Help, Dict, Options, + Script, } impl Page { - /// All pages for iteration + /// All pages for iteration (grid pages only — Script excluded) pub const ALL: &'static [Page] = &[ Page::Main, Page::Patterns, @@ -28,6 +29,7 @@ impl Page { /// col 0 col 1 col 2 /// row 0 Dict Patterns Options /// row 1 Help Sequencer Engine + /// Script lives outside the grid at (1, 2) pub const fn grid_pos(self) -> (i8, i8) { match self { Page::Dict => (0, 0), @@ -36,10 +38,11 @@ impl Page { Page::Main => (1, 1), Page::Options => (2, 0), Page::Engine => (2, 1), + Page::Script => (1, 2), } } - /// Find page at grid position, if any + /// Find page at grid position, if any (grid pages only) pub fn at_pos(col: i8, row: i8) -> Option { Self::ALL.iter().copied().find(|p| p.grid_pos() == (col, row)) } @@ -53,10 +56,15 @@ impl Page { Page::Help => "Help", Page::Dict => "Dict", Page::Options => "Options", + Page::Script => "Script", } } pub fn left(&mut self) { + if *self == Page::Script { + *self = Page::Help; + return; + } let (col, row) = self.grid_pos(); for offset in 1..=Self::GRID_SIZE.0 { let new_col = (col - offset).rem_euclid(Self::GRID_SIZE.0); @@ -68,6 +76,10 @@ impl Page { } pub fn right(&mut self) { + if *self == Page::Script { + *self = Page::Engine; + return; + } let (col, row) = self.grid_pos(); for offset in 1..=Self::GRID_SIZE.0 { let new_col = (col + offset).rem_euclid(Self::GRID_SIZE.0); @@ -79,6 +91,10 @@ impl Page { } pub fn up(&mut self) { + if *self == Page::Script { + *self = Page::Main; + return; + } let (col, row) = self.grid_pos(); if let Some(page) = Self::at_pos(col, row - 1) { *self = page; @@ -87,6 +103,11 @@ impl Page { pub fn down(&mut self) { let (col, row) = self.grid_pos(); + // From Main (1,1), going down reaches Script + if *self == Page::Main { + *self = Page::Script; + return; + } if let Some(page) = Self::at_pos(col, row + 1) { *self = page; } @@ -100,6 +121,12 @@ impl Page { Page::Help => Some(0), // "Welcome" Page::Dict => Some(7), // "About Forth" Page::Options => None, + Page::Script => None, } } + + /// Whether this page appears in the navigation minimap grid. + pub const fn visible_in_minimap(self) -> bool { + !matches!(self, Page::Script) + } } diff --git a/src/state/editor.rs b/src/state/editor.rs index 6f6f5e3..942db2e 100644 --- a/src/state/editor.rs +++ b/src/state/editor.rs @@ -16,6 +16,12 @@ pub enum PatternField { Speed, } +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum ScriptField { + Speed, + Length, +} + #[derive(Clone, Copy, PartialEq, Eq, Default)] pub enum PatternPropsField { #[default] @@ -155,3 +161,25 @@ impl Default for EditorContext { } } } + +pub struct ScriptEditorState { + pub editor: Editor, + pub show_stack: bool, + pub stack_cache: RefCell>, + pub dirty: bool, + pub focused: bool, + pub mouse_selecting: bool, +} + +impl Default for ScriptEditorState { + fn default() -> Self { + Self { + editor: Editor::new(), + show_stack: false, + stack_cache: RefCell::new(None), + dirty: false, + focused: true, + mouse_selecting: false, + } + } +} diff --git a/src/state/mod.rs b/src/state/mod.rs index a5d4fe7..40724a7 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -34,7 +34,7 @@ pub use audio::{AudioSettings, DeviceKind, EngineSection, MainLayout, Metrics, S pub use color_scheme::ColorScheme; pub use editor::{ CopiedStepData, CopiedSteps, EditorContext, EditorTarget, EuclideanField, PatternField, - PatternPropsField, StackCache, + PatternPropsField, ScriptEditorState, ScriptField, StackCache, }; pub use live_keys::LiveKeyState; pub use modal::{ConfirmAction, Modal, RenameTarget}; diff --git a/src/state/modal.rs b/src/state/modal.rs index b1d172a..d99fd1d 100644 --- a/src/state/modal.rs +++ b/src/state/modal.rs @@ -1,5 +1,5 @@ use crate::model::{self, FollowUp, LaunchQuantization, PatternSpeed, SyncMode}; -use crate::state::editor::{EuclideanField, PatternField, PatternPropsField}; +use crate::state::editor::{EuclideanField, PatternField, PatternPropsField, ScriptField}; use crate::state::file_browser::FileBrowserState; #[derive(Clone, PartialEq, Eq)] @@ -69,6 +69,10 @@ pub enum Modal { field: PatternField, input: String, }, + SetScript { + field: ScriptField, + input: String, + }, SetTempo(String), JumpToStep(String), AddSamplePath(Box), diff --git a/src/views/keybindings.rs b/src/views/keybindings.rs index b0c7fd7..da632ab 100644 --- a/src/views/keybindings.rs +++ b/src/views/keybindings.rs @@ -2,7 +2,7 @@ use crate::page::Page; pub fn bindings_for(page: Page, plugin_mode: bool) -> Vec<(&'static str, &'static str, &'static str)> { let mut bindings = vec![ - ("F1–F6", "Go to view", "Dict/Patterns/Options/Help/Sequencer/Engine"), + ("F1–F7", "Go to view", "Dict/Patterns/Options/Help/Sequencer/Engine/Script"), ("Ctrl+←→↑↓", "Navigate", "Switch between adjacent views"), ]; if !plugin_mode { @@ -128,6 +128,14 @@ pub fn bindings_for(page: Page, plugin_mode: bool) -> Vec<(&'static str, &'stati bindings.push(("Ctrl+F", "Search", "Activate search")); bindings.push(("Esc", "Clear", "Clear search")); } + Page::Script => { + bindings.push(("Enter", "Focus", "Focus editor for typing")); + bindings.push(("Esc", "Unfocus", "Unfocus editor to use page keybindings")); + bindings.push(("Ctrl+E", "Evaluate", "Compile and check for errors (focused)")); + bindings.push(("S", "Set Speed", "Set script speed via text input (unfocused)")); + bindings.push(("L", "Set Length", "Set script length via text input (unfocused)")); + bindings.push(("Ctrl+S", "Stack", "Toggle stack preview (focused)")); + } } bindings diff --git a/src/views/main_view.rs b/src/views/main_view.rs index eb3d2e3..08adb79 100644 --- a/src/views/main_view.rs +++ b/src/views/main_view.rs @@ -87,7 +87,7 @@ fn render_top_layout( render_sequencer(frame, app, snapshot, areas[idx]); } -fn render_audio_viz(frame: &mut Frame, app: &App, area: Rect) { +pub(crate) fn render_audio_viz(frame: &mut Frame, app: &App, area: Rect) { let mut panels: Vec = Vec::new(); if app.audio.config.show_scope { panels.push(VizPanel::Scope); } if app.audio.config.show_spectrum { panels.push(VizPanel::Spectrum); } @@ -491,7 +491,7 @@ fn viz_gain(data: &[f32], config: &crate::state::audio::AudioConfig) -> f32 { } } -fn render_scope(frame: &mut Frame, app: &App, area: Rect, orientation: Orientation) { +pub(crate) fn render_scope(frame: &mut Frame, app: &App, area: Rect, orientation: Orientation) { let theme = theme::get(); let block = Block::default() .borders(Borders::ALL) @@ -507,7 +507,7 @@ fn render_scope(frame: &mut Frame, app: &App, area: Rect, orientation: Orientati frame.render_widget(scope, inner); } -fn render_spectrum(frame: &mut Frame, app: &App, area: Rect) { +pub(crate) fn render_spectrum(frame: &mut Frame, app: &App, area: Rect) { let theme = theme::get(); let block = Block::default() .borders(Borders::ALL) @@ -525,7 +525,7 @@ fn render_spectrum(frame: &mut Frame, app: &App, area: Rect) { frame.render_widget(spectrum, inner); } -fn render_lissajous(frame: &mut Frame, app: &App, area: Rect) { +pub(crate) fn render_lissajous(frame: &mut Frame, app: &App, area: Rect) { let theme = theme::get(); let block = Block::default() .borders(Borders::ALL) @@ -600,7 +600,7 @@ fn render_script_preview( frame.render_widget(Paragraph::new(lines), inner); } -fn render_prelude_preview( +pub(crate) fn render_prelude_preview( frame: &mut Frame, app: &App, user_words: &HashSet, diff --git a/src/views/mod.rs b/src/views/mod.rs index 0407e24..c58eed9 100644 --- a/src/views/mod.rs +++ b/src/views/mod.rs @@ -7,6 +7,7 @@ pub mod main_view; pub mod options_view; pub mod patterns_view; mod render; +pub mod script_view; pub mod title_view; pub use render::{horizontal_padding, render}; diff --git a/src/views/render.rs b/src/views/render.rs index c532ed7..96e3520 100644 --- a/src/views/render.rs +++ b/src/views/render.rs @@ -25,7 +25,8 @@ use crate::widgets::{ }; 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, script_view, + title_view, }; fn clip_span(span: SourceSpan, line_start: usize, line_len: usize) -> Option { @@ -188,6 +189,7 @@ pub fn render( Page::Options => options_view::render(frame, app, link, page_area), Page::Help => help_view::render(frame, app, page_area), Page::Dict => dict_view::render(frame, app, page_area), + Page::Script => script_view::render(frame, app, snapshot, page_area), } if let Some(side_area) = panel_area { @@ -202,6 +204,7 @@ pub fn render( if app.ui.show_minimap() { let tiles: Vec = Page::ALL .iter() + .filter(|p| p.visible_in_minimap()) .map(|p| { let (col, row) = p.grid_pos(); NavTile { @@ -449,6 +452,7 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) { Page::Options => " OPTIONS ", Page::Help => " HELP ", Page::Dict => " DICT ", + Page::Script => " SCRIPT ", }; let content = if let Some(ref msg) = app.ui.status_message { @@ -509,6 +513,13 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) { ("/", "Search"), ("?", "Keys"), ], + Page::Script => vec![ + ("Esc", "Save & Back"), + ("C-e", "Eval"), + ("[ ]", "Speed"), + ("C-s", "Stack"), + ("?", "Keys"), + ], }; let page_width = page_indicator.chars().count(); @@ -608,6 +619,18 @@ fn render_modal( .border_color(theme.modal.confirm) .render_centered(frame, term) } + Modal::SetScript { field, input } => { + use crate::state::ScriptField; + let (title, hint) = match field { + ScriptField::Length => ("Set Script Length (1-256)", "Enter number"), + ScriptField::Speed => ("Set Script Speed", "e.g. 1/3, 2/5, 1x, 2x"), + }; + TextInputModal::new(title, input) + .hint(hint) + .width(45) + .border_color(theme.modal.confirm) + .render_centered(frame, term) + } Modal::JumpToStep(input) => { let pattern_len = app.current_edit_pattern().length; let title = format!("Jump to Step (1-{})", pattern_len); diff --git a/src/views/script_view.rs b/src/views/script_view.rs new file mode 100644 index 0000000..920d967 --- /dev/null +++ b/src/views/script_view.rs @@ -0,0 +1,166 @@ +use std::collections::HashSet; + +use ratatui::layout::{Alignment, Constraint, Layout, Rect}; +use ratatui::style::Style; +use ratatui::text::Span; +use ratatui::widgets::{Block, Borders, Paragraph}; +use ratatui::Frame; + +use crate::app::App; +use crate::engine::SequencerSnapshot; +use crate::model::SourceSpan; +use crate::theme; +use crate::views::highlight; +use crate::views::render::{adjust_resolved_for_line, adjust_spans_for_line}; +use crate::widgets::hint_line; + +pub fn layout(area: Rect) -> [Rect; 2] { + Layout::horizontal([Constraint::Percentage(60), Constraint::Percentage(40)]).areas(area) +} + +pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) { + let [editor_area, sidebar_area] = layout(area); + + render_editor(frame, app, snapshot, editor_area); + render_sidebar(frame, app, sidebar_area); +} + +fn render_editor(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) { + let theme = theme::get(); + let focused = app.script_editor.focused; + let speed_label = app.project_state.project.script_speed.label(); + let length = app.project_state.project.script_length; + let title = format!(" Periodic Script ({speed_label}, {length} steps) "); + + let border_color = if focused { theme.modal.editor } else { theme.ui.border }; + let block = Block::default() + .borders(Borders::ALL) + .title(title) + .border_style(Style::new().fg(border_color)); + let inner = block.inner(area); + frame.render_widget(block, area); + + if inner.height < 2 { + return; + } + + let editor_height = inner.height.saturating_sub(1); + let editor_area = Rect::new(inner.x, inner.y, inner.width, editor_height); + let hint_area = Rect::new(inner.x, inner.y + editor_height, inner.width, 1); + + let user_words: HashSet = app.dict.lock().keys().cloned().collect(); + + let trace = if app.ui.runtime_highlight && app.playback.playing { + snapshot.script_trace() + } else { + None + }; + + let text_lines = app.script_editor.editor.lines(); + let mut line_offsets: Vec = Vec::with_capacity(text_lines.len()); + let mut offset = 0; + for line in text_lines.iter() { + line_offsets.push(offset); + offset += line.len() + 1; + } + + let resolved_display: Vec<(SourceSpan, String)> = trace + .map(|t| t.resolved.iter().map(|(s, v)| (*s, v.display())).collect()) + .unwrap_or_default(); + + let highlighter = |row: usize, line: &str| -> Vec<(Style, String, bool)> { + let line_start = line_offsets[row]; + let (exec, sel, res) = match trace { + Some(t) => ( + adjust_spans_for_line(&t.executed_spans, line_start, line.len()), + adjust_spans_for_line(&t.selected_spans, line_start, line.len()), + adjust_resolved_for_line(&resolved_display, line_start, line.len()), + ), + None => (Vec::new(), Vec::new(), Vec::new()), + }; + highlight::highlight_line_with_runtime(line, &exec, &sel, &res, &user_words) + }; + + app.script_editor.editor.render(frame, editor_area, &highlighter); + + if !focused { + let hints = hint_line(&[ + ("Enter", "edit"), + ("S", "speed"), + ("L", "length"), + ("s", "save"), + ("l", "load"), + ("?", "keys"), + ]); + frame.render_widget(Paragraph::new(hints).alignment(Alignment::Right), hint_area); + } else if app.script_editor.show_stack { + let stack_text = app + .script_editor + .stack_cache + .borrow() + .as_ref() + .map(|c| c.result.clone()) + .unwrap_or_else(|| "Stack: []".to_string()); + let hints = hint_line(&[("Esc", "unfocus"), ("C-e", "eval"), ("C-s", "hide stack")]); + let [hint_left, stack_right] = Layout::horizontal([ + Constraint::Length(hints.width() as u16), + Constraint::Fill(1), + ]) + .areas(hint_area); + frame.render_widget(Paragraph::new(hints), hint_left); + let dim = Style::default().fg(theme.hint.text); + frame.render_widget( + Paragraph::new(Span::styled(stack_text, dim)).alignment(Alignment::Right), + stack_right, + ); + } else { + let hints = hint_line(&[ + ("Esc", "unfocus"), + ("C-e", "eval"), + ("C-s", "stack"), + ]); + frame.render_widget(Paragraph::new(hints).alignment(Alignment::Right), hint_area); + } +} + +fn render_sidebar(frame: &mut Frame, app: &App, area: Rect) { + use crate::widgets::Orientation; + + let mut constraints = Vec::new(); + if app.audio.config.show_scope { + constraints.push(Constraint::Fill(1)); + } + if app.audio.config.show_spectrum { + constraints.push(Constraint::Fill(1)); + } + if app.audio.config.show_lissajous { + constraints.push(Constraint::Fill(1)); + } + let has_prelude = !app.project_state.project.prelude.trim().is_empty(); + if has_prelude { + constraints.push(Constraint::Fill(1)); + } + if constraints.is_empty() { + return; + } + + let areas: Vec = Layout::vertical(&constraints).split(area).to_vec(); + let mut idx = 0; + + if app.audio.config.show_scope { + super::main_view::render_scope(frame, app, areas[idx], Orientation::Horizontal); + idx += 1; + } + if app.audio.config.show_spectrum { + super::main_view::render_spectrum(frame, app, areas[idx]); + idx += 1; + } + if app.audio.config.show_lissajous { + super::main_view::render_lissajous(frame, app, areas[idx]); + idx += 1; + } + if has_prelude { + let user_words: HashSet = app.dict.lock().keys().cloned().collect(); + super::main_view::render_prelude_preview(frame, app, &user_words, areas[idx]); + } +}