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