Feat: UI / UX

This commit is contained in:
2026-02-16 01:22:40 +01:00
parent b23dd85d0f
commit af6732db1c
37 changed files with 1045 additions and 64 deletions

View File

@@ -160,6 +160,12 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
ops.push(Op::Branch(else_ops.len()));
ops.extend(else_ops);
}
} else if word == "case" {
let (case_ops, consumed) = compile_case(&tokens[i + 1..], dict)?;
i += consumed;
ops.extend(case_ops);
} else if word == "of" || word == "endof" || word == "endcase" {
return Err(format!("unexpected '{word}'"));
} else if !compile_word(word, Some(*span), &mut ops, dict) {
return Err(format!("unknown word: {word}"));
}
@@ -300,3 +306,69 @@ fn compile_if(
Ok((then_ops, else_ops, then_pos + 1, then_span, else_span))
}
fn compile_case(tokens: &[Token], dict: &Dictionary) -> Result<(Vec<Op>, usize), String> {
let mut depth = 1;
let mut endcase_pos = None;
let mut clauses: Vec<(usize, usize)> = Vec::new();
let mut last_of = None;
for (i, tok) in tokens.iter().enumerate() {
if let Token::Word(w, _) = tok {
match w.as_str() {
"case" => depth += 1,
"endcase" => {
depth -= 1;
if depth == 0 {
endcase_pos = Some(i);
break;
}
}
"of" if depth == 1 => last_of = Some(i),
"endof" if depth == 1 => {
let of_pos = last_of.ok_or("'endof' without matching 'of'")?;
clauses.push((of_pos, i));
last_of = None;
}
_ => {}
}
}
}
let endcase_pos = endcase_pos.ok_or("missing 'endcase'")?;
let mut ops = Vec::new();
let mut branch_fixups: Vec<usize> = Vec::new();
let mut clause_start = 0;
for &(of_pos, endof_pos) in &clauses {
let test_ops = compile(&tokens[clause_start..of_pos], dict)?;
let body_ops = compile(&tokens[of_pos + 1..endof_pos], dict)?;
ops.extend(test_ops);
ops.push(Op::Over);
ops.push(Op::Eq);
ops.push(Op::BranchIfZero(body_ops.len() + 2, None, None));
ops.push(Op::Drop);
ops.extend(body_ops);
branch_fixups.push(ops.len());
ops.push(Op::Branch(0));
clause_start = endof_pos + 1;
}
let default_tokens = &tokens[clause_start..endcase_pos];
if !default_tokens.is_empty() {
let default_ops = compile(default_tokens, dict)?;
ops.extend(default_ops);
}
ops.push(Op::Drop);
let end = ops.len();
for pos in branch_fixups {
ops[pos] = Op::Branch(end - pos - 1);
}
Ok((ops, endcase_pos + 1))
}

View File

@@ -64,6 +64,7 @@ pub enum Op {
Emit,
Get,
Set,
SetKeep,
GetContext(&'static str),
Rand(Option<SourceSpan>),
ExpRand(Option<SourceSpan>),

View File

@@ -637,6 +637,16 @@ impl Forth {
.expect("var_writes taken")
.insert(name, val);
}
Op::SetKeep => {
let name = pop(stack)?;
let name = name.as_str()?.to_string();
let val = stack.last().ok_or("Stack underflow")?.clone();
var_writes_cell
.borrow_mut()
.as_mut()
.expect("var_writes taken")
.insert(name, val);
}
Op::GetContext(name) => {
let val = match *name {

View File

@@ -272,6 +272,14 @@ pub(crate) fn compile_word(
}
}
if let Some(var_name) = name.strip_prefix(',') {
if !var_name.is_empty() {
ops.push(Op::PushStr(Arc::from(var_name), span));
ops.push(Op::SetKeep);
return true;
}
}
if let Some(midi) = parse_note_name(name) {
ops.push(Op::PushInt(midi, span));
return true;

View File

@@ -558,6 +558,16 @@ pub(super) const WORDS: &[Word] = &[
compile: Simple,
varargs: false,
},
Word {
name: ",<var>",
aliases: &[],
category: "Variables",
stack: "(val -- val)",
desc: "Store value in variable, keep on stack",
example: "440 ,freq => 440",
compile: Simple,
varargs: false,
},
// Definitions
Word {
name: ":",

View File

@@ -3,5 +3,5 @@ mod parser;
mod theme;
pub use highlighter::{CodeHighlighter, NoHighlight};
pub use parser::parse;
pub use parser::{parse, CodeBlock, ParsedMarkdown};
pub use theme::{DefaultTheme, MarkdownTheme};

View File

@@ -5,17 +5,31 @@ use ratatui::text::{Line as RLine, Span};
use crate::highlighter::CodeHighlighter;
use crate::theme::MarkdownTheme;
pub struct CodeBlock {
pub start_line: usize,
pub end_line: usize,
pub source: String,
}
pub struct ParsedMarkdown {
pub lines: Vec<RLine<'static>>,
pub code_blocks: Vec<CodeBlock>,
}
pub fn parse<T: MarkdownTheme, H: CodeHighlighter>(
md: &str,
theme: &T,
highlighter: &H,
) -> Vec<RLine<'static>> {
) -> ParsedMarkdown {
let processed = preprocess_markdown(md);
let text = minimad::Text::from(processed.as_str());
let mut lines = Vec::new();
let mut code_line_nr: usize = 0;
let mut table_buffer: Vec<TableRow> = Vec::new();
let mut code_blocks: Vec<CodeBlock> = Vec::new();
let mut current_block_start: Option<usize> = None;
let mut current_block_source: Vec<String> = Vec::new();
let flush_table = |buf: &mut Vec<TableRow>, out: &mut Vec<RLine<'static>>, theme: &T| {
if buf.is_empty() {
@@ -27,16 +41,43 @@ pub fn parse<T: MarkdownTheme, H: CodeHighlighter>(
}
};
let close_block = |start: Option<usize>,
source: &mut Vec<String>,
blocks: &mut Vec<CodeBlock>,
lines: &Vec<RLine<'static>>| {
if let Some(start) = start {
blocks.push(CodeBlock {
start_line: start,
end_line: lines.len(),
source: std::mem::take(source).join("\n"),
});
}
};
for line in text.lines {
let is_code = matches!(&line, Line::Normal(c) if c.style == CompositeStyle::Code);
if !is_code {
close_block(
current_block_start.take(),
&mut current_block_source,
&mut code_blocks,
&lines,
);
}
match line {
Line::Normal(composite) if composite.style == CompositeStyle::Code => {
flush_table(&mut table_buffer, &mut lines, theme);
code_line_nr += 1;
if current_block_start.is_none() {
current_block_start = Some(lines.len());
}
let raw: String = composite
.compounds
.iter()
.map(|c: &minimad::Compound| c.src)
.collect();
current_block_source.push(raw.clone());
let mut spans = vec![
Span::styled(format!(" {code_line_nr:>2} "), theme.code_border()),
Span::styled("", theme.code_border()),
@@ -66,9 +107,15 @@ pub fn parse<T: MarkdownTheme, H: CodeHighlighter>(
}
}
}
close_block(
current_block_start.take(),
&mut current_block_source,
&mut code_blocks,
&lines,
);
flush_table(&mut table_buffer, &mut lines, theme);
lines
ParsedMarkdown { lines, code_blocks }
}
pub fn preprocess_markdown(md: &str) -> String {
@@ -300,28 +347,39 @@ mod tests {
#[test]
fn test_parse_headings() {
let md = "# H1\n## H2\n### H3";
let lines = parse(md, &DefaultTheme, &NoHighlight);
assert_eq!(lines.len(), 3);
let parsed = parse(md, &DefaultTheme, &NoHighlight);
assert_eq!(parsed.lines.len(), 3);
}
#[test]
fn test_parse_code_block() {
let md = "```\ncode line\n```";
let lines = parse(md, &DefaultTheme, &NoHighlight);
assert!(!lines.is_empty());
let parsed = parse(md, &DefaultTheme, &NoHighlight);
assert!(!parsed.lines.is_empty());
assert_eq!(parsed.code_blocks.len(), 1);
assert_eq!(parsed.code_blocks[0].source, "code line");
}
#[test]
fn test_parse_table() {
let md = "| A | B |\n|---|---|\n| 1 | 2 |";
let lines = parse(md, &DefaultTheme, &NoHighlight);
assert_eq!(lines.len(), 2);
let parsed = parse(md, &DefaultTheme, &NoHighlight);
assert_eq!(parsed.lines.len(), 2);
}
#[test]
fn test_default_theme_works() {
let md = "Hello **world**";
let lines = parse(md, &DefaultTheme, &NoHighlight);
assert_eq!(lines.len(), 1);
let parsed = parse(md, &DefaultTheme, &NoHighlight);
assert_eq!(parsed.lines.len(), 1);
}
#[test]
fn test_multiple_code_blocks() {
let md = "text\n```\nfirst\n```\nmore text\n```\nsecond line 1\nsecond line 2\n```";
let parsed = parse(md, &DefaultTheme, &NoHighlight);
assert_eq!(parsed.code_blocks.len(), 2);
assert_eq!(parsed.code_blocks[0].source, "first");
assert_eq!(parsed.code_blocks[1].source, "second line 1\nsecond line 2");
}
}