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#[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 #[default]
33 Auto,
34 Compact,
36 Standard,
38 Wide,
40}
41
42#[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,
49 #[default]
51 Minimal,
52 Focused,
54}
55
56#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
58#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
59#[serde(rename_all = "snake_case")]
60pub enum NotificationDeliveryMode {
61 Terminal,
63 #[default]
65 Hybrid,
66 Desktop,
68}
69
70#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
72#[derive(Debug, Clone, Deserialize, Serialize)]
73pub struct UiNotificationsConfig {
74 #[serde(default = "default_notifications_enabled")]
76 pub enabled: bool,
77
78 #[serde(default)]
80 pub delivery_mode: NotificationDeliveryMode,
81
82 #[serde(default = "default_notifications_suppress_when_focused")]
84 pub suppress_when_focused: bool,
85
86 #[serde(default = "default_notifications_tool_failure")]
88 pub tool_failure: bool,
89
90 #[serde(default = "default_notifications_error")]
92 pub error: bool,
93
94 #[serde(default = "default_notifications_completion")]
96 pub completion: bool,
97
98 #[serde(default = "default_notifications_hitl")]
100 pub hitl: bool,
101
102 #[serde(default = "default_notifications_tool_success")]
104 pub tool_success: bool,
105}
106
107impl Default for UiNotificationsConfig {
108 fn default() -> Self {
109 Self {
110 enabled: default_notifications_enabled(),
111 delivery_mode: NotificationDeliveryMode::default(),
112 suppress_when_focused: default_notifications_suppress_when_focused(),
113 tool_failure: default_notifications_tool_failure(),
114 error: default_notifications_error(),
115 completion: default_notifications_completion(),
116 hitl: default_notifications_hitl(),
117 tool_success: default_notifications_tool_success(),
118 }
119 }
120}
121
122#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
123#[derive(Debug, Clone, Deserialize, Serialize)]
124pub struct UiConfig {
125 #[serde(default = "default_tool_output_mode")]
127 pub tool_output_mode: ToolOutputMode,
128
129 #[serde(default = "default_tool_output_max_lines")]
131 pub tool_output_max_lines: usize,
132
133 #[serde(default = "default_tool_output_spool_bytes")]
135 pub tool_output_spool_bytes: usize,
136
137 #[serde(default)]
139 pub tool_output_spool_dir: Option<String>,
140
141 #[serde(default = "default_allow_tool_ansi")]
143 pub allow_tool_ansi: bool,
144
145 #[serde(default = "default_inline_viewport_rows")]
147 pub inline_viewport_rows: u16,
148
149 #[serde(default = "default_reasoning_display_mode")]
151 pub reasoning_display_mode: ReasoningDisplayMode,
152
153 #[serde(default = "default_reasoning_visible_default")]
155 pub reasoning_visible_default: bool,
156
157 #[serde(default)]
159 pub status_line: StatusLineConfig,
160
161 #[serde(default)]
163 pub keyboard_protocol: KeyboardProtocolConfig,
164
165 #[serde(default)]
167 pub layout_mode: LayoutModeOverride,
168
169 #[serde(default)]
171 pub display_mode: UiDisplayMode,
172
173 #[serde(default = "default_show_sidebar")]
175 pub show_sidebar: bool,
176
177 #[serde(default = "default_dim_completed_todos")]
179 pub dim_completed_todos: bool,
180
181 #[serde(default = "default_message_block_spacing")]
183 pub message_block_spacing: bool,
184
185 #[serde(default = "default_show_turn_timer")]
187 pub show_turn_timer: bool,
188
189 #[serde(default = "default_show_diagnostics_in_transcript")]
193 pub show_diagnostics_in_transcript: bool,
194
195 #[serde(default = "default_minimum_contrast")]
204 pub minimum_contrast: f64,
205
206 #[serde(default = "default_bold_is_bright")]
210 pub bold_is_bright: bool,
211
212 #[serde(default = "default_safe_colors_only")]
218 pub safe_colors_only: bool,
219
220 #[serde(default = "default_color_scheme_mode")]
225 pub color_scheme_mode: ColorSchemeMode,
226
227 #[serde(default)]
229 pub notifications: UiNotificationsConfig,
230
231 #[serde(default = "default_screen_reader_mode")]
235 pub screen_reader_mode: bool,
236
237 #[serde(default = "default_reduce_motion_mode")]
240 pub reduce_motion_mode: bool,
241
242 #[serde(default = "default_reduce_motion_keep_progress_animation")]
244 pub reduce_motion_keep_progress_animation: bool,
245}
246
247#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
249#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
250#[serde(rename_all = "snake_case")]
251pub enum ColorSchemeMode {
252 #[default]
254 Auto,
255 Light,
257 Dark,
259}
260
261fn default_minimum_contrast() -> f64 {
262 crate::constants::ui::THEME_MIN_CONTRAST_RATIO
263}
264
265fn default_bold_is_bright() -> bool {
266 false
267}
268
269fn default_safe_colors_only() -> bool {
270 false
271}
272
273fn default_color_scheme_mode() -> ColorSchemeMode {
274 ColorSchemeMode::Auto
275}
276
277fn default_show_sidebar() -> bool {
278 true
279}
280
281fn default_dim_completed_todos() -> bool {
282 true
283}
284
285fn default_message_block_spacing() -> bool {
286 true
287}
288
289fn default_show_turn_timer() -> bool {
290 false
291}
292
293fn default_show_diagnostics_in_transcript() -> bool {
294 false
295}
296
297fn default_notifications_enabled() -> bool {
298 true
299}
300
301fn default_notifications_suppress_when_focused() -> bool {
302 true
303}
304
305fn default_notifications_tool_failure() -> bool {
306 true
307}
308
309fn default_notifications_error() -> bool {
310 true
311}
312
313fn default_notifications_completion() -> bool {
314 true
315}
316
317fn default_notifications_hitl() -> bool {
318 true
319}
320
321fn default_notifications_tool_success() -> bool {
322 false
323}
324
325fn env_bool_var(name: &str) -> Option<bool> {
326 std::env::var(name).ok().and_then(|v| {
327 let normalized = v.trim().to_ascii_lowercase();
328 match normalized.as_str() {
329 "1" | "true" | "yes" | "on" => Some(true),
330 "0" | "false" | "no" | "off" => Some(false),
331 _ => None,
332 }
333 })
334}
335
336fn default_screen_reader_mode() -> bool {
337 env_bool_var("VTCODE_SCREEN_READER").unwrap_or(false)
338}
339
340fn default_reduce_motion_mode() -> bool {
341 env_bool_var("VTCODE_REDUCE_MOTION").unwrap_or(false)
342}
343
344fn default_reduce_motion_keep_progress_animation() -> bool {
345 false
346}
347
348fn default_ask_questions_enabled() -> bool {
349 true
350}
351
352impl Default for UiConfig {
353 fn default() -> Self {
354 Self {
355 tool_output_mode: default_tool_output_mode(),
356 tool_output_max_lines: default_tool_output_max_lines(),
357 tool_output_spool_bytes: default_tool_output_spool_bytes(),
358 tool_output_spool_dir: None,
359 allow_tool_ansi: default_allow_tool_ansi(),
360 inline_viewport_rows: default_inline_viewport_rows(),
361 reasoning_display_mode: default_reasoning_display_mode(),
362 reasoning_visible_default: default_reasoning_visible_default(),
363 status_line: StatusLineConfig::default(),
364 keyboard_protocol: KeyboardProtocolConfig::default(),
365 layout_mode: LayoutModeOverride::default(),
366 display_mode: UiDisplayMode::default(),
367 show_sidebar: default_show_sidebar(),
368 dim_completed_todos: default_dim_completed_todos(),
369 message_block_spacing: default_message_block_spacing(),
370 show_turn_timer: default_show_turn_timer(),
371 show_diagnostics_in_transcript: default_show_diagnostics_in_transcript(),
372 minimum_contrast: default_minimum_contrast(),
374 bold_is_bright: default_bold_is_bright(),
375 safe_colors_only: default_safe_colors_only(),
376 color_scheme_mode: default_color_scheme_mode(),
377 notifications: UiNotificationsConfig::default(),
378 screen_reader_mode: default_screen_reader_mode(),
379 reduce_motion_mode: default_reduce_motion_mode(),
380 reduce_motion_keep_progress_animation: default_reduce_motion_keep_progress_animation(),
381 }
382 }
383}
384
385#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
387#[derive(Debug, Clone, Deserialize, Serialize, Default)]
388pub struct ChatConfig {
389 #[serde(default, rename = "askQuestions", alias = "ask_questions")]
391 pub ask_questions: AskQuestionsConfig,
392}
393
394#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
396#[derive(Debug, Clone, Deserialize, Serialize)]
397pub struct AskQuestionsConfig {
398 #[serde(default = "default_ask_questions_enabled")]
400 pub enabled: bool,
401}
402
403impl Default for AskQuestionsConfig {
404 fn default() -> Self {
405 Self {
406 enabled: default_ask_questions_enabled(),
407 }
408 }
409}
410
411#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
413#[derive(Debug, Clone, Deserialize, Serialize)]
414pub struct PtyConfig {
415 #[serde(default = "default_pty_enabled")]
417 pub enabled: bool,
418
419 #[serde(default = "default_pty_rows")]
421 pub default_rows: u16,
422
423 #[serde(default = "default_pty_cols")]
425 pub default_cols: u16,
426
427 #[serde(default = "default_max_pty_sessions")]
429 pub max_sessions: usize,
430
431 #[serde(default = "default_pty_timeout")]
433 pub command_timeout_seconds: u64,
434
435 #[serde(default = "default_stdout_tail_lines")]
437 pub stdout_tail_lines: usize,
438
439 #[serde(default = "default_scrollback_lines")]
441 pub scrollback_lines: usize,
442
443 #[serde(default = "default_max_scrollback_bytes")]
445 pub max_scrollback_bytes: usize,
446
447 #[serde(default = "default_large_output_threshold_kb")]
449 pub large_output_threshold_kb: usize,
450
451 #[serde(default)]
453 pub preferred_shell: Option<String>,
454}
455
456impl Default for PtyConfig {
457 fn default() -> Self {
458 Self {
459 enabled: default_pty_enabled(),
460 default_rows: default_pty_rows(),
461 default_cols: default_pty_cols(),
462 max_sessions: default_max_pty_sessions(),
463 command_timeout_seconds: default_pty_timeout(),
464 stdout_tail_lines: default_stdout_tail_lines(),
465 scrollback_lines: default_scrollback_lines(),
466 max_scrollback_bytes: default_max_scrollback_bytes(),
467 large_output_threshold_kb: default_large_output_threshold_kb(),
468 preferred_shell: None,
469 }
470 }
471}
472
473fn default_pty_enabled() -> bool {
474 true
475}
476
477fn default_pty_rows() -> u16 {
478 24
479}
480
481fn default_pty_cols() -> u16 {
482 80
483}
484
485fn default_max_pty_sessions() -> usize {
486 10
487}
488
489fn default_pty_timeout() -> u64 {
490 300
491}
492
493fn default_stdout_tail_lines() -> usize {
494 crate::constants::defaults::DEFAULT_PTY_STDOUT_TAIL_LINES
495}
496
497fn default_scrollback_lines() -> usize {
498 crate::constants::defaults::DEFAULT_PTY_SCROLLBACK_LINES
499}
500
501fn default_max_scrollback_bytes() -> usize {
502 25_000_000 }
506
507fn default_large_output_threshold_kb() -> usize {
508 5_000 }
510
511fn default_tool_output_mode() -> ToolOutputMode {
512 ToolOutputMode::Compact
513}
514
515fn default_tool_output_max_lines() -> usize {
516 600
517}
518
519fn default_tool_output_spool_bytes() -> usize {
520 200_000
521}
522
523fn default_allow_tool_ansi() -> bool {
524 false
525}
526
527fn default_inline_viewport_rows() -> u16 {
528 crate::constants::ui::DEFAULT_INLINE_VIEWPORT_ROWS
529}
530
531fn default_reasoning_display_mode() -> ReasoningDisplayMode {
532 ReasoningDisplayMode::Toggle
533}
534
535fn default_reasoning_visible_default() -> bool {
536 crate::constants::ui::DEFAULT_REASONING_VISIBLE
537}
538
539#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
542#[derive(Debug, Clone, Deserialize, Serialize)]
543pub struct KeyboardProtocolConfig {
544 #[serde(default = "default_keyboard_protocol_enabled")]
546 pub enabled: bool,
547
548 #[serde(default = "default_keyboard_protocol_mode")]
550 pub mode: String,
551
552 #[serde(default = "default_disambiguate_escape_codes")]
554 pub disambiguate_escape_codes: bool,
555
556 #[serde(default = "default_report_event_types")]
558 pub report_event_types: bool,
559
560 #[serde(default = "default_report_alternate_keys")]
562 pub report_alternate_keys: bool,
563
564 #[serde(default = "default_report_all_keys")]
566 pub report_all_keys: bool,
567}
568
569impl Default for KeyboardProtocolConfig {
570 fn default() -> Self {
571 Self {
572 enabled: default_keyboard_protocol_enabled(),
573 mode: default_keyboard_protocol_mode(),
574 disambiguate_escape_codes: default_disambiguate_escape_codes(),
575 report_event_types: default_report_event_types(),
576 report_alternate_keys: default_report_alternate_keys(),
577 report_all_keys: default_report_all_keys(),
578 }
579 }
580}
581
582impl KeyboardProtocolConfig {
583 pub fn validate(&self) -> anyhow::Result<()> {
584 match self.mode.as_str() {
585 "default" | "full" | "minimal" | "custom" => Ok(()),
586 _ => anyhow::bail!(
587 "Invalid keyboard protocol mode '{}'. Must be: default, full, minimal, or custom",
588 self.mode
589 ),
590 }
591 }
592}
593
594fn default_keyboard_protocol_enabled() -> bool {
595 std::env::var("VTCODE_KEYBOARD_PROTOCOL_ENABLED")
596 .ok()
597 .and_then(|v| v.parse().ok())
598 .unwrap_or(true)
599}
600
601fn default_keyboard_protocol_mode() -> String {
602 std::env::var("VTCODE_KEYBOARD_PROTOCOL_MODE").unwrap_or_else(|_| "default".to_string())
603}
604
605fn default_disambiguate_escape_codes() -> bool {
606 true
607}
608
609fn default_report_event_types() -> bool {
610 true
611}
612
613fn default_report_alternate_keys() -> bool {
614 true
615}
616
617fn default_report_all_keys() -> bool {
618 false
619}