1use anyhow::{Result, anyhow, bail};
2use serde::{Deserialize, Serialize};
3
4use crate::status_line::StatusLineConfig;
5use crate::terminal_title::TerminalTitleConfig;
6
7#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
8#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
9#[serde(rename_all = "snake_case")]
10#[derive(Default)]
11pub enum ToolOutputMode {
12 #[default]
13 Compact,
14 Full,
15}
16
17#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
18#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
19#[serde(rename_all = "snake_case")]
20#[derive(Default)]
21pub enum ReasoningDisplayMode {
22 Always,
23 #[default]
24 Toggle,
25 Hidden,
26}
27
28#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
30#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
31#[serde(rename_all = "snake_case")]
32pub enum LayoutModeOverride {
33 #[default]
35 Auto,
36 Compact,
38 Standard,
40 Wide,
42}
43
44#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
46#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
47#[serde(rename_all = "snake_case")]
48pub enum UiDisplayMode {
49 Full,
51 #[default]
53 Minimal,
54 Focused,
56}
57
58#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
60#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
61#[serde(rename_all = "snake_case")]
62pub enum NotificationDeliveryMode {
63 Terminal,
65 #[default]
67 Hybrid,
68 Desktop,
70}
71
72#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
74#[derive(Debug, Clone, Deserialize, Serialize)]
75pub struct UiNotificationsConfig {
76 #[serde(default = "default_notifications_enabled")]
78 pub enabled: bool,
79
80 #[serde(default)]
82 pub delivery_mode: NotificationDeliveryMode,
83
84 #[serde(default = "default_notifications_suppress_when_focused")]
86 pub suppress_when_focused: bool,
87
88 #[serde(default)]
91 pub command_failure: Option<bool>,
92
93 #[serde(default = "default_notifications_tool_failure")]
95 pub tool_failure: bool,
96
97 #[serde(default = "default_notifications_error")]
99 pub error: bool,
100
101 #[serde(default = "default_notifications_completion")]
104 pub completion: bool,
105
106 #[serde(default)]
109 pub completion_success: Option<bool>,
110
111 #[serde(default)]
114 pub completion_failure: Option<bool>,
115
116 #[serde(default = "default_notifications_hitl")]
118 pub hitl: bool,
119
120 #[serde(default)]
123 pub policy_approval: Option<bool>,
124
125 #[serde(default)]
128 pub request: Option<bool>,
129
130 #[serde(default = "default_notifications_tool_success")]
132 pub tool_success: bool,
133
134 #[serde(default = "default_notifications_repeat_window_seconds")]
136 pub repeat_window_seconds: u64,
137
138 #[serde(default = "default_notifications_max_identical_in_window")]
140 pub max_identical_in_window: u32,
141}
142
143impl Default for UiNotificationsConfig {
144 fn default() -> Self {
145 Self {
146 enabled: default_notifications_enabled(),
147 delivery_mode: NotificationDeliveryMode::default(),
148 suppress_when_focused: default_notifications_suppress_when_focused(),
149 command_failure: Some(default_notifications_command_failure()),
150 tool_failure: default_notifications_tool_failure(),
151 error: default_notifications_error(),
152 completion: default_notifications_completion(),
153 completion_success: Some(default_notifications_completion_success()),
154 completion_failure: Some(default_notifications_completion_failure()),
155 hitl: default_notifications_hitl(),
156 policy_approval: Some(default_notifications_policy_approval()),
157 request: Some(default_notifications_request()),
158 tool_success: default_notifications_tool_success(),
159 repeat_window_seconds: default_notifications_repeat_window_seconds(),
160 max_identical_in_window: default_notifications_max_identical_in_window(),
161 }
162 }
163}
164
165#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
166#[derive(Debug, Clone, Deserialize, Serialize)]
167pub struct UiFullscreenConfig {
168 #[serde(default = "default_fullscreen_mouse_capture")]
171 pub mouse_capture: bool,
172
173 #[serde(default = "default_fullscreen_copy_on_select")]
176 pub copy_on_select: bool,
177
178 #[serde(default = "default_fullscreen_scroll_speed")]
182 pub scroll_speed: u8,
183}
184
185impl Default for UiFullscreenConfig {
186 fn default() -> Self {
187 Self {
188 mouse_capture: default_fullscreen_mouse_capture(),
189 copy_on_select: default_fullscreen_copy_on_select(),
190 scroll_speed: default_fullscreen_scroll_speed(),
191 }
192 }
193}
194
195#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
196#[derive(Debug, Clone, Deserialize, Serialize)]
197pub struct UiConfig {
198 #[serde(default = "default_tool_output_mode")]
200 pub tool_output_mode: ToolOutputMode,
201
202 #[serde(default = "default_tool_output_max_lines")]
204 pub tool_output_max_lines: usize,
205
206 #[serde(default = "default_tool_output_spool_bytes")]
208 pub tool_output_spool_bytes: usize,
209
210 #[serde(default)]
212 pub tool_output_spool_dir: Option<String>,
213
214 #[serde(default = "default_allow_tool_ansi")]
216 pub allow_tool_ansi: bool,
217
218 #[serde(default = "default_inline_viewport_rows")]
220 pub inline_viewport_rows: u16,
221
222 #[serde(default = "default_reasoning_display_mode")]
224 pub reasoning_display_mode: ReasoningDisplayMode,
225
226 #[serde(default = "default_reasoning_visible_default")]
228 pub reasoning_visible_default: bool,
229
230 #[serde(default = "default_vim_mode")]
232 pub vim_mode: bool,
233
234 #[serde(default)]
236 pub status_line: StatusLineConfig,
237
238 #[serde(default)]
240 pub terminal_title: TerminalTitleConfig,
241
242 #[serde(default)]
244 pub keyboard_protocol: KeyboardProtocolConfig,
245
246 #[serde(default)]
248 pub layout_mode: LayoutModeOverride,
249
250 #[serde(default)]
252 pub display_mode: UiDisplayMode,
253
254 #[serde(default = "default_show_sidebar")]
256 pub show_sidebar: bool,
257
258 #[serde(default = "default_dim_completed_todos")]
260 pub dim_completed_todos: bool,
261
262 #[serde(default = "default_message_block_spacing")]
264 pub message_block_spacing: bool,
265
266 #[serde(default = "default_show_turn_timer")]
268 pub show_turn_timer: bool,
269
270 #[serde(default = "default_show_diagnostics_in_transcript")]
274 pub show_diagnostics_in_transcript: bool,
275
276 #[serde(default = "default_minimum_contrast")]
285 pub minimum_contrast: f64,
286
287 #[serde(default = "default_bold_is_bright")]
291 pub bold_is_bright: bool,
292
293 #[serde(default = "default_safe_colors_only")]
299 pub safe_colors_only: bool,
300
301 #[serde(default = "default_color_scheme_mode")]
306 pub color_scheme_mode: ColorSchemeMode,
307
308 #[serde(default)]
310 pub notifications: UiNotificationsConfig,
311
312 #[serde(default)]
314 pub fullscreen: UiFullscreenConfig,
315
316 #[serde(default = "default_screen_reader_mode")]
320 pub screen_reader_mode: bool,
321
322 #[serde(default = "default_reduce_motion_mode")]
325 pub reduce_motion_mode: bool,
326
327 #[serde(default = "default_reduce_motion_keep_progress_animation")]
329 pub reduce_motion_keep_progress_animation: bool,
330}
331
332#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
334#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
335#[serde(rename_all = "snake_case")]
336pub enum ColorSchemeMode {
337 #[default]
339 Auto,
340 Light,
342 Dark,
344}
345
346fn default_minimum_contrast() -> f64 {
347 crate::constants::ui::THEME_MIN_CONTRAST_RATIO
348}
349
350fn default_bold_is_bright() -> bool {
351 false
352}
353
354fn default_safe_colors_only() -> bool {
355 false
356}
357
358fn default_color_scheme_mode() -> ColorSchemeMode {
359 ColorSchemeMode::Auto
360}
361
362fn default_show_sidebar() -> bool {
363 true
364}
365
366fn default_dim_completed_todos() -> bool {
367 true
368}
369
370fn default_message_block_spacing() -> bool {
371 true
372}
373
374fn default_show_turn_timer() -> bool {
375 false
376}
377
378fn default_show_diagnostics_in_transcript() -> bool {
379 false
380}
381
382fn default_vim_mode() -> bool {
383 false
384}
385
386fn default_notifications_enabled() -> bool {
387 true
388}
389
390fn default_notifications_suppress_when_focused() -> bool {
391 true
392}
393
394fn default_notifications_command_failure() -> bool {
395 false
396}
397
398fn default_notifications_tool_failure() -> bool {
399 false
400}
401
402fn default_notifications_error() -> bool {
403 true
404}
405
406fn default_notifications_completion() -> bool {
407 true
408}
409
410fn default_notifications_completion_success() -> bool {
411 false
412}
413
414fn default_notifications_completion_failure() -> bool {
415 true
416}
417
418fn default_notifications_hitl() -> bool {
419 true
420}
421
422fn default_notifications_policy_approval() -> bool {
423 true
424}
425
426fn default_notifications_request() -> bool {
427 false
428}
429
430fn default_notifications_tool_success() -> bool {
431 false
432}
433
434fn default_notifications_repeat_window_seconds() -> u64 {
435 30
436}
437
438fn default_notifications_max_identical_in_window() -> u32 {
439 1
440}
441
442fn env_bool_var(name: &str) -> Option<bool> {
443 read_env_var(name).and_then(|v| {
444 let normalized = v.trim().to_ascii_lowercase();
445 match normalized.as_str() {
446 "1" | "true" | "yes" | "on" => Some(true),
447 "0" | "false" | "no" | "off" => Some(false),
448 _ => None,
449 }
450 })
451}
452
453fn env_u8_var(name: &str) -> Option<u8> {
454 read_env_var(name)
455 .and_then(|value| value.trim().parse::<u8>().ok())
456 .map(clamp_fullscreen_scroll_speed)
457}
458
459fn clamp_fullscreen_scroll_speed(value: u8) -> u8 {
460 value.clamp(1, 20)
461}
462
463fn default_fullscreen_mouse_capture() -> bool {
464 env_bool_var("VTCODE_FULLSCREEN_MOUSE_CAPTURE").unwrap_or(true)
465}
466
467fn default_fullscreen_copy_on_select() -> bool {
468 env_bool_var("VTCODE_FULLSCREEN_COPY_ON_SELECT").unwrap_or(true)
469}
470
471fn default_fullscreen_scroll_speed() -> u8 {
472 env_u8_var("VTCODE_FULLSCREEN_SCROLL_SPEED").unwrap_or(3)
473}
474
475fn default_screen_reader_mode() -> bool {
476 env_bool_var("VTCODE_SCREEN_READER").unwrap_or(false)
477}
478
479fn default_reduce_motion_mode() -> bool {
480 env_bool_var("VTCODE_REDUCE_MOTION").unwrap_or(false)
481}
482
483fn default_reduce_motion_keep_progress_animation() -> bool {
484 false
485}
486
487fn default_ask_questions_enabled() -> bool {
488 true
489}
490
491impl Default for UiConfig {
492 fn default() -> Self {
493 Self {
494 tool_output_mode: default_tool_output_mode(),
495 tool_output_max_lines: default_tool_output_max_lines(),
496 tool_output_spool_bytes: default_tool_output_spool_bytes(),
497 tool_output_spool_dir: None,
498 allow_tool_ansi: default_allow_tool_ansi(),
499 inline_viewport_rows: default_inline_viewport_rows(),
500 reasoning_display_mode: default_reasoning_display_mode(),
501 reasoning_visible_default: default_reasoning_visible_default(),
502 vim_mode: default_vim_mode(),
503 status_line: StatusLineConfig::default(),
504 terminal_title: TerminalTitleConfig::default(),
505 keyboard_protocol: KeyboardProtocolConfig::default(),
506 layout_mode: LayoutModeOverride::default(),
507 display_mode: UiDisplayMode::default(),
508 show_sidebar: default_show_sidebar(),
509 dim_completed_todos: default_dim_completed_todos(),
510 message_block_spacing: default_message_block_spacing(),
511 show_turn_timer: default_show_turn_timer(),
512 show_diagnostics_in_transcript: default_show_diagnostics_in_transcript(),
513 minimum_contrast: default_minimum_contrast(),
515 bold_is_bright: default_bold_is_bright(),
516 safe_colors_only: default_safe_colors_only(),
517 color_scheme_mode: default_color_scheme_mode(),
518 notifications: UiNotificationsConfig::default(),
519 fullscreen: UiFullscreenConfig::default(),
520 screen_reader_mode: default_screen_reader_mode(),
521 reduce_motion_mode: default_reduce_motion_mode(),
522 reduce_motion_keep_progress_animation: default_reduce_motion_keep_progress_animation(),
523 }
524 }
525}
526
527fn read_env_var(name: &str) -> Option<String> {
528 #[cfg(test)]
529 if let Some(override_value) = test_env_overrides::get(name) {
530 return override_value;
531 }
532
533 std::env::var(name).ok()
534}
535
536#[cfg(test)]
537mod test_env_overrides {
538 use std::collections::HashMap;
539 use std::sync::{Mutex, OnceLock};
540
541 static ENV_OVERRIDES: OnceLock<Mutex<HashMap<String, Option<String>>>> = OnceLock::new();
542
543 fn overrides() -> &'static Mutex<HashMap<String, Option<String>>> {
544 ENV_OVERRIDES.get_or_init(|| Mutex::new(HashMap::new()))
545 }
546
547 pub(super) fn get(name: &str) -> Option<Option<String>> {
548 overrides()
549 .lock()
550 .expect("env overrides lock poisoned")
551 .get(name)
552 .cloned()
553 }
554
555 pub(super) fn set(name: &str, value: Option<&str>) {
556 overrides()
557 .lock()
558 .expect("env overrides lock poisoned")
559 .insert(name.to_string(), value.map(ToOwned::to_owned));
560 }
561
562 pub(super) fn restore(name: &str, previous: Option<Option<String>>) {
563 let mut guard = overrides().lock().expect("env overrides lock poisoned");
564 match previous {
565 Some(value) => {
566 guard.insert(name.to_string(), value);
567 }
568 None => {
569 guard.remove(name);
570 }
571 }
572 }
573}
574
575#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
577#[derive(Debug, Clone, Deserialize, Serialize, Default)]
578pub struct ChatConfig {
579 #[serde(default, rename = "askQuestions", alias = "ask_questions")]
581 pub ask_questions: AskQuestionsConfig,
582}
583
584#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
586#[derive(Debug, Clone, Deserialize, Serialize)]
587pub struct AskQuestionsConfig {
588 #[serde(default = "default_ask_questions_enabled")]
590 pub enabled: bool,
591}
592
593impl Default for AskQuestionsConfig {
594 fn default() -> Self {
595 Self {
596 enabled: default_ask_questions_enabled(),
597 }
598 }
599}
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604 use serial_test::serial;
605
606 fn with_env_var<F>(key: &str, value: Option<&str>, f: F)
607 where
608 F: FnOnce(),
609 {
610 let previous = test_env_overrides::get(key);
611 test_env_overrides::set(key, value);
612 f();
613 test_env_overrides::restore(key, previous);
614 }
615
616 #[test]
617 #[serial]
618 fn fullscreen_defaults_match_expected_values() {
619 let fullscreen = UiFullscreenConfig::default();
620
621 assert!(fullscreen.mouse_capture);
622 assert!(fullscreen.copy_on_select);
623 assert_eq!(fullscreen.scroll_speed, 3);
624 }
625
626 #[test]
627 #[serial]
628 fn fullscreen_env_overrides_apply_to_defaults() {
629 with_env_var("VTCODE_FULLSCREEN_MOUSE_CAPTURE", Some("0"), || {
630 with_env_var("VTCODE_FULLSCREEN_COPY_ON_SELECT", Some("false"), || {
631 with_env_var("VTCODE_FULLSCREEN_SCROLL_SPEED", Some("7"), || {
632 let fullscreen = UiFullscreenConfig::default();
633 assert!(!fullscreen.mouse_capture);
634 assert!(!fullscreen.copy_on_select);
635 assert_eq!(fullscreen.scroll_speed, 7);
636 });
637 });
638 });
639 }
640
641 #[test]
642 #[serial]
643 fn fullscreen_scroll_speed_is_clamped() {
644 with_env_var("VTCODE_FULLSCREEN_SCROLL_SPEED", Some("0"), || {
645 assert_eq!(UiFullscreenConfig::default().scroll_speed, 1);
646 });
647
648 with_env_var("VTCODE_FULLSCREEN_SCROLL_SPEED", Some("99"), || {
649 assert_eq!(UiFullscreenConfig::default().scroll_speed, 20);
650 });
651 }
652}
653
654#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
656#[derive(Debug, Clone, Deserialize, Serialize)]
657pub struct PtyConfig {
658 #[serde(default = "default_pty_enabled")]
660 pub enabled: bool,
661
662 #[serde(default = "default_pty_rows")]
664 pub default_rows: u16,
665
666 #[serde(default = "default_pty_cols")]
668 pub default_cols: u16,
669
670 #[serde(default = "default_max_pty_sessions")]
672 pub max_sessions: usize,
673
674 #[serde(default = "default_pty_timeout")]
676 pub command_timeout_seconds: u64,
677
678 #[serde(default = "default_stdout_tail_lines")]
680 pub stdout_tail_lines: usize,
681
682 #[serde(default = "default_scrollback_lines")]
684 pub scrollback_lines: usize,
685
686 #[serde(default = "default_max_scrollback_bytes")]
688 pub max_scrollback_bytes: usize,
689
690 #[serde(default)]
692 pub emulation_backend: PtyEmulationBackend,
693
694 #[serde(default = "default_large_output_threshold_kb")]
696 pub large_output_threshold_kb: usize,
697
698 #[serde(default)]
700 pub preferred_shell: Option<String>,
701
702 #[serde(default = "default_shell_zsh_fork")]
704 pub shell_zsh_fork: bool,
705
706 #[serde(default)]
708 pub zsh_path: Option<String>,
709}
710
711impl Default for PtyConfig {
712 fn default() -> Self {
713 Self {
714 enabled: default_pty_enabled(),
715 default_rows: default_pty_rows(),
716 default_cols: default_pty_cols(),
717 max_sessions: default_max_pty_sessions(),
718 command_timeout_seconds: default_pty_timeout(),
719 stdout_tail_lines: default_stdout_tail_lines(),
720 scrollback_lines: default_scrollback_lines(),
721 max_scrollback_bytes: default_max_scrollback_bytes(),
722 emulation_backend: PtyEmulationBackend::default(),
723 large_output_threshold_kb: default_large_output_threshold_kb(),
724 preferred_shell: None,
725 shell_zsh_fork: default_shell_zsh_fork(),
726 zsh_path: None,
727 }
728 }
729}
730
731#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
732#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
733#[serde(rename_all = "snake_case")]
734pub enum PtyEmulationBackend {
735 #[default]
736 Ghostty,
737 LegacyVt100,
738}
739
740impl PtyEmulationBackend {
741 #[must_use]
742 pub const fn as_str(self) -> &'static str {
743 match self {
744 Self::Ghostty => "ghostty",
745 Self::LegacyVt100 => "legacy_vt100",
746 }
747 }
748}
749
750impl PtyConfig {
751 pub fn validate(&self) -> Result<()> {
752 self.zsh_fork_shell_path()?;
753 Ok(())
754 }
755
756 pub fn zsh_fork_shell_path(&self) -> Result<Option<&str>> {
757 if !self.shell_zsh_fork {
758 return Ok(None);
759 }
760
761 let zsh_path = self
762 .zsh_path
763 .as_deref()
764 .map(str::trim)
765 .filter(|path| !path.is_empty())
766 .ok_or_else(|| {
767 anyhow!(
768 "pty.shell_zsh_fork is enabled, but pty.zsh_path is not configured. \
769 Set pty.zsh_path to an absolute path to patched zsh."
770 )
771 })?;
772
773 #[cfg(not(unix))]
774 {
775 let _ = zsh_path;
776 bail!("pty.shell_zsh_fork is only supported on Unix platforms");
777 }
778
779 #[cfg(unix)]
780 {
781 let path = std::path::Path::new(zsh_path);
782 if !path.is_absolute() {
783 bail!(
784 "pty.zsh_path '{}' must be an absolute path when pty.shell_zsh_fork is enabled",
785 zsh_path
786 );
787 }
788 if !path.exists() {
789 bail!(
790 "pty.zsh_path '{}' does not exist (required when pty.shell_zsh_fork is enabled)",
791 zsh_path
792 );
793 }
794 if !path.is_file() {
795 bail!(
796 "pty.zsh_path '{}' is not a file (required when pty.shell_zsh_fork is enabled)",
797 zsh_path
798 );
799 }
800 }
801
802 Ok(Some(zsh_path))
803 }
804}
805
806fn default_pty_enabled() -> bool {
807 true
808}
809
810fn default_pty_rows() -> u16 {
811 24
812}
813
814fn default_pty_cols() -> u16 {
815 80
816}
817
818fn default_max_pty_sessions() -> usize {
819 10
820}
821
822fn default_pty_timeout() -> u64 {
823 300
824}
825
826fn default_shell_zsh_fork() -> bool {
827 false
828}
829
830fn default_stdout_tail_lines() -> usize {
831 crate::constants::defaults::DEFAULT_PTY_STDOUT_TAIL_LINES
832}
833
834fn default_scrollback_lines() -> usize {
835 crate::constants::defaults::DEFAULT_PTY_SCROLLBACK_LINES
836}
837
838fn default_max_scrollback_bytes() -> usize {
839 25_000_000 }
843
844fn default_large_output_threshold_kb() -> usize {
845 5_000 }
847
848fn default_tool_output_mode() -> ToolOutputMode {
849 ToolOutputMode::Compact
850}
851
852fn default_tool_output_max_lines() -> usize {
853 600
854}
855
856fn default_tool_output_spool_bytes() -> usize {
857 200_000
858}
859
860fn default_allow_tool_ansi() -> bool {
861 false
862}
863
864fn default_inline_viewport_rows() -> u16 {
865 crate::constants::ui::DEFAULT_INLINE_VIEWPORT_ROWS
866}
867
868fn default_reasoning_display_mode() -> ReasoningDisplayMode {
869 ReasoningDisplayMode::Toggle
870}
871
872fn default_reasoning_visible_default() -> bool {
873 crate::constants::ui::DEFAULT_REASONING_VISIBLE
874}
875
876#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
879#[derive(Debug, Clone, Deserialize, Serialize)]
880pub struct KeyboardProtocolConfig {
881 #[serde(default = "default_keyboard_protocol_enabled")]
883 pub enabled: bool,
884
885 #[serde(default = "default_keyboard_protocol_mode")]
887 pub mode: String,
888
889 #[serde(default = "default_disambiguate_escape_codes")]
891 pub disambiguate_escape_codes: bool,
892
893 #[serde(default = "default_report_event_types")]
895 pub report_event_types: bool,
896
897 #[serde(default = "default_report_alternate_keys")]
899 pub report_alternate_keys: bool,
900
901 #[serde(default = "default_report_all_keys")]
903 pub report_all_keys: bool,
904}
905
906impl Default for KeyboardProtocolConfig {
907 fn default() -> Self {
908 Self {
909 enabled: default_keyboard_protocol_enabled(),
910 mode: default_keyboard_protocol_mode(),
911 disambiguate_escape_codes: default_disambiguate_escape_codes(),
912 report_event_types: default_report_event_types(),
913 report_alternate_keys: default_report_alternate_keys(),
914 report_all_keys: default_report_all_keys(),
915 }
916 }
917}
918
919impl KeyboardProtocolConfig {
920 pub fn validate(&self) -> Result<()> {
921 match self.mode.as_str() {
922 "default" | "full" | "minimal" | "custom" => Ok(()),
923 _ => anyhow::bail!(
924 "Invalid keyboard protocol mode '{}'. Must be: default, full, minimal, or custom",
925 self.mode
926 ),
927 }
928 }
929}
930
931fn default_keyboard_protocol_enabled() -> bool {
932 std::env::var("VTCODE_KEYBOARD_PROTOCOL_ENABLED")
933 .ok()
934 .and_then(|v| v.parse().ok())
935 .unwrap_or(true)
936}
937
938fn default_keyboard_protocol_mode() -> String {
939 std::env::var("VTCODE_KEYBOARD_PROTOCOL_MODE").unwrap_or_else(|_| "default".to_string())
940}
941
942fn default_disambiguate_escape_codes() -> bool {
943 true
944}
945
946fn default_report_event_types() -> bool {
947 true
948}
949
950fn default_report_alternate_keys() -> bool {
951 true
952}
953
954fn default_report_all_keys() -> bool {
955 false
956}