feat(tui): add role‑based dimmed message border style and color utilities

- Introduce `message_border_style` to render message borders with a dimmed version of the role color.
- Add `dim_color` and `color_to_rgb` helpers for color manipulation.
- Update role styling to use `theme.mode_command` for system messages.
- Adjust card rendering functions to accept role and apply the new border style.
This commit is contained in:
2025-10-13 23:45:04 +02:00
parent 990f93d467
commit ee58b0ac32

View File

@@ -15,7 +15,7 @@ use owlen_core::{
ui::{AppState, AutoScroll, FocusedPanel, InputMode, RoleLabelDisplay}, ui::{AppState, AutoScroll, FocusedPanel, InputMode, RoleLabelDisplay},
}; };
use pathdiff::diff_paths; use pathdiff::diff_paths;
use ratatui::style::{Modifier, Style}; use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span}; use ratatui::text::{Line, Span};
use textwrap::{Options, WordSeparator, wrap}; use textwrap::{Options, WordSeparator, wrap};
use tokio::{ use tokio::{
@@ -1806,11 +1806,24 @@ impl ChatApp {
match role { match role {
Role::User => Style::default().fg(theme.user_message_role), Role::User => Style::default().fg(theme.user_message_role),
Role::Assistant => Style::default().fg(theme.assistant_message_role), Role::Assistant => Style::default().fg(theme.assistant_message_role),
Role::System => Style::default().fg(theme.unfocused_panel_border), Role::System => Style::default().fg(theme.mode_command),
Role::Tool => Style::default().fg(theme.info), Role::Tool => Style::default().fg(theme.info),
} }
} }
fn message_border_style(theme: &Theme, role: &Role) -> Style {
let base_color = match role {
Role::User => theme.user_message_role,
Role::Assistant => theme.assistant_message_role,
Role::System => theme.mode_command,
Role::Tool => theme.info,
};
let dimmed = Self::dim_color(base_color);
Style::default().fg(dimmed).add_modifier(Modifier::DIM)
}
fn content_style(theme: &Theme, role: &Role) -> Style { fn content_style(theme: &Theme, role: &Role) -> Style {
if matches!(role, Role::Tool) { if matches!(role, Role::Tool) {
Style::default().fg(theme.tool_output) Style::default().fg(theme.tool_output)
@@ -1819,6 +1832,46 @@ impl ChatApp {
} }
} }
fn dim_color(color: Color) -> Color {
match color {
Color::Reset | Color::Indexed(_) => color,
_ => {
if let Some((r, g, b)) = Self::color_to_rgb(color) {
let dim_component = |component: u8| -> u8 {
let value = ((component as u16) * 2) / 5;
value as u8
};
Color::Rgb(dim_component(r), dim_component(g), dim_component(b))
} else {
color
}
}
}
}
fn color_to_rgb(color: Color) -> Option<(u8, u8, u8)> {
match color {
Color::Black => Some((0, 0, 0)),
Color::Red => Some((205, 0, 0)),
Color::Green => Some((0, 205, 0)),
Color::Yellow => Some((205, 205, 0)),
Color::Blue => Some((0, 0, 205)),
Color::Magenta => Some((205, 0, 205)),
Color::Cyan => Some((0, 205, 205)),
Color::Gray => Some((170, 170, 170)),
Color::DarkGray => Some((85, 85, 85)),
Color::LightRed => Some((255, 85, 85)),
Color::LightGreen => Some((85, 255, 85)),
Color::LightYellow => Some((255, 255, 85)),
Color::LightBlue => Some((85, 85, 255)),
Color::LightMagenta => Some((255, 85, 255)),
Color::LightCyan => Some((85, 255, 255)),
Color::White => Some((255, 255, 255)),
Color::Rgb(r, g, b) => Some((r, g, b)),
Color::Reset | Color::Indexed(_) => None,
}
}
fn message_content_hash(role: &Role, content: &str, tool_signature: &str) -> u64 { fn message_content_hash(role: &Role, content: &str, tool_signature: &str) -> u64 {
let mut hasher = DefaultHasher::new(); let mut hasher = DefaultHasher::new();
role.to_string().hash(&mut hasher); role.to_string().hash(&mut hasher);
@@ -2128,10 +2181,10 @@ impl ChatApp {
} }
for line in lines { for line in lines {
card_lines.push(Self::wrap_card_body_line(line, inner_width, theme)); card_lines.push(Self::wrap_card_body_line(line, inner_width, theme, role));
} }
card_lines.push(Self::build_card_footer(card_width, theme)); card_lines.push(Self::build_card_footer(card_width, theme, role));
card_lines card_lines
} }
@@ -2142,7 +2195,7 @@ impl ChatApp {
card_width: usize, card_width: usize,
theme: &Theme, theme: &Theme,
) -> Line<'static> { ) -> Line<'static> {
let border_style = Style::default().fg(theme.unfocused_panel_border); let border_style = Self::message_border_style(theme, role);
let role_style = Self::role_style(theme, role).add_modifier(Modifier::BOLD); let role_style = Self::role_style(theme, role).add_modifier(Modifier::BOLD);
let meta_style = Style::default().fg(theme.placeholder); let meta_style = Style::default().fg(theme.placeholder);
let tool_style = Style::default() let tool_style = Style::default()
@@ -2199,8 +2252,8 @@ impl ChatApp {
Line::from(spans) Line::from(spans)
} }
fn build_card_footer(card_width: usize, theme: &Theme) -> Line<'static> { fn build_card_footer(card_width: usize, theme: &Theme, role: &Role) -> Line<'static> {
let border_style = Style::default().fg(theme.unfocused_panel_border); let border_style = Self::message_border_style(theme, role);
let mut spans = Vec::new(); let mut spans = Vec::new();
spans.push(Span::styled("", border_style)); spans.push(Span::styled("", border_style));
let horizontal = card_width.saturating_sub(2); let horizontal = card_width.saturating_sub(2);
@@ -2215,8 +2268,9 @@ impl ChatApp {
line: Line<'static>, line: Line<'static>,
inner_width: usize, inner_width: usize,
theme: &Theme, theme: &Theme,
role: &Role,
) -> Line<'static> { ) -> Line<'static> {
let border_style = Style::default().fg(theme.unfocused_panel_border); let border_style = Self::message_border_style(theme, role);
let mut spans = Vec::new(); let mut spans = Vec::new();
spans.push(Span::styled("", border_style)); spans.push(Span::styled("", border_style));