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 Hybrid,
67 #[default]
69 Desktop,
70}
71
72#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
74#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
75#[serde(rename_all = "snake_case")]
76pub enum NotificationBackend {
77 #[default]
79 Auto,
80 Osascript,
82 NotifyRust,
84 Terminal,
86}
87
88#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
90#[derive(Debug, Clone, Deserialize, Serialize)]
91pub struct UiNotificationsConfig {
92 #[serde(default = "default_notifications_enabled")]
94 pub enabled: bool,
95
96 #[serde(default)]
98 pub delivery_mode: NotificationDeliveryMode,
99
100 #[serde(default)]
102 pub backend: NotificationBackend,
103
104 #[serde(default = "default_notifications_suppress_when_focused")]
106 pub suppress_when_focused: bool,
107
108 #[serde(default)]
111 pub command_failure: Option<bool>,
112
113 #[serde(default = "default_notifications_tool_failure")]
115 pub tool_failure: bool,
116
117 #[serde(default = "default_notifications_error")]
119 pub error: bool,
120
121 #[serde(default = "default_notifications_completion")]
124 pub completion: bool,
125
126 #[serde(default)]
129 pub completion_success: Option<bool>,
130
131 #[serde(default)]
134 pub completion_failure: Option<bool>,
135
136 #[serde(default = "default_notifications_hitl")]
138 pub hitl: bool,
139
140 #[serde(default)]
143 pub policy_approval: Option<bool>,
144
145 #[serde(default)]
148 pub request: Option<bool>,
149
150 #[serde(default = "default_notifications_tool_success")]
152 pub tool_success: bool,
153
154 #[serde(default = "default_notifications_repeat_window_seconds")]
156 pub repeat_window_seconds: u64,
157
158 #[serde(default = "default_notifications_max_identical_in_window")]
160 pub max_identical_in_window: u32,
161}
162
163impl Default for UiNotificationsConfig {
164 fn default() -> Self {
165 Self {
166 enabled: default_notifications_enabled(),
167 delivery_mode: NotificationDeliveryMode::default(),
168 backend: NotificationBackend::default(),
169 suppress_when_focused: default_notifications_suppress_when_focused(),
170 command_failure: Some(default_notifications_command_failure()),
171 tool_failure: default_notifications_tool_failure(),
172 error: default_notifications_error(),
173 completion: default_notifications_completion(),
174 completion_success: Some(default_notifications_completion_success()),
175 completion_failure: Some(default_notifications_completion_failure()),
176 hitl: default_notifications_hitl(),
177 policy_approval: Some(default_notifications_policy_approval()),
178 request: Some(default_notifications_request()),
179 tool_success: default_notifications_tool_success(),
180 repeat_window_seconds: default_notifications_repeat_window_seconds(),
181 max_identical_in_window: default_notifications_max_identical_in_window(),
182 }
183 }
184}
185
186#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
187#[derive(Debug, Clone, Deserialize, Serialize)]
188pub struct UiFullscreenConfig {
189 #[serde(default = "default_fullscreen_mouse_capture")]
192 pub mouse_capture: bool,
193
194 #[serde(default = "default_fullscreen_copy_on_select")]
197 pub copy_on_select: bool,
198
199 #[serde(default = "default_fullscreen_scroll_speed")]
203 pub scroll_speed: u8,
204}
205
206impl Default for UiFullscreenConfig {
207 fn default() -> Self {
208 Self {
209 mouse_capture: default_fullscreen_mouse_capture(),
210 copy_on_select: default_fullscreen_copy_on_select(),
211 scroll_speed: default_fullscreen_scroll_speed(),
212 }
213 }
214}
215
216#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
217#[derive(Debug, Clone, Deserialize, Serialize)]
218pub struct UiConfig {
219 #[serde(default = "default_tool_output_mode")]
221 pub tool_output_mode: ToolOutputMode,
222
223 #[serde(default = "default_tool_output_max_lines")]
225 pub tool_output_max_lines: usize,
226
227 #[serde(default = "default_tool_output_spool_bytes")]
229 pub tool_output_spool_bytes: usize,
230
231 #[serde(default)]
233 pub tool_output_spool_dir: Option<String>,
234
235 #[serde(default = "default_allow_tool_ansi")]
237 pub allow_tool_ansi: bool,
238
239 #[serde(default = "default_inline_viewport_rows")]
241 pub inline_viewport_rows: u16,
242
243 #[serde(default = "default_reasoning_display_mode")]
245 pub reasoning_display_mode: ReasoningDisplayMode,
246
247 #[serde(default = "default_reasoning_visible_default")]
249 pub reasoning_visible_default: bool,
250
251 #[serde(default = "default_vim_mode")]
253 pub vim_mode: bool,
254
255 #[serde(default)]
257 pub status_line: StatusLineConfig,
258
259 #[serde(default)]
261 pub terminal_title: TerminalTitleConfig,
262
263 #[serde(default)]
265 pub keyboard_protocol: KeyboardProtocolConfig,
266
267 #[serde(default)]
269 pub layout_mode: LayoutModeOverride,
270
271 #[serde(default)]
273 pub display_mode: UiDisplayMode,
274
275 #[serde(default = "default_show_sidebar")]
277 pub show_sidebar: bool,
278
279 #[serde(default = "default_dim_completed_todos")]
281 pub dim_completed_todos: bool,
282
283 #[serde(default = "default_message_block_spacing")]
285 pub message_block_spacing: bool,
286
287 #[serde(default = "default_show_turn_timer")]
289 pub show_turn_timer: bool,
290
291 #[serde(default = "default_show_diagnostics_in_transcript")]
295 pub show_diagnostics_in_transcript: bool,
296
297 #[serde(default = "default_minimum_contrast")]
306 pub minimum_contrast: f64,
307
308 #[serde(default = "default_bold_is_bright")]
312 pub bold_is_bright: bool,
313
314 #[serde(default = "default_safe_colors_only")]
320 pub safe_colors_only: bool,
321
322 #[serde(default = "default_color_scheme_mode")]
327 pub color_scheme_mode: ColorSchemeMode,
328
329 #[serde(default)]
331 pub notifications: UiNotificationsConfig,
332
333 #[serde(default)]
335 pub fullscreen: UiFullscreenConfig,
336
337 #[serde(default = "default_screen_reader_mode")]
341 pub screen_reader_mode: bool,
342
343 #[serde(default = "default_reduce_motion_mode")]
346 pub reduce_motion_mode: bool,
347
348 #[serde(default = "default_reduce_motion_keep_progress_animation")]
350 pub reduce_motion_keep_progress_animation: bool,
351}
352
353#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
355#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
356#[serde(rename_all = "snake_case")]
357pub enum ColorSchemeMode {
358 #[default]
360 Auto,
361 Light,
363 Dark,
365}
366
367fn default_minimum_contrast() -> f64 {
368 crate::constants::ui::THEME_MIN_CONTRAST_RATIO
369}
370
371fn default_bold_is_bright() -> bool {
372 false
373}
374
375fn default_safe_colors_only() -> bool {
376 false
377}
378
379fn default_color_scheme_mode() -> ColorSchemeMode {
380 ColorSchemeMode::Auto
381}
382
383fn default_show_sidebar() -> bool {
384 true
385}
386
387fn default_dim_completed_todos() -> bool {
388 true
389}
390
391fn default_message_block_spacing() -> bool {
392 true
393}
394
395fn default_show_turn_timer() -> bool {
396 false
397}
398
399fn default_show_diagnostics_in_transcript() -> bool {
400 false
401}
402
403fn default_vim_mode() -> bool {
404 false
405}
406
407fn default_notifications_enabled() -> bool {
408 true
409}
410
411fn default_notifications_suppress_when_focused() -> bool {
412 true
413}
414
415fn default_notifications_command_failure() -> bool {
416 false
417}
418
419fn default_notifications_tool_failure() -> bool {
420 false
421}
422
423fn default_notifications_error() -> bool {
424 true
425}
426
427fn default_notifications_completion() -> bool {
428 true
429}
430
431fn default_notifications_completion_success() -> bool {
432 false
433}
434
435fn default_notifications_completion_failure() -> bool {
436 true
437}
438
439fn default_notifications_hitl() -> bool {
440 true
441}
442
443fn default_notifications_policy_approval() -> bool {
444 true
445}
446
447fn default_notifications_request() -> bool {
448 false
449}
450
451fn default_notifications_tool_success() -> bool {
452 false
453}
454
455fn default_notifications_repeat_window_seconds() -> u64 {
456 30
457}
458
459fn default_notifications_max_identical_in_window() -> u32 {
460 1
461}
462
463fn env_bool_var(name: &str) -> Option<bool> {
464 read_env_var(name).and_then(|v| {
465 let normalized = v.trim().to_ascii_lowercase();
466 match normalized.as_str() {
467 "1" | "true" | "yes" | "on" => Some(true),
468 "0" | "false" | "no" | "off" => Some(false),
469 _ => None,
470 }
471 })
472}
473
474fn env_u8_var(name: &str) -> Option<u8> {
475 read_env_var(name)
476 .and_then(|value| value.trim().parse::<u8>().ok())
477 .map(clamp_fullscreen_scroll_speed)
478}
479
480fn clamp_fullscreen_scroll_speed(value: u8) -> u8 {
481 value.clamp(1, 20)
482}
483
484fn default_fullscreen_mouse_capture() -> bool {
485 env_bool_var("VTCODE_FULLSCREEN_MOUSE_CAPTURE").unwrap_or(true)
486}
487
488fn default_fullscreen_copy_on_select() -> bool {
489 env_bool_var("VTCODE_FULLSCREEN_COPY_ON_SELECT").unwrap_or(true)
490}
491
492fn default_fullscreen_scroll_speed() -> u8 {
493 env_u8_var("VTCODE_FULLSCREEN_SCROLL_SPEED").unwrap_or(3)
494}
495
496fn default_screen_reader_mode() -> bool {
497 env_bool_var("VTCODE_SCREEN_READER").unwrap_or(false)
498}
499
500fn default_reduce_motion_mode() -> bool {
501 env_bool_var("VTCODE_REDUCE_MOTION").unwrap_or(false)
502}
503
504fn default_reduce_motion_keep_progress_animation() -> bool {
505 false
506}
507
508fn default_ask_questions_enabled() -> bool {
509 true
510}
511
512impl Default for UiConfig {
513 fn default() -> Self {
514 Self {
515 tool_output_mode: default_tool_output_mode(),
516 tool_output_max_lines: default_tool_output_max_lines(),
517 tool_output_spool_bytes: default_tool_output_spool_bytes(),
518 tool_output_spool_dir: None,
519 allow_tool_ansi: default_allow_tool_ansi(),
520 inline_viewport_rows: default_inline_viewport_rows(),
521 reasoning_display_mode: default_reasoning_display_mode(),
522 reasoning_visible_default: default_reasoning_visible_default(),
523 vim_mode: default_vim_mode(),
524 status_line: StatusLineConfig::default(),
525 terminal_title: TerminalTitleConfig::default(),
526 keyboard_protocol: KeyboardProtocolConfig::default(),
527 layout_mode: LayoutModeOverride::default(),
528 display_mode: UiDisplayMode::default(),
529 show_sidebar: default_show_sidebar(),
530 dim_completed_todos: default_dim_completed_todos(),
531 message_block_spacing: default_message_block_spacing(),
532 show_turn_timer: default_show_turn_timer(),
533 show_diagnostics_in_transcript: default_show_diagnostics_in_transcript(),
534 minimum_contrast: default_minimum_contrast(),
536 bold_is_bright: default_bold_is_bright(),
537 safe_colors_only: default_safe_colors_only(),
538 color_scheme_mode: default_color_scheme_mode(),
539 notifications: UiNotificationsConfig::default(),
540 fullscreen: UiFullscreenConfig::default(),
541 screen_reader_mode: default_screen_reader_mode(),
542 reduce_motion_mode: default_reduce_motion_mode(),
543 reduce_motion_keep_progress_animation: default_reduce_motion_keep_progress_animation(),
544 }
545 }
546}
547
548fn read_env_var(name: &str) -> Option<String> {
549 #[cfg(test)]
550 if let Some(override_value) = test_env_overrides::get(name) {
551 return override_value;
552 }
553
554 std::env::var(name).ok()
555}
556
557#[cfg(test)]
558mod test_env_overrides {
559 use std::collections::HashMap;
560 use std::sync::{Mutex, OnceLock};
561
562 static ENV_OVERRIDES: OnceLock<Mutex<HashMap<String, Option<String>>>> = OnceLock::new();
563
564 fn overrides() -> &'static Mutex<HashMap<String, Option<String>>> {
565 ENV_OVERRIDES.get_or_init(|| Mutex::new(HashMap::new()))
566 }
567
568 pub(super) fn get(name: &str) -> Option<Option<String>> {
569 overrides()
570 .lock()
571 .expect("env overrides lock poisoned")
572 .get(name)
573 .cloned()
574 }
575
576 pub(super) fn set(name: &str, value: Option<&str>) {
577 overrides()
578 .lock()
579 .expect("env overrides lock poisoned")
580 .insert(name.to_string(), value.map(ToOwned::to_owned));
581 }
582
583 pub(super) fn restore(name: &str, previous: Option<Option<String>>) {
584 let mut guard = overrides().lock().expect("env overrides lock poisoned");
585 match previous {
586 Some(value) => {
587 guard.insert(name.to_string(), value);
588 }
589 None => {
590 guard.remove(name);
591 }
592 }
593 }
594}
595
596#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
598#[derive(Debug, Clone, Deserialize, Serialize, Default)]
599pub struct ChatConfig {
600 #[serde(default, rename = "askQuestions", alias = "ask_questions")]
602 pub ask_questions: AskQuestionsConfig,
603}
604
605#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
607#[derive(Debug, Clone, Deserialize, Serialize)]
608pub struct AskQuestionsConfig {
609 #[serde(default = "default_ask_questions_enabled")]
611 pub enabled: bool,
612}
613
614impl Default for AskQuestionsConfig {
615 fn default() -> Self {
616 Self {
617 enabled: default_ask_questions_enabled(),
618 }
619 }
620}
621
622#[cfg(test)]
623mod tests {
624 use super::*;
625 use serial_test::serial;
626
627 fn with_env_var<F>(key: &str, value: Option<&str>, f: F)
628 where
629 F: FnOnce(),
630 {
631 let previous = test_env_overrides::get(key);
632 test_env_overrides::set(key, value);
633 f();
634 test_env_overrides::restore(key, previous);
635 }
636
637 #[test]
638 #[serial]
639 fn fullscreen_defaults_match_expected_values() {
640 let fullscreen = UiFullscreenConfig::default();
641
642 assert!(fullscreen.mouse_capture);
643 assert!(fullscreen.copy_on_select);
644 assert_eq!(fullscreen.scroll_speed, 3);
645 }
646
647 #[test]
648 #[serial]
649 fn fullscreen_env_overrides_apply_to_defaults() {
650 with_env_var("VTCODE_FULLSCREEN_MOUSE_CAPTURE", Some("0"), || {
651 with_env_var("VTCODE_FULLSCREEN_COPY_ON_SELECT", Some("false"), || {
652 with_env_var("VTCODE_FULLSCREEN_SCROLL_SPEED", Some("7"), || {
653 let fullscreen = UiFullscreenConfig::default();
654 assert!(!fullscreen.mouse_capture);
655 assert!(!fullscreen.copy_on_select);
656 assert_eq!(fullscreen.scroll_speed, 7);
657 });
658 });
659 });
660 }
661
662 #[test]
663 #[serial]
664 fn fullscreen_scroll_speed_is_clamped() {
665 with_env_var("VTCODE_FULLSCREEN_SCROLL_SPEED", Some("0"), || {
666 assert_eq!(UiFullscreenConfig::default().scroll_speed, 1);
667 });
668
669 with_env_var("VTCODE_FULLSCREEN_SCROLL_SPEED", Some("99"), || {
670 assert_eq!(UiFullscreenConfig::default().scroll_speed, 20);
671 });
672 }
673}
674
675#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
677#[derive(Debug, Clone, Deserialize, Serialize)]
678pub struct PtyConfig {
679 #[serde(default = "default_pty_enabled")]
681 pub enabled: bool,
682
683 #[serde(default = "default_pty_rows")]
685 pub default_rows: u16,
686
687 #[serde(default = "default_pty_cols")]
689 pub default_cols: u16,
690
691 #[serde(default = "default_max_pty_sessions")]
693 pub max_sessions: usize,
694
695 #[serde(default = "default_pty_timeout")]
697 pub command_timeout_seconds: u64,
698
699 #[serde(default = "default_stdout_tail_lines")]
701 pub stdout_tail_lines: usize,
702
703 #[serde(default = "default_scrollback_lines")]
705 pub scrollback_lines: usize,
706
707 #[serde(default = "default_max_scrollback_bytes")]
709 pub max_scrollback_bytes: usize,
710
711 #[serde(default)]
713 pub emulation_backend: PtyEmulationBackend,
714
715 #[serde(default = "default_large_output_threshold_kb")]
717 pub large_output_threshold_kb: usize,
718
719 #[serde(default)]
721 pub preferred_shell: Option<String>,
722
723 #[serde(default = "default_shell_zsh_fork")]
725 pub shell_zsh_fork: bool,
726
727 #[serde(default)]
729 pub zsh_path: Option<String>,
730}
731
732impl Default for PtyConfig {
733 fn default() -> Self {
734 Self {
735 enabled: default_pty_enabled(),
736 default_rows: default_pty_rows(),
737 default_cols: default_pty_cols(),
738 max_sessions: default_max_pty_sessions(),
739 command_timeout_seconds: default_pty_timeout(),
740 stdout_tail_lines: default_stdout_tail_lines(),
741 scrollback_lines: default_scrollback_lines(),
742 max_scrollback_bytes: default_max_scrollback_bytes(),
743 emulation_backend: PtyEmulationBackend::default(),
744 large_output_threshold_kb: default_large_output_threshold_kb(),
745 preferred_shell: None,
746 shell_zsh_fork: default_shell_zsh_fork(),
747 zsh_path: None,
748 }
749 }
750}
751
752#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
753#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
754#[serde(rename_all = "snake_case")]
755pub enum PtyEmulationBackend {
756 #[default]
757 Ghostty,
758 LegacyVt100,
759}
760
761impl PtyEmulationBackend {
762 #[must_use]
763 pub const fn as_str(self) -> &'static str {
764 match self {
765 Self::Ghostty => "ghostty",
766 Self::LegacyVt100 => "legacy_vt100",
767 }
768 }
769}
770
771impl PtyConfig {
772 pub fn validate(&self) -> Result<()> {
773 self.zsh_fork_shell_path()?;
774 Ok(())
775 }
776
777 pub fn zsh_fork_shell_path(&self) -> Result<Option<&str>> {
778 if !self.shell_zsh_fork {
779 return Ok(None);
780 }
781
782 let zsh_path = self
783 .zsh_path
784 .as_deref()
785 .map(str::trim)
786 .filter(|path| !path.is_empty())
787 .ok_or_else(|| {
788 anyhow!(
789 "pty.shell_zsh_fork is enabled, but pty.zsh_path is not configured. \
790 Set pty.zsh_path to an absolute path to patched zsh."
791 )
792 })?;
793
794 #[cfg(not(unix))]
795 {
796 let _ = zsh_path;
797 bail!("pty.shell_zsh_fork is only supported on Unix platforms");
798 }
799
800 #[cfg(unix)]
801 {
802 let path = std::path::Path::new(zsh_path);
803 if !path.is_absolute() {
804 bail!(
805 "pty.zsh_path '{}' must be an absolute path when pty.shell_zsh_fork is enabled",
806 zsh_path
807 );
808 }
809 if !path.exists() {
810 bail!(
811 "pty.zsh_path '{}' does not exist (required when pty.shell_zsh_fork is enabled)",
812 zsh_path
813 );
814 }
815 if !path.is_file() {
816 bail!(
817 "pty.zsh_path '{}' is not a file (required when pty.shell_zsh_fork is enabled)",
818 zsh_path
819 );
820 }
821 }
822
823 Ok(Some(zsh_path))
824 }
825}
826
827fn default_pty_enabled() -> bool {
828 true
829}
830
831fn default_pty_rows() -> u16 {
832 24
833}
834
835fn default_pty_cols() -> u16 {
836 80
837}
838
839fn default_max_pty_sessions() -> usize {
840 10
841}
842
843fn default_pty_timeout() -> u64 {
844 300
845}
846
847fn default_shell_zsh_fork() -> bool {
848 false
849}
850
851fn default_stdout_tail_lines() -> usize {
852 crate::constants::defaults::DEFAULT_PTY_STDOUT_TAIL_LINES
853}
854
855fn default_scrollback_lines() -> usize {
856 crate::constants::defaults::DEFAULT_PTY_SCROLLBACK_LINES
857}
858
859fn default_max_scrollback_bytes() -> usize {
860 25_000_000 }
864
865fn default_large_output_threshold_kb() -> usize {
866 5_000 }
868
869fn default_tool_output_mode() -> ToolOutputMode {
870 ToolOutputMode::Compact
871}
872
873fn default_tool_output_max_lines() -> usize {
874 600
875}
876
877fn default_tool_output_spool_bytes() -> usize {
878 200_000
879}
880
881fn default_allow_tool_ansi() -> bool {
882 false
883}
884
885fn default_inline_viewport_rows() -> u16 {
886 crate::constants::ui::DEFAULT_INLINE_VIEWPORT_ROWS
887}
888
889fn default_reasoning_display_mode() -> ReasoningDisplayMode {
890 ReasoningDisplayMode::Toggle
891}
892
893fn default_reasoning_visible_default() -> bool {
894 crate::constants::ui::DEFAULT_REASONING_VISIBLE
895}
896
897#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
900#[derive(Debug, Clone, Deserialize, Serialize)]
901pub struct KeyboardProtocolConfig {
902 #[serde(default = "default_keyboard_protocol_enabled")]
904 pub enabled: bool,
905
906 #[serde(default = "default_keyboard_protocol_mode")]
908 pub mode: String,
909
910 #[serde(default = "default_disambiguate_escape_codes")]
912 pub disambiguate_escape_codes: bool,
913
914 #[serde(default = "default_report_event_types")]
916 pub report_event_types: bool,
917
918 #[serde(default = "default_report_alternate_keys")]
920 pub report_alternate_keys: bool,
921
922 #[serde(default = "default_report_all_keys")]
924 pub report_all_keys: bool,
925}
926
927impl Default for KeyboardProtocolConfig {
928 fn default() -> Self {
929 Self {
930 enabled: default_keyboard_protocol_enabled(),
931 mode: default_keyboard_protocol_mode(),
932 disambiguate_escape_codes: default_disambiguate_escape_codes(),
933 report_event_types: default_report_event_types(),
934 report_alternate_keys: default_report_alternate_keys(),
935 report_all_keys: default_report_all_keys(),
936 }
937 }
938}
939
940impl KeyboardProtocolConfig {
941 pub fn validate(&self) -> Result<()> {
942 match self.mode.as_str() {
943 "default" | "full" | "minimal" | "custom" => Ok(()),
944 _ => anyhow::bail!(
945 "Invalid keyboard protocol mode '{}'. Must be: default, full, minimal, or custom",
946 self.mode
947 ),
948 }
949 }
950}
951
952fn default_keyboard_protocol_enabled() -> bool {
953 std::env::var("VTCODE_KEYBOARD_PROTOCOL_ENABLED")
954 .ok()
955 .and_then(|v| v.parse().ok())
956 .unwrap_or(true)
957}
958
959fn default_keyboard_protocol_mode() -> String {
960 std::env::var("VTCODE_KEYBOARD_PROTOCOL_MODE").unwrap_or_else(|_| "default".to_string())
961}
962
963fn default_disambiguate_escape_codes() -> bool {
964 true
965}
966
967fn default_report_event_types() -> bool {
968 true
969}
970
971fn default_report_alternate_keys() -> bool {
972 true
973}
974
975fn default_report_all_keys() -> bool {
976 false
977}