use anstyle::{AnsiColor, Color as AnsiColorEnum, RgbColor};
use ratatui::prelude::*;
use crate::config::constants::ui;
use crate::ui::tui::{
style::{ratatui_color_from_ansi, ratatui_style_from_inline},
types::{InlineMessageKind, InlineTextStyle, InlineTheme},
};
use super::message::MessageLine;
fn mix(color: RgbColor, target: RgbColor, ratio: f64) -> RgbColor {
let ratio = ratio.clamp(ui::THEME_MIX_RATIO_MIN, ui::THEME_MIX_RATIO_MAX);
let blend = |c: u8, t: u8| -> u8 {
let c = c as f64;
let t = t as f64;
((c + (t - c) * ratio).round()).clamp(ui::THEME_BLEND_CLAMP_MIN, ui::THEME_BLEND_CLAMP_MAX)
as u8
};
RgbColor(
blend(color.0, target.0),
blend(color.1, target.1),
blend(color.2, target.2),
)
}
fn highlight_foreground_for(background: AnsiColorEnum) -> Color {
match background {
AnsiColorEnum::Rgb(rgb) => {
let luminance = (0.2126 * f32::from(rgb.0) / 255.0)
+ (0.7152 * f32::from(rgb.1) / 255.0)
+ (0.0722 * f32::from(rgb.2) / 255.0);
if luminance >= 0.55 {
Color::Black
} else {
Color::White
}
}
AnsiColorEnum::Ansi(color) => match color {
AnsiColor::White
| AnsiColor::BrightWhite
| AnsiColor::Yellow
| AnsiColor::BrightYellow
| AnsiColor::Cyan
| AnsiColor::BrightCyan
| AnsiColor::Green
| AnsiColor::BrightGreen => Color::Black,
_ => Color::White,
},
AnsiColorEnum::Ansi256(value) => {
if value.index() >= 244 {
Color::Black
} else {
Color::White
}
}
}
}
pub fn normalize_tool_name(tool_name: &str) -> &'static str {
match tool_name.to_lowercase().as_str() {
"grep" | "rg" | "ripgrep" | "grep_file" | "search" | "find" | "ag" => "search",
"list" | "ls" | "dir" | "list_files" => "list",
"read" | "cat" | "file" | "read_file" => "read",
"write" | "edit" | "save" | "insert" | "edit_file" => "write",
"run" | "command" | "bash" | "sh" => "run",
_ => "other",
}
}
pub fn tool_inline_style_for(tool_name: &str, theme: &InlineTheme) -> InlineTextStyle {
let normalized_name = normalize_tool_name(tool_name);
let mut style = InlineTextStyle::default().bold();
style.color = match normalized_name {
"read" => Some(AnsiColor::Cyan.into()),
"list" => Some(AnsiColor::Green.into()),
"search" => Some(AnsiColor::Cyan.into()),
"write" => Some(AnsiColor::Magenta.into()),
"run" => Some(AnsiColor::Red.into()),
"git" | "version_control" => Some(AnsiColor::Cyan.into()),
_ => theme.tool_accent.or(theme.primary).or(theme.foreground),
};
style
}
pub struct SessionStyles {
theme: InlineTheme,
}
impl SessionStyles {
pub fn new(theme: InlineTheme) -> Self {
Self { theme }
}
#[allow(dead_code)]
pub fn theme(&self) -> &InlineTheme {
&self.theme
}
pub fn set_theme(&mut self, theme: InlineTheme) {
self.theme = theme;
}
pub fn modal_list_highlight_style(&self) -> Style {
let accent = self
.theme
.primary
.or(self.theme.tool_accent)
.or(self.theme.foreground);
let mut style = self.default_style().add_modifier(Modifier::BOLD);
if let Some(accent) = accent {
style = style
.bg(ratatui_color_from_ansi(accent))
.fg(highlight_foreground_for(accent));
}
style
}
#[allow(dead_code)]
pub fn tool_inline_style(&self, tool_name: &str) -> InlineTextStyle {
tool_inline_style_for(tool_name, &self.theme)
}
pub fn tool_border_style(&self) -> InlineTextStyle {
self.border_inline_style()
}
pub fn default_style(&self) -> Style {
let mut style = Style::default();
if let Some(background) = self.theme.background.map(ratatui_color_from_ansi) {
style = style.bg(background);
}
if let Some(foreground) = self.theme.foreground.map(ratatui_color_from_ansi) {
style = style.fg(foreground);
}
style
}
#[allow(dead_code)]
pub fn default_inline_style(&self) -> InlineTextStyle {
InlineTextStyle {
color: self.theme.foreground,
..InlineTextStyle::default()
}
}
pub fn accent_inline_style(&self) -> InlineTextStyle {
InlineTextStyle {
color: self.theme.primary.or(self.theme.foreground),
..InlineTextStyle::default()
}
}
pub fn accent_style(&self) -> Style {
ratatui_style_from_inline(&self.accent_inline_style(), self.theme.foreground)
}
pub fn transcript_link_style(&self) -> Style {
let style = InlineTextStyle {
color: self
.theme
.tool_accent
.or(self.theme.primary)
.or(self.theme.foreground),
..InlineTextStyle::default()
};
ratatui_style_from_inline(&style, self.theme.foreground)
}
pub fn border_inline_style(&self) -> InlineTextStyle {
InlineTextStyle {
color: self.theme.secondary.or(self.theme.foreground),
..InlineTextStyle::default()
}
}
pub fn border_style(&self) -> Style {
self.dimmed_border_style(true)
}
pub fn dimmed_border_style(&self, suppress_bold: bool) -> Style {
let mut style =
ratatui_style_from_inline(&self.border_inline_style(), self.theme.foreground)
.add_modifier(Modifier::DIM);
if suppress_bold {
style = style.remove_modifier(Modifier::BOLD);
}
style
}
pub fn input_background_style(&self) -> Style {
let mut style = self.default_style();
let Some(background) = self.theme.background else {
return style;
};
let resolved = match (background, self.theme.foreground) {
(AnsiColorEnum::Rgb(bg), Some(AnsiColorEnum::Rgb(fg))) => {
AnsiColorEnum::Rgb(mix(bg, fg, ui::THEME_INPUT_BACKGROUND_MIX_RATIO))
}
(color, _) => color,
};
style = style.bg(ratatui_color_from_ansi(resolved));
style
}
pub fn prefix_style(&self, line: &MessageLine) -> InlineTextStyle {
let fallback = self.text_fallback(line.kind).or(self.theme.foreground);
let color = line
.segments
.iter()
.find_map(|segment| segment.style.color)
.or(fallback);
InlineTextStyle {
color,
..InlineTextStyle::default()
}
}
pub fn text_fallback(&self, kind: InlineMessageKind) -> Option<AnsiColorEnum> {
match kind {
InlineMessageKind::Agent => self.theme.foreground.or(self.theme.primary),
InlineMessageKind::Policy => self.theme.primary.or(self.theme.foreground),
InlineMessageKind::User => self.theme.secondary.or(self.theme.foreground),
InlineMessageKind::Tool | InlineMessageKind::Error => {
self.theme.primary.or(self.theme.foreground)
}
InlineMessageKind::Pty => self
.theme
.pty_body
.or(self.theme.tool_body)
.or(self.theme.foreground),
InlineMessageKind::Info => self.theme.foreground,
InlineMessageKind::Warning => Some(AnsiColor::Red.into()),
}
}
pub fn message_divider_style(&self, kind: InlineMessageKind) -> Style {
let mut style = InlineTextStyle::default();
if kind == InlineMessageKind::User {
style.color = self.theme.primary.or(self.theme.foreground);
} else {
style.color = self.text_fallback(kind).or(self.theme.foreground);
}
let resolved = ratatui_style_from_inline(&style, self.theme.foreground);
resolved.add_modifier(Modifier::DIM)
}
}