1use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
9#[serde(rename_all = "lowercase")]
10#[derive(Default)]
11pub enum TraceLevel {
12 Error,
13 Warn,
14 #[default]
15 Info,
16 Debug,
17 Trace,
18}
19
20impl TraceLevel {
21 pub fn as_str(self) -> &'static str {
22 match self {
23 Self::Error => "error",
24 Self::Warn => "warn",
25 Self::Info => "info",
26 Self::Debug => "debug",
27 Self::Trace => "trace",
28 }
29 }
30
31 pub fn parse(value: &str) -> Option<Self> {
32 match value.trim().to_lowercase().as_str() {
33 "error" => Some(Self::Error),
34 "warn" => Some(Self::Warn),
35 "info" => Some(Self::Info),
36 "debug" => Some(Self::Debug),
37 "trace" => Some(Self::Trace),
38 _ => None,
39 }
40 }
41}
42
43impl std::fmt::Display for TraceLevel {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 f.write_str(self.as_str())
46 }
47}
48
49impl<'de> serde::Deserialize<'de> for TraceLevel {
50 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
51 where
52 D: serde::Deserializer<'de>,
53 {
54 let raw = String::deserialize(deserializer)?;
55 if let Some(parsed) = Self::parse(&raw) {
56 Ok(parsed)
57 } else {
58 tracing::warn!(
59 input = raw,
60 "Invalid trace level; falling back to default (info)"
61 );
62 Ok(Self::default())
63 }
64 }
65}
66
67#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
69#[derive(Debug, Clone, Deserialize, Serialize)]
70pub struct DebugConfig {
71 #[serde(default)]
73 pub enable_tracing: bool,
74
75 #[serde(default)]
77 pub trace_level: TraceLevel,
78
79 #[serde(default)]
82 pub trace_targets: Vec<String>,
83
84 #[serde(default)]
86 pub debug_log_dir: Option<String>,
87
88 #[serde(default = "default_max_debug_log_size_mb")]
90 pub max_debug_log_size_mb: u64,
91
92 #[serde(default = "default_max_debug_log_age_days")]
94 pub max_debug_log_age_days: u32,
95}
96
97fn default_max_debug_log_size_mb() -> u64 {
98 50
99}
100
101fn default_max_debug_log_age_days() -> u32 {
102 7
103}
104
105impl Default for DebugConfig {
106 fn default() -> Self {
107 Self {
108 enable_tracing: false,
109 trace_level: TraceLevel::Info,
110 trace_targets: Vec::new(),
111 debug_log_dir: None,
112 max_debug_log_size_mb: 50,
113 max_debug_log_age_days: 7,
114 }
115 }
116}
117
118impl DebugConfig {
119 pub fn debug_log_path(&self) -> PathBuf {
121 self.debug_log_dir
122 .as_ref()
123 .and_then(|dir| {
124 if dir.starts_with("~") {
125 dirs::home_dir()
126 .map(|home| home.join(dir.trim_start_matches('~').trim_start_matches('/')))
127 } else {
128 Some(PathBuf::from(dir))
129 }
130 })
131 .unwrap_or_else(|| {
132 dirs::home_dir()
133 .map(|home| home.join(".vtcode/debug"))
134 .unwrap_or_else(|| PathBuf::from(".vtcode/debug"))
135 })
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn test_trace_level_parsing() {
145 assert_eq!(TraceLevel::parse("error"), Some(TraceLevel::Error));
146 assert_eq!(TraceLevel::parse("WARN"), Some(TraceLevel::Warn));
147 assert_eq!(TraceLevel::parse("info"), Some(TraceLevel::Info));
148 assert_eq!(TraceLevel::parse("DEBUG"), Some(TraceLevel::Debug));
149 assert_eq!(TraceLevel::parse("trace"), Some(TraceLevel::Trace));
150 assert_eq!(TraceLevel::parse("invalid"), None);
151 }
152
153 #[test]
154 fn test_debug_config_default() {
155 let cfg = DebugConfig::default();
156 assert!(!cfg.enable_tracing);
157 assert_eq!(cfg.trace_level, TraceLevel::Info);
158 assert!(cfg.trace_targets.is_empty());
159 assert_eq!(cfg.max_debug_log_size_mb, 50);
160 assert_eq!(cfg.max_debug_log_age_days, 7);
161 }
162}