diff --git a/crates/ratatui/src/theme/mod.rs b/crates/ratatui/src/theme/mod.rs index 75fa679..b714302 100644 --- a/crates/ratatui/src/theme/mod.rs +++ b/crates/ratatui/src/theme/mod.rs @@ -30,6 +30,7 @@ pub mod transform; use ratatui::style::Color; use std::cell::RefCell; +use std::rc::Rc; /// Entry in the theme registry: id, display label, and palette constructor. pub struct ThemeEntry { @@ -66,17 +67,17 @@ pub const THEMES: &[ThemeEntry] = &[ ]; thread_local! { - static CURRENT_THEME: RefCell = RefCell::new(build::build(&(THEMES[0].palette)())); + static CURRENT_THEME: RefCell> = RefCell::new(Rc::new(build::build(&(THEMES[0].palette)()))); } -/// Return the current thread-local theme. -pub fn get() -> ThemeColors { - CURRENT_THEME.with(|t| t.borrow().clone()) +/// Return the current thread-local theme (cheap Rc clone, not a deep copy). +pub fn get() -> Rc { + CURRENT_THEME.with(|t| Rc::clone(&t.borrow())) } /// Set the current thread-local theme. pub fn set(theme: ThemeColors) { - CURRENT_THEME.with(|t| *t.borrow_mut() = theme); + CURRENT_THEME.with(|t| *t.borrow_mut() = Rc::new(theme)); } /// Complete set of resolved colors for all UI components. diff --git a/src/app/mod.rs b/src/app/mod.rs index d2e2815..595274c 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -14,7 +14,7 @@ use arc_swap::ArcSwap; use parking_lot::Mutex; use rand::rngs::StdRng; use rand::SeedableRng; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::{Arc, LazyLock}; use cagire_ratatui::CompletionCandidate; @@ -69,6 +69,7 @@ pub struct App { pub sample_browser: Option, pub midi: MidiState, pub plugin_mode: bool, + pub dict_keys: HashSet, } impl Default for App { @@ -126,6 +127,7 @@ impl App { sample_browser: None, midi: MidiState::new(), plugin_mode, + dict_keys: HashSet::new(), } } diff --git a/src/app/scripting.rs b/src/app/scripting.rs index 887bfa4..2e97b39 100644 --- a/src/app/scripting.rs +++ b/src/app/scripting.rs @@ -149,7 +149,9 @@ impl App { } let ctx = self.create_step_context(0, link); match self.script_engine.evaluate(prelude, &ctx) { - Ok(_) => {} + Ok(_) => { + self.dict_keys = self.dict.lock().keys().cloned().collect(); + } Err(e) => { let fallback = format!("Bank {}", bank + 1); let bank_name = self.project_state.project.banks[bank] @@ -202,6 +204,7 @@ impl App { } } } + self.dict_keys = self.dict.lock().keys().cloned().collect(); self.ui.flash("Preludes evaluated", 150, FlashKind::Info); } diff --git a/src/engine/audio.rs b/src/engine/audio.rs index 5055c57..3b8f7e7 100644 --- a/src/engine/audio.rs +++ b/src/engine/audio.rs @@ -264,10 +264,6 @@ use cpal::Stream; use crossbeam_channel::{Receiver, Sender}; #[cfg(feature = "cli")] use doux::{Engine, EngineMetrics}; -#[cfg(feature = "cli")] -use std::collections::VecDeque; -#[cfg(feature = "cli")] -use std::sync::Mutex; #[cfg(feature = "cli")] use super::AudioCommand; @@ -360,8 +356,7 @@ pub fn build_stream( let registry = Arc::clone(&engine.sample_registry); const INPUT_BUFFER_SIZE: usize = 8192; - let input_buffer: Arc>> = - Arc::new(Mutex::new(VecDeque::with_capacity(INPUT_BUFFER_SIZE))); + let (input_producer, input_consumer) = HeapRb::::new(INPUT_BUFFER_SIZE).split(); let input_device = config .input_device @@ -399,17 +394,12 @@ pub fn build_stream( input_cfg.channels(), input_cfg.sample_rate() ); - let buf = Arc::clone(&input_buffer); + let mut input_producer = input_producer; let stream = dev .build_input_stream( &input_cfg.into(), move |data: &[f32], _| { - let mut b = buf.lock().unwrap(); - b.extend(data.iter().copied()); - let excess = b.len().saturating_sub(INPUT_BUFFER_SIZE); - if excess > 0 { - drop(b.drain(..excess)); - } + input_producer.push_slice(data); }, { let device_lost = Arc::clone(&device_lost); @@ -436,7 +426,7 @@ pub fn build_stream( let mut cmd_buffer = String::with_capacity(256); let mut rt_set = false; let mut live_scratch = vec![0.0f32; 4096]; - let input_buf_clone = Arc::clone(&input_buffer); + let mut input_consumer = input_consumer; let stream = device .build_output_stream( @@ -491,29 +481,28 @@ pub fn build_stream( if live_scratch.len() < stereo_len { live_scratch.resize(stereo_len, 0.0); } - let mut buf = input_buf_clone.lock().unwrap(); match input_channels { 0 => { live_scratch[..stereo_len].fill(0.0); } 1 => { for i in 0..buffer_samples { - let s = buf.pop_front().unwrap_or(0.0); + let s = input_consumer.try_pop().unwrap_or(0.0); live_scratch[i * 2] = s; live_scratch[i * 2 + 1] = s; } } 2 => { for sample in &mut live_scratch[..stereo_len] { - *sample = buf.pop_front().unwrap_or(0.0); + *sample = input_consumer.try_pop().unwrap_or(0.0); } } _ => { for i in 0..buffer_samples { - let l = buf.pop_front().unwrap_or(0.0); - let r = buf.pop_front().unwrap_or(0.0); + let l = input_consumer.try_pop().unwrap_or(0.0); + let r = input_consumer.try_pop().unwrap_or(0.0); for _ in 2..input_channels { - buf.pop_front(); + input_consumer.try_pop(); } live_scratch[i * 2] = l; live_scratch[i * 2 + 1] = r; @@ -521,11 +510,10 @@ pub fn build_stream( } } // Discard excess if input produced more than we consumed - let excess = buf.len().saturating_sub(INPUT_BUFFER_SIZE / 2); - if excess > 0 { - drop(buf.drain(..excess)); + let excess = input_consumer.occupied_len().saturating_sub(INPUT_BUFFER_SIZE / 2); + for _ in 0..excess { + input_consumer.try_pop(); } - drop(buf); engine.metrics.load.set_buffer_time(buffer_time_ns); engine.process_block(data, &[], &live_scratch[..stereo_len]); diff --git a/src/views/main_view.rs b/src/views/main_view.rs index a027c15..6a14172 100644 --- a/src/views/main_view.rs +++ b/src/views/main_view.rs @@ -75,7 +75,6 @@ fn render_top_layout( idx += 1; } if has_preview { - let user_words: HashSet = app.dict.lock().keys().cloned().collect(); let has_prelude = !app.project_state.project.prelude.trim().is_empty() || !app.project_state.project.banks[app.editor_ctx.bank] .prelude @@ -84,10 +83,10 @@ fn render_top_layout( if has_prelude { let [script_area, prelude_area] = Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)]).areas(areas[idx]); - render_script_preview(frame, app, snapshot, &user_words, script_area); - render_prelude_preview(frame, app, &user_words, prelude_area); + render_script_preview(frame, app, snapshot, &app.dict_keys, script_area); + render_prelude_preview(frame, app, &app.dict_keys, prelude_area); } else { - render_script_preview(frame, app, snapshot, &user_words, areas[idx]); + render_script_preview(frame, app, snapshot, &app.dict_keys, areas[idx]); } idx += 1; } @@ -186,19 +185,12 @@ fn render_viz_area( Orientation::Horizontal }; - let user_words_once: Option> = if panels.iter().any(|p| matches!(p, VizPanel::Preview)) { - Some(app.dict.lock().keys().cloned().collect()) - } else { - None - }; - for (panel, panel_area) in panels.iter().zip(areas.iter()) { match panel { VizPanel::Scope => render_scope(frame, app, *panel_area, orientation), VizPanel::Spectrum => render_spectrum(frame, app, *panel_area), VizPanel::Lissajous => render_lissajous(frame, app, *panel_area), VizPanel::Preview => { - let user_words = user_words_once.as_ref().expect("user_words initialized"); let has_prelude = !app.project_state.project.prelude.trim().is_empty() || !app.project_state.project.banks[app.editor_ctx.bank] .prelude @@ -212,10 +204,10 @@ fn render_viz_area( Layout::horizontal([Constraint::Fill(1), Constraint::Fill(1)]) .areas(*panel_area) }; - render_script_preview(frame, app, snapshot, user_words, script_area); - render_prelude_preview(frame, app, user_words, prelude_area); + render_script_preview(frame, app, snapshot, &app.dict_keys, script_area); + render_prelude_preview(frame, app, &app.dict_keys, prelude_area); } else { - render_script_preview(frame, app, snapshot, user_words, *panel_area); + render_script_preview(frame, app, snapshot, &app.dict_keys, *panel_area); } } } diff --git a/src/views/render.rs b/src/views/render.rs index 46f4594..10d89e2 100644 --- a/src/views/render.rs +++ b/src/views/render.rs @@ -675,8 +675,7 @@ fn render_modal( .render_centered(frame, term) } Modal::Editor => { - let user_words: HashSet = app.dict.lock().keys().cloned().collect(); - render_modal_editor(frame, app, snapshot, &user_words, term) + render_modal_editor(frame, app, snapshot, &app.dict_keys, term) } Modal::PatternProps { bank, diff --git a/src/views/script_view.rs b/src/views/script_view.rs index 8bc0169..da9930d 100644 --- a/src/views/script_view.rs +++ b/src/views/script_view.rs @@ -1,5 +1,3 @@ -use std::collections::HashSet; - use ratatui::layout::{Alignment, Constraint, Layout, Rect}; use ratatui::style::Style; use ratatui::widgets::{Block, Borders, Paragraph}; @@ -47,7 +45,7 @@ fn render_editor(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, are 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 user_words = &app.dict_keys; let trace = if app.ui.runtime_highlight && app.playback.playing { snapshot.script_trace() @@ -77,7 +75,7 @@ fn render_editor(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, are ), None => (Vec::new(), Vec::new(), Vec::new()), }; - highlight::highlight_line_with_runtime(line, &exec, &sel, &res, &user_words) + highlight::highlight_line_with_runtime(line, &exec, &sel, &res, user_words) }; app.script_editor.editor.render(frame, editor_area, &highlighter); @@ -142,7 +140,6 @@ fn render_sidebar(frame: &mut Frame, app: &App, area: Rect) { 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]); + super::main_view::render_prelude_preview(frame, app, &app.dict_keys, areas[idx]); } }