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