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#[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 #[default]
34 Auto,
35 Compact,
37 Standard,
39 Wide,
41}
42
43#[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,
50 #[default]
52 Minimal,
53 Focused,
55}
56
57#[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,
64 #[default]
66 Hybrid,
67 Desktop,
69}
70
71#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
73#[derive(Debug, Clone, Deserialize, Serialize)]
74pub struct UiNotificationsConfig {
75 #[serde(default = "default_notifications_enabled")]
77 pub enabled: bool,
78
79 #[serde(default)]
81 pub delivery_mode: NotificationDeliveryMode,
82
83 #[serde(default = "default_notifications_suppress_when_focused")]
85 pub suppress_when_focused: bool,
86
87 #[serde(default = "default_notifications_tool_failure")]
89 pub tool_failure: bool,
90
91 #[serde(default = "default_notifications_error")]
93 pub error: bool,
94
95 #[serde(default = "default_notifications_completion")]
97 pub completion: bool,
98
99 #[serde(default = "default_notifications_hitl")]
101 pub hitl: bool,
102
103 #[serde(default = "default_notifications_tool_success")]
105 pub tool_success: bool,
106}
107
108impl Default for UiNotificationsConfig {
109 fn default() -> Self {
110 Self {
111 enabled: default_notifications_enabled(),
112 delivery_mode: NotificationDeliveryMode::default(),
113 suppress_when_focused: default_notifications_suppress_when_focused(),
114 tool_failure: default_notifications_tool_failure(),
115 error: default_notifications_error(),
116 completion: default_notifications_completion(),
117 hitl: default_notifications_hitl(),
118 tool_success: default_notifications_tool_success(),
119 }
120 }
121}
122
123#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
124#[derive(Debug, Clone, Deserialize, Serialize)]
125pub struct UiConfig {
126 #[serde(default = "default_tool_output_mode")]
128 pub tool_output_mode: ToolOutputMode,
129
130 #[serde(default = "default_tool_output_max_lines")]
132 pub tool_output_max_lines: usize,
133
134 #[serde(default = "default_tool_output_spool_bytes")]
136 pub tool_output_spool_bytes: usize,
137
138 #[serde(default)]
140 pub tool_output_spool_dir: Option<String>,
141
142 #[serde(default = "default_allow_tool_ansi")]
144 pub allow_tool_ansi: bool,
145
146 #[serde(default = "default_inline_viewport_rows")]
148 pub inline_viewport_rows: u16,
149
150 #[serde(default = "default_reasoning_display_mode")]
152 pub reasoning_display_mode: ReasoningDisplayMode,
153
154 #[serde(default = "default_reasoning_visible_default")]
156 pub reasoning_visible_default: bool,
157
158 #[serde(default)]
160 pub status_line: StatusLineConfig,
161
162 #[serde(default)]
164 pub keyboard_protocol: KeyboardProtocolConfig,
165
166 #[serde(default)]
168 pub layout_mode: LayoutModeOverride,
169
170 #[serde(default)]
172 pub display_mode: UiDisplayMode,
173
174 #[serde(default = "default_show_sidebar")]
176 pub show_sidebar: bool,
177
178 #[serde(default = "default_dim_completed_todos")]
180 pub dim_completed_todos: bool,
181
182 #[serde(default = "default_message_block_spacing")]
184 pub message_block_spacing: bool,
185
186 #[serde(default = "default_show_turn_timer")]
188 pub show_turn_timer: bool,
189
190 #[serde(default = "default_show_diagnostics_in_transcript")]
194 pub show_diagnostics_in_transcript: bool,
195
196 #[serde(default = "default_minimum_contrast")]
205 pub minimum_contrast: f64,
206
207 #[serde(default = "default_bold_is_bright")]
211 pub bold_is_bright: bool,
212
213 #[serde(default = "default_safe_colors_only")]
219 pub safe_colors_only: bool,
220
221 #[serde(default = "default_color_scheme_mode")]
226 pub color_scheme_mode: ColorSchemeMode,
227
228 #[serde(default)]
230 pub notifications: UiNotificationsConfig,
231
232 #[serde(default = "default_screen_reader_mode")]
236 pub screen_reader_mode: bool,
237
238 #[serde(default = "default_reduce_motion_mode")]
241 pub reduce_motion_mode: bool,
242
243 #[serde(default = "default_reduce_motion_keep_progress_animation")]
245 pub reduce_motion_keep_progress_animation: bool,
246}
247
248#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
250#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
251#[serde(rename_all = "snake_case")]
252pub enum ColorSchemeMode {
253 #[default]
255 Auto,
256 Light,
258 Dark,
260}
261
262fn default_minimum_contrast() -> f64 {
263 crate::constants::ui::THEME_MIN_CONTRAST_RATIO
264}
265
266fn default_bold_is_bright() -> bool {
267 false
268}
269
270fn default_safe_colors_only() -> bool {
271 false
272}
273
274fn default_color_scheme_mode() -> ColorSchemeMode {
275 ColorSchemeMode::Auto
276}
277
278fn default_show_sidebar() -> bool {
279 true
280}
281
282fn default_dim_completed_todos() -> bool {
283 true
284}
285
286fn default_message_block_spacing() -> bool {
287 true
288}
289
290fn default_show_turn_timer() -> bool {
291 false
292}
293
294fn default_show_diagnostics_in_transcript() -> bool {
295 false
296}
297
298fn default_notifications_enabled() -> bool {
299 true
300}
301
302fn default_notifications_suppress_when_focused() -> bool {
303 true
304}
305
306fn default_notifications_tool_failure() -> bool {
307 true
308}
309
310fn default_notifications_error() -> bool {
311 true
312}
313
314fn default_notifications_completion() -> bool {
315 true
316}
317
318fn default_notifications_hitl() -> bool {
319 true
320}
321
322fn default_notifications_tool_success() -> bool {
323 false
324}
325
326fn env_bool_var(name: &str) -> Option<bool> {
327 std::env::var(name).ok().and_then(|v| {
328 let normalized = v.trim().to_ascii_lowercase();
329 match normalized.as_str() {
330 "1" | "true" | "yes" | "on" => Some(true),
331 "0" | "false" | "no" | "off" => Some(false),
332 _ => None,
333 }
334 })
335}
336
337fn default_screen_reader_mode() -> bool {
338 env_bool_var("VTCODE_SCREEN_READER").unwrap_or(false)
339}
340
341fn default_reduce_motion_mode() -> bool {
342 env_bool_var("VTCODE_REDUCE_MOTION").unwrap_or(false)
343}
344
345fn default_reduce_motion_keep_progress_animation() -> bool {
346 false
347}
348
349fn default_ask_questions_enabled() -> bool {
350 true
351}
352
353impl Default for UiConfig {
354 fn default() -> Self {
355 Self {
356 tool_output_mode: default_tool_output_mode(),
357 tool_output_max_lines: default_tool_output_max_lines(),
358 tool_output_spool_bytes: default_tool_output_spool_bytes(),
359 tool_output_spool_dir: None,
360 allow_tool_ansi: default_allow_tool_ansi(),
361 inline_viewport_rows: default_inline_viewport_rows(),
362 reasoning_display_mode: default_reasoning_display_mode(),
363 reasoning_visible_default: default_reasoning_visible_default(),
364 status_line: StatusLineConfig::default(),
365 keyboard_protocol: KeyboardProtocolConfig::default(),
366 layout_mode: LayoutModeOverride::default(),
367 display_mode: UiDisplayMode::default(),
368 show_sidebar: default_show_sidebar(),
369 dim_completed_todos: default_dim_completed_todos(),
370 message_block_spacing: default_message_block_spacing(),
371 show_turn_timer: default_show_turn_timer(),
372 show_diagnostics_in_transcript: default_show_diagnostics_in_transcript(),
373 minimum_contrast: default_minimum_contrast(),
375 bold_is_bright: default_bold_is_bright(),
376 safe_colors_only: default_safe_colors_only(),
377 color_scheme_mode: default_color_scheme_mode(),
378 notifications: UiNotificationsConfig::default(),
379 screen_reader_mode: default_screen_reader_mode(),
380 reduce_motion_mode: default_reduce_motion_mode(),
381 reduce_motion_keep_progress_animation: default_reduce_motion_keep_progress_animation(),
382 }
383 }
384}
385
386#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
388#[derive(Debug, Clone, Deserialize, Serialize, Default)]
389pub struct ChatConfig {
390 #[serde(default, rename = "askQuestions", alias = "ask_questions")]
392 pub ask_questions: AskQuestionsConfig,
393}
394
395#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
397#[derive(Debug, Clone, Deserialize, Serialize)]
398pub struct AskQuestionsConfig {
399 #[serde(default = "default_ask_questions_enabled")]
401 pub enabled: bool,
402}
403
404impl Default for AskQuestionsConfig {
405 fn default() -> Self {
406 Self {
407 enabled: default_ask_questions_enabled(),
408 }
409 }
410}
411
412#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
414#[derive(Debug, Clone, Deserialize, Serialize)]
415pub struct PtyConfig {
416 #[serde(default = "default_pty_enabled")]
418 pub enabled: bool,
419
420 #[serde(default = "default_pty_rows")]
422 pub default_rows: u16,
423
424 #[serde(default = "default_pty_cols")]
426 pub default_cols: u16,
427
428 #[serde(default = "default_max_pty_sessions")]
430 pub max_sessions: usize,
431
432 #[serde(default = "default_pty_timeout")]
434 pub command_timeout_seconds: u64,
435
436 #[serde(default = "default_stdout_tail_lines")]
438 pub stdout_tail_lines: usize,
439
440 #[serde(default = "default_scrollback_lines")]
442 pub scrollback_lines: usize,
443
444 #[serde(default = "default_max_scrollback_bytes")]
446 pub max_scrollback_bytes: usize,
447
448 #[serde(default = "default_large_output_threshold_kb")]
450 pub large_output_threshold_kb: usize,
451
452 #[serde(default)]
454 pub preferred_shell: Option<String>,
455
456 #[serde(default = "default_shell_zsh_fork")]
458 pub shell_zsh_fork: bool,
459
460 #[serde(default)]
462 pub zsh_path: Option<String>,
463}
464
465impl Default for PtyConfig {
466 fn default() -> Self {
467 Self {
468 enabled: default_pty_enabled(),
469 default_rows: default_pty_rows(),
470 default_cols: default_pty_cols(),
471 max_sessions: default_max_pty_sessions(),
472 command_timeout_seconds: default_pty_timeout(),
473 stdout_tail_lines: default_stdout_tail_lines(),
474 scrollback_lines: default_scrollback_lines(),
475 max_scrollback_bytes: default_max_scrollback_bytes(),
476 large_output_threshold_kb: default_large_output_threshold_kb(),
477 preferred_shell: None,
478 shell_zsh_fork: default_shell_zsh_fork(),
479 zsh_path: None,
480 }
481 }
482}
483
484impl PtyConfig {
485 pub fn validate(&self) -> Result<()> {
486 self.zsh_fork_shell_path()?;
487 Ok(())
488 }
489
490 pub fn zsh_fork_shell_path(&self) -> Result<Option<&str>> {
491 if !self.shell_zsh_fork {
492 return Ok(None);
493 }
494
495 let zsh_path = self
496 .zsh_path
497 .as_deref()
498 .map(str::trim)
499 .filter(|path| !path.is_empty())
500 .ok_or_else(|| {
501 anyhow!(
502 "pty.shell_zsh_fork is enabled, but pty.zsh_path is not configured. \
503 Set pty.zsh_path to an absolute path to patched zsh."
504 )
505 })?;
506
507 #[cfg(not(unix))]
508 {
509 let _ = zsh_path;
510 bail!("pty.shell_zsh_fork is only supported on Unix platforms");
511 }
512
513 #[cfg(unix)]
514 {
515 let path = std::path::Path::new(zsh_path);
516 if !path.is_absolute() {
517 bail!(
518 "pty.zsh_path '{}' must be an absolute path when pty.shell_zsh_fork is enabled",
519 zsh_path
520 );
521 }
522 if !path.exists() {
523 bail!(
524 "pty.zsh_path '{}' does not exist (required when pty.shell_zsh_fork is enabled)",
525 zsh_path
526 );
527 }
528 if !path.is_file() {
529 bail!(
530 "pty.zsh_path '{}' is not a file (required when pty.shell_zsh_fork is enabled)",
531 zsh_path
532 );
533 }
534 }
535
536 Ok(Some(zsh_path))
537 }
538}
539
540fn default_pty_enabled() -> bool {
541 true
542}
543
544fn default_pty_rows() -> u16 {
545 24
546}
547
548fn default_pty_cols() -> u16 {
549 80
550}
551
552fn default_max_pty_sessions() -> usize {
553 10
554}
555
556fn default_pty_timeout() -> u64 {
557 300
558}
559
560fn default_shell_zsh_fork() -> bool {
561 false
562}
563
564fn default_stdout_tail_lines() -> usize {
565 crate::constants::defaults::DEFAULT_PTY_STDOUT_TAIL_LINES
566}
567
568fn default_scrollback_lines() -> usize {
569 crate::constants::defaults::DEFAULT_PTY_SCROLLBACK_LINES
570}
571
572fn default_max_scrollback_bytes() -> usize {
573 25_000_000 }
577
578fn default_large_output_threshold_kb() -> usize {
579 5_000 }
581
582fn default_tool_output_mode() -> ToolOutputMode {
583 ToolOutputMode::Compact
584}
585
586fn default_tool_output_max_lines() -> usize {
587 600
588}
589
590fn default_tool_output_spool_bytes() -> usize {
591 200_000
592}
593
594fn default_allow_tool_ansi() -> bool {
595 false
596}
597
598fn default_inline_viewport_rows() -> u16 {
599 crate::constants::ui::DEFAULT_INLINE_VIEWPORT_ROWS
600}
601
602fn default_reasoning_display_mode() -> ReasoningDisplayMode {
603 ReasoningDisplayMode::Toggle
604}
605
606fn default_reasoning_visible_default() -> bool {
607 crate::constants::ui::DEFAULT_REASONING_VISIBLE
608}
609
610#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
613#[derive(Debug, Clone, Deserialize, Serialize)]
614pub struct KeyboardProtocolConfig {
615 #[serde(default = "default_keyboard_protocol_enabled")]
617 pub enabled: bool,
618
619 #[serde(default = "default_keyboard_protocol_mode")]
621 pub mode: String,
622
623 #[serde(default = "default_disambiguate_escape_codes")]
625 pub disambiguate_escape_codes: bool,
626
627 #[serde(default = "default_report_event_types")]
629 pub report_event_types: bool,
630
631 #[serde(default = "default_report_alternate_keys")]
633 pub report_alternate_keys: bool,
634
635 #[serde(default = "default_report_all_keys")]
637 pub report_all_keys: bool,
638}
639
640impl Default for KeyboardProtocolConfig {
641 fn default() -> Self {
642 Self {
643 enabled: default_keyboard_protocol_enabled(),
644 mode: default_keyboard_protocol_mode(),
645 disambiguate_escape_codes: default_disambiguate_escape_codes(),
646 report_event_types: default_report_event_types(),
647 report_alternate_keys: default_report_alternate_keys(),
648 report_all_keys: default_report_all_keys(),
649 }
650 }
651}
652
653impl KeyboardProtocolConfig {
654 pub fn validate(&self) -> Result<()> {
655 match self.mode.as_str() {
656 "default" | "full" | "minimal" | "custom" => Ok(()),
657 _ => anyhow::bail!(
658 "Invalid keyboard protocol mode '{}'. Must be: default, full, minimal, or custom",
659 self.mode
660 ),
661 }
662 }
663}
664
665fn default_keyboard_protocol_enabled() -> bool {
666 std::env::var("VTCODE_KEYBOARD_PROTOCOL_ENABLED")
667 .ok()
668 .and_then(|v| v.parse().ok())
669 .unwrap_or(true)
670}
671
672fn default_keyboard_protocol_mode() -> String {
673 std::env::var("VTCODE_KEYBOARD_PROTOCOL_MODE").unwrap_or_else(|_| "default".to_string())
674}
675
676fn default_disambiguate_escape_codes() -> bool {
677 true
678}
679
680fn default_report_event_types() -> bool {
681 true
682}
683
684fn default_report_alternate_keys() -> bool {
685 true
686}
687
688fn default_report_all_keys() -> bool {
689 false
690}