Skip to main content

vtcode_commons/ui_protocol/
style.rs

1//! Style and theming types that depend on `anstyle`.
2
3use std::sync::Arc;
4
5use anstyle::{Color as AnsiColorEnum, Effects, Style as AnsiStyle};
6
7/// Inline text styling with foreground/background color and text effects.
8#[derive(Clone, Debug, Default, PartialEq)]
9pub struct InlineTextStyle {
10    pub color: Option<AnsiColorEnum>,
11    pub bg_color: Option<AnsiColorEnum>,
12    pub effects: Effects,
13}
14
15impl InlineTextStyle {
16    #[must_use]
17    pub fn with_color(mut self, color: Option<AnsiColorEnum>) -> Self {
18        self.color = color;
19        self
20    }
21
22    #[must_use]
23    pub fn with_bg_color(mut self, color: Option<AnsiColorEnum>) -> Self {
24        self.bg_color = color;
25        self
26    }
27
28    #[must_use]
29    pub fn merge_color(mut self, fallback: Option<AnsiColorEnum>) -> Self {
30        if self.color.is_none() {
31            self.color = fallback;
32        }
33        self
34    }
35
36    #[must_use]
37    pub fn merge_bg_color(mut self, fallback: Option<AnsiColorEnum>) -> Self {
38        if self.bg_color.is_none() {
39            self.bg_color = fallback;
40        }
41        self
42    }
43
44    #[must_use]
45    pub fn bold(mut self) -> Self {
46        self.effects |= Effects::BOLD;
47        self
48    }
49
50    #[must_use]
51    pub fn italic(mut self) -> Self {
52        self.effects |= Effects::ITALIC;
53        self
54    }
55
56    #[must_use]
57    pub fn underline(mut self) -> Self {
58        self.effects |= Effects::UNDERLINE;
59        self
60    }
61
62    #[must_use]
63    pub fn dim(mut self) -> Self {
64        self.effects |= Effects::DIMMED;
65        self
66    }
67
68    #[must_use]
69    pub fn to_ansi_style(&self, fallback: Option<AnsiColorEnum>) -> AnsiStyle {
70        let mut style = AnsiStyle::new();
71        if let Some(color) = self.color.or(fallback) {
72            style = style.fg_color(Some(color));
73        }
74        if let Some(bg) = self.bg_color {
75            style = style.bg_color(Some(bg));
76        }
77        if self.effects.contains(Effects::BOLD) {
78            style = style.bold();
79        }
80        if self.effects.contains(Effects::ITALIC) {
81            style = style.italic();
82        }
83        if self.effects.contains(Effects::UNDERLINE) {
84            style = style.underline();
85        }
86        if self.effects.contains(Effects::DIMMED) {
87            style = style.dimmed();
88        }
89        style
90    }
91}
92
93/// A styled text segment with shared style.
94#[derive(Clone, Debug, Default)]
95pub struct InlineSegment {
96    pub text: String,
97    pub style: Arc<InlineTextStyle>,
98}
99
100/// A clickable link target inside a transcript line.
101#[derive(Clone, Debug, PartialEq, Eq)]
102pub enum InlineLinkTarget {
103    Url(String),
104}
105
106/// Byte-range inside a line that is a clickable link.
107#[derive(Clone, Debug, PartialEq, Eq)]
108pub struct InlineLinkRange {
109    pub start: usize,
110    pub end: usize,
111    pub target: InlineLinkTarget,
112}
113
114/// Resolved theme colors for inline rendering.
115#[derive(Clone, Debug, Default)]
116pub struct InlineTheme {
117    pub foreground: Option<AnsiColorEnum>,
118    pub background: Option<AnsiColorEnum>,
119    pub primary: Option<AnsiColorEnum>,
120    pub secondary: Option<AnsiColorEnum>,
121    pub tool_accent: Option<AnsiColorEnum>,
122    pub tool_body: Option<AnsiColorEnum>,
123    pub pty_body: Option<AnsiColorEnum>,
124    pub error: Option<AnsiColorEnum>,
125    pub warning: Option<AnsiColorEnum>,
126}
127
128// ---------------------------------------------------------------------------
129// List / modal presentation tones
130// ---------------------------------------------------------------------------
131
132/// Semantic tone for list badges, values, and modal status strips.
133///
134/// Renderers map each tone onto theme styles; callers pick the tone, never a
135/// raw color, so every surface stays theme-consistent and WCAG-checked.
136#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
137pub enum InlineTone {
138    #[default]
139    Neutral,
140    Accent,
141    Success,
142    Warning,
143    Danger,
144    /// Live/current selection marker (the active model, the current value).
145    Current,
146}
147
148/// A short-lived status message shown inside a list modal (footer strip).
149///
150/// One status is kept at a time; a new action overwrites the previous status.
151#[derive(Clone, Debug, PartialEq, Eq)]
152pub struct InlineStatus {
153    pub tone: InlineTone,
154    pub message: String,
155}
156
157impl InlineStatus {
158    #[must_use]
159    pub fn new(tone: InlineTone, message: impl Into<String>) -> Self {
160        Self { tone, message: message.into() }
161    }
162
163    #[must_use]
164    pub fn success(message: impl Into<String>) -> Self {
165        Self::new(InlineTone::Success, message)
166    }
167
168    #[must_use]
169    pub fn warning(message: impl Into<String>) -> Self {
170        Self::new(InlineTone::Warning, message)
171    }
172
173    #[must_use]
174    pub fn error(message: impl Into<String>) -> Self {
175        Self::new(InlineTone::Danger, message)
176    }
177
178    #[must_use]
179    pub fn info(message: impl Into<String>) -> Self {
180        Self::new(InlineTone::Accent, message)
181    }
182}
183
184// ---------------------------------------------------------------------------
185// Header context types
186// ---------------------------------------------------------------------------
187
188/// Status-badge tone used in header status indicators.
189#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
190pub enum InlineHeaderStatusTone {
191    #[default]
192    Ready,
193    Warning,
194    Error,
195}
196
197/// A labelled status badge for the header bar.
198#[derive(Clone, Debug, Default, PartialEq, Eq)]
199pub struct InlineHeaderStatusBadge {
200    pub text: String,
201    pub tone: InlineHeaderStatusTone,
202}
203
204/// A compact pill badge rendered in the header.
205#[derive(Clone, Debug, Default, PartialEq)]
206pub struct InlineHeaderBadge {
207    pub text: String,
208    pub style: InlineTextStyle,
209    pub full_background: bool,
210}
211
212/// A title + content highlight block in the header.
213#[derive(Clone, Debug, Default, PartialEq, Eq)]
214pub struct InlineHeaderHighlight {
215    pub title: String,
216    pub lines: Vec<String>,
217}
218
219/// Session metadata displayed in the inline header.
220#[derive(Clone, Debug)]
221pub struct InlineHeaderContext {
222    pub app_name: String,
223    pub provider: String,
224    pub model: String,
225    pub context_window_size: Option<usize>,
226    pub version: String,
227    pub search_tools: Option<InlineHeaderStatusBadge>,
228    pub persistent_memory: Option<InlineHeaderStatusBadge>,
229    pub pr_review: Option<InlineHeaderStatusBadge>,
230    pub git: String,
231    pub reasoning: String,
232    pub reasoning_stage: Option<String>,
233    pub workspace_trust: String,
234    pub tools: String,
235    pub mcp: String,
236    pub primary_agent: Option<String>,
237    pub primary_agent_color: Option<String>,
238    pub highlights: Vec<InlineHeaderHighlight>,
239    pub subagent_badges: Vec<InlineHeaderBadge>,
240}
241
242impl Default for InlineHeaderContext {
243    fn default() -> Self {
244        let version = env!("CARGO_PKG_VERSION").to_string();
245        Self {
246            // Keep in sync with `vtcode-config::constants::app::DISPLAY_NAME`.
247            // `vtcode-commons` cannot depend on `vtcode-config` (config depends
248            // on commons), so the product name is duplicated here. Covered by
249            // the `header_placeholder_app_name_matches_product` ratchet in
250            // `vtcode-ui`.
251            app_name: "VT Code".to_string(),
252            provider: "Provider: unavailable".to_string(),
253            model: "Model: unavailable".to_string(),
254            context_window_size: None,
255            version,
256            search_tools: None,
257            persistent_memory: None,
258            pr_review: None,
259            git: "git: unavailable".to_string(),
260            reasoning: "unavailable".to_string(),
261            reasoning_stage: None,
262            workspace_trust: "Trust: unavailable".to_string(),
263            tools: "Tools: unavailable".to_string(),
264            mcp: "MCP: unavailable".to_string(),
265            primary_agent: None,
266            primary_agent_color: None,
267            highlights: Vec::new(),
268            subagent_badges: Vec::new(),
269        }
270    }
271}
272
273// ---------------------------------------------------------------------------
274// Conversion helpers
275// ---------------------------------------------------------------------------
276
277fn convert_ansi_color(color: AnsiColorEnum) -> Option<AnsiColorEnum> {
278    Some(match color {
279        AnsiColorEnum::Ansi(ansi) => AnsiColorEnum::Ansi(ansi),
280        AnsiColorEnum::Ansi256(value) => AnsiColorEnum::Ansi256(value),
281        AnsiColorEnum::Rgb(rgb) => AnsiColorEnum::Rgb(rgb),
282    })
283}
284
285fn convert_style_color(style: &AnsiStyle) -> Option<AnsiColorEnum> {
286    style.get_fg_color().and_then(convert_ansi_color)
287}
288
289fn convert_style_bg_color(style: &AnsiStyle) -> Option<AnsiColorEnum> {
290    style.get_bg_color().and_then(convert_ansi_color)
291}
292
293/// Convert an `anstyle::Style` to an [`InlineTextStyle`].
294pub fn convert_style(style: AnsiStyle) -> InlineTextStyle {
295    InlineTextStyle {
296        color: convert_style_color(&style),
297        bg_color: convert_style_bg_color(&style),
298        effects: style.get_effects(),
299    }
300}
301
302/// Build an [`InlineTheme`] from individual theme colour fields.
303pub fn theme_from_color_fields(
304    foreground: AnsiColorEnum,
305    background: AnsiColorEnum,
306    primary: AnsiStyle,
307    secondary: AnsiStyle,
308    tool: AnsiStyle,
309    tool_detail: AnsiStyle,
310    pty_output: AnsiStyle,
311    error: AnsiStyle,
312    warning: AnsiStyle,
313) -> InlineTheme {
314    InlineTheme {
315        foreground: convert_ansi_color(foreground),
316        background: convert_ansi_color(background),
317        primary: convert_style_color(&primary),
318        secondary: convert_style_color(&secondary),
319        tool_accent: convert_style_color(&tool),
320        tool_body: convert_style_color(&tool_detail),
321        pty_body: convert_style_color(&pty_output),
322        error: convert_style_color(&error),
323        warning: convert_style_color(&warning),
324    }
325}