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 highlights: Vec<InlineHeaderHighlight>,
181    pub subagent_badges: Vec<InlineHeaderBadge>,
182}
183
184impl Default for InlineHeaderContext {
185    fn default() -> Self {
186        let version = env!("CARGO_PKG_VERSION").to_string();
187        Self {
188            app_name: "App".to_string(),
189            provider: "Provider: unavailable".to_string(),
190            model: "Model: unavailable".to_string(),
191            context_window_size: None,
192            version,
193            search_tools: None,
194            persistent_memory: None,
195            pr_review: None,
196            editor_context: None,
197            git: "git: unavailable".to_string(),
198            reasoning: "Reasoning effort: unavailable".to_string(),
199            reasoning_stage: None,
200            workspace_trust: "Trust: unavailable".to_string(),
201            tools: "Tools: unavailable".to_string(),
202            mcp: "MCP: unavailable".to_string(),
203            primary_agent: None,
204            highlights: Vec::new(),
205            subagent_badges: Vec::new(),
206        }
207    }
208}
209
210// ---------------------------------------------------------------------------
211// Conversion helpers
212// ---------------------------------------------------------------------------
213
214fn convert_ansi_color(color: AnsiColorEnum) -> Option<AnsiColorEnum> {
215    Some(match color {
216        AnsiColorEnum::Ansi(ansi) => AnsiColorEnum::Ansi(ansi),
217        AnsiColorEnum::Ansi256(value) => AnsiColorEnum::Ansi256(value),
218        AnsiColorEnum::Rgb(rgb) => AnsiColorEnum::Rgb(rgb),
219    })
220}
221
222fn convert_style_color(style: &AnsiStyle) -> Option<AnsiColorEnum> {
223    style.get_fg_color().and_then(convert_ansi_color)
224}
225
226fn convert_style_bg_color(style: &AnsiStyle) -> Option<AnsiColorEnum> {
227    style.get_bg_color().and_then(convert_ansi_color)
228}
229
230/// Convert an `anstyle::Style` to an [`InlineTextStyle`].
231pub fn convert_style(style: AnsiStyle) -> InlineTextStyle {
232    InlineTextStyle {
233        color: convert_style_color(&style),
234        bg_color: convert_style_bg_color(&style),
235        effects: style.get_effects(),
236    }
237}
238
239/// Build an [`InlineTheme`] from individual theme colour fields.
240pub fn theme_from_color_fields(
241    foreground: AnsiColorEnum,
242    background: AnsiColorEnum,
243    primary: AnsiStyle,
244    secondary: AnsiStyle,
245    tool: AnsiStyle,
246    tool_detail: AnsiStyle,
247    pty_output: AnsiStyle,
248) -> InlineTheme {
249    InlineTheme {
250        foreground: convert_ansi_color(foreground),
251        background: convert_ansi_color(background),
252        primary: convert_style_color(&primary),
253        secondary: convert_style_color(&secondary),
254        tool_accent: convert_style_color(&tool),
255        tool_body: convert_style_color(&tool_detail),
256        pty_body: convert_style_color(&pty_output),
257    }
258}