Skip to main content

vtcode_config/
root.rs

1use anyhow::{Result, anyhow, bail};
2use serde::{Deserialize, Serialize};
3
4use crate::status_line::StatusLineConfig;
5
6#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
7#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
8#[serde(rename_all = "snake_case")]
9#[derive(Default)]
10pub enum ToolOutputMode {
11    #[default]
12    Compact,
13    Full,
14}
15
16#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
17#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
18#[serde(rename_all = "snake_case")]
19#[derive(Default)]
20pub enum ReasoningDisplayMode {
21    Always,
22    #[default]
23    Toggle,
24    Hidden,
25}
26
27/// Layout mode override for responsive UI
28#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
29#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
30#[serde(rename_all = "snake_case")]
31pub enum LayoutModeOverride {
32    /// Auto-detect based on terminal size
33    #[default]
34    Auto,
35    /// Force compact mode (no borders)
36    Compact,
37    /// Force standard mode (borders, no sidebar/footer)
38    Standard,
39    /// Force wide mode (sidebar + footer)
40    Wide,
41}
42
43/// UI display mode variants for quick presets
44#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
45#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
46#[serde(rename_all = "snake_case")]
47pub enum UiDisplayMode {
48    /// Full UI with all features (sidebar, footer)
49    Full,
50    /// Minimal UI - no sidebar, no footer
51    #[default]
52    Minimal,
53    /// Focused mode - transcript only, maximum content space
54    Focused,
55}
56
57/// Notification delivery mode for terminal attention events.
58#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
59#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
60#[serde(rename_all = "snake_case")]
61pub enum NotificationDeliveryMode {
62    /// Terminal-native alerts only (bell/OSC).
63    Terminal,
64    /// Terminal alerts with desktop notifications when supported.
65    #[default]
66    Hybrid,
67    /// Desktop notifications first; fall back to terminal alerts when unavailable.
68    Desktop,
69}
70
71/// Notification preferences for terminal and desktop alerts.
72#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
73#[derive(Debug, Clone, Deserialize, Serialize)]
74pub struct UiNotificationsConfig {
75    /// Master toggle for all runtime notifications.
76    #[serde(default = "default_notifications_enabled")]
77    pub enabled: bool,
78
79    /// Notification transport strategy.
80    #[serde(default)]
81    pub delivery_mode: NotificationDeliveryMode,
82
83    /// Suppress notifications while terminal focus is active.
84    #[serde(default = "default_notifications_suppress_when_focused")]
85    pub suppress_when_focused: bool,
86
87    /// Notify when a shell/command execution fails.
88    /// If omitted, falls back to `tool_failure` for backward compatibility.
89    #[serde(default)]
90    pub command_failure: Option<bool>,
91
92    /// Notify when a tool call fails.
93    #[serde(default = "default_notifications_tool_failure")]
94    pub tool_failure: bool,
95
96    /// Notify on runtime/system errors.
97    #[serde(default = "default_notifications_error")]
98    pub error: bool,
99
100    /// Legacy master toggle for completion notifications.
101    /// New installs should prefer `completion_success` and `completion_failure`.
102    #[serde(default = "default_notifications_completion")]
103    pub completion: bool,
104
105    /// Notify when a turn/session completes successfully.
106    /// If omitted, falls back to `completion`.
107    #[serde(default)]
108    pub completion_success: Option<bool>,
109
110    /// Notify when a turn/session is partial, failed, or cancelled.
111    /// If omitted, falls back to `completion`.
112    #[serde(default)]
113    pub completion_failure: Option<bool>,
114
115    /// Notify when human input/approval is required.
116    #[serde(default = "default_notifications_hitl")]
117    pub hitl: bool,
118
119    /// Notify when policy approval is required.
120    /// If omitted, falls back to `hitl` for backward compatibility.
121    #[serde(default)]
122    pub policy_approval: Option<bool>,
123
124    /// Notify on generic request events.
125    /// If omitted, falls back to `hitl` for backward compatibility.
126    #[serde(default)]
127    pub request: Option<bool>,
128
129    /// Notify on successful tool calls.
130    #[serde(default = "default_notifications_tool_success")]
131    pub tool_success: bool,
132
133    /// Suppression window for repeated identical notifications.
134    #[serde(default = "default_notifications_repeat_window_seconds")]
135    pub repeat_window_seconds: u64,
136
137    /// Maximum identical notifications allowed within the suppression window.
138    #[serde(default = "default_notifications_max_identical_in_window")]
139    pub max_identical_in_window: u32,
140}
141
142impl Default for UiNotificationsConfig {
143    fn default() -> Self {
144        Self {
145            enabled: default_notifications_enabled(),
146            delivery_mode: NotificationDeliveryMode::default(),
147            suppress_when_focused: default_notifications_suppress_when_focused(),
148            command_failure: Some(default_notifications_command_failure()),
149            tool_failure: default_notifications_tool_failure(),
150            error: default_notifications_error(),
151            completion: default_notifications_completion(),
152            completion_success: Some(default_notifications_completion_success()),
153            completion_failure: Some(default_notifications_completion_failure()),
154            hitl: default_notifications_hitl(),
155            policy_approval: Some(default_notifications_policy_approval()),
156            request: Some(default_notifications_request()),
157            tool_success: default_notifications_tool_success(),
158            repeat_window_seconds: default_notifications_repeat_window_seconds(),
159            max_identical_in_window: default_notifications_max_identical_in_window(),
160        }
161    }
162}
163
164#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
165#[derive(Debug, Clone, Deserialize, Serialize)]
166pub struct UiConfig {
167    /// Tool output display mode ("compact" or "full")
168    #[serde(default = "default_tool_output_mode")]
169    pub tool_output_mode: ToolOutputMode,
170
171    /// Maximum number of lines to display in tool output (prevents transcript flooding)
172    #[serde(default = "default_tool_output_max_lines")]
173    pub tool_output_max_lines: usize,
174
175    /// Maximum bytes of output to display before auto-spooling to disk
176    #[serde(default = "default_tool_output_spool_bytes")]
177    pub tool_output_spool_bytes: usize,
178
179    /// Optional custom directory for spooled tool output logs
180    #[serde(default)]
181    pub tool_output_spool_dir: Option<String>,
182
183    /// Allow ANSI escape sequences in tool output (enables colors but may cause layout issues)
184    #[serde(default = "default_allow_tool_ansi")]
185    pub allow_tool_ansi: bool,
186
187    /// Number of rows to allocate for inline UI viewport
188    #[serde(default = "default_inline_viewport_rows")]
189    pub inline_viewport_rows: u16,
190
191    /// Reasoning display mode for chat UI ("always", "toggle", or "hidden")
192    #[serde(default = "default_reasoning_display_mode")]
193    pub reasoning_display_mode: ReasoningDisplayMode,
194
195    /// Default visibility for reasoning when display mode is "toggle"
196    #[serde(default = "default_reasoning_visible_default")]
197    pub reasoning_visible_default: bool,
198
199    /// Enable Vim-style prompt editing in the interactive terminal UI.
200    #[serde(default = "default_vim_mode")]
201    pub vim_mode: bool,
202
203    /// Status line configuration settings
204    #[serde(default)]
205    pub status_line: StatusLineConfig,
206
207    /// Keyboard protocol enhancements for modern terminals (e.g. Kitty protocol)
208    #[serde(default)]
209    pub keyboard_protocol: KeyboardProtocolConfig,
210
211    /// Override the responsive layout mode
212    #[serde(default)]
213    pub layout_mode: LayoutModeOverride,
214
215    /// UI display mode preset (full, minimal, focused)
216    #[serde(default)]
217    pub display_mode: UiDisplayMode,
218
219    /// Show the right sidebar (queue, context, tools)
220    #[serde(default = "default_show_sidebar")]
221    pub show_sidebar: bool,
222
223    /// Dim completed todo items (- [x]) in agent output
224    #[serde(default = "default_dim_completed_todos")]
225    pub dim_completed_todos: bool,
226
227    /// Add spacing between message blocks
228    #[serde(default = "default_message_block_spacing")]
229    pub message_block_spacing: bool,
230
231    /// Show per-turn elapsed timer line after completed turns
232    #[serde(default = "default_show_turn_timer")]
233    pub show_turn_timer: bool,
234
235    /// Show warning/error/fatal diagnostic lines in the TUI transcript and log panel.
236    /// Also controls whether ERROR-level tracing logs appear in the TUI session log.
237    /// Errors are always captured in the session archive JSON regardless of this setting.
238    #[serde(default = "default_show_diagnostics_in_transcript")]
239    pub show_diagnostics_in_transcript: bool,
240
241    // === Color Accessibility Configuration ===
242    // Based on NO_COLOR standard, Ghostty minimum-contrast, and terminal color portability research
243    // See: https://no-color.org/, https://ghostty.org/docs/config/reference#minimum-contrast
244    /// Minimum contrast ratio for text against background (WCAG 2.1 standard)
245    /// - 4.5: WCAG AA (default, suitable for most users)
246    /// - 7.0: WCAG AAA (enhanced, for low-vision users)
247    /// - 3.0: Large text minimum
248    /// - 1.0: Disable contrast enforcement
249    #[serde(default = "default_minimum_contrast")]
250    pub minimum_contrast: f64,
251
252    /// Compatibility mode for legacy terminals that map bold to bright colors.
253    /// When enabled, avoids using bold styling on text that would become bright colors,
254    /// preventing visibility issues in terminals with "bold is bright" behavior.
255    #[serde(default = "default_bold_is_bright")]
256    pub bold_is_bright: bool,
257
258    /// Restrict color palette to the 11 "safe" ANSI colors portable across common themes.
259    /// Safe colors: red, green, yellow, blue, magenta, cyan + brred, brgreen, brmagenta, brcyan
260    /// Problematic colors avoided: brblack (invisible in Solarized Dark), bryellow (light themes),
261    /// white/brwhite (light themes), brblue (Basic Dark).
262    /// See: https://blog.xoria.org/terminal-colors/
263    #[serde(default = "default_safe_colors_only")]
264    pub safe_colors_only: bool,
265
266    /// Color scheme mode for automatic light/dark theme switching.
267    /// - "auto": Detect from terminal (via OSC 11 or COLORFGBG env var)
268    /// - "light": Force light mode theme selection
269    /// - "dark": Force dark mode theme selection
270    #[serde(default = "default_color_scheme_mode")]
271    pub color_scheme_mode: ColorSchemeMode,
272
273    /// Notification preferences for attention events.
274    #[serde(default)]
275    pub notifications: UiNotificationsConfig,
276
277    /// Screen reader mode: disables animations, uses plain text indicators,
278    /// and optimizes output for assistive technology compatibility.
279    /// Can also be enabled via VTCODE_SCREEN_READER=1 environment variable.
280    #[serde(default = "default_screen_reader_mode")]
281    pub screen_reader_mode: bool,
282
283    /// Reduce motion mode: minimizes shimmer/flashing animations.
284    /// Can also be enabled via VTCODE_REDUCE_MOTION=1 environment variable.
285    #[serde(default = "default_reduce_motion_mode")]
286    pub reduce_motion_mode: bool,
287
288    /// Keep animated progress indicators while reduce_motion_mode is enabled.
289    #[serde(default = "default_reduce_motion_keep_progress_animation")]
290    pub reduce_motion_keep_progress_animation: bool,
291}
292
293/// Color scheme mode for theme selection
294#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
295#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
296#[serde(rename_all = "snake_case")]
297pub enum ColorSchemeMode {
298    /// Detect from terminal environment (OSC 11 query or COLORFGBG)
299    #[default]
300    Auto,
301    /// Force light color scheme
302    Light,
303    /// Force dark color scheme
304    Dark,
305}
306
307fn default_minimum_contrast() -> f64 {
308    crate::constants::ui::THEME_MIN_CONTRAST_RATIO
309}
310
311fn default_bold_is_bright() -> bool {
312    false
313}
314
315fn default_safe_colors_only() -> bool {
316    false
317}
318
319fn default_color_scheme_mode() -> ColorSchemeMode {
320    ColorSchemeMode::Auto
321}
322
323fn default_show_sidebar() -> bool {
324    true
325}
326
327fn default_dim_completed_todos() -> bool {
328    true
329}
330
331fn default_message_block_spacing() -> bool {
332    true
333}
334
335fn default_show_turn_timer() -> bool {
336    false
337}
338
339fn default_show_diagnostics_in_transcript() -> bool {
340    false
341}
342
343fn default_vim_mode() -> bool {
344    false
345}
346
347fn default_notifications_enabled() -> bool {
348    true
349}
350
351fn default_notifications_suppress_when_focused() -> bool {
352    true
353}
354
355fn default_notifications_command_failure() -> bool {
356    false
357}
358
359fn default_notifications_tool_failure() -> bool {
360    false
361}
362
363fn default_notifications_error() -> bool {
364    true
365}
366
367fn default_notifications_completion() -> bool {
368    true
369}
370
371fn default_notifications_completion_success() -> bool {
372    false
373}
374
375fn default_notifications_completion_failure() -> bool {
376    true
377}
378
379fn default_notifications_hitl() -> bool {
380    true
381}
382
383fn default_notifications_policy_approval() -> bool {
384    true
385}
386
387fn default_notifications_request() -> bool {
388    false
389}
390
391fn default_notifications_tool_success() -> bool {
392    false
393}
394
395fn default_notifications_repeat_window_seconds() -> u64 {
396    30
397}
398
399fn default_notifications_max_identical_in_window() -> u32 {
400    1
401}
402
403fn env_bool_var(name: &str) -> Option<bool> {
404    std::env::var(name).ok().and_then(|v| {
405        let normalized = v.trim().to_ascii_lowercase();
406        match normalized.as_str() {
407            "1" | "true" | "yes" | "on" => Some(true),
408            "0" | "false" | "no" | "off" => Some(false),
409            _ => None,
410        }
411    })
412}
413
414fn default_screen_reader_mode() -> bool {
415    env_bool_var("VTCODE_SCREEN_READER").unwrap_or(false)
416}
417
418fn default_reduce_motion_mode() -> bool {
419    env_bool_var("VTCODE_REDUCE_MOTION").unwrap_or(false)
420}
421
422fn default_reduce_motion_keep_progress_animation() -> bool {
423    false
424}
425
426fn default_ask_questions_enabled() -> bool {
427    true
428}
429
430impl Default for UiConfig {
431    fn default() -> Self {
432        Self {
433            tool_output_mode: default_tool_output_mode(),
434            tool_output_max_lines: default_tool_output_max_lines(),
435            tool_output_spool_bytes: default_tool_output_spool_bytes(),
436            tool_output_spool_dir: None,
437            allow_tool_ansi: default_allow_tool_ansi(),
438            inline_viewport_rows: default_inline_viewport_rows(),
439            reasoning_display_mode: default_reasoning_display_mode(),
440            reasoning_visible_default: default_reasoning_visible_default(),
441            vim_mode: default_vim_mode(),
442            status_line: StatusLineConfig::default(),
443            keyboard_protocol: KeyboardProtocolConfig::default(),
444            layout_mode: LayoutModeOverride::default(),
445            display_mode: UiDisplayMode::default(),
446            show_sidebar: default_show_sidebar(),
447            dim_completed_todos: default_dim_completed_todos(),
448            message_block_spacing: default_message_block_spacing(),
449            show_turn_timer: default_show_turn_timer(),
450            show_diagnostics_in_transcript: default_show_diagnostics_in_transcript(),
451            // Color accessibility defaults
452            minimum_contrast: default_minimum_contrast(),
453            bold_is_bright: default_bold_is_bright(),
454            safe_colors_only: default_safe_colors_only(),
455            color_scheme_mode: default_color_scheme_mode(),
456            notifications: UiNotificationsConfig::default(),
457            screen_reader_mode: default_screen_reader_mode(),
458            reduce_motion_mode: default_reduce_motion_mode(),
459            reduce_motion_keep_progress_animation: default_reduce_motion_keep_progress_animation(),
460        }
461    }
462}
463
464/// Chat configuration
465#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
466#[derive(Debug, Clone, Deserialize, Serialize, Default)]
467pub struct ChatConfig {
468    /// Ask Questions tool configuration (chat.askQuestions.*)
469    #[serde(default, rename = "askQuestions", alias = "ask_questions")]
470    pub ask_questions: AskQuestionsConfig,
471}
472
473/// Ask Questions tool configuration
474#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
475#[derive(Debug, Clone, Deserialize, Serialize)]
476pub struct AskQuestionsConfig {
477    /// Enable the Ask Questions tool in interactive chat
478    #[serde(default = "default_ask_questions_enabled")]
479    pub enabled: bool,
480}
481
482impl Default for AskQuestionsConfig {
483    fn default() -> Self {
484        Self {
485            enabled: default_ask_questions_enabled(),
486        }
487    }
488}
489
490/// PTY configuration
491#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
492#[derive(Debug, Clone, Deserialize, Serialize)]
493pub struct PtyConfig {
494    /// Enable PTY support for interactive commands
495    #[serde(default = "default_pty_enabled")]
496    pub enabled: bool,
497
498    /// Default terminal rows for PTY sessions
499    #[serde(default = "default_pty_rows")]
500    pub default_rows: u16,
501
502    /// Default terminal columns for PTY sessions
503    #[serde(default = "default_pty_cols")]
504    pub default_cols: u16,
505
506    /// Maximum number of concurrent PTY sessions
507    #[serde(default = "default_max_pty_sessions")]
508    pub max_sessions: usize,
509
510    /// Command timeout in seconds (prevents hanging commands)
511    #[serde(default = "default_pty_timeout")]
512    pub command_timeout_seconds: u64,
513
514    /// Number of recent PTY output lines to display in the chat transcript
515    #[serde(default = "default_stdout_tail_lines")]
516    pub stdout_tail_lines: usize,
517
518    /// Total scrollback buffer size (lines) retained per PTY session
519    #[serde(default = "default_scrollback_lines")]
520    pub scrollback_lines: usize,
521
522    /// Maximum bytes of output to retain per PTY session (prevents memory explosion)
523    #[serde(default = "default_max_scrollback_bytes")]
524    pub max_scrollback_bytes: usize,
525
526    /// Terminal emulation backend used for screen and scrollback snapshots.
527    #[serde(default)]
528    pub emulation_backend: PtyEmulationBackend,
529
530    /// Threshold (KB) at which to auto-spool large outputs to disk instead of memory
531    #[serde(default = "default_large_output_threshold_kb")]
532    pub large_output_threshold_kb: usize,
533
534    /// Preferred shell program for PTY sessions (e.g. "zsh", "bash"); falls back to $SHELL
535    #[serde(default)]
536    pub preferred_shell: Option<String>,
537
538    /// Feature-gated shell runtime path that routes shell execution through zsh EXEC_WRAPPER hooks.
539    #[serde(default = "default_shell_zsh_fork")]
540    pub shell_zsh_fork: bool,
541
542    /// Optional absolute path to patched zsh used when shell_zsh_fork is enabled.
543    #[serde(default)]
544    pub zsh_path: Option<String>,
545}
546
547impl Default for PtyConfig {
548    fn default() -> Self {
549        Self {
550            enabled: default_pty_enabled(),
551            default_rows: default_pty_rows(),
552            default_cols: default_pty_cols(),
553            max_sessions: default_max_pty_sessions(),
554            command_timeout_seconds: default_pty_timeout(),
555            stdout_tail_lines: default_stdout_tail_lines(),
556            scrollback_lines: default_scrollback_lines(),
557            max_scrollback_bytes: default_max_scrollback_bytes(),
558            emulation_backend: PtyEmulationBackend::default(),
559            large_output_threshold_kb: default_large_output_threshold_kb(),
560            preferred_shell: None,
561            shell_zsh_fork: default_shell_zsh_fork(),
562            zsh_path: None,
563        }
564    }
565}
566
567#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
568#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
569#[serde(rename_all = "snake_case")]
570pub enum PtyEmulationBackend {
571    #[default]
572    LegacyVt100,
573    Ghostty,
574}
575
576impl PtyEmulationBackend {
577    #[must_use]
578    pub const fn as_str(self) -> &'static str {
579        match self {
580            Self::Ghostty => "ghostty",
581            Self::LegacyVt100 => "legacy_vt100",
582        }
583    }
584}
585
586impl PtyConfig {
587    pub fn validate(&self) -> Result<()> {
588        self.zsh_fork_shell_path()?;
589        Ok(())
590    }
591
592    pub fn zsh_fork_shell_path(&self) -> Result<Option<&str>> {
593        if !self.shell_zsh_fork {
594            return Ok(None);
595        }
596
597        let zsh_path = self
598            .zsh_path
599            .as_deref()
600            .map(str::trim)
601            .filter(|path| !path.is_empty())
602            .ok_or_else(|| {
603                anyhow!(
604                    "pty.shell_zsh_fork is enabled, but pty.zsh_path is not configured. \
605                     Set pty.zsh_path to an absolute path to patched zsh."
606                )
607            })?;
608
609        #[cfg(not(unix))]
610        {
611            let _ = zsh_path;
612            bail!("pty.shell_zsh_fork is only supported on Unix platforms");
613        }
614
615        #[cfg(unix)]
616        {
617            let path = std::path::Path::new(zsh_path);
618            if !path.is_absolute() {
619                bail!(
620                    "pty.zsh_path '{}' must be an absolute path when pty.shell_zsh_fork is enabled",
621                    zsh_path
622                );
623            }
624            if !path.exists() {
625                bail!(
626                    "pty.zsh_path '{}' does not exist (required when pty.shell_zsh_fork is enabled)",
627                    zsh_path
628                );
629            }
630            if !path.is_file() {
631                bail!(
632                    "pty.zsh_path '{}' is not a file (required when pty.shell_zsh_fork is enabled)",
633                    zsh_path
634                );
635            }
636        }
637
638        Ok(Some(zsh_path))
639    }
640}
641
642fn default_pty_enabled() -> bool {
643    true
644}
645
646fn default_pty_rows() -> u16 {
647    24
648}
649
650fn default_pty_cols() -> u16 {
651    80
652}
653
654fn default_max_pty_sessions() -> usize {
655    10
656}
657
658fn default_pty_timeout() -> u64 {
659    300
660}
661
662fn default_shell_zsh_fork() -> bool {
663    false
664}
665
666fn default_stdout_tail_lines() -> usize {
667    crate::constants::defaults::DEFAULT_PTY_STDOUT_TAIL_LINES
668}
669
670fn default_scrollback_lines() -> usize {
671    crate::constants::defaults::DEFAULT_PTY_SCROLLBACK_LINES
672}
673
674fn default_max_scrollback_bytes() -> usize {
675    // Reduced from 50MB to 25MB for memory-constrained development environments
676    // Can be overridden in vtcode.toml with: pty.max_scrollback_bytes = 52428800
677    25_000_000 // 25MB max to prevent memory explosion
678}
679
680fn default_large_output_threshold_kb() -> usize {
681    5_000 // 5MB threshold for auto-spooling
682}
683
684fn default_tool_output_mode() -> ToolOutputMode {
685    ToolOutputMode::Compact
686}
687
688fn default_tool_output_max_lines() -> usize {
689    600
690}
691
692fn default_tool_output_spool_bytes() -> usize {
693    200_000
694}
695
696fn default_allow_tool_ansi() -> bool {
697    false
698}
699
700fn default_inline_viewport_rows() -> u16 {
701    crate::constants::ui::DEFAULT_INLINE_VIEWPORT_ROWS
702}
703
704fn default_reasoning_display_mode() -> ReasoningDisplayMode {
705    ReasoningDisplayMode::Toggle
706}
707
708fn default_reasoning_visible_default() -> bool {
709    crate::constants::ui::DEFAULT_REASONING_VISIBLE
710}
711
712/// Kitty keyboard protocol configuration
713/// Reference: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
714#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
715#[derive(Debug, Clone, Deserialize, Serialize)]
716pub struct KeyboardProtocolConfig {
717    /// Enable keyboard protocol enhancements (master toggle)
718    #[serde(default = "default_keyboard_protocol_enabled")]
719    pub enabled: bool,
720
721    /// Preset mode: "default", "full", "minimal", or "custom"
722    #[serde(default = "default_keyboard_protocol_mode")]
723    pub mode: String,
724
725    /// Resolve Esc key ambiguity (recommended for performance)
726    #[serde(default = "default_disambiguate_escape_codes")]
727    pub disambiguate_escape_codes: bool,
728
729    /// Report press, release, and repeat events
730    #[serde(default = "default_report_event_types")]
731    pub report_event_types: bool,
732
733    /// Report alternate key layouts (e.g. for non-US keyboards)
734    #[serde(default = "default_report_alternate_keys")]
735    pub report_alternate_keys: bool,
736
737    /// Report all keys, including modifier-only keys (Shift, Ctrl)
738    #[serde(default = "default_report_all_keys")]
739    pub report_all_keys: bool,
740}
741
742impl Default for KeyboardProtocolConfig {
743    fn default() -> Self {
744        Self {
745            enabled: default_keyboard_protocol_enabled(),
746            mode: default_keyboard_protocol_mode(),
747            disambiguate_escape_codes: default_disambiguate_escape_codes(),
748            report_event_types: default_report_event_types(),
749            report_alternate_keys: default_report_alternate_keys(),
750            report_all_keys: default_report_all_keys(),
751        }
752    }
753}
754
755impl KeyboardProtocolConfig {
756    pub fn validate(&self) -> Result<()> {
757        match self.mode.as_str() {
758            "default" | "full" | "minimal" | "custom" => Ok(()),
759            _ => anyhow::bail!(
760                "Invalid keyboard protocol mode '{}'. Must be: default, full, minimal, or custom",
761                self.mode
762            ),
763        }
764    }
765}
766
767fn default_keyboard_protocol_enabled() -> bool {
768    std::env::var("VTCODE_KEYBOARD_PROTOCOL_ENABLED")
769        .ok()
770        .and_then(|v| v.parse().ok())
771        .unwrap_or(true)
772}
773
774fn default_keyboard_protocol_mode() -> String {
775    std::env::var("VTCODE_KEYBOARD_PROTOCOL_MODE").unwrap_or_else(|_| "default".to_string())
776}
777
778fn default_disambiguate_escape_codes() -> bool {
779    true
780}
781
782fn default_report_event_types() -> bool {
783    true
784}
785
786fn default_report_alternate_keys() -> bool {
787    true
788}
789
790fn default_report_all_keys() -> bool {
791    false
792}