Feat: lots of improvements
12
CHANGELOG.md
@@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
### 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.
|
- 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`).
|
- 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.
|
- 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%+.
|
- 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
|
## [0.0.8] - 2026-02-07
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ mod vm;
|
|||||||
mod words;
|
mod words;
|
||||||
|
|
||||||
pub use types::{
|
pub use types::{
|
||||||
CcAccess, Dictionary, ExecutionTrace, Rng, SourceSpan, StepContext, Value, Variables,
|
CcAccess, Dictionary, ExecutionTrace, ResolvedValue, Rng, SourceSpan, StepContext, Value,
|
||||||
VariablesMap,
|
Variables, VariablesMap,
|
||||||
};
|
};
|
||||||
pub use vm::Forth;
|
pub use vm::Forth;
|
||||||
pub use words::{lookup_word, Word, WordCompile, WORDS};
|
pub use words::{lookup_word, Word, WordCompile, WORDS};
|
||||||
|
|||||||
@@ -65,18 +65,18 @@ pub enum Op {
|
|||||||
Get,
|
Get,
|
||||||
Set,
|
Set,
|
||||||
GetContext(&'static str),
|
GetContext(&'static str),
|
||||||
Rand,
|
Rand(Option<SourceSpan>),
|
||||||
ExpRand,
|
ExpRand(Option<SourceSpan>),
|
||||||
LogRand,
|
LogRand(Option<SourceSpan>),
|
||||||
Seed,
|
Seed,
|
||||||
Cycle,
|
Cycle(Option<SourceSpan>),
|
||||||
PCycle,
|
PCycle(Option<SourceSpan>),
|
||||||
Choose,
|
Choose(Option<SourceSpan>),
|
||||||
Bounce,
|
Bounce(Option<SourceSpan>),
|
||||||
WChoose,
|
WChoose(Option<SourceSpan>),
|
||||||
ChanceExec,
|
ChanceExec(Option<SourceSpan>),
|
||||||
ProbExec,
|
ProbExec(Option<SourceSpan>),
|
||||||
Coin,
|
Coin(Option<SourceSpan>),
|
||||||
Mtof,
|
Mtof,
|
||||||
Ftom,
|
Ftom,
|
||||||
SetTempo,
|
SetTempo,
|
||||||
|
|||||||
@@ -18,10 +18,30 @@ pub struct SourceSpan {
|
|||||||
pub end: u32,
|
pub end: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum ResolvedValue {
|
||||||
|
Int(i64),
|
||||||
|
Float(f64),
|
||||||
|
Bool(bool),
|
||||||
|
Str(Arc<str>),
|
||||||
|
}
|
||||||
|
|
||||||
|
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)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct ExecutionTrace {
|
pub struct ExecutionTrace {
|
||||||
pub executed_spans: Vec<SourceSpan>,
|
pub executed_spans: Vec<SourceSpan>,
|
||||||
pub selected_spans: Vec<SourceSpan>,
|
pub selected_spans: Vec<SourceSpan>,
|
||||||
|
pub resolved: Vec<(SourceSpan, ResolvedValue)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct StepContext<'a> {
|
pub struct StepContext<'a> {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ use std::sync::Arc;
|
|||||||
use super::compiler::compile_script;
|
use super::compiler::compile_script;
|
||||||
use super::ops::Op;
|
use super::ops::Op;
|
||||||
use super::types::{
|
use super::types::{
|
||||||
CmdRegister, Dictionary, ExecutionTrace, Rng, Stack, StepContext, Value, Variables,
|
CmdRegister, Dictionary, ExecutionTrace, ResolvedValue, Rng, SourceSpan, Stack, StepContext,
|
||||||
VariablesMap,
|
Value, Variables, VariablesMap,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Forth {
|
pub struct Forth {
|
||||||
@@ -637,7 +637,7 @@ impl Forth {
|
|||||||
stack.push(val);
|
stack.push(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
Op::Rand => {
|
Op::Rand(word_span) => {
|
||||||
let b = stack.pop().ok_or("stack underflow")?;
|
let b = stack.pop().ok_or("stack underflow")?;
|
||||||
let a = stack.pop().ok_or("stack underflow")?;
|
let a = stack.pop().ok_or("stack underflow")?;
|
||||||
match (&a, &b) {
|
match (&a, &b) {
|
||||||
@@ -648,6 +648,7 @@ impl Forth {
|
|||||||
(*b_i, *a_i)
|
(*b_i, *a_i)
|
||||||
};
|
};
|
||||||
let val = self.rng.lock().gen_range(lo..=hi);
|
let val = self.rng.lock().gen_range(lo..=hi);
|
||||||
|
record_resolved(&trace_cell, *word_span, ResolvedValue::Int(val));
|
||||||
stack.push(Value::Int(val, None));
|
stack.push(Value::Int(val, None));
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
@@ -659,11 +660,12 @@ impl Forth {
|
|||||||
} else {
|
} else {
|
||||||
self.rng.lock().gen_range(lo..hi)
|
self.rng.lock().gen_range(lo..hi)
|
||||||
};
|
};
|
||||||
|
record_resolved(&trace_cell, *word_span, ResolvedValue::Float(val));
|
||||||
stack.push(Value::Float(val, None));
|
stack.push(Value::Float(val, None));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Op::ExpRand => {
|
Op::ExpRand(word_span) => {
|
||||||
let hi = stack.pop().ok_or("stack underflow")?.as_float()?;
|
let hi = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
let lo = 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 {
|
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 (lo, hi) = if lo <= hi { (lo, hi) } else { (hi, lo) };
|
||||||
let u: f64 = self.rng.lock().gen();
|
let u: f64 = self.rng.lock().gen();
|
||||||
let val = lo * (hi / lo).powf(u);
|
let val = lo * (hi / lo).powf(u);
|
||||||
|
record_resolved(&trace_cell, *word_span, ResolvedValue::Float(val));
|
||||||
stack.push(Value::Float(val, None));
|
stack.push(Value::Float(val, None));
|
||||||
}
|
}
|
||||||
Op::LogRand => {
|
Op::LogRand(word_span) => {
|
||||||
let hi = stack.pop().ok_or("stack underflow")?.as_float()?;
|
let hi = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
let lo = 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 {
|
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 (lo, hi) = if lo <= hi { (lo, hi) } else { (hi, lo) };
|
||||||
let u: f64 = self.rng.lock().gen();
|
let u: f64 = self.rng.lock().gen();
|
||||||
let val = hi * (lo / hi).powf(u);
|
let val = hi * (lo / hi).powf(u);
|
||||||
|
record_resolved(&trace_cell, *word_span, ResolvedValue::Float(val));
|
||||||
stack.push(Value::Float(val, None));
|
stack.push(Value::Float(val, None));
|
||||||
}
|
}
|
||||||
Op::Seed => {
|
Op::Seed => {
|
||||||
@@ -690,28 +694,42 @@ impl Forth {
|
|||||||
*self.rng.lock() = StdRng::seed_from_u64(s as u64);
|
*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;
|
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
return Err("cycle count must be > 0".into());
|
return Err("cycle count must be > 0".into());
|
||||||
}
|
}
|
||||||
let idx = match &ops[pc] {
|
let idx = match &ops[pc] {
|
||||||
Op::Cycle => ctx.runs,
|
Op::Cycle(_) => ctx.runs,
|
||||||
_ => ctx.iter,
|
_ => ctx.iter,
|
||||||
} % count;
|
} % 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)?;
|
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;
|
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
return Err("choose count must be > 0".into());
|
return Err("choose count must be > 0".into());
|
||||||
}
|
}
|
||||||
let idx = self.rng.lock().gen_range(0..count);
|
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)?;
|
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;
|
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
return Err("bounce count must be > 0".into());
|
return Err("bounce count must be > 0".into());
|
||||||
@@ -723,10 +741,17 @@ impl Forth {
|
|||||||
let raw = ctx.runs % period;
|
let raw = ctx.runs % period;
|
||||||
if raw < count { raw } else { period - raw }
|
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)?;
|
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;
|
let count = stack.pop().ok_or("stack underflow")?.as_int()? as usize;
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
return Err("wchoose count must be > 0".into());
|
return Err("wchoose count must be > 0".into());
|
||||||
@@ -763,25 +788,30 @@ impl Forth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let selected = values.swap_remove(selected_idx);
|
let selected = values.swap_remove(selected_idx);
|
||||||
|
record_resolved_from_value(&trace_cell, *word_span, &selected);
|
||||||
select_and_run(selected, stack, outputs, cmd)?;
|
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 threshold = stack.pop().ok_or("stack underflow")?.as_float()?;
|
||||||
let quot = stack.pop().ok_or("stack underflow")?;
|
let quot = stack.pop().ok_or("stack underflow")?;
|
||||||
let val: f64 = self.rng.lock().gen();
|
let val: f64 = self.rng.lock().gen();
|
||||||
let limit = match &ops[pc] {
|
let limit = match &ops[pc] {
|
||||||
Op::ChanceExec => threshold,
|
Op::ChanceExec(_) => threshold,
|
||||||
_ => threshold / 100.0,
|
_ => 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)?;
|
run_quotation(quot, stack, outputs, cmd)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Op::Coin => {
|
Op::Coin(word_span) => {
|
||||||
let val: f64 = self.rng.lock().gen();
|
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 => {
|
Op::Every => {
|
||||||
@@ -1241,6 +1271,36 @@ impl Forth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn record_resolved(
|
||||||
|
trace_cell: &std::cell::RefCell<Option<&mut ExecutionTrace>>,
|
||||||
|
span: Option<SourceSpan>,
|
||||||
|
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<Option<&mut ExecutionTrace>>,
|
||||||
|
span: Option<SourceSpan>,
|
||||||
|
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 {
|
fn extract_dev_param(params: &[(&str, Value)]) -> u8 {
|
||||||
params
|
params
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1323,7 +1383,7 @@ fn emit_output(
|
|||||||
let _ = write!(&mut out, "delta/{nudge_secs}");
|
let _ = write!(&mut out, "delta/{nudge_secs}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if sound.is_some() && !has_dur {
|
if !has_dur {
|
||||||
if !out.ends_with('/') {
|
if !out.ends_with('/') {
|
||||||
out.push('/');
|
out.push('/');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,19 +59,19 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
|||||||
"pick" => Op::Pick,
|
"pick" => Op::Pick,
|
||||||
"sound" => Op::NewCmd,
|
"sound" => Op::NewCmd,
|
||||||
"." => Op::Emit,
|
"." => Op::Emit,
|
||||||
"rand" => Op::Rand,
|
"rand" => Op::Rand(None),
|
||||||
"exprand" => Op::ExpRand,
|
"exprand" => Op::ExpRand(None),
|
||||||
"logrand" => Op::LogRand,
|
"logrand" => Op::LogRand(None),
|
||||||
"seed" => Op::Seed,
|
"seed" => Op::Seed,
|
||||||
"cycle" => Op::Cycle,
|
"cycle" => Op::Cycle(None),
|
||||||
"pcycle" => Op::PCycle,
|
"pcycle" => Op::PCycle(None),
|
||||||
"choose" => Op::Choose,
|
"choose" => Op::Choose(None),
|
||||||
"bounce" => Op::Bounce,
|
"bounce" => Op::Bounce(None),
|
||||||
"wchoose" => Op::WChoose,
|
"wchoose" => Op::WChoose(None),
|
||||||
"every" => Op::Every,
|
"every" => Op::Every,
|
||||||
"chance" => Op::ChanceExec,
|
"chance" => Op::ChanceExec(None),
|
||||||
"prob" => Op::ProbExec,
|
"prob" => Op::ProbExec(None),
|
||||||
"coin" => Op::Coin,
|
"coin" => Op::Coin(None),
|
||||||
"mtof" => Op::Mtof,
|
"mtof" => Op::Mtof,
|
||||||
"ftom" => Op::Ftom,
|
"ftom" => Op::Ftom,
|
||||||
"?" => Op::When,
|
"?" => Op::When,
|
||||||
@@ -187,6 +187,15 @@ fn parse_interval(name: &str) -> Option<i64> {
|
|||||||
Some(simple)
|
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(
|
pub(crate) fn compile_word(
|
||||||
name: &str,
|
name: &str,
|
||||||
span: Option<SourceSpan>,
|
span: Option<SourceSpan>,
|
||||||
@@ -225,7 +234,10 @@ pub(crate) fn compile_word(
|
|||||||
if let Some(word) = lookup_word(name) {
|
if let Some(word) = lookup_word(name) {
|
||||||
match &word.compile {
|
match &word.compile {
|
||||||
Simple => {
|
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);
|
ops.push(op);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,7 +245,7 @@ pub(crate) fn compile_word(
|
|||||||
Param => ops.push(Op::SetParam(word.name)),
|
Param => ops.push(Op::SetParam(word.name)),
|
||||||
Probability(p) => {
|
Probability(p) => {
|
||||||
ops.push(Op::PushFloat(*p, None));
|
ops.push(Op::PushFloat(*p, None));
|
||||||
ops.push(Op::ChanceExec);
|
ops.push(Op::ChanceExec(span));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use ratatui::{
|
|||||||
};
|
};
|
||||||
use tui_textarea::TextArea;
|
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)]
|
#[derive(Clone)]
|
||||||
pub struct CompletionCandidate {
|
pub struct CompletionCandidate {
|
||||||
@@ -452,21 +452,25 @@ impl Editor {
|
|||||||
let mut spans: Vec<Span> = Vec::new();
|
let mut spans: Vec<Span> = Vec::new();
|
||||||
let mut col = 0;
|
let mut col = 0;
|
||||||
|
|
||||||
for (base_style, text) in tokens {
|
for (base_style, text, is_annotation) in tokens {
|
||||||
for ch in text.chars() {
|
for ch in text.chars() {
|
||||||
let is_cursor = row == cursor_row && col == cursor_col;
|
let style = if is_annotation {
|
||||||
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 {
|
|
||||||
base_style
|
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));
|
spans.push(Span::styled(ch.to_string(), style));
|
||||||
col += 1;
|
if !is_annotation {
|
||||||
|
col += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,9 @@ struct ForthHighlighter;
|
|||||||
impl CodeHighlighter for ForthHighlighter {
|
impl CodeHighlighter for ForthHighlighter {
|
||||||
fn highlight(&self, line: &str) -> Vec<(Style, String)> {
|
fn highlight(&self, line: &str) -> Vec<(Style, String)> {
|
||||||
highlight::highlight_line(line)
|
highlight::highlight_line(line)
|
||||||
|
.into_iter()
|
||||||
|
.map(|(s, t, _)| (s, t))
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -202,24 +202,27 @@ fn classify_word(word: &str, user_words: &HashSet<String>) -> (TokenKind, bool)
|
|||||||
(TokenKind::Default, false)
|
(TokenKind::Default, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn highlight_line(line: &str) -> Vec<(Style, String)> {
|
pub fn highlight_line(line: &str) -> Vec<(Style, String, bool)> {
|
||||||
highlight_line_with_runtime(line, &[], &[], &EMPTY_SET)
|
highlight_line_with_runtime(line, &[], &[], &[], &EMPTY_SET)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn highlight_line_with_runtime(
|
pub fn highlight_line_with_runtime(
|
||||||
line: &str,
|
line: &str,
|
||||||
executed_spans: &[SourceSpan],
|
executed_spans: &[SourceSpan],
|
||||||
selected_spans: &[SourceSpan],
|
selected_spans: &[SourceSpan],
|
||||||
|
resolved: &[(SourceSpan, String)],
|
||||||
user_words: &HashSet<String>,
|
user_words: &HashSet<String>,
|
||||||
) -> Vec<(Style, String)> {
|
) -> Vec<(Style, String, bool)> {
|
||||||
let tokens = tokenize_line(line, user_words);
|
let tokens = tokenize_line(line, user_words);
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
let mut last_end = 0;
|
let mut last_end = 0;
|
||||||
let gap_style = TokenKind::gap_style();
|
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 {
|
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
|
let is_selected = selected_spans
|
||||||
@@ -233,19 +236,25 @@ pub fn highlight_line_with_runtime(
|
|||||||
if token.varargs {
|
if token.varargs {
|
||||||
style = style.add_modifier(Modifier::UNDERLINED);
|
style = style.add_modifier(Modifier::UNDERLINED);
|
||||||
}
|
}
|
||||||
let theme = theme::get();
|
|
||||||
if is_selected {
|
if is_selected {
|
||||||
style = style.bg(theme.syntax.selected_bg).add_modifier(Modifier::BOLD);
|
style = style.bg(theme.syntax.selected_bg).add_modifier(Modifier::BOLD);
|
||||||
} else if is_executed {
|
} else if is_executed {
|
||||||
style = style.bg(theme.syntax.executed_bg);
|
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;
|
last_end = token.end;
|
||||||
}
|
}
|
||||||
|
|
||||||
if last_end < line.len() {
|
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
|
result
|
||||||
|
|||||||
@@ -45,6 +45,30 @@ fn adjust_spans_for_line(
|
|||||||
.collect()
|
.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) {
|
pub fn render(frame: &mut Frame, app: &App, link: &LinkState, snapshot: &SequencerSnapshot, elapsed: Duration) {
|
||||||
let term = frame.area();
|
let term = frame.area();
|
||||||
|
|
||||||
@@ -627,6 +651,15 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
|||||||
None
|
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 mut line_start = 0usize;
|
||||||
let lines: Vec<Line> = script
|
let lines: Vec<Line> = script
|
||||||
.lines()
|
.lines()
|
||||||
@@ -642,14 +675,19 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
|||||||
line_start,
|
line_start,
|
||||||
line_str.len(),
|
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 {
|
} else {
|
||||||
highlight_line_with_runtime(line_str, &[], &[], &user_words)
|
highlight_line_with_runtime(line_str, &[], &[], &[], &user_words)
|
||||||
};
|
};
|
||||||
line_start += line_str.len() + 1;
|
line_start += line_str.len() + 1;
|
||||||
let spans: Vec<Span> = tokens
|
let spans: Vec<Span> = tokens
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(style, text)| Span::styled(text, style))
|
.map(|(style, text, _)| Span::styled(text, style))
|
||||||
.collect();
|
.collect();
|
||||||
Line::from(spans)
|
Line::from(spans)
|
||||||
})
|
})
|
||||||
@@ -712,16 +750,26 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term
|
|||||||
offset += line.len() + 1;
|
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 line_start = line_offsets[row];
|
||||||
let (exec, sel) = match trace {
|
let (exec, sel, res) = match trace {
|
||||||
Some(t) => (
|
Some(t) => (
|
||||||
adjust_spans_for_line(&t.executed_spans, line_start, line.len()),
|
adjust_spans_for_line(&t.executed_spans, line_start, line.len()),
|
||||||
adjust_spans_for_line(&t.selected_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()
|
let show_search = app.editor_ctx.editor.search_active()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use arc_swap::ArcSwap;
|
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 parking_lot::Mutex;
|
||||||
use rand::rngs::StdRng;
|
use rand::rngs::StdRng;
|
||||||
use rand::SeedableRng;
|
use rand::SeedableRng;
|
||||||
@@ -140,3 +140,11 @@ pub fn expect_outputs(script: &str, count: usize) -> Vec<String> {
|
|||||||
assert_eq!(outputs.len(), count, "expected {} outputs", count);
|
assert_eq!(outputs.len(), count, "expected {} outputs", count);
|
||||||
outputs
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use super::harness::*;
|
use super::harness::*;
|
||||||
|
use cagire::forth::ResolvedValue;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rand_in_range() {
|
fn rand_in_range() {
|
||||||
@@ -253,3 +254,14 @@ fn wchoose_quotation() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(stack_int(&f), 20);
|
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"));
|
||||||
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ fn param_only_emit() {
|
|||||||
assert!(outputs[0].contains("voice/0"));
|
assert!(outputs[0].contains("voice/0"));
|
||||||
assert!(outputs[0].contains("freq/880"));
|
assert!(outputs[0].contains("freq/880"));
|
||||||
assert!(!outputs[0].contains("sound/"));
|
assert!(!outputs[0].contains("sound/"));
|
||||||
assert!(!outputs[0].contains("dur/"));
|
assert!(outputs[0].contains("dur/"));
|
||||||
assert!(!outputs[0].contains("delaytime/"));
|
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);
|
let outputs = expect_outputs(r#"0 0.5 at 60 64 note sine s ."#, 4);
|
||||||
assert_eq!(outputs.len(), 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"));
|
||||||
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 926 KiB |
BIN
website/public/eight_pic.webp
Normal file
|
After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 941 KiB |
BIN
website/public/fifth_pic.webp
Normal file
|
After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 887 KiB |
BIN
website/public/fourth_pic.webp
Normal file
|
After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 805 KiB |
BIN
website/public/ninth_pic.webp
Normal file
|
After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 925 KiB |
BIN
website/public/one_pic.webp
Normal file
|
After Width: | Height: | Size: 67 KiB |
@@ -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 => {
|
document.querySelectorAll('.example-cell').forEach(cell => {
|
||||||
cell.addEventListener('click', () => {
|
cell.addEventListener('click', () => {
|
||||||
const video = cell.querySelector('video');
|
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) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
document.querySelectorAll('.example-cell.expanded').forEach(c => {
|
document.querySelectorAll('.example-cell.expanded').forEach(c => {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 833 KiB |
BIN
website/public/second_pic.webp
Normal file
|
After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 842 KiB |
BIN
website/public/seventh_pic.webp
Normal file
|
After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 870 KiB |
BIN
website/public/sixth_pic.webp
Normal file
|
After Width: | Height: | Size: 55 KiB |
@@ -6,14 +6,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg: #000;
|
|
||||||
--surface: #121212;
|
|
||||||
--text: #fff;
|
|
||||||
--text-dim: #b4b4b4;
|
|
||||||
--text-muted: #787878;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root.light {
|
|
||||||
--bg: #fff;
|
--bg: #fff;
|
||||||
--surface: #f0f0f0;
|
--surface: #f0f0f0;
|
||||||
--text: #000;
|
--text: #000;
|
||||||
@@ -216,6 +208,51 @@ li {
|
|||||||
display: none;
|
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 {
|
.colophon {
|
||||||
margin-top: 3rem;
|
margin-top: 3rem;
|
||||||
padding-top: 1rem;
|
padding-top: 1rem;
|
||||||
@@ -227,17 +264,3 @@ li {
|
|||||||
color: var(--text-dim);
|
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);
|
|
||||||
}
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 835 KiB |
BIN
website/public/third_pic.webp
Normal file
|
After Width: | Height: | Size: 50 KiB |
@@ -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];
|
||||||
---
|
---
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@@ -23,24 +26,29 @@
|
|||||||
<link rel="stylesheet" href="/style.css">
|
<link rel="stylesheet" href="/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div class="kofi-popup" id="kofi-popup">
|
||||||
|
<button class="kofi-close" id="kofi-close" aria-label="Close">×</button>
|
||||||
|
<p>Consider donating, I need to buy some coffee! Donations help me to rent servers and to secure funding for working on software.</p>
|
||||||
|
<a href="https://ko-fi.com/raphaelbubo" target="_blank" rel="noopener">Support on Ko-fi</a>
|
||||||
|
</div>
|
||||||
<header>
|
<header>
|
||||||
<img class="icon" src="/Cagire.png" alt="Cagire">
|
<img class="icon" src="/Cagire.png" alt="Cagire">
|
||||||
<div>
|
<div>
|
||||||
<h1>CAGIRE: LIVE CODING IN FORTH</h1>
|
<h1>CAGIRE: LIVE CODING IN FORTH</h1>
|
||||||
<p class="subtitle">AGPL-3.0 · Raphaël Maurice Forment · 2026</p>
|
<p class="subtitle">AGPL-3.0 · Raphaël Maurice Forment · 2026 · v{version}</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="examples-grid">
|
<div class="examples-grid">
|
||||||
<div class="example-cell"><img src="/one_pic.png" alt="Cagire screenshot 1"></div>
|
<div class="example-cell"><img src="/one_pic.webp" alt="Cagire screenshot 1"></div>
|
||||||
<div class="example-cell"><img src="/second_pic.png" alt="Cagire screenshot 2"></div>
|
<div class="example-cell"><img src="/second_pic.webp" alt="Cagire screenshot 2"></div>
|
||||||
<div class="example-cell"><img src="/third_pic.png" alt="Cagire screenshot 3"></div>
|
<div class="example-cell"><img src="/third_pic.webp" alt="Cagire screenshot 3"></div>
|
||||||
<div class="example-cell"><img src="/fourth_pic.png" alt="Cagire screenshot 4"></div>
|
<div class="example-cell"><img src="/fourth_pic.webp" alt="Cagire screenshot 4"></div>
|
||||||
<div class="example-cell"><img src="/fifth_pic.png" alt="Cagire screenshot 5"></div>
|
<div class="example-cell"><img src="/fifth_pic.webp" alt="Cagire screenshot 5"></div>
|
||||||
<div class="example-cell"><img src="/sixth_pic.png" alt="Cagire screenshot 6"></div>
|
<div class="example-cell"><img src="/sixth_pic.webp" alt="Cagire screenshot 6"></div>
|
||||||
<div class="example-cell"><img src="/seventh_pic.png" alt="Cagire screenshot 7"></div>
|
<div class="example-cell"><img src="/seventh_pic.webp" alt="Cagire screenshot 7"></div>
|
||||||
<div class="example-cell"><img src="/eight_pic.png" alt="Cagire screenshot 8"></div>
|
<div class="example-cell"><img src="/eight_pic.webp" alt="Cagire screenshot 8"></div>
|
||||||
<div class="example-cell"><img src="/ninth_pic.png" alt="Cagire screenshot 9"></div>
|
<div class="example-cell"><img src="/ninth_pic.webp" alt="Cagire screenshot 9"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>Download</h2>
|
<h2>Download</h2>
|
||||||
@@ -109,8 +117,7 @@
|
|||||||
<video src="/mono_cagire.mp4" autoplay muted loop playsinline></video>
|
<video src="/mono_cagire.mp4" autoplay muted loop playsinline></video>
|
||||||
|
|
||||||
<p class="colophon">
|
<p class="colophon">
|
||||||
<a href="https://raphaelforment.fr">BuboBubo</a> · Audio engine: <a href="https://doux.livecoding.fr">Doux</a> · <a href="https://github.com/Bubobubobubobubo/cagire">GitHub</a> · AGPL-3.0 · <button id="theme-toggle" aria-label="Toggle theme">LIGHT</button>
|
<a href="https://raphaelforment.fr">BuboBubo</a> · Audio engine: <a href="https://doux.livecoding.fr">Doux</a> · <a href="https://github.com/Bubobubobubobubo/cagire">GitHub</a> · AGPL-3.0 </p>
|
||||||
</p>
|
|
||||||
|
|
||||||
<script is:inline src="/script.js"></script>
|
<script is:inline src="/script.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||