diff --git a/CHANGELOG.md b/CHANGELOG.md index a51fcc4..4edcfbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. ## [0.0.8] - 2026-06-05 +### Added +- New themes: **Eden** (dark forest — black background with green-only palette, terminal aesthetic) and **Georges** (Commodore 64 palette on pure black background). + ### Improved - Sample library browser: search now shows folder names only (no files) while typing, sorted by fuzzy match score. After confirming search with Enter, folders can be expanded and collapsed normally. Esc clears the search filter before closing the panel. Left arrow on a file collapses the parent folder. Cursor and scroll position stay valid after expand/collapse operations. - RAM optimizations saving ~5 MB at startup plus smaller enums and fewer hot-path allocations: diff --git a/crates/forth/src/compiler.rs b/crates/forth/src/compiler.rs index 0a21f4c..1817d9d 100644 --- a/crates/forth/src/compiler.rs +++ b/crates/forth/src/compiler.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::sync::Arc; use super::ops::Op; @@ -88,18 +89,18 @@ fn tokenize(input: &str) -> Vec { let span = SourceSpan { start: start as u32, end: end as u32 }; // Normalize shorthand float syntax: .25 -> 0.25, -.5 -> -0.5 - let word_to_parse = if word.starts_with('.') + let word_to_parse: Cow = if word.starts_with('.') && word.len() > 1 && word.as_bytes()[1].is_ascii_digit() { - format!("0{word}") + Cow::Owned(format!("0{word}")) } else if word.starts_with("-.") && word.len() > 2 && word.as_bytes()[2].is_ascii_digit() { - format!("-0{}", &word[1..]) + Cow::Owned(format!("-0{}", &word[1..])) } else { - word.clone() + Cow::Borrowed(&word) }; if let Ok(i) = word_to_parse.parse::() { @@ -121,20 +122,10 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result, String> { while i < tokens.len() { match &tokens[i] { Token::Int(n, span) => { - let key = n.to_string(); - if let Some(body) = dict.lock().get(&key).cloned() { - ops.extend(body); - } else { - ops.push(Op::PushInt(*n, Some(*span))); - } + ops.push(Op::PushInt(*n, Some(*span))); } Token::Float(f, span) => { - let key = f.to_string(); - if let Some(body) = dict.lock().get(&key).cloned() { - ops.extend(body); - } else { - ops.push(Op::PushFloat(*f, Some(*span))); - } + ops.push(Op::PushFloat(*f, Some(*span))); } Token::Str(s, span) => ops.push(Op::PushStr(Arc::from(s.as_str()), Some(*span))), Token::Word(w, span) => { diff --git a/crates/ratatui/src/editor.rs b/crates/ratatui/src/editor.rs index 76614c0..9163302 100644 --- a/crates/ratatui/src/editor.rs +++ b/crates/ratatui/src/editor.rs @@ -12,6 +12,7 @@ use tui_textarea::TextArea; pub type Highlighter<'a> = &'a dyn Fn(usize, &str) -> Vec<(Style, String)>; +#[derive(Clone)] pub struct CompletionCandidate { pub name: String, pub signature: String, diff --git a/crates/ratatui/src/theme/eden.rs b/crates/ratatui/src/theme/eden.rs new file mode 100644 index 0000000..4389cc4 --- /dev/null +++ b/crates/ratatui/src/theme/eden.rs @@ -0,0 +1,282 @@ +use super::*; +use ratatui::style::Color; + +pub fn theme() -> ThemeColors { + let bg = Color::Rgb(8, 12, 8); + let surface = Color::Rgb(16, 24, 16); + let surface2 = Color::Rgb(24, 32, 24); + let border = Color::Rgb(32, 48, 32); + let fg = Color::Rgb(200, 216, 192); + let fg_dim = Color::Rgb(122, 144, 112); + let fg_muted = Color::Rgb(64, 88, 56); + + let green = Color::Rgb(64, 192, 64); + let bright_green = Color::Rgb(96, 224, 96); + let dark_green = Color::Rgb(48, 128, 48); + let cyan = Color::Rgb(80, 168, 144); + let yellow = Color::Rgb(160, 160, 64); + let red = Color::Rgb(192, 80, 64); + let orange = Color::Rgb(176, 128, 48); + let blue = Color::Rgb(80, 128, 160); + let purple = Color::Rgb(128, 104, 144); + + ThemeColors { + ui: UiColors { + bg, + bg_rgb: (8, 12, 8), + text_primary: fg, + text_muted: fg_dim, + text_dim: fg_muted, + border, + header: green, + unfocused: fg_muted, + accent: green, + surface, + }, + status: StatusColors { + playing_bg: Color::Rgb(14, 30, 14), + playing_fg: bright_green, + stopped_bg: Color::Rgb(36, 16, 14), + stopped_fg: red, + fill_on: green, + fill_off: dark_green, + fill_bg: surface, + }, + selection: SelectionColors { + cursor_bg: green, + cursor_fg: bg, + selected_bg: Color::Rgb(20, 40, 20), + selected_fg: bright_green, + in_range_bg: Color::Rgb(16, 30, 16), + in_range_fg: fg, + cursor: green, + selected: Color::Rgb(20, 40, 20), + in_range: Color::Rgb(16, 30, 16), + }, + tile: TileColors { + playing_active_bg: Color::Rgb(16, 38, 16), + playing_active_fg: bright_green, + playing_inactive_bg: Color::Rgb(28, 36, 14), + playing_inactive_fg: yellow, + active_bg: Color::Rgb(14, 28, 26), + active_fg: cyan, + inactive_bg: surface, + inactive_fg: fg_dim, + active_selected_bg: Color::Rgb(22, 36, 28), + active_in_range_bg: Color::Rgb(16, 28, 20), + link_bright: [ + (64, 192, 64), + (80, 168, 144), + (160, 160, 64), + (80, 128, 160), + (192, 80, 64), + ], + link_dim: [ + (14, 38, 14), + (16, 34, 28), + (32, 32, 14), + (16, 26, 32), + (38, 16, 14), + ], + }, + header: HeaderColors { + tempo_bg: Color::Rgb(26, 22, 30), + tempo_fg: purple, + bank_bg: Color::Rgb(16, 26, 32), + bank_fg: blue, + pattern_bg: Color::Rgb(14, 28, 26), + pattern_fg: cyan, + stats_bg: surface, + stats_fg: fg_dim, + }, + modal: ModalColors { + border: green, + border_accent: bright_green, + border_warn: yellow, + border_dim: fg_muted, + confirm: red, + rename: green, + input: cyan, + editor: green, + preview: fg_muted, + }, + flash: FlashColors { + error_bg: Color::Rgb(40, 16, 14), + error_fg: red, + success_bg: Color::Rgb(14, 32, 14), + success_fg: bright_green, + info_bg: surface, + info_fg: fg, + }, + list: ListColors { + playing_bg: Color::Rgb(14, 32, 14), + playing_fg: bright_green, + staged_play_bg: Color::Rgb(26, 22, 30), + staged_play_fg: purple, + staged_stop_bg: Color::Rgb(40, 16, 14), + staged_stop_fg: red, + edit_bg: Color::Rgb(14, 28, 26), + edit_fg: cyan, + hover_bg: surface2, + hover_fg: fg, + muted_bg: Color::Rgb(10, 14, 10), + muted_fg: fg_muted, + soloed_bg: Color::Rgb(30, 30, 14), + soloed_fg: yellow, + }, + link_status: LinkStatusColors { + disabled: red, + connected: bright_green, + listening: yellow, + }, + syntax: SyntaxColors { + gap_bg: bg, + executed_bg: Color::Rgb(14, 24, 14), + selected_bg: Color::Rgb(20, 40, 16), + emit: (fg, Color::Rgb(20, 34, 20)), + number: (yellow, Color::Rgb(30, 30, 12)), + string: (bright_green, Color::Rgb(16, 32, 16)), + comment: (fg_muted, bg), + keyword: (red, Color::Rgb(38, 18, 14)), + stack_op: (blue, Color::Rgb(16, 26, 32)), + operator: (green, Color::Rgb(14, 36, 14)), + sound: (cyan, Color::Rgb(16, 30, 28)), + param: (purple, Color::Rgb(26, 22, 28)), + context: (orange, Color::Rgb(34, 26, 12)), + note: (bright_green, Color::Rgb(16, 34, 16)), + interval: (Color::Rgb(100, 180, 100), Color::Rgb(18, 34, 18)), + variable: (purple, Color::Rgb(26, 22, 28)), + vary: (yellow, Color::Rgb(30, 30, 12)), + generator: (cyan, Color::Rgb(16, 30, 26)), + default: (fg_dim, bg), + }, + table: TableColors { + row_even: bg, + row_odd: surface, + }, + values: ValuesColors { + tempo: green, + value: fg_dim, + }, + hint: HintColors { + key: green, + text: fg_muted, + }, + view_badge: ViewBadgeColors { bg: fg, fg: bg }, + nav: NavColors { + selected_bg: Color::Rgb(18, 38, 18), + selected_fg: fg, + unselected_bg: surface, + unselected_fg: fg_muted, + }, + editor_widget: EditorWidgetColors { + cursor_bg: fg, + cursor_fg: bg, + selection_bg: Color::Rgb(24, 48, 24), + completion_bg: surface, + completion_fg: fg, + completion_selected: green, + completion_example: cyan, + }, + browser: BrowserColors { + directory: blue, + project_file: green, + selected: bright_green, + file: fg, + focused_border: green, + unfocused_border: fg_muted, + root: fg, + file_icon: fg_muted, + folder_icon: blue, + empty_text: fg_muted, + }, + input: InputColors { + text: green, + cursor: fg, + hint: fg_muted, + }, + search: SearchColors { + active: green, + inactive: fg_muted, + match_bg: yellow, + match_fg: bg, + }, + markdown: MarkdownColors { + h1: green, + h2: cyan, + h3: purple, + code: bright_green, + code_border: Color::Rgb(28, 42, 28), + link: cyan, + link_url: Color::Rgb(56, 72, 52), + quote: fg_muted, + text: fg, + list: fg, + }, + engine: EngineColors { + header: green, + header_focused: bright_green, + divider: Color::Rgb(24, 36, 24), + scroll_indicator: Color::Rgb(36, 52, 36), + label: Color::Rgb(100, 120, 92), + label_focused: Color::Rgb(140, 160, 130), + label_dim: Color::Rgb(68, 84, 60), + value: Color::Rgb(180, 196, 172), + focused: bright_green, + normal: fg, + dim: Color::Rgb(36, 52, 36), + path: Color::Rgb(100, 120, 92), + border_magenta: purple, + border_green: green, + border_cyan: cyan, + separator: Color::Rgb(24, 36, 24), + hint_active: green, + hint_inactive: Color::Rgb(24, 36, 24), + }, + dict: DictColors { + word_name: bright_green, + word_bg: Color::Rgb(14, 24, 14), + alias: fg_muted, + stack_sig: purple, + description: fg, + example: Color::Rgb(100, 120, 92), + category_focused: bright_green, + category_selected: green, + category_normal: fg, + category_dimmed: Color::Rgb(36, 52, 36), + border_focused: bright_green, + border_normal: Color::Rgb(24, 36, 24), + header_desc: Color::Rgb(120, 140, 112), + }, + title: TitleColors { + big_title: green, + author: cyan, + link: bright_green, + license: yellow, + prompt: Color::Rgb(100, 120, 92), + subtitle: fg, + }, + meter: MeterColors { + low: green, + mid: yellow, + high: red, + low_rgb: (64, 192, 64), + mid_rgb: (160, 160, 64), + high_rgb: (192, 80, 64), + }, + sparkle: SparkleColors { + colors: [ + (64, 192, 64), + (96, 224, 96), + (80, 168, 144), + (160, 160, 64), + (48, 128, 48), + ], + }, + confirm: ConfirmColors { + border: red, + button_selected_bg: green, + button_selected_fg: bg, + }, + } +} diff --git a/crates/ratatui/src/theme/ember.rs b/crates/ratatui/src/theme/ember.rs new file mode 100644 index 0000000..0a4908b --- /dev/null +++ b/crates/ratatui/src/theme/ember.rs @@ -0,0 +1,280 @@ +use super::*; +use ratatui::style::Color; + +pub fn theme() -> ThemeColors { + let bg = Color::Rgb(10, 8, 8); + let surface = Color::Rgb(20, 16, 16); + let surface2 = Color::Rgb(26, 20, 20); + let border = Color::Rgb(42, 32, 32); + let fg = Color::Rgb(232, 221, 208); + let fg_dim = Color::Rgb(160, 144, 128); + let fg_muted = Color::Rgb(96, 80, 64); + + let red = Color::Rgb(224, 80, 64); + let orange = Color::Rgb(224, 128, 48); + let yellow = Color::Rgb(208, 160, 48); + let green = Color::Rgb(128, 160, 80); + let cyan = Color::Rgb(112, 160, 160); + let purple = Color::Rgb(160, 112, 144); + let blue = Color::Rgb(96, 128, 176); + + ThemeColors { + ui: UiColors { + bg, + bg_rgb: (10, 8, 8), + text_primary: fg, + text_muted: fg_dim, + text_dim: fg_muted, + border, + header: orange, + unfocused: fg_muted, + accent: orange, + surface, + }, + status: StatusColors { + playing_bg: Color::Rgb(20, 28, 16), + playing_fg: green, + stopped_bg: Color::Rgb(40, 16, 14), + stopped_fg: red, + fill_on: orange, + fill_off: fg_muted, + fill_bg: surface, + }, + selection: SelectionColors { + cursor_bg: orange, + cursor_fg: bg, + selected_bg: Color::Rgb(50, 35, 20), + selected_fg: orange, + in_range_bg: Color::Rgb(35, 25, 18), + in_range_fg: fg, + cursor: orange, + selected: Color::Rgb(50, 35, 20), + in_range: Color::Rgb(35, 25, 18), + }, + tile: TileColors { + playing_active_bg: Color::Rgb(45, 25, 15), + playing_active_fg: orange, + playing_inactive_bg: Color::Rgb(40, 32, 12), + playing_inactive_fg: yellow, + active_bg: Color::Rgb(20, 30, 30), + active_fg: cyan, + inactive_bg: surface, + inactive_fg: fg_dim, + active_selected_bg: Color::Rgb(40, 30, 35), + active_in_range_bg: Color::Rgb(30, 25, 25), + link_bright: [ + (224, 128, 48), + (224, 80, 64), + (208, 160, 48), + (112, 160, 160), + (128, 160, 80), + ], + link_dim: [ + (45, 28, 14), + (45, 18, 14), + (42, 32, 12), + (22, 32, 32), + (26, 32, 18), + ], + }, + header: HeaderColors { + tempo_bg: Color::Rgb(40, 28, 30), + tempo_fg: purple, + bank_bg: Color::Rgb(22, 30, 40), + bank_fg: blue, + pattern_bg: Color::Rgb(22, 34, 34), + pattern_fg: cyan, + stats_bg: surface, + stats_fg: fg_dim, + }, + modal: ModalColors { + border: orange, + border_accent: red, + border_warn: yellow, + border_dim: fg_muted, + confirm: red, + rename: orange, + input: cyan, + editor: orange, + preview: fg_muted, + }, + flash: FlashColors { + error_bg: Color::Rgb(50, 16, 14), + error_fg: red, + success_bg: Color::Rgb(20, 35, 18), + success_fg: green, + info_bg: surface, + info_fg: fg, + }, + list: ListColors { + playing_bg: Color::Rgb(20, 35, 18), + playing_fg: green, + staged_play_bg: Color::Rgb(35, 25, 30), + staged_play_fg: purple, + staged_stop_bg: Color::Rgb(45, 18, 16), + staged_stop_fg: red, + edit_bg: Color::Rgb(20, 32, 32), + edit_fg: cyan, + hover_bg: surface2, + hover_fg: fg, + muted_bg: Color::Rgb(18, 14, 14), + muted_fg: fg_muted, + soloed_bg: Color::Rgb(40, 32, 12), + soloed_fg: yellow, + }, + link_status: LinkStatusColors { + disabled: red, + connected: green, + listening: yellow, + }, + syntax: SyntaxColors { + gap_bg: bg, + executed_bg: Color::Rgb(25, 18, 14), + selected_bg: Color::Rgb(45, 30, 15), + emit: (fg, Color::Rgb(45, 20, 18)), + number: (yellow, Color::Rgb(40, 30, 12)), + string: (green, Color::Rgb(25, 30, 18)), + comment: (fg_muted, bg), + keyword: (red, Color::Rgb(42, 18, 15)), + stack_op: (blue, Color::Rgb(22, 28, 38)), + operator: (orange, Color::Rgb(42, 26, 12)), + sound: (cyan, Color::Rgb(22, 32, 32)), + param: (purple, Color::Rgb(32, 24, 28)), + context: (orange, Color::Rgb(42, 26, 12)), + note: (green, Color::Rgb(25, 30, 18)), + interval: (Color::Rgb(150, 180, 100), Color::Rgb(28, 34, 20)), + variable: (purple, Color::Rgb(32, 24, 28)), + vary: (yellow, Color::Rgb(40, 30, 12)), + generator: (cyan, Color::Rgb(22, 32, 30)), + default: (fg_dim, bg), + }, + table: TableColors { + row_even: bg, + row_odd: surface, + }, + values: ValuesColors { + tempo: orange, + value: fg_dim, + }, + hint: HintColors { + key: orange, + text: fg_muted, + }, + view_badge: ViewBadgeColors { bg: fg, fg: bg }, + nav: NavColors { + selected_bg: Color::Rgb(45, 30, 20), + selected_fg: fg, + unselected_bg: surface, + unselected_fg: fg_muted, + }, + editor_widget: EditorWidgetColors { + cursor_bg: fg, + cursor_fg: bg, + selection_bg: Color::Rgb(50, 35, 25), + completion_bg: surface, + completion_fg: fg, + completion_selected: orange, + completion_example: cyan, + }, + browser: BrowserColors { + directory: blue, + project_file: orange, + selected: red, + file: fg, + focused_border: orange, + unfocused_border: fg_muted, + root: fg, + file_icon: fg_muted, + folder_icon: blue, + empty_text: fg_muted, + }, + input: InputColors { + text: orange, + cursor: fg, + hint: fg_muted, + }, + search: SearchColors { + active: orange, + inactive: fg_muted, + match_bg: yellow, + match_fg: bg, + }, + markdown: MarkdownColors { + h1: orange, + h2: red, + h3: purple, + code: green, + code_border: Color::Rgb(50, 38, 38), + link: cyan, + link_url: Color::Rgb(80, 68, 56), + quote: fg_muted, + text: fg, + list: fg, + }, + engine: EngineColors { + header: orange, + header_focused: yellow, + divider: Color::Rgb(38, 30, 30), + scroll_indicator: Color::Rgb(55, 42, 42), + label: Color::Rgb(130, 115, 100), + label_focused: Color::Rgb(170, 155, 140), + label_dim: Color::Rgb(90, 76, 64), + value: Color::Rgb(200, 188, 175), + focused: yellow, + normal: fg, + dim: Color::Rgb(55, 42, 42), + path: Color::Rgb(130, 115, 100), + border_magenta: purple, + border_green: green, + border_cyan: cyan, + separator: Color::Rgb(38, 30, 30), + hint_active: orange, + hint_inactive: Color::Rgb(38, 30, 30), + }, + dict: DictColors { + word_name: green, + word_bg: Color::Rgb(22, 28, 22), + alias: fg_muted, + stack_sig: purple, + description: fg, + example: Color::Rgb(130, 115, 100), + category_focused: yellow, + category_selected: orange, + category_normal: fg, + category_dimmed: Color::Rgb(55, 42, 42), + border_focused: yellow, + border_normal: Color::Rgb(38, 30, 30), + header_desc: Color::Rgb(145, 130, 115), + }, + title: TitleColors { + big_title: orange, + author: red, + link: cyan, + license: yellow, + prompt: Color::Rgb(130, 115, 100), + subtitle: fg, + }, + meter: MeterColors { + low: green, + mid: yellow, + high: red, + low_rgb: (128, 160, 80), + mid_rgb: (208, 160, 48), + high_rgb: (224, 80, 64), + }, + sparkle: SparkleColors { + colors: [ + (224, 128, 48), + (224, 80, 64), + (208, 160, 48), + (112, 160, 160), + (128, 160, 80), + ], + }, + confirm: ConfirmColors { + border: red, + button_selected_bg: orange, + button_selected_fg: bg, + }, + } +} diff --git a/crates/ratatui/src/theme/georges.rs b/crates/ratatui/src/theme/georges.rs new file mode 100644 index 0000000..25854a2 --- /dev/null +++ b/crates/ratatui/src/theme/georges.rs @@ -0,0 +1,284 @@ +use super::*; +use ratatui::style::Color; + +// C64 palette on pure black +pub fn theme() -> ThemeColors { + let bg = Color::Rgb(0, 0, 0); + let surface = Color::Rgb(16, 16, 16); + let surface2 = Color::Rgb(24, 24, 24); + let border = Color::Rgb(51, 51, 51); + let fg = Color::Rgb(187, 187, 187); + let fg_dim = Color::Rgb(119, 119, 119); + let fg_muted = Color::Rgb(51, 51, 51); + + let white = Color::Rgb(255, 255, 255); + let lightred = Color::Rgb(255, 119, 119); + let cyan = Color::Rgb(170, 255, 238); + let violet = Color::Rgb(204, 68, 204); + let green = Color::Rgb(0, 204, 85); + let lightgreen = Color::Rgb(170, 255, 102); + let lightblue = Color::Rgb(0, 136, 255); + let yellow = Color::Rgb(238, 238, 119); + let orange = Color::Rgb(221, 136, 85); + let brown = Color::Rgb(102, 68, 0); + + ThemeColors { + ui: UiColors { + bg, + bg_rgb: (0, 0, 0), + text_primary: fg, + text_muted: fg_dim, + text_dim: fg_muted, + border, + header: lightblue, + unfocused: fg_muted, + accent: lightblue, + surface, + }, + status: StatusColors { + playing_bg: Color::Rgb(0, 24, 10), + playing_fg: green, + stopped_bg: Color::Rgb(28, 0, 0), + stopped_fg: lightred, + fill_on: lightblue, + fill_off: brown, + fill_bg: surface, + }, + selection: SelectionColors { + cursor_bg: lightblue, + cursor_fg: bg, + selected_bg: Color::Rgb(0, 20, 40), + selected_fg: cyan, + in_range_bg: Color::Rgb(0, 14, 28), + in_range_fg: fg, + cursor: lightblue, + selected: Color::Rgb(0, 20, 40), + in_range: Color::Rgb(0, 14, 28), + }, + tile: TileColors { + playing_active_bg: Color::Rgb(0, 30, 12), + playing_active_fg: lightgreen, + playing_inactive_bg: Color::Rgb(30, 30, 14), + playing_inactive_fg: yellow, + active_bg: Color::Rgb(0, 16, 32), + active_fg: lightblue, + inactive_bg: surface, + inactive_fg: fg_dim, + active_selected_bg: Color::Rgb(10, 20, 36), + active_in_range_bg: Color::Rgb(6, 14, 28), + link_bright: [ + (0, 136, 255), + (0, 204, 85), + (238, 238, 119), + (204, 68, 204), + (170, 255, 238), + ], + link_dim: [ + (0, 20, 40), + (0, 30, 12), + (34, 34, 16), + (30, 10, 30), + (24, 36, 34), + ], + }, + header: HeaderColors { + tempo_bg: Color::Rgb(28, 10, 28), + tempo_fg: violet, + bank_bg: Color::Rgb(0, 0, 24), + bank_fg: lightblue, + pattern_bg: Color::Rgb(0, 24, 10), + pattern_fg: green, + stats_bg: surface, + stats_fg: fg_dim, + }, + modal: ModalColors { + border: lightblue, + border_accent: cyan, + border_warn: yellow, + border_dim: fg_muted, + confirm: lightred, + rename: lightblue, + input: cyan, + editor: lightblue, + preview: fg_muted, + }, + flash: FlashColors { + error_bg: Color::Rgb(28, 0, 0), + error_fg: lightred, + success_bg: Color::Rgb(0, 24, 10), + success_fg: green, + info_bg: surface, + info_fg: fg, + }, + list: ListColors { + playing_bg: Color::Rgb(0, 24, 10), + playing_fg: green, + staged_play_bg: Color::Rgb(28, 10, 28), + staged_play_fg: violet, + staged_stop_bg: Color::Rgb(28, 0, 0), + staged_stop_fg: lightred, + edit_bg: Color::Rgb(0, 16, 32), + edit_fg: lightblue, + hover_bg: surface2, + hover_fg: fg, + muted_bg: Color::Rgb(8, 8, 8), + muted_fg: fg_muted, + soloed_bg: Color::Rgb(30, 30, 14), + soloed_fg: yellow, + }, + link_status: LinkStatusColors { + disabled: lightred, + connected: green, + listening: yellow, + }, + syntax: SyntaxColors { + gap_bg: bg, + executed_bg: Color::Rgb(0, 12, 24), + selected_bg: Color::Rgb(0, 20, 40), + emit: (white, Color::Rgb(20, 20, 20)), + number: (yellow, Color::Rgb(30, 30, 14)), + string: (lightgreen, Color::Rgb(18, 30, 12)), + comment: (fg_muted, bg), + keyword: (lightred, Color::Rgb(30, 14, 14)), + stack_op: (lightblue, Color::Rgb(0, 16, 32)), + operator: (cyan, Color::Rgb(20, 30, 28)), + sound: (green, Color::Rgb(0, 24, 10)), + param: (violet, Color::Rgb(24, 8, 24)), + context: (orange, Color::Rgb(28, 16, 10)), + note: (lightgreen, Color::Rgb(18, 30, 12)), + interval: (Color::Rgb(130, 210, 80), Color::Rgb(14, 26, 8)), + variable: (violet, Color::Rgb(24, 8, 24)), + vary: (yellow, Color::Rgb(30, 30, 14)), + generator: (cyan, Color::Rgb(20, 30, 28)), + default: (fg_dim, bg), + }, + table: TableColors { + row_even: bg, + row_odd: surface, + }, + values: ValuesColors { + tempo: lightblue, + value: fg_dim, + }, + hint: HintColors { + key: lightblue, + text: fg_muted, + }, + view_badge: ViewBadgeColors { bg: fg, fg: bg }, + nav: NavColors { + selected_bg: Color::Rgb(0, 20, 40), + selected_fg: fg, + unselected_bg: surface, + unselected_fg: fg_muted, + }, + editor_widget: EditorWidgetColors { + cursor_bg: fg, + cursor_fg: bg, + selection_bg: Color::Rgb(0, 24, 48), + completion_bg: surface, + completion_fg: fg, + completion_selected: lightblue, + completion_example: cyan, + }, + browser: BrowserColors { + directory: lightblue, + project_file: green, + selected: cyan, + file: fg, + focused_border: lightblue, + unfocused_border: fg_muted, + root: fg, + file_icon: fg_muted, + folder_icon: lightblue, + empty_text: fg_muted, + }, + input: InputColors { + text: lightblue, + cursor: fg, + hint: fg_muted, + }, + search: SearchColors { + active: lightblue, + inactive: fg_muted, + match_bg: yellow, + match_fg: bg, + }, + markdown: MarkdownColors { + h1: lightblue, + h2: green, + h3: violet, + code: lightgreen, + code_border: Color::Rgb(36, 36, 36), + link: cyan, + link_url: brown, + quote: fg_muted, + text: fg, + list: fg, + }, + engine: EngineColors { + header: lightblue, + header_focused: cyan, + divider: Color::Rgb(28, 28, 28), + scroll_indicator: Color::Rgb(51, 51, 51), + label: fg_dim, + label_focused: fg, + label_dim: fg_muted, + value: fg, + focused: cyan, + normal: fg, + dim: Color::Rgb(36, 36, 36), + path: fg_dim, + border_magenta: violet, + border_green: green, + border_cyan: cyan, + separator: Color::Rgb(28, 28, 28), + hint_active: lightblue, + hint_inactive: Color::Rgb(28, 28, 28), + }, + dict: DictColors { + word_name: lightgreen, + word_bg: Color::Rgb(10, 18, 6), + alias: fg_muted, + stack_sig: violet, + description: fg, + example: fg_dim, + category_focused: cyan, + category_selected: lightblue, + category_normal: fg, + category_dimmed: Color::Rgb(36, 36, 36), + border_focused: cyan, + border_normal: Color::Rgb(28, 28, 28), + header_desc: fg_dim, + }, + title: TitleColors { + big_title: lightblue, + author: green, + link: cyan, + license: yellow, + prompt: fg_dim, + subtitle: fg, + }, + meter: MeterColors { + low: green, + mid: yellow, + high: lightred, + low_rgb: (0, 204, 85), + mid_rgb: (238, 238, 119), + high_rgb: (255, 119, 119), + }, + sparkle: SparkleColors { + colors: [ + (0, 136, 255), + (170, 255, 238), + (0, 204, 85), + (238, 238, 119), + (204, 68, 204), + ], + }, + confirm: ConfirmColors { + border: lightred, + button_selected_bg: lightblue, + button_selected_fg: bg, + }, + } +} diff --git a/crates/ratatui/src/theme/letz_light.rs b/crates/ratatui/src/theme/letz_light.rs new file mode 100644 index 0000000..97537a3 --- /dev/null +++ b/crates/ratatui/src/theme/letz_light.rs @@ -0,0 +1,281 @@ +use super::*; +use ratatui::style::Color; + +pub fn theme() -> ThemeColors { + let bg = Color::Rgb(255, 255, 255); + let off_white = Color::Rgb(245, 245, 247); + let surface = Color::Rgb(235, 235, 240); + let border = Color::Rgb(210, 210, 215); + let text = Color::Rgb(29, 29, 31); + let text_dim = Color::Rgb(110, 110, 115); + let text_muted = Color::Rgb(160, 160, 165); + + let keyword = Color::Rgb(173, 61, 164); + let string = Color::Rgb(209, 47, 27); + let comment = Color::Rgb(112, 127, 52); + let number = Color::Rgb(39, 42, 216); + let types = Color::Rgb(112, 61, 170); + let function = Color::Rgb(62, 128, 135); + let preproc = Color::Rgb(120, 73, 42); + let accent = Color::Rgb(0, 112, 243); + + ThemeColors { + ui: UiColors { + bg, + bg_rgb: (255, 255, 255), + text_primary: text, + text_muted: text_dim, + text_dim: text_muted, + border, + header: accent, + unfocused: text_muted, + accent, + surface, + }, + status: StatusColors { + playing_bg: Color::Rgb(220, 240, 220), + playing_fg: comment, + stopped_bg: Color::Rgb(245, 220, 220), + stopped_fg: string, + fill_on: comment, + fill_off: text_muted, + fill_bg: surface, + }, + selection: SelectionColors { + cursor_bg: accent, + cursor_fg: bg, + selected_bg: Color::Rgb(200, 220, 250), + selected_fg: accent, + in_range_bg: Color::Rgb(220, 233, 250), + in_range_fg: text, + cursor: accent, + selected: Color::Rgb(200, 220, 250), + in_range: Color::Rgb(220, 233, 250), + }, + tile: TileColors { + playing_active_bg: Color::Rgb(250, 225, 210), + playing_active_fg: preproc, + playing_inactive_bg: Color::Rgb(250, 240, 200), + playing_inactive_fg: Color::Rgb(180, 140, 20), + active_bg: Color::Rgb(210, 235, 240), + active_fg: function, + inactive_bg: surface, + inactive_fg: text_dim, + active_selected_bg: Color::Rgb(210, 215, 245), + active_in_range_bg: Color::Rgb(220, 230, 245), + link_bright: [ + (173, 61, 164), + (0, 112, 243), + (120, 73, 42), + (62, 128, 135), + (112, 127, 52), + ], + link_dim: [ + (235, 215, 235), + (210, 225, 250), + (240, 225, 210), + (215, 235, 240), + (225, 235, 215), + ], + }, + header: HeaderColors { + tempo_bg: Color::Rgb(225, 215, 240), + tempo_fg: types, + bank_bg: Color::Rgb(210, 230, 250), + bank_fg: accent, + pattern_bg: Color::Rgb(210, 235, 235), + pattern_fg: function, + stats_bg: surface, + stats_fg: text_dim, + }, + modal: ModalColors { + border: accent, + border_accent: keyword, + border_warn: preproc, + border_dim: text_muted, + confirm: preproc, + rename: keyword, + input: accent, + editor: accent, + preview: text_muted, + }, + flash: FlashColors { + error_bg: Color::Rgb(250, 215, 215), + error_fg: string, + success_bg: Color::Rgb(215, 240, 215), + success_fg: comment, + info_bg: surface, + info_fg: text, + }, + list: ListColors { + playing_bg: Color::Rgb(215, 240, 220), + playing_fg: comment, + staged_play_bg: Color::Rgb(230, 215, 240), + staged_play_fg: keyword, + staged_stop_bg: Color::Rgb(245, 215, 220), + staged_stop_fg: string, + edit_bg: Color::Rgb(210, 235, 240), + edit_fg: function, + hover_bg: Color::Rgb(240, 240, 242), + hover_fg: text, + muted_bg: Color::Rgb(230, 230, 235), + muted_fg: text_muted, + soloed_bg: Color::Rgb(250, 240, 200), + soloed_fg: Color::Rgb(170, 130, 10), + }, + link_status: LinkStatusColors { + disabled: string, + connected: comment, + listening: Color::Rgb(180, 140, 20), + }, + syntax: SyntaxColors { + gap_bg: off_white, + executed_bg: Color::Rgb(230, 225, 245), + selected_bg: Color::Rgb(250, 240, 210), + emit: (text, Color::Rgb(250, 220, 215)), + number: (number, Color::Rgb(220, 220, 250)), + string: (string, Color::Rgb(250, 225, 220)), + comment: (Color::Rgb(130, 145, 75), off_white), + keyword: (keyword, Color::Rgb(240, 225, 240)), + stack_op: (accent, Color::Rgb(220, 235, 250)), + operator: (preproc, Color::Rgb(240, 230, 215)), + sound: (function, Color::Rgb(215, 240, 240)), + param: (types, Color::Rgb(230, 220, 240)), + context: (preproc, Color::Rgb(240, 230, 215)), + note: (comment, Color::Rgb(225, 240, 220)), + interval: (Color::Rgb(90, 110, 40), Color::Rgb(225, 240, 215)), + variable: (keyword, Color::Rgb(240, 225, 240)), + vary: (Color::Rgb(180, 140, 20), Color::Rgb(250, 240, 210)), + generator: (function, Color::Rgb(215, 240, 235)), + default: (text_dim, off_white), + }, + table: TableColors { + row_even: off_white, + row_odd: bg, + }, + values: ValuesColors { + tempo: preproc, + value: text_dim, + }, + hint: HintColors { + key: accent, + text: text_muted, + }, + view_badge: ViewBadgeColors { bg: text, fg: bg }, + nav: NavColors { + selected_bg: Color::Rgb(210, 225, 250), + selected_fg: text, + unselected_bg: surface, + unselected_fg: text_muted, + }, + editor_widget: EditorWidgetColors { + cursor_bg: text, + cursor_fg: bg, + selection_bg: Color::Rgb(200, 220, 250), + completion_bg: surface, + completion_fg: text, + completion_selected: accent, + completion_example: function, + }, + browser: BrowserColors { + directory: accent, + project_file: keyword, + selected: preproc, + file: text, + focused_border: accent, + unfocused_border: text_muted, + root: text, + file_icon: text_muted, + folder_icon: accent, + empty_text: text_muted, + }, + input: InputColors { + text: accent, + cursor: text, + hint: text_muted, + }, + search: SearchColors { + active: accent, + inactive: text_muted, + match_bg: Color::Rgb(255, 230, 80), + match_fg: text, + }, + markdown: MarkdownColors { + h1: accent, + h2: preproc, + h3: keyword, + code: comment, + code_border: Color::Rgb(200, 200, 205), + link: function, + link_url: Color::Rgb(150, 150, 155), + quote: text_muted, + text, + list: text, + }, + engine: EngineColors { + header: accent, + header_focused: preproc, + divider: Color::Rgb(200, 200, 205), + scroll_indicator: Color::Rgb(180, 180, 185), + label: Color::Rgb(120, 120, 125), + label_focused: Color::Rgb(80, 80, 85), + label_dim: Color::Rgb(150, 150, 155), + value: Color::Rgb(60, 60, 65), + focused: preproc, + normal: text, + dim: Color::Rgb(180, 180, 185), + path: Color::Rgb(120, 120, 125), + border_magenta: keyword, + border_green: comment, + border_cyan: function, + separator: Color::Rgb(200, 200, 210), + hint_active: preproc, + hint_inactive: Color::Rgb(200, 200, 210), + }, + dict: DictColors { + word_name: function, + word_bg: Color::Rgb(215, 235, 240), + alias: text_muted, + stack_sig: keyword, + description: text, + example: Color::Rgb(110, 110, 120), + category_focused: preproc, + category_selected: accent, + category_normal: text, + category_dimmed: Color::Rgb(180, 180, 185), + border_focused: preproc, + border_normal: Color::Rgb(200, 200, 205), + header_desc: Color::Rgb(100, 100, 110), + }, + title: TitleColors { + big_title: accent, + author: types, + link: function, + license: preproc, + prompt: Color::Rgb(100, 100, 110), + subtitle: text, + }, + meter: MeterColors { + low: comment, + mid: Color::Rgb(200, 150, 20), + high: string, + low_rgb: (112, 127, 52), + mid_rgb: (200, 150, 20), + high_rgb: (209, 47, 27), + }, + sparkle: SparkleColors { + colors: [ + (0, 112, 243), + (173, 61, 164), + (112, 127, 52), + (62, 128, 135), + (120, 73, 42), + ], + }, + confirm: ConfirmColors { + border: accent, + button_selected_bg: accent, + button_selected_fg: bg, + }, + } +} diff --git a/crates/ratatui/src/theme/mod.rs b/crates/ratatui/src/theme/mod.rs index 2721ce2..08b24ef 100644 --- a/crates/ratatui/src/theme/mod.rs +++ b/crates/ratatui/src/theme/mod.rs @@ -4,10 +4,14 @@ mod catppuccin_latte; mod catppuccin_mocha; mod dracula; +mod eden; +mod ember; +mod georges; mod fairyfloss; mod gruvbox_dark; mod hot_dog_stand; mod kanagawa; +mod letz_light; mod monochrome_black; mod monochrome_white; mod monokai; @@ -41,6 +45,10 @@ pub const THEMES: &[ThemeEntry] = &[ ThemeEntry { id: "Kanagawa", label: "Kanagawa", colors: kanagawa::theme }, ThemeEntry { id: "Fairyfloss", label: "Fairyfloss", colors: fairyfloss::theme }, ThemeEntry { id: "HotDogStand", label: "Hot Dog Stand", colors: hot_dog_stand::theme }, + ThemeEntry { id: "LetzLight", label: "Letz Light", colors: letz_light::theme }, + ThemeEntry { id: "Ember", label: "Ember", colors: ember::theme }, + ThemeEntry { id: "Eden", label: "Eden", colors: eden::theme }, + ThemeEntry { id: "Georges", label: "Georges", colors: georges::theme }, ]; thread_local! { diff --git a/src/app.rs b/src/app.rs index 583c41b..2a525ae 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,7 +4,9 @@ use rand::rngs::StdRng; use rand::SeedableRng; use std::collections::HashMap; use std::path::PathBuf; -use std::sync::Arc; +use std::sync::{Arc, LazyLock}; + +use cagire_ratatui::CompletionCandidate; use crossbeam_channel::Sender; @@ -25,6 +27,18 @@ use crate::state::{ const STEPS_PER_PAGE: usize = 32; +static COMPLETION_CANDIDATES: LazyLock> = LazyLock::new(|| { + model::WORDS + .iter() + .map(|w| CompletionCandidate { + name: w.name.to_string(), + signature: w.stack.to_string(), + description: w.desc.to_string(), + example: w.example.to_string(), + }) + .collect() +}); + pub struct App { pub project_state: ProjectState, pub ui: UiState, @@ -310,16 +324,7 @@ impl App { script.lines().map(String::from).collect() }; self.editor_ctx.editor.set_content(lines); - let candidates = model::WORDS - .iter() - .map(|w| cagire_ratatui::CompletionCandidate { - name: w.name.to_string(), - signature: w.stack.to_string(), - description: w.desc.to_string(), - example: w.example.to_string(), - }) - .collect(); - self.editor_ctx.editor.set_candidates(candidates); + self.editor_ctx.editor.set_candidates(COMPLETION_CANDIDATES.clone()); self.editor_ctx .editor .set_completion_enabled(self.ui.show_completion); @@ -350,16 +355,7 @@ impl App { prelude.lines().map(String::from).collect() }; self.editor_ctx.editor.set_content(lines); - let candidates = model::WORDS - .iter() - .map(|w| cagire_ratatui::CompletionCandidate { - name: w.name.to_string(), - signature: w.stack.to_string(), - description: w.desc.to_string(), - example: w.example.to_string(), - }) - .collect(); - self.editor_ctx.editor.set_candidates(candidates); + self.editor_ctx.editor.set_candidates(COMPLETION_CANDIDATES.clone()); self.editor_ctx .editor .set_completion_enabled(self.ui.show_completion); diff --git a/src/state/project.rs b/src/state/project.rs index aaa3b66..c3ae39e 100644 --- a/src/state/project.rs +++ b/src/state/project.rs @@ -7,6 +7,7 @@ pub struct ProjectState { pub project: Project, pub file_path: Option, dirty_patterns: [[bool; MAX_PATTERNS]; MAX_BANKS], + dirty_count: usize, } impl Default for ProjectState { @@ -15,6 +16,7 @@ impl Default for ProjectState { project: Project::default(), file_path: None, dirty_patterns: [[false; MAX_PATTERNS]; MAX_BANKS], + dirty_count: 0, }; state.mark_all_dirty(); state @@ -23,15 +25,22 @@ impl Default for ProjectState { impl ProjectState { pub fn mark_dirty(&mut self, bank: usize, pattern: usize) { - self.dirty_patterns[bank][pattern] = true; + if !self.dirty_patterns[bank][pattern] { + self.dirty_patterns[bank][pattern] = true; + self.dirty_count += 1; + } } pub fn mark_all_dirty(&mut self) { self.dirty_patterns = [[true; MAX_PATTERNS]; MAX_BANKS]; + self.dirty_count = MAX_BANKS * MAX_PATTERNS; } pub fn take_dirty(&mut self) -> Vec<(usize, usize)> { - let mut result = Vec::new(); + if self.dirty_count == 0 { + return Vec::new(); + } + let mut result = Vec::with_capacity(self.dirty_count); for (bank, patterns) in self.dirty_patterns.iter_mut().enumerate() { for (pattern, dirty) in patterns.iter_mut().enumerate() { if *dirty { @@ -40,6 +49,7 @@ impl ProjectState { } } } + self.dirty_count = 0; result } } diff --git a/src/views/render.rs b/src/views/render.rs index cf68e33..940a481 100644 --- a/src/views/render.rs +++ b/src/views/render.rs @@ -3,7 +3,7 @@ use std::time::{Duration, Instant}; use ratatui::layout::{Alignment, Constraint, Layout, Rect}; use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; -use ratatui::widgets::{Block, Borders, Cell, Padding, Paragraph, Row, Table}; +use ratatui::widgets::{Block, Borders, Cell, Clear, Padding, Paragraph, Row, Table}; use ratatui::Frame; use crate::app::App; @@ -50,12 +50,8 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, snapshot: &Sequenc let theme = theme::get(); let bg_color = theme.ui.bg; - let blank = " ".repeat(term.width as usize); - let lines: Vec = (0..term.height).map(|_| Line::raw(&blank)).collect(); - frame.render_widget( - Paragraph::new(lines).style(Style::default().bg(bg_color)), - term, - ); + frame.render_widget(Clear, term); + frame.render_widget(Block::new().style(Style::default().bg(bg_color)), term); if app.ui.show_title { title_view::render(frame, term, &app.ui);