Skip to main content

vtcode_config/
root.rs

1use serde::{Deserialize, Serialize};
2
3use crate::status_line::StatusLineConfig;
4
5#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
6#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
7#[serde(rename_all = "snake_case")]
8#[derive(Default)]
9pub enum ToolOutputMode {
10    #[default]
11    Compact,
12    Full,
13}
14
15#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
16#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
17#[serde(rename_all = "snake_case")]
18#[derive(Default)]
19pub enum ReasoningDisplayMode {
20    Always,
21    #[default]
22    Toggle,
23    Hidden,
24}
25
26/// Layout mode override for responsive UI
27#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
28#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
29#[serde(rename_all = "snake_case")]
30pub enum LayoutModeOverride {
31    /// Auto-detect based on terminal size
32    #[default]
33    Auto,
34    /// Force compact mode (no borders)
35    Compact,
36    /// Force standard mode (borders, no sidebar/footer)
37    Standard,
38    /// Force wide mode (sidebar + footer)
39    Wide,
40}
41
42/// UI display mode variants for quick presets
43#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
44#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
45#[serde(rename_all = "snake_case")]
46pub enum UiDisplayMode {
47    /// Full UI with all features (sidebar, footer)
48    Full,
49    /// Minimal UI - no sidebar, no footer
50    #[default]
51    Minimal,
52    /// Focused mode - transcript only, maximum content space
53    Focused,
54}
55
56#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
57#[derive(Debug, Clone, Deserialize, Serialize)]
58pub struct UiConfig {
59    #[serde(default = "default_tool_output_mode")]
60    pub tool_output_mode: ToolOutputMode,
61    #[serde(default = "default_tool_output_max_lines")]
62    pub tool_output_max_lines: usize,
63    #[serde(default = "default_tool_output_spool_bytes")]
64    pub tool_output_spool_bytes: usize,
65    #[serde(default)]
66    pub tool_output_spool_dir: Option<String>,
67    #[serde(default = "default_allow_tool_ansi")]
68    pub allow_tool_ansi: bool,
69    #[serde(default = "default_inline_viewport_rows")]
70    pub inline_viewport_rows: u16,
71    #[serde(default = "default_reasoning_display_mode")]
72    pub reasoning_display_mode: ReasoningDisplayMode,
73    #[serde(default = "default_reasoning_visible_default")]
74    pub reasoning_visible_default: bool,
75    #[serde(default)]
76    pub status_line: StatusLineConfig,
77    #[serde(default)]
78    pub keyboard_protocol: KeyboardProtocolConfig,
79
80    /// Override the responsive layout mode
81    #[serde(default)]
82    pub layout_mode: LayoutModeOverride,
83
84    /// UI display mode preset (full, minimal, focused)
85    #[serde(default)]
86    pub display_mode: UiDisplayMode,
87
88    /// Show the right sidebar (queue, context, tools)
89    #[serde(default = "default_show_sidebar")]
90    pub show_sidebar: bool,
91
92    /// Dim completed todo items (- [x]) in agent output
93    #[serde(default = "default_dim_completed_todos")]
94    pub dim_completed_todos: bool,
95
96    /// Add spacing between message blocks
97    #[serde(default = "default_message_block_spacing")]
98    pub message_block_spacing: bool,
99
100    // === Color Accessibility Configuration ===
101    // Based on NO_COLOR standard, Ghostty minimum-contrast, and terminal color portability research
102    // See: https://no-color.org/, https://ghostty.org/docs/config/reference#minimum-contrast
103    /// Minimum contrast ratio for text against background (WCAG 2.1 standard)
104    /// - 4.5: WCAG AA (default, suitable for most users)
105    /// - 7.0: WCAG AAA (enhanced, for low-vision users)
106    /// - 3.0: Large text minimum
107    /// - 1.0: Disable contrast enforcement
108    #[serde(default = "default_minimum_contrast")]
109    pub minimum_contrast: f64,
110
111    /// Compatibility mode for legacy terminals that map bold to bright colors.
112    /// When enabled, avoids using bold styling on text that would become bright colors,
113    /// preventing visibility issues in terminals with "bold is bright" behavior.
114    #[serde(default = "default_bold_is_bright")]
115    pub bold_is_bright: bool,
116
117    /// Restrict color palette to the 11 "safe" ANSI colors portable across common themes.
118    /// Safe colors: red, green, yellow, blue, magenta, cyan + brred, brgreen, brmagenta, brcyan
119    /// Problematic colors avoided: brblack (invisible in Solarized Dark), bryellow (light themes),
120    /// white/brwhite (light themes), brblue (Basic Dark).
121    /// See: https://blog.xoria.org/terminal-colors/
122    #[serde(default = "default_safe_colors_only")]
123    pub safe_colors_only: bool,
124
125    /// Color scheme mode for automatic light/dark theme switching.
126    /// - "auto": Detect from terminal (via OSC 11 or COLORFGBG env var)
127    /// - "light": Force light mode theme selection
128    /// - "dark": Force dark mode theme selection
129    #[serde(default = "default_color_scheme_mode")]
130    pub color_scheme_mode: ColorSchemeMode,
131}
132
133/// Color scheme mode for theme selection
134#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
135#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
136#[serde(rename_all = "snake_case")]
137pub enum ColorSchemeMode {
138    /// Detect from terminal environment (OSC 11 query or COLORFGBG)
139    #[default]
140    Auto,
141    /// Force light color scheme
142    Light,
143    /// Force dark color scheme
144    Dark,
145}
146
147fn default_minimum_contrast() -> f64 {
148    crate::constants::ui::THEME_MIN_CONTRAST_RATIO
149}
150
151fn default_bold_is_bright() -> bool {
152    false
153}
154
155fn default_safe_colors_only() -> bool {
156    false
157}
158
159fn default_color_scheme_mode() -> ColorSchemeMode {
160    ColorSchemeMode::Auto
161}
162
163fn default_show_sidebar() -> bool {
164    true
165}
166
167fn default_dim_completed_todos() -> bool {
168    true
169}
170
171fn default_message_block_spacing() -> bool {
172    true
173}
174
175impl Default for UiConfig {
176    fn default() -> Self {
177        Self {
178            tool_output_mode: default_tool_output_mode(),
179            tool_output_max_lines: default_tool_output_max_lines(),
180            tool_output_spool_bytes: default_tool_output_spool_bytes(),
181            tool_output_spool_dir: None,
182            allow_tool_ansi: default_allow_tool_ansi(),
183            inline_viewport_rows: default_inline_viewport_rows(),
184            reasoning_display_mode: default_reasoning_display_mode(),
185            reasoning_visible_default: default_reasoning_visible_default(),
186            status_line: StatusLineConfig::default(),
187            keyboard_protocol: KeyboardProtocolConfig::default(),
188            layout_mode: LayoutModeOverride::default(),
189            display_mode: UiDisplayMode::default(),
190            show_sidebar: default_show_sidebar(),
191            dim_completed_todos: default_dim_completed_todos(),
192            message_block_spacing: default_message_block_spacing(),
193            // Color accessibility defaults
194            minimum_contrast: default_minimum_contrast(),
195            bold_is_bright: default_bold_is_bright(),
196            safe_colors_only: default_safe_colors_only(),
197            color_scheme_mode: default_color_scheme_mode(),
198        }
199    }
200}
201
202/// PTY configuration
203#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
204#[derive(Debug, Clone, Deserialize, Serialize)]
205pub struct PtyConfig {
206    /// Enable PTY functionality
207    #[serde(default = "default_pty_enabled")]
208    pub enabled: bool,
209
210    /// Default terminal rows
211    #[serde(default = "default_pty_rows")]
212    pub default_rows: u16,
213
214    /// Default terminal columns
215    #[serde(default = "default_pty_cols")]
216    pub default_cols: u16,
217
218    /// Maximum number of concurrent PTY sessions
219    #[serde(default = "default_max_pty_sessions")]
220    pub max_sessions: usize,
221
222    /// Command timeout in seconds
223    #[serde(default = "default_pty_timeout")]
224    pub command_timeout_seconds: u64,
225
226    /// Number of PTY stdout lines to display in chat output
227    #[serde(default = "default_stdout_tail_lines")]
228    pub stdout_tail_lines: usize,
229
230    /// Maximum number of scrollback lines retained per PTY session
231    #[serde(default = "default_scrollback_lines")]
232    pub scrollback_lines: usize,
233
234    /// Maximum bytes of output to retain per PTY session (prevents memory explosion)
235    #[serde(default = "default_max_scrollback_bytes")]
236    pub max_scrollback_bytes: usize,
237
238    /// Threshold (KB) at which to auto-spool large outputs to disk
239    #[serde(default = "default_large_output_threshold_kb")]
240    pub large_output_threshold_kb: usize,
241
242    /// Preferred shell program for PTY sessions (falls back to environment when unset)
243    #[serde(default)]
244    pub preferred_shell: Option<String>,
245}
246
247impl Default for PtyConfig {
248    fn default() -> Self {
249        Self {
250            enabled: default_pty_enabled(),
251            default_rows: default_pty_rows(),
252            default_cols: default_pty_cols(),
253            max_sessions: default_max_pty_sessions(),
254            command_timeout_seconds: default_pty_timeout(),
255            stdout_tail_lines: default_stdout_tail_lines(),
256            scrollback_lines: default_scrollback_lines(),
257            max_scrollback_bytes: default_max_scrollback_bytes(),
258            large_output_threshold_kb: default_large_output_threshold_kb(),
259            preferred_shell: None,
260        }
261    }
262}
263
264fn default_pty_enabled() -> bool {
265    true
266}
267
268fn default_pty_rows() -> u16 {
269    24
270}
271
272fn default_pty_cols() -> u16 {
273    80
274}
275
276fn default_max_pty_sessions() -> usize {
277    10
278}
279
280fn default_pty_timeout() -> u64 {
281    300
282}
283
284fn default_stdout_tail_lines() -> usize {
285    crate::constants::defaults::DEFAULT_PTY_STDOUT_TAIL_LINES
286}
287
288fn default_scrollback_lines() -> usize {
289    crate::constants::defaults::DEFAULT_PTY_SCROLLBACK_LINES
290}
291
292fn default_max_scrollback_bytes() -> usize {
293    // Reduced from 50MB to 25MB for memory-constrained development environments
294    // Can be overridden in vtcode.toml with: pty.max_scrollback_bytes = 52428800
295    25_000_000 // 25MB max to prevent memory explosion
296}
297
298fn default_large_output_threshold_kb() -> usize {
299    5_000 // 5MB threshold for auto-spooling
300}
301
302fn default_tool_output_mode() -> ToolOutputMode {
303    ToolOutputMode::Compact
304}
305
306fn default_tool_output_max_lines() -> usize {
307    600
308}
309
310fn default_tool_output_spool_bytes() -> usize {
311    200_000
312}
313
314fn default_allow_tool_ansi() -> bool {
315    false
316}
317
318fn default_inline_viewport_rows() -> u16 {
319    crate::constants::ui::DEFAULT_INLINE_VIEWPORT_ROWS
320}
321
322fn default_reasoning_display_mode() -> ReasoningDisplayMode {
323    ReasoningDisplayMode::Toggle
324}
325
326fn default_reasoning_visible_default() -> bool {
327    crate::constants::ui::DEFAULT_REASONING_VISIBLE
328}
329
330/// Kitty keyboard protocol configuration
331/// Reference: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
332#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
333#[derive(Debug, Clone, Deserialize, Serialize)]
334pub struct KeyboardProtocolConfig {
335    /// Enable keyboard protocol enhancements (master toggle)
336    #[serde(default = "default_keyboard_protocol_enabled")]
337    pub enabled: bool,
338
339    /// Preset mode: "default", "full", "minimal", "custom"
340    #[serde(default = "default_keyboard_protocol_mode")]
341    pub mode: String,
342
343    /// Individual flag controls (used when mode = "custom")
344    /// Resolve Esc key ambiguity (recommended)
345    #[serde(default = "default_disambiguate_escape_codes")]
346    pub disambiguate_escape_codes: bool,
347
348    /// Report press/release/repeat events
349    #[serde(default = "default_report_event_types")]
350    pub report_event_types: bool,
351
352    /// Report alternate key layouts
353    #[serde(default = "default_report_alternate_keys")]
354    pub report_alternate_keys: bool,
355
356    /// Report modifier-only keys (Shift, Ctrl, Alt alone)
357    #[serde(default = "default_report_all_keys")]
358    pub report_all_keys: bool,
359}
360
361impl Default for KeyboardProtocolConfig {
362    fn default() -> Self {
363        Self {
364            enabled: default_keyboard_protocol_enabled(),
365            mode: default_keyboard_protocol_mode(),
366            disambiguate_escape_codes: default_disambiguate_escape_codes(),
367            report_event_types: default_report_event_types(),
368            report_alternate_keys: default_report_alternate_keys(),
369            report_all_keys: default_report_all_keys(),
370        }
371    }
372}
373
374impl KeyboardProtocolConfig {
375    pub fn validate(&self) -> anyhow::Result<()> {
376        match self.mode.as_str() {
377            "default" | "full" | "minimal" | "custom" => Ok(()),
378            _ => anyhow::bail!(
379                "Invalid keyboard protocol mode '{}'. Must be: default, full, minimal, or custom",
380                self.mode
381            ),
382        }
383    }
384}
385
386fn default_keyboard_protocol_enabled() -> bool {
387    std::env::var("VTCODE_KEYBOARD_PROTOCOL_ENABLED")
388        .ok()
389        .and_then(|v| v.parse().ok())
390        .unwrap_or(true)
391}
392
393fn default_keyboard_protocol_mode() -> String {
394    std::env::var("VTCODE_KEYBOARD_PROTOCOL_MODE").unwrap_or_else(|_| "default".to_string())
395}
396
397fn default_disambiguate_escape_codes() -> bool {
398    true
399}
400
401fn default_report_event_types() -> bool {
402    true
403}
404
405fn default_report_alternate_keys() -> bool {
406    true
407}
408
409fn default_report_all_keys() -> bool {
410    false
411}