1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "snake_case")]
6pub enum SnapshotScope {
7 Visible,
8 Recent(usize),
9 Full,
10}
11
12impl SnapshotScope {
13 pub fn from_config_str(s: &str) -> Self {
14 if s == "visible" {
15 Self::Visible
16 } else if s == "full" {
17 Self::Full
18 } else if let Some(n) = s.strip_prefix("recent_") {
19 Self::Recent(n.parse().unwrap_or(10))
20 } else {
21 Self::Visible
22 }
23 }
24
25 pub fn to_config_str(&self) -> String {
26 match self {
27 Self::Visible => "visible".to_string(),
28 Self::Recent(n) => format!("recent_{n}"),
29 Self::Full => "full".to_string(),
30 }
31 }
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct CommandEntry {
37 pub command: String,
38 pub exit_code: Option<i32>,
39 pub duration_ms: u64,
40 pub cwd: Option<String>,
41 pub output: Option<String>,
42 pub output_line_count: usize,
43}
44
45#[derive(Debug, Clone, Default, Serialize, Deserialize)]
47pub struct EnvironmentInfo {
48 pub hostname: Option<String>,
49 pub username: Option<String>,
50 pub cwd: Option<String>,
51 pub shell: Option<String>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct TerminalInfo {
57 pub cols: usize,
58 pub rows: usize,
59 pub cursor: (usize, usize),
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct SnapshotData {
65 pub timestamp: String,
66 pub scope: String,
67 pub environment: EnvironmentInfo,
68 pub terminal: TerminalInfo,
69 pub commands: Vec<CommandEntry>,
70}
71
72impl SnapshotData {
73 pub fn to_json(&self) -> Result<String, serde_json::Error> {
74 serde_json::to_string_pretty(self)
75 }
76
77 pub fn gather(
88 terminal: &par_term_terminal::TerminalManager,
89 scope: &SnapshotScope,
90 _max_output_lines: usize,
91 ) -> Self {
92 let history = terminal.core_command_history();
95
96 let commands_to_include: Vec<_> = match scope {
97 SnapshotScope::Visible => {
98 history.iter().rev().take(10).rev().cloned().collect()
100 }
101 SnapshotScope::Recent(n) => history.iter().rev().take(*n).rev().cloned().collect(),
102 SnapshotScope::Full => history,
103 };
104
105 let cwd = terminal.shell_integration_cwd();
106
107 let commands: Vec<CommandEntry> = commands_to_include
108 .into_iter()
109 .map(|(cmd, exit_code, duration_ms)| CommandEntry {
110 command: cmd,
111 exit_code,
112 duration_ms: duration_ms.unwrap_or(0),
113 cwd: cwd.clone(),
114 output: None,
115 output_line_count: 0,
116 })
117 .collect();
118
119 let (cursor_col, cursor_row) = terminal.cursor_position();
120
121 let shell = std::env::var("SHELL").ok();
122
123 let environment = EnvironmentInfo {
124 hostname: terminal.shell_integration_hostname(),
125 username: terminal.shell_integration_username(),
126 cwd,
127 shell,
128 };
129
130 let (cols, rows) = terminal.dimensions();
131
132 let terminal_info = TerminalInfo {
133 cols,
134 rows,
135 cursor: (cursor_col, cursor_row),
136 };
137
138 Self {
139 timestamp: chrono::Utc::now().to_rfc3339(),
140 scope: scope.to_config_str(),
141 environment,
142 terminal: terminal_info,
143 commands,
144 }
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn test_scope_from_config_str() {
154 assert_eq!(
155 SnapshotScope::from_config_str("visible"),
156 SnapshotScope::Visible
157 );
158 assert_eq!(SnapshotScope::from_config_str("full"), SnapshotScope::Full);
159 assert_eq!(
160 SnapshotScope::from_config_str("recent_10"),
161 SnapshotScope::Recent(10)
162 );
163 assert_eq!(
164 SnapshotScope::from_config_str("recent_25"),
165 SnapshotScope::Recent(25)
166 );
167 assert_eq!(
168 SnapshotScope::from_config_str("unknown"),
169 SnapshotScope::Visible
170 );
171 }
172
173 #[test]
174 fn test_scope_roundtrip() {
175 let scopes = vec![
176 SnapshotScope::Visible,
177 SnapshotScope::Full,
178 SnapshotScope::Recent(10),
179 ];
180 for scope in scopes {
181 let s = scope.to_config_str();
182 assert_eq!(SnapshotScope::from_config_str(&s), scope);
183 }
184 }
185
186 #[test]
187 fn test_snapshot_to_json() {
188 let snapshot = SnapshotData {
189 timestamp: "2026-02-15T10:00:00Z".to_string(),
190 scope: "visible".to_string(),
191 environment: EnvironmentInfo {
192 hostname: Some("test-host".to_string()),
193 username: Some("user".to_string()),
194 cwd: Some("/home/user".to_string()),
195 shell: Some("zsh".to_string()),
196 },
197 terminal: TerminalInfo {
198 cols: 80,
199 rows: 24,
200 cursor: (0, 0),
201 },
202 commands: vec![CommandEntry {
203 command: "echo hello".to_string(),
204 exit_code: Some(0),
205 duration_ms: 100,
206 cwd: Some("/home/user".to_string()),
207 output: Some("hello\n".to_string()),
208 output_line_count: 1,
209 }],
210 };
211 let json = snapshot.to_json().unwrap();
212 assert!(json.contains("echo hello"));
213 assert!(json.contains("test-host"));
214 }
215}