From 4049c7787c3d6565576c2142c1d09879dd6ebd30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Fri, 30 Jan 2026 21:19:48 +0100 Subject: [PATCH] Feat: extend CI to cover desktop --- crates/forth/src/types.rs | 9 +++++++-- crates/forth/src/vm.rs | 37 ++++++++++++++++++++++++------------- docs/welcome.md | 5 ++++- src/views/help_view.rs | 6 ++++++ tests/forth/sound.rs | 21 ++++++++++++++++++++- 5 files changed, 61 insertions(+), 17 deletions(-) diff --git a/crates/forth/src/types.rs b/crates/forth/src/types.rs index 1fb9db5..d5bbd52 100644 --- a/crates/forth/src/types.rs +++ b/crates/forth/src/types.rs @@ -41,6 +41,7 @@ pub type Variables = Arc>>; pub type Dictionary = Arc>>>; pub type Rng = Arc>; pub type Stack = Arc>>; +pub(super) type CmdSnapshot<'a> = (Option<&'a Value>, &'a [(String, Value)]); #[derive(Clone, Debug)] pub enum Value { @@ -140,8 +141,12 @@ impl CmdRegister { &self.deltas } - pub(super) fn snapshot(&self) -> Option<(&Value, &[(String, Value)])> { - self.sound.as_ref().map(|s| (s, self.params.as_slice())) + pub(super) fn snapshot(&self) -> Option> { + if self.sound.is_some() || !self.params.is_empty() { + Some((self.sound.as_ref(), self.params.as_slice())) + } else { + None + } } pub(super) fn clear(&mut self) { diff --git a/crates/forth/src/vm.rs b/crates/forth/src/vm.rs index 9aa157b..0a4d0e0 100644 --- a/crates/forth/src/vm.rs +++ b/crates/forth/src/vm.rs @@ -147,9 +147,12 @@ impl Forth { }; let emit_with_cycling = |cmd: &CmdRegister, emit_idx: usize, delta_secs: f64, outputs: &mut Vec| -> Result, String> { - let (sound_val, params) = cmd.snapshot().ok_or("no sound set")?; - let resolved_sound_val = resolve_cycling(sound_val, emit_idx); - let sound = resolved_sound_val.as_str()?.to_string(); + let (sound_opt, params) = cmd.snapshot().ok_or("nothing to emit")?; + let resolved_sound_val = sound_opt.map(|sv| resolve_cycling(sv, emit_idx)); + let sound_str = match &resolved_sound_val { + Some(v) => Some(v.as_str()?.to_string()), + None => None, + }; let resolved_params: Vec<(String, String)> = params.iter().map(|(k, v)| { let resolved = resolve_cycling(v, emit_idx); @@ -162,8 +165,8 @@ impl Forth { } (k.clone(), resolved.to_param_string()) }).collect(); - emit_output(&sound, &resolved_params, ctx.step_duration(), delta_secs, outputs); - Ok(Some(resolved_sound_val.into_owned())) + emit_output(sound_str.as_deref(), &resolved_params, ctx.step_duration(), delta_secs, outputs); + Ok(resolved_sound_val.map(|v| v.into_owned())) }; while pc < ops.len() { @@ -796,25 +799,33 @@ fn is_tempo_scaled_param(name: &str) -> bool { } fn emit_output( - sound: &str, + sound: Option<&str>, params: &[(String, String)], step_duration: f64, nudge_secs: f64, outputs: &mut Vec, ) { - let mut pairs = vec![("sound".into(), sound.to_string())]; + let mut pairs: Vec<(String, String)> = if let Some(s) = sound { + vec![("sound".into(), s.to_string())] + } else { + vec![] + }; pairs.extend(params.iter().cloned()); if nudge_secs > 0.0 { pairs.push(("delta".into(), nudge_secs.to_string())); } - if !pairs.iter().any(|(k, _)| k == "dur") { + // Only add default dur if there's a sound (new voice) + if sound.is_some() && !pairs.iter().any(|(k, _)| k == "dur") { pairs.push(("dur".into(), step_duration.to_string())); } - if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") { - let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0); - pairs[idx].1 = (ratio * step_duration).to_string(); - } else { - pairs.push(("delaytime".into(), step_duration.to_string())); + // Only add default delaytime if there's a sound (new voice) + if sound.is_some() { + if let Some(idx) = pairs.iter().position(|(k, _)| k == "delaytime") { + let ratio: f64 = pairs[idx].1.parse().unwrap_or(1.0); + pairs[idx].1 = (ratio * step_duration).to_string(); + } else { + pairs.push(("delaytime".into(), step_duration.to_string())); + } } for pair in &mut pairs { if is_tempo_scaled_param(&pair.0) { diff --git a/docs/welcome.md b/docs/welcome.md index b1debfe..9de1696 100644 --- a/docs/welcome.md +++ b/docs/welcome.md @@ -1,6 +1,9 @@ # Welcome to Cagire -Cagire is a terminal-based step sequencer for live coding music. Each step in a pattern contains a **Forth** script that produces sound and create events. It is made by BuboBubo (Raphaël Maurice Forment): [https://raphaelforment.fr](https://raphaelforment.fr). Cagire is open-source (AGPL-3.0 licensed) and available on GitHub : [https://github.com/BuboBubo/cagire](https://github.com/BuboBubo/cagire). This help view will teach you everything you need to know to start using Cagire and and to live code with it. +Cagire is a terminal-based step sequencer for live coding music. Each step in a pattern contains a **Forth** script that produces sound and create events. It is made by BuboBubo (Raphaël Maurice Forment): [https://raphaelforment.fr](https://raphaelforment.fr). Cagire is open-source (AGPL-3.0 licensed) and available on GitHub : [https://github.com/BuboBubo/cagire](https://github.com/BuboBubo/cagire). This help view will teach you everything you need to know to start using Cagire and and to live code with it. To use Cagire, you will need to understand two things: + +1) How the sequencer works: dealing with steps, patterns and banks. +2) How to write a script: how to make sound using code. ## Pages diff --git a/src/views/help_view.rs b/src/views/help_view.rs index 1a636fa..b986e95 100644 --- a/src/views/help_view.rs +++ b/src/views/help_view.rs @@ -245,6 +245,7 @@ fn preprocess_underscores(md: &str) -> String { fn parse_markdown(md: &str) -> Vec> { let processed = preprocess_underscores(md); + eprintln!("DEBUG parse_markdown: processed={:?}", &processed[..100.min(processed.len())]); let text = minimad::Text::from(processed.as_str()); let mut lines = Vec::new(); @@ -315,6 +316,11 @@ fn compound_to_spans(compound: Compound, base: Style, out: &mut Vec