diff --git a/crates/ratatui/src/confirm.rs b/crates/ratatui/src/confirm.rs index 2843685..6858ead 100644 --- a/crates/ratatui/src/confirm.rs +++ b/crates/ratatui/src/confirm.rs @@ -1,4 +1,4 @@ -use crate::theme::confirm; +use crate::theme; use ratatui::layout::{Alignment, Constraint, Layout, Rect}; use ratatui::style::Style; use ratatui::text::{Line, Span}; @@ -23,10 +23,11 @@ impl<'a> ConfirmModal<'a> { } pub fn render_centered(self, frame: &mut Frame, term: Rect) { + let t = theme::get(); let inner = ModalFrame::new(self.title) .width(30) .height(5) - .border_color(confirm::BORDER) + .border_color(t.confirm.border) .render_centered(frame, term); let rows = Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).split(inner); @@ -37,12 +38,12 @@ impl<'a> ConfirmModal<'a> { ); let yes_style = if self.selected { - Style::new().fg(confirm::BUTTON_SELECTED_FG).bg(confirm::BUTTON_SELECTED_BG) + Style::new().fg(t.confirm.button_selected_fg).bg(t.confirm.button_selected_bg) } else { Style::default() }; let no_style = if !self.selected { - Style::new().fg(confirm::BUTTON_SELECTED_FG).bg(confirm::BUTTON_SELECTED_BG) + Style::new().fg(t.confirm.button_selected_fg).bg(t.confirm.button_selected_bg) } else { Style::default() }; diff --git a/crates/ratatui/src/editor.rs b/crates/ratatui/src/editor.rs index eeaa5d8..d11d1c5 100644 --- a/crates/ratatui/src/editor.rs +++ b/crates/ratatui/src/editor.rs @@ -1,4 +1,4 @@ -use crate::theme::editor_widget; +use crate::theme; use ratatui::{ layout::Rect, style::{Modifier, Style}, @@ -333,9 +333,10 @@ impl Editor { } pub fn render(&self, frame: &mut Frame, area: Rect, highlighter: Highlighter) { + let t = theme::get(); let (cursor_row, cursor_col) = self.text.cursor(); - let cursor_style = Style::default().bg(editor_widget::CURSOR_BG).fg(editor_widget::CURSOR_FG); - let selection_style = Style::default().bg(editor_widget::SELECTION_BG); + let cursor_style = Style::default().bg(t.editor_widget.cursor_bg).fg(t.editor_widget.cursor_fg); + let selection_style = Style::default().bg(t.editor_widget.selection_bg); let selection = self.text.selection_range(); @@ -383,6 +384,7 @@ impl Editor { } fn render_completion(&self, frame: &mut Frame, editor_area: Rect, cursor_row: usize) { + let t = theme::get(); let max_visible: usize = 6; let list_width: u16 = 18; let doc_width: u16 = 40; @@ -413,9 +415,9 @@ impl Editor { let list_area = Rect::new(popup_x, popup_y, list_width, total_height); frame.render_widget(Clear, list_area); - let highlight_style = Style::default().fg(editor_widget::COMPLETION_SELECTED).add_modifier(Modifier::BOLD); - let normal_style = Style::default().fg(editor_widget::COMPLETION_FG); - let bg_style = Style::default().bg(editor_widget::COMPLETION_BG); + let highlight_style = Style::default().fg(t.editor_widget.completion_selected).add_modifier(Modifier::BOLD); + let normal_style = Style::default().fg(t.editor_widget.completion_fg); + let bg_style = Style::default().bg(t.editor_widget.completion_bg); let list_lines: Vec = (scroll_offset..scroll_offset + visible_count) .map(|i| { @@ -428,7 +430,7 @@ impl Editor { }; let prefix = if i == self.completion.cursor { "> " } else { " " }; let display = format!("{prefix}{name: = Vec::new(); diff --git a/crates/ratatui/src/modal.rs b/crates/ratatui/src/modal.rs index f09b565..0fe92ad 100644 --- a/crates/ratatui/src/modal.rs +++ b/crates/ratatui/src/modal.rs @@ -1,4 +1,4 @@ -use crate::theme::ui; +use crate::theme; use ratatui::layout::Rect; use ratatui::style::{Color, Style}; use ratatui::widgets::{Block, Borders, Clear, Paragraph}; @@ -8,7 +8,7 @@ pub struct ModalFrame<'a> { title: &'a str, width: u16, height: u16, - border_color: Color, + border_color: Option, } impl<'a> ModalFrame<'a> { @@ -17,7 +17,7 @@ impl<'a> ModalFrame<'a> { title, width: 40, height: 5, - border_color: ui::TEXT_PRIMARY, + border_color: None, } } @@ -32,11 +32,12 @@ impl<'a> ModalFrame<'a> { } pub fn border_color(mut self, c: Color) -> Self { - self.border_color = c; + self.border_color = Some(c); self } pub fn render_centered(&self, frame: &mut Frame, term: Rect) -> Rect { + let t = theme::get(); let width = self.width.min(term.width.saturating_sub(4)); let height = self.height.min(term.height.saturating_sub(4)); @@ -51,15 +52,16 @@ impl<'a> ModalFrame<'a> { for row in 0..area.height { let line_area = Rect::new(area.x, area.y + row, area.width, 1); frame.render_widget( - Paragraph::new(bg_fill.clone()).style(Style::new().bg(ui::BG)), + Paragraph::new(bg_fill.clone()).style(Style::new().bg(t.ui.bg)), line_area, ); } + let border_color = self.border_color.unwrap_or(t.ui.text_primary); let block = Block::default() .borders(Borders::ALL) .title(self.title) - .border_style(Style::new().fg(self.border_color)); + .border_style(Style::new().fg(border_color)); let inner = block.inner(area); frame.render_widget(block, area); diff --git a/crates/ratatui/src/nav_minimap.rs b/crates/ratatui/src/nav_minimap.rs index 01634d6..2c84a2b 100644 --- a/crates/ratatui/src/nav_minimap.rs +++ b/crates/ratatui/src/nav_minimap.rs @@ -1,4 +1,4 @@ -use crate::theme::{nav, ui}; +use crate::theme; use ratatui::layout::{Alignment, Rect}; use ratatui::style::Style; use ratatui::widgets::{Clear, Paragraph}; @@ -50,11 +50,12 @@ impl<'a> NavMinimap<'a> { frame.render_widget(Clear, area); // Fill background with theme color + let t = theme::get(); let bg_fill = " ".repeat(area.width as usize); for row in 0..area.height { let line_area = Rect::new(area.x, area.y + row, area.width, 1); frame.render_widget( - Paragraph::new(bg_fill.clone()).style(Style::new().bg(ui::BG)), + Paragraph::new(bg_fill.clone()).style(Style::new().bg(t.ui.bg)), line_area, ); } @@ -72,10 +73,11 @@ impl<'a> NavMinimap<'a> { } fn render_tile(&self, frame: &mut Frame, area: Rect, label: &str, is_selected: bool) { + let t = theme::get(); let (bg, fg) = if is_selected { - (nav::SELECTED_BG, nav::SELECTED_FG) + (t.nav.selected_bg, t.nav.selected_fg) } else { - (nav::UNSELECTED_BG, nav::UNSELECTED_FG) + (t.nav.unselected_bg, t.nav.unselected_fg) }; // Fill background diff --git a/crates/ratatui/src/theme.rs b/crates/ratatui/src/theme.rs index 2462126..e44fdb5 100644 --- a/crates/ratatui/src/theme.rs +++ b/crates/ratatui/src/theme.rs @@ -1,84 +1,2372 @@ //! Centralized color definitions for Cagire TUI. -//! Based on Catppuccin Mocha palette. +//! Supports multiple color schemes with runtime switching. use ratatui::style::Color; +use std::cell::RefCell; -// Catppuccin Mocha base palette -mod palette { - use super::*; - - // Backgrounds (dark to light) - pub const CRUST: Color = Color::Rgb(17, 17, 27); - pub const MANTLE: Color = Color::Rgb(24, 24, 37); - pub const BASE: Color = Color::Rgb(30, 30, 46); - pub const SURFACE0: Color = Color::Rgb(49, 50, 68); - pub const SURFACE1: Color = Color::Rgb(69, 71, 90); - pub const SURFACE2: Color = Color::Rgb(88, 91, 112); - - // Overlays - pub const OVERLAY0: Color = Color::Rgb(108, 112, 134); - pub const OVERLAY1: Color = Color::Rgb(127, 132, 156); - pub const OVERLAY2: Color = Color::Rgb(147, 153, 178); - - // Text (dim to bright) - pub const SUBTEXT0: Color = Color::Rgb(166, 173, 200); - pub const SUBTEXT1: Color = Color::Rgb(186, 194, 222); - pub const TEXT: Color = Color::Rgb(205, 214, 244); - - // Accent colors - pub const ROSEWATER: Color = Color::Rgb(245, 224, 220); - pub const FLAMINGO: Color = Color::Rgb(242, 205, 205); - pub const PINK: Color = Color::Rgb(245, 194, 231); - pub const MAUVE: Color = Color::Rgb(203, 166, 247); - pub const RED: Color = Color::Rgb(243, 139, 168); - pub const MAROON: Color = Color::Rgb(235, 160, 172); - pub const PEACH: Color = Color::Rgb(250, 179, 135); - pub const YELLOW: Color = Color::Rgb(249, 226, 175); - pub const GREEN: Color = Color::Rgb(166, 227, 161); - pub const TEAL: Color = Color::Rgb(148, 226, 213); - pub const SKY: Color = Color::Rgb(137, 220, 235); - pub const SAPPHIRE: Color = Color::Rgb(116, 199, 236); - pub const BLUE: Color = Color::Rgb(137, 180, 250); - pub const LAVENDER: Color = Color::Rgb(180, 190, 254); +// Thread-local current theme +thread_local! { + static CURRENT_THEME: RefCell = RefCell::new(ThemeColors::catppuccin_mocha()); } +/// Get the current theme colors +pub fn get() -> ThemeColors { + CURRENT_THEME.with(|t| t.borrow().clone()) +} + +/// Set the current theme colors +pub fn set(theme: ThemeColors) { + CURRENT_THEME.with(|t| *t.borrow_mut() = theme); +} + +#[derive(Clone)] +pub struct ThemeColors { + pub ui: UiColors, + pub status: StatusColors, + pub selection: SelectionColors, + pub tile: TileColors, + pub header: HeaderColors, + pub modal: ModalColors, + pub flash: FlashColors, + pub list: ListColors, + pub link_status: LinkStatusColors, + pub syntax: SyntaxColors, + pub table: TableColors, + pub values: ValuesColors, + pub hint: HintColors, + pub view_badge: ViewBadgeColors, + pub nav: NavColors, + pub editor_widget: EditorWidgetColors, + pub browser: BrowserColors, + pub input: InputColors, + pub search: SearchColors, + pub markdown: MarkdownColors, + pub engine: EngineColors, + pub dict: DictColors, + pub title: TitleColors, + pub meter: MeterColors, + pub sparkle: SparkleColors, + pub confirm: ConfirmColors, +} + +#[derive(Clone)] +pub struct UiColors { + pub bg: Color, + pub bg_rgb: (u8, u8, u8), + pub text_primary: Color, + pub text_muted: Color, + pub text_dim: Color, + pub border: Color, + pub header: Color, + pub unfocused: Color, + pub accent: Color, + pub surface: Color, +} + +#[derive(Clone)] +pub struct StatusColors { + pub playing_bg: Color, + pub playing_fg: Color, + pub stopped_bg: Color, + pub stopped_fg: Color, + pub fill_on: Color, + pub fill_off: Color, + pub fill_bg: Color, +} + +#[derive(Clone)] +pub struct SelectionColors { + pub cursor_bg: Color, + pub cursor_fg: Color, + pub selected_bg: Color, + pub selected_fg: Color, + pub in_range_bg: Color, + pub in_range_fg: Color, + pub cursor: Color, + pub selected: Color, + pub in_range: Color, +} + +#[derive(Clone)] +pub struct TileColors { + pub playing_active_bg: Color, + pub playing_active_fg: Color, + pub playing_inactive_bg: Color, + pub playing_inactive_fg: Color, + pub active_bg: Color, + pub active_fg: Color, + pub inactive_bg: Color, + pub inactive_fg: Color, + pub active_selected_bg: Color, + pub active_in_range_bg: Color, + pub link_bright: [(u8, u8, u8); 5], + pub link_dim: [(u8, u8, u8); 5], +} + +#[derive(Clone)] +pub struct HeaderColors { + pub tempo_bg: Color, + pub tempo_fg: Color, + pub bank_bg: Color, + pub bank_fg: Color, + pub pattern_bg: Color, + pub pattern_fg: Color, + pub stats_bg: Color, + pub stats_fg: Color, +} + +#[derive(Clone)] +pub struct ModalColors { + pub border: Color, + pub border_accent: Color, + pub border_warn: Color, + pub border_dim: Color, + pub confirm: Color, + pub rename: Color, + pub input: Color, + pub editor: Color, + pub preview: Color, +} + +#[derive(Clone)] +pub struct FlashColors { + pub error_bg: Color, + pub error_fg: Color, + pub success_bg: Color, + pub success_fg: Color, + pub info_bg: Color, + pub info_fg: Color, + pub event_rgb: (u8, u8, u8), +} + +#[derive(Clone)] +pub struct ListColors { + pub playing_bg: Color, + pub playing_fg: Color, + pub staged_play_bg: Color, + pub staged_play_fg: Color, + pub staged_stop_bg: Color, + pub staged_stop_fg: Color, + pub edit_bg: Color, + pub edit_fg: Color, + pub hover_bg: Color, + pub hover_fg: Color, +} + +#[derive(Clone)] +pub struct LinkStatusColors { + pub disabled: Color, + pub connected: Color, + pub listening: Color, +} + +#[derive(Clone)] +pub struct SyntaxColors { + pub gap_bg: Color, + pub executed_bg: Color, + pub selected_bg: Color, + pub emit: (Color, Color), + pub number: (Color, Color), + pub string: (Color, Color), + pub comment: (Color, Color), + pub keyword: (Color, Color), + pub stack_op: (Color, Color), + pub operator: (Color, Color), + pub sound: (Color, Color), + pub param: (Color, Color), + pub context: (Color, Color), + pub note: (Color, Color), + pub interval: (Color, Color), + pub variable: (Color, Color), + pub vary: (Color, Color), + pub generator: (Color, Color), + pub default: (Color, Color), +} + +#[derive(Clone)] +pub struct TableColors { + pub row_even: Color, + pub row_odd: Color, +} + +#[derive(Clone)] +pub struct ValuesColors { + pub tempo: Color, + pub value: Color, +} + +#[derive(Clone)] +pub struct HintColors { + pub key: Color, + pub text: Color, +} + +#[derive(Clone)] +pub struct ViewBadgeColors { + pub bg: Color, + pub fg: Color, +} + +#[derive(Clone)] +pub struct NavColors { + pub selected_bg: Color, + pub selected_fg: Color, + pub unselected_bg: Color, + pub unselected_fg: Color, +} + +#[derive(Clone)] +pub struct EditorWidgetColors { + pub cursor_bg: Color, + pub cursor_fg: Color, + pub selection_bg: Color, + pub completion_bg: Color, + pub completion_fg: Color, + pub completion_selected: Color, + pub completion_example: Color, +} + +#[derive(Clone)] +pub struct BrowserColors { + pub directory: Color, + pub project_file: Color, + pub selected: Color, + pub file: Color, + pub focused_border: Color, + pub unfocused_border: Color, + pub root: Color, + pub file_icon: Color, + pub folder_icon: Color, + pub empty_text: Color, +} + +#[derive(Clone)] +pub struct InputColors { + pub text: Color, + pub cursor: Color, + pub hint: Color, +} + +#[derive(Clone)] +pub struct SearchColors { + pub active: Color, + pub inactive: Color, + pub match_bg: Color, + pub match_fg: Color, +} + +#[derive(Clone)] +pub struct MarkdownColors { + pub h1: Color, + pub h2: Color, + pub h3: Color, + pub code: Color, + pub code_border: Color, + pub link: Color, + pub link_url: Color, + pub quote: Color, + pub text: Color, + pub list: Color, +} + +#[derive(Clone)] +pub struct EngineColors { + pub header: Color, + pub header_focused: Color, + pub divider: Color, + pub scroll_indicator: Color, + pub label: Color, + pub label_focused: Color, + pub label_dim: Color, + pub value: Color, + pub focused: Color, + pub normal: Color, + pub dim: Color, + pub path: Color, + pub border_magenta: Color, + pub border_green: Color, + pub border_cyan: Color, + pub separator: Color, + pub hint_active: Color, + pub hint_inactive: Color, +} + +#[derive(Clone)] +pub struct DictColors { + pub word_name: Color, + pub word_bg: Color, + pub alias: Color, + pub stack_sig: Color, + pub description: Color, + pub example: Color, + pub category_focused: Color, + pub category_selected: Color, + pub category_normal: Color, + pub category_dimmed: Color, + pub border_focused: Color, + pub border_normal: Color, + pub header_desc: Color, +} + +#[derive(Clone)] +pub struct TitleColors { + pub big_title: Color, + pub author: Color, + pub link: Color, + pub license: Color, + pub prompt: Color, + pub subtitle: Color, +} + +#[derive(Clone)] +pub struct MeterColors { + pub low: Color, + pub mid: Color, + pub high: Color, + pub low_rgb: (u8, u8, u8), + pub mid_rgb: (u8, u8, u8), + pub high_rgb: (u8, u8, u8), +} + +#[derive(Clone)] +pub struct SparkleColors { + pub colors: [(u8, u8, u8); 5], +} + +#[derive(Clone)] +pub struct ConfirmColors { + pub border: Color, + pub button_selected_bg: Color, + pub button_selected_fg: Color, +} + +impl ThemeColors { + pub fn catppuccin_mocha() -> Self { + // Catppuccin Mocha base palette + let crust = Color::Rgb(17, 17, 27); + let mantle = Color::Rgb(24, 24, 37); + let base = Color::Rgb(30, 30, 46); + let surface0 = Color::Rgb(49, 50, 68); + let surface1 = Color::Rgb(69, 71, 90); + let _surface2 = Color::Rgb(88, 91, 112); + let overlay0 = Color::Rgb(108, 112, 134); + let overlay1 = Color::Rgb(127, 132, 156); + let _overlay2 = Color::Rgb(147, 153, 178); + let subtext0 = Color::Rgb(166, 173, 200); + let subtext1 = Color::Rgb(186, 194, 222); + let text = Color::Rgb(205, 214, 244); + let _rosewater = Color::Rgb(245, 224, 220); + let _flamingo = Color::Rgb(242, 205, 205); + let pink = Color::Rgb(245, 194, 231); + let mauve = Color::Rgb(203, 166, 247); + let red = Color::Rgb(243, 139, 168); + let maroon = Color::Rgb(235, 160, 172); + let peach = Color::Rgb(250, 179, 135); + let yellow = Color::Rgb(249, 226, 175); + let green = Color::Rgb(166, 227, 161); + let teal = Color::Rgb(148, 226, 213); + let _sky = Color::Rgb(137, 220, 235); + let sapphire = Color::Rgb(116, 199, 236); + let _blue = Color::Rgb(137, 180, 250); + let lavender = Color::Rgb(180, 190, 254); + + Self { + ui: UiColors { + bg: base, + bg_rgb: (30, 30, 46), + text_primary: text, + text_muted: subtext0, + text_dim: overlay1, + border: surface1, + header: lavender, + unfocused: overlay0, + accent: mauve, + surface: surface0, + }, + status: StatusColors { + playing_bg: Color::Rgb(30, 50, 40), + playing_fg: green, + stopped_bg: Color::Rgb(50, 30, 40), + stopped_fg: red, + fill_on: green, + fill_off: overlay0, + fill_bg: surface0, + }, + selection: SelectionColors { + cursor_bg: mauve, + cursor_fg: crust, + selected_bg: Color::Rgb(60, 60, 90), + selected_fg: lavender, + in_range_bg: Color::Rgb(50, 50, 75), + in_range_fg: subtext1, + cursor: mauve, + selected: Color::Rgb(60, 60, 90), + in_range: Color::Rgb(50, 50, 75), + }, + tile: TileColors { + playing_active_bg: Color::Rgb(80, 50, 60), + playing_active_fg: peach, + playing_inactive_bg: Color::Rgb(70, 55, 45), + playing_inactive_fg: yellow, + active_bg: Color::Rgb(40, 55, 55), + active_fg: teal, + inactive_bg: surface0, + inactive_fg: subtext0, + active_selected_bg: Color::Rgb(70, 60, 80), + active_in_range_bg: Color::Rgb(55, 55, 70), + link_bright: [ + (203, 166, 247), // Mauve + (245, 194, 231), // Pink + (250, 179, 135), // Peach + (137, 220, 235), // Sky + (166, 227, 161), // Green + ], + link_dim: [ + (70, 55, 85), // Mauve dimmed + (85, 65, 80), // Pink dimmed + (85, 60, 45), // Peach dimmed + (45, 75, 80), // Sky dimmed + (55, 80, 55), // Green dimmed + ], + }, + header: HeaderColors { + tempo_bg: Color::Rgb(50, 40, 60), + tempo_fg: mauve, + bank_bg: Color::Rgb(35, 50, 55), + bank_fg: sapphire, + pattern_bg: Color::Rgb(40, 50, 50), + pattern_fg: teal, + stats_bg: surface0, + stats_fg: subtext0, + }, + modal: ModalColors { + border: lavender, + border_accent: mauve, + border_warn: peach, + border_dim: overlay1, + confirm: peach, + rename: mauve, + input: sapphire, + editor: lavender, + preview: overlay1, + }, + flash: FlashColors { + error_bg: Color::Rgb(50, 30, 40), + error_fg: red, + success_bg: Color::Rgb(30, 50, 40), + success_fg: green, + info_bg: surface0, + info_fg: text, + event_rgb: (55, 45, 70), + }, + list: ListColors { + playing_bg: Color::Rgb(35, 55, 45), + playing_fg: green, + staged_play_bg: Color::Rgb(55, 45, 65), + staged_play_fg: mauve, + staged_stop_bg: Color::Rgb(60, 40, 50), + staged_stop_fg: maroon, + edit_bg: Color::Rgb(40, 55, 55), + edit_fg: teal, + hover_bg: surface1, + hover_fg: text, + }, + link_status: LinkStatusColors { + disabled: red, + connected: green, + listening: yellow, + }, + syntax: SyntaxColors { + gap_bg: mantle, + executed_bg: Color::Rgb(45, 40, 55), + selected_bg: Color::Rgb(70, 55, 40), + emit: (text, Color::Rgb(80, 50, 60)), + number: (peach, Color::Rgb(55, 45, 35)), + string: (green, Color::Rgb(35, 50, 40)), + comment: (overlay1, crust), + keyword: (mauve, Color::Rgb(50, 40, 60)), + stack_op: (sapphire, Color::Rgb(35, 45, 55)), + operator: (yellow, Color::Rgb(55, 50, 35)), + sound: (teal, Color::Rgb(35, 55, 55)), + param: (lavender, Color::Rgb(45, 45, 60)), + context: (peach, Color::Rgb(55, 45, 35)), + note: (green, Color::Rgb(35, 50, 40)), + interval: (Color::Rgb(180, 230, 150), Color::Rgb(40, 55, 35)), + variable: (pink, Color::Rgb(55, 40, 55)), + vary: (yellow, Color::Rgb(55, 50, 35)), + generator: (teal, Color::Rgb(35, 55, 50)), + default: (subtext0, mantle), + }, + table: TableColors { + row_even: mantle, + row_odd: base, + }, + values: ValuesColors { + tempo: peach, + value: subtext0, + }, + hint: HintColors { + key: peach, + text: overlay1, + }, + view_badge: ViewBadgeColors { + bg: text, + fg: crust, + }, + nav: NavColors { + selected_bg: Color::Rgb(60, 50, 75), + selected_fg: text, + unselected_bg: surface0, + unselected_fg: overlay1, + }, + editor_widget: EditorWidgetColors { + cursor_bg: text, + cursor_fg: crust, + selection_bg: Color::Rgb(50, 60, 90), + completion_bg: surface0, + completion_fg: text, + completion_selected: peach, + completion_example: teal, + }, + browser: BrowserColors { + directory: sapphire, + project_file: mauve, + selected: peach, + file: text, + focused_border: peach, + unfocused_border: overlay0, + root: text, + file_icon: overlay1, + folder_icon: sapphire, + empty_text: overlay1, + }, + input: InputColors { + text: sapphire, + cursor: text, + hint: overlay1, + }, + search: SearchColors { + active: peach, + inactive: overlay0, + match_bg: yellow, + match_fg: crust, + }, + markdown: MarkdownColors { + h1: sapphire, + h2: peach, + h3: mauve, + code: green, + code_border: Color::Rgb(60, 60, 70), + link: teal, + link_url: Color::Rgb(100, 100, 100), + quote: overlay1, + text, + list: text, + }, + engine: EngineColors { + header: Color::Rgb(100, 160, 180), + header_focused: yellow, + divider: Color::Rgb(60, 65, 70), + scroll_indicator: Color::Rgb(80, 85, 95), + label: Color::Rgb(120, 125, 135), + label_focused: Color::Rgb(150, 155, 165), + label_dim: Color::Rgb(100, 105, 115), + value: Color::Rgb(180, 180, 190), + focused: yellow, + normal: text, + dim: Color::Rgb(80, 85, 95), + path: Color::Rgb(120, 125, 135), + border_magenta: mauve, + border_green: green, + border_cyan: sapphire, + separator: Color::Rgb(60, 65, 75), + hint_active: Color::Rgb(180, 180, 100), + hint_inactive: Color::Rgb(60, 60, 70), + }, + dict: DictColors { + word_name: green, + word_bg: Color::Rgb(40, 50, 60), + alias: overlay1, + stack_sig: mauve, + description: text, + example: Color::Rgb(120, 130, 140), + category_focused: yellow, + category_selected: sapphire, + category_normal: text, + category_dimmed: Color::Rgb(80, 80, 90), + border_focused: yellow, + border_normal: Color::Rgb(60, 60, 70), + header_desc: Color::Rgb(140, 145, 155), + }, + title: TitleColors { + big_title: mauve, + author: lavender, + link: teal, + license: peach, + prompt: Color::Rgb(140, 160, 170), + subtitle: text, + }, + meter: MeterColors { + low: green, + mid: yellow, + high: red, + low_rgb: (40, 180, 80), + mid_rgb: (220, 180, 40), + high_rgb: (220, 60, 40), + }, + sparkle: SparkleColors { + colors: [ + (200, 220, 255), // Lavender-ish + (250, 179, 135), // Peach + (166, 227, 161), // Green + (245, 194, 231), // Pink + (203, 166, 247), // Mauve + ], + }, + confirm: ConfirmColors { + border: peach, + button_selected_bg: peach, + button_selected_fg: crust, + }, + } + } + + pub fn catppuccin_latte() -> Self { + // Catppuccin Latte base palette (light theme) + let crust = Color::Rgb(220, 224, 232); + let mantle = Color::Rgb(230, 233, 239); + let base = Color::Rgb(239, 241, 245); + let surface0 = Color::Rgb(204, 208, 218); + let surface1 = Color::Rgb(188, 192, 204); + let _surface2 = Color::Rgb(172, 176, 190); + let overlay0 = Color::Rgb(156, 160, 176); + let overlay1 = Color::Rgb(140, 143, 161); + let _overlay2 = Color::Rgb(124, 127, 147); + let subtext0 = Color::Rgb(108, 111, 133); + let subtext1 = Color::Rgb(92, 95, 119); + let text = Color::Rgb(76, 79, 105); + let _rosewater = Color::Rgb(220, 138, 120); + let _flamingo = Color::Rgb(221, 120, 120); + let pink = Color::Rgb(234, 118, 203); + let mauve = Color::Rgb(136, 57, 239); + let red = Color::Rgb(210, 15, 57); + let maroon = Color::Rgb(230, 69, 83); + let peach = Color::Rgb(254, 100, 11); + let yellow = Color::Rgb(223, 142, 29); + let green = Color::Rgb(64, 160, 43); + let teal = Color::Rgb(23, 146, 153); + let _sky = Color::Rgb(4, 165, 229); + let sapphire = Color::Rgb(32, 159, 181); + let _blue = Color::Rgb(30, 102, 245); + let lavender = Color::Rgb(114, 135, 253); + + Self { + ui: UiColors { + bg: base, + bg_rgb: (239, 241, 245), + text_primary: text, + text_muted: subtext0, + text_dim: overlay1, + border: surface1, + header: lavender, + unfocused: overlay0, + accent: mauve, + surface: surface0, + }, + status: StatusColors { + playing_bg: Color::Rgb(220, 240, 225), + playing_fg: green, + stopped_bg: Color::Rgb(245, 220, 225), + stopped_fg: red, + fill_on: green, + fill_off: overlay0, + fill_bg: surface0, + }, + selection: SelectionColors { + cursor_bg: mauve, + cursor_fg: base, + selected_bg: Color::Rgb(200, 200, 230), + selected_fg: lavender, + in_range_bg: Color::Rgb(210, 210, 235), + in_range_fg: subtext1, + cursor: mauve, + selected: Color::Rgb(200, 200, 230), + in_range: Color::Rgb(210, 210, 235), + }, + tile: TileColors { + playing_active_bg: Color::Rgb(250, 220, 210), + playing_active_fg: peach, + playing_inactive_bg: Color::Rgb(250, 235, 200), + playing_inactive_fg: yellow, + active_bg: Color::Rgb(200, 235, 235), + active_fg: teal, + inactive_bg: surface0, + inactive_fg: subtext0, + active_selected_bg: Color::Rgb(215, 210, 240), + active_in_range_bg: Color::Rgb(210, 215, 230), + link_bright: [ + (136, 57, 239), // Mauve + (234, 118, 203), // Pink + (254, 100, 11), // Peach + (4, 165, 229), // Sky + (64, 160, 43), // Green + ], + link_dim: [ + (210, 200, 240), // Mauve dimmed + (240, 210, 230), // Pink dimmed + (250, 220, 200), // Peach dimmed + (200, 230, 240), // Sky dimmed + (210, 235, 210), // Green dimmed + ], + }, + header: HeaderColors { + tempo_bg: Color::Rgb(220, 210, 240), + tempo_fg: mauve, + bank_bg: Color::Rgb(200, 230, 235), + bank_fg: sapphire, + pattern_bg: Color::Rgb(200, 230, 225), + pattern_fg: teal, + stats_bg: surface0, + stats_fg: subtext0, + }, + modal: ModalColors { + border: lavender, + border_accent: mauve, + border_warn: peach, + border_dim: overlay1, + confirm: peach, + rename: mauve, + input: sapphire, + editor: lavender, + preview: overlay1, + }, + flash: FlashColors { + error_bg: Color::Rgb(250, 215, 220), + error_fg: red, + success_bg: Color::Rgb(210, 240, 215), + success_fg: green, + info_bg: surface0, + info_fg: text, + event_rgb: (225, 215, 240), + }, + list: ListColors { + playing_bg: Color::Rgb(210, 235, 220), + playing_fg: green, + staged_play_bg: Color::Rgb(225, 215, 245), + staged_play_fg: mauve, + staged_stop_bg: Color::Rgb(245, 215, 225), + staged_stop_fg: maroon, + edit_bg: Color::Rgb(210, 235, 235), + edit_fg: teal, + hover_bg: surface1, + hover_fg: text, + }, + link_status: LinkStatusColors { + disabled: red, + connected: green, + listening: yellow, + }, + syntax: SyntaxColors { + gap_bg: mantle, + executed_bg: Color::Rgb(225, 220, 240), + selected_bg: Color::Rgb(250, 235, 210), + emit: (text, Color::Rgb(250, 220, 215)), + number: (peach, Color::Rgb(252, 235, 220)), + string: (green, Color::Rgb(215, 240, 215)), + comment: (overlay1, crust), + keyword: (mauve, Color::Rgb(230, 220, 245)), + stack_op: (sapphire, Color::Rgb(215, 230, 240)), + operator: (yellow, Color::Rgb(245, 235, 210)), + sound: (teal, Color::Rgb(210, 240, 240)), + param: (lavender, Color::Rgb(220, 225, 245)), + context: (peach, Color::Rgb(252, 235, 220)), + note: (green, Color::Rgb(215, 240, 215)), + interval: (Color::Rgb(50, 140, 30), Color::Rgb(215, 240, 210)), + variable: (pink, Color::Rgb(245, 220, 240)), + vary: (yellow, Color::Rgb(245, 235, 210)), + generator: (teal, Color::Rgb(210, 240, 235)), + default: (subtext0, mantle), + }, + table: TableColors { + row_even: mantle, + row_odd: base, + }, + values: ValuesColors { + tempo: peach, + value: subtext0, + }, + hint: HintColors { + key: peach, + text: overlay1, + }, + view_badge: ViewBadgeColors { + bg: text, + fg: base, + }, + nav: NavColors { + selected_bg: Color::Rgb(215, 205, 245), + selected_fg: text, + unselected_bg: surface0, + unselected_fg: overlay1, + }, + editor_widget: EditorWidgetColors { + cursor_bg: text, + cursor_fg: base, + selection_bg: Color::Rgb(200, 210, 240), + completion_bg: surface0, + completion_fg: text, + completion_selected: peach, + completion_example: teal, + }, + browser: BrowserColors { + directory: sapphire, + project_file: mauve, + selected: peach, + file: text, + focused_border: peach, + unfocused_border: overlay0, + root: text, + file_icon: overlay1, + folder_icon: sapphire, + empty_text: overlay1, + }, + input: InputColors { + text: sapphire, + cursor: text, + hint: overlay1, + }, + search: SearchColors { + active: peach, + inactive: overlay0, + match_bg: yellow, + match_fg: base, + }, + markdown: MarkdownColors { + h1: sapphire, + h2: peach, + h3: mauve, + code: green, + code_border: Color::Rgb(190, 195, 205), + link: teal, + link_url: Color::Rgb(150, 150, 150), + quote: overlay1, + text, + list: text, + }, + engine: EngineColors { + header: Color::Rgb(30, 120, 150), + header_focused: yellow, + divider: Color::Rgb(180, 185, 195), + scroll_indicator: Color::Rgb(160, 165, 175), + label: Color::Rgb(100, 105, 120), + label_focused: Color::Rgb(70, 75, 90), + label_dim: Color::Rgb(120, 125, 140), + value: Color::Rgb(60, 65, 80), + focused: yellow, + normal: text, + dim: Color::Rgb(160, 165, 175), + path: Color::Rgb(100, 105, 120), + border_magenta: mauve, + border_green: green, + border_cyan: sapphire, + separator: Color::Rgb(180, 185, 200), + hint_active: Color::Rgb(180, 140, 40), + hint_inactive: Color::Rgb(190, 195, 205), + }, + dict: DictColors { + word_name: green, + word_bg: Color::Rgb(210, 225, 235), + alias: overlay1, + stack_sig: mauve, + description: text, + example: Color::Rgb(100, 105, 115), + category_focused: yellow, + category_selected: sapphire, + category_normal: text, + category_dimmed: Color::Rgb(160, 165, 175), + border_focused: yellow, + border_normal: Color::Rgb(180, 185, 195), + header_desc: Color::Rgb(90, 95, 110), + }, + title: TitleColors { + big_title: mauve, + author: lavender, + link: teal, + license: peach, + prompt: Color::Rgb(90, 100, 115), + subtitle: text, + }, + meter: MeterColors { + low: green, + mid: yellow, + high: red, + low_rgb: (50, 150, 40), + mid_rgb: (200, 140, 30), + high_rgb: (200, 40, 50), + }, + sparkle: SparkleColors { + colors: [ + (114, 135, 253), // Lavender + (254, 100, 11), // Peach + (64, 160, 43), // Green + (234, 118, 203), // Pink + (136, 57, 239), // Mauve + ], + }, + confirm: ConfirmColors { + border: peach, + button_selected_bg: peach, + button_selected_fg: base, + }, + } + } + + pub fn nord() -> Self { + // Nord color palette + let polar_night0 = Color::Rgb(46, 52, 64); // nord0 + let polar_night1 = Color::Rgb(59, 66, 82); // nord1 + let polar_night2 = Color::Rgb(67, 76, 94); // nord2 + let polar_night3 = Color::Rgb(76, 86, 106); // nord3 + let snow_storm0 = Color::Rgb(216, 222, 233); // nord4 + let _snow_storm1 = Color::Rgb(229, 233, 240); // nord5 + let snow_storm2 = Color::Rgb(236, 239, 244); // nord6 + let frost0 = Color::Rgb(143, 188, 187); // nord7 (teal) + let frost1 = Color::Rgb(136, 192, 208); // nord8 (light blue) + let frost2 = Color::Rgb(129, 161, 193); // nord9 (blue) + let _frost3 = Color::Rgb(94, 129, 172); // nord10 (dark blue) + let aurora_red = Color::Rgb(191, 97, 106); // nord11 + let aurora_orange = Color::Rgb(208, 135, 112); // nord12 + let aurora_yellow = Color::Rgb(235, 203, 139); // nord13 + let aurora_green = Color::Rgb(163, 190, 140); // nord14 + let aurora_purple = Color::Rgb(180, 142, 173); // nord15 + + Self { + ui: UiColors { + bg: polar_night0, + bg_rgb: (46, 52, 64), + text_primary: snow_storm2, + text_muted: snow_storm0, + text_dim: polar_night3, + border: polar_night2, + header: frost1, + unfocused: polar_night3, + accent: frost1, + surface: polar_night1, + }, + status: StatusColors { + playing_bg: Color::Rgb(50, 65, 60), + playing_fg: aurora_green, + stopped_bg: Color::Rgb(65, 50, 55), + stopped_fg: aurora_red, + fill_on: aurora_green, + fill_off: polar_night3, + fill_bg: polar_night1, + }, + selection: SelectionColors { + cursor_bg: frost1, + cursor_fg: polar_night0, + selected_bg: Color::Rgb(70, 85, 105), + selected_fg: frost1, + in_range_bg: Color::Rgb(60, 70, 90), + in_range_fg: snow_storm0, + cursor: frost1, + selected: Color::Rgb(70, 85, 105), + in_range: Color::Rgb(60, 70, 90), + }, + tile: TileColors { + playing_active_bg: Color::Rgb(80, 70, 65), + playing_active_fg: aurora_orange, + playing_inactive_bg: Color::Rgb(75, 70, 55), + playing_inactive_fg: aurora_yellow, + active_bg: Color::Rgb(50, 65, 65), + active_fg: frost0, + inactive_bg: polar_night1, + inactive_fg: snow_storm0, + active_selected_bg: Color::Rgb(75, 75, 95), + active_in_range_bg: Color::Rgb(60, 70, 85), + link_bright: [ + (136, 192, 208), // Frost1 + (180, 142, 173), // Aurora purple + (208, 135, 112), // Aurora orange + (143, 188, 187), // Frost0 + (163, 190, 140), // Aurora green + ], + link_dim: [ + (55, 75, 85), // Frost1 dimmed + (70, 60, 70), // Purple dimmed + (75, 55, 50), // Orange dimmed + (55, 75, 75), // Frost0 dimmed + (60, 75, 55), // Green dimmed + ], + }, + header: HeaderColors { + tempo_bg: Color::Rgb(65, 55, 70), + tempo_fg: aurora_purple, + bank_bg: Color::Rgb(45, 60, 70), + bank_fg: frost2, + pattern_bg: Color::Rgb(50, 65, 65), + pattern_fg: frost0, + stats_bg: polar_night1, + stats_fg: snow_storm0, + }, + modal: ModalColors { + border: frost1, + border_accent: aurora_purple, + border_warn: aurora_orange, + border_dim: polar_night3, + confirm: aurora_orange, + rename: aurora_purple, + input: frost2, + editor: frost1, + preview: polar_night3, + }, + flash: FlashColors { + error_bg: Color::Rgb(65, 50, 55), + error_fg: aurora_red, + success_bg: Color::Rgb(50, 65, 55), + success_fg: aurora_green, + info_bg: polar_night1, + info_fg: snow_storm2, + event_rgb: (60, 55, 75), + }, + list: ListColors { + playing_bg: Color::Rgb(50, 65, 55), + playing_fg: aurora_green, + staged_play_bg: Color::Rgb(65, 55, 70), + staged_play_fg: aurora_purple, + staged_stop_bg: Color::Rgb(70, 55, 60), + staged_stop_fg: aurora_red, + edit_bg: Color::Rgb(50, 65, 65), + edit_fg: frost0, + hover_bg: polar_night2, + hover_fg: snow_storm2, + }, + link_status: LinkStatusColors { + disabled: aurora_red, + connected: aurora_green, + listening: aurora_yellow, + }, + syntax: SyntaxColors { + gap_bg: polar_night1, + executed_bg: Color::Rgb(55, 55, 70), + selected_bg: Color::Rgb(80, 70, 55), + emit: (snow_storm2, Color::Rgb(75, 55, 60)), + number: (aurora_orange, Color::Rgb(65, 55, 50)), + string: (aurora_green, Color::Rgb(50, 60, 50)), + comment: (polar_night3, polar_night0), + keyword: (aurora_purple, Color::Rgb(60, 50, 65)), + stack_op: (frost2, Color::Rgb(45, 55, 70)), + operator: (aurora_yellow, Color::Rgb(65, 60, 45)), + sound: (frost0, Color::Rgb(45, 60, 60)), + param: (frost1, Color::Rgb(50, 60, 70)), + context: (aurora_orange, Color::Rgb(65, 55, 50)), + note: (aurora_green, Color::Rgb(50, 60, 50)), + interval: (Color::Rgb(170, 200, 150), Color::Rgb(50, 60, 45)), + variable: (aurora_purple, Color::Rgb(60, 50, 60)), + vary: (aurora_yellow, Color::Rgb(65, 60, 45)), + generator: (frost0, Color::Rgb(45, 60, 55)), + default: (snow_storm0, polar_night1), + }, + table: TableColors { + row_even: polar_night1, + row_odd: polar_night0, + }, + values: ValuesColors { + tempo: aurora_orange, + value: snow_storm0, + }, + hint: HintColors { + key: aurora_orange, + text: polar_night3, + }, + view_badge: ViewBadgeColors { + bg: snow_storm2, + fg: polar_night0, + }, + nav: NavColors { + selected_bg: Color::Rgb(65, 75, 95), + selected_fg: snow_storm2, + unselected_bg: polar_night1, + unselected_fg: polar_night3, + }, + editor_widget: EditorWidgetColors { + cursor_bg: snow_storm2, + cursor_fg: polar_night0, + selection_bg: Color::Rgb(60, 75, 100), + completion_bg: polar_night1, + completion_fg: snow_storm2, + completion_selected: aurora_orange, + completion_example: frost0, + }, + browser: BrowserColors { + directory: frost2, + project_file: aurora_purple, + selected: aurora_orange, + file: snow_storm2, + focused_border: aurora_orange, + unfocused_border: polar_night3, + root: snow_storm2, + file_icon: polar_night3, + folder_icon: frost2, + empty_text: polar_night3, + }, + input: InputColors { + text: frost2, + cursor: snow_storm2, + hint: polar_night3, + }, + search: SearchColors { + active: aurora_orange, + inactive: polar_night3, + match_bg: aurora_yellow, + match_fg: polar_night0, + }, + markdown: MarkdownColors { + h1: frost2, + h2: aurora_orange, + h3: aurora_purple, + code: aurora_green, + code_border: Color::Rgb(75, 85, 100), + link: frost0, + link_url: Color::Rgb(100, 110, 125), + quote: polar_night3, + text: snow_storm2, + list: snow_storm2, + }, + engine: EngineColors { + header: frost1, + header_focused: aurora_yellow, + divider: Color::Rgb(70, 80, 95), + scroll_indicator: Color::Rgb(85, 95, 110), + label: Color::Rgb(130, 140, 155), + label_focused: Color::Rgb(160, 170, 185), + label_dim: Color::Rgb(100, 110, 125), + value: Color::Rgb(190, 200, 215), + focused: aurora_yellow, + normal: snow_storm2, + dim: Color::Rgb(85, 95, 110), + path: Color::Rgb(130, 140, 155), + border_magenta: aurora_purple, + border_green: aurora_green, + border_cyan: frost2, + separator: Color::Rgb(70, 80, 95), + hint_active: Color::Rgb(200, 180, 100), + hint_inactive: Color::Rgb(70, 80, 95), + }, + dict: DictColors { + word_name: aurora_green, + word_bg: Color::Rgb(50, 60, 75), + alias: polar_night3, + stack_sig: aurora_purple, + description: snow_storm2, + example: Color::Rgb(130, 140, 155), + category_focused: aurora_yellow, + category_selected: frost2, + category_normal: snow_storm2, + category_dimmed: Color::Rgb(85, 95, 110), + border_focused: aurora_yellow, + border_normal: Color::Rgb(70, 80, 95), + header_desc: Color::Rgb(150, 160, 175), + }, + title: TitleColors { + big_title: frost1, + author: frost2, + link: frost0, + license: aurora_orange, + prompt: Color::Rgb(150, 160, 175), + subtitle: snow_storm2, + }, + meter: MeterColors { + low: aurora_green, + mid: aurora_yellow, + high: aurora_red, + low_rgb: (140, 180, 130), + mid_rgb: (220, 190, 120), + high_rgb: (180, 90, 100), + }, + sparkle: SparkleColors { + colors: [ + (136, 192, 208), // Frost1 + (208, 135, 112), // Aurora orange + (163, 190, 140), // Aurora green + (180, 142, 173), // Aurora purple + (235, 203, 139), // Aurora yellow + ], + }, + confirm: ConfirmColors { + border: aurora_orange, + button_selected_bg: aurora_orange, + button_selected_fg: polar_night0, + }, + } + } + + pub fn dracula() -> Self { + // Dracula color palette + let background = Color::Rgb(40, 42, 54); + let current_line = Color::Rgb(68, 71, 90); + let foreground = Color::Rgb(248, 248, 242); + let comment = Color::Rgb(98, 114, 164); + let cyan = Color::Rgb(139, 233, 253); + let green = Color::Rgb(80, 250, 123); + let orange = Color::Rgb(255, 184, 108); + let pink = Color::Rgb(255, 121, 198); + let purple = Color::Rgb(189, 147, 249); + let red = Color::Rgb(255, 85, 85); + let yellow = Color::Rgb(241, 250, 140); + + let darker_bg = Color::Rgb(33, 34, 44); + let lighter_bg = Color::Rgb(55, 57, 70); + + Self { + ui: UiColors { + bg: background, + bg_rgb: (40, 42, 54), + text_primary: foreground, + text_muted: comment, + text_dim: Color::Rgb(80, 85, 110), + border: current_line, + header: purple, + unfocused: comment, + accent: purple, + surface: current_line, + }, + status: StatusColors { + playing_bg: Color::Rgb(40, 60, 50), + playing_fg: green, + stopped_bg: Color::Rgb(65, 45, 50), + stopped_fg: red, + fill_on: green, + fill_off: comment, + fill_bg: current_line, + }, + selection: SelectionColors { + cursor_bg: purple, + cursor_fg: background, + selected_bg: Color::Rgb(80, 75, 110), + selected_fg: purple, + in_range_bg: Color::Rgb(65, 65, 90), + in_range_fg: foreground, + cursor: purple, + selected: Color::Rgb(80, 75, 110), + in_range: Color::Rgb(65, 65, 90), + }, + tile: TileColors { + playing_active_bg: Color::Rgb(85, 60, 65), + playing_active_fg: orange, + playing_inactive_bg: Color::Rgb(80, 75, 55), + playing_inactive_fg: yellow, + active_bg: Color::Rgb(50, 70, 70), + active_fg: cyan, + inactive_bg: current_line, + inactive_fg: comment, + active_selected_bg: Color::Rgb(80, 70, 95), + active_in_range_bg: Color::Rgb(65, 65, 85), + link_bright: [ + (189, 147, 249), // Purple + (255, 121, 198), // Pink + (255, 184, 108), // Orange + (139, 233, 253), // Cyan + (80, 250, 123), // Green + ], + link_dim: [ + (75, 60, 95), // Purple dimmed + (95, 55, 80), // Pink dimmed + (95, 70, 50), // Orange dimmed + (55, 90, 95), // Cyan dimmed + (40, 95, 55), // Green dimmed + ], + }, + header: HeaderColors { + tempo_bg: Color::Rgb(65, 50, 75), + tempo_fg: purple, + bank_bg: Color::Rgb(45, 65, 70), + bank_fg: cyan, + pattern_bg: Color::Rgb(40, 70, 60), + pattern_fg: green, + stats_bg: current_line, + stats_fg: comment, + }, + modal: ModalColors { + border: purple, + border_accent: pink, + border_warn: orange, + border_dim: comment, + confirm: orange, + rename: purple, + input: cyan, + editor: purple, + preview: comment, + }, + flash: FlashColors { + error_bg: Color::Rgb(70, 45, 50), + error_fg: red, + success_bg: Color::Rgb(40, 65, 50), + success_fg: green, + info_bg: current_line, + info_fg: foreground, + event_rgb: (70, 55, 85), + }, + list: ListColors { + playing_bg: Color::Rgb(40, 65, 50), + playing_fg: green, + staged_play_bg: Color::Rgb(70, 55, 85), + staged_play_fg: purple, + staged_stop_bg: Color::Rgb(80, 50, 60), + staged_stop_fg: red, + edit_bg: Color::Rgb(45, 70, 70), + edit_fg: cyan, + hover_bg: lighter_bg, + hover_fg: foreground, + }, + link_status: LinkStatusColors { + disabled: red, + connected: green, + listening: yellow, + }, + syntax: SyntaxColors { + gap_bg: darker_bg, + executed_bg: Color::Rgb(55, 50, 70), + selected_bg: Color::Rgb(85, 70, 50), + emit: (foreground, Color::Rgb(85, 55, 65)), + number: (orange, Color::Rgb(75, 55, 45)), + string: (yellow, Color::Rgb(70, 70, 45)), + comment: (comment, darker_bg), + keyword: (pink, Color::Rgb(80, 50, 70)), + stack_op: (cyan, Color::Rgb(45, 65, 75)), + operator: (green, Color::Rgb(40, 70, 50)), + sound: (cyan, Color::Rgb(45, 70, 70)), + param: (purple, Color::Rgb(60, 50, 75)), + context: (orange, Color::Rgb(75, 55, 45)), + note: (green, Color::Rgb(40, 70, 50)), + interval: (Color::Rgb(120, 255, 150), Color::Rgb(40, 75, 50)), + variable: (pink, Color::Rgb(80, 50, 65)), + vary: (yellow, Color::Rgb(70, 70, 45)), + generator: (cyan, Color::Rgb(45, 70, 65)), + default: (comment, darker_bg), + }, + table: TableColors { + row_even: darker_bg, + row_odd: background, + }, + values: ValuesColors { + tempo: orange, + value: comment, + }, + hint: HintColors { + key: orange, + text: comment, + }, + view_badge: ViewBadgeColors { + bg: foreground, + fg: background, + }, + nav: NavColors { + selected_bg: Color::Rgb(75, 65, 100), + selected_fg: foreground, + unselected_bg: current_line, + unselected_fg: comment, + }, + editor_widget: EditorWidgetColors { + cursor_bg: foreground, + cursor_fg: background, + selection_bg: Color::Rgb(70, 75, 105), + completion_bg: current_line, + completion_fg: foreground, + completion_selected: orange, + completion_example: cyan, + }, + browser: BrowserColors { + directory: cyan, + project_file: purple, + selected: orange, + file: foreground, + focused_border: orange, + unfocused_border: comment, + root: foreground, + file_icon: comment, + folder_icon: cyan, + empty_text: comment, + }, + input: InputColors { + text: cyan, + cursor: foreground, + hint: comment, + }, + search: SearchColors { + active: orange, + inactive: comment, + match_bg: yellow, + match_fg: background, + }, + markdown: MarkdownColors { + h1: cyan, + h2: orange, + h3: purple, + code: green, + code_border: Color::Rgb(85, 90, 110), + link: pink, + link_url: Color::Rgb(120, 130, 150), + quote: comment, + text: foreground, + list: foreground, + }, + engine: EngineColors { + header: cyan, + header_focused: yellow, + divider: Color::Rgb(80, 85, 105), + scroll_indicator: Color::Rgb(95, 100, 120), + label: Color::Rgb(140, 145, 165), + label_focused: Color::Rgb(170, 175, 195), + label_dim: Color::Rgb(110, 115, 135), + value: Color::Rgb(200, 205, 220), + focused: yellow, + normal: foreground, + dim: Color::Rgb(95, 100, 120), + path: Color::Rgb(140, 145, 165), + border_magenta: pink, + border_green: green, + border_cyan: cyan, + separator: Color::Rgb(80, 85, 105), + hint_active: Color::Rgb(220, 200, 100), + hint_inactive: Color::Rgb(80, 85, 105), + }, + dict: DictColors { + word_name: green, + word_bg: Color::Rgb(55, 65, 80), + alias: comment, + stack_sig: purple, + description: foreground, + example: Color::Rgb(140, 145, 165), + category_focused: yellow, + category_selected: cyan, + category_normal: foreground, + category_dimmed: Color::Rgb(95, 100, 120), + border_focused: yellow, + border_normal: Color::Rgb(80, 85, 105), + header_desc: Color::Rgb(160, 165, 185), + }, + title: TitleColors { + big_title: purple, + author: pink, + link: cyan, + license: orange, + prompt: Color::Rgb(160, 165, 185), + subtitle: foreground, + }, + meter: MeterColors { + low: green, + mid: yellow, + high: red, + low_rgb: (70, 230, 110), + mid_rgb: (230, 240, 130), + high_rgb: (240, 80, 80), + }, + sparkle: SparkleColors { + colors: [ + (189, 147, 249), // Purple + (255, 184, 108), // Orange + (80, 250, 123), // Green + (255, 121, 198), // Pink + (139, 233, 253), // Cyan + ], + }, + confirm: ConfirmColors { + border: orange, + button_selected_bg: orange, + button_selected_fg: background, + }, + } + } + + pub fn gruvbox_dark() -> Self { + // Gruvbox Dark palette + let bg0 = Color::Rgb(40, 40, 40); // #282828 + let bg1 = Color::Rgb(60, 56, 54); // #3c3836 + let bg2 = Color::Rgb(80, 73, 69); // #504945 + let _bg3 = Color::Rgb(102, 92, 84); // #665c54 + let fg = Color::Rgb(235, 219, 178); // #ebdbb2 + let fg2 = Color::Rgb(213, 196, 161); // #d5c4a1 + let fg3 = Color::Rgb(189, 174, 147); // #bdae93 + let fg4 = Color::Rgb(168, 153, 132); // #a89984 + let red = Color::Rgb(251, 73, 52); // #fb4934 + let green = Color::Rgb(184, 187, 38); // #b8bb26 + let yellow = Color::Rgb(250, 189, 47); // #fabd2f + let blue = Color::Rgb(131, 165, 152); // #83a598 + let purple = Color::Rgb(211, 134, 155); // #d3869b + let aqua = Color::Rgb(142, 192, 124); // #8ec07c + let orange = Color::Rgb(254, 128, 25); // #fe8019 + + let darker_bg = Color::Rgb(29, 32, 33); // #1d2021 + + Self { + ui: UiColors { + bg: bg0, + bg_rgb: (40, 40, 40), + text_primary: fg, + text_muted: fg3, + text_dim: fg4, + border: bg2, + header: yellow, + unfocused: fg4, + accent: orange, + surface: bg1, + }, + status: StatusColors { + playing_bg: Color::Rgb(50, 60, 45), + playing_fg: green, + stopped_bg: Color::Rgb(65, 45, 45), + stopped_fg: red, + fill_on: green, + fill_off: fg4, + fill_bg: bg1, + }, + selection: SelectionColors { + cursor_bg: orange, + cursor_fg: bg0, + selected_bg: Color::Rgb(80, 70, 55), + selected_fg: yellow, + in_range_bg: Color::Rgb(65, 60, 50), + in_range_fg: fg2, + cursor: orange, + selected: Color::Rgb(80, 70, 55), + in_range: Color::Rgb(65, 60, 50), + }, + tile: TileColors { + playing_active_bg: Color::Rgb(90, 65, 50), + playing_active_fg: orange, + playing_inactive_bg: Color::Rgb(80, 75, 45), + playing_inactive_fg: yellow, + active_bg: Color::Rgb(50, 65, 55), + active_fg: aqua, + inactive_bg: bg1, + inactive_fg: fg3, + active_selected_bg: Color::Rgb(85, 70, 60), + active_in_range_bg: Color::Rgb(70, 65, 55), + link_bright: [ + (254, 128, 25), // Orange + (211, 134, 155), // Purple + (250, 189, 47), // Yellow + (131, 165, 152), // Blue + (184, 187, 38), // Green + ], + link_dim: [ + (85, 55, 35), // Orange dimmed + (75, 55, 65), // Purple dimmed + (80, 70, 40), // Yellow dimmed + (50, 60, 60), // Blue dimmed + (60, 65, 35), // Green dimmed + ], + }, + header: HeaderColors { + tempo_bg: Color::Rgb(75, 55, 40), + tempo_fg: orange, + bank_bg: Color::Rgb(50, 60, 60), + bank_fg: blue, + pattern_bg: Color::Rgb(50, 65, 50), + pattern_fg: aqua, + stats_bg: bg1, + stats_fg: fg3, + }, + modal: ModalColors { + border: yellow, + border_accent: orange, + border_warn: red, + border_dim: fg4, + confirm: orange, + rename: purple, + input: blue, + editor: yellow, + preview: fg4, + }, + flash: FlashColors { + error_bg: Color::Rgb(70, 45, 45), + error_fg: red, + success_bg: Color::Rgb(50, 65, 45), + success_fg: green, + info_bg: bg1, + info_fg: fg, + event_rgb: (70, 55, 45), + }, + list: ListColors { + playing_bg: Color::Rgb(50, 65, 45), + playing_fg: green, + staged_play_bg: Color::Rgb(70, 55, 60), + staged_play_fg: purple, + staged_stop_bg: Color::Rgb(75, 50, 50), + staged_stop_fg: red, + edit_bg: Color::Rgb(50, 65, 55), + edit_fg: aqua, + hover_bg: bg2, + hover_fg: fg, + }, + link_status: LinkStatusColors { + disabled: red, + connected: green, + listening: yellow, + }, + syntax: SyntaxColors { + gap_bg: darker_bg, + executed_bg: Color::Rgb(55, 50, 45), + selected_bg: Color::Rgb(85, 70, 45), + emit: (fg, Color::Rgb(80, 55, 50)), + number: (orange, Color::Rgb(70, 50, 40)), + string: (green, Color::Rgb(50, 60, 40)), + comment: (fg4, darker_bg), + keyword: (red, Color::Rgb(70, 45, 45)), + stack_op: (blue, Color::Rgb(50, 55, 60)), + operator: (yellow, Color::Rgb(70, 65, 40)), + sound: (aqua, Color::Rgb(45, 60, 50)), + param: (purple, Color::Rgb(65, 50, 55)), + context: (orange, Color::Rgb(70, 50, 40)), + note: (green, Color::Rgb(50, 60, 40)), + interval: (Color::Rgb(170, 200, 100), Color::Rgb(55, 65, 40)), + variable: (purple, Color::Rgb(65, 50, 55)), + vary: (yellow, Color::Rgb(70, 65, 40)), + generator: (aqua, Color::Rgb(45, 60, 50)), + default: (fg3, darker_bg), + }, + table: TableColors { + row_even: darker_bg, + row_odd: bg0, + }, + values: ValuesColors { + tempo: orange, + value: fg3, + }, + hint: HintColors { + key: orange, + text: fg4, + }, + view_badge: ViewBadgeColors { + bg: fg, + fg: bg0, + }, + nav: NavColors { + selected_bg: Color::Rgb(80, 65, 50), + selected_fg: fg, + unselected_bg: bg1, + unselected_fg: fg4, + }, + editor_widget: EditorWidgetColors { + cursor_bg: fg, + cursor_fg: bg0, + selection_bg: Color::Rgb(70, 65, 55), + completion_bg: bg1, + completion_fg: fg, + completion_selected: orange, + completion_example: aqua, + }, + browser: BrowserColors { + directory: blue, + project_file: purple, + selected: orange, + file: fg, + focused_border: orange, + unfocused_border: fg4, + root: fg, + file_icon: fg4, + folder_icon: blue, + empty_text: fg4, + }, + input: InputColors { + text: blue, + cursor: fg, + hint: fg4, + }, + search: SearchColors { + active: orange, + inactive: fg4, + match_bg: yellow, + match_fg: bg0, + }, + markdown: MarkdownColors { + h1: blue, + h2: orange, + h3: purple, + code: green, + code_border: Color::Rgb(80, 75, 70), + link: aqua, + link_url: Color::Rgb(120, 115, 105), + quote: fg4, + text: fg, + list: fg, + }, + engine: EngineColors { + header: blue, + header_focused: yellow, + divider: Color::Rgb(75, 70, 65), + scroll_indicator: Color::Rgb(90, 85, 80), + label: Color::Rgb(145, 135, 125), + label_focused: Color::Rgb(175, 165, 155), + label_dim: Color::Rgb(115, 105, 95), + value: Color::Rgb(200, 190, 175), + focused: yellow, + normal: fg, + dim: Color::Rgb(90, 85, 80), + path: Color::Rgb(145, 135, 125), + border_magenta: purple, + border_green: green, + border_cyan: aqua, + separator: Color::Rgb(75, 70, 65), + hint_active: Color::Rgb(220, 180, 80), + hint_inactive: Color::Rgb(75, 70, 65), + }, + dict: DictColors { + word_name: green, + word_bg: Color::Rgb(55, 60, 55), + alias: fg4, + stack_sig: purple, + description: fg, + example: Color::Rgb(145, 135, 125), + category_focused: yellow, + category_selected: blue, + category_normal: fg, + category_dimmed: Color::Rgb(90, 85, 80), + border_focused: yellow, + border_normal: Color::Rgb(75, 70, 65), + header_desc: Color::Rgb(165, 155, 145), + }, + title: TitleColors { + big_title: orange, + author: yellow, + link: aqua, + license: purple, + prompt: Color::Rgb(165, 155, 145), + subtitle: fg, + }, + meter: MeterColors { + low: green, + mid: yellow, + high: red, + low_rgb: (170, 175, 35), + mid_rgb: (235, 180, 45), + high_rgb: (240, 70, 50), + }, + sparkle: SparkleColors { + colors: [ + (250, 189, 47), // Yellow + (254, 128, 25), // Orange + (184, 187, 38), // Green + (211, 134, 155), // Purple + (131, 165, 152), // Blue + ], + }, + confirm: ConfirmColors { + border: orange, + button_selected_bg: orange, + button_selected_fg: bg0, + }, + } + } + + pub fn monokai() -> Self { + // Monokai palette + let bg = Color::Rgb(39, 40, 34); // #272822 + let bg_light = Color::Rgb(53, 54, 47); // #35362f + let bg_lighter = Color::Rgb(70, 71, 62); + let fg = Color::Rgb(248, 248, 242); // #f8f8f2 + let fg_dim = Color::Rgb(190, 190, 180); + let comment = Color::Rgb(117, 113, 94); // #75715e + let pink = Color::Rgb(249, 38, 114); // #f92672 + let green = Color::Rgb(166, 226, 46); // #a6e22e + let yellow = Color::Rgb(230, 219, 116); // #e6db74 + let blue = Color::Rgb(102, 217, 239); // #66d9ef + let purple = Color::Rgb(174, 129, 255); // #ae81ff + let orange = Color::Rgb(253, 151, 31); // #fd971f + + let darker_bg = Color::Rgb(30, 31, 26); + + Self { + ui: UiColors { + bg, + bg_rgb: (39, 40, 34), + text_primary: fg, + text_muted: fg_dim, + text_dim: comment, + border: bg_lighter, + header: blue, + unfocused: comment, + accent: pink, + surface: bg_light, + }, + status: StatusColors { + playing_bg: Color::Rgb(50, 65, 40), + playing_fg: green, + stopped_bg: Color::Rgb(70, 40, 55), + stopped_fg: pink, + fill_on: green, + fill_off: comment, + fill_bg: bg_light, + }, + selection: SelectionColors { + cursor_bg: pink, + cursor_fg: bg, + selected_bg: Color::Rgb(85, 70, 80), + selected_fg: pink, + in_range_bg: Color::Rgb(70, 65, 70), + in_range_fg: fg, + cursor: pink, + selected: Color::Rgb(85, 70, 80), + in_range: Color::Rgb(70, 65, 70), + }, + tile: TileColors { + playing_active_bg: Color::Rgb(90, 65, 45), + playing_active_fg: orange, + playing_inactive_bg: Color::Rgb(80, 75, 50), + playing_inactive_fg: yellow, + active_bg: Color::Rgb(55, 75, 70), + active_fg: blue, + inactive_bg: bg_light, + inactive_fg: fg_dim, + active_selected_bg: Color::Rgb(85, 65, 80), + active_in_range_bg: Color::Rgb(70, 65, 70), + link_bright: [ + (249, 38, 114), // Pink + (174, 129, 255), // Purple + (253, 151, 31), // Orange + (102, 217, 239), // Blue + (166, 226, 46), // Green + ], + link_dim: [ + (90, 40, 60), // Pink dimmed + (70, 55, 90), // Purple dimmed + (85, 60, 35), // Orange dimmed + (50, 75, 85), // Blue dimmed + (60, 80, 40), // Green dimmed + ], + }, + header: HeaderColors { + tempo_bg: Color::Rgb(75, 50, 65), + tempo_fg: pink, + bank_bg: Color::Rgb(50, 70, 75), + bank_fg: blue, + pattern_bg: Color::Rgb(55, 75, 50), + pattern_fg: green, + stats_bg: bg_light, + stats_fg: fg_dim, + }, + modal: ModalColors { + border: blue, + border_accent: pink, + border_warn: orange, + border_dim: comment, + confirm: orange, + rename: purple, + input: blue, + editor: blue, + preview: comment, + }, + flash: FlashColors { + error_bg: Color::Rgb(75, 40, 55), + error_fg: pink, + success_bg: Color::Rgb(50, 70, 45), + success_fg: green, + info_bg: bg_light, + info_fg: fg, + event_rgb: (70, 55, 70), + }, + list: ListColors { + playing_bg: Color::Rgb(50, 70, 45), + playing_fg: green, + staged_play_bg: Color::Rgb(70, 55, 80), + staged_play_fg: purple, + staged_stop_bg: Color::Rgb(80, 45, 60), + staged_stop_fg: pink, + edit_bg: Color::Rgb(50, 70, 70), + edit_fg: blue, + hover_bg: bg_lighter, + hover_fg: fg, + }, + link_status: LinkStatusColors { + disabled: pink, + connected: green, + listening: yellow, + }, + syntax: SyntaxColors { + gap_bg: darker_bg, + executed_bg: Color::Rgb(55, 50, 55), + selected_bg: Color::Rgb(85, 75, 50), + emit: (fg, Color::Rgb(85, 55, 65)), + number: (purple, Color::Rgb(60, 50, 75)), + string: (yellow, Color::Rgb(70, 65, 45)), + comment: (comment, darker_bg), + keyword: (pink, Color::Rgb(80, 45, 60)), + stack_op: (blue, Color::Rgb(50, 70, 75)), + operator: (pink, Color::Rgb(80, 45, 60)), + sound: (blue, Color::Rgb(50, 70, 75)), + param: (orange, Color::Rgb(80, 60, 40)), + context: (orange, Color::Rgb(80, 60, 40)), + note: (green, Color::Rgb(55, 75, 45)), + interval: (Color::Rgb(180, 235, 80), Color::Rgb(55, 75, 40)), + variable: (green, Color::Rgb(55, 75, 45)), + vary: (yellow, Color::Rgb(70, 65, 45)), + generator: (blue, Color::Rgb(50, 70, 70)), + default: (fg_dim, darker_bg), + }, + table: TableColors { + row_even: darker_bg, + row_odd: bg, + }, + values: ValuesColors { + tempo: orange, + value: fg_dim, + }, + hint: HintColors { + key: orange, + text: comment, + }, + view_badge: ViewBadgeColors { + bg: fg, + fg: bg, + }, + nav: NavColors { + selected_bg: Color::Rgb(80, 60, 75), + selected_fg: fg, + unselected_bg: bg_light, + unselected_fg: comment, + }, + editor_widget: EditorWidgetColors { + cursor_bg: fg, + cursor_fg: bg, + selection_bg: Color::Rgb(75, 70, 75), + completion_bg: bg_light, + completion_fg: fg, + completion_selected: orange, + completion_example: blue, + }, + browser: BrowserColors { + directory: blue, + project_file: purple, + selected: orange, + file: fg, + focused_border: orange, + unfocused_border: comment, + root: fg, + file_icon: comment, + folder_icon: blue, + empty_text: comment, + }, + input: InputColors { + text: blue, + cursor: fg, + hint: comment, + }, + search: SearchColors { + active: orange, + inactive: comment, + match_bg: yellow, + match_fg: bg, + }, + markdown: MarkdownColors { + h1: blue, + h2: orange, + h3: purple, + code: green, + code_border: Color::Rgb(85, 85, 75), + link: pink, + link_url: Color::Rgb(130, 125, 115), + quote: comment, + text: fg, + list: fg, + }, + engine: EngineColors { + header: blue, + header_focused: yellow, + divider: Color::Rgb(80, 80, 72), + scroll_indicator: Color::Rgb(95, 95, 88), + label: Color::Rgb(150, 145, 135), + label_focused: Color::Rgb(180, 175, 165), + label_dim: Color::Rgb(120, 115, 105), + value: Color::Rgb(210, 205, 195), + focused: yellow, + normal: fg, + dim: Color::Rgb(95, 95, 88), + path: Color::Rgb(150, 145, 135), + border_magenta: pink, + border_green: green, + border_cyan: blue, + separator: Color::Rgb(80, 80, 72), + hint_active: Color::Rgb(220, 200, 100), + hint_inactive: Color::Rgb(80, 80, 72), + }, + dict: DictColors { + word_name: green, + word_bg: Color::Rgb(55, 65, 60), + alias: comment, + stack_sig: purple, + description: fg, + example: Color::Rgb(150, 145, 135), + category_focused: yellow, + category_selected: blue, + category_normal: fg, + category_dimmed: Color::Rgb(95, 95, 88), + border_focused: yellow, + border_normal: Color::Rgb(80, 80, 72), + header_desc: Color::Rgb(170, 165, 155), + }, + title: TitleColors { + big_title: pink, + author: blue, + link: green, + license: orange, + prompt: Color::Rgb(170, 165, 155), + subtitle: fg, + }, + meter: MeterColors { + low: green, + mid: yellow, + high: pink, + low_rgb: (155, 215, 45), + mid_rgb: (220, 210, 105), + high_rgb: (240, 50, 110), + }, + sparkle: SparkleColors { + colors: [ + (102, 217, 239), // Blue + (253, 151, 31), // Orange + (166, 226, 46), // Green + (249, 38, 114), // Pink + (174, 129, 255), // Purple + ], + }, + confirm: ConfirmColors { + border: orange, + button_selected_bg: orange, + button_selected_fg: bg, + }, + } + } + + pub fn pitch_black() -> Self { + // Pitch Black (OLED) palette - pure black background with high contrast + let bg = Color::Rgb(0, 0, 0); // Pure black + let surface = Color::Rgb(10, 10, 10); // Very subtle surface + let surface2 = Color::Rgb(21, 21, 21); // Slightly visible surface + let border = Color::Rgb(40, 40, 40); // Subtle borders + let fg = Color::Rgb(230, 230, 230); // Bright white text + let fg_dim = Color::Rgb(160, 160, 160); + let fg_muted = Color::Rgb(100, 100, 100); + + // High contrast accent colors + let red = Color::Rgb(255, 80, 80); + let green = Color::Rgb(80, 255, 120); + let yellow = Color::Rgb(255, 230, 80); + let blue = Color::Rgb(80, 180, 255); + let purple = Color::Rgb(200, 120, 255); + let cyan = Color::Rgb(80, 230, 230); + let orange = Color::Rgb(255, 160, 60); + + Self { + ui: UiColors { + bg, + bg_rgb: (0, 0, 0), + text_primary: fg, + text_muted: fg_dim, + text_dim: fg_muted, + border, + header: blue, + unfocused: fg_muted, + accent: cyan, + surface, + }, + status: StatusColors { + playing_bg: Color::Rgb(15, 35, 20), + playing_fg: green, + stopped_bg: Color::Rgb(40, 15, 20), + stopped_fg: red, + fill_on: green, + fill_off: fg_muted, + fill_bg: surface, + }, + selection: SelectionColors { + cursor_bg: cyan, + cursor_fg: bg, + selected_bg: Color::Rgb(40, 50, 60), + selected_fg: cyan, + in_range_bg: Color::Rgb(25, 35, 45), + in_range_fg: fg, + cursor: cyan, + selected: Color::Rgb(40, 50, 60), + in_range: Color::Rgb(25, 35, 45), + }, + tile: TileColors { + playing_active_bg: Color::Rgb(50, 35, 20), + playing_active_fg: orange, + playing_inactive_bg: Color::Rgb(45, 40, 15), + playing_inactive_fg: yellow, + active_bg: Color::Rgb(15, 40, 40), + active_fg: cyan, + inactive_bg: surface, + inactive_fg: fg_dim, + active_selected_bg: Color::Rgb(45, 40, 55), + active_in_range_bg: Color::Rgb(30, 35, 45), + link_bright: [ + (80, 230, 230), // Cyan + (200, 120, 255), // Purple + (255, 160, 60), // Orange + (80, 180, 255), // Blue + (80, 255, 120), // Green + ], + link_dim: [ + (25, 60, 60), // Cyan dimmed + (50, 35, 65), // Purple dimmed + (60, 45, 20), // Orange dimmed + (25, 50, 70), // Blue dimmed + (25, 65, 35), // Green dimmed + ], + }, + header: HeaderColors { + tempo_bg: Color::Rgb(50, 35, 55), + tempo_fg: purple, + bank_bg: Color::Rgb(20, 45, 60), + bank_fg: blue, + pattern_bg: Color::Rgb(20, 55, 50), + pattern_fg: cyan, + stats_bg: surface, + stats_fg: fg_dim, + }, + modal: ModalColors { + border: cyan, + border_accent: purple, + border_warn: orange, + border_dim: fg_muted, + confirm: orange, + rename: purple, + input: blue, + editor: cyan, + preview: fg_muted, + }, + flash: FlashColors { + error_bg: Color::Rgb(50, 15, 20), + error_fg: red, + success_bg: Color::Rgb(15, 45, 25), + success_fg: green, + info_bg: surface, + info_fg: fg, + event_rgb: (40, 30, 50), + }, + list: ListColors { + playing_bg: Color::Rgb(15, 45, 25), + playing_fg: green, + staged_play_bg: Color::Rgb(45, 30, 55), + staged_play_fg: purple, + staged_stop_bg: Color::Rgb(55, 25, 30), + staged_stop_fg: red, + edit_bg: Color::Rgb(15, 45, 45), + edit_fg: cyan, + hover_bg: surface2, + hover_fg: fg, + }, + link_status: LinkStatusColors { + disabled: red, + connected: green, + listening: yellow, + }, + syntax: SyntaxColors { + gap_bg: bg, + executed_bg: Color::Rgb(25, 25, 35), + selected_bg: Color::Rgb(55, 45, 25), + emit: (fg, Color::Rgb(50, 30, 35)), + number: (orange, Color::Rgb(50, 35, 20)), + string: (green, Color::Rgb(20, 45, 25)), + comment: (fg_muted, bg), + keyword: (purple, Color::Rgb(40, 25, 50)), + stack_op: (blue, Color::Rgb(20, 40, 55)), + operator: (yellow, Color::Rgb(50, 45, 20)), + sound: (cyan, Color::Rgb(20, 45, 45)), + param: (purple, Color::Rgb(40, 25, 50)), + context: (orange, Color::Rgb(50, 35, 20)), + note: (green, Color::Rgb(20, 45, 25)), + interval: (Color::Rgb(130, 255, 150), Color::Rgb(25, 55, 35)), + variable: (purple, Color::Rgb(40, 25, 50)), + vary: (yellow, Color::Rgb(50, 45, 20)), + generator: (cyan, Color::Rgb(20, 45, 40)), + 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(40, 45, 55), + selected_fg: fg, + unselected_bg: surface, + unselected_fg: fg_muted, + }, + editor_widget: EditorWidgetColors { + cursor_bg: fg, + cursor_fg: bg, + selection_bg: Color::Rgb(40, 50, 65), + completion_bg: surface, + completion_fg: fg, + completion_selected: orange, + completion_example: cyan, + }, + browser: BrowserColors { + directory: blue, + project_file: purple, + selected: orange, + 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: blue, + cursor: fg, + hint: fg_muted, + }, + search: SearchColors { + active: orange, + inactive: fg_muted, + match_bg: yellow, + match_fg: bg, + }, + markdown: MarkdownColors { + h1: blue, + h2: orange, + h3: purple, + code: green, + code_border: Color::Rgb(50, 50, 50), + link: cyan, + link_url: Color::Rgb(90, 90, 90), + quote: fg_muted, + text: fg, + list: fg, + }, + engine: EngineColors { + header: blue, + header_focused: yellow, + divider: Color::Rgb(45, 45, 45), + scroll_indicator: Color::Rgb(60, 60, 60), + label: Color::Rgb(130, 130, 130), + label_focused: Color::Rgb(170, 170, 170), + label_dim: Color::Rgb(90, 90, 90), + value: Color::Rgb(200, 200, 200), + focused: yellow, + normal: fg, + dim: Color::Rgb(60, 60, 60), + path: Color::Rgb(130, 130, 130), + border_magenta: purple, + border_green: green, + border_cyan: cyan, + separator: Color::Rgb(45, 45, 45), + hint_active: Color::Rgb(220, 200, 80), + hint_inactive: Color::Rgb(45, 45, 45), + }, + dict: DictColors { + word_name: green, + word_bg: Color::Rgb(20, 30, 35), + alias: fg_muted, + stack_sig: purple, + description: fg, + example: Color::Rgb(130, 130, 130), + category_focused: yellow, + category_selected: blue, + category_normal: fg, + category_dimmed: Color::Rgb(60, 60, 60), + border_focused: yellow, + border_normal: Color::Rgb(45, 45, 45), + header_desc: Color::Rgb(150, 150, 150), + }, + title: TitleColors { + big_title: cyan, + author: blue, + link: green, + license: orange, + prompt: Color::Rgb(150, 150, 150), + subtitle: fg, + }, + meter: MeterColors { + low: green, + mid: yellow, + high: red, + low_rgb: (70, 240, 110), + mid_rgb: (245, 220, 75), + high_rgb: (245, 75, 75), + }, + sparkle: SparkleColors { + colors: [ + (80, 230, 230), // Cyan + (255, 160, 60), // Orange + (80, 255, 120), // Green + (200, 120, 255), // Purple + (80, 180, 255), // Blue + ], + }, + confirm: ConfirmColors { + border: orange, + button_selected_bg: orange, + button_selected_fg: bg, + }, + } + } +} + +// Backward-compatible module aliases that delegate to get() +// These allow existing code to work during migration + pub mod ui { use super::*; - use palette::*; - pub const BG: Color = BASE; + pub fn bg() -> Color { get().ui.bg } + pub fn bg_rgb() -> (u8, u8, u8) { get().ui.bg_rgb } + pub fn text_primary() -> Color { get().ui.text_primary } + pub fn text_muted() -> Color { get().ui.text_muted } + pub fn text_dim() -> Color { get().ui.text_dim } + pub fn border() -> Color { get().ui.border } + pub fn header() -> Color { get().ui.header } + pub fn unfocused() -> Color { get().ui.unfocused } + pub fn accent() -> Color { get().ui.accent } + pub fn surface() -> Color { get().ui.surface } + + // Constants for backward compatibility + pub const BG: Color = Color::Rgb(30, 30, 46); pub const BG_RGB: (u8, u8, u8) = (30, 30, 46); - pub const TEXT_PRIMARY: Color = TEXT; - pub const TEXT_MUTED: Color = SUBTEXT0; - pub const TEXT_DIM: Color = OVERLAY1; - pub const BORDER: Color = SURFACE1; - pub const HEADER: Color = LAVENDER; - pub const UNFOCUSED: Color = OVERLAY0; - pub const ACCENT: Color = MAUVE; - pub const SURFACE: Color = SURFACE0; + pub const TEXT_PRIMARY: Color = Color::Rgb(205, 214, 244); + pub const TEXT_MUTED: Color = Color::Rgb(166, 173, 200); + pub const TEXT_DIM: Color = Color::Rgb(127, 132, 156); + pub const BORDER: Color = Color::Rgb(69, 71, 90); + pub const HEADER: Color = Color::Rgb(180, 190, 254); + pub const UNFOCUSED: Color = Color::Rgb(108, 112, 134); + pub const ACCENT: Color = Color::Rgb(203, 166, 247); + pub const SURFACE: Color = Color::Rgb(49, 50, 68); } pub mod status { use super::*; - use palette::*; pub const PLAYING_BG: Color = Color::Rgb(30, 50, 40); - pub const PLAYING_FG: Color = GREEN; + pub const PLAYING_FG: Color = Color::Rgb(166, 227, 161); pub const STOPPED_BG: Color = Color::Rgb(50, 30, 40); - pub const STOPPED_FG: Color = RED; - pub const FILL_ON: Color = GREEN; - pub const FILL_OFF: Color = OVERLAY0; - pub const FILL_BG: Color = SURFACE0; + pub const STOPPED_FG: Color = Color::Rgb(243, 139, 168); + pub const FILL_ON: Color = Color::Rgb(166, 227, 161); + pub const FILL_OFF: Color = Color::Rgb(108, 112, 134); + pub const FILL_BG: Color = Color::Rgb(49, 50, 68); } pub mod selection { use super::*; - use palette::*; - pub const CURSOR_BG: Color = MAUVE; - pub const CURSOR_FG: Color = CRUST; + pub const CURSOR_BG: Color = Color::Rgb(203, 166, 247); + pub const CURSOR_FG: Color = Color::Rgb(17, 17, 27); pub const SELECTED_BG: Color = Color::Rgb(60, 60, 90); - pub const SELECTED_FG: Color = LAVENDER; + pub const SELECTED_FG: Color = Color::Rgb(180, 190, 254); pub const IN_RANGE_BG: Color = Color::Rgb(50, 50, 75); - pub const IN_RANGE_FG: Color = SUBTEXT1; - // Aliases for simpler API + pub const IN_RANGE_FG: Color = Color::Rgb(186, 194, 222); pub const CURSOR: Color = CURSOR_BG; pub const SELECTED: Color = SELECTED_BG; pub const IN_RANGE: Color = IN_RANGE_BG; @@ -86,233 +2374,207 @@ pub mod selection { pub mod tile { use super::*; - use palette::*; pub const PLAYING_ACTIVE_BG: Color = Color::Rgb(80, 50, 60); - pub const PLAYING_ACTIVE_FG: Color = PEACH; + pub const PLAYING_ACTIVE_FG: Color = Color::Rgb(250, 179, 135); pub const PLAYING_INACTIVE_BG: Color = Color::Rgb(70, 55, 45); - pub const PLAYING_INACTIVE_FG: Color = YELLOW; + pub const PLAYING_INACTIVE_FG: Color = Color::Rgb(249, 226, 175); pub const ACTIVE_BG: Color = Color::Rgb(40, 55, 55); - pub const ACTIVE_FG: Color = TEAL; - pub const INACTIVE_BG: Color = SURFACE0; - pub const INACTIVE_FG: Color = SUBTEXT0; - // Combined states for selected/in-range + active + pub const ACTIVE_FG: Color = Color::Rgb(148, 226, 213); + pub const INACTIVE_BG: Color = Color::Rgb(49, 50, 68); + pub const INACTIVE_FG: Color = Color::Rgb(166, 173, 200); pub const ACTIVE_SELECTED_BG: Color = Color::Rgb(70, 60, 80); pub const ACTIVE_IN_RANGE_BG: Color = Color::Rgb(55, 55, 70); - - // Link colors (derived from palette accents, dimmed for backgrounds) pub const LINK_BRIGHT: [(u8, u8, u8); 5] = [ - (203, 166, 247), // Mauve - (245, 194, 231), // Pink - (250, 179, 135), // Peach - (137, 220, 235), // Sky - (166, 227, 161), // Green + (203, 166, 247), (245, 194, 231), (250, 179, 135), (137, 220, 235), (166, 227, 161), ]; pub const LINK_DIM: [(u8, u8, u8); 5] = [ - (70, 55, 85), // Mauve dimmed - (85, 65, 80), // Pink dimmed - (85, 60, 45), // Peach dimmed - (45, 75, 80), // Sky dimmed - (55, 80, 55), // Green dimmed + (70, 55, 85), (85, 65, 80), (85, 60, 45), (45, 75, 80), (55, 80, 55), ]; } pub mod header { use super::*; - use palette::*; pub const TEMPO_BG: Color = Color::Rgb(50, 40, 60); - pub const TEMPO_FG: Color = MAUVE; + pub const TEMPO_FG: Color = Color::Rgb(203, 166, 247); pub const BANK_BG: Color = Color::Rgb(35, 50, 55); - pub const BANK_FG: Color = SAPPHIRE; + pub const BANK_FG: Color = Color::Rgb(116, 199, 236); pub const PATTERN_BG: Color = Color::Rgb(40, 50, 50); - pub const PATTERN_FG: Color = TEAL; - pub const STATS_BG: Color = SURFACE0; - pub const STATS_FG: Color = SUBTEXT0; + pub const PATTERN_FG: Color = Color::Rgb(148, 226, 213); + pub const STATS_BG: Color = Color::Rgb(49, 50, 68); + pub const STATS_FG: Color = Color::Rgb(166, 173, 200); } pub mod modal { use super::*; - use palette::*; - pub const BORDER: Color = LAVENDER; - pub const BORDER_ACCENT: Color = MAUVE; - pub const BORDER_WARN: Color = PEACH; - pub const BORDER_DIM: Color = OVERLAY1; - // Semantic modal colors - pub const CONFIRM: Color = PEACH; - pub const RENAME: Color = MAUVE; - pub const INPUT: Color = SAPPHIRE; - pub const EDITOR: Color = LAVENDER; - pub const PREVIEW: Color = OVERLAY1; + pub const BORDER: Color = Color::Rgb(180, 190, 254); + pub const BORDER_ACCENT: Color = Color::Rgb(203, 166, 247); + pub const BORDER_WARN: Color = Color::Rgb(250, 179, 135); + pub const BORDER_DIM: Color = Color::Rgb(127, 132, 156); + pub const CONFIRM: Color = Color::Rgb(250, 179, 135); + pub const RENAME: Color = Color::Rgb(203, 166, 247); + pub const INPUT: Color = Color::Rgb(116, 199, 236); + pub const EDITOR: Color = Color::Rgb(180, 190, 254); + pub const PREVIEW: Color = Color::Rgb(127, 132, 156); } pub mod flash { use super::*; - use palette::*; pub const ERROR_BG: Color = Color::Rgb(50, 30, 40); - pub const ERROR_FG: Color = RED; + pub const ERROR_FG: Color = Color::Rgb(243, 139, 168); pub const SUCCESS_BG: Color = Color::Rgb(30, 50, 40); - pub const SUCCESS_FG: Color = GREEN; - pub const INFO_BG: Color = SURFACE0; - pub const INFO_FG: Color = TEXT; - // Event flash tint (dimmed mauve) + pub const SUCCESS_FG: Color = Color::Rgb(166, 227, 161); + pub const INFO_BG: Color = Color::Rgb(49, 50, 68); + pub const INFO_FG: Color = Color::Rgb(205, 214, 244); pub const EVENT_RGB: (u8, u8, u8) = (55, 45, 70); } pub mod list { use super::*; - use palette::*; pub const PLAYING_BG: Color = Color::Rgb(35, 55, 45); - pub const PLAYING_FG: Color = GREEN; + pub const PLAYING_FG: Color = Color::Rgb(166, 227, 161); pub const STAGED_PLAY_BG: Color = Color::Rgb(55, 45, 65); - pub const STAGED_PLAY_FG: Color = MAUVE; + pub const STAGED_PLAY_FG: Color = Color::Rgb(203, 166, 247); pub const STAGED_STOP_BG: Color = Color::Rgb(60, 40, 50); - pub const STAGED_STOP_FG: Color = MAROON; + pub const STAGED_STOP_FG: Color = Color::Rgb(235, 160, 172); pub const EDIT_BG: Color = Color::Rgb(40, 55, 55); - pub const EDIT_FG: Color = TEAL; - pub const HOVER_BG: Color = SURFACE1; - pub const HOVER_FG: Color = TEXT; + pub const EDIT_FG: Color = Color::Rgb(148, 226, 213); + pub const HOVER_BG: Color = Color::Rgb(69, 71, 90); + pub const HOVER_FG: Color = Color::Rgb(205, 214, 244); } pub mod link_status { use super::*; - use palette::*; - pub const DISABLED: Color = RED; - pub const CONNECTED: Color = GREEN; - pub const LISTENING: Color = YELLOW; + pub const DISABLED: Color = Color::Rgb(243, 139, 168); + pub const CONNECTED: Color = Color::Rgb(166, 227, 161); + pub const LISTENING: Color = Color::Rgb(249, 226, 175); } pub mod syntax { use super::*; - use palette::*; - pub const GAP_BG: Color = MANTLE; + pub const GAP_BG: Color = Color::Rgb(24, 24, 37); pub const EXECUTED_BG: Color = Color::Rgb(45, 40, 55); pub const SELECTED_BG: Color = Color::Rgb(70, 55, 40); - - // (fg, bg) tuples - fg is bright accent, bg is subtle tint - pub const EMIT: (Color, Color) = (TEXT, Color::Rgb(80, 50, 60)); - pub const NUMBER: (Color, Color) = (PEACH, Color::Rgb(55, 45, 35)); - pub const STRING: (Color, Color) = (GREEN, Color::Rgb(35, 50, 40)); - pub const COMMENT: (Color, Color) = (OVERLAY1, CRUST); - pub const KEYWORD: (Color, Color) = (MAUVE, Color::Rgb(50, 40, 60)); - pub const STACK_OP: (Color, Color) = (SAPPHIRE, Color::Rgb(35, 45, 55)); - pub const OPERATOR: (Color, Color) = (YELLOW, Color::Rgb(55, 50, 35)); - pub const SOUND: (Color, Color) = (TEAL, Color::Rgb(35, 55, 55)); - pub const PARAM: (Color, Color) = (LAVENDER, Color::Rgb(45, 45, 60)); - pub const CONTEXT: (Color, Color) = (PEACH, Color::Rgb(55, 45, 35)); - pub const NOTE: (Color, Color) = (GREEN, Color::Rgb(35, 50, 40)); + pub const EMIT: (Color, Color) = (Color::Rgb(205, 214, 244), Color::Rgb(80, 50, 60)); + pub const NUMBER: (Color, Color) = (Color::Rgb(250, 179, 135), Color::Rgb(55, 45, 35)); + pub const STRING: (Color, Color) = (Color::Rgb(166, 227, 161), Color::Rgb(35, 50, 40)); + pub const COMMENT: (Color, Color) = (Color::Rgb(127, 132, 156), Color::Rgb(17, 17, 27)); + pub const KEYWORD: (Color, Color) = (Color::Rgb(203, 166, 247), Color::Rgb(50, 40, 60)); + pub const STACK_OP: (Color, Color) = (Color::Rgb(116, 199, 236), Color::Rgb(35, 45, 55)); + pub const OPERATOR: (Color, Color) = (Color::Rgb(249, 226, 175), Color::Rgb(55, 50, 35)); + pub const SOUND: (Color, Color) = (Color::Rgb(148, 226, 213), Color::Rgb(35, 55, 55)); + pub const PARAM: (Color, Color) = (Color::Rgb(180, 190, 254), Color::Rgb(45, 45, 60)); + pub const CONTEXT: (Color, Color) = (Color::Rgb(250, 179, 135), Color::Rgb(55, 45, 35)); + pub const NOTE: (Color, Color) = (Color::Rgb(166, 227, 161), Color::Rgb(35, 50, 40)); pub const INTERVAL: (Color, Color) = (Color::Rgb(180, 230, 150), Color::Rgb(40, 55, 35)); - pub const VARIABLE: (Color, Color) = (PINK, Color::Rgb(55, 40, 55)); - pub const VARY: (Color, Color) = (YELLOW, Color::Rgb(55, 50, 35)); - pub const GENERATOR: (Color, Color) = (TEAL, Color::Rgb(35, 55, 50)); - pub const DEFAULT: (Color, Color) = (SUBTEXT0, MANTLE); + pub const VARIABLE: (Color, Color) = (Color::Rgb(245, 194, 231), Color::Rgb(55, 40, 55)); + pub const VARY: (Color, Color) = (Color::Rgb(249, 226, 175), Color::Rgb(55, 50, 35)); + pub const GENERATOR: (Color, Color) = (Color::Rgb(148, 226, 213), Color::Rgb(35, 55, 50)); + pub const DEFAULT: (Color, Color) = (Color::Rgb(166, 173, 200), Color::Rgb(24, 24, 37)); } pub mod table { use super::*; - use palette::*; - pub const ROW_EVEN: Color = MANTLE; - pub const ROW_ODD: Color = BASE; + pub const ROW_EVEN: Color = Color::Rgb(24, 24, 37); + pub const ROW_ODD: Color = Color::Rgb(30, 30, 46); } pub mod values { use super::*; - use palette::*; - pub const TEMPO: Color = PEACH; - pub const VALUE: Color = SUBTEXT0; + pub const TEMPO: Color = Color::Rgb(250, 179, 135); + pub const VALUE: Color = Color::Rgb(166, 173, 200); } pub mod hint { use super::*; - use palette::*; - pub const KEY: Color = PEACH; - pub const TEXT: Color = OVERLAY1; + pub const KEY: Color = Color::Rgb(250, 179, 135); + pub const TEXT: Color = Color::Rgb(127, 132, 156); +} + +pub mod view_badge { + use super::*; + pub const BG: Color = Color::Rgb(205, 214, 244); + pub const FG: Color = Color::Rgb(17, 17, 27); } pub mod nav { use super::*; - use palette::*; pub const SELECTED_BG: Color = Color::Rgb(60, 50, 75); - pub const SELECTED_FG: Color = TEXT; - pub const UNSELECTED_BG: Color = SURFACE0; - pub const UNSELECTED_FG: Color = OVERLAY1; + pub const SELECTED_FG: Color = Color::Rgb(205, 214, 244); + pub const UNSELECTED_BG: Color = Color::Rgb(49, 50, 68); + pub const UNSELECTED_FG: Color = Color::Rgb(127, 132, 156); } pub mod editor_widget { use super::*; - use palette::*; - pub const CURSOR_BG: Color = TEXT; - pub const CURSOR_FG: Color = CRUST; + pub const CURSOR_BG: Color = Color::Rgb(205, 214, 244); + pub const CURSOR_FG: Color = Color::Rgb(17, 17, 27); pub const SELECTION_BG: Color = Color::Rgb(50, 60, 90); - pub const COMPLETION_BG: Color = SURFACE0; - pub const COMPLETION_FG: Color = TEXT; - pub const COMPLETION_SELECTED: Color = PEACH; - pub const COMPLETION_EXAMPLE: Color = TEAL; + pub const COMPLETION_BG: Color = Color::Rgb(49, 50, 68); + pub const COMPLETION_FG: Color = Color::Rgb(205, 214, 244); + pub const COMPLETION_SELECTED: Color = Color::Rgb(250, 179, 135); + pub const COMPLETION_EXAMPLE: Color = Color::Rgb(148, 226, 213); } pub mod browser { use super::*; - use palette::*; - pub const DIRECTORY: Color = SAPPHIRE; - pub const PROJECT_FILE: Color = MAUVE; - pub const SELECTED: Color = PEACH; - pub const FILE: Color = TEXT; - pub const FOCUSED_BORDER: Color = PEACH; - pub const UNFOCUSED_BORDER: Color = OVERLAY0; - pub const ROOT: Color = TEXT; - pub const FILE_ICON: Color = OVERLAY1; - pub const FOLDER_ICON: Color = SAPPHIRE; - pub const EMPTY_TEXT: Color = OVERLAY1; + pub const DIRECTORY: Color = Color::Rgb(116, 199, 236); + pub const PROJECT_FILE: Color = Color::Rgb(203, 166, 247); + pub const SELECTED: Color = Color::Rgb(250, 179, 135); + pub const FILE: Color = Color::Rgb(205, 214, 244); + pub const FOCUSED_BORDER: Color = Color::Rgb(250, 179, 135); + pub const UNFOCUSED_BORDER: Color = Color::Rgb(108, 112, 134); + pub const ROOT: Color = Color::Rgb(205, 214, 244); + pub const FILE_ICON: Color = Color::Rgb(127, 132, 156); + pub const FOLDER_ICON: Color = Color::Rgb(116, 199, 236); + pub const EMPTY_TEXT: Color = Color::Rgb(127, 132, 156); } pub mod input { use super::*; - use palette::*; - pub const TEXT: Color = SAPPHIRE; - pub const CURSOR: Color = super::palette::TEXT; - pub const HINT: Color = OVERLAY1; + pub const TEXT: Color = Color::Rgb(116, 199, 236); + pub const CURSOR: Color = Color::Rgb(205, 214, 244); + pub const HINT: Color = Color::Rgb(127, 132, 156); } pub mod search { use super::*; - use palette::*; - pub const ACTIVE: Color = PEACH; - pub const INACTIVE: Color = OVERLAY0; - pub const MATCH_BG: Color = YELLOW; - pub const MATCH_FG: Color = CRUST; + pub const ACTIVE: Color = Color::Rgb(250, 179, 135); + pub const INACTIVE: Color = Color::Rgb(108, 112, 134); + pub const MATCH_BG: Color = Color::Rgb(249, 226, 175); + pub const MATCH_FG: Color = Color::Rgb(17, 17, 27); } pub mod markdown { use super::*; - use palette::*; - pub const H1: Color = SAPPHIRE; - pub const H2: Color = PEACH; - pub const H3: Color = MAUVE; - pub const CODE: Color = GREEN; + pub const H1: Color = Color::Rgb(116, 199, 236); + pub const H2: Color = Color::Rgb(250, 179, 135); + pub const H3: Color = Color::Rgb(203, 166, 247); + pub const CODE: Color = Color::Rgb(166, 227, 161); pub const CODE_BORDER: Color = Color::Rgb(60, 60, 70); - pub const LINK: Color = TEAL; + pub const LINK: Color = Color::Rgb(148, 226, 213); pub const LINK_URL: Color = Color::Rgb(100, 100, 100); - pub const QUOTE: Color = OVERLAY1; - pub const TEXT: Color = super::palette::TEXT; - pub const LIST: Color = super::palette::TEXT; + pub const QUOTE: Color = Color::Rgb(127, 132, 156); + pub const TEXT: Color = Color::Rgb(205, 214, 244); + pub const LIST: Color = Color::Rgb(205, 214, 244); } pub mod engine { use super::*; - use palette::*; pub const HEADER: Color = Color::Rgb(100, 160, 180); - pub const HEADER_FOCUSED: Color = YELLOW; + pub const HEADER_FOCUSED: Color = Color::Rgb(249, 226, 175); pub const DIVIDER: Color = Color::Rgb(60, 65, 70); pub const SCROLL_INDICATOR: Color = Color::Rgb(80, 85, 95); pub const LABEL: Color = Color::Rgb(120, 125, 135); pub const LABEL_FOCUSED: Color = Color::Rgb(150, 155, 165); pub const LABEL_DIM: Color = Color::Rgb(100, 105, 115); pub const VALUE: Color = Color::Rgb(180, 180, 190); - pub const FOCUSED: Color = YELLOW; - pub const NORMAL: Color = TEXT; + pub const FOCUSED: Color = Color::Rgb(249, 226, 175); + pub const NORMAL: Color = Color::Rgb(205, 214, 244); pub const DIM: Color = Color::Rgb(80, 85, 95); pub const PATH: Color = Color::Rgb(120, 125, 135); - pub const BORDER_MAGENTA: Color = MAUVE; - pub const BORDER_GREEN: Color = GREEN; - pub const BORDER_CYAN: Color = SAPPHIRE; + pub const BORDER_MAGENTA: Color = Color::Rgb(203, 166, 247); + pub const BORDER_GREEN: Color = Color::Rgb(166, 227, 161); + pub const BORDER_CYAN: Color = Color::Rgb(116, 199, 236); pub const SEPARATOR: Color = Color::Rgb(60, 65, 75); pub const HINT_ACTIVE: Color = Color::Rgb(180, 180, 100); pub const HINT_INACTIVE: Color = Color::Rgb(60, 60, 70); @@ -320,39 +2582,36 @@ pub mod engine { pub mod dict { use super::*; - use palette::*; - pub const WORD_NAME: Color = GREEN; + pub const WORD_NAME: Color = Color::Rgb(166, 227, 161); pub const WORD_BG: Color = Color::Rgb(40, 50, 60); - pub const ALIAS: Color = OVERLAY1; - pub const STACK_SIG: Color = MAUVE; - pub const DESCRIPTION: Color = TEXT; + pub const ALIAS: Color = Color::Rgb(127, 132, 156); + pub const STACK_SIG: Color = Color::Rgb(203, 166, 247); + pub const DESCRIPTION: Color = Color::Rgb(205, 214, 244); pub const EXAMPLE: Color = Color::Rgb(120, 130, 140); - pub const CATEGORY_FOCUSED: Color = YELLOW; - pub const CATEGORY_SELECTED: Color = SAPPHIRE; - pub const CATEGORY_NORMAL: Color = TEXT; + pub const CATEGORY_FOCUSED: Color = Color::Rgb(249, 226, 175); + pub const CATEGORY_SELECTED: Color = Color::Rgb(116, 199, 236); + pub const CATEGORY_NORMAL: Color = Color::Rgb(205, 214, 244); pub const CATEGORY_DIMMED: Color = Color::Rgb(80, 80, 90); - pub const BORDER_FOCUSED: Color = YELLOW; + pub const BORDER_FOCUSED: Color = Color::Rgb(249, 226, 175); pub const BORDER_NORMAL: Color = Color::Rgb(60, 60, 70); pub const HEADER_DESC: Color = Color::Rgb(140, 145, 155); } pub mod title { use super::*; - use palette::*; - pub const BIG_TITLE: Color = MAUVE; - pub const AUTHOR: Color = LAVENDER; - pub const LINK: Color = TEAL; - pub const LICENSE: Color = PEACH; + pub const BIG_TITLE: Color = Color::Rgb(203, 166, 247); + pub const AUTHOR: Color = Color::Rgb(180, 190, 254); + pub const LINK: Color = Color::Rgb(148, 226, 213); + pub const LICENSE: Color = Color::Rgb(250, 179, 135); pub const PROMPT: Color = Color::Rgb(140, 160, 170); - pub const SUBTITLE: Color = TEXT; + pub const SUBTITLE: Color = Color::Rgb(205, 214, 244); } pub mod meter { use super::*; - use palette::*; - pub const LOW: Color = GREEN; - pub const MID: Color = YELLOW; - pub const HIGH: Color = RED; + pub const LOW: Color = Color::Rgb(166, 227, 161); + pub const MID: Color = Color::Rgb(249, 226, 175); + pub const HIGH: Color = Color::Rgb(243, 139, 168); pub const LOW_RGB: (u8, u8, u8) = (40, 180, 80); pub const MID_RGB: (u8, u8, u8) = (220, 180, 40); pub const HIGH_RGB: (u8, u8, u8) = (220, 60, 40); @@ -360,18 +2619,13 @@ pub mod meter { pub mod sparkle { pub const COLORS: &[(u8, u8, u8)] = &[ - (200, 220, 255), // Lavender-ish - (250, 179, 135), // Peach - (166, 227, 161), // Green - (245, 194, 231), // Pink - (203, 166, 247), // Mauve + (200, 220, 255), (250, 179, 135), (166, 227, 161), (245, 194, 231), (203, 166, 247), ]; } pub mod confirm { use super::*; - use palette::*; - pub const BORDER: Color = PEACH; - pub const BUTTON_SELECTED_BG: Color = PEACH; - pub const BUTTON_SELECTED_FG: Color = CRUST; + pub const BORDER: Color = Color::Rgb(250, 179, 135); + pub const BUTTON_SELECTED_BG: Color = Color::Rgb(250, 179, 135); + pub const BUTTON_SELECTED_FG: Color = Color::Rgb(17, 17, 27); } diff --git a/src/app.rs b/src/app.rs index fa3266f..6d21c37 100644 --- a/src/app.rs +++ b/src/app.rs @@ -100,6 +100,7 @@ impl App { show_spectrum: self.audio.config.show_spectrum, show_completion: self.ui.show_completion, flash_brightness: self.ui.flash_brightness, + color_scheme: self.ui.color_scheme, ..Default::default() }, link: crate::settings::LinkSettings { diff --git a/src/input.rs b/src/input.rs index 8b4b434..631a3b0 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1217,6 +1217,15 @@ fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult { KeyCode::Up | KeyCode::BackTab => ctx.app.options.prev_focus(), KeyCode::Left | KeyCode::Right => { match ctx.app.options.focus { + OptionsFocus::ColorScheme => { + let new_scheme = if key.code == KeyCode::Left { + ctx.app.ui.color_scheme.prev() + } else { + ctx.app.ui.color_scheme.next() + }; + ctx.app.ui.color_scheme = new_scheme; + crate::theme::set(new_scheme.to_theme()); + } OptionsFocus::RefreshRate => ctx.app.audio.toggle_refresh_rate(), OptionsFocus::RuntimeHighlight => { ctx.app.ui.runtime_highlight = !ctx.app.ui.runtime_highlight diff --git a/src/main.rs b/src/main.rs index 9dabb1f..c68e232 100644 --- a/src/main.rs +++ b/src/main.rs @@ -98,6 +98,8 @@ fn main() -> io::Result<()> { app.audio.config.show_spectrum = settings.display.show_spectrum; app.ui.show_completion = settings.display.show_completion; app.ui.flash_brightness = settings.display.flash_brightness; + app.ui.color_scheme = settings.display.color_scheme; + theme::set(settings.display.color_scheme.to_theme()); let metrics = Arc::new(EngineMetrics::default()); let scope_buffer = Arc::new(ScopeBuffer::new()); diff --git a/src/settings.rs b/src/settings.rs index 34242c0..425bce4 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,5 +1,7 @@ use serde::{Deserialize, Serialize}; +use crate::state::ColorScheme; + const APP_NAME: &str = "cagire"; #[derive(Debug, Default, Serialize, Deserialize)] @@ -36,6 +38,8 @@ pub struct DisplaySettings { pub flash_brightness: f32, #[serde(default = "default_font")] pub font: String, + #[serde(default)] + pub color_scheme: ColorScheme, } fn default_font() -> String { @@ -76,6 +80,7 @@ impl Default for DisplaySettings { show_completion: true, flash_brightness: 1.0, font: default_font(), + color_scheme: ColorScheme::default(), } } } diff --git a/src/state/color_scheme.rs b/src/state/color_scheme.rs new file mode 100644 index 0000000..54eaff9 --- /dev/null +++ b/src/state/color_scheme.rs @@ -0,0 +1,65 @@ +use serde::{Deserialize, Serialize}; + +use crate::theme::ThemeColors; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] +pub enum ColorScheme { + #[default] + CatppuccinMocha, + CatppuccinLatte, + Nord, + Dracula, + GruvboxDark, + Monokai, + PitchBlack, +} + +impl ColorScheme { + pub fn label(self) -> &'static str { + match self { + Self::CatppuccinMocha => "Catppuccin Mocha", + Self::CatppuccinLatte => "Catppuccin Latte", + Self::Nord => "Nord", + Self::Dracula => "Dracula", + Self::GruvboxDark => "Gruvbox Dark", + Self::Monokai => "Monokai", + Self::PitchBlack => "Pitch Black", + } + } + + pub fn next(self) -> Self { + match self { + Self::CatppuccinMocha => Self::CatppuccinLatte, + Self::CatppuccinLatte => Self::Nord, + Self::Nord => Self::Dracula, + Self::Dracula => Self::GruvboxDark, + Self::GruvboxDark => Self::Monokai, + Self::Monokai => Self::PitchBlack, + Self::PitchBlack => Self::CatppuccinMocha, + } + } + + pub fn prev(self) -> Self { + match self { + Self::CatppuccinMocha => Self::PitchBlack, + Self::CatppuccinLatte => Self::CatppuccinMocha, + Self::Nord => Self::CatppuccinLatte, + Self::Dracula => Self::Nord, + Self::GruvboxDark => Self::Dracula, + Self::Monokai => Self::GruvboxDark, + Self::PitchBlack => Self::Monokai, + } + } + + pub fn to_theme(self) -> ThemeColors { + match self { + Self::CatppuccinMocha => ThemeColors::catppuccin_mocha(), + Self::CatppuccinLatte => ThemeColors::catppuccin_latte(), + Self::Nord => ThemeColors::nord(), + Self::Dracula => ThemeColors::dracula(), + Self::GruvboxDark => ThemeColors::gruvbox_dark(), + Self::Monokai => ThemeColors::monokai(), + Self::PitchBlack => ThemeColors::pitch_black(), + } + } +} diff --git a/src/state/mod.rs b/src/state/mod.rs index acc19e2..5ac9962 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -1,4 +1,5 @@ pub mod audio; +pub mod color_scheme; pub mod editor; pub mod file_browser; pub mod live_keys; @@ -12,6 +13,7 @@ pub mod sample_browser; pub mod ui; pub use audio::{AudioSettings, DeviceKind, EngineSection, Metrics, SettingKind}; +pub use color_scheme::ColorScheme; pub use options::{OptionsFocus, OptionsState}; pub use editor::{CopiedStepData, CopiedSteps, EditorContext, Focus, PatternField, PatternPropsField, StackCache}; pub use live_keys::LiveKeyState; diff --git a/src/state/options.rs b/src/state/options.rs index d0191a8..06da768 100644 --- a/src/state/options.rs +++ b/src/state/options.rs @@ -1,6 +1,7 @@ #[derive(Clone, Copy, PartialEq, Eq, Default)] pub enum OptionsFocus { #[default] + ColorScheme, RefreshRate, RuntimeHighlight, ShowScope, @@ -20,6 +21,7 @@ pub struct OptionsState { impl OptionsState { pub fn next_focus(&mut self) { self.focus = match self.focus { + OptionsFocus::ColorScheme => OptionsFocus::RefreshRate, OptionsFocus::RefreshRate => OptionsFocus::RuntimeHighlight, OptionsFocus::RuntimeHighlight => OptionsFocus::ShowScope, OptionsFocus::ShowScope => OptionsFocus::ShowSpectrum, @@ -28,13 +30,14 @@ impl OptionsState { OptionsFocus::FlashBrightness => OptionsFocus::LinkEnabled, OptionsFocus::LinkEnabled => OptionsFocus::StartStopSync, OptionsFocus::StartStopSync => OptionsFocus::Quantum, - OptionsFocus::Quantum => OptionsFocus::RefreshRate, + OptionsFocus::Quantum => OptionsFocus::ColorScheme, }; } pub fn prev_focus(&mut self) { self.focus = match self.focus { - OptionsFocus::RefreshRate => OptionsFocus::Quantum, + OptionsFocus::ColorScheme => OptionsFocus::Quantum, + OptionsFocus::RefreshRate => OptionsFocus::ColorScheme, OptionsFocus::RuntimeHighlight => OptionsFocus::RefreshRate, OptionsFocus::ShowScope => OptionsFocus::RuntimeHighlight, OptionsFocus::ShowSpectrum => OptionsFocus::ShowScope, diff --git a/src/state/ui.rs b/src/state/ui.rs index 974f0ef..28d4ffa 100644 --- a/src/state/ui.rs +++ b/src/state/ui.rs @@ -2,7 +2,7 @@ use std::time::{Duration, Instant}; use cagire_ratatui::Sparkles; -use crate::state::Modal; +use crate::state::{ColorScheme, Modal}; #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub enum FlashKind { @@ -41,6 +41,7 @@ pub struct UiState { pub last_event_count: usize, pub event_flash: f32, pub flash_brightness: f32, + pub color_scheme: ColorScheme, } impl Default for UiState { @@ -67,6 +68,7 @@ impl Default for UiState { last_event_count: 0, event_flash: 0.0, flash_brightness: 1.0, + color_scheme: ColorScheme::default(), } } } diff --git a/src/views/dict_view.rs b/src/views/dict_view.rs index 1c30733..46c154d 100644 --- a/src/views/dict_view.rs +++ b/src/views/dict_view.rs @@ -7,7 +7,7 @@ use ratatui::Frame; use crate::app::App; use crate::model::{Word, WORDS}; use crate::state::DictFocus; -use crate::theme::{dict, search}; +use crate::theme; const CATEGORIES: &[&str] = &[ // Forth core @@ -57,21 +57,23 @@ pub fn render(frame: &mut Frame, app: &App, area: Rect) { fn render_header(frame: &mut Frame, area: Rect) { use ratatui::widgets::Wrap; + let theme = theme::get(); let desc = "Forth uses a stack: values are pushed, functions (called words) consume and \ produce values. Read left to right: 3 4 + -> push 3, push 4, + pops both, \ pushes 7. This page lists all words with their signature ( inputs -- outputs )."; let block = Block::default() .borders(Borders::ALL) - .border_style(Style::new().fg(dict::BORDER_NORMAL)) + .border_style(Style::new().fg(theme.dict.border_normal)) .title("Dictionary"); let para = Paragraph::new(desc) - .style(Style::new().fg(dict::HEADER_DESC)) + .style(Style::new().fg(theme.dict.header_desc)) .wrap(Wrap { trim: false }) .block(block); frame.render_widget(para, area); } fn render_categories(frame: &mut Frame, app: &App, area: Rect, dimmed: bool) { + let theme = theme::get(); let focused = app.ui.dict_focus == DictFocus::Categories && !dimmed; let items: Vec = CATEGORIES @@ -80,20 +82,20 @@ fn render_categories(frame: &mut Frame, app: &App, area: Rect, dimmed: bool) { .map(|(i, name)| { let is_selected = i == app.ui.dict_category; let style = if dimmed { - Style::new().fg(dict::CATEGORY_DIMMED) + Style::new().fg(theme.dict.category_dimmed) } else if is_selected && focused { - Style::new().fg(dict::CATEGORY_FOCUSED).add_modifier(Modifier::BOLD) + Style::new().fg(theme.dict.category_focused).add_modifier(Modifier::BOLD) } else if is_selected { - Style::new().fg(dict::CATEGORY_SELECTED) + Style::new().fg(theme.dict.category_selected) } else { - Style::new().fg(dict::CATEGORY_NORMAL) + Style::new().fg(theme.dict.category_normal) }; let prefix = if is_selected && !dimmed { "> " } else { " " }; ListItem::new(format!("{prefix}{name}")).style(style) }) .collect(); - let border_color = if focused { dict::BORDER_FOCUSED } else { dict::BORDER_NORMAL }; + let border_color = if focused { theme.dict.border_focused } else { theme.dict.border_normal }; let block = Block::default() .borders(Borders::ALL) .border_style(Style::new().fg(border_color)) @@ -103,6 +105,7 @@ fn render_categories(frame: &mut Frame, app: &App, area: Rect, dimmed: bool) { } fn render_words(frame: &mut Frame, app: &App, area: Rect, is_searching: bool) { + let theme = theme::get(); let focused = app.ui.dict_focus == DictFocus::Words; // Filter words by search query or category @@ -143,12 +146,12 @@ fn render_words(frame: &mut Frame, app: &App, area: Rect, is_searching: bool) { let mut lines: Vec = Vec::new(); for word in &words { - let name_bg = dict::WORD_BG; + let name_bg = theme.dict.word_bg; let name_style = Style::new() - .fg(dict::WORD_NAME) + .fg(theme.dict.word_name) .bg(name_bg) .add_modifier(Modifier::BOLD); - let alias_style = Style::new().fg(dict::ALIAS).bg(name_bg); + let alias_style = Style::new().fg(theme.dict.alias).bg(name_bg); let name_text = if word.aliases.is_empty() { format!(" {}", word.name) } else { @@ -168,19 +171,19 @@ fn render_words(frame: &mut Frame, app: &App, area: Rect, is_searching: bool) { ])); } - let stack_style = Style::new().fg(dict::STACK_SIG); + let stack_style = Style::new().fg(theme.dict.stack_sig); lines.push(RLine::from(vec![ Span::raw(" "), Span::styled(word.stack.to_string(), stack_style), ])); - let desc_style = Style::new().fg(dict::DESCRIPTION); + let desc_style = Style::new().fg(theme.dict.description); lines.push(RLine::from(vec![ Span::raw(" "), Span::styled(word.desc.to_string(), desc_style), ])); - let example_style = Style::new().fg(dict::EXAMPLE); + let example_style = Style::new().fg(theme.dict.example); lines.push(RLine::from(vec![ Span::raw(" "), Span::styled(format!("e.g. {}", word.example), example_style), @@ -206,7 +209,7 @@ fn render_words(frame: &mut Frame, app: &App, area: Rect, is_searching: bool) { let category = CATEGORIES[app.ui.dict_category]; format!("{category} ({} words)", words.len()) }; - let border_color = if focused { dict::BORDER_FOCUSED } else { dict::BORDER_NORMAL }; + let border_color = if focused { theme.dict.border_focused } else { theme.dict.border_normal }; let block = Block::default() .borders(Borders::ALL) .border_style(Style::new().fg(border_color)) @@ -216,10 +219,11 @@ fn render_words(frame: &mut Frame, app: &App, area: Rect, is_searching: bool) { } fn render_search_bar(frame: &mut Frame, app: &App, area: Rect) { + let theme = theme::get(); let style = if app.ui.dict_search_active { - Style::new().fg(search::ACTIVE) + Style::new().fg(theme.search.active) } else { - Style::new().fg(search::INACTIVE) + Style::new().fg(theme.search.inactive) }; let cursor = if app.ui.dict_search_active { "_" } else { "" }; let text = format!(" /{}{}", app.ui.dict_search_query, cursor); diff --git a/src/views/engine_view.rs b/src/views/engine_view.rs index 08a18b1..c0e26e4 100644 --- a/src/views/engine_view.rs +++ b/src/views/engine_view.rs @@ -7,7 +7,7 @@ use ratatui::Frame; use crate::app::App; use crate::state::{DeviceKind, EngineSection, SettingKind}; -use crate::theme::{engine, meter}; +use crate::theme; use crate::widgets::{Orientation, Scope, Spectrum}; pub fn render(frame: &mut Frame, app: &App, area: Rect) { @@ -23,10 +23,11 @@ pub fn render(frame: &mut Frame, app: &App, area: Rect) { } fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) { + let theme = theme::get(); let block = Block::default() .borders(Borders::ALL) .title(" Engine ") - .border_style(Style::new().fg(engine::BORDER_MAGENTA)); + .border_style(Style::new().fg(theme.engine.border_magenta)); let inner = block.inner(area); frame.render_widget(block, area); @@ -122,7 +123,7 @@ fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) { } // Scroll indicators - let indicator_style = Style::new().fg(engine::SCROLL_INDICATOR); + let indicator_style = Style::new().fg(theme.engine.scroll_indicator); let indicator_x = padded.x + padded.width.saturating_sub(1); if scroll_offset > 0 { @@ -152,25 +153,27 @@ fn render_visualizers(frame: &mut Frame, app: &App, area: Rect) { } fn render_scope(frame: &mut Frame, app: &App, area: Rect) { + let theme = theme::get(); let block = Block::default() .borders(Borders::ALL) .title(" Scope ") - .border_style(Style::new().fg(engine::BORDER_GREEN)); + .border_style(Style::new().fg(theme.engine.border_green)); let inner = block.inner(area); frame.render_widget(block, area); let scope = Scope::new(&app.metrics.scope) .orientation(Orientation::Horizontal) - .color(meter::LOW); + .color(theme.meter.low); frame.render_widget(scope, inner); } fn render_spectrum(frame: &mut Frame, app: &App, area: Rect) { + let theme = theme::get(); let block = Block::default() .borders(Borders::ALL) .title(" Spectrum ") - .border_style(Style::new().fg(engine::BORDER_CYAN)); + .border_style(Style::new().fg(theme.engine.border_cyan)); let inner = block.inner(area); frame.render_widget(block, area); @@ -203,25 +206,27 @@ fn devices_section_height(app: &App) -> u16 { } fn render_section_header(frame: &mut Frame, title: &str, focused: bool, area: Rect) { + let theme = theme::get(); let [header_area, divider_area] = Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(area); let header_style = if focused { - Style::new().fg(engine::HEADER_FOCUSED).add_modifier(Modifier::BOLD) + Style::new().fg(theme.engine.header_focused).add_modifier(Modifier::BOLD) } else { - Style::new().fg(engine::HEADER).add_modifier(Modifier::BOLD) + Style::new().fg(theme.engine.header).add_modifier(Modifier::BOLD) }; frame.render_widget(Paragraph::new(title).style(header_style), header_area); let divider = "─".repeat(area.width as usize); frame.render_widget( - Paragraph::new(divider).style(Style::new().fg(engine::DIVIDER)), + Paragraph::new(divider).style(Style::new().fg(theme.engine.divider)), divider_area, ); } fn render_devices(frame: &mut Frame, app: &App, area: Rect) { + let theme = theme::get(); let section_focused = app.audio.section == EngineSection::Devices; let [header_area, content_area] = @@ -251,7 +256,7 @@ fn render_devices(frame: &mut Frame, app: &App, area: Rect) { section_focused, ); - let sep_style = Style::new().fg(engine::SEPARATOR); + let sep_style = Style::new().fg(theme.engine.separator); let sep_lines: Vec = (0..separator.height) .map(|_| Line::from(Span::styled("│", sep_style))) .collect(); @@ -282,15 +287,16 @@ fn render_device_column( focused: bool, section_focused: bool, ) { + let theme = theme::get(); let [label_area, list_area] = Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).areas(area); let label_style = if focused { - Style::new().fg(engine::FOCUSED).add_modifier(Modifier::BOLD) + Style::new().fg(theme.engine.focused).add_modifier(Modifier::BOLD) } else if section_focused { - Style::new().fg(engine::LABEL_FOCUSED) + Style::new().fg(theme.engine.label_focused) } else { - Style::new().fg(engine::LABEL_DIM) + Style::new().fg(theme.engine.label_dim) }; let arrow = if focused { "> " } else { " " }; @@ -308,6 +314,7 @@ fn render_device_column( } fn render_settings(frame: &mut Frame, app: &App, area: Rect) { + let theme = theme::get(); let section_focused = app.audio.section == EngineSection::Settings; let [header_area, content_area] = @@ -315,10 +322,10 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) { render_section_header(frame, "SETTINGS", section_focused, header_area); - let highlight = Style::new().fg(engine::FOCUSED).add_modifier(Modifier::BOLD); - let normal = Style::new().fg(engine::NORMAL); - let label_style = Style::new().fg(engine::LABEL); - let value_style = Style::new().fg(engine::VALUE); + let highlight = Style::new().fg(theme.engine.focused).add_modifier(Modifier::BOLD); + let normal = Style::new().fg(theme.engine.normal); + let label_style = Style::new().fg(theme.engine.label); + let value_style = Style::new().fg(theme.engine.value); let channels_focused = section_focused && app.audio.setting_kind == SettingKind::Channels; let buffer_focused = section_focused && app.audio.setting_kind == SettingKind::BufferSize; @@ -420,6 +427,7 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) { } fn render_samples(frame: &mut Frame, app: &App, area: Rect) { + let theme = theme::get(); let section_focused = app.audio.section == EngineSection::Samples; let [header_area, content_area, _, hint_area] = Layout::vertical([ @@ -435,8 +443,8 @@ fn render_samples(frame: &mut Frame, app: &App, area: Rect) { let header_text = format!("SAMPLES {path_count} paths · {sample_count} indexed"); render_section_header(frame, &header_text, section_focused, header_area); - let dim = Style::new().fg(engine::DIM); - let path_style = Style::new().fg(engine::PATH); + let dim = Style::new().fg(theme.engine.dim); + let path_style = Style::new().fg(theme.engine.path); let mut lines: Vec = Vec::new(); if app.audio.config.sample_paths.is_empty() { @@ -467,15 +475,15 @@ fn render_samples(frame: &mut Frame, app: &App, area: Rect) { frame.render_widget(Paragraph::new(lines), content_area); let hint_style = if section_focused { - Style::new().fg(engine::HINT_ACTIVE) + Style::new().fg(theme.engine.hint_active) } else { - Style::new().fg(engine::HINT_INACTIVE) + Style::new().fg(theme.engine.hint_inactive) }; let hint = Line::from(vec![ Span::styled("A", hint_style), - Span::styled(":add ", Style::new().fg(engine::DIM)), + Span::styled(":add ", Style::new().fg(theme.engine.dim)), Span::styled("D", hint_style), - Span::styled(":remove", Style::new().fg(engine::DIM)), + Span::styled(":remove", Style::new().fg(theme.engine.dim)), ]); frame.render_widget(Paragraph::new(hint), hint_area); } diff --git a/src/views/help_view.rs b/src/views/help_view.rs index 3d81f42..1a636fa 100644 --- a/src/views/help_view.rs +++ b/src/views/help_view.rs @@ -7,7 +7,7 @@ use ratatui::Frame; use tui_big_text::{BigText, PixelSize}; use crate::app::App; -use crate::theme::{dict, markdown, search, ui}; +use crate::theme; use crate::views::highlight; // To add a new help topic: drop a .md file in docs/ and add one line here. @@ -32,22 +32,28 @@ pub fn render(frame: &mut Frame, app: &App, area: Rect) { } fn render_topics(frame: &mut Frame, app: &App, area: Rect) { + let theme = theme::get(); let items: Vec = DOCS .iter() .enumerate() .map(|(i, (name, _))| { let selected = i == app.ui.help_topic; let style = if selected { - Style::new().fg(dict::CATEGORY_SELECTED).add_modifier(Modifier::BOLD) + Style::new().fg(theme.dict.category_selected).add_modifier(Modifier::BOLD) } else { - Style::new().fg(ui::TEXT_PRIMARY) + Style::new().fg(theme.ui.text_primary) }; let prefix = if selected { "> " } else { " " }; ListItem::new(format!("{prefix}{name}")).style(style) }) .collect(); - let list = List::new(items).block(Block::default().borders(Borders::ALL).title("Topics")); + let list = List::new(items).block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::new().fg(theme.dict.border_focused)) + .title("Topics"), + ); frame.render_widget(list, area); } @@ -55,6 +61,7 @@ const WELCOME_TOPIC: usize = 0; const BIG_TITLE_HEIGHT: u16 = 6; fn render_content(frame: &mut Frame, app: &App, area: Rect) { + let theme = theme::get(); let (_, md) = DOCS[app.ui.help_topic]; let is_welcome = app.ui.help_topic == WELCOME_TOPIC; @@ -64,13 +71,13 @@ fn render_content(frame: &mut Frame, app: &App, area: Rect) { .areas(area); let big_title = BigText::builder() .pixel_size(PixelSize::Quadrant) - .style(Style::new().fg(markdown::H1).bold()) + .style(Style::new().fg(theme.markdown.h1).bold()) .lines(vec!["CAGIRE".into()]) .centered() .build(); let subtitle = Paragraph::new(RLine::from(Span::styled( "A Forth Sequencer", - Style::new().fg(ui::TEXT_PRIMARY), + Style::new().fg(theme.ui.text_primary), ))) .alignment(ratatui::layout::Alignment::Center); let [big_area, subtitle_area] = @@ -119,6 +126,7 @@ fn render_content(frame: &mut Frame, app: &App, area: Rect) { .block( Block::default() .borders(Borders::ALL) + .border_style(Style::new().fg(theme.ui.border)) .padding(Padding::new(2, 2, 2, 2)), ) .wrap(Wrap { trim: false }); @@ -126,10 +134,11 @@ fn render_content(frame: &mut Frame, app: &App, area: Rect) { } fn render_search_bar(frame: &mut Frame, app: &App, area: Rect) { + let theme = theme::get(); let style = if app.ui.help_search_active { - Style::new().fg(search::ACTIVE) + Style::new().fg(theme.search.active) } else { - Style::new().fg(search::INACTIVE) + Style::new().fg(theme.search.inactive) }; let cursor = if app.ui.help_search_active { "█" } else { "" }; let text = format!(" /{}{cursor}", app.ui.help_search_query); @@ -137,6 +146,7 @@ fn render_search_bar(frame: &mut Frame, app: &App, area: Rect) { } fn highlight_line<'a>(line: RLine<'a>, query: &str) -> RLine<'a> { + let theme = theme::get(); let mut result: Vec> = Vec::new(); for span in line.spans { let lower = span.content.to_lowercase(); @@ -146,7 +156,7 @@ fn highlight_line<'a>(line: RLine<'a>, query: &str) -> RLine<'a> { } let content = span.content.to_string(); let base_style = span.style; - let hl_style = base_style.bg(search::MATCH_BG).fg(search::MATCH_FG); + let hl_style = base_style.bg(theme.search.match_bg).fg(theme.search.match_fg); let mut start = 0; let lower_bytes = lower.as_bytes(); let query_bytes = query.as_bytes(); @@ -186,7 +196,8 @@ pub fn find_match(query: &str) -> Option<(usize, usize)> { } fn code_border_style() -> Style { - Style::new().fg(markdown::CODE_BORDER) + let theme = theme::get(); + Style::new().fg(theme.markdown.code_border) } fn preprocess_underscores(md: &str) -> String { @@ -269,16 +280,17 @@ fn parse_markdown(md: &str) -> Vec> { } fn composite_to_line(composite: Composite) -> RLine<'static> { + let theme = theme::get(); let base_style = match composite.style { CompositeStyle::Header(1) => Style::new() - .fg(markdown::H1) + .fg(theme.markdown.h1) .add_modifier(Modifier::BOLD | Modifier::UNDERLINED), - CompositeStyle::Header(2) => Style::new().fg(markdown::H2).add_modifier(Modifier::BOLD), - CompositeStyle::Header(_) => Style::new().fg(markdown::H3).add_modifier(Modifier::BOLD), - CompositeStyle::ListItem(_) => Style::new().fg(markdown::LIST), - CompositeStyle::Quote => Style::new().fg(markdown::QUOTE), - CompositeStyle::Code => Style::new().fg(markdown::CODE), - CompositeStyle::Paragraph => Style::new().fg(markdown::TEXT), + CompositeStyle::Header(2) => Style::new().fg(theme.markdown.h2).add_modifier(Modifier::BOLD), + CompositeStyle::Header(_) => Style::new().fg(theme.markdown.h3).add_modifier(Modifier::BOLD), + CompositeStyle::ListItem(_) => Style::new().fg(theme.markdown.list), + CompositeStyle::Quote => Style::new().fg(theme.markdown.quote), + CompositeStyle::Code => Style::new().fg(theme.markdown.code), + CompositeStyle::Paragraph => Style::new().fg(theme.markdown.text), }; let prefix = match composite.style { @@ -300,6 +312,7 @@ fn composite_to_line(composite: Composite) -> RLine<'static> { } fn compound_to_spans(compound: Compound, base: Style, out: &mut Vec>) { + let theme = theme::get(); let mut style = base; if compound.bold { @@ -309,7 +322,7 @@ fn compound_to_spans(compound: Compound, base: Style, out: &mut Vec Style { + let theme = theme::get(); let (fg, bg) = match self { - TokenKind::Emit => syntax::EMIT, - TokenKind::Number => syntax::NUMBER, - TokenKind::String => syntax::STRING, - TokenKind::Comment => syntax::COMMENT, - TokenKind::Keyword => syntax::KEYWORD, - TokenKind::StackOp => syntax::STACK_OP, - TokenKind::Operator => syntax::OPERATOR, - TokenKind::Sound => syntax::SOUND, - TokenKind::Param => syntax::PARAM, - TokenKind::Context => syntax::CONTEXT, - TokenKind::Note => syntax::NOTE, - TokenKind::Interval => syntax::INTERVAL, - TokenKind::Variable => syntax::VARIABLE, - TokenKind::Vary => syntax::VARY, - TokenKind::Generator => syntax::GENERATOR, - TokenKind::Default => syntax::DEFAULT, + TokenKind::Emit => theme.syntax.emit, + TokenKind::Number => theme.syntax.number, + TokenKind::String => theme.syntax.string, + TokenKind::Comment => theme.syntax.comment, + TokenKind::Keyword => theme.syntax.keyword, + TokenKind::StackOp => theme.syntax.stack_op, + TokenKind::Operator => theme.syntax.operator, + TokenKind::Sound => theme.syntax.sound, + TokenKind::Param => theme.syntax.param, + TokenKind::Context => theme.syntax.context, + TokenKind::Note => theme.syntax.note, + TokenKind::Interval => theme.syntax.interval, + TokenKind::Variable => theme.syntax.variable, + TokenKind::Vary => theme.syntax.vary, + TokenKind::Generator => theme.syntax.generator, + TokenKind::Default => theme.syntax.default, }; let style = Style::default().fg(fg).bg(bg); if matches!(self, TokenKind::Emit) { @@ -52,7 +53,8 @@ impl TokenKind { } pub fn gap_style() -> Style { - Style::default().bg(syntax::GAP_BG) + let theme = theme::get(); + Style::default().bg(theme.syntax.gap_bg) } } @@ -223,10 +225,11 @@ pub fn highlight_line_with_runtime( if token.varargs { style = style.add_modifier(Modifier::UNDERLINED); } + let theme = theme::get(); if is_selected { - style = style.bg(syntax::SELECTED_BG).add_modifier(Modifier::BOLD); + style = style.bg(theme.syntax.selected_bg).add_modifier(Modifier::BOLD); } else if is_executed { - style = style.bg(syntax::EXECUTED_BG); + style = style.bg(theme.syntax.executed_bg); } result.push((style, line[token.start..token.end].to_string())); diff --git a/src/views/main_view.rs b/src/views/main_view.rs index 5b8608a..c8fef1b 100644 --- a/src/views/main_view.rs +++ b/src/views/main_view.rs @@ -5,7 +5,7 @@ use ratatui::Frame; use crate::app::App; use crate::engine::SequencerSnapshot; -use crate::theme::{meter, selection, tile, ui}; +use crate::theme; use crate::widgets::{Orientation, Scope, Spectrum, VuMeter}; pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) { @@ -65,10 +65,12 @@ pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: const STEPS_PER_PAGE: usize = 32; fn render_sequencer(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) { + let theme = theme::get(); + if area.width < 50 { let msg = Paragraph::new("Terminal too narrow") .alignment(Alignment::Center) - .style(Style::new().fg(ui::TEXT_MUTED)); + .style(Style::new().fg(theme.ui.text_muted)); frame.render_widget(msg, area); return; } @@ -132,6 +134,7 @@ fn render_tile( snapshot: &SequencerSnapshot, step_idx: usize, ) { + let theme = theme::get(); let pattern = app.current_edit_pattern(); let step = pattern.step(step_idx); let is_active = step.map(|s| s.active).unwrap_or(false); @@ -149,26 +152,26 @@ fn render_tile( let link_color = step.and_then(|s| s.source).map(|src| { let i = src % 5; - (tile::LINK_BRIGHT[i], tile::LINK_DIM[i]) + (theme.tile.link_bright[i], theme.tile.link_dim[i]) }); let (bg, fg) = match (is_playing, is_active, is_selected, is_linked, in_selection) { - (true, true, _, _, _) => (tile::PLAYING_ACTIVE_BG, tile::PLAYING_ACTIVE_FG), - (true, false, _, _, _) => (tile::PLAYING_INACTIVE_BG, tile::PLAYING_INACTIVE_FG), + (true, true, _, _, _) => (theme.tile.playing_active_bg, theme.tile.playing_active_fg), + (true, false, _, _, _) => (theme.tile.playing_inactive_bg, theme.tile.playing_inactive_fg), (false, true, true, true, _) => { let (r, g, b) = link_color.unwrap().0; - (Color::Rgb(r, g, b), selection::CURSOR_FG) + (Color::Rgb(r, g, b), theme.selection.cursor_fg) } - (false, true, true, false, _) => (tile::ACTIVE_SELECTED_BG, selection::CURSOR_FG), - (false, true, _, _, true) => (tile::ACTIVE_IN_RANGE_BG, selection::CURSOR_FG), + (false, true, true, false, _) => (theme.tile.active_selected_bg, theme.selection.cursor_fg), + (false, true, _, _, true) => (theme.tile.active_in_range_bg, theme.selection.cursor_fg), (false, true, false, true, _) => { let (r, g, b) = link_color.unwrap().1; - (Color::Rgb(r, g, b), tile::ACTIVE_FG) + (Color::Rgb(r, g, b), theme.tile.active_fg) } - (false, true, false, false, _) => (tile::ACTIVE_BG, tile::ACTIVE_FG), - (false, false, true, _, _) => (selection::SELECTED, selection::CURSOR_FG), - (false, false, _, _, true) => (selection::IN_RANGE, selection::CURSOR_FG), - (false, false, false, _, _) => (tile::INACTIVE_BG, tile::INACTIVE_FG), + (false, true, false, false, _) => (theme.tile.active_bg, theme.tile.active_fg), + (false, false, true, _, _) => (theme.selection.selected, theme.selection.cursor_fg), + (false, false, _, _, true) => (theme.selection.in_range, theme.selection.cursor_fg), + (false, false, false, _, _) => (theme.tile.inactive_bg, theme.tile.inactive_fg), }; let source_idx = step.and_then(|s| s.source); @@ -231,9 +234,10 @@ fn render_tile( } fn render_scope(frame: &mut Frame, app: &App, area: Rect) { + let theme = theme::get(); let scope = Scope::new(&app.metrics.scope) .orientation(Orientation::Horizontal) - .color(meter::LOW); + .color(theme.meter.low); frame.render_widget(scope, area); } @@ -247,4 +251,3 @@ fn render_vu_meter(frame: &mut Frame, app: &App, area: Rect) { let vu = VuMeter::new(app.metrics.peak_left, app.metrics.peak_right); frame.render_widget(vu, area); } - diff --git a/src/views/options_view.rs b/src/views/options_view.rs index cab427e..b7dfdf7 100644 --- a/src/views/options_view.rs +++ b/src/views/options_view.rs @@ -7,13 +7,15 @@ use ratatui::Frame; use crate::app::App; use crate::engine::LinkState; use crate::state::OptionsFocus; -use crate::theme::{hint, link_status, modal, ui, values}; +use crate::theme; pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) { + let theme = theme::get(); + let block = Block::default() .borders(Borders::ALL) .title(" Options ") - .border_style(Style::new().fg(modal::INPUT)); + .border_style(Style::new().fg(theme.modal.input)); let inner = block.inner(area); frame.render_widget(block, area); @@ -32,11 +34,11 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) { let enabled = link.is_enabled(); let peers = link.peers(); let (status_text, status_color) = if !enabled { - ("DISABLED", link_status::DISABLED) + ("DISABLED", theme.link_status.disabled) } else if peers > 0 { - ("CONNECTED", link_status::CONNECTED) + ("CONNECTED", theme.link_status.connected) } else { - ("LISTENING", link_status::LISTENING) + ("LISTENING", theme.link_status.listening) }; let peer_text = if enabled && peers > 0 { if peers == 1 { @@ -51,14 +53,14 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) { let link_header = Line::from(vec![ Span::styled( "ABLETON LINK", - Style::new().fg(ui::HEADER).add_modifier(Modifier::BOLD), + Style::new().fg(theme.ui.header).add_modifier(Modifier::BOLD), ), Span::raw(" "), Span::styled( status_text, Style::new().fg(status_color).add_modifier(Modifier::BOLD), ), - Span::styled(peer_text, Style::new().fg(ui::TEXT_MUTED)), + Span::styled(peer_text, Style::new().fg(theme.ui.text_muted)), ]); // Prepare values @@ -68,28 +70,37 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) { let beat_str = format!("{:.2}", link.beat()); let phase_str = format!("{:.2}", link.phase()); - let tempo_style = Style::new().fg(values::TEMPO).add_modifier(Modifier::BOLD); - let value_style = Style::new().fg(values::VALUE); + let tempo_style = Style::new().fg(theme.values.tempo).add_modifier(Modifier::BOLD); + let value_style = Style::new().fg(theme.values.value); // Build flat list of all lines let lines: Vec = vec![ - // DISPLAY section (lines 0-7) - render_section_header("DISPLAY"), - render_divider(content_width), + // DISPLAY section (lines 0-8) + render_section_header("DISPLAY", &theme), + render_divider(content_width, &theme), + render_option_line( + "Theme", + app.ui.color_scheme.label(), + focus == OptionsFocus::ColorScheme, + &theme, + ), render_option_line( "Refresh rate", app.audio.config.refresh_rate.label(), focus == OptionsFocus::RefreshRate, + &theme, ), render_option_line( "Runtime highlight", if app.ui.runtime_highlight { "On" } else { "Off" }, focus == OptionsFocus::RuntimeHighlight, + &theme, ), render_option_line( "Show scope", if app.audio.config.show_scope { "On" } else { "Off" }, focus == OptionsFocus::ShowScope, + &theme, ), render_option_line( "Show spectrum", @@ -99,22 +110,25 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) { "Off" }, focus == OptionsFocus::ShowSpectrum, + &theme, ), render_option_line( "Completion", if app.ui.show_completion { "On" } else { "Off" }, focus == OptionsFocus::ShowCompletion, + &theme, ), - render_option_line("Flash brightness", &flash_str, focus == OptionsFocus::FlashBrightness), - // Blank line (line 8) + render_option_line("Flash brightness", &flash_str, focus == OptionsFocus::FlashBrightness, &theme), + // Blank line (line 9) Line::from(""), - // ABLETON LINK section (lines 9-14) + // ABLETON LINK section (lines 10-15) link_header, - render_divider(content_width), + render_divider(content_width, &theme), render_option_line( "Enabled", if link.is_enabled() { "On" } else { "Off" }, focus == OptionsFocus::LinkEnabled, + &theme, ), render_option_line( "Start/Stop sync", @@ -124,16 +138,17 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) { "Off" }, focus == OptionsFocus::StartStopSync, + &theme, ), - render_option_line("Quantum", &quantum_str, focus == OptionsFocus::Quantum), - // Blank line (line 15) + render_option_line("Quantum", &quantum_str, focus == OptionsFocus::Quantum, &theme), + // Blank line (line 16) Line::from(""), - // SESSION section (lines 16-21) - render_section_header("SESSION"), - render_divider(content_width), - render_readonly_line("Tempo", &tempo_str, tempo_style), - render_readonly_line("Beat", &beat_str, value_style), - render_readonly_line("Phase", &phase_str, value_style), + // SESSION section (lines 17-22) + render_section_header("SESSION", &theme), + render_divider(content_width, &theme), + render_readonly_line("Tempo", &tempo_str, tempo_style, &theme), + render_readonly_line("Beat", &beat_str, value_style, &theme), + render_readonly_line("Phase", &phase_str, value_style, &theme), ]; let total_lines = lines.len(); @@ -141,15 +156,16 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) { // Map focus to line index let focus_line: usize = match focus { - OptionsFocus::RefreshRate => 2, - OptionsFocus::RuntimeHighlight => 3, - OptionsFocus::ShowScope => 4, - OptionsFocus::ShowSpectrum => 5, - OptionsFocus::ShowCompletion => 6, - OptionsFocus::FlashBrightness => 7, - OptionsFocus::LinkEnabled => 11, - OptionsFocus::StartStopSync => 12, - OptionsFocus::Quantum => 13, + OptionsFocus::ColorScheme => 2, + OptionsFocus::RefreshRate => 3, + OptionsFocus::RuntimeHighlight => 4, + OptionsFocus::ShowScope => 5, + OptionsFocus::ShowSpectrum => 6, + OptionsFocus::ShowCompletion => 7, + OptionsFocus::FlashBrightness => 8, + OptionsFocus::LinkEnabled => 12, + OptionsFocus::StartStopSync => 13, + OptionsFocus::Quantum => 14, }; // Calculate scroll offset to keep focused line visible (centered when possible) @@ -172,7 +188,7 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) { frame.render_widget(Paragraph::new(visible_lines), padded); // Render scroll indicators - let indicator_style = Style::new().fg(ui::TEXT_DIM); + let indicator_style = Style::new().fg(theme.ui.text_dim); let indicator_x = padded.x + padded.width.saturating_sub(1); if scroll_offset > 0 { @@ -192,24 +208,24 @@ pub fn render(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) { } } -fn render_section_header(title: &str) -> Line<'static> { +fn render_section_header(title: &str, theme: &theme::ThemeColors) -> Line<'static> { Line::from(Span::styled( title.to_string(), - Style::new().fg(ui::HEADER).add_modifier(Modifier::BOLD), + Style::new().fg(theme.ui.header).add_modifier(Modifier::BOLD), )) } -fn render_divider(width: usize) -> Line<'static> { +fn render_divider(width: usize, theme: &theme::ThemeColors) -> Line<'static> { Line::from(Span::styled( "─".repeat(width), - Style::new().fg(ui::BORDER), + Style::new().fg(theme.ui.border), )) } -fn render_option_line<'a>(label: &'a str, value: &'a str, focused: bool) -> Line<'a> { - let highlight = Style::new().fg(hint::KEY).add_modifier(Modifier::BOLD); - let normal = Style::new().fg(ui::TEXT_PRIMARY); - let label_style = Style::new().fg(ui::TEXT_MUTED); +fn render_option_line<'a>(label: &'a str, value: &'a str, focused: bool, theme: &theme::ThemeColors) -> Line<'a> { + let highlight = Style::new().fg(theme.hint.key).add_modifier(Modifier::BOLD); + let normal = Style::new().fg(theme.ui.text_primary); + let label_style = Style::new().fg(theme.ui.text_muted); let prefix = if focused { "> " } else { " " }; let prefix_style = if focused { highlight } else { normal }; @@ -231,8 +247,8 @@ fn render_option_line<'a>(label: &'a str, value: &'a str, focused: bool) -> Line ]) } -fn render_readonly_line<'a>(label: &'a str, value: &'a str, value_style: Style) -> Line<'a> { - let label_style = Style::new().fg(ui::TEXT_MUTED); +fn render_readonly_line<'a>(label: &'a str, value: &'a str, value_style: Style, theme: &theme::ThemeColors) -> Line<'a> { + let label_style = Style::new().fg(theme.ui.text_muted); let label_width = 20; let padded_label = format!("{label: (selection::CURSOR, selection::CURSOR_FG, ""), - (false, true, _) => (list::PLAYING_BG, list::PLAYING_FG, "> "), - (false, false, true) => (list::STAGED_PLAY_BG, list::STAGED_PLAY_FG, "+ "), - (false, false, false) if is_selected => (list::HOVER_BG, list::HOVER_FG, ""), - (false, false, false) if is_edit => (list::EDIT_BG, list::EDIT_FG, ""), - (false, false, false) => (ui::BG, ui::TEXT_MUTED, ""), + (true, _, _) => (theme.selection.cursor, theme.selection.cursor_fg, ""), + (false, true, _) => (theme.list.playing_bg, theme.list.playing_fg, "> "), + (false, false, true) => (theme.list.staged_play_bg, theme.list.staged_play_fg, "+ "), + (false, false, false) if is_selected => (theme.list.hover_bg, theme.list.hover_fg, ""), + (false, false, false) if is_edit => (theme.list.edit_bg, theme.list.edit_fg, ""), + (false, false, false) => (theme.ui.bg, theme.ui.text_muted, ""), }; let name = app.project_state.project.banks[idx] @@ -136,7 +137,7 @@ fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area } // Scroll indicators - let indicator_style = Style::new().fg(ui::TEXT_MUTED); + let indicator_style = Style::new().fg(theme.ui.text_muted); if scroll_offset > 0 { let indicator = Paragraph::new("▲") .style(indicator_style) @@ -155,12 +156,13 @@ fn render_banks(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) { use crate::model::PatternSpeed; + let theme = theme::get(); let is_focused = matches!(app.patterns_nav.column, PatternsColumn::Patterns); let [title_area, inner] = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).areas(area); - let title_color = if is_focused { ui::HEADER } else { ui::UNFOCUSED }; + let title_color = if is_focused { theme.ui.header } else { theme.ui.unfocused }; let bank = app.patterns_nav.bank_cursor; let bank_name = app.project_state.project.banks[bank].name.as_deref(); @@ -249,13 +251,13 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a let is_staged_stop = staged_to_stop.contains(&idx); let (bg, fg, prefix) = match (is_cursor, is_playing, is_staged_play, is_staged_stop) { - (true, _, _, _) => (selection::CURSOR, selection::CURSOR_FG, ""), - (false, true, _, true) => (list::STAGED_STOP_BG, list::STAGED_STOP_FG, "- "), - (false, true, _, false) => (list::PLAYING_BG, list::PLAYING_FG, "> "), - (false, false, true, _) => (list::STAGED_PLAY_BG, list::STAGED_PLAY_FG, "+ "), - (false, false, false, _) if is_selected => (list::HOVER_BG, list::HOVER_FG, ""), - (false, false, false, _) if is_edit => (list::EDIT_BG, list::EDIT_FG, ""), - (false, false, false, _) => (ui::BG, ui::TEXT_MUTED, ""), + (true, _, _, _) => (theme.selection.cursor, theme.selection.cursor_fg, ""), + (false, true, _, true) => (theme.list.staged_stop_bg, theme.list.staged_stop_fg, "- "), + (false, true, _, false) => (theme.list.playing_bg, theme.list.playing_fg, "> "), + (false, false, true, _) => (theme.list.staged_play_bg, theme.list.staged_play_fg, "+ "), + (false, false, false, _) if is_selected => (theme.list.hover_bg, theme.list.hover_fg, ""), + (false, false, false, _) if is_edit => (theme.list.edit_bg, theme.list.edit_fg, ""), + (false, false, false, _) => (theme.ui.bg, theme.ui.text_muted, ""), }; let pattern = &app.project_state.project.banks[bank].patterns[idx]; @@ -314,7 +316,7 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a } // Scroll indicators - let indicator_style = Style::new().fg(ui::TEXT_MUTED); + let indicator_style = Style::new().fg(theme.ui.text_muted); if scroll_offset > 0 { let indicator = Paragraph::new("▲") .style(indicator_style) diff --git a/src/views/render.rs b/src/views/render.rs index 1ecbc0d..414c2da 100644 --- a/src/views/render.rs +++ b/src/views/render.rs @@ -18,7 +18,7 @@ use crate::engine::{LinkState, SequencerSnapshot}; use crate::model::{SourceSpan, StepContext, Value}; use crate::page::Page; use crate::state::{FlashKind, Modal, PanelFocus, PatternField, SidePanel, StackCache}; -use crate::theme::{browser, flash, header, hint, modal, search, status, table, ui}; +use crate::theme; use crate::views::highlight::{self, highlight_line, highlight_line_with_runtime}; use crate::widgets::{ ConfirmModal, ModalFrame, NavMinimap, NavTile, SampleBrowser, TextInputModal, @@ -132,16 +132,17 @@ fn adjust_spans_for_line( pub fn render(frame: &mut Frame, app: &App, link: &LinkState, snapshot: &SequencerSnapshot) { let term = frame.area(); + let theme = theme::get(); let bg_color = if app.ui.event_flash > 0.0 { let t = (app.ui.event_flash * app.ui.flash_brightness).min(1.0); - let (base_r, base_g, base_b) = ui::BG_RGB; - let (tgt_r, tgt_g, tgt_b) = flash::EVENT_RGB; + let (base_r, base_g, base_b) = theme.ui.bg_rgb; + let (tgt_r, tgt_g, tgt_b) = theme.flash.event_rgb; let r = base_r + ((tgt_r as f32 - base_r as f32) * t) as u8; let g = base_g + ((tgt_g as f32 - base_g as f32) * t) as u8; let b = base_b + ((tgt_b as f32 - base_b as f32) * t) as u8; Color::Rgb(r, g, b) } else { - ui::BG + theme.ui.bg }; let blank = " ".repeat(term.width as usize); @@ -259,6 +260,7 @@ fn render_header( ) { use crate::model::PatternSpeed; + let theme = theme::get(); let bank = &app.project_state.project.banks[app.editor_ctx.bank]; let pattern = &bank.patterns[app.editor_ctx.pattern]; @@ -300,11 +302,11 @@ fn render_header( // Transport block let (transport_bg, transport_text) = if app.playback.playing { - (status::PLAYING_BG, " ▶ PLAYING ") + (theme.status.playing_bg, " ▶ PLAYING ") } else { - (status::STOPPED_BG, " ■ STOPPED ") + (theme.status.stopped_bg, " ■ STOPPED ") }; - let transport_style = Style::new().bg(transport_bg).fg(ui::TEXT_PRIMARY); + let transport_style = Style::new().bg(transport_bg).fg(theme.ui.text_primary); frame.render_widget( Paragraph::new(transport_text) .style(transport_style) @@ -314,8 +316,8 @@ fn render_header( // Fill indicator let fill = app.live_keys.fill(); - let fill_fg = if fill { status::FILL_ON } else { status::FILL_OFF }; - let fill_style = Style::new().bg(status::FILL_BG).fg(fill_fg); + let fill_fg = if fill { theme.status.fill_on } else { theme.status.fill_off }; + let fill_style = Style::new().bg(theme.status.fill_bg).fg(fill_fg); frame.render_widget( Paragraph::new(if fill { "F" } else { "·" }) .style(fill_style) @@ -325,8 +327,8 @@ fn render_header( // Tempo block let tempo_style = Style::new() - .bg(header::TEMPO_BG) - .fg(ui::TEXT_PRIMARY) + .bg(theme.header.tempo_bg) + .fg(theme.ui.text_primary) .add_modifier(Modifier::BOLD); frame.render_widget( Paragraph::new(format!(" {:.1} BPM ", link.tempo())) @@ -341,7 +343,7 @@ fn render_header( .as_deref() .map(|n| format!(" {n} ")) .unwrap_or_else(|| format!(" Bank {:02} ", app.editor_ctx.bank + 1)); - let bank_style = Style::new().bg(header::BANK_BG).fg(ui::TEXT_PRIMARY); + let bank_style = Style::new().bg(theme.header.bank_bg).fg(theme.ui.text_primary); frame.render_widget( Paragraph::new(bank_name) .style(bank_style) @@ -372,7 +374,7 @@ fn render_header( " {} · {} steps{}{}{} ", pattern_name, pattern.length, speed_info, page_info, iter_info ); - let pattern_style = Style::new().bg(header::PATTERN_BG).fg(ui::TEXT_PRIMARY); + let pattern_style = Style::new().bg(theme.header.pattern_bg).fg(theme.ui.text_primary); frame.render_widget( Paragraph::new(pattern_text) .style(pattern_style) @@ -385,7 +387,7 @@ fn render_header( let peers = link.peers(); let voices = app.metrics.active_voices; let stats_text = format!(" CPU {cpu_pct:.0}% V:{voices} L:{peers} "); - let stats_style = Style::new().bg(header::STATS_BG).fg(header::STATS_FG); + let stats_style = Style::new().bg(theme.header.stats_bg).fg(theme.header.stats_fg); frame.render_widget( Paragraph::new(stats_text) .style(stats_style) @@ -395,27 +397,30 @@ fn render_header( } fn render_footer(frame: &mut Frame, app: &App, area: Rect) { - let block = Block::default().borders(Borders::ALL); + let theme = theme::get(); + let block = Block::default() + .borders(Borders::ALL) + .border_style(Style::new().fg(theme.ui.border)); let inner = block.inner(area); let available_width = inner.width as usize; let page_indicator = match app.page { - Page::Main => "[MAIN]", - Page::Patterns => "[PATTERNS]", - Page::Engine => "[ENGINE]", - Page::Options => "[OPTIONS]", - Page::Help => "[HELP]", - Page::Dict => "[DICT]", + Page::Main => " MAIN ", + Page::Patterns => " PATTERNS ", + Page::Engine => " ENGINE ", + Page::Options => " OPTIONS ", + Page::Help => " HELP ", + Page::Dict => " DICT ", }; let content = if let Some(ref msg) = app.ui.status_message { Line::from(vec![ Span::styled( page_indicator.to_string(), - Style::new().fg(ui::TEXT_PRIMARY).add_modifier(Modifier::DIM), + Style::new().bg(theme.view_badge.bg).fg(theme.view_badge.fg), ), Span::raw(" "), - Span::styled(msg.clone(), Style::new().fg(modal::CONFIRM)), + Span::styled(msg.clone(), Style::new().fg(theme.modal.confirm)), ]) } else { let bindings: Vec<(&str, &str)> = match app.page { @@ -477,7 +482,7 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) { let mut spans = vec![ Span::styled( page_indicator.to_string(), - Style::new().fg(ui::TEXT_PRIMARY).add_modifier(Modifier::DIM), + Style::new().bg(theme.view_badge.bg).fg(theme.view_badge.fg), ), Span::raw(" ".repeat(base_gap + if extra > 0 { 1 } else { 0 })), ]; @@ -485,11 +490,11 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) { for (i, (key, action)) in bindings.into_iter().enumerate() { spans.push(Span::styled( key.to_string(), - Style::new().fg(hint::KEY), + Style::new().fg(theme.hint.key), )); spans.push(Span::styled( - format!(":{action}"), - Style::new().fg(hint::TEXT), + format!(": {action}"), + Style::new().fg(theme.hint.text), )); if i < n - 1 { @@ -506,6 +511,7 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) { } fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term: Rect) { + let theme = theme::get(); match &app.ui.modal { Modal::None => {} Modal::ConfirmQuit { selected } => { @@ -539,8 +545,8 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term use crate::state::file_browser::FileBrowserMode; use crate::widgets::FileBrowserModal; let (title, border_color) = match state.mode { - FileBrowserMode::Save => ("Save As", flash::SUCCESS_FG), - FileBrowserMode::Load => ("Load From", browser::DIRECTORY), + FileBrowserMode::Save => ("Save As", theme.flash.success_fg), + FileBrowserMode::Load => ("Load From", theme.browser.directory), }; let entries: Vec<(String, bool, bool)> = state .entries @@ -558,7 +564,7 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term Modal::RenameBank { bank, name } => { TextInputModal::new(&format!("Rename Bank {:02}", bank + 1), name) .width(40) - .border_color(modal::RENAME) + .border_color(theme.modal.rename) .render_centered(frame, term); } Modal::RenamePattern { @@ -571,13 +577,13 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term name, ) .width(40) - .border_color(modal::RENAME) + .border_color(theme.modal.rename) .render_centered(frame, term); } Modal::RenameStep { step, name, .. } => { TextInputModal::new(&format!("Name Step {:02}", step + 1), name) .width(40) - .border_color(modal::INPUT) + .border_color(theme.modal.input) .render_centered(frame, term); } Modal::SetPattern { field, input } => { @@ -588,14 +594,14 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term TextInputModal::new(title, input) .hint(hint) .width(45) - .border_color(modal::CONFIRM) + .border_color(theme.modal.confirm) .render_centered(frame, term); } Modal::SetTempo(input) => { TextInputModal::new("Set Tempo (20-300 BPM)", input) .hint("Enter BPM") .width(30) - .border_color(modal::RENAME) + .border_color(theme.modal.rename) .render_centered(frame, term); } Modal::AddSamplePath(state) => { @@ -608,7 +614,7 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term FileBrowserModal::new("Add Sample Path", &state.input, &entries) .selected(state.selected) .scroll_offset(state.scroll_offset) - .border_color(modal::RENAME) + .border_color(theme.modal.rename) .width(60) .height(18) .render_centered(frame, term); @@ -633,14 +639,14 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term let inner = ModalFrame::new(&title) .width(width) .height(height) - .border_color(modal::PREVIEW) + .border_color(theme.modal.preview) .render_centered(frame, term); let script = pattern.resolve_script(step_idx).unwrap_or(""); if script.is_empty() { let empty = Paragraph::new("(empty)") .alignment(Alignment::Center) - .style(Style::new().fg(ui::TEXT_DIM)); + .style(Style::new().fg(theme.ui.text_dim)); let centered_area = Rect { y: inner.y + inner.height / 2, height: 1, @@ -695,10 +701,10 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term let flash_kind = app.ui.flash_kind(); let border_color = match flash_kind { - Some(FlashKind::Error) => flash::ERROR_FG, - Some(FlashKind::Info) => ui::TEXT_PRIMARY, - Some(FlashKind::Success) => flash::SUCCESS_FG, - None => modal::EDITOR, + Some(FlashKind::Error) => theme.flash.error_fg, + Some(FlashKind::Info) => theme.ui.text_primary, + Some(FlashKind::Success) => theme.flash.success_fg, + None => theme.modal.editor, }; let title = if let Some(ref name) = step.and_then(|s| s.name.as_ref()) { @@ -765,9 +771,9 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term if let Some(sa) = search_area { let style = if app.editor_ctx.editor.search_active() { - Style::default().fg(search::ACTIVE) + Style::default().fg(theme.search.active) } else { - Style::default().fg(search::INACTIVE) + Style::default().fg(theme.search.inactive) }; let cursor = if app.editor_ctx.editor.search_active() { "_" @@ -780,9 +786,9 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term if let Some(kind) = flash_kind { let bg = match kind { - FlashKind::Error => flash::ERROR_BG, - FlashKind::Info => flash::INFO_BG, - FlashKind::Success => flash::SUCCESS_BG, + FlashKind::Error => theme.flash.error_bg, + FlashKind::Info => theme.flash.info_bg, + FlashKind::Success => theme.flash.success_bg, }; let flash_block = Block::default().style(Style::default().bg(bg)); frame.render_widget(flash_block, editor_area); @@ -791,8 +797,8 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term .editor .render(frame, editor_area, &highlighter); - let dim = Style::default().fg(hint::TEXT); - let key = Style::default().fg(hint::KEY); + let dim = Style::default().fg(theme.hint.text); + let key = Style::default().fg(theme.hint.key); if app.editor_ctx.editor.search_active() { let hint = Line::from(vec![ @@ -860,7 +866,7 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term let block = Block::bordered() .title(format!(" Pattern B{:02}:P{:02} ", bank + 1, pattern + 1)) - .border_style(Style::default().fg(modal::INPUT)); + .border_style(Style::default().fg(theme.modal.input)); let inner = block.inner(area); frame.render_widget(Clear, area); @@ -895,14 +901,14 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term let (label_style, value_style) = if *selected { ( Style::default() - .fg(hint::KEY) + .fg(theme.hint.key) .add_modifier(Modifier::BOLD), - Style::default().fg(ui::TEXT_PRIMARY).bg(ui::SURFACE), + Style::default().fg(theme.ui.text_primary).bg(theme.ui.surface), ) } else { ( - Style::default().fg(ui::TEXT_MUTED), - Style::default().fg(ui::TEXT_PRIMARY), + Style::default().fg(theme.ui.text_muted), + Style::default().fg(theme.ui.text_primary), ) }; @@ -918,14 +924,14 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term let hint_area = Rect::new(inner.x, inner.y + inner.height - 1, inner.width, 1); let hint_line = Line::from(vec![ - Span::styled("↑↓", Style::default().fg(hint::KEY)), - Span::styled(" nav ", Style::default().fg(hint::TEXT)), - Span::styled("←→", Style::default().fg(hint::KEY)), - Span::styled(" change ", Style::default().fg(hint::TEXT)), - Span::styled("Enter", Style::default().fg(hint::KEY)), - Span::styled(" save ", Style::default().fg(hint::TEXT)), - Span::styled("Esc", Style::default().fg(hint::KEY)), - Span::styled(" cancel", Style::default().fg(hint::TEXT)), + Span::styled("↑↓", Style::default().fg(theme.hint.key)), + Span::styled(" nav ", Style::default().fg(theme.hint.text)), + Span::styled("←→", Style::default().fg(theme.hint.key)), + Span::styled(" change ", Style::default().fg(theme.hint.text)), + Span::styled("Enter", Style::default().fg(theme.hint.key)), + Span::styled(" save ", Style::default().fg(theme.hint.text)), + Span::styled("Esc", Style::default().fg(theme.hint.key)), + Span::styled(" cancel", Style::default().fg(theme.hint.text)), ]); frame.render_widget(Paragraph::new(hint_line), hint_area); } @@ -937,7 +943,7 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term let inner = ModalFrame::new(&title) .width(width) .height(height) - .border_color(modal::EDITOR) + .border_color(theme.modal.editor) .render_centered(frame, term); let bindings = super::keybindings::bindings_for(app.page); @@ -949,11 +955,11 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term .skip(*scroll) .take(visible_rows) .map(|(i, (key, name, desc))| { - let bg = if i % 2 == 0 { table::ROW_EVEN } else { table::ROW_ODD }; + let bg = if i % 2 == 0 { theme.table.row_even } else { theme.table.row_odd }; Row::new(vec![ - Cell::from(*key).style(Style::default().fg(modal::CONFIRM)), - Cell::from(*name).style(Style::default().fg(modal::INPUT)), - Cell::from(*desc).style(Style::default().fg(ui::TEXT_PRIMARY)), + Cell::from(*key).style(Style::default().fg(theme.modal.confirm)), + Cell::from(*name).style(Style::default().fg(theme.modal.input)), + Cell::from(*desc).style(Style::default().fg(theme.ui.text_primary)), ]) .style(Style::default().bg(bg)) }) @@ -984,12 +990,12 @@ fn render_modal(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, term height: 1, }; let keybind_hint = Line::from(vec![ - Span::styled("↑↓", Style::default().fg(hint::KEY)), - Span::styled(" scroll ", Style::default().fg(hint::TEXT)), - Span::styled("PgUp/Dn", Style::default().fg(hint::KEY)), - Span::styled(" page ", Style::default().fg(hint::TEXT)), - Span::styled("Esc/?", Style::default().fg(hint::KEY)), - Span::styled(" close", Style::default().fg(hint::TEXT)), + Span::styled("↑↓", Style::default().fg(theme.hint.key)), + Span::styled(" scroll ", Style::default().fg(theme.hint.text)), + Span::styled("PgUp/Dn", Style::default().fg(theme.hint.key)), + Span::styled(" page ", Style::default().fg(theme.hint.text)), + Span::styled("Esc/?", Style::default().fg(theme.hint.key)), + Span::styled(" close", Style::default().fg(theme.hint.text)), ]); frame.render_widget(Paragraph::new(keybind_hint).alignment(Alignment::Right), hint_area); } diff --git a/src/views/title_view.rs b/src/views/title_view.rs index a497916..7b4a5a6 100644 --- a/src/views/title_view.rs +++ b/src/views/title_view.rs @@ -6,18 +6,19 @@ use ratatui::Frame; use tui_big_text::{BigText, PixelSize}; use crate::state::ui::UiState; -use crate::theme::title; +use crate::theme; pub fn render(frame: &mut Frame, area: Rect, ui: &UiState) { + let theme = theme::get(); frame.render_widget(&ui.sparkles, area); - let author_style = Style::new().fg(title::AUTHOR); - let link_style = Style::new().fg(title::LINK); - let license_style = Style::new().fg(title::LICENSE); + let author_style = Style::new().fg(theme.title.author); + let link_style = Style::new().fg(theme.title.link); + let license_style = Style::new().fg(theme.title.license); let big_title = BigText::builder() .pixel_size(PixelSize::Quadrant) - .style(Style::new().fg(title::BIG_TITLE).bold()) + .style(Style::new().fg(theme.title.big_title).bold()) .lines(vec!["CAGIRE".into()]) .centered() .build(); @@ -26,7 +27,7 @@ pub fn render(frame: &mut Frame, area: Rect, ui: &UiState) { Line::from(""), Line::from(Span::styled( "A Forth Music Sequencer", - Style::new().fg(title::SUBTITLE), + Style::new().fg(theme.title.subtitle), )), Line::from(""), Line::from(Span::styled("by BuboBubo", author_style)), @@ -38,7 +39,7 @@ pub fn render(frame: &mut Frame, area: Rect, ui: &UiState) { Line::from(""), Line::from(Span::styled( "Press any key to continue", - Style::new().fg(title::PROMPT), + Style::new().fg(theme.title.prompt), )), ];