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_minimum_contrast")]
198 pub minimum_contrast: f64,
199
200 #[serde(default = "default_bold_is_bright")]
204 pub bold_is_bright: bool,
205
206 #[serde(default = "default_safe_colors_only")]
212 pub safe_colors_only: bool,
213
214 #[serde(default = "default_color_scheme_mode")]
219 pub color_scheme_mode: ColorSchemeMode,
220
221 #[serde(default)]
223 pub notifications: UiNotificationsConfig,
224
225 #[serde(default = "default_screen_reader_mode")]
229 pub screen_reader_mode: bool,
230
231 #[serde(default = "default_reduce_motion_mode")]
234 pub reduce_motion_mode: bool,
235
236 #[serde(default = "default_reduce_motion_keep_progress_animation")]
238 pub reduce_motion_keep_progress_animation: bool,
239}
240
241#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
243#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
244#[serde(rename_all = "snake_case")]
245pub enum ColorSchemeMode {
246 #[default]
248 Auto,
249 Light,
251 Dark,
253}
254
255fn default_minimum_contrast() -> f64 {
256 crate::constants::ui::THEME_MIN_CONTRAST_RATIO
257}
258
259fn default_bold_is_bright() -> bool {
260 false
261}
262
263fn default_safe_colors_only() -> bool {
264 false
265}
266
267fn default_color_scheme_mode() -> ColorSchemeMode {
268 ColorSchemeMode::Auto
269}
270
271fn default_show_sidebar() -> bool {
272 true
273}
274
275fn default_dim_completed_todos() -> bool {
276 true
277}
278
279fn default_message_block_spacing() -> bool {
280 true
281}
282
283fn default_show_turn_timer() -> bool {
284 true
285}
286
287fn default_notifications_enabled() -> bool {
288 true
289}
290
291fn default_notifications_suppress_when_focused() -> bool {
292 true
293}
294
295fn default_notifications_tool_failure() -> bool {
296 true
297}
298
299fn default_notifications_error() -> bool {
300 true
301}
302
303fn default_notifications_completion() -> bool {
304 true
305}
306
307fn default_notifications_hitl() -> bool {
308 true
309}
310
311fn default_notifications_tool_success() -> bool {
312 false
313}
314
315fn env_bool_var(name: &str) -> Option<bool> {
316 std::env::var(name).ok().and_then(|v| {
317 let normalized = v.trim().to_ascii_lowercase();
318 match normalized.as_str() {
319 "1" | "true" | "yes" | "on" => Some(true),
320 "0" | "false" | "no" | "off" => Some(false),
321 _ => None,
322 }
323 })
324}
325
326fn default_screen_reader_mode() -> bool {
327 env_bool_var("VTCODE_SCREEN_READER").unwrap_or(false)
328}
329
330fn default_reduce_motion_mode() -> bool {
331 env_bool_var("VTCODE_REDUCE_MOTION").unwrap_or(false)
332}
333
334fn default_reduce_motion_keep_progress_animation() -> bool {
335 false
336}
337
338fn default_ask_questions_enabled() -> bool {
339 true
340}
341
342impl Default for UiConfig {
343 fn default() -> Self {
344 Self {
345 tool_output_mode: default_tool_output_mode(),
346 tool_output_max_lines: default_tool_output_max_lines(),
347 tool_output_spool_bytes: default_tool_output_spool_bytes(),
348 tool_output_spool_dir: None,
349 allow_tool_ansi: default_allow_tool_ansi(),
350 inline_viewport_rows: default_inline_viewport_rows(),
351 reasoning_display_mode: default_reasoning_display_mode(),
352 reasoning_visible_default: default_reasoning_visible_default(),
353 status_line: StatusLineConfig::default(),
354 keyboard_protocol: KeyboardProtocolConfig::default(),
355 layout_mode: LayoutModeOverride::default(),
356 display_mode: UiDisplayMode::default(),
357 show_sidebar: default_show_sidebar(),
358 dim_completed_todos: default_dim_completed_todos(),
359 message_block_spacing: default_message_block_spacing(),
360 show_turn_timer: default_show_turn_timer(),
361 minimum_contrast: default_minimum_contrast(),
363 bold_is_bright: default_bold_is_bright(),
364 safe_colors_only: default_safe_colors_only(),
365 color_scheme_mode: default_color_scheme_mode(),
366 notifications: UiNotificationsConfig::default(),
367 screen_reader_mode: default_screen_reader_mode(),
368 reduce_motion_mode: default_reduce_motion_mode(),
369 reduce_motion_keep_progress_animation: default_reduce_motion_keep_progress_animation(),
370 }
371 }
372}
373
374#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
376#[derive(Debug, Clone, Deserialize, Serialize, Default)]
377pub struct ChatConfig {
378 #[serde(default, rename = "askQuestions", alias = "ask_questions")]
380 pub ask_questions: AskQuestionsConfig,
381}
382
383#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
385#[derive(Debug, Clone, Deserialize, Serialize)]
386pub struct AskQuestionsConfig {
387 #[serde(default = "default_ask_questions_enabled")]
389 pub enabled: bool,
390}
391
392impl Default for AskQuestionsConfig {
393 fn default() -> Self {
394 Self {
395 enabled: default_ask_questions_enabled(),
396 }
397 }
398}
399
400#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
402#[derive(Debug, Clone, Deserialize, Serialize)]
403pub struct PtyConfig {
404 #[serde(default = "default_pty_enabled")]
406 pub enabled: bool,
407
408 #[serde(default = "default_pty_rows")]
410 pub default_rows: u16,
411
412 #[serde(default = "default_pty_cols")]
414 pub default_cols: u16,
415
416 #[serde(default = "default_max_pty_sessions")]
418 pub max_sessions: usize,
419
420 #[serde(default = "default_pty_timeout")]
422 pub command_timeout_seconds: u64,
423
424 #[serde(default = "default_stdout_tail_lines")]
426 pub stdout_tail_lines: usize,
427
428 #[serde(default = "default_scrollback_lines")]
430 pub scrollback_lines: usize,
431
432 #[serde(default = "default_max_scrollback_bytes")]
434 pub max_scrollback_bytes: usize,
435
436 #[serde(default = "default_large_output_threshold_kb")]
438 pub large_output_threshold_kb: usize,
439
440 #[serde(default)]
442 pub preferred_shell: Option<String>,
443}
444
445impl Default for PtyConfig {
446 fn default() -> Self {
447 Self {
448 enabled: default_pty_enabled(),
449 default_rows: default_pty_rows(),
450 default_cols: default_pty_cols(),
451 max_sessions: default_max_pty_sessions(),
452 command_timeout_seconds: default_pty_timeout(),
453 stdout_tail_lines: default_stdout_tail_lines(),
454 scrollback_lines: default_scrollback_lines(),
455 max_scrollback_bytes: default_max_scrollback_bytes(),
456 large_output_threshold_kb: default_large_output_threshold_kb(),
457 preferred_shell: None,
458 }
459 }
460}
461
462fn default_pty_enabled() -> bool {
463 true
464}
465
466fn default_pty_rows() -> u16 {
467 24
468}
469
470fn default_pty_cols() -> u16 {
471 80
472}
473
474fn default_max_pty_sessions() -> usize {
475 10
476}
477
478fn default_pty_timeout() -> u64 {
479 300
480}
481
482fn default_stdout_tail_lines() -> usize {
483 crate::constants::defaults::DEFAULT_PTY_STDOUT_TAIL_LINES
484}
485
486fn default_scrollback_lines() -> usize {
487 crate::constants::defaults::DEFAULT_PTY_SCROLLBACK_LINES
488}
489
490fn default_max_scrollback_bytes() -> usize {
491 25_000_000 }
495
496fn default_large_output_threshold_kb() -> usize {
497 5_000 }
499
500fn default_tool_output_mode() -> ToolOutputMode {
501 ToolOutputMode::Compact
502}
503
504fn default_tool_output_max_lines() -> usize {
505 600
506}
507
508fn default_tool_output_spool_bytes() -> usize {
509 200_000
510}
511
512fn default_allow_tool_ansi() -> bool {
513 false
514}
515
516fn default_inline_viewport_rows() -> u16 {
517 crate::constants::ui::DEFAULT_INLINE_VIEWPORT_ROWS
518}
519
520fn default_reasoning_display_mode() -> ReasoningDisplayMode {
521 ReasoningDisplayMode::Toggle
522}
523
524fn default_reasoning_visible_default() -> bool {
525 crate::constants::ui::DEFAULT_REASONING_VISIBLE
526}
527
528#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
531#[derive(Debug, Clone, Deserialize, Serialize)]
532pub struct KeyboardProtocolConfig {
533 #[serde(default = "default_keyboard_protocol_enabled")]
535 pub enabled: bool,
536
537 #[serde(default = "default_keyboard_protocol_mode")]
539 pub mode: String,
540
541 #[serde(default = "default_disambiguate_escape_codes")]
543 pub disambiguate_escape_codes: bool,
544
545 #[serde(default = "default_report_event_types")]
547 pub report_event_types: bool,
548
549 #[serde(default = "default_report_alternate_keys")]
551 pub report_alternate_keys: bool,
552
553 #[serde(default = "default_report_all_keys")]
555 pub report_all_keys: bool,
556}
557
558impl Default for KeyboardProtocolConfig {
559 fn default() -> Self {
560 Self {
561 enabled: default_keyboard_protocol_enabled(),
562 mode: default_keyboard_protocol_mode(),
563 disambiguate_escape_codes: default_disambiguate_escape_codes(),
564 report_event_types: default_report_event_types(),
565 report_alternate_keys: default_report_alternate_keys(),
566 report_all_keys: default_report_all_keys(),
567 }
568 }
569}
570
571impl KeyboardProtocolConfig {
572 pub fn validate(&self) -> anyhow::Result<()> {
573 match self.mode.as_str() {
574 "default" | "full" | "minimal" | "custom" => Ok(()),
575 _ => anyhow::bail!(
576 "Invalid keyboard protocol mode '{}'. Must be: default, full, minimal, or custom",
577 self.mode
578 ),
579 }
580 }
581}
582
583fn default_keyboard_protocol_enabled() -> bool {
584 std::env::var("VTCODE_KEYBOARD_PROTOCOL_ENABLED")
585 .ok()
586 .and_then(|v| v.parse().ok())
587 .unwrap_or(true)
588}
589
590fn default_keyboard_protocol_mode() -> String {
591 std::env::var("VTCODE_KEYBOARD_PROTOCOL_MODE").unwrap_or_else(|_| "default".to_string())
592}
593
594fn default_disambiguate_escape_codes() -> bool {
595 true
596}
597
598fn default_report_event_types() -> bool {
599 true
600}
601
602fn default_report_alternate_keys() -> bool {
603 true
604}
605
606fn default_report_all_keys() -> bool {
607 false
608}