diff --git a/crates/forth/src/compiler.rs b/crates/forth/src/compiler.rs index 1ab4bc2..bd6d208 100644 --- a/crates/forth/src/compiler.rs +++ b/crates/forth/src/compiler.rs @@ -143,6 +143,19 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result, String> { ops.push(Op::Quotation(Arc::from(quote_ops), Some(body_span))); } else if word == "}" { return Err("unexpected }".into()); + } else if word == "[" { + let (bracket_ops, consumed, end_span) = + compile_bracket(&tokens[i + 1..], dict)?; + i += consumed; + ops.push(Op::Mark); + ops.extend(bracket_ops); + let count_span = SourceSpan { + start: span.start, + end: end_span.end, + }; + ops.push(Op::Count(Some(count_span))); + } else if word == "]" { + return Err("unexpected ]".into()); } else if word == ":" { let (consumed, name, body) = compile_colon_def(&tokens[i + 1..], dict)?; i += consumed; @@ -211,6 +224,38 @@ fn compile_quotation( Ok((quote_ops, end_idx + 1, end_span)) } +fn compile_bracket( + tokens: &[Token], + dict: &Dictionary, +) -> Result<(Vec, usize, SourceSpan), String> { + let mut depth = 1; + let mut end_idx = None; + + for (i, tok) in tokens.iter().enumerate() { + if let Token::Word(w, _) = tok { + match w.as_str() { + "[" => depth += 1, + "]" => { + depth -= 1; + if depth == 0 { + end_idx = Some(i); + break; + } + } + _ => {} + } + } + } + + let end_idx = end_idx.ok_or("missing ]")?; + let end_span = match &tokens[end_idx] { + Token::Word(_, span) => *span, + _ => unreachable!(), + }; + let body_ops = compile(&tokens[..end_idx], dict)?; + Ok((body_ops, end_idx + 1, end_span)) +} + fn token_span(tok: &Token) -> Option { match tok { Token::Int(_, s) | Token::Float(_, s) | Token::Str(_, s) | Token::Word(_, s) => Some(*s), diff --git a/crates/forth/src/ops.rs b/crates/forth/src/ops.rs index 0446474..668512d 100644 --- a/crates/forth/src/ops.rs +++ b/crates/forth/src/ops.rs @@ -136,4 +136,8 @@ pub enum Op { MidiStart, MidiStop, MidiContinue, + // Bracket syntax (mark/count for auto-counting) + Mark, + Count(Option), + Index(Option), } diff --git a/crates/forth/src/vm.rs b/crates/forth/src/vm.rs index e156e55..8acf7b4 100644 --- a/crates/forth/src/vm.rs +++ b/crates/forth/src/vm.rs @@ -140,6 +140,7 @@ impl Forth { var_writes: &mut HashMap, ) -> Result<(), String> { let mut pc = 0; + let mut marks: Vec = Vec::new(); let trace_cell = std::cell::RefCell::new(trace); let var_writes_cell = std::cell::RefCell::new(Some(var_writes)); @@ -1541,6 +1542,29 @@ impl Forth { .unwrap_or(0); stack.push(Value::Int(val as i64, None)); } + Op::Mark => { + marks.push(stack.len()); + } + Op::Count(span) => { + let mark = marks.pop().ok_or("count without mark")?; + stack.push(Value::Int((stack.len() - mark) as i64, *span)); + } + Op::Index(word_span) => { + let idx = pop_int(stack)?; + let count = pop_int(stack)? as usize; + if count == 0 { + return Err("index count must be > 0".into()); + } + let resolved_idx = ((idx % count as i64 + count as i64) % count as i64) as usize; + if let Some(span) = word_span { + if stack.len() >= count { + let start = stack.len() - count; + let selected = &stack[start + resolved_idx]; + record_resolved_from_value(&trace_cell, Some(*span), selected); + } + } + drain_select_run(count, resolved_idx, stack, outputs, cmd)?; + } Op::Forget => { let name = pop(stack)?; self.dict.lock().remove(name.as_str()?); diff --git a/crates/forth/src/words/compile.rs b/crates/forth/src/words/compile.rs index d6d2411..98b88b7 100644 --- a/crates/forth/src/words/compile.rs +++ b/crates/forth/src/words/compile.rs @@ -110,6 +110,7 @@ pub(super) fn simple_op(name: &str) -> Option { "mstop" => Op::MidiStop, "mcont" => Op::MidiContinue, "forget" => Op::Forget, + "index" => Op::Index(None), "key!" => Op::SetKey, "tp" => Op::Transpose, "inv" => Op::Invert, @@ -205,7 +206,8 @@ fn attach_span(op: &mut Op, span: SourceSpan) { | Op::Choose(s) | Op::WChoose(s) | Op::Cycle(s) | Op::PCycle(s) | Op::Bounce(s) | Op::ChanceExec(s) | Op::ProbExec(s) | Op::Every(s) - | Op::Bjork(s) | Op::PBjork(s) => *s = Some(span), + | Op::Bjork(s) | Op::PBjork(s) + | Op::Count(s) | Op::Index(s) => *s = Some(span), _ => {} } } diff --git a/crates/forth/src/words/sequencing.rs b/crates/forth/src/words/sequencing.rs index 69dee76..6c3325a 100644 --- a/crates/forth/src/words/sequencing.rs +++ b/crates/forth/src/words/sequencing.rs @@ -113,6 +113,16 @@ pub(super) const WORDS: &[Word] = &[ compile: Simple, varargs: true, }, + Word { + name: "index", + aliases: &[], + category: "Probability", + stack: "(v1..vn n idx -- selected)", + desc: "Select item at explicit index", + example: "[ c4 e4 g4 ] step index", + compile: Simple, + varargs: true, + }, Word { name: "wchoose", aliases: &[], diff --git a/crates/ratatui/src/theme/everforest.rs b/crates/ratatui/src/theme/everforest.rs new file mode 100644 index 0000000..fc4f5e3 --- /dev/null +++ b/crates/ratatui/src/theme/everforest.rs @@ -0,0 +1,39 @@ +use super::palette::Palette; + +pub fn palette() -> Palette { + Palette { + bg: (45, 53, 59), + surface: (52, 62, 68), + surface2: (68, 80, 86), + fg: (211, 198, 170), + fg_dim: (135, 131, 116), + fg_muted: (80, 80, 68), + accent: (167, 192, 128), + red: (230, 126, 128), + green: (167, 192, 128), + yellow: (219, 188, 127), + blue: (127, 187, 179), + purple: (214, 153, 182), + cyan: (131, 192, 146), + orange: (230, 152, 117), + tempo_color: (214, 153, 182), + bank_color: (127, 187, 179), + pattern_color: (131, 192, 146), + title_accent: (167, 192, 128), + title_author: (127, 187, 179), + secondary: (230, 152, 117), + link_bright: [ + (167, 192, 128), (214, 153, 182), (230, 152, 117), + (127, 187, 179), (219, 188, 127), + ], + link_dim: [ + (56, 66, 46), (70, 52, 62), (72, 52, 42), + (44, 64, 60), (70, 62, 44), + ], + sparkle: [ + (167, 192, 128), (230, 152, 117), (131, 192, 146), + (214, 153, 182), (219, 188, 127), + ], + meter: [(148, 172, 110), (200, 170, 108), (210, 108, 110)], + } +} diff --git a/crates/ratatui/src/theme/fauve.rs b/crates/ratatui/src/theme/fauve.rs new file mode 100644 index 0000000..d9d0b5b --- /dev/null +++ b/crates/ratatui/src/theme/fauve.rs @@ -0,0 +1,39 @@ +use super::palette::Palette; + +pub fn palette() -> Palette { + Palette { + bg: (28, 22, 18), + surface: (42, 33, 26), + surface2: (58, 46, 36), + fg: (240, 228, 210), + fg_dim: (170, 150, 130), + fg_muted: (100, 82, 66), + accent: (230, 60, 20), + red: (220, 38, 32), + green: (30, 170, 80), + yellow: (255, 210, 0), + blue: (20, 80, 200), + purple: (170, 40, 150), + cyan: (0, 150, 180), + orange: (240, 120, 0), + tempo_color: (230, 60, 20), + bank_color: (20, 80, 200), + pattern_color: (0, 150, 180), + title_accent: (230, 60, 20), + title_author: (20, 80, 200), + secondary: (170, 40, 150), + link_bright: [ + (230, 60, 20), (20, 80, 200), (240, 120, 0), + (0, 150, 180), (30, 170, 80), + ], + link_dim: [ + (72, 24, 10), (10, 28, 65), (76, 40, 6), + (6, 48, 58), (14, 54, 28), + ], + sparkle: [ + (230, 60, 20), (255, 210, 0), (30, 170, 80), + (20, 80, 200), (170, 40, 150), + ], + meter: [(26, 152, 72), (235, 190, 0), (200, 34, 28)], + } +} diff --git a/crates/ratatui/src/theme/iceberg.rs b/crates/ratatui/src/theme/iceberg.rs new file mode 100644 index 0000000..39122f4 --- /dev/null +++ b/crates/ratatui/src/theme/iceberg.rs @@ -0,0 +1,39 @@ +use super::palette::Palette; + +pub fn palette() -> Palette { + Palette { + bg: (22, 24, 33), + surface: (30, 33, 46), + surface2: (45, 48, 64), + fg: (198, 200, 209), + fg_dim: (109, 112, 126), + fg_muted: (64, 66, 78), + accent: (132, 160, 198), + red: (226, 120, 120), + green: (180, 190, 130), + yellow: (226, 164, 120), + blue: (132, 160, 198), + purple: (160, 147, 199), + cyan: (137, 184, 194), + orange: (226, 164, 120), + tempo_color: (160, 147, 199), + bank_color: (132, 160, 198), + pattern_color: (137, 184, 194), + title_accent: (132, 160, 198), + title_author: (160, 147, 199), + secondary: (226, 164, 120), + link_bright: [ + (132, 160, 198), (160, 147, 199), (226, 164, 120), + (137, 184, 194), (180, 190, 130), + ], + link_dim: [ + (45, 55, 70), (55, 50, 68), (70, 55, 42), + (46, 62, 66), (58, 62, 44), + ], + sparkle: [ + (132, 160, 198), (226, 164, 120), (180, 190, 130), + (160, 147, 199), (226, 120, 120), + ], + meter: [(160, 175, 115), (210, 150, 105), (200, 105, 105)], + } +} diff --git a/crates/ratatui/src/theme/jaipur.rs b/crates/ratatui/src/theme/jaipur.rs new file mode 100644 index 0000000..6b348e0 --- /dev/null +++ b/crates/ratatui/src/theme/jaipur.rs @@ -0,0 +1,39 @@ +use super::palette::Palette; + +pub fn palette() -> Palette { + Palette { + bg: (30, 24, 22), + surface: (44, 36, 32), + surface2: (60, 48, 42), + fg: (238, 222, 200), + fg_dim: (165, 145, 125), + fg_muted: (95, 78, 65), + accent: (210, 90, 100), + red: (200, 44, 52), + green: (30, 160, 120), + yellow: (240, 180, 20), + blue: (60, 60, 180), + purple: (150, 50, 120), + cyan: (0, 155, 155), + orange: (220, 120, 50), + tempo_color: (210, 90, 100), + bank_color: (60, 60, 180), + pattern_color: (0, 155, 155), + title_accent: (210, 90, 100), + title_author: (60, 60, 180), + secondary: (220, 120, 50), + link_bright: [ + (210, 90, 100), (60, 60, 180), (220, 120, 50), + (0, 155, 155), (30, 160, 120), + ], + link_dim: [ + (66, 30, 34), (22, 22, 58), (70, 40, 18), + (6, 48, 48), (12, 50, 38), + ], + sparkle: [ + (210, 90, 100), (240, 180, 20), (30, 160, 120), + (60, 60, 180), (150, 50, 120), + ], + meter: [(26, 144, 106), (222, 164, 18), (184, 40, 46)], + } +} diff --git a/crates/ratatui/src/theme/mod.rs b/crates/ratatui/src/theme/mod.rs index 2d9c3b8..5241a04 100644 --- a/crates/ratatui/src/theme/mod.rs +++ b/crates/ratatui/src/theme/mod.rs @@ -8,17 +8,22 @@ mod catppuccin_mocha; mod dracula; mod eden; mod ember; +mod everforest; mod georges; mod fairyfloss; mod gruvbox_dark; mod hot_dog_stand; +mod iceberg; +mod jaipur; mod kanagawa; mod letz_light; mod monochrome_black; mod monochrome_white; mod monokai; mod nord; +mod fauve; mod pitch_black; +mod tropicalia; mod rose_pine; mod tokyo_night; pub mod transform; @@ -51,6 +56,11 @@ pub const THEMES: &[ThemeEntry] = &[ ThemeEntry { id: "Ember", label: "Ember", palette: ember::palette }, ThemeEntry { id: "Eden", label: "Eden", palette: eden::palette }, ThemeEntry { id: "Georges", label: "Georges", palette: georges::palette }, + ThemeEntry { id: "Iceberg", label: "Iceberg", palette: iceberg::palette }, + ThemeEntry { id: "Everforest", label: "Everforest", palette: everforest::palette }, + ThemeEntry { id: "Fauve", label: "Fauve", palette: fauve::palette }, + ThemeEntry { id: "Tropicalia", label: "Tropicalia", palette: tropicalia::palette }, + ThemeEntry { id: "Jaipur", label: "Jaipur", palette: jaipur::palette }, ]; thread_local! { diff --git a/crates/ratatui/src/theme/tropicalia.rs b/crates/ratatui/src/theme/tropicalia.rs new file mode 100644 index 0000000..faf6a65 --- /dev/null +++ b/crates/ratatui/src/theme/tropicalia.rs @@ -0,0 +1,39 @@ +use super::palette::Palette; + +pub fn palette() -> Palette { + Palette { + bg: (20, 26, 22), + surface: (30, 40, 34), + surface2: (44, 56, 48), + fg: (235, 225, 200), + fg_dim: (155, 145, 120), + fg_muted: (85, 80, 62), + accent: (230, 50, 120), + red: (240, 70, 70), + green: (80, 200, 50), + yellow: (255, 195, 0), + blue: (0, 160, 200), + purple: (180, 60, 180), + cyan: (0, 200, 170), + orange: (255, 140, 30), + tempo_color: (230, 50, 120), + bank_color: (0, 160, 200), + pattern_color: (0, 200, 170), + title_accent: (230, 50, 120), + title_author: (0, 160, 200), + secondary: (255, 140, 30), + link_bright: [ + (230, 50, 120), (0, 160, 200), (255, 140, 30), + (0, 200, 170), (80, 200, 50), + ], + link_dim: [ + (72, 20, 40), (6, 50, 64), (80, 44, 12), + (6, 62, 54), (26, 62, 18), + ], + sparkle: [ + (230, 50, 120), (255, 195, 0), (80, 200, 50), + (0, 160, 200), (180, 60, 180), + ], + meter: [(70, 182, 44), (236, 178, 0), (220, 62, 62)], + } +} diff --git a/src/input/mod.rs b/src/input/mod.rs index a6fc3be..6bbe8a1 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -68,7 +68,9 @@ pub fn handle_key(ctx: &mut InputContext, key: KeyEvent) -> InputResult { if ctx.app.ui.show_title { ctx.dispatch(AppCommand::HideTitle); - return InputResult::Continue; + if matches!(key.code, KeyCode::Char('q') | KeyCode::Esc) { + return InputResult::Continue; + } } ctx.dispatch(AppCommand::ClearStatus); diff --git a/src/input_egui.rs b/src/input_egui.rs index f499a9d..e208773 100644 --- a/src/input_egui.rs +++ b/src/input_egui.rs @@ -60,15 +60,13 @@ pub fn convert_egui_events(ctx: &egui::Context) -> Vec { let mut events = Vec::new(); for event in &ctx.input(|i| i.events.clone()) { - if let Some(key_event) = convert_event(event) { - events.push(key_event); - } + convert_event(event, &mut events); } events } -fn convert_event(event: &egui::Event) -> Option { +fn convert_event(event: &egui::Event, events: &mut Vec) { match event { egui::Event::Key { key, @@ -77,33 +75,39 @@ fn convert_event(event: &egui::Event) -> Option { .. } => { if !*pressed { - return None; + return; } let mods = convert_modifiers(*modifiers); - // For character keys without ctrl/alt, let Event::Text handle it - if is_character_key(*key) && !mods.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) - { - return None; - } - let code = convert_key(*key)?; - Some(KeyEvent::new(code, mods)) - } - egui::Event::Text(text) => { - if text.len() == 1 { - let c = text.chars().next()?; - if !c.is_control() { - return Some(KeyEvent::new(KeyCode::Char(c), KeyModifiers::empty())); + // For character keys, only handle Ctrl+key (without Alt) as shortcuts. + // All other character input (bare, Shift, Alt/Option, AltGr=Ctrl+Alt) + // defers to Event::Text which respects the active keyboard layout. + if is_character_key(*key) { + let ctrl_without_alt = + mods.contains(KeyModifiers::CONTROL) && !mods.contains(KeyModifiers::ALT); + if !ctrl_without_alt { + return; + } + } + if let Some(code) = convert_key(*key) { + events.push(KeyEvent::new(code, mods)); + } + } + egui::Event::Text(text) => { + for c in text.chars() { + if !c.is_control() { + events.push(KeyEvent::new(KeyCode::Char(c), KeyModifiers::empty())); } } - None } // egui intercepts Ctrl+C/V/X and converts them to these high-level events // instead of passing through raw Key events (see egui issue #4065). // Synthesize the equivalent KeyEvent so the application's input handler receives them. - egui::Event::Copy => Some(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)), - egui::Event::Cut => Some(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL)), - egui::Event::Paste(_) => Some(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::CONTROL)), - _ => None, + egui::Event::Copy => events.push(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)), + egui::Event::Cut => events.push(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL)), + egui::Event::Paste(_) => { + events.push(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::CONTROL)); + } + _ => {} } } @@ -136,6 +140,14 @@ fn convert_key(key: egui::Key) -> Option { egui::Key::F10 => KeyCode::F(10), egui::Key::F11 => KeyCode::F(11), egui::Key::F12 => KeyCode::F(12), + egui::Key::F13 => KeyCode::F(13), + egui::Key::F14 => KeyCode::F(14), + egui::Key::F15 => KeyCode::F(15), + egui::Key::F16 => KeyCode::F(16), + egui::Key::F17 => KeyCode::F(17), + egui::Key::F18 => KeyCode::F(18), + egui::Key::F19 => KeyCode::F(19), + egui::Key::F20 => KeyCode::F(20), egui::Key::A => KeyCode::Char('a'), egui::Key::B => KeyCode::Char('b'), egui::Key::C => KeyCode::Char('c'), @@ -183,6 +195,13 @@ fn convert_key(key: egui::Key) -> Option { egui::Key::Backslash => KeyCode::Char('\\'), egui::Key::Backtick => KeyCode::Char('`'), egui::Key::Quote => KeyCode::Char('\''), + egui::Key::Colon => KeyCode::Char(':'), + egui::Key::Pipe => KeyCode::Char('|'), + egui::Key::Questionmark => KeyCode::Char('?'), + egui::Key::Exclamationmark => KeyCode::Char('!'), + egui::Key::OpenCurlyBracket => KeyCode::Char('{'), + egui::Key::CloseCurlyBracket => KeyCode::Char('}'), + egui::Key::Plus => KeyCode::Char('+'), _ => return None, }) } @@ -252,5 +271,12 @@ fn is_character_key(key: egui::Key) -> bool { | egui::Key::Backslash | egui::Key::Backtick | egui::Key::Quote + | egui::Key::Colon + | egui::Key::Pipe + | egui::Key::Questionmark + | egui::Key::Exclamationmark + | egui::Key::OpenCurlyBracket + | egui::Key::CloseCurlyBracket + | egui::Key::Plus ) } diff --git a/src/views/render.rs b/src/views/render.rs index 9179d05..a6e477a 100644 --- a/src/views/render.rs +++ b/src/views/render.rs @@ -465,7 +465,7 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) { Page::Main => vec![ ("Space", "Play"), ("Enter", "Edit"), - ("t", "Toggle"), + ("t", "On/Off"), ("Tab", "Samples"), ("?", "Keys"), ], diff --git a/tests/forth/list_words.rs b/tests/forth/list_words.rs index b710665..c03a6be 100644 --- a/tests/forth/list_words.rs +++ b/tests/forth/list_words.rs @@ -83,3 +83,99 @@ fn cycle_zero_count_error() { fn choose_zero_count_error() { expect_error("1 2 3 0 choose", "choose count must be > 0"); } + +// Bracket syntax tests + +#[test] +fn bracket_cycle() { + let ctx = ctx_with(|c| c.runs = 0); + let f = run_ctx("[ 10 20 30 ] cycle", &ctx); + assert_eq!(stack_int(&f), 10); + + let ctx = ctx_with(|c| c.runs = 1); + let f = run_ctx("[ 10 20 30 ] cycle", &ctx); + assert_eq!(stack_int(&f), 20); + + let ctx = ctx_with(|c| c.runs = 2); + let f = run_ctx("[ 10 20 30 ] cycle", &ctx); + assert_eq!(stack_int(&f), 30); + + let ctx = ctx_with(|c| c.runs = 3); + let f = run_ctx("[ 10 20 30 ] cycle", &ctx); + assert_eq!(stack_int(&f), 10); +} + +#[test] +fn bracket_with_quotations() { + let ctx = ctx_with(|c| c.runs = 0); + let f = run_ctx("5 [ { 3 + } { 5 + } ] cycle", &ctx); + assert_eq!(stack_int(&f), 8); + + let ctx = ctx_with(|c| c.runs = 1); + let f = run_ctx("5 [ { 3 + } { 5 + } ] cycle", &ctx); + assert_eq!(stack_int(&f), 10); +} + +#[test] +fn bracket_nested() { + let ctx = ctx_with(|c| { c.runs = 0; c.iter = 0; }); + let f = run_ctx("[ [ 10 20 ] cycle [ 30 40 ] cycle ] pcycle", &ctx); + assert_eq!(stack_int(&f), 10); + + let ctx = ctx_with(|c| { c.runs = 0; c.iter = 1; }); + let f = run_ctx("[ [ 10 20 ] cycle [ 30 40 ] cycle ] pcycle", &ctx); + assert_eq!(stack_int(&f), 30); +} + +#[test] +fn bracket_with_generator() { + let ctx = ctx_with(|c| c.runs = 0); + let f = run_ctx("[ 1 4 .. ] cycle", &ctx); + assert_eq!(stack_int(&f), 1); + + let ctx = ctx_with(|c| c.runs = 3); + let f = run_ctx("[ 1 4 .. ] cycle", &ctx); + assert_eq!(stack_int(&f), 4); +} + +#[test] +fn stray_bracket_error() { + expect_error("10 ] cycle", "unexpected ]"); +} + +#[test] +fn unclosed_bracket_error() { + expect_error("[ 10 20", "missing ]"); +} + +// Index tests + +#[test] +fn index_basic() { + expect_int("10 20 30 3 1 index", 20); +} + +#[test] +fn index_with_brackets() { + expect_int("[ 10 20 30 ] 1 index", 20); +} + +#[test] +fn index_modulo_wraps() { + expect_int("[ 10 20 30 ] 5 index", 30); +} + +#[test] +fn index_negative_wraps() { + expect_int("[ 10 20 30 ] -1 index", 30); +} + +#[test] +fn index_with_quotation() { + expect_int("5 [ { 3 + } { 5 + } ] 0 index", 8); +} + +#[test] +fn index_zero_count_error() { + expect_error("0 0 index", "index count must be > 0"); +}