use oino_ui::style::{Color as Token, Modifier as TokenModifier, Style as TokenStyle};
use oino_ui::text::{Alignment, Line as TokenLine, Span as TokenSpan, Text as TokenText};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span, Text};
use crate::theme::Theme;
const CATEGORY: [Color; 12] = [
Color::Rgb(0x69, 0xad, 0xd0),
Color::Rgb(0x68, 0xb7, 0x8d),
Color::Rgb(0xcd, 0x9a, 0x22),
Color::Rgb(0xa7, 0x98, 0xd7),
Color::Rgb(0xea, 0x7f, 0x81),
Color::Rgb(0x5d, 0xb3, 0xba),
Color::Rgb(0xde, 0x8d, 0x5a),
Color::Rgb(0x97, 0x76, 0xe5),
Color::Rgb(0xde, 0x54, 0xc1),
Color::Rgb(0x61, 0x85, 0xcc),
Color::Rgb(0x5e, 0xb2, 0x86),
Color::Rgb(0xfe, 0x54, 0x62),
];
#[must_use]
pub(crate) fn token(token: &Token, theme: &Theme) -> Option<Color> {
Some(match token {
Token::Reset => Color::Reset,
Token::Text | Token::Assistant => theme.text,
Token::Muted => theme.muted,
Token::Dim => theme.dim,
Token::Accent | Token::Primary => theme.accent,
Token::Info | Token::Link => theme.md_link,
Token::Success => theme.success,
Token::Warning => theme.warning,
Token::Error => theme.error,
Token::Harness | Token::BorderAccent => theme.border_accent,
Token::Thinking => theme.thinking_text,
Token::Tool => theme.tool_title,
Token::User => theme.user_message_text,
Token::Code => theme.md_code,
Token::Border => theme.border,
Token::BorderMuted => theme.border_muted,
Token::C1 => CATEGORY[0],
Token::C2 => CATEGORY[1],
Token::C3 => CATEGORY[2],
Token::C4 => CATEGORY[3],
Token::C5 => CATEGORY[4],
Token::C6 => CATEGORY[5],
Token::C7 => CATEGORY[6],
Token::C8 => CATEGORY[7],
Token::C9 => CATEGORY[8],
Token::C10 => CATEGORY[9],
Token::C11 => CATEGORY[10],
Token::C12 => CATEGORY[11],
Token::Surface1 | Token::InfoBg => theme.custom_message_bg,
Token::Surface2 => theme.user_message_bg,
Token::Surface3 | Token::SelectedBg | Token::AccentBg => theme.selected_bg,
Token::SuccessBg => theme.tool_success_bg,
Token::ErrorBg => theme.tool_error_bg,
Token::WarningBg => theme.tool_pending_bg,
Token::DiffAdded => theme.diff_added,
Token::DiffRemoved => theme.diff_removed,
Token::DiffContext => theme.diff_context,
Token::Other(_) => return None,
})
}
#[must_use]
pub(crate) fn modifier(m: TokenModifier) -> Modifier {
match m {
TokenModifier::BOLD => Modifier::BOLD,
TokenModifier::DIM => Modifier::DIM,
TokenModifier::ITALIC => Modifier::ITALIC,
TokenModifier::UNDERLINED => Modifier::UNDERLINED,
TokenModifier::SLOW_BLINK => Modifier::SLOW_BLINK,
TokenModifier::RAPID_BLINK => Modifier::RAPID_BLINK,
TokenModifier::REVERSED => Modifier::REVERSED,
TokenModifier::HIDDEN => Modifier::HIDDEN,
TokenModifier::CROSSED_OUT => Modifier::CROSSED_OUT,
}
}
#[must_use]
pub(crate) fn style(source: &TokenStyle, base: Style, theme: &Theme) -> Style {
let mut out = base;
if let Some(fg) = source.fg.as_ref().and_then(|t| token(t, theme)) {
out = out.fg(fg);
}
if let Some(bg) = source.bg.as_ref().and_then(|t| token(t, theme)) {
out = out.bg(bg);
}
if let Some(color) = source
.underline_color
.as_ref()
.and_then(|t| token(t, theme))
{
out = out.underline_color(color);
}
for m in source.add_modifier.iter() {
out = out.add_modifier(modifier(m));
}
for m in source.sub_modifier.iter() {
out = out.remove_modifier(modifier(m));
}
out
}
#[must_use]
pub(crate) fn patch(style: Option<&TokenStyle>, base: Style, theme: &Theme) -> Style {
style.map_or(base, |s| self::style(s, base, theme))
}
#[must_use]
pub(crate) fn alignment(alignment: Option<Alignment>) -> ratatui::layout::Alignment {
match alignment {
Some(Alignment::Center) => ratatui::layout::Alignment::Center,
Some(Alignment::Right) => ratatui::layout::Alignment::Right,
Some(Alignment::Left) | None => ratatui::layout::Alignment::Left,
}
}
#[must_use]
pub(crate) fn span(span: &TokenSpan, base: Style, theme: &Theme) -> Span<'static> {
let mut style = patch(span.style(), base, theme);
if span.link().is_some() {
style = style.add_modifier(Modifier::UNDERLINED);
}
Span::styled(span.content().to_owned(), style)
}
#[must_use]
pub(crate) fn line(line: &TokenLine, base: Style, theme: &Theme) -> Line<'static> {
let line_style = patch(line.style(), base, theme);
let spans: Vec<Span<'static>> = line
.spans()
.iter()
.map(|s| span(s, line_style, theme))
.collect();
Line::from(spans)
.style(line_style)
.alignment(alignment(line.alignment()))
}
#[must_use]
pub(crate) fn text(text: &TokenText, base: Style, theme: &Theme) -> Text<'static> {
let style = patch(text.style(), base, theme);
let lines: Vec<Line<'static>> = text
.lines()
.iter()
.map(|l| {
let line_style = patch(l.style(), style, theme);
let spans: Vec<Span<'static>> = l
.spans()
.iter()
.map(|s| span(s, line_style, theme))
.collect();
Line::from(spans)
.style(line_style)
.alignment(alignment(l.alignment().or(text.alignment())))
})
.collect();
Text::from(lines).style(style)
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
reason = "tests assert by unwrapping"
)]
use super::*;
use crate::theme::DARK;
use oino_ui::text::Span as OSpan;
#[test]
fn tokens_resolve_and_unknowns_drop() {
assert_eq!(token(&Token::Accent, &DARK), Some(DARK.accent));
assert_eq!(token(&Token::Primary, &DARK), Some(DARK.accent));
assert_eq!(token(&Token::C1, &DARK), Some(CATEGORY[0]));
assert_eq!(token(&Token::Other("neon".into()), &DARK), None);
let patched = style(
&TokenStyle::new().fg(Token::Other("neon".into())),
Style::default(),
&DARK,
);
assert_eq!(patched.fg, Style::default().fg);
}
#[test]
fn text_patches_styles_and_links() {
let source = TokenText::Str("a\nb".into());
let rendered = text(&source, Style::default().fg(DARK.text), &DARK);
assert_eq!(rendered.lines.len(), 2);
let linked = TokenSpan::linked("docs", "https://example.com");
let rendered_span = span(&linked, Style::default(), &DARK);
assert!(
rendered_span
.style
.add_modifier
.contains(Modifier::UNDERLINED)
);
let source = TokenLine::from_spans(vec![OSpan::Str("x".into())]);
let rendered = line(&source, Style::default(), &DARK);
assert_eq!(rendered.spans[0].content, "x");
}
}
use oino_ui::widgets::block::{
Block as TokenBlock, BlockProps, BlockTitle, BorderSide, BorderType, Borders, Padding,
TitlePosition,
};
use ratatui::widgets::{
Block, BorderType as RBorderType, Borders as RBorders, Padding as RPadding,
};
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct BlockSpec<'a> {
pub(crate) title: Option<&'a BlockTitle>,
pub(crate) title_alignment: Option<Alignment>,
pub(crate) borders: Option<&'a Borders>,
pub(crate) border_type: Option<BorderType>,
pub(crate) border_style: Option<&'a TokenStyle>,
pub(crate) title_style: Option<&'a TokenStyle>,
pub(crate) padding: Option<&'a Padding>,
pub(crate) rail: Option<&'a Token>,
}
impl<'a> From<&'a BlockProps> for BlockSpec<'a> {
fn from(props: &'a BlockProps) -> Self {
Self {
title: props.title.as_ref(),
title_alignment: props.title_alignment,
borders: props.borders.as_ref(),
border_type: props.border_type,
border_style: props.border_style.as_ref(),
title_style: props.title_style.as_ref(),
padding: props.padding.as_ref(),
rail: props.rail.as_ref(),
}
}
}
impl<'a> From<&'a TokenBlock> for BlockSpec<'a> {
fn from(block: &'a TokenBlock) -> Self {
Self {
title: block.title.as_ref(),
title_alignment: block.title_alignment,
borders: block.borders.as_ref(),
border_type: block.border_type,
border_style: block.border_style.as_ref(),
title_style: block.title_style.as_ref(),
padding: block.padding.as_ref(),
rail: block.rail.as_ref(),
}
}
}
fn borders(spec: &BlockSpec<'_>) -> RBorders {
let mut out = match spec.borders {
None | Some(Borders::None_ | Borders::Other(_)) => RBorders::NONE,
Some(Borders::All) => RBorders::ALL,
Some(Borders::Sides(sides)) => {
let mut b = RBorders::NONE;
for side in sides {
b |= match side {
BorderSide::TOP => RBorders::TOP,
BorderSide::RIGHT => RBorders::RIGHT,
BorderSide::BOTTOM => RBorders::BOTTOM,
BorderSide::LEFT => RBorders::LEFT,
};
}
b
}
};
if spec.rail.is_some() {
out |= RBorders::LEFT;
}
out
}
fn border_type(border_type: Option<BorderType>) -> RBorderType {
match border_type.unwrap_or_default() {
BorderType::Plain => RBorderType::Plain,
BorderType::Rounded => RBorderType::Rounded,
BorderType::Double => RBorderType::Double,
BorderType::Thick => RBorderType::Thick,
}
}
fn padding(padding: Option<&Padding>) -> RPadding {
match padding {
None => RPadding::default(),
Some(Padding::Uniform(n)) => RPadding::uniform(*n),
Some(Padding::Rect {
left,
right,
top,
bottom,
}) => RPadding::new(
left.unwrap_or(0),
right.unwrap_or(0),
top.unwrap_or(0),
bottom.unwrap_or(0),
),
}
}
#[must_use]
pub(crate) fn block(
spec: &BlockSpec<'_>,
marker: Option<bool>,
base: Style,
theme: &Theme,
) -> Block<'static> {
let mut out = Block::default()
.borders(borders(spec))
.border_type(border_type(spec.border_type));
let border_style = match spec.border_style {
Some(style) => patch(Some(style), base, theme),
None => base.fg(theme.border),
};
let border_style = match spec.rail.and_then(|t| token(t, theme)) {
Some(rail) => border_style.fg(rail),
None => border_style,
};
out = out.border_style(border_style);
out = out.padding(padding(spec.padding));
let title_style = patch(spec.title_style, base, theme);
if let Some(title) = spec.title {
match title {
BlockTitle::Line(title_line) => {
let mut line = line(title_line, title_style, theme);
if let Some(collapsed) = marker {
let glyph = if collapsed { "▸ " } else { "▾ " };
let mut spans = vec![Span::styled(glyph, title_style)];
spans.extend(line.spans);
line = Line::from(spans);
}
out = out.title(line);
out = out.title_alignment(alignment(spec.title_alignment));
}
BlockTitle::Titles(titles) => {
for title in titles {
let mut line = line(&title.content, title_style, theme);
if let Some(collapsed) = marker
&& title.position != Some(TitlePosition::Bottom)
{
let glyph = if collapsed { "▸ " } else { "▾ " };
let mut spans = vec![Span::styled(glyph, title_style)];
spans.extend(line.spans);
line = Line::from(spans);
}
let alignment = alignment(title.alignment.or(spec.title_alignment));
out = match title.position {
Some(TitlePosition::Bottom) => out.title_bottom(line),
_ => out.title_top(line),
};
out = out.title_alignment(alignment);
}
}
}
}
out
}