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// Header context types
130// ---------------------------------------------------------------------------
131
132/// Status-badge tone used in header status indicators.
133#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
134pub enum InlineHeaderStatusTone {
135    #[default]
136    Ready,
137    Warning,
138    Error,
139}
140
141/// A labelled status badge for the header bar.
142#[derive(Clone, Debug, Default, PartialEq, Eq)]
143pub struct InlineHeaderStatusBadge {
144    pub text: String,
145    pub tone: InlineHeaderStatusTone,
146}
147
148/// A compact pill badge rendered in the header.
149#[derive(Clone, Debug, Default, PartialEq)]
150pub struct InlineHeaderBadge {
151    pub text: String,
152    pub style: InlineTextStyle,
153    pub full_background: bool,
154}
155
156/// A title + content highlight block in the header.
157#[derive(Clone, Debug, Default, PartialEq, Eq)]
158pub struct InlineHeaderHighlight {
159    pub title: String,
160    pub lines: Vec<String>,
161}
162
163/// Session metadata displayed in the inline header.
164#[derive(Clone, Debug)]
165pub struct InlineHeaderContext {
166    pub app_name: String,
167    pub provider: String,
168    pub model: String,
169    pub context_window_size: Option<usize>,
170    pub version: String,
171    pub search_tools: Option<InlineHeaderStatusBadge>,
172    pub persistent_memory: Option<InlineHeaderStatusBadge>,
173    pub pr_review: Option<InlineHeaderStatusBadge>,
174    pub editor_context: Option<String>,
175    pub git: String,
176    pub reasoning: String,
177    pub reasoning_stage: Option<String>,
178    pub workspace_trust: String,
179    pub tools: String,
180    pub mcp: String,
181    pub primary_agent: Option<String>,
182    pub primary_agent_color: Option<String>,
183    pub highlights: Vec<InlineHeaderHighlight>,
184    pub subagent_badges: Vec<InlineHeaderBadge>,
185}
186
187impl Default for InlineHeaderContext {
188    fn default() -> Self {
189        let version = env!("CARGO_PKG_VERSION").to_string();
190        Self {
191            app_name: "App".to_string(),
192            provider: "Provider: unavailable".to_string(),
193            model: "Model: unavailable".to_string(),
194            context_window_size: None,
195            version,
196            search_tools: None,
197            persistent_memory: None,
198            pr_review: None,
199            editor_context: None,
200            git: "git: unavailable".to_string(),
201            reasoning: "unavailable".to_string(),
202            reasoning_stage: None,
203            workspace_trust: "Trust: unavailable".to_string(),
204            tools: "Tools: unavailable".to_string(),
205            mcp: "MCP: unavailable".to_string(),
206            primary_agent: None,
207            primary_agent_color: None,
208            highlights: Vec::new(),
209            subagent_badges: Vec::new(),
210        }
211    }
212}
213
214// ---------------------------------------------------------------------------
215// Conversion helpers
216// ---------------------------------------------------------------------------
217
218fn convert_ansi_color(color: AnsiColorEnum) -> Option<AnsiColorEnum> {
219    Some(match color {
220        AnsiColorEnum::Ansi(ansi) => AnsiColorEnum::Ansi(ansi),
221        AnsiColorEnum::Ansi256(value) => AnsiColorEnum::Ansi256(value),
222        AnsiColorEnum::Rgb(rgb) => AnsiColorEnum::Rgb(rgb),
223    })
224}
225
226fn convert_style_color(style: &AnsiStyle) -> Option<AnsiColorEnum> {
227    style.get_fg_color().and_then(convert_ansi_color)
228}
229
230fn convert_style_bg_color(style: &AnsiStyle) -> Option<AnsiColorEnum> {
231    style.get_bg_color().and_then(convert_ansi_color)
232}
233
234/// Convert an `anstyle::Style` to an [`InlineTextStyle`].
235pub fn convert_style(style: AnsiStyle) -> InlineTextStyle {
236    InlineTextStyle {
237        color: convert_style_color(&style),
238        bg_color: convert_style_bg_color(&style),
239        effects: style.get_effects(),
240    }
241}
242
243/// Build an [`InlineTheme`] from individual theme colour fields.
244pub fn theme_from_color_fields(
245    foreground: AnsiColorEnum,
246    background: AnsiColorEnum,
247    primary: AnsiStyle,
248    secondary: AnsiStyle,
249    tool: AnsiStyle,
250    tool_detail: AnsiStyle,
251    pty_output: AnsiStyle,
252    error: AnsiStyle,
253    warning: AnsiStyle,
254) -> InlineTheme {
255    InlineTheme {
256        foreground: convert_ansi_color(foreground),
257        background: convert_ansi_color(background),
258        primary: convert_style_color(&primary),
259        secondary: convert_style_color(&secondary),
260        tool_accent: convert_style_color(&tool),
261        tool_body: convert_style_color(&tool_detail),
262        pty_body: convert_style_color(&pty_output),
263        error: convert_style_color(&error),
264        warning: convert_style_color(&warning),
265    }
266}