use ratatui::style::{Color, Modifier, Style};
use ratatui::text::Span;
use crate::config::Theme;
use crate::domain::highlight::TokenKind;
pub const SELECTION_BG: Color = Color::Rgb(45, 55, 70);
pub const FOCUS_BG: Color = Color::Rgb(34, 42, 54);
pub const INPUT_CURSOR: Color = Color::Rgb(214, 92, 92);
pub const ERROR: Color = Color::Rgb(224, 108, 108);
const FALLBACK_ACCENT: Color = Color::Rgb(109, 208, 255);
pub fn parse_color(value: &str) -> Color {
if let Some(color) = parse_hex(value) {
return color;
}
match value.trim().to_lowercase().as_str() {
"cyan" => Color::Rgb(109, 208, 255),
"green" => Color::Rgb(140, 200, 140),
"magenta" => Color::Rgb(200, 140, 200),
"yellow" => Color::Rgb(214, 196, 120),
"blue" => Color::Rgb(120, 160, 220),
_ => FALLBACK_ACCENT,
}
}
fn parse_hex(value: &str) -> Option<Color> {
let hex = value.trim().strip_prefix('#')?;
if hex.len() != 6 {
return None;
}
let red = u8::from_str_radix(&hex[0..2], 16).ok()?;
let green = u8::from_str_radix(&hex[2..4], 16).ok()?;
let blue = u8::from_str_radix(&hex[4..6], 16).ok()?;
Some(Color::Rgb(red, green, blue))
}
pub fn dim() -> Style {
Style::default().add_modifier(Modifier::DIM)
}
#[derive(Debug, Clone)]
pub struct Highlight {
function: Style,
constant: Style,
operator: Style,
number: Style,
variable: Style,
unit: Style,
paren: Style,
ans: Style,
comment: Style,
plain: Style,
}
impl Highlight {
pub fn from_theme(theme: &Theme) -> Self {
Highlight {
function: Style::default().fg(parse_color(&theme.function_color)),
constant: Style::default().fg(parse_color(&theme.constant_color)),
operator: Style::default()
.fg(parse_color(&theme.operator_color))
.add_modifier(Modifier::BOLD),
number: Style::default().fg(parse_color(&theme.number_color)),
variable: Style::default().fg(parse_color(&theme.variable_color)),
unit: Style::default().fg(parse_color(&theme.unit_color)),
paren: Style::default().add_modifier(Modifier::DIM),
ans: Style::default()
.fg(parse_color(&theme.ans_color))
.add_modifier(Modifier::UNDERLINED),
comment: Style::default()
.fg(parse_color(&theme.comment_color))
.add_modifier(Modifier::ITALIC),
plain: Style::default(),
}
}
pub fn style(&self, kind: TokenKind) -> Style {
match kind {
TokenKind::Function => self.function,
TokenKind::Constant => self.constant,
TokenKind::Operator => self.operator,
TokenKind::Number => self.number,
TokenKind::Variable => self.variable,
TokenKind::Unit => self.unit,
TokenKind::Paren => self.paren,
TokenKind::Ans => self.ans,
TokenKind::Comment => self.comment,
TokenKind::Plain => self.plain,
}
}
}
pub fn styles_for(kinds: &[TokenKind], highlight: &Highlight) -> Vec<Style> {
kinds.iter().map(|kind| highlight.style(*kind)).collect()
}
pub fn cursor_block_span() -> Span<'static> {
Span::styled("\u{2588}", Style::default().fg(INPUT_CURSOR))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_hex_colours() {
assert_eq!(parse_color("#6dd0ff"), Color::Rgb(109, 208, 255));
assert_eq!(parse_color("#000000"), Color::Rgb(0, 0, 0));
}
#[test]
fn falls_back_on_malformed_input() {
assert_eq!(parse_color("not-a-color"), FALLBACK_ACCENT);
assert_eq!(parse_color("#12"), FALLBACK_ACCENT);
}
#[test]
fn highlight_maps_theme_colours_and_makes_operators_bold() {
let theme = Theme::default();
let highlight = Highlight::from_theme(&theme);
assert_eq!(
highlight.style(TokenKind::Function).fg,
Some(parse_color(&theme.function_color))
);
let ans = highlight.style(TokenKind::Ans);
assert_eq!(ans.fg, Some(parse_color(&theme.ans_color)));
assert!(ans.add_modifier.contains(Modifier::UNDERLINED));
let operator = highlight.style(TokenKind::Operator);
assert_eq!(operator.fg, Some(parse_color(&theme.operator_color)));
assert!(operator.add_modifier.contains(Modifier::BOLD));
assert!(
highlight
.style(TokenKind::Paren)
.add_modifier
.contains(Modifier::DIM)
);
}
}