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