diff --git a/CHANGELOG.md b/CHANGELOG.md index 3441651..256f93c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added +- Resolved value annotations: nondeterministic words (`rand`, `choose`, `cycle`, `bounce`, `wchoose`, `coin`, `chance`, `prob`, `exprand`, `logrand`) now display their resolved value inline (e.g., `choose [sine]`, `rand [7]`, `chance [yes]`) during playback in both Preview and Editor modals. + +## [0.0.9] - 2026-02-08 + +### Website +- Compressed screenshot images: resized to 1600px and converted PNG to WebP (8MB → 538KB). +- Version number displayed in subtitle, read automatically from `Cargo.toml` at build time. + ### Added - Inline sample finder in the editor: press `Ctrl+B` to open a fuzzy-search popup of all sample folder names. Type to filter, `Ctrl+N`/`Ctrl+P` to navigate, `Tab`/`Enter` to insert the folder name at cursor, `Esc` to dismiss. Mutually exclusive with word completion. - Sample browser now displays the 0-based file index next to each sample name, making it easy to reference samples by index in Forth scripts (e.g., `"drums" bank 0 n`). @@ -12,6 +21,9 @@ All notable changes to this project will be documented in this file. - Header bar stats block (CPU/voices/Link peers) is now centered like all other header sections. - CPU percentage changes color when load is high: accent color at 50%+, error color at 80%+. +### Fixed +- Soundless emits (e.g., `1 gain .`) no longer stack infinite voices. All emitted commands now receive a default duration of one beat unless the user explicitly sets `dur`. Use `0 dur` for intentionally infinite voices. + ## [0.0.8] - 2026-02-07 ### Fixed diff --git a/crates/forth/src/lib.rs b/crates/forth/src/lib.rs index 4ce27f5..1f37c50 100644 --- a/crates/forth/src/lib.rs +++ b/crates/forth/src/lib.rs @@ -6,8 +6,8 @@ mod vm; mod words; pub use types::{ - CcAccess, Dictionary, ExecutionTrace, Rng, SourceSpan, StepContext, Value, Variables, - VariablesMap, + CcAccess, Dictionary, ExecutionTrace, ResolvedValue, Rng, SourceSpan, StepContext, Value, + Variables, VariablesMap, }; pub use vm::Forth; pub use words::{lookup_word, Word, WordCompile, WORDS}; diff --git a/crates/forth/src/ops.rs b/crates/forth/src/ops.rs index 5eab270..121dfc6 100644 --- a/crates/forth/src/ops.rs +++ b/crates/forth/src/ops.rs @@ -65,18 +65,18 @@ pub enum Op { Get, Set, GetContext(&'static str), - Rand, - ExpRand, - LogRand, + Rand(Option), + ExpRand(Option), + LogRand(Option), Seed, - Cycle, - PCycle, - Choose, - Bounce, - WChoose, - ChanceExec, - ProbExec, - Coin, + Cycle(Option), + PCycle(Option), + Choose(Option), + Bounce(Option), + WChoose(Option), + ChanceExec(Option), + ProbExec(Option), + Coin(Option), Mtof, Ftom, SetTempo, diff --git a/crates/forth/src/types.rs b/crates/forth/src/types.rs index e24d189..814bfc5 100644 --- a/crates/forth/src/types.rs +++ b/crates/forth/src/types.rs @@ -18,10 +18,30 @@ pub struct SourceSpan { pub end: u32, } +#[derive(Clone, Debug)] +pub enum ResolvedValue { + Int(i64), + Float(f64), + Bool(bool), + Str(Arc), +} + +impl ResolvedValue { + pub fn display(&self) -> String { + match self { + ResolvedValue::Int(i) => i.to_string(), + ResolvedValue::Float(f) => format!("{f:.2}"), + ResolvedValue::Bool(b) => if *b { "yes" } else { "no" }.into(), + ResolvedValue::Str(s) => s.to_string(), + } + } +} + #[derive(Clone, Debug, Default)] pub struct ExecutionTrace { pub executed_spans: Vec, pub selected_spans: Vec, + pub resolved: Vec<(SourceSpan, ResolvedValue)>, } pub struct StepContext<'a> { diff --git a/crates/forth/src/vm.rs b/crates/forth/src/vm.rs index ab02e81..793fb20 100644 --- a/crates/forth/src/vm.rs +++ b/crates/forth/src/vm.rs @@ -8,8 +8,8 @@ use std::sync::Arc; use super::compiler::compile_script; use super::ops::Op; use super::types::{ - CmdRegister, Dictionary, ExecutionTrace, Rng, Stack, StepContext, Value, Variables, - VariablesMap, + CmdRegister, Dictionary, ExecutionTrace, ResolvedValue, Rng, SourceSpan, Stack, StepContext, + Value, Variables, VariablesMap, }; pub struct Forth { @@ -637,7 +637,7 @@ impl Forth { stack.push(val); } - Op::Rand => { + Op::Rand(word_span) => { let b = stack.pop().ok_or("stack underflow")?; let a = stack.pop().ok_or("stack underflow")?; match (&a, &b) { @@ -648,6 +648,7 @@ impl Forth { (*b_i, *a_i) }; let val = self.rng.lock().gen_range(lo..=hi); + record_resolved(&trace_cell, *word_span, ResolvedValue::Int(val)); stack.push(Value::Int(val, None)); } _ => { @@ -659,11 +660,12 @@ impl Forth { } else { self.rng.lock().gen_range(lo..hi) }; + record_resolved(&trace_cell, *word_span, ResolvedValue::Float(val)); stack.push(Value::Float(val, None)); } } } - Op::ExpRand => { + Op::ExpRand(word_span) => { let hi = stack.pop().ok_or("stack underflow")?.as_float()?; let lo = stack.pop().ok_or("stack underflow")?.as_float()?; if lo <= 0.0 || hi <= 0.0 { @@ -672,9 +674,10 @@ impl Forth { let (lo, hi) = if lo <= hi { (lo, hi) } else { (hi, lo) }; let u: f64 = self.rng.lock().gen(); let val = lo * (hi / lo).powf(u); + record_resolved(&trace_cell, *word_span, ResolvedValue::Float(val)); stack.push(Value::Float(val, None)); } - Op::LogRand => { + Op::LogRand(word_span) => { let hi = stack.pop().ok_or("stack underflow")?.as_float()?; let lo = stack.pop().ok_or("stack underflow")?.as_float()?; if lo <= 0.0 || hi <= 0.0 { @@ -683,6 +686,7 @@ impl Forth { let (lo, hi) = if lo <= hi { (lo, hi) } else { (hi, lo) }; let u: f64 = self.rng.lock().gen(); let val = hi * (lo / hi).powf(u); + record_resolved(&trace_cell, *word_span, ResolvedValue::Float(val)); stack.push(Value::Float(val, None)); } Op::Seed => { @@ -690,28 +694,42 @@ impl Forth { *self.rng.lock() = StdRng::seed_from_u64(s as u64); } - Op::Cycle | Op::PCycle => { + Op::Cycle(word_span) | Op::PCycle(word_span) => { let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize; if count == 0 { return Err("cycle count must be > 0".into()); } let idx = match &ops[pc] { - Op::Cycle => ctx.runs, + Op::Cycle(_) => ctx.runs, _ => ctx.iter, } % count; + if let Some(span) = word_span { + if stack.len() >= count { + let start = stack.len() - count; + let selected = &stack[start + idx]; + record_resolved_from_value(&trace_cell, Some(*span), selected); + } + } drain_select_run(count, idx, stack, outputs, cmd)?; } - Op::Choose => { + Op::Choose(word_span) => { let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize; if count == 0 { return Err("choose count must be > 0".into()); } let idx = self.rng.lock().gen_range(0..count); + if let Some(span) = word_span { + if stack.len() >= count { + let start = stack.len() - count; + let selected = &stack[start + idx]; + record_resolved_from_value(&trace_cell, Some(*span), selected); + } + } drain_select_run(count, idx, stack, outputs, cmd)?; } - Op::Bounce => { + Op::Bounce(word_span) => { let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize; if count == 0 { return Err("bounce count must be > 0".into()); @@ -723,10 +741,17 @@ impl Forth { let raw = ctx.runs % period; if raw < count { raw } else { period - raw } }; + if let Some(span) = word_span { + if stack.len() >= count { + let start = stack.len() - count; + let selected = &stack[start + idx]; + record_resolved_from_value(&trace_cell, Some(*span), selected); + } + } drain_select_run(count, idx, stack, outputs, cmd)?; } - Op::WChoose => { + Op::WChoose(word_span) => { let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize; if count == 0 { return Err("wchoose count must be > 0".into()); @@ -763,25 +788,30 @@ impl Forth { } } let selected = values.swap_remove(selected_idx); + record_resolved_from_value(&trace_cell, *word_span, &selected); select_and_run(selected, stack, outputs, cmd)?; } - Op::ChanceExec | Op::ProbExec => { + Op::ChanceExec(word_span) | Op::ProbExec(word_span) => { let threshold = stack.pop().ok_or("stack underflow")?.as_float()?; let quot = stack.pop().ok_or("stack underflow")?; let val: f64 = self.rng.lock().gen(); let limit = match &ops[pc] { - Op::ChanceExec => threshold, + Op::ChanceExec(_) => threshold, _ => threshold / 100.0, }; - if val < limit { + let fired = val < limit; + record_resolved(&trace_cell, *word_span, ResolvedValue::Bool(fired)); + if fired { run_quotation(quot, stack, outputs, cmd)?; } } - Op::Coin => { + Op::Coin(word_span) => { let val: f64 = self.rng.lock().gen(); - stack.push(Value::Int(if val < 0.5 { 1 } else { 0 }, None)); + let result = val < 0.5; + record_resolved(&trace_cell, *word_span, ResolvedValue::Bool(result)); + stack.push(Value::Int(if result { 1 } else { 0 }, None)); } Op::Every => { @@ -1241,6 +1271,36 @@ impl Forth { } } +fn record_resolved( + trace_cell: &std::cell::RefCell>, + span: Option, + value: ResolvedValue, +) { + if let Some(span) = span { + if let Some(trace) = trace_cell.borrow_mut().as_mut() { + trace.resolved.push((span, value)); + } + } +} + +fn record_resolved_from_value( + trace_cell: &std::cell::RefCell>, + span: Option, + value: &Value, +) { + if let Some(span) = span { + let resolved = match value { + Value::Int(i, _) => ResolvedValue::Int(*i), + Value::Float(f, _) => ResolvedValue::Float(*f), + Value::Str(s, _) => ResolvedValue::Str(s.clone()), + _ => return, + }; + if let Some(trace) = trace_cell.borrow_mut().as_mut() { + trace.resolved.push((span, resolved)); + } + } +} + fn extract_dev_param(params: &[(&str, Value)]) -> u8 { params .iter() @@ -1323,7 +1383,7 @@ fn emit_output( let _ = write!(&mut out, "delta/{nudge_secs}"); } - if sound.is_some() && !has_dur { + if !has_dur { if !out.ends_with('/') { out.push('/'); } diff --git a/crates/forth/src/words/compile.rs b/crates/forth/src/words/compile.rs index 74003a8..479d148 100644 --- a/crates/forth/src/words/compile.rs +++ b/crates/forth/src/words/compile.rs @@ -59,19 +59,19 @@ pub(super) fn simple_op(name: &str) -> Option { "pick" => Op::Pick, "sound" => Op::NewCmd, "." => Op::Emit, - "rand" => Op::Rand, - "exprand" => Op::ExpRand, - "logrand" => Op::LogRand, + "rand" => Op::Rand(None), + "exprand" => Op::ExpRand(None), + "logrand" => Op::LogRand(None), "seed" => Op::Seed, - "cycle" => Op::Cycle, - "pcycle" => Op::PCycle, - "choose" => Op::Choose, - "bounce" => Op::Bounce, - "wchoose" => Op::WChoose, + "cycle" => Op::Cycle(None), + "pcycle" => Op::PCycle(None), + "choose" => Op::Choose(None), + "bounce" => Op::Bounce(None), + "wchoose" => Op::WChoose(None), "every" => Op::Every, - "chance" => Op::ChanceExec, - "prob" => Op::ProbExec, - "coin" => Op::Coin, + "chance" => Op::ChanceExec(None), + "prob" => Op::ProbExec(None), + "coin" => Op::Coin(None), "mtof" => Op::Mtof, "ftom" => Op::Ftom, "?" => Op::When, @@ -187,6 +187,15 @@ fn parse_interval(name: &str) -> Option { Some(simple) } +fn attach_span(op: &mut Op, span: SourceSpan) { + match op { + Op::Rand(s) | Op::ExpRand(s) | Op::LogRand(s) | Op::Coin(s) + | Op::Choose(s) | Op::WChoose(s) | Op::Cycle(s) | Op::PCycle(s) + | Op::Bounce(s) | Op::ChanceExec(s) | Op::ProbExec(s) => *s = Some(span), + _ => {} + } +} + pub(crate) fn compile_word( name: &str, span: Option, @@ -225,7 +234,10 @@ pub(crate) fn compile_word( if let Some(word) = lookup_word(name) { match &word.compile { Simple => { - if let Some(op) = simple_op(word.name) { + if let Some(mut op) = simple_op(word.name) { + if let Some(sp) = span { + attach_span(&mut op, sp); + } ops.push(op); } } @@ -233,7 +245,7 @@ pub(crate) fn compile_word( Param => ops.push(Op::SetParam(word.name)), Probability(p) => { ops.push(Op::PushFloat(*p, None)); - ops.push(Op::ChanceExec); + ops.push(Op::ChanceExec(span)); } } return true; diff --git a/crates/ratatui/src/editor.rs b/crates/ratatui/src/editor.rs index 9e4bbc6..71f9079 100644 --- a/crates/ratatui/src/editor.rs +++ b/crates/ratatui/src/editor.rs @@ -10,7 +10,7 @@ use ratatui::{ }; use tui_textarea::TextArea; -pub type Highlighter<'a> = &'a dyn Fn(usize, &str) -> Vec<(Style, String)>; +pub type Highlighter<'a> = &'a dyn Fn(usize, &str) -> Vec<(Style, String, bool)>; #[derive(Clone)] pub struct CompletionCandidate { @@ -452,21 +452,25 @@ impl Editor { let mut spans: Vec = Vec::new(); let mut col = 0; - for (base_style, text) in tokens { + for (base_style, text, is_annotation) in tokens { for ch in text.chars() { - let is_cursor = row == cursor_row && col == cursor_col; - let is_selected = is_in_selection(row, col, selection); - - let style = if is_cursor { - cursor_style - } else if is_selected { - base_style.bg(selection_style.bg.unwrap()) - } else { + let style = if is_annotation { base_style + } else { + let is_cursor = row == cursor_row && col == cursor_col; + let is_selected = is_in_selection(row, col, selection); + if is_cursor { + cursor_style + } else if is_selected { + base_style.bg(selection_style.bg.unwrap()) + } else { + base_style + } }; - spans.push(Span::styled(ch.to_string(), style)); - col += 1; + if !is_annotation { + col += 1; + } } } diff --git a/src/views/help_view.rs b/src/views/help_view.rs index c9a3901..d77624d 100644 --- a/src/views/help_view.rs +++ b/src/views/help_view.rs @@ -84,6 +84,9 @@ struct ForthHighlighter; impl CodeHighlighter for ForthHighlighter { fn highlight(&self, line: &str) -> Vec<(Style, String)> { highlight::highlight_line(line) + .into_iter() + .map(|(s, t, _)| (s, t)) + .collect() } } diff --git a/src/views/highlight.rs b/src/views/highlight.rs index cb71eab..b89c044 100644 --- a/src/views/highlight.rs +++ b/src/views/highlight.rs @@ -202,24 +202,27 @@ fn classify_word(word: &str, user_words: &HashSet) -> (TokenKind, bool) (TokenKind::Default, false) } -pub fn highlight_line(line: &str) -> Vec<(Style, String)> { - highlight_line_with_runtime(line, &[], &[], &EMPTY_SET) +pub fn highlight_line(line: &str) -> Vec<(Style, String, bool)> { + highlight_line_with_runtime(line, &[], &[], &[], &EMPTY_SET) } pub fn highlight_line_with_runtime( line: &str, executed_spans: &[SourceSpan], selected_spans: &[SourceSpan], + resolved: &[(SourceSpan, String)], user_words: &HashSet, -) -> Vec<(Style, String)> { +) -> Vec<(Style, String, bool)> { let tokens = tokenize_line(line, user_words); let mut result = Vec::new(); let mut last_end = 0; let gap_style = TokenKind::gap_style(); + let theme = theme::get(); + let annotation_style = Style::default().fg(theme.ui.text_dim); - for token in tokens { + for token in &tokens { if token.start > last_end { - result.push((gap_style, line[last_end..token.start].to_string())); + result.push((gap_style, line[last_end..token.start].to_string(), false)); } let is_selected = selected_spans @@ -233,19 +236,25 @@ pub fn highlight_line_with_runtime( if token.varargs { style = style.add_modifier(Modifier::UNDERLINED); } - let theme = theme::get(); if is_selected { style = style.bg(theme.syntax.selected_bg).add_modifier(Modifier::BOLD); } else if is_executed { style = style.bg(theme.syntax.executed_bg); } - result.push((style, line[token.start..token.end].to_string())); + result.push((style, line[token.start..token.end].to_string(), false)); + + for (span, display) in resolved { + if token.start == span.start as usize { + result.push((annotation_style, format!(" [{display}]"), true)); + } + } + last_end = token.end; } if last_end < line.len() { - result.push((gap_style, line[last_end..].to_string())); + result.push((gap_style, line[last_end..].to_string(), false)); } result diff --git a/src/views/render.rs b/src/views/render.rs index 7bc941b..4e36eb5 100644 --- a/src/views/render.rs +++ b/src/views/render.rs @@ -45,6 +45,30 @@ fn adjust_spans_for_line( .collect() } +fn adjust_resolved_for_line( + resolved: &[(SourceSpan, String)], + line_start: usize, + line_len: usize, +) -> Vec<(SourceSpan, String)> { + let ls = line_start as u32; + let ll = line_len as u32; + resolved + .iter() + .filter_map(|(s, display)| { + if s.end <= ls || s.start >= ls + ll { + return None; + } + Some(( + SourceSpan { + start: s.start.max(ls) - ls, + end: (s.end.min(ls + ll)) - ls, + }, + display.clone(), + )) + }) + .collect() +} + pub fn render(frame: &mut Frame, app: &App, link: &LinkState, snapshot: &SequencerSnapshot, elapsed: Duration) { let term = frame.area(); @@ -627,6 +651,15 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term None }; + let resolved_display: Vec<(SourceSpan, String)> = trace + .map(|t| { + t.resolved + .iter() + .map(|(s, v)| (*s, v.display())) + .collect() + }) + .unwrap_or_default(); + let mut line_start = 0usize; let lines: Vec = script .lines() @@ -642,14 +675,19 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term line_start, line_str.len(), ); - highlight_line_with_runtime(line_str, &exec, &sel, &user_words) + let res = adjust_resolved_for_line( + &resolved_display, + line_start, + line_str.len(), + ); + highlight_line_with_runtime(line_str, &exec, &sel, &res, &user_words) } else { - highlight_line_with_runtime(line_str, &[], &[], &user_words) + highlight_line_with_runtime(line_str, &[], &[], &[], &user_words) }; line_start += line_str.len() + 1; let spans: Vec = tokens .into_iter() - .map(|(style, text)| Span::styled(text, style)) + .map(|(style, text, _)| Span::styled(text, style)) .collect(); Line::from(spans) }) @@ -712,16 +750,26 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term offset += line.len() + 1; } - let highlighter = |row: usize, line: &str| -> Vec<(Style, String)> { + 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) = match trace { + 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()), + None => (Vec::new(), Vec::new(), Vec::new()), }; - highlight::highlight_line_with_runtime(line, &exec, &sel, &user_words) + highlight::highlight_line_with_runtime(line, &exec, &sel, &res, &user_words) }; let show_search = app.editor_ctx.editor.search_active() diff --git a/tests/forth/harness.rs b/tests/forth/harness.rs index c0cacb7..209f834 100644 --- a/tests/forth/harness.rs +++ b/tests/forth/harness.rs @@ -1,5 +1,5 @@ use arc_swap::ArcSwap; -use cagire::forth::{Dictionary, Forth, Rng, StepContext, Value, Variables}; +use cagire::forth::{Dictionary, ExecutionTrace, Forth, Rng, StepContext, Value, Variables}; use parking_lot::Mutex; use rand::rngs::StdRng; use rand::SeedableRng; @@ -140,3 +140,11 @@ pub fn expect_outputs(script: &str, count: usize) -> Vec { assert_eq!(outputs.len(), count, "expected {} outputs", count); outputs } + +pub fn run_with_trace(script: &str) -> (Forth, ExecutionTrace) { + let f = forth(); + let mut trace = ExecutionTrace::default(); + f.evaluate_with_trace(script, &default_ctx(), &mut trace) + .unwrap(); + (f, trace) +} diff --git a/tests/forth/randomness.rs b/tests/forth/randomness.rs index 239b5cd..46fa6e8 100644 --- a/tests/forth/randomness.rs +++ b/tests/forth/randomness.rs @@ -1,4 +1,5 @@ use super::harness::*; +use cagire::forth::ResolvedValue; #[test] fn rand_in_range() { @@ -253,3 +254,14 @@ fn wchoose_quotation() { .unwrap(); assert_eq!(stack_int(&f), 20); } + +#[test] +fn choose_trace_resolved_span() { + let script = "sine tri 2 choose"; + let (_f, trace) = run_with_trace(script); + assert_eq!(trace.resolved.len(), 1, "expected 1 resolved entry: {:?}", trace.resolved); + let (span, ref val) = trace.resolved[0]; + assert_eq!(span.start, 11); + assert_eq!(span.end, 17); + assert!(matches!(val, ResolvedValue::Str(s) if s.as_ref() == "sine" || s.as_ref() == "tri")); +} diff --git a/tests/forth/sound.rs b/tests/forth/sound.rs index f1ae198..4c7fc6a 100644 --- a/tests/forth/sound.rs +++ b/tests/forth/sound.rs @@ -94,7 +94,7 @@ fn param_only_emit() { assert!(outputs[0].contains("voice/0")); assert!(outputs[0].contains("freq/880")); assert!(!outputs[0].contains("sound/")); - assert!(!outputs[0].contains("dur/")); + assert!(outputs[0].contains("dur/")); assert!(!outputs[0].contains("delaytime/")); } @@ -138,3 +138,9 @@ fn polyphonic_with_at() { let outputs = expect_outputs(r#"0 0.5 at 60 64 note sine s ."#, 4); assert_eq!(outputs.len(), 4); } + +#[test] +fn explicit_dur_zero_is_infinite() { + let outputs = expect_outputs("880 freq 0 dur .", 1); + assert!(outputs[0].contains("dur/0")); +} diff --git a/website/public/eight_pic.png b/website/public/eight_pic.png deleted file mode 100644 index c952577..0000000 Binary files a/website/public/eight_pic.png and /dev/null differ diff --git a/website/public/eight_pic.webp b/website/public/eight_pic.webp new file mode 100644 index 0000000..b322b53 Binary files /dev/null and b/website/public/eight_pic.webp differ diff --git a/website/public/fifth_pic.png b/website/public/fifth_pic.png deleted file mode 100644 index 9c4680d..0000000 Binary files a/website/public/fifth_pic.png and /dev/null differ diff --git a/website/public/fifth_pic.webp b/website/public/fifth_pic.webp new file mode 100644 index 0000000..4bc9540 Binary files /dev/null and b/website/public/fifth_pic.webp differ diff --git a/website/public/fourth_pic.png b/website/public/fourth_pic.png deleted file mode 100644 index 373eb2a..0000000 Binary files a/website/public/fourth_pic.png and /dev/null differ diff --git a/website/public/fourth_pic.webp b/website/public/fourth_pic.webp new file mode 100644 index 0000000..a90f933 Binary files /dev/null and b/website/public/fourth_pic.webp differ diff --git a/website/public/ninth_pic.png b/website/public/ninth_pic.png deleted file mode 100644 index 93b8ca8..0000000 Binary files a/website/public/ninth_pic.png and /dev/null differ diff --git a/website/public/ninth_pic.webp b/website/public/ninth_pic.webp new file mode 100644 index 0000000..1e68995 Binary files /dev/null and b/website/public/ninth_pic.webp differ diff --git a/website/public/one_pic.png b/website/public/one_pic.png deleted file mode 100644 index caae09f..0000000 Binary files a/website/public/one_pic.png and /dev/null differ diff --git a/website/public/one_pic.webp b/website/public/one_pic.webp new file mode 100644 index 0000000..b54f88f Binary files /dev/null and b/website/public/one_pic.webp differ diff --git a/website/public/script.js b/website/public/script.js index 9c7b49b..68ef85c 100644 --- a/website/public/script.js +++ b/website/public/script.js @@ -1,21 +1,3 @@ -const toggle = document.getElementById('theme-toggle'); -const root = document.documentElement; -const stored = localStorage.getItem('theme'); -const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; -const isLight = stored ? stored === 'light' : !prefersDark; - -if (isLight) { - root.classList.add('light'); -} -toggle.textContent = isLight ? 'DARK' : 'LIGHT'; - -toggle.addEventListener('click', () => { - root.classList.toggle('light'); - const light = root.classList.contains('light'); - toggle.textContent = light ? 'DARK' : 'LIGHT'; - localStorage.setItem('theme', light ? 'light' : 'dark'); -}); - document.querySelectorAll('.example-cell').forEach(cell => { cell.addEventListener('click', () => { const video = cell.querySelector('video'); @@ -45,6 +27,10 @@ document.querySelectorAll('.feature-tags button').forEach(btn => { }); }); +document.getElementById('kofi-close').addEventListener('click', () => { + document.getElementById('kofi-popup').style.display = 'none'; +}); + document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { document.querySelectorAll('.example-cell.expanded').forEach(c => { diff --git a/website/public/second_pic.png b/website/public/second_pic.png deleted file mode 100644 index 6fc79df..0000000 Binary files a/website/public/second_pic.png and /dev/null differ diff --git a/website/public/second_pic.webp b/website/public/second_pic.webp new file mode 100644 index 0000000..40e6bd6 Binary files /dev/null and b/website/public/second_pic.webp differ diff --git a/website/public/seventh_pic.png b/website/public/seventh_pic.png deleted file mode 100644 index f36c8fe..0000000 Binary files a/website/public/seventh_pic.png and /dev/null differ diff --git a/website/public/seventh_pic.webp b/website/public/seventh_pic.webp new file mode 100644 index 0000000..1c7bab3 Binary files /dev/null and b/website/public/seventh_pic.webp differ diff --git a/website/public/sixth_pic.png b/website/public/sixth_pic.png deleted file mode 100644 index 4c9039d..0000000 Binary files a/website/public/sixth_pic.png and /dev/null differ diff --git a/website/public/sixth_pic.webp b/website/public/sixth_pic.webp new file mode 100644 index 0000000..871c57d Binary files /dev/null and b/website/public/sixth_pic.webp differ diff --git a/website/public/style.css b/website/public/style.css index fd8e447..d368de6 100644 --- a/website/public/style.css +++ b/website/public/style.css @@ -6,14 +6,6 @@ } :root { - --bg: #000; - --surface: #121212; - --text: #fff; - --text-dim: #b4b4b4; - --text-muted: #787878; -} - -:root.light { --bg: #fff; --surface: #f0f0f0; --text: #000; @@ -216,6 +208,51 @@ li { display: none; } +.kofi-popup { + position: fixed; + top: 1rem; + left: 1rem; + z-index: 50; + max-width: 14rem; + font-size: 0.75rem; + color: var(--text-dim); + background: var(--surface); + border: 1px solid var(--text-muted); + padding: 0.75rem 1rem; +} + +.kofi-popup p { + margin: 0 0 0.5rem; + text-align: left; +} + +.kofi-popup a { + color: var(--text); + text-decoration: underline; +} + +.kofi-popup a:hover { + color: var(--text-dim); +} + +.kofi-close { + position: absolute; + top: 0.25rem; + right: 0.5rem; + font-family: 'VCR OSD Mono', monospace; + font-size: 1rem; + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + padding: 0; + line-height: 1; +} + +.kofi-close:hover { + color: var(--text); +} + .colophon { margin-top: 3rem; padding-top: 1rem; @@ -227,17 +264,3 @@ li { color: var(--text-dim); } -#theme-toggle { - font-family: 'VCR OSD Mono', monospace; - background: none; - color: var(--text-muted); - border: none; - padding: 0; - cursor: pointer; - font-size: inherit; - text-decoration: underline; -} - -#theme-toggle:hover { - color: var(--text); -} diff --git a/website/public/third_pic.png b/website/public/third_pic.png deleted file mode 100644 index a31947e..0000000 Binary files a/website/public/third_pic.png and /dev/null differ diff --git a/website/public/third_pic.webp b/website/public/third_pic.webp new file mode 100644 index 0000000..4989644 Binary files /dev/null and b/website/public/third_pic.webp differ diff --git a/website/src/pages/index.astro b/website/src/pages/index.astro index 1625455..b6678f4 100644 --- a/website/src/pages/index.astro +++ b/website/src/pages/index.astro @@ -1,4 +1,7 @@ --- +import fs from 'node:fs'; +const cargo = fs.readFileSync('../Cargo.toml', 'utf-8'); +const version = cargo.match(/\[workspace\.package\]\s*\nversion\s*=\s*"([^"]+)"/)?.[1]; --- @@ -23,24 +26,29 @@ +
+ +

Consider donating, I need to buy some coffee! Donations help me to rent servers and to secure funding for working on software.

+ Support on Ko-fi +
Cagire

CAGIRE: LIVE CODING IN FORTH

-

AGPL-3.0 · Raphaël Maurice Forment · 2026

+

AGPL-3.0 · Raphaël Maurice Forment · 2026 · v{version}

-
Cagire screenshot 1
-
Cagire screenshot 2
-
Cagire screenshot 3
-
Cagire screenshot 4
-
Cagire screenshot 5
-
Cagire screenshot 6
-
Cagire screenshot 7
-
Cagire screenshot 8
-
Cagire screenshot 9
+
Cagire screenshot 1
+
Cagire screenshot 2
+
Cagire screenshot 3
+
Cagire screenshot 4
+
Cagire screenshot 5
+
Cagire screenshot 6
+
Cagire screenshot 7
+
Cagire screenshot 8
+
Cagire screenshot 9

Download

@@ -109,8 +117,7 @@

- BuboBubo · Audio engine: Doux · GitHub · AGPL-3.0 · -

+ BuboBubo · Audio engine: Doux · GitHub · AGPL-3.0