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)]
90 pub command_failure: Option<bool>,
91
92 #[serde(default = "default_notifications_tool_failure")]
94 pub tool_failure: bool,
95
96 #[serde(default = "default_notifications_error")]
98 pub error: bool,
99
100 #[serde(default = "default_notifications_completion")]
103 pub completion: bool,
104
105 #[serde(default)]
108 pub completion_success: Option<bool>,
109
110 #[serde(default)]
113 pub completion_failure: Option<bool>,
114
115 #[serde(default = "default_notifications_hitl")]
117 pub hitl: bool,
118
119 #[serde(default)]
122 pub policy_approval: Option<bool>,
123
124 #[serde(default)]
127 pub request: Option<bool>,
128
129 #[serde(default = "default_notifications_tool_success")]
131 pub tool_success: bool,
132
133 #[serde(default = "default_notifications_repeat_window_seconds")]
135 pub repeat_window_seconds: u64,
136
137 #[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 #[serde(default = "default_tool_output_mode")]
169 pub tool_output_mode: ToolOutputMode,
170
171 #[serde(default = "default_tool_output_max_lines")]
173 pub tool_output_max_lines: usize,
174
175 #[serde(default = "default_tool_output_spool_bytes")]
177 pub tool_output_spool_bytes: usize,
178
179 #[serde(default)]
181 pub tool_output_spool_dir: Option<String>,
182
183 #[serde(default = "default_allow_tool_ansi")]
185 pub allow_tool_ansi: bool,
186
187 #[serde(default = "default_inline_viewport_rows")]
189 pub inline_viewport_rows: u16,
190
191 #[serde(default = "default_reasoning_display_mode")]
193 pub reasoning_display_mode: ReasoningDisplayMode,
194
195 #[serde(default = "default_reasoning_visible_default")]
197 pub reasoning_visible_default: bool,
198
199 #[serde(default = "default_vim_mode")]
201 pub vim_mode: bool,
202
203 #[serde(default)]
205 pub status_line: StatusLineConfig,
206
207 #[serde(default)]
209 pub keyboard_protocol: KeyboardProtocolConfig,
210
211 #[serde(default)]
213 pub layout_mode: LayoutModeOverride,
214
215 #[serde(default)]
217 pub display_mode: UiDisplayMode,
218
219 #[serde(default = "default_show_sidebar")]
221 pub show_sidebar: bool,
222
223 #[serde(default = "default_dim_completed_todos")]
225 pub dim_completed_todos: bool,
226
227 #[serde(default = "default_message_block_spacing")]
229 pub message_block_spacing: bool,
230
231 #[serde(default = "default_show_turn_timer")]
233 pub show_turn_timer: bool,
234
235 #[serde(default = "default_show_diagnostics_in_transcript")]
239 pub show_diagnostics_in_transcript: bool,
240
241 #[serde(default = "default_minimum_contrast")]
250 pub minimum_contrast: f64,
251
252 #[serde(default = "default_bold_is_bright")]
256 pub bold_is_bright: bool,
257
258 #[serde(default = "default_safe_colors_only")]
264 pub safe_colors_only: bool,
265
266 #[serde(default = "default_color_scheme_mode")]
271 pub color_scheme_mode: ColorSchemeMode,
272
273 #[serde(default)]
275 pub notifications: UiNotificationsConfig,
276
277 #[serde(default = "default_screen_reader_mode")]
281 pub screen_reader_mode: bool,
282
283 #[serde(default = "default_reduce_motion_mode")]
286 pub reduce_motion_mode: bool,
287
288 #[serde(default = "default_reduce_motion_keep_progress_animation")]
290 pub reduce_motion_keep_progress_animation: bool,
291}
292
293#[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 #[default]
300 Auto,
301 Light,
303 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 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#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
466#[derive(Debug, Clone, Deserialize, Serialize, Default)]
467pub struct ChatConfig {
468 #[serde(default, rename = "askQuestions", alias = "ask_questions")]
470 pub ask_questions: AskQuestionsConfig,
471}
472
473#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
475#[derive(Debug, Clone, Deserialize, Serialize)]
476pub struct AskQuestionsConfig {
477 #[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#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
492#[derive(Debug, Clone, Deserialize, Serialize)]
493pub struct PtyConfig {
494 #[serde(default = "default_pty_enabled")]
496 pub enabled: bool,
497
498 #[serde(default = "default_pty_rows")]
500 pub default_rows: u16,
501
502 #[serde(default = "default_pty_cols")]
504 pub default_cols: u16,
505
506 #[serde(default = "default_max_pty_sessions")]
508 pub max_sessions: usize,
509
510 #[serde(default = "default_pty_timeout")]
512 pub command_timeout_seconds: u64,
513
514 #[serde(default = "default_stdout_tail_lines")]
516 pub stdout_tail_lines: usize,
517
518 #[serde(default = "default_scrollback_lines")]
520 pub scrollback_lines: usize,
521
522 #[serde(default = "default_max_scrollback_bytes")]
524 pub max_scrollback_bytes: usize,
525
526 #[serde(default)]
528 pub emulation_backend: PtyEmulationBackend,
529
530 #[serde(default = "default_large_output_threshold_kb")]
532 pub large_output_threshold_kb: usize,
533
534 #[serde(default)]
536 pub preferred_shell: Option<String>,
537
538 #[serde(default = "default_shell_zsh_fork")]
540 pub shell_zsh_fork: bool,
541
542 #[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 25_000_000 }
679
680fn default_large_output_threshold_kb() -> usize {
681 5_000 }
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#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
715#[derive(Debug, Clone, Deserialize, Serialize)]
716pub struct KeyboardProtocolConfig {
717 #[serde(default = "default_keyboard_protocol_enabled")]
719 pub enabled: bool,
720
721 #[serde(default = "default_keyboard_protocol_mode")]
723 pub mode: String,
724
725 #[serde(default = "default_disambiguate_escape_codes")]
727 pub disambiguate_escape_codes: bool,
728
729 #[serde(default = "default_report_event_types")]
731 pub report_event_types: bool,
732
733 #[serde(default = "default_report_alternate_keys")]
735 pub report_alternate_keys: bool,
736
737 #[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}