1use colored::Colorize;
7
8use crate::api::{DocumentationSnippet, LibrarySearchResult, RespostaDocumentacao};
9use crate::i18n::{idioma_atual, t, Idioma, Mensagem};
10use crate::storage::ChaveArmazenada;
11
12pub fn exibir_bibliotecas_formatado(resultados: &[LibrarySearchResult]) {
19 if resultados.is_empty() {
20 println!("{}", t(Mensagem::NenhumaBibliotecaEncontrada).yellow());
21 return;
22 }
23
24 println!("{}", t(Mensagem::BibliotecasEncontradas).green().bold());
25 println!("{}", "─".repeat(60).dimmed());
26
27 for (i, lib) in resultados.iter().enumerate() {
28 let numero = format!("{}.", i + 1);
29
30 let titulo = if let Some(score) = lib.trust_score {
32 format!(
33 "{} {} ({} {:.1}/10)",
34 numero.cyan(),
35 lib.title.bold(),
36 t(Mensagem::ConfiancaScore),
37 score
38 )
39 } else {
40 format!("{} {}", numero.cyan(), lib.title.bold())
41 };
42 println!("{}", titulo);
43
44 println!(" {}", lib.id.dimmed());
46
47 if let Some(desc) = &lib.description {
48 println!(" {}", desc.italic());
49 }
50
51 println!();
52 }
53}
54
55pub fn exibir_dica_biblioteca_nao_encontrada() {
60 eprintln!("{}", t(Mensagem::BibliotecaNaoEncontradaApi).yellow());
61}
62
63pub fn exibir_documentacao_formatada(doc: &RespostaDocumentacao) {
69 let snippets = match &doc.snippets {
70 Some(s) if !s.is_empty() => s,
71 _ => {
72 println!("{}", t(Mensagem::NenhumaDocumentacaoEncontrada).yellow());
73 return;
74 }
75 };
76
77 println!("{}", t(Mensagem::TituloDocumentacao).green().bold());
78 println!("{}", "─".repeat(60).dimmed());
79
80 for snippet in snippets {
81 exibir_snippet(snippet);
82 }
83}
84
85fn exibir_snippet(snippet: &DocumentationSnippet) {
89 if let Some(titulo_pagina) = &snippet.page_title {
90 println!("{}", format!("## {}", titulo_pagina).green().bold());
91 }
92
93 if let Some(titulo_codigo) = &snippet.code_title {
94 println!("{}", format!("▸ {}", titulo_codigo).cyan());
95 }
96
97 if let Some(descricao) = &snippet.code_description {
98 println!(" {}", descricao.dimmed().italic());
99 }
100
101 if let Some(blocos) = &snippet.code_list {
102 for bloco in blocos {
103 println!("```{}", bloco.language);
104 println!("{}", bloco.code);
105 println!("```");
106 }
107 }
108
109 if let Some(source) = &snippet.code_id {
110 println!("{}", source.blue().bold().dimmed());
111 }
112
113 println!();
114}
115
116pub fn exibir_chaves_mascaradas(chaves: &[ChaveArmazenada], mascarar: impl Fn(&str) -> String) {
120 println!(
121 "{}",
122 format!("{} {}", chaves.len(), t(Mensagem::ContadorChaves))
123 .green()
124 .bold()
125 );
126 println!("{}", "─".repeat(60).dimmed());
127
128 let rotulo_adicionada = match idioma_atual() {
129 Idioma::English => "added:",
130 Idioma::Portugues => "adicionada:",
131 };
132
133 for (i, chave) in chaves.iter().enumerate() {
134 println!(
135 " {} {} {}",
136 format!("[{}]", i + 1).cyan(),
137 mascarar(&chave.value).bold(),
138 format!(
139 "({} {})",
140 rotulo_adicionada,
141 formatar_added_at_display(&chave.added_at)
142 )
143 .dimmed()
144 );
145 }
146}
147
148pub fn formatar_added_at_display(iso: &str) -> String {
152 chrono::DateTime::parse_from_rfc3339(iso)
153 .map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
154 .unwrap_or_else(|_| iso.to_string())
155}
156
157pub fn exibir_nenhuma_chave() {
159 println!("{}", t(Mensagem::NenhumaChaveArmazenada).yellow());
160 println!("{}", t(Mensagem::UsarKeysAdd).cyan());
161}
162
163pub fn exibir_nenhuma_chave_para_remover() {
165 println!("{}", t(Mensagem::NenhumaChaveParaRemover).yellow());
166}
167
168pub fn exibir_indice_invalido(_indice: usize, total: usize) {
170 println!(
171 "{}",
172 format!("{} {}.", t(Mensagem::IndiceInvalido), total).red()
173 );
174}
175
176pub fn exibir_chave_adicionada(caminho: &std::path::Path) {
178 println!(
179 "{} {}",
180 t(Mensagem::ChaveAdicionada),
181 caminho.display().to_string().green()
182 );
183}
184
185pub fn exibir_chave_ja_existia() {
187 println!("{}", t(Mensagem::ChaveJaExistia).yellow());
188}
189
190pub fn exibir_chave_invalida_vazia() {
192 eprintln!("{}", t(Mensagem::ChaveVaziaOuInvalida).red());
193}
194
195pub fn exibir_aviso_formato_chave() {
197 eprintln!("{}", t(Mensagem::AvisoFormatoChave).yellow());
198}
199
200pub fn exibir_chave_removida(chave_mascarada: &str) {
202 println!(
203 "{} {}",
204 chave_mascarada.bold(),
205 t(Mensagem::ChaveRemovidaSucesso)
206 );
207}
208
209pub fn exibir_operacao_cancelada() {
211 println!("{}", t(Mensagem::OperacaoCancelada).yellow());
212}
213
214pub fn exibir_chaves_removidas() {
216 println!("{}", t(Mensagem::TodasChavesRemovidas).green());
217}
218
219pub fn exibir_xdg_nao_suportado() {
221 println!("{}", t(Mensagem::SistemaXdgNaoSuportado).red());
222}
223
224pub fn exibir_importacao_concluida(importadas: usize, total: usize) {
226 println!(
227 "{}",
228 format!(
229 "{}/{} {}",
230 importadas,
231 total,
232 t(Mensagem::ChavesImportadasSucesso)
233 )
234 .green()
235 );
236}
237
238#[cfg(test)]
239mod testes {
240 use super::formatar_added_at_display;
241
242 #[test]
243 fn testa_formatar_added_at_rfc3339_com_nanossegundos() {
244 let resultado = formatar_added_at_display("2026-04-09T13:34:59.060818734+00:00");
245 assert_eq!(resultado, "2026-04-09 13:34:59");
246 assert!(
247 !resultado.contains('T'),
248 "Resultado não deve conter 'T': {resultado}"
249 );
250 assert!(
251 !resultado.contains('.'),
252 "Resultado não deve conter nanossegundos: {resultado}"
253 );
254 assert!(
255 !resultado.contains("+00:00"),
256 "Resultado não deve conter offset de timezone: {resultado}"
257 );
258 }
259
260 #[test]
261 fn testa_formatar_added_at_rfc3339_sem_nanossegundos() {
262 let resultado = formatar_added_at_display("2026-01-01T00:00:00+00:00");
263 assert_eq!(resultado, "2026-01-01 00:00:00");
264 }
265
266 #[test]
267 fn testa_formatar_added_at_rfc3339_offset_nao_utc() {
268 let resultado = formatar_added_at_display("2026-04-09T10:00:00-03:00");
270 assert_eq!(resultado, "2026-04-09 10:00:00");
272 assert!(
274 !resultado.contains("-03:00"),
275 "Resultado não deve conter offset de timezone: {resultado}"
276 );
277 }
278
279 #[test]
280 fn testa_formatar_added_at_fallback_string_invalida() {
281 let resultado = formatar_added_at_display("lixo-nao-e-data");
282 assert_eq!(
283 resultado, "lixo-nao-e-data",
284 "String inválida deve ser retornada sem modificação"
285 );
286 }
287
288 #[test]
289 fn testa_formatar_added_at_string_vazia() {
290 let resultado = formatar_added_at_display("");
291 assert_eq!(
292 resultado, "",
293 "String vazia deve ser retornada sem modificação"
294 );
295 }
296
297 #[test]
298 fn testa_formatar_added_at_formato_saida_legivel() {
299 let resultado = formatar_added_at_display("2026-04-09T13:34:59.123456789+00:00");
300 assert_eq!(
302 resultado.len(),
303 19,
304 "Formato de saída deve ter 19 caracteres, obteve: '{resultado}'"
305 );
306 assert!(
307 resultado.contains(' '),
308 "Resultado deve conter espaço separando data e hora: {resultado}"
309 );
310 }
311}