vtcode_tui/core_tui/session/
styling.rs1use anstyle::{AnsiColor, Color as AnsiColorEnum, RgbColor};
2use ratatui::prelude::*;
3
4use crate::config::constants::ui;
5use crate::ui::tui::{
6 style::{ratatui_color_from_ansi, ratatui_style_from_inline},
7 types::{InlineMessageKind, InlineTextStyle, InlineTheme},
8};
9
10use super::message::MessageLine;
11
12fn mix(color: RgbColor, target: RgbColor, ratio: f64) -> RgbColor {
13 let ratio = ratio.clamp(ui::THEME_MIX_RATIO_MIN, ui::THEME_MIX_RATIO_MAX);
14 let blend = |c: u8, t: u8| -> u8 {
15 let c = c as f64;
16 let t = t as f64;
17 ((c + (t - c) * ratio).round()).clamp(ui::THEME_BLEND_CLAMP_MIN, ui::THEME_BLEND_CLAMP_MAX)
18 as u8
19 };
20
21 RgbColor(
22 blend(color.0, target.0),
23 blend(color.1, target.1),
24 blend(color.2, target.2),
25 )
26}
27
28pub fn normalize_tool_name(tool_name: &str) -> &'static str {
29 match tool_name.to_lowercase().as_str() {
30 "grep" | "rg" | "ripgrep" | "grep_file" | "search" | "find" | "ag" => "search",
31 "list" | "ls" | "dir" | "list_files" => "list",
32 "read" | "cat" | "file" | "read_file" => "read",
33 "write" | "edit" | "save" | "insert" | "edit_file" => "write",
34 "run" | "command" | "bash" | "sh" => "run",
35 _ => "other",
36 }
37}
38
39pub fn tool_inline_style_for(tool_name: &str, theme: &InlineTheme) -> InlineTextStyle {
42 let normalized_name = normalize_tool_name(tool_name);
43 let mut style = InlineTextStyle::default().bold();
44
45 style.color = match normalized_name {
46 "read" => Some(AnsiColor::Cyan.into()),
47 "list" => Some(AnsiColor::Green.into()),
48 "search" => Some(AnsiColor::Cyan.into()),
49 "write" => Some(AnsiColor::Magenta.into()),
50 "run" => Some(AnsiColor::Red.into()),
51 "git" | "version_control" => Some(AnsiColor::Cyan.into()),
52 _ => theme.tool_accent.or(theme.primary).or(theme.foreground),
53 };
54
55 style
56}
57
58pub struct SessionStyles {
60 theme: InlineTheme,
61}
62
63impl SessionStyles {
64 pub fn new(theme: InlineTheme) -> Self {
65 Self { theme }
66 }
67
68 #[allow(dead_code)]
69 pub fn theme(&self) -> &InlineTheme {
70 &self.theme
71 }
72
73 pub fn set_theme(&mut self, theme: InlineTheme) {
74 self.theme = theme;
75 }
76
77 pub fn modal_list_highlight_style(&self) -> Style {
79 let mut style = Style::default().add_modifier(Modifier::REVERSED | Modifier::BOLD);
80 if let Some(primary) = self.theme.primary.or(self.theme.foreground) {
81 style = style.fg(ratatui_color_from_ansi(primary));
82 }
83 style
84 }
85
86 #[allow(dead_code)]
88 pub fn tool_inline_style(&self, tool_name: &str) -> InlineTextStyle {
89 tool_inline_style_for(tool_name, &self.theme)
90 }
91
92 pub fn tool_border_style(&self) -> InlineTextStyle {
94 self.border_inline_style()
95 }
96
97 pub fn default_style(&self) -> Style {
101 let mut style = Style::default();
102 if let Some(background) = self.theme.background.map(ratatui_color_from_ansi) {
103 style = style.bg(background);
104 }
105 if let Some(foreground) = self.theme.foreground.map(ratatui_color_from_ansi) {
106 style = style.fg(foreground);
107 }
108 style
109 }
110
111 #[allow(dead_code)]
113 pub fn default_inline_style(&self) -> InlineTextStyle {
114 InlineTextStyle {
115 color: self.theme.foreground,
116 ..InlineTextStyle::default()
117 }
118 }
119
120 pub fn accent_inline_style(&self) -> InlineTextStyle {
122 InlineTextStyle {
123 color: self.theme.primary.or(self.theme.foreground),
124 ..InlineTextStyle::default()
125 }
126 }
127
128 pub fn accent_style(&self) -> Style {
130 ratatui_style_from_inline(&self.accent_inline_style(), self.theme.foreground)
131 }
132
133 pub fn transcript_link_style(&self) -> Style {
134 let style = InlineTextStyle {
135 color: self
136 .theme
137 .tool_accent
138 .or(self.theme.primary)
139 .or(self.theme.foreground),
140 ..InlineTextStyle::default()
141 };
142 ratatui_style_from_inline(&style, self.theme.foreground)
143 }
144
145 pub fn border_inline_style(&self) -> InlineTextStyle {
147 InlineTextStyle {
148 color: self.theme.secondary.or(self.theme.foreground),
149 ..InlineTextStyle::default()
150 }
151 }
152
153 pub fn border_style(&self) -> Style {
155 self.dimmed_border_style(true)
156 }
157
158 pub fn dimmed_border_style(&self, suppress_bold: bool) -> Style {
162 let mut style =
163 ratatui_style_from_inline(&self.border_inline_style(), self.theme.foreground)
164 .add_modifier(Modifier::DIM);
165 if suppress_bold {
166 style = style.remove_modifier(Modifier::BOLD);
167 }
168 style
169 }
170
171 pub fn input_background_style(&self) -> Style {
172 let mut style = self.default_style();
173 let Some(background) = self.theme.background else {
174 return style;
175 };
176
177 let resolved = match (background, self.theme.foreground) {
178 (AnsiColorEnum::Rgb(bg), Some(AnsiColorEnum::Rgb(fg))) => {
179 AnsiColorEnum::Rgb(mix(bg, fg, ui::THEME_INPUT_BACKGROUND_MIX_RATIO))
180 }
181 (color, _) => color,
182 };
183
184 style = style.bg(ratatui_color_from_ansi(resolved));
185 style
186 }
187
188 pub fn prefix_style(&self, line: &MessageLine) -> InlineTextStyle {
190 let fallback = self.text_fallback(line.kind).or(self.theme.foreground);
191
192 let color = line
193 .segments
194 .iter()
195 .find_map(|segment| segment.style.color)
196 .or(fallback);
197
198 InlineTextStyle {
199 color,
200 ..InlineTextStyle::default()
201 }
202 }
203
204 pub fn text_fallback(&self, kind: InlineMessageKind) -> Option<AnsiColorEnum> {
206 match kind {
207 InlineMessageKind::Agent => self.theme.foreground.or(self.theme.primary),
209 InlineMessageKind::Policy => self.theme.primary.or(self.theme.foreground),
210 InlineMessageKind::User => self.theme.secondary.or(self.theme.foreground),
211 InlineMessageKind::Tool | InlineMessageKind::Error => {
212 self.theme.primary.or(self.theme.foreground)
213 }
214 InlineMessageKind::Pty => self
215 .theme
216 .pty_body
217 .or(self.theme.tool_body)
218 .or(self.theme.foreground),
219 InlineMessageKind::Info => self.theme.foreground,
220 InlineMessageKind::Warning => Some(AnsiColor::Red.into()),
221 }
222 }
223
224 pub fn message_divider_style(&self, kind: InlineMessageKind) -> Style {
226 let mut style = InlineTextStyle::default();
227 if kind == InlineMessageKind::User {
228 style.color = self.theme.primary.or(self.theme.foreground);
229 } else {
230 style.color = self.text_fallback(kind).or(self.theme.foreground);
231 }
232 let resolved = ratatui_style_from_inline(&style, self.theme.foreground);
233 resolved.add_modifier(Modifier::DIM)
234 }
235}