Ungoing refactoring
This commit is contained in:
14
src/app.rs
14
src/app.rs
@@ -22,7 +22,7 @@ use crate::state::{
|
||||
MuteState, OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav,
|
||||
PlaybackState, ProjectState, StagedChange, StagedPropChange, UiState,
|
||||
};
|
||||
use crate::views::{dict_view, help_view};
|
||||
use crate::model::{categories, docs};
|
||||
|
||||
const STEPS_PER_PAGE: usize = 32;
|
||||
|
||||
@@ -1240,11 +1240,11 @@ impl App {
|
||||
};
|
||||
}
|
||||
AppCommand::HelpNextTopic(n) => {
|
||||
let count = help_view::topic_count();
|
||||
let count = docs::topic_count();
|
||||
self.ui.help_topic = (self.ui.help_topic + n) % count;
|
||||
}
|
||||
AppCommand::HelpPrevTopic(n) => {
|
||||
let count = help_view::topic_count();
|
||||
let count = docs::topic_count();
|
||||
self.ui.help_topic = (self.ui.help_topic + count - (n % count)) % count;
|
||||
}
|
||||
AppCommand::HelpScrollDown(n) => {
|
||||
@@ -1264,7 +1264,7 @@ impl App {
|
||||
}
|
||||
AppCommand::HelpSearchInput(c) => {
|
||||
self.ui.help_search_query.push(c);
|
||||
if let Some((topic, line)) = help_view::find_match(&self.ui.help_search_query) {
|
||||
if let Some((topic, line)) = docs::find_match(&self.ui.help_search_query) {
|
||||
self.ui.help_topic = topic;
|
||||
self.ui.help_scrolls[topic] = line;
|
||||
}
|
||||
@@ -1274,7 +1274,7 @@ impl App {
|
||||
if self.ui.help_search_query.is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Some((topic, line)) = help_view::find_match(&self.ui.help_search_query) {
|
||||
if let Some((topic, line)) = docs::find_match(&self.ui.help_search_query) {
|
||||
self.ui.help_topic = topic;
|
||||
self.ui.help_scrolls[topic] = line;
|
||||
}
|
||||
@@ -1291,11 +1291,11 @@ impl App {
|
||||
};
|
||||
}
|
||||
AppCommand::DictNextCategory => {
|
||||
let count = dict_view::category_count();
|
||||
let count = categories::category_count();
|
||||
self.ui.dict_category = (self.ui.dict_category + 1) % count;
|
||||
}
|
||||
AppCommand::DictPrevCategory => {
|
||||
let count = dict_view::category_count();
|
||||
let count = categories::category_count();
|
||||
self.ui.dict_category = (self.ui.dict_category + count - 1) % count;
|
||||
}
|
||||
AppCommand::DictScrollDown(n) => {
|
||||
|
||||
@@ -15,15 +15,14 @@ use soft_ratatui::embedded_graphics_unicodefonts::{
|
||||
};
|
||||
use soft_ratatui::{EmbeddedGraphics, SoftBackend};
|
||||
|
||||
use cagire::app::App;
|
||||
use cagire::init::{init, InitArgs};
|
||||
use cagire::engine::{
|
||||
build_stream, spawn_sequencer, AnalysisHandle, AudioStreamConfig, LinkState, MidiCommand,
|
||||
ScopeBuffer, SequencerConfig, SequencerHandle, SpectrumBuffer,
|
||||
build_stream, AnalysisHandle, AudioStreamConfig, LinkState, MidiCommand, ScopeBuffer,
|
||||
SequencerHandle, SpectrumBuffer,
|
||||
};
|
||||
use cagire::input::{handle_key, InputContext, InputResult};
|
||||
use cagire::input_egui::convert_egui_events;
|
||||
use cagire::settings::Settings;
|
||||
use cagire::state::audio::RefreshRate;
|
||||
use cagire::views;
|
||||
use crossbeam_channel::Receiver;
|
||||
|
||||
@@ -129,7 +128,7 @@ fn create_terminal(font: FontChoice) -> TerminalType {
|
||||
}
|
||||
|
||||
struct CagireDesktop {
|
||||
app: App,
|
||||
app: cagire::app::App,
|
||||
terminal: TerminalType,
|
||||
link: Arc<LinkState>,
|
||||
sequencer: Option<SequencerHandle>,
|
||||
@@ -153,139 +152,39 @@ struct CagireDesktop {
|
||||
|
||||
impl CagireDesktop {
|
||||
fn new(cc: &eframe::CreationContext<'_>, args: Args) -> Self {
|
||||
let settings = Settings::load();
|
||||
let b = init(InitArgs {
|
||||
samples: args.samples,
|
||||
output: args.output,
|
||||
input: args.input,
|
||||
channels: args.channels,
|
||||
buffer: args.buffer,
|
||||
});
|
||||
|
||||
let link = Arc::new(LinkState::new(settings.link.tempo, settings.link.quantum));
|
||||
if settings.link.enabled {
|
||||
link.enable();
|
||||
}
|
||||
|
||||
let playing = Arc::new(AtomicBool::new(true));
|
||||
let nudge_us = Arc::new(AtomicI64::new(0));
|
||||
|
||||
let mut app = App::new();
|
||||
|
||||
app.playback
|
||||
.queued_changes
|
||||
.push(cagire::state::StagedChange {
|
||||
change: cagire::engine::PatternChange::Start {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
},
|
||||
quantization: cagire::model::LaunchQuantization::Immediate,
|
||||
sync_mode: cagire::model::SyncMode::Reset,
|
||||
});
|
||||
|
||||
app.audio.config.output_device = args.output.or(settings.audio.output_device);
|
||||
app.audio.config.input_device = args.input.or(settings.audio.input_device);
|
||||
app.audio.config.channels = args.channels.unwrap_or(settings.audio.channels);
|
||||
app.audio.config.buffer_size = args.buffer.unwrap_or(settings.audio.buffer_size);
|
||||
app.audio.config.max_voices = settings.audio.max_voices;
|
||||
app.audio.config.lookahead_ms = settings.audio.lookahead_ms;
|
||||
app.audio.config.sample_paths = args.samples;
|
||||
app.audio.config.refresh_rate = RefreshRate::from_fps(settings.display.fps);
|
||||
app.ui.runtime_highlight = settings.display.runtime_highlight;
|
||||
app.audio.config.show_scope = settings.display.show_scope;
|
||||
app.audio.config.show_spectrum = settings.display.show_spectrum;
|
||||
app.ui.show_completion = settings.display.show_completion;
|
||||
|
||||
let metrics = Arc::new(EngineMetrics::default());
|
||||
let scope_buffer = Arc::new(ScopeBuffer::new());
|
||||
let spectrum_buffer = Arc::new(SpectrumBuffer::new());
|
||||
|
||||
let audio_sample_pos = Arc::new(AtomicU64::new(0));
|
||||
let sample_rate_shared = Arc::new(AtomicU32::new(44100));
|
||||
let lookahead_ms = Arc::new(AtomicU32::new(settings.audio.lookahead_ms));
|
||||
|
||||
let mut initial_samples = Vec::new();
|
||||
for path in &app.audio.config.sample_paths {
|
||||
let index = doux::sampling::scan_samples_dir(path);
|
||||
app.audio.config.sample_count += index.len();
|
||||
initial_samples.extend(index);
|
||||
}
|
||||
|
||||
let mouse_x = Arc::new(AtomicU32::new(0.5_f32.to_bits()));
|
||||
let mouse_y = Arc::new(AtomicU32::new(0.5_f32.to_bits()));
|
||||
let mouse_down = Arc::new(AtomicU32::new(0.0_f32.to_bits()));
|
||||
|
||||
let seq_config = SequencerConfig {
|
||||
audio_sample_pos: Arc::clone(&audio_sample_pos),
|
||||
sample_rate: Arc::clone(&sample_rate_shared),
|
||||
lookahead_ms: Arc::clone(&lookahead_ms),
|
||||
cc_access: Some(
|
||||
Arc::new(app.midi.cc_memory.clone()) as Arc<dyn cagire::model::CcAccess>
|
||||
),
|
||||
mouse_x: Arc::clone(&mouse_x),
|
||||
mouse_y: Arc::clone(&mouse_y),
|
||||
mouse_down: Arc::clone(&mouse_down),
|
||||
};
|
||||
|
||||
let (sequencer, initial_audio_rx, midi_rx) = spawn_sequencer(
|
||||
Arc::clone(&link),
|
||||
Arc::clone(&playing),
|
||||
Arc::clone(&app.variables),
|
||||
Arc::clone(&app.dict),
|
||||
Arc::clone(&app.rng),
|
||||
settings.link.quantum,
|
||||
Arc::clone(&app.live_keys),
|
||||
Arc::clone(&nudge_us),
|
||||
seq_config,
|
||||
);
|
||||
|
||||
let stream_config = AudioStreamConfig {
|
||||
output_device: app.audio.config.output_device.clone(),
|
||||
channels: app.audio.config.channels,
|
||||
buffer_size: app.audio.config.buffer_size,
|
||||
max_voices: app.audio.config.max_voices,
|
||||
};
|
||||
|
||||
let (stream, analysis_handle) = match build_stream(
|
||||
&stream_config,
|
||||
initial_audio_rx,
|
||||
Arc::clone(&scope_buffer),
|
||||
Arc::clone(&spectrum_buffer),
|
||||
Arc::clone(&metrics),
|
||||
initial_samples,
|
||||
Arc::clone(&audio_sample_pos),
|
||||
) {
|
||||
Ok((s, info, analysis)) => {
|
||||
app.audio.config.sample_rate = info.sample_rate;
|
||||
sample_rate_shared.store(info.sample_rate as u32, Ordering::Relaxed);
|
||||
(Some(s), Some(analysis))
|
||||
}
|
||||
Err(e) => {
|
||||
app.ui.set_status(format!("Audio failed: {e}"));
|
||||
app.audio.error = Some(e);
|
||||
(None, None)
|
||||
}
|
||||
};
|
||||
app.mark_all_patterns_dirty();
|
||||
|
||||
let current_font = FontChoice::from_setting(&settings.display.font);
|
||||
let current_font = FontChoice::from_setting(&b.settings.display.font);
|
||||
let terminal = create_terminal(current_font);
|
||||
|
||||
cc.egui_ctx.set_visuals(egui::Visuals::dark());
|
||||
|
||||
Self {
|
||||
app,
|
||||
app: b.app,
|
||||
terminal,
|
||||
link,
|
||||
sequencer: Some(sequencer),
|
||||
playing,
|
||||
nudge_us,
|
||||
lookahead_ms,
|
||||
metrics,
|
||||
scope_buffer,
|
||||
spectrum_buffer,
|
||||
audio_sample_pos,
|
||||
sample_rate_shared,
|
||||
_stream: stream,
|
||||
_analysis_handle: analysis_handle,
|
||||
midi_rx,
|
||||
link: b.link,
|
||||
sequencer: Some(b.sequencer),
|
||||
playing: b.playing,
|
||||
nudge_us: b.nudge_us,
|
||||
lookahead_ms: b.lookahead_ms,
|
||||
metrics: b.metrics,
|
||||
scope_buffer: b.scope_buffer,
|
||||
spectrum_buffer: b.spectrum_buffer,
|
||||
audio_sample_pos: b.audio_sample_pos,
|
||||
sample_rate_shared: b.sample_rate_shared,
|
||||
_stream: b.stream,
|
||||
_analysis_handle: b.analysis_handle,
|
||||
midi_rx: b.midi_rx,
|
||||
current_font,
|
||||
mouse_x,
|
||||
mouse_y,
|
||||
mouse_down,
|
||||
mouse_x: b.mouse_x,
|
||||
mouse_y: b.mouse_y,
|
||||
mouse_down: b.mouse_down,
|
||||
last_frame: std::time::Instant::now(),
|
||||
}
|
||||
}
|
||||
@@ -334,6 +233,8 @@ impl CagireDesktop {
|
||||
self._stream = Some(new_stream);
|
||||
self._analysis_handle = Some(new_analysis);
|
||||
self.app.audio.config.sample_rate = info.sample_rate;
|
||||
self.app.audio.config.host_name = info.host_name;
|
||||
self.app.audio.config.channels = info.channels;
|
||||
self.sample_rate_shared
|
||||
.store(info.sample_rate as u32, Ordering::Relaxed);
|
||||
self.app.audio.error = None;
|
||||
@@ -419,7 +320,6 @@ impl eframe::App for CagireDesktop {
|
||||
let seq_snapshot = sequencer.snapshot();
|
||||
|
||||
self.app.metrics.event_count = seq_snapshot.event_count;
|
||||
self.app.metrics.dropped_events = seq_snapshot.dropped_events;
|
||||
|
||||
self.app.flush_queued_changes(&sequencer.cmd_tx);
|
||||
self.app.flush_dirty_patterns(&sequencer.cmd_tx);
|
||||
@@ -562,7 +462,6 @@ fn load_icon() -> egui::IconData {
|
||||
}
|
||||
|
||||
fn main() -> eframe::Result<()> {
|
||||
// Lock memory BEFORE any threads are spawned to prevent page faults in RT context
|
||||
#[cfg(unix)]
|
||||
cagire::engine::realtime::lock_memory();
|
||||
|
||||
|
||||
@@ -154,14 +154,12 @@ pub struct SharedSequencerState {
|
||||
pub active_patterns: Vec<ActivePatternState>,
|
||||
pub step_traces: Arc<StepTracesMap>,
|
||||
pub event_count: usize,
|
||||
pub dropped_events: usize,
|
||||
}
|
||||
|
||||
pub struct SequencerSnapshot {
|
||||
pub active_patterns: Vec<ActivePatternState>,
|
||||
step_traces: Arc<StepTracesMap>,
|
||||
pub event_count: usize,
|
||||
pub dropped_events: usize,
|
||||
}
|
||||
|
||||
impl SequencerSnapshot {
|
||||
@@ -205,12 +203,11 @@ impl SequencerHandle {
|
||||
active_patterns: state.active_patterns.clone(),
|
||||
step_traces: Arc::clone(&state.step_traces),
|
||||
event_count: state.event_count,
|
||||
dropped_events: state.dropped_events,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn swap_audio_channel(&self) -> Receiver<AudioCommand> {
|
||||
let (new_tx, new_rx) = bounded::<AudioCommand>(256);
|
||||
let (new_tx, new_rx) = unbounded::<AudioCommand>();
|
||||
self.audio_tx.store(Arc::new(new_tx));
|
||||
new_rx
|
||||
}
|
||||
@@ -294,7 +291,7 @@ pub fn spawn_sequencer(
|
||||
Receiver<MidiCommand>,
|
||||
) {
|
||||
let (cmd_tx, cmd_rx) = bounded::<SeqCommand>(64);
|
||||
let (audio_tx, audio_rx) = bounded::<AudioCommand>(256);
|
||||
let (audio_tx, audio_rx) = unbounded::<AudioCommand>();
|
||||
let (midi_tx, midi_rx) = bounded::<MidiCommand>(256);
|
||||
let audio_tx = Arc::new(ArcSwap::from_pointee(audio_tx));
|
||||
let midi_tx = Arc::new(ArcSwap::from_pointee(midi_tx));
|
||||
@@ -535,7 +532,6 @@ pub(crate) struct SequencerState {
|
||||
runs_counter: RunsCounter,
|
||||
step_traces: Arc<StepTracesMap>,
|
||||
event_count: usize,
|
||||
dropped_events: usize,
|
||||
script_engine: ScriptEngine,
|
||||
variables: Variables,
|
||||
speed_overrides: HashMap<(usize, usize), f64>,
|
||||
@@ -564,7 +560,6 @@ impl SequencerState {
|
||||
runs_counter: RunsCounter::new(),
|
||||
step_traces: Arc::new(HashMap::new()),
|
||||
event_count: 0,
|
||||
dropped_events: 0,
|
||||
script_engine,
|
||||
variables,
|
||||
speed_overrides: HashMap::with_capacity(MAX_PATTERNS),
|
||||
@@ -1057,7 +1052,6 @@ impl SequencerState {
|
||||
.collect(),
|
||||
step_traces: Arc::clone(&self.step_traces),
|
||||
event_count: self.event_count,
|
||||
dropped_events: self.dropped_events,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1175,7 +1169,7 @@ fn sequencer_loop(
|
||||
}
|
||||
} else {
|
||||
// Audio direct to doux — sample-accurate scheduling via /time/ parameter
|
||||
let _ = audio_tx.load().try_send(AudioCommand::Evaluate {
|
||||
let _ = audio_tx.load().send(AudioCommand::Evaluate {
|
||||
cmd: tsc.cmd,
|
||||
time: tsc.time,
|
||||
});
|
||||
|
||||
217
src/init.rs
Normal file
217
src/init.rs
Normal file
@@ -0,0 +1,217 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
use doux::EngineMetrics;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::engine::{
|
||||
build_stream, spawn_sequencer, AnalysisHandle, AudioStreamConfig, LinkState, MidiCommand,
|
||||
PatternChange, ScopeBuffer, SequencerConfig, SequencerHandle, SpectrumBuffer,
|
||||
};
|
||||
use crate::midi;
|
||||
use crate::model;
|
||||
use crate::settings::Settings;
|
||||
use crate::state::audio::RefreshRate;
|
||||
use crate::state::StagedChange;
|
||||
use crate::theme;
|
||||
|
||||
pub struct InitArgs {
|
||||
pub samples: Vec<PathBuf>,
|
||||
pub output: Option<String>,
|
||||
pub input: Option<String>,
|
||||
pub channels: Option<u16>,
|
||||
pub buffer: Option<u32>,
|
||||
}
|
||||
|
||||
pub struct Init {
|
||||
pub app: App,
|
||||
pub link: Arc<LinkState>,
|
||||
pub sequencer: SequencerHandle,
|
||||
pub playing: Arc<AtomicBool>,
|
||||
pub nudge_us: Arc<AtomicI64>,
|
||||
pub lookahead_ms: Arc<AtomicU32>,
|
||||
pub metrics: Arc<EngineMetrics>,
|
||||
pub scope_buffer: Arc<ScopeBuffer>,
|
||||
pub spectrum_buffer: Arc<SpectrumBuffer>,
|
||||
pub audio_sample_pos: Arc<AtomicU64>,
|
||||
pub sample_rate_shared: Arc<AtomicU32>,
|
||||
pub stream: Option<cpal::Stream>,
|
||||
pub analysis_handle: Option<AnalysisHandle>,
|
||||
pub midi_rx: Receiver<MidiCommand>,
|
||||
#[cfg(feature = "desktop")]
|
||||
pub settings: Settings,
|
||||
#[cfg(feature = "desktop")]
|
||||
pub mouse_x: Arc<AtomicU32>,
|
||||
#[cfg(feature = "desktop")]
|
||||
pub mouse_y: Arc<AtomicU32>,
|
||||
#[cfg(feature = "desktop")]
|
||||
pub mouse_down: Arc<AtomicU32>,
|
||||
}
|
||||
|
||||
pub fn init(args: InitArgs) -> Init {
|
||||
let settings = Settings::load();
|
||||
|
||||
let link = Arc::new(LinkState::new(settings.link.tempo, settings.link.quantum));
|
||||
if settings.link.enabled {
|
||||
link.enable();
|
||||
}
|
||||
|
||||
let playing = Arc::new(AtomicBool::new(true));
|
||||
let nudge_us = Arc::new(AtomicI64::new(0));
|
||||
|
||||
let mut app = App::new();
|
||||
|
||||
app.playback.queued_changes.push(StagedChange {
|
||||
change: PatternChange::Start {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
},
|
||||
quantization: model::LaunchQuantization::Immediate,
|
||||
sync_mode: model::SyncMode::Reset,
|
||||
});
|
||||
|
||||
app.audio.config.output_device = args.output.or(settings.audio.output_device.clone());
|
||||
app.audio.config.input_device = args.input.or(settings.audio.input_device.clone());
|
||||
app.audio.config.channels = args.channels.unwrap_or(settings.audio.channels);
|
||||
app.audio.config.buffer_size = args.buffer.unwrap_or(settings.audio.buffer_size);
|
||||
app.audio.config.max_voices = settings.audio.max_voices;
|
||||
app.audio.config.lookahead_ms = settings.audio.lookahead_ms;
|
||||
app.audio.config.sample_paths = args.samples;
|
||||
app.audio.config.refresh_rate = RefreshRate::from_fps(settings.display.fps);
|
||||
app.ui.runtime_highlight = settings.display.runtime_highlight;
|
||||
app.audio.config.show_scope = settings.display.show_scope;
|
||||
app.audio.config.show_spectrum = settings.display.show_spectrum;
|
||||
app.ui.show_completion = settings.display.show_completion;
|
||||
app.ui.color_scheme = settings.display.color_scheme;
|
||||
app.ui.hue_rotation = settings.display.hue_rotation;
|
||||
app.audio.config.layout = settings.display.layout;
|
||||
|
||||
let base_theme = settings.display.color_scheme.to_theme();
|
||||
let rotated =
|
||||
cagire_ratatui::theme::transform::rotate_theme(base_theme, settings.display.hue_rotation);
|
||||
theme::set(rotated);
|
||||
|
||||
// MIDI connections
|
||||
let outputs = midi::list_midi_outputs();
|
||||
let inputs = midi::list_midi_inputs();
|
||||
for (slot, name) in settings.midi.output_devices.iter().enumerate() {
|
||||
if !name.is_empty() {
|
||||
if let Some(idx) = outputs.iter().position(|d| &d.name == name) {
|
||||
let _ = app.midi.connect_output(slot, idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (slot, name) in settings.midi.input_devices.iter().enumerate() {
|
||||
if !name.is_empty() {
|
||||
if let Some(idx) = inputs.iter().position(|d| &d.name == name) {
|
||||
let _ = app.midi.connect_input(slot, idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let metrics = Arc::new(EngineMetrics::default());
|
||||
let scope_buffer = Arc::new(ScopeBuffer::new());
|
||||
let spectrum_buffer = Arc::new(SpectrumBuffer::new());
|
||||
|
||||
let audio_sample_pos = Arc::new(AtomicU64::new(0));
|
||||
let sample_rate_shared = Arc::new(AtomicU32::new(44100));
|
||||
let lookahead_ms = Arc::new(AtomicU32::new(settings.audio.lookahead_ms));
|
||||
|
||||
let mut initial_samples = Vec::new();
|
||||
for path in &app.audio.config.sample_paths {
|
||||
let index = doux::sampling::scan_samples_dir(path);
|
||||
app.audio.config.sample_count += index.len();
|
||||
initial_samples.extend(index);
|
||||
}
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
let mouse_x = Arc::new(AtomicU32::new(0.5_f32.to_bits()));
|
||||
#[cfg(feature = "desktop")]
|
||||
let mouse_y = Arc::new(AtomicU32::new(0.5_f32.to_bits()));
|
||||
#[cfg(feature = "desktop")]
|
||||
let mouse_down = Arc::new(AtomicU32::new(0.0_f32.to_bits()));
|
||||
|
||||
let seq_config = SequencerConfig {
|
||||
audio_sample_pos: Arc::clone(&audio_sample_pos),
|
||||
sample_rate: Arc::clone(&sample_rate_shared),
|
||||
lookahead_ms: Arc::clone(&lookahead_ms),
|
||||
cc_access: Some(Arc::new(app.midi.cc_memory.clone()) as Arc<dyn model::CcAccess>),
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_x: Arc::clone(&mouse_x),
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_y: Arc::clone(&mouse_y),
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_down: Arc::clone(&mouse_down),
|
||||
};
|
||||
|
||||
let (sequencer, initial_audio_rx, midi_rx) = spawn_sequencer(
|
||||
Arc::clone(&link),
|
||||
Arc::clone(&playing),
|
||||
Arc::clone(&app.variables),
|
||||
Arc::clone(&app.dict),
|
||||
Arc::clone(&app.rng),
|
||||
settings.link.quantum,
|
||||
Arc::clone(&app.live_keys),
|
||||
Arc::clone(&nudge_us),
|
||||
seq_config,
|
||||
);
|
||||
|
||||
let stream_config = AudioStreamConfig {
|
||||
output_device: app.audio.config.output_device.clone(),
|
||||
channels: app.audio.config.channels,
|
||||
buffer_size: app.audio.config.buffer_size,
|
||||
max_voices: app.audio.config.max_voices,
|
||||
};
|
||||
|
||||
let (stream, analysis_handle) = match build_stream(
|
||||
&stream_config,
|
||||
initial_audio_rx,
|
||||
Arc::clone(&scope_buffer),
|
||||
Arc::clone(&spectrum_buffer),
|
||||
Arc::clone(&metrics),
|
||||
initial_samples,
|
||||
Arc::clone(&audio_sample_pos),
|
||||
) {
|
||||
Ok((s, info, analysis)) => {
|
||||
app.audio.config.sample_rate = info.sample_rate;
|
||||
app.audio.config.host_name = info.host_name;
|
||||
app.audio.config.channels = info.channels;
|
||||
sample_rate_shared.store(info.sample_rate as u32, Ordering::Relaxed);
|
||||
(Some(s), Some(analysis))
|
||||
}
|
||||
Err(e) => {
|
||||
app.ui.set_status(format!("Audio failed: {e}"));
|
||||
app.audio.error = Some(e);
|
||||
(None, None)
|
||||
}
|
||||
};
|
||||
|
||||
app.mark_all_patterns_dirty();
|
||||
|
||||
Init {
|
||||
app,
|
||||
link,
|
||||
sequencer,
|
||||
playing,
|
||||
nudge_us,
|
||||
lookahead_ms,
|
||||
metrics,
|
||||
scope_buffer,
|
||||
spectrum_buffer,
|
||||
audio_sample_pos,
|
||||
sample_rate_shared,
|
||||
stream,
|
||||
analysis_handle,
|
||||
midi_rx,
|
||||
#[cfg(feature = "desktop")]
|
||||
settings,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_x,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_y,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_down,
|
||||
}
|
||||
}
|
||||
@@ -1435,7 +1435,7 @@ fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
ctx.dispatch(AppCommand::SetColorScheme(new_scheme));
|
||||
}
|
||||
OptionsFocus::HueRotation => {
|
||||
let delta = if key.code == KeyCode::Left { -1.0 } else { 1.0 };
|
||||
let delta = if key.code == KeyCode::Left { -5.0 } else { 5.0 };
|
||||
let new_rotation = (ctx.app.ui.hue_rotation + delta).rem_euclid(360.0);
|
||||
ctx.dispatch(AppCommand::SetHueRotation(new_rotation));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub use cagire_forth as forth;
|
||||
|
||||
pub mod app;
|
||||
pub mod init;
|
||||
pub mod commands;
|
||||
pub mod engine;
|
||||
pub mod input;
|
||||
|
||||
173
src/main.rs
173
src/main.rs
@@ -1,4 +1,5 @@
|
||||
mod app;
|
||||
mod init;
|
||||
mod commands;
|
||||
mod engine;
|
||||
mod input;
|
||||
@@ -14,7 +15,7 @@ mod widgets;
|
||||
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, AtomicU64, Ordering};
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
@@ -24,18 +25,12 @@ use crossterm::terminal::{
|
||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
};
|
||||
use crossterm::ExecutableCommand;
|
||||
use doux::EngineMetrics;
|
||||
use ratatui::prelude::CrosstermBackend;
|
||||
use ratatui::Terminal;
|
||||
|
||||
use app::App;
|
||||
use engine::{
|
||||
build_stream, spawn_sequencer, AudioStreamConfig, LinkState, ScopeBuffer, SequencerConfig,
|
||||
SpectrumBuffer,
|
||||
};
|
||||
use engine::{build_stream, AudioStreamConfig};
|
||||
use init::InitArgs;
|
||||
use input::{handle_key, InputContext, InputResult};
|
||||
use settings::Settings;
|
||||
use state::audio::RefreshRate;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "cagire", version, about = "Forth-based live coding sequencer")]
|
||||
@@ -62,149 +57,33 @@ struct Args {
|
||||
}
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
// Lock memory BEFORE any threads are spawned to prevent page faults in RT context
|
||||
#[cfg(unix)]
|
||||
engine::realtime::lock_memory();
|
||||
|
||||
let args = Args::parse();
|
||||
let settings = Settings::load();
|
||||
|
||||
let link = Arc::new(LinkState::new(settings.link.tempo, settings.link.quantum));
|
||||
if settings.link.enabled {
|
||||
link.enable();
|
||||
}
|
||||
let b = init::init(InitArgs {
|
||||
samples: args.samples,
|
||||
output: args.output,
|
||||
input: args.input,
|
||||
channels: args.channels,
|
||||
buffer: args.buffer,
|
||||
});
|
||||
|
||||
let playing = Arc::new(AtomicBool::new(true));
|
||||
let nudge_us = Arc::new(AtomicI64::new(0));
|
||||
|
||||
let mut app = App::new();
|
||||
|
||||
app.playback
|
||||
.queued_changes
|
||||
.push(crate::state::StagedChange {
|
||||
change: engine::PatternChange::Start {
|
||||
bank: 0,
|
||||
pattern: 0,
|
||||
},
|
||||
quantization: crate::model::LaunchQuantization::Immediate,
|
||||
sync_mode: crate::model::SyncMode::Reset,
|
||||
});
|
||||
|
||||
app.audio.config.output_device = args.output.or(settings.audio.output_device);
|
||||
app.audio.config.input_device = args.input.or(settings.audio.input_device);
|
||||
app.audio.config.channels = args.channels.unwrap_or(settings.audio.channels);
|
||||
app.audio.config.buffer_size = args.buffer.unwrap_or(settings.audio.buffer_size);
|
||||
app.audio.config.max_voices = settings.audio.max_voices;
|
||||
app.audio.config.lookahead_ms = settings.audio.lookahead_ms;
|
||||
app.audio.config.sample_paths = args.samples;
|
||||
app.audio.config.refresh_rate = RefreshRate::from_fps(settings.display.fps);
|
||||
app.ui.runtime_highlight = settings.display.runtime_highlight;
|
||||
app.audio.config.show_scope = settings.display.show_scope;
|
||||
app.audio.config.show_spectrum = settings.display.show_spectrum;
|
||||
app.ui.show_completion = settings.display.show_completion;
|
||||
app.ui.color_scheme = settings.display.color_scheme;
|
||||
app.ui.hue_rotation = settings.display.hue_rotation;
|
||||
app.audio.config.layout = settings.display.layout;
|
||||
let base_theme = settings.display.color_scheme.to_theme();
|
||||
let rotated =
|
||||
cagire_ratatui::theme::transform::rotate_theme(base_theme, settings.display.hue_rotation);
|
||||
theme::set(rotated);
|
||||
|
||||
// Load MIDI settings
|
||||
let outputs = midi::list_midi_outputs();
|
||||
let inputs = midi::list_midi_inputs();
|
||||
for (slot, name) in settings.midi.output_devices.iter().enumerate() {
|
||||
if !name.is_empty() {
|
||||
if let Some(idx) = outputs.iter().position(|d| &d.name == name) {
|
||||
let _ = app.midi.connect_output(slot, idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (slot, name) in settings.midi.input_devices.iter().enumerate() {
|
||||
if !name.is_empty() {
|
||||
if let Some(idx) = inputs.iter().position(|d| &d.name == name) {
|
||||
let _ = app.midi.connect_input(slot, idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let metrics = Arc::new(EngineMetrics::default());
|
||||
let scope_buffer = Arc::new(ScopeBuffer::new());
|
||||
let spectrum_buffer = Arc::new(SpectrumBuffer::new());
|
||||
|
||||
let audio_sample_pos = Arc::new(AtomicU64::new(0));
|
||||
let sample_rate_shared = Arc::new(AtomicU32::new(44100));
|
||||
let lookahead_ms = Arc::new(AtomicU32::new(settings.audio.lookahead_ms));
|
||||
|
||||
let mut initial_samples = Vec::new();
|
||||
for path in &app.audio.config.sample_paths {
|
||||
let index = doux::sampling::scan_samples_dir(path);
|
||||
app.audio.config.sample_count += index.len();
|
||||
initial_samples.extend(index);
|
||||
}
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
let mouse_x = Arc::new(AtomicU32::new(0.5_f32.to_bits()));
|
||||
#[cfg(feature = "desktop")]
|
||||
let mouse_y = Arc::new(AtomicU32::new(0.5_f32.to_bits()));
|
||||
#[cfg(feature = "desktop")]
|
||||
let mouse_down = Arc::new(AtomicU32::new(0.0_f32.to_bits()));
|
||||
|
||||
let seq_config = SequencerConfig {
|
||||
audio_sample_pos: Arc::clone(&audio_sample_pos),
|
||||
sample_rate: Arc::clone(&sample_rate_shared),
|
||||
lookahead_ms: Arc::clone(&lookahead_ms),
|
||||
cc_access: Some(Arc::new(app.midi.cc_memory.clone()) as Arc<dyn crate::model::CcAccess>),
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_x: Arc::clone(&mouse_x),
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_y: Arc::clone(&mouse_y),
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_down: Arc::clone(&mouse_down),
|
||||
};
|
||||
|
||||
let (sequencer, initial_audio_rx, mut midi_rx) = spawn_sequencer(
|
||||
Arc::clone(&link),
|
||||
Arc::clone(&playing),
|
||||
Arc::clone(&app.variables),
|
||||
Arc::clone(&app.dict),
|
||||
Arc::clone(&app.rng),
|
||||
settings.link.quantum,
|
||||
Arc::clone(&app.live_keys),
|
||||
Arc::clone(&nudge_us),
|
||||
seq_config,
|
||||
);
|
||||
|
||||
let stream_config = AudioStreamConfig {
|
||||
output_device: app.audio.config.output_device.clone(),
|
||||
channels: app.audio.config.channels,
|
||||
buffer_size: app.audio.config.buffer_size,
|
||||
max_voices: app.audio.config.max_voices,
|
||||
};
|
||||
|
||||
let (mut _stream, mut _analysis_handle) = match build_stream(
|
||||
&stream_config,
|
||||
initial_audio_rx,
|
||||
Arc::clone(&scope_buffer),
|
||||
Arc::clone(&spectrum_buffer),
|
||||
Arc::clone(&metrics),
|
||||
initial_samples,
|
||||
Arc::clone(&audio_sample_pos),
|
||||
) {
|
||||
Ok((s, info, analysis)) => {
|
||||
app.audio.config.sample_rate = info.sample_rate;
|
||||
app.audio.config.host_name = info.host_name;
|
||||
app.audio.config.channels = info.channels;
|
||||
sample_rate_shared.store(info.sample_rate as u32, Ordering::Relaxed);
|
||||
(Some(s), Some(analysis))
|
||||
}
|
||||
Err(e) => {
|
||||
app.ui.set_status(format!("Audio failed: {e}"));
|
||||
app.audio.error = Some(e);
|
||||
(None, None)
|
||||
}
|
||||
};
|
||||
app.mark_all_patterns_dirty();
|
||||
let mut app = b.app;
|
||||
let link = b.link;
|
||||
let sequencer = b.sequencer;
|
||||
let playing = b.playing;
|
||||
let nudge_us = b.nudge_us;
|
||||
let lookahead_ms = b.lookahead_ms;
|
||||
let metrics = b.metrics;
|
||||
let scope_buffer = b.scope_buffer;
|
||||
let spectrum_buffer = b.spectrum_buffer;
|
||||
let audio_sample_pos = b.audio_sample_pos;
|
||||
let sample_rate_shared = b.sample_rate_shared;
|
||||
let mut _stream = b.stream;
|
||||
let mut _analysis_handle = b.analysis_handle;
|
||||
let mut midi_rx = b.midi_rx;
|
||||
|
||||
enable_raw_mode()?;
|
||||
io::stdout().execute(EnableBracketedPaste)?;
|
||||
@@ -268,7 +147,6 @@ fn main() -> io::Result<()> {
|
||||
|
||||
app.playback.playing = playing.load(Ordering::Relaxed);
|
||||
|
||||
// Process pending MIDI commands
|
||||
while let Ok(midi_cmd) = midi_rx.try_recv() {
|
||||
match midi_cmd {
|
||||
engine::MidiCommand::NoteOn {
|
||||
@@ -335,7 +213,6 @@ fn main() -> io::Result<()> {
|
||||
|
||||
let seq_snapshot = sequencer.snapshot();
|
||||
app.metrics.event_count = seq_snapshot.event_count;
|
||||
app.metrics.dropped_events = seq_snapshot.dropped_events;
|
||||
|
||||
app.flush_queued_changes(&sequencer.cmd_tx);
|
||||
app.flush_dirty_patterns(&sequencer.cmd_tx);
|
||||
|
||||
65
src/model/categories.rs
Normal file
65
src/model/categories.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
pub enum CatEntry {
|
||||
Section(&'static str),
|
||||
Category(&'static str),
|
||||
}
|
||||
|
||||
use CatEntry::{Category, Section};
|
||||
|
||||
pub const CATEGORIES: &[CatEntry] = &[
|
||||
// Forth core
|
||||
Section("Forth"),
|
||||
Category("Stack"),
|
||||
Category("Arithmetic"),
|
||||
Category("Comparison"),
|
||||
Category("Logic"),
|
||||
Category("Control"),
|
||||
Category("Variables"),
|
||||
Category("Probability"),
|
||||
Category("Definitions"),
|
||||
// Live coding
|
||||
Section("Live Coding"),
|
||||
Category("Sound"),
|
||||
Category("Time"),
|
||||
Category("Context"),
|
||||
Category("Music"),
|
||||
Category("LFO"),
|
||||
// Synthesis
|
||||
Section("Synthesis"),
|
||||
Category("Oscillator"),
|
||||
Category("Wavetable"),
|
||||
Category("Generator"),
|
||||
Category("Envelope"),
|
||||
Category("Sample"),
|
||||
// Effects
|
||||
Section("Effects"),
|
||||
Category("Filter"),
|
||||
Category("FM"),
|
||||
Category("Modulation"),
|
||||
Category("Mod FX"),
|
||||
Category("Lo-fi"),
|
||||
Category("Stereo"),
|
||||
Category("Delay"),
|
||||
Category("Reverb"),
|
||||
// External I/O
|
||||
Section("I/O"),
|
||||
Category("MIDI"),
|
||||
Category("Desktop"),
|
||||
];
|
||||
|
||||
pub fn category_count() -> usize {
|
||||
CATEGORIES
|
||||
.iter()
|
||||
.filter(|e| matches!(e, Category(_)))
|
||||
.count()
|
||||
}
|
||||
|
||||
pub fn get_category_name(index: usize) -> &'static str {
|
||||
CATEGORIES
|
||||
.iter()
|
||||
.filter_map(|e| match e {
|
||||
Category(name) => Some(*name),
|
||||
Section(_) => None,
|
||||
})
|
||||
.nth(index)
|
||||
.unwrap_or("Unknown")
|
||||
}
|
||||
86
src/model/docs.rs
Normal file
86
src/model/docs.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
pub enum DocEntry {
|
||||
Section(&'static str),
|
||||
Topic(&'static str, &'static str),
|
||||
}
|
||||
|
||||
use DocEntry::{Section, Topic};
|
||||
|
||||
pub const DOCS: &[DocEntry] = &[
|
||||
// Getting Started
|
||||
Section("Getting Started"),
|
||||
Topic("Welcome", include_str!("../../docs/welcome.md")),
|
||||
Topic("Moving Around", include_str!("../../docs/navigation.md")),
|
||||
Topic(
|
||||
"How Does It Work?",
|
||||
include_str!("../../docs/how_it_works.md"),
|
||||
),
|
||||
Topic(
|
||||
"Banks & Patterns",
|
||||
include_str!("../../docs/banks_patterns.md"),
|
||||
),
|
||||
Topic("Stage / Commit", include_str!("../../docs/staging.md")),
|
||||
Topic("Using the Sequencer", include_str!("../../docs/grid.md")),
|
||||
Topic("Editing a Step", include_str!("../../docs/editing.md")),
|
||||
// Forth fundamentals
|
||||
Section("Forth"),
|
||||
Topic("About Forth", include_str!("../../docs/about_forth.md")),
|
||||
Topic("The Dictionary", include_str!("../../docs/dictionary.md")),
|
||||
Topic("The Stack", include_str!("../../docs/stack.md")),
|
||||
Topic("Creating Words", include_str!("../../docs/definitions.md")),
|
||||
Topic("Oddities", include_str!("../../docs/oddities.md")),
|
||||
// Audio Engine
|
||||
Section("Audio Engine"),
|
||||
Topic("Introduction", include_str!("../../docs/engine_intro.md")),
|
||||
Topic("Settings", include_str!("../../docs/engine_settings.md")),
|
||||
Topic("Sources", include_str!("../../docs/engine_sources.md")),
|
||||
Topic("Samples", include_str!("../../docs/engine_samples.md")),
|
||||
Topic("Wavetables", include_str!("../../docs/engine_wavetable.md")),
|
||||
Topic("Filters", include_str!("../../docs/engine_filters.md")),
|
||||
Topic(
|
||||
"Modulation",
|
||||
include_str!("../../docs/engine_modulation.md"),
|
||||
),
|
||||
Topic(
|
||||
"Distortion",
|
||||
include_str!("../../docs/engine_distortion.md"),
|
||||
),
|
||||
Topic("Space & Time", include_str!("../../docs/engine_space.md")),
|
||||
Topic("Words & Sounds", include_str!("../../docs/engine_words.md")),
|
||||
// MIDI
|
||||
Section("MIDI"),
|
||||
Topic("Introduction", include_str!("../../docs/midi_intro.md")),
|
||||
Topic("MIDI Output", include_str!("../../docs/midi_output.md")),
|
||||
Topic("MIDI Input", include_str!("../../docs/midi_input.md")),
|
||||
];
|
||||
|
||||
pub fn topic_count() -> usize {
|
||||
DOCS.iter().filter(|e| matches!(e, Topic(_, _))).count()
|
||||
}
|
||||
|
||||
pub fn get_topic(index: usize) -> Option<(&'static str, &'static str)> {
|
||||
DOCS.iter()
|
||||
.filter_map(|e| match e {
|
||||
Topic(name, content) => Some((*name, *content)),
|
||||
Section(_) => None,
|
||||
})
|
||||
.nth(index)
|
||||
}
|
||||
|
||||
pub fn find_match(query: &str) -> Option<(usize, usize)> {
|
||||
let query = query.to_lowercase();
|
||||
for (topic_idx, (_, content)) in DOCS
|
||||
.iter()
|
||||
.filter_map(|e| match e {
|
||||
Topic(name, content) => Some((*name, *content)),
|
||||
Section(_) => None,
|
||||
})
|
||||
.enumerate()
|
||||
{
|
||||
for (line_idx, line) in content.lines().enumerate() {
|
||||
if line.to_lowercase().contains(&query) {
|
||||
return Some((topic_idx, line_idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod categories;
|
||||
pub mod docs;
|
||||
mod script;
|
||||
|
||||
pub use cagire_forth::{lookup_word, Word, WordCompile, WORDS};
|
||||
|
||||
@@ -188,7 +188,6 @@ impl CyclicEnum for SettingKind {
|
||||
|
||||
pub struct Metrics {
|
||||
pub event_count: usize,
|
||||
pub dropped_events: usize,
|
||||
pub active_voices: usize,
|
||||
pub peak_voices: usize,
|
||||
pub cpu_load: f32,
|
||||
@@ -204,7 +203,6 @@ impl Default for Metrics {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
event_count: 0,
|
||||
dropped_events: 0,
|
||||
active_voices: 0,
|
||||
peak_voices: 0,
|
||||
cpu_load: 0.0,
|
||||
|
||||
@@ -70,12 +70,12 @@ impl Default for UiState {
|
||||
modal: Modal::None,
|
||||
help_focus: HelpFocus::default(),
|
||||
help_topic: 0,
|
||||
help_scrolls: vec![0; crate::views::help_view::topic_count()],
|
||||
help_scrolls: vec![0; crate::model::docs::topic_count()],
|
||||
help_search_active: false,
|
||||
help_search_query: String::new(),
|
||||
dict_focus: DictFocus::default(),
|
||||
dict_category: 0,
|
||||
dict_scrolls: vec![0; crate::views::dict_view::category_count()],
|
||||
dict_scrolls: vec![0; crate::model::categories::category_count()],
|
||||
dict_search_query: String::new(),
|
||||
dict_search_active: false,
|
||||
show_title: true,
|
||||
|
||||
@@ -5,58 +5,13 @@ use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::model::categories::{get_category_name, CatEntry, CATEGORIES};
|
||||
use crate::model::{Word, WORDS};
|
||||
use crate::state::DictFocus;
|
||||
use crate::theme;
|
||||
|
||||
enum CatEntry {
|
||||
Section(&'static str),
|
||||
Category(&'static str),
|
||||
}
|
||||
|
||||
use CatEntry::{Category, Section};
|
||||
|
||||
const CATEGORIES: &[CatEntry] = &[
|
||||
// Forth core
|
||||
Section("Forth"),
|
||||
Category("Stack"),
|
||||
Category("Arithmetic"),
|
||||
Category("Comparison"),
|
||||
Category("Logic"),
|
||||
Category("Control"),
|
||||
Category("Variables"),
|
||||
Category("Probability"),
|
||||
Category("Definitions"),
|
||||
// Live coding
|
||||
Section("Live Coding"),
|
||||
Category("Sound"),
|
||||
Category("Time"),
|
||||
Category("Context"),
|
||||
Category("Music"),
|
||||
Category("LFO"),
|
||||
// Synthesis
|
||||
Section("Synthesis"),
|
||||
Category("Oscillator"),
|
||||
Category("Wavetable"),
|
||||
Category("Generator"),
|
||||
Category("Envelope"),
|
||||
Category("Sample"),
|
||||
// Effects
|
||||
Section("Effects"),
|
||||
Category("Filter"),
|
||||
Category("FM"),
|
||||
Category("Modulation"),
|
||||
Category("Mod FX"),
|
||||
Category("Lo-fi"),
|
||||
Category("Stereo"),
|
||||
Category("Delay"),
|
||||
Category("Reverb"),
|
||||
// External I/O
|
||||
Section("I/O"),
|
||||
Category("MIDI"),
|
||||
Category("Desktop"),
|
||||
];
|
||||
|
||||
pub fn render(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let [header_area, body_area] =
|
||||
Layout::vertical([Constraint::Length(5), Constraint::Fill(1)]).areas(area);
|
||||
@@ -165,17 +120,6 @@ fn render_categories(frame: &mut Frame, app: &App, area: Rect, dimmed: bool) {
|
||||
frame.render_widget(list, area);
|
||||
}
|
||||
|
||||
fn get_category_name(index: usize) -> &'static str {
|
||||
CATEGORIES
|
||||
.iter()
|
||||
.filter_map(|e| match e {
|
||||
Category(name) => Some(*name),
|
||||
Section(_) => None,
|
||||
})
|
||||
.nth(index)
|
||||
.unwrap_or("Unknown")
|
||||
}
|
||||
|
||||
fn render_words(frame: &mut Frame, app: &App, area: Rect, is_searching: bool) {
|
||||
let theme = theme::get();
|
||||
let focused = app.ui.dict_focus == DictFocus::Words;
|
||||
@@ -299,9 +243,3 @@ fn render_search_bar(frame: &mut Frame, app: &App, area: Rect) {
|
||||
frame.render_widget(Paragraph::new(vec![line]), area);
|
||||
}
|
||||
|
||||
pub fn category_count() -> usize {
|
||||
CATEGORIES
|
||||
.iter()
|
||||
.filter(|e| matches!(e, Category(_)))
|
||||
.count()
|
||||
}
|
||||
|
||||
@@ -7,10 +7,13 @@ use ratatui::Frame;
|
||||
use tui_big_text::{BigText, PixelSize};
|
||||
|
||||
use crate::app::App;
|
||||
use crate::model::docs::{get_topic, DocEntry, DOCS};
|
||||
use crate::state::HelpFocus;
|
||||
use crate::theme;
|
||||
use crate::views::highlight;
|
||||
|
||||
use DocEntry::{Section, Topic};
|
||||
|
||||
struct AppTheme;
|
||||
|
||||
impl MarkdownTheme for AppTheme {
|
||||
@@ -83,74 +86,6 @@ impl CodeHighlighter for ForthHighlighter {
|
||||
}
|
||||
}
|
||||
|
||||
enum DocEntry {
|
||||
Section(&'static str),
|
||||
Topic(&'static str, &'static str),
|
||||
}
|
||||
|
||||
use DocEntry::{Section, Topic};
|
||||
|
||||
const DOCS: &[DocEntry] = &[
|
||||
// Getting Started
|
||||
Section("Getting Started"),
|
||||
Topic("Welcome", include_str!("../../docs/welcome.md")),
|
||||
Topic("Moving Around", include_str!("../../docs/navigation.md")),
|
||||
Topic(
|
||||
"How Does It Work?",
|
||||
include_str!("../../docs/how_it_works.md"),
|
||||
),
|
||||
Topic(
|
||||
"Banks & Patterns",
|
||||
include_str!("../../docs/banks_patterns.md"),
|
||||
),
|
||||
Topic("Stage / Commit", include_str!("../../docs/staging.md")),
|
||||
Topic("Using the Sequencer", include_str!("../../docs/grid.md")),
|
||||
Topic("Editing a Step", include_str!("../../docs/editing.md")),
|
||||
// Forth fundamentals
|
||||
Section("Forth"),
|
||||
Topic("About Forth", include_str!("../../docs/about_forth.md")),
|
||||
Topic("The Dictionary", include_str!("../../docs/dictionary.md")),
|
||||
Topic("The Stack", include_str!("../../docs/stack.md")),
|
||||
Topic("Creating Words", include_str!("../../docs/definitions.md")),
|
||||
Topic("Oddities", include_str!("../../docs/oddities.md")),
|
||||
// Audio Engine
|
||||
Section("Audio Engine"),
|
||||
Topic("Introduction", include_str!("../../docs/engine_intro.md")),
|
||||
Topic("Settings", include_str!("../../docs/engine_settings.md")),
|
||||
Topic("Sources", include_str!("../../docs/engine_sources.md")),
|
||||
Topic("Samples", include_str!("../../docs/engine_samples.md")),
|
||||
Topic("Wavetables", include_str!("../../docs/engine_wavetable.md")),
|
||||
Topic("Filters", include_str!("../../docs/engine_filters.md")),
|
||||
Topic(
|
||||
"Modulation",
|
||||
include_str!("../../docs/engine_modulation.md"),
|
||||
),
|
||||
Topic(
|
||||
"Distortion",
|
||||
include_str!("../../docs/engine_distortion.md"),
|
||||
),
|
||||
Topic("Space & Time", include_str!("../../docs/engine_space.md")),
|
||||
Topic("Words & Sounds", include_str!("../../docs/engine_words.md")),
|
||||
// MIDI
|
||||
Section("MIDI"),
|
||||
Topic("Introduction", include_str!("../../docs/midi_intro.md")),
|
||||
Topic("MIDI Output", include_str!("../../docs/midi_output.md")),
|
||||
Topic("MIDI Input", include_str!("../../docs/midi_input.md")),
|
||||
];
|
||||
|
||||
pub fn topic_count() -> usize {
|
||||
DOCS.iter().filter(|e| matches!(e, Topic(_, _))).count()
|
||||
}
|
||||
|
||||
fn get_topic(index: usize) -> Option<(&'static str, &'static str)> {
|
||||
DOCS.iter()
|
||||
.filter_map(|e| match e {
|
||||
Topic(name, content) => Some((*name, *content)),
|
||||
Section(_) => None,
|
||||
})
|
||||
.nth(index)
|
||||
}
|
||||
|
||||
pub fn render(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let [topics_area, content_area] =
|
||||
Layout::horizontal([Constraint::Length(24), Constraint::Fill(1)]).areas(area);
|
||||
@@ -386,22 +321,3 @@ fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
|
||||
haystack.windows(needle.len()).position(|w| w == needle)
|
||||
}
|
||||
|
||||
/// Find first line matching query across all topics. Returns (topic_index, line_index).
|
||||
pub fn find_match(query: &str) -> Option<(usize, usize)> {
|
||||
let query = query.to_lowercase();
|
||||
for (topic_idx, (_, content)) in DOCS
|
||||
.iter()
|
||||
.filter_map(|e| match e {
|
||||
Topic(name, content) => Some((*name, *content)),
|
||||
Section(_) => None,
|
||||
})
|
||||
.enumerate()
|
||||
{
|
||||
for (line_idx, line) in content.lines().enumerate() {
|
||||
if line.to_lowercase().contains(&query) {
|
||||
return Some((topic_idx, line_idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user