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