1use anyhow::Context;
7use colored::Colorize;
8
9use crate::api::{DocumentationSnippet, LibrarySearchResult, RespostaDocumentacao};
10use crate::i18n::{idioma_atual, t, Idioma, Mensagem};
11use crate::storage::ChaveArmazenada;
12
13pub fn exibir_bibliotecas_formatado(resultados: &[LibrarySearchResult]) {
20 if resultados.is_empty() {
21 println!("{}", t(Mensagem::NenhumaBibliotecaEncontrada).yellow());
22 return;
23 }
24
25 println!("{}", t(Mensagem::BibliotecasEncontradas).green().bold());
26 println!("{}", "─".repeat(60).dimmed());
27
28 for (i, lib) in resultados.iter().enumerate() {
29 let numero = format!("{}.", i + 1);
30
31 let titulo = if let Some(score) = lib.trust_score {
33 format!(
34 "{} {} ({} {:.1}/10)",
35 numero.cyan(),
36 lib.title.bold(),
37 t(Mensagem::ConfiancaScore),
38 score
39 )
40 } else {
41 format!("{} {}", numero.cyan(), lib.title.bold())
42 };
43 println!("{}", titulo);
44
45 println!(" {}", lib.id.dimmed());
47
48 if let Some(desc) = &lib.description {
49 println!(" {}", desc.italic());
50 }
51
52 println!();
53 }
54}
55
56pub fn exibir_dica_biblioteca_nao_encontrada() {
61 eprintln!("{}", t(Mensagem::BibliotecaNaoEncontradaApi).yellow());
62}
63
64pub fn exibir_documentacao_formatada(doc: &RespostaDocumentacao) {
70 let snippets = match &doc.snippets {
71 Some(s) if !s.is_empty() => s,
72 _ => {
73 println!("{}", t(Mensagem::NenhumaDocumentacaoEncontrada).yellow());
74 return;
75 }
76 };
77
78 println!("{}", t(Mensagem::TituloDocumentacao).green().bold());
79 println!("{}", "─".repeat(60).dimmed());
80
81 for snippet in snippets {
82 exibir_snippet(snippet);
83 }
84}
85
86fn exibir_snippet(snippet: &DocumentationSnippet) {
90 if let Some(titulo_pagina) = &snippet.page_title {
91 println!("{}", format!("## {}", titulo_pagina).green().bold());
92 }
93
94 if let Some(titulo_codigo) = &snippet.code_title {
95 println!("{}", format!("▸ {}", titulo_codigo).cyan());
96 }
97
98 if let Some(descricao) = &snippet.code_description {
99 println!(" {}", descricao.dimmed().italic());
100 }
101
102 if let Some(blocos) = &snippet.code_list {
103 for bloco in blocos {
104 println!("```{}", bloco.language);
105 println!("{}", bloco.code);
106 println!("```");
107 }
108 }
109
110 if let Some(source) = &snippet.code_id {
111 println!("{}", source.blue().bold().dimmed());
112 }
113
114 println!();
115}
116
117pub fn exibir_chaves_mascaradas(chaves: &[ChaveArmazenada], mascarar: impl Fn(&str) -> String) {
121 println!(
122 "{}",
123 format!("{} {}", chaves.len(), t(Mensagem::ContadorChaves))
124 .green()
125 .bold()
126 );
127 println!("{}", "─".repeat(60).dimmed());
128
129 let rotulo_adicionada = match idioma_atual() {
130 Idioma::English => "added:",
131 Idioma::Portugues => "adicionada:",
132 };
133
134 for (i, chave) in chaves.iter().enumerate() {
135 println!(
136 " {} {} {}",
137 format!("[{}]", i + 1).cyan(),
138 mascarar(&chave.value).bold(),
139 format!(
140 "({} {})",
141 rotulo_adicionada,
142 formatar_added_at_display(&chave.added_at)
143 )
144 .dimmed()
145 );
146 }
147}
148
149pub fn formatar_added_at_display(iso: &str) -> String {
153 chrono::DateTime::parse_from_rfc3339(iso)
154 .map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
155 .unwrap_or_else(|_| iso.to_string())
156}
157
158pub fn exibir_nenhuma_chave() {
160 println!("{}", t(Mensagem::NenhumaChaveArmazenada).yellow());
161 println!("{}", t(Mensagem::UsarKeysAdd).cyan());
162}
163
164pub fn exibir_nenhuma_chave_para_remover() {
166 println!("{}", t(Mensagem::NenhumaChaveParaRemover).yellow());
167}
168
169pub fn exibir_indice_invalido(_indice: usize, total: usize) {
171 println!(
172 "{}",
173 format!("{} {}.", t(Mensagem::IndiceInvalido), total).red()
174 );
175}
176
177pub fn exibir_chave_adicionada(caminho: &std::path::Path) {
179 println!(
180 "{} {}",
181 t(Mensagem::ChaveAdicionada),
182 caminho.display().to_string().green()
183 );
184}
185
186pub fn exibir_chave_ja_existia() {
188 println!("{}", t(Mensagem::ChaveJaExistia).yellow());
189}
190
191pub fn exibir_chave_invalida_vazia() {
193 eprintln!("{}", t(Mensagem::ChaveVaziaOuInvalida).red());
194}
195
196pub fn exibir_aviso_formato_chave() {
198 eprintln!("{}", t(Mensagem::AvisoFormatoChave).yellow());
199}
200
201pub fn exibir_chave_removida(chave_mascarada: &str) {
203 println!(
204 "{} {}",
205 chave_mascarada.bold(),
206 t(Mensagem::ChaveRemovidaSucesso)
207 );
208}
209
210pub fn exibir_operacao_cancelada() {
212 println!("{}", t(Mensagem::OperacaoCancelada).yellow());
213}
214
215pub fn exibir_chaves_removidas() {
217 println!("{}", t(Mensagem::TodasChavesRemovidas).green());
218}
219
220pub fn exibir_xdg_nao_suportado() {
222 println!("{}", t(Mensagem::SistemaXdgNaoSuportado).red());
223}
224
225pub fn exibir_json_array_vazio() {
227 println!("[]");
228}
229
230pub fn exibir_json_bruto(json: &str) {
232 println!("{}", json);
233}
234
235pub fn exibir_caminho_config(caminho: &std::path::Path) {
237 println!("{}", caminho.display());
238}
239
240pub fn exibir_chave_exportada(valor: &str) {
242 println!("CONTEXT7_API={}", valor);
243}
244
245pub fn exibir_json_resultados(json: &str) {
247 println!("{}", json);
248}
249
250pub fn exibir_texto_plano(texto: &str) {
252 println!("{}", texto);
253}
254
255pub fn confirmar_clear() -> anyhow::Result<bool> {
259 use std::io::Write;
260 print!("{}", t(Mensagem::ConfirmarRemoverTodas));
261 std::io::stdout()
262 .flush()
263 .context("Falha ao limpar buffer de saída")?;
264
265 let mut entrada = String::new();
266 std::io::stdin()
267 .read_line(&mut entrada)
268 .context("Falha ao ler confirmação do usuário")?;
269
270 Ok(matches!(
271 entrada.trim().to_lowercase().as_str(),
272 "s" | "sim" | "y" | "yes"
273 ))
274}
275
276pub fn exibir_importacao_concluida(importadas: usize, total: usize) {
278 println!(
279 "{}",
280 format!(
281 "{}/{} {}",
282 importadas,
283 total,
284 t(Mensagem::ChavesImportadasSucesso)
285 )
286 .green()
287 );
288}
289
290#[cfg(test)]
291mod testes {
292 use super::formatar_added_at_display;
293
294 #[test]
295 fn testa_formatar_added_at_rfc3339_com_nanossegundos() {
296 let resultado = formatar_added_at_display("2026-04-09T13:34:59.060818734+00:00");
297 assert_eq!(resultado, "2026-04-09 13:34:59");
298 assert!(
299 !resultado.contains('T'),
300 "Resultado não deve conter 'T': {resultado}"
301 );
302 assert!(
303 !resultado.contains('.'),
304 "Resultado não deve conter nanossegundos: {resultado}"
305 );
306 assert!(
307 !resultado.contains("+00:00"),
308 "Resultado não deve conter offset de timezone: {resultado}"
309 );
310 }
311
312 #[test]
313 fn testa_formatar_added_at_rfc3339_sem_nanossegundos() {
314 let resultado = formatar_added_at_display("2026-01-01T00:00:00+00:00");
315 assert_eq!(resultado, "2026-01-01 00:00:00");
316 }
317
318 #[test]
319 fn testa_formatar_added_at_rfc3339_offset_nao_utc() {
320 let resultado = formatar_added_at_display("2026-04-09T10:00:00-03:00");
322 assert_eq!(resultado, "2026-04-09 10:00:00");
324 assert!(
326 !resultado.contains("-03:00"),
327 "Resultado não deve conter offset de timezone: {resultado}"
328 );
329 }
330
331 #[test]
332 fn testa_formatar_added_at_fallback_string_invalida() {
333 let resultado = formatar_added_at_display("lixo-nao-e-data");
334 assert_eq!(
335 resultado, "lixo-nao-e-data",
336 "String inválida deve ser retornada sem modificação"
337 );
338 }
339
340 #[test]
341 fn testa_formatar_added_at_string_vazia() {
342 let resultado = formatar_added_at_display("");
343 assert_eq!(
344 resultado, "",
345 "String vazia deve ser retornada sem modificação"
346 );
347 }
348
349 #[test]
350 fn testa_formatar_added_at_formato_saida_legivel() {
351 let resultado = formatar_added_at_display("2026-04-09T13:34:59.123456789+00:00");
352 assert_eq!(
354 resultado.len(),
355 19,
356 "Formato de saída deve ter 19 caracteres, obteve: '{resultado}'"
357 );
358 assert!(
359 resultado.contains(' '),
360 "Resultado deve conter espaço separando data e hora: {resultado}"
361 );
362 }
363}