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