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    /// Configured native OpenAI `service_tier` (`Tier: <name>`), when set.
234    /// Rendered in the header summary only when present; `None` hides it.
235    pub service_tier: Option<String>,
236    pub workspace_trust: String,
237    pub tools: String,
238    pub mcp: String,
239    pub primary_agent: Option<String>,
240    pub primary_agent_color: Option<String>,
241    pub highlights: Vec<InlineHeaderHighlight>,
242    pub subagent_badges: Vec<InlineHeaderBadge>,
243}
244
245impl Default for InlineHeaderContext {
246    fn default() -> Self {
247        let version = env!("CARGO_PKG_VERSION").to_string();
248        Self {
249            // Keep in sync with `vtcode-config::constants::app::DISPLAY_NAME`.
250            // `vtcode-commons` cannot depend on `vtcode-config` (config depends
251            // on commons), so the product name is duplicated here. Covered by
252            // the `header_placeholder_app_name_matches_product` ratchet in
253            // `vtcode-ui`.
254            app_name: "VT Code".to_string(),
255            provider: "Provider: unavailable".to_string(),
256            model: "Model: unavailable".to_string(),
257            context_window_size: None,
258            version,
259            search_tools: None,
260            persistent_memory: None,
261            pr_review: None,
262            git: "git: unavailable".to_string(),
263            reasoning: "unavailable".to_string(),
264            reasoning_stage: None,
265            service_tier: None,
266            workspace_trust: "Trust: unavailable".to_string(),
267            tools: "Tools: unavailable".to_string(),
268            mcp: "MCP: unavailable".to_string(),
269            primary_agent: None,
270            primary_agent_color: None,
271            highlights: Vec::new(),
272            subagent_badges: Vec::new(),
273        }
274    }
275}
276
277// ---------------------------------------------------------------------------
278// Conversion helpers
279// ---------------------------------------------------------------------------
280
281fn convert_ansi_color(color: AnsiColorEnum) -> Option<AnsiColorEnum> {
282    Some(match color {
283        AnsiColorEnum::Ansi(ansi) => AnsiColorEnum::Ansi(ansi),
284        AnsiColorEnum::Ansi256(value) => AnsiColorEnum::Ansi256(value),
285        AnsiColorEnum::Rgb(rgb) => AnsiColorEnum::Rgb(rgb),
286    })
287}
288
289fn convert_style_color(style: &AnsiStyle) -> Option<AnsiColorEnum> {
290    style.get_fg_color().and_then(convert_ansi_color)
291}
292
293fn convert_style_bg_color(style: &AnsiStyle) -> Option<AnsiColorEnum> {
294    style.get_bg_color().and_then(convert_ansi_color)
295}
296
297/// Convert an `anstyle::Style` to an [`InlineTextStyle`].
298pub fn convert_style(style: AnsiStyle) -> InlineTextStyle {
299    InlineTextStyle {
300        color: convert_style_color(&style),
301        bg_color: convert_style_bg_color(&style),
302        effects: style.get_effects(),
303    }
304}
305
306/// Build an [`InlineTheme`] from individual theme colour fields.
307pub fn theme_from_color_fields(
308    foreground: AnsiColorEnum,
309    background: AnsiColorEnum,
310    primary: AnsiStyle,
311    secondary: AnsiStyle,
312    tool: AnsiStyle,
313    tool_detail: AnsiStyle,
314    pty_output: AnsiStyle,
315    error: AnsiStyle,
316    warning: AnsiStyle,
317) -> InlineTheme {
318    InlineTheme {
319        foreground: convert_ansi_color(foreground),
320        background: convert_ansi_color(background),
321        primary: convert_style_color(&primary),
322        secondary: convert_style_color(&secondary),
323        tool_accent: convert_style_color(&tool),
324        tool_body: convert_style_color(&tool_detail),
325        pty_body: convert_style_color(&pty_output),
326        error: convert_style_color(&error),
327        warning: convert_style_color(&warning),
328    }
329}