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
7use crate::ui_protocol::types::EditingMode;
8
9/// Inline text styling with foreground/background color and text effects.
10#[derive(Clone, Debug, Default, PartialEq)]
11pub struct InlineTextStyle {
12    pub color: Option<AnsiColorEnum>,
13    pub bg_color: Option<AnsiColorEnum>,
14    pub effects: Effects,
15}
16
17impl InlineTextStyle {
18    #[must_use]
19    pub fn with_color(mut self, color: Option<AnsiColorEnum>) -> Self {
20        self.color = color;
21        self
22    }
23
24    #[must_use]
25    pub fn with_bg_color(mut self, color: Option<AnsiColorEnum>) -> Self {
26        self.bg_color = color;
27        self
28    }
29
30    #[must_use]
31    pub fn merge_color(mut self, fallback: Option<AnsiColorEnum>) -> Self {
32        if self.color.is_none() {
33            self.color = fallback;
34        }
35        self
36    }
37
38    #[must_use]
39    pub fn merge_bg_color(mut self, fallback: Option<AnsiColorEnum>) -> Self {
40        if self.bg_color.is_none() {
41            self.bg_color = fallback;
42        }
43        self
44    }
45
46    #[must_use]
47    pub fn bold(mut self) -> Self {
48        self.effects |= Effects::BOLD;
49        self
50    }
51
52    #[must_use]
53    pub fn italic(mut self) -> Self {
54        self.effects |= Effects::ITALIC;
55        self
56    }
57
58    #[must_use]
59    pub fn underline(mut self) -> Self {
60        self.effects |= Effects::UNDERLINE;
61        self
62    }
63
64    #[must_use]
65    pub fn dim(mut self) -> Self {
66        self.effects |= Effects::DIMMED;
67        self
68    }
69
70    #[must_use]
71    pub fn to_ansi_style(&self, fallback: Option<AnsiColorEnum>) -> AnsiStyle {
72        let mut style = AnsiStyle::new();
73        if let Some(color) = self.color.or(fallback) {
74            style = style.fg_color(Some(color));
75        }
76        if let Some(bg) = self.bg_color {
77            style = style.bg_color(Some(bg));
78        }
79        if self.effects.contains(Effects::BOLD) {
80            style = style.bold();
81        }
82        if self.effects.contains(Effects::ITALIC) {
83            style = style.italic();
84        }
85        if self.effects.contains(Effects::UNDERLINE) {
86            style = style.underline();
87        }
88        if self.effects.contains(Effects::DIMMED) {
89            style = style.dimmed();
90        }
91        style
92    }
93}
94
95/// A styled text segment with shared style.
96#[derive(Clone, Debug, Default)]
97pub struct InlineSegment {
98    pub text: String,
99    pub style: Arc<InlineTextStyle>,
100}
101
102/// A clickable link target inside a transcript line.
103#[derive(Clone, Debug, PartialEq, Eq)]
104pub enum InlineLinkTarget {
105    Url(String),
106}
107
108/// Byte-range inside a line that is a clickable link.
109#[derive(Clone, Debug, PartialEq, Eq)]
110pub struct InlineLinkRange {
111    pub start: usize,
112    pub end: usize,
113    pub target: InlineLinkTarget,
114}
115
116/// Resolved theme colors for inline rendering.
117#[derive(Clone, Debug, Default)]
118pub struct InlineTheme {
119    pub foreground: Option<AnsiColorEnum>,
120    pub background: Option<AnsiColorEnum>,
121    pub primary: Option<AnsiColorEnum>,
122    pub secondary: Option<AnsiColorEnum>,
123    pub tool_accent: Option<AnsiColorEnum>,
124    pub tool_body: Option<AnsiColorEnum>,
125    pub pty_body: Option<AnsiColorEnum>,
126}
127
128// ---------------------------------------------------------------------------
129// Header context types
130// ---------------------------------------------------------------------------
131
132/// Status-badge tone used in header status indicators.
133#[derive(Clone, 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 mode: String,
177    pub reasoning: String,
178    pub reasoning_stage: Option<String>,
179    pub workspace_trust: String,
180    pub tools: String,
181    pub mcp: String,
182    pub highlights: Vec<InlineHeaderHighlight>,
183    pub subagent_badges: Vec<InlineHeaderBadge>,
184    /// Current editing mode for display in header.
185    pub editing_mode: EditingMode,
186    /// Current autonomous mode status.
187    pub autonomous_mode: bool,
188}
189
190impl Default for InlineHeaderContext {
191    fn default() -> Self {
192        let version = env!("CARGO_PKG_VERSION").to_string();
193        Self {
194            app_name: "App".to_string(),
195            provider: "Provider: unavailable".to_string(),
196            model: "Model: unavailable".to_string(),
197            context_window_size: None,
198            version,
199            search_tools: None,
200            persistent_memory: None,
201            pr_review: None,
202            editor_context: None,
203            git: "git: unavailable".to_string(),
204            mode: "Inline session".to_string(),
205            reasoning: "Reasoning effort: unavailable".to_string(),
206            reasoning_stage: None,
207            workspace_trust: "Trust: unavailable".to_string(),
208            tools: "Tools: unavailable".to_string(),
209            mcp: "MCP: unavailable".to_string(),
210            highlights: Vec::new(),
211            subagent_badges: Vec::new(),
212            editing_mode: EditingMode::default(),
213            autonomous_mode: false,
214        }
215    }
216}
217
218// ---------------------------------------------------------------------------
219// Conversion helpers
220// ---------------------------------------------------------------------------
221
222fn convert_ansi_color(color: AnsiColorEnum) -> Option<AnsiColorEnum> {
223    Some(match color {
224        AnsiColorEnum::Ansi(ansi) => AnsiColorEnum::Ansi(ansi),
225        AnsiColorEnum::Ansi256(value) => AnsiColorEnum::Ansi256(value),
226        AnsiColorEnum::Rgb(rgb) => AnsiColorEnum::Rgb(rgb),
227    })
228}
229
230fn convert_style_color(style: &AnsiStyle) -> Option<AnsiColorEnum> {
231    style.get_fg_color().and_then(convert_ansi_color)
232}
233
234fn convert_style_bg_color(style: &AnsiStyle) -> Option<AnsiColorEnum> {
235    style.get_bg_color().and_then(convert_ansi_color)
236}
237
238/// Convert an `anstyle::Style` to an [`InlineTextStyle`].
239pub fn convert_style(style: AnsiStyle) -> InlineTextStyle {
240    InlineTextStyle {
241        color: convert_style_color(&style),
242        bg_color: convert_style_bg_color(&style),
243        effects: style.get_effects(),
244    }
245}
246
247/// Build an [`InlineTheme`] from individual theme colour fields.
248pub fn theme_from_color_fields(
249    foreground: AnsiColorEnum,
250    background: AnsiColorEnum,
251    primary: AnsiStyle,
252    secondary: AnsiStyle,
253    tool: AnsiStyle,
254    tool_detail: AnsiStyle,
255    pty_output: AnsiStyle,
256) -> InlineTheme {
257    InlineTheme {
258        foreground: convert_ansi_color(foreground),
259        background: convert_ansi_color(background),
260        primary: convert_style_color(&primary),
261        secondary: convert_style_color(&secondary),
262        tool_accent: convert_style_color(&tool),
263        tool_body: convert_style_color(&tool_detail),
264        pty_body: convert_style_color(&pty_output),
265    }
266}