1use std::{fmt, pin::Pin};
2
3use futures_util::Stream;
4
5use super::{Command, VariableCompletion};
6use crate::{config::Theme, errors::Result, format_error, format_msg, process::ProcessOutput};
7
8#[derive(Clone)]
10pub enum ImportExportItem {
11 Command(Command),
12 Completion(VariableCompletion),
13}
14
15impl fmt::Display for ImportExportItem {
16 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17 match self {
18 ImportExportItem::Command(c) => c.fmt(f),
19 ImportExportItem::Completion(c) => c.fmt(f),
20 }
21 }
22}
23
24pub type ImportExportStream = Pin<Box<dyn Stream<Item = Result<ImportExportItem>> + Send>>;
26
27#[derive(Default)]
29pub struct ImportStats {
30 pub commands_imported: u64,
31 pub commands_updated: u64,
32 pub commands_skipped: u64,
33 pub completions_imported: u64,
34 pub completions_updated: u64,
35 pub completions_skipped: u64,
36}
37
38#[derive(Default)]
40pub struct ExportStats {
41 pub commands_exported: u64,
42 pub completions_exported: u64,
43 pub stdout: Option<String>,
44}
45
46impl ImportStats {
47 pub fn into_output(self, theme: &Theme) -> ProcessOutput {
49 let ImportStats {
50 commands_imported,
51 commands_updated,
52 commands_skipped,
53 completions_imported,
54 completions_updated,
55 completions_skipped,
56 } = self;
57
58 let total_actions = commands_imported
60 + commands_updated
61 + commands_skipped
62 + completions_imported
63 + completions_updated
64 + completions_skipped;
65
66 if total_actions == 0 {
68 return ProcessOutput::fail().stderr(format_error!(theme, "No commands or completions were found"));
69 }
70
71 let was_changed =
73 commands_imported > 0 || commands_updated > 0 || completions_imported > 0 || completions_updated > 0;
74
75 let message = if was_changed {
76 let mut imported_parts = Vec::with_capacity(2);
78 if commands_imported > 0 {
79 imported_parts.push(format!(
80 "{} new command{}",
81 commands_imported,
82 plural_s(commands_imported)
83 ));
84 }
85 if completions_imported > 0 {
86 imported_parts.push(format!(
87 "{} new completion{}",
88 completions_imported,
89 plural_s(completions_imported),
90 ));
91 }
92
93 let mut updated_parts = Vec::with_capacity(2);
94 if commands_updated > 0 {
95 updated_parts.push(format!("{} command{}", commands_updated, plural_s(commands_updated)));
96 }
97 if completions_updated > 0 {
98 updated_parts.push(format!(
99 "{} completion{}",
100 completions_updated,
101 plural_s(completions_updated)
102 ));
103 }
104
105 let mut skipped_parts = Vec::with_capacity(2);
106 if commands_skipped > 0 {
107 skipped_parts.push(format!("{} command{}", commands_skipped, plural_s(commands_skipped)));
108 }
109 if completions_skipped > 0 {
110 skipped_parts.push(format!(
111 "{} completion{}",
112 completions_skipped,
113 plural_s(completions_skipped)
114 ));
115 }
116
117 let main_msg;
119 let mut secondary_msg_parts = Vec::with_capacity(2);
120
121 if !imported_parts.is_empty() {
122 main_msg = format!("Imported {}", imported_parts.join(" and "));
123 if !updated_parts.is_empty() {
125 secondary_msg_parts.push(format!("{} updated", updated_parts.join(" and ")));
126 }
127 } else {
128 main_msg = format!("Updated {}", updated_parts.join(" and "));
130 }
131
132 if !skipped_parts.is_empty() {
134 secondary_msg_parts.push(format!("{} already existed", skipped_parts.join(" and ")));
135 }
136
137 let secondary_msg = if !secondary_msg_parts.is_empty() {
139 format!(" ({})", secondary_msg_parts.join("; "))
140 } else {
141 String::new()
142 };
143
144 format_msg!(theme, "{main_msg}{}", theme.secondary.apply(secondary_msg))
145 } else {
146 let mut skipped_parts = Vec::with_capacity(2);
148 if commands_skipped > 0 {
149 skipped_parts.push(format!("{} command{}", commands_skipped, plural_s(commands_skipped)));
150 }
151 if completions_skipped > 0 {
152 skipped_parts.push(format!(
153 "{} completion{}",
154 completions_skipped,
155 plural_s(completions_skipped),
156 ));
157 }
158 format!("No new changes; {} already existed", skipped_parts.join(" and "))
159 };
160
161 ProcessOutput::success().stderr(message)
162 }
163}
164
165impl ExportStats {
166 pub fn into_output(self, theme: &Theme) -> ProcessOutput {
168 let ExportStats {
169 commands_exported,
170 completions_exported,
171 stdout,
172 } = self;
173
174 if commands_exported == 0 && completions_exported == 0 {
176 return ProcessOutput::fail().stderr(format_error!(theme, "No commands or completions to export"));
177 }
178
179 let mut parts = Vec::with_capacity(2);
181 if commands_exported > 0 {
182 parts.push(format!("{} command{}", commands_exported, plural_s(commands_exported)));
183 }
184 if completions_exported > 0 {
185 parts.push(format!(
186 "{} completion{}",
187 completions_exported,
188 plural_s(completions_exported)
189 ));
190 }
191
192 let summary = parts.join(" and ");
193 let stderr_msg = format_msg!(theme, "Exported {summary}");
194
195 let mut output = ProcessOutput::success().stderr(stderr_msg);
197
198 if let Some(stdout_content) = stdout {
200 output = output.stdout(stdout_content);
201 }
202
203 output
204 }
205}
206
207fn plural_s(count: u64) -> &'static str {
209 if count == 1 { "" } else { "s" }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use crate::config::Theme;
216
217 #[test]
218 fn test_import_stats_into_output_no_actions() {
219 let stats = ImportStats::default();
220 let theme = Theme::default();
221 let output = stats.into_output(&theme);
222
223 if let ProcessOutput::Output(info) = output {
224 assert!(info.failed);
225 assert!(info.stdout.is_none());
226 assert_eq!(
227 strip_ansi(info.stderr.as_deref().unwrap()),
228 "[Error] No commands or completions were found",
229 );
230 } else {
231 panic!("Expected ProcessOutput::Output variant");
232 }
233 }
234
235 #[test]
236 fn test_import_stats_into_output_only_skipped() {
237 let stats = ImportStats {
238 commands_skipped: 5,
239 completions_skipped: 2,
240 ..Default::default()
241 };
242 let theme = Theme::default();
243 let output = stats.into_output(&theme);
244
245 if let ProcessOutput::Output(info) = output {
246 assert!(!info.failed);
247 assert!(info.stdout.is_none());
248 assert_eq!(
249 strip_ansi(info.stderr.as_deref().unwrap()),
250 "No new changes; 5 commands and 2 completions already existed",
251 );
252 } else {
253 panic!("Expected ProcessOutput::Output variant");
254 }
255 }
256
257 #[test]
258 fn test_import_stats_into_output_only_skipped_singular() {
259 let stats = ImportStats {
260 commands_skipped: 1,
261 completions_skipped: 1,
262 ..Default::default()
263 };
264 let theme = Theme::default();
265 let output = stats.into_output(&theme);
266
267 if let ProcessOutput::Output(info) = output {
268 assert!(!info.failed);
269 assert!(info.stdout.is_none());
270 assert_eq!(
271 strip_ansi(info.stderr.as_deref().unwrap()),
272 "No new changes; 1 command and 1 completion already existed",
273 );
274 } else {
275 panic!("Expected ProcessOutput::Output variant");
276 }
277 }
278
279 #[test]
280 fn test_import_stats_into_output_only_imports() {
281 let stats = ImportStats {
282 commands_imported: 1,
283 completions_imported: 1,
284 ..Default::default()
285 };
286 let theme = Theme::default();
287 let output = stats.into_output(&theme);
288
289 if let ProcessOutput::Output(info) = output {
290 assert!(!info.failed);
291 assert!(info.stdout.is_none());
292 assert_eq!(
293 strip_ansi(info.stderr.as_deref().unwrap()),
294 "-> Imported 1 new command and 1 new completion",
295 );
296 } else {
297 panic!("Expected ProcessOutput::Output variant");
298 }
299 }
300
301 #[test]
302 fn test_import_stats_into_output_only_updates() {
303 let stats = ImportStats {
304 commands_updated: 10,
305 completions_updated: 1,
306 ..Default::default()
307 };
308 let theme = Theme::default();
309 let output = stats.into_output(&theme);
310
311 if let ProcessOutput::Output(info) = output {
312 assert!(!info.failed);
313 assert!(info.stdout.is_none());
314 assert_eq!(
315 strip_ansi(info.stderr.as_deref().unwrap()),
316 "-> Updated 10 commands and 1 completion",
317 );
318 } else {
319 panic!("Expected ProcessOutput::Output variant");
320 }
321 }
322
323 #[test]
324 fn test_import_stats_into_output_imports_and_skipped() {
325 let stats = ImportStats {
326 commands_imported: 3,
327 commands_skipped: 2,
328 completions_imported: 4,
329 completions_skipped: 1,
330 ..Default::default()
331 };
332 let theme = Theme::default();
333 let output = stats.into_output(&theme);
334
335 if let ProcessOutput::Output(info) = output {
336 assert!(!info.failed);
337 assert!(info.stdout.is_none());
338 assert_eq!(
339 strip_ansi(info.stderr.as_deref().unwrap()),
340 "-> Imported 3 new commands and 4 new completions (2 commands and 1 completion already existed)",
341 );
342 } else {
343 panic!("Expected ProcessOutput::Output variant");
344 }
345 }
346
347 #[test]
348 fn test_import_stats_into_output_imports_cmds_skipped_completions() {
349 let stats = ImportStats {
350 commands_imported: 5,
351 completions_skipped: 3,
352 ..Default::default()
353 };
354 let theme = Theme::default();
355 let output = stats.into_output(&theme);
356
357 if let ProcessOutput::Output(info) = output {
358 assert!(!info.failed);
359 assert!(info.stdout.is_none());
360 assert_eq!(
361 strip_ansi(info.stderr.as_deref().unwrap()),
362 "-> Imported 5 new commands (3 completions already existed)",
363 );
364 } else {
365 panic!("Expected ProcessOutput::Output variant");
366 }
367 }
368
369 #[test]
370 fn test_export_stats_into_output_no_actions() {
371 let stats = ExportStats::default();
372 let theme = Theme::default();
373 let output = stats.into_output(&theme);
374
375 if let ProcessOutput::Output(info) = output {
376 assert!(info.failed);
377 assert!(info.stdout.is_none());
378 assert_eq!(
379 strip_ansi(info.stderr.as_deref().unwrap()),
380 "[Error] No commands or completions to export"
381 );
382 } else {
383 panic!("Expected ProcessOutput::Output variant");
384 }
385 }
386
387 #[test]
388 fn test_export_stats_into_output_only_commands_singular() {
389 let stats = ExportStats {
390 commands_exported: 1,
391 ..Default::default()
392 };
393 let theme = Theme::default();
394 let output = stats.into_output(&theme);
395
396 if let ProcessOutput::Output(info) = output {
397 assert!(!info.failed);
398 assert!(info.stdout.is_none());
399 assert_eq!(strip_ansi(info.stderr.as_deref().unwrap()), "-> Exported 1 command");
400 } else {
401 panic!("Expected ProcessOutput::Output variant");
402 }
403 }
404
405 #[test]
406 fn test_export_stats_into_output_only_completions_plural() {
407 let stats = ExportStats {
408 completions_exported: 10,
409 ..Default::default()
410 };
411 let theme = Theme::default();
412 let output = stats.into_output(&theme);
413
414 if let ProcessOutput::Output(info) = output {
415 assert!(!info.failed);
416 assert!(info.stdout.is_none());
417 assert_eq!(
418 strip_ansi(info.stderr.as_deref().unwrap()),
419 "-> Exported 10 completions"
420 );
421 } else {
422 panic!("Expected ProcessOutput::Output variant");
423 }
424 }
425
426 #[test]
427 fn test_export_stats_into_output_both_commands_and_completions() {
428 let stats = ExportStats {
429 commands_exported: 5,
430 completions_exported: 8,
431 ..Default::default()
432 };
433 let theme = Theme::default();
434 let output = stats.into_output(&theme);
435
436 if let ProcessOutput::Output(info) = output {
437 assert!(!info.failed);
438 assert!(info.stdout.is_none());
439 assert_eq!(
440 strip_ansi(info.stderr.as_deref().unwrap()),
441 "-> Exported 5 commands and 8 completions"
442 );
443 } else {
444 panic!("Expected ProcessOutput::Output variant");
445 }
446 }
447
448 fn strip_ansi(s: &str) -> String {
450 String::from_utf8(strip_ansi_escapes::strip(s.as_bytes())).unwrap()
451 }
452}