1use std::io::IsTerminal;
7use std::sync::OnceLock;
8
9use anyhow::Context;
10use chrono::Utc;
11use colored::Colorize;
12use serde::Serialize;
13
14use crate::api::{DocumentationSnippet, LibrarySearchResult, RespostaDocumentacao};
15use crate::i18n::{idioma_atual, t, Idioma, Mensagem};
16use crate::storage::ChaveArmazenada;
17
18static SILENCIOSO: OnceLock<bool> = OnceLock::new();
21
22pub fn definir_silencioso(v: bool) {
26 let _ = SILENCIOSO.set(v);
27}
28
29fn stdout_permitido() -> bool {
30 !SILENCIOSO.get().copied().unwrap_or(false)
31}
32
33fn imprimir_linha(s: &str) {
34 if stdout_permitido() {
35 println!("{s}");
36 }
37}
38
39fn imprimir_vazio() {
40 if stdout_permitido() {
41 println!();
42 }
43}
44
45fn simbolo_ou_ascii<'a>(unicode: &'a str, ascii: &'a str) -> &'a str {
50 static USAR_ASCII: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
51 let usar_ascii = *USAR_ASCII.get_or_init(|| {
52 !std::io::stdout().is_terminal()
53 || std::env::var("NO_COLOR").is_ok()
54 || std::env::var("TERM").map(|t| t == "dumb").unwrap_or(false)
55 });
56 if usar_ascii {
57 ascii
58 } else {
59 unicode
60 }
61}
62
63#[derive(Serialize)]
69struct EventoNdjson<'a, T: Serialize> {
70 #[serde(rename = "type")]
71 tipo: &'a str,
72 timestamp: String,
73 #[serde(flatten)]
74 dados: T,
75}
76
77pub fn emitir_ndjson<T: Serialize>(tipo: &str, dados: &T) {
79 let evento = EventoNdjson {
80 tipo,
81 timestamp: Utc::now().to_rfc3339(),
82 dados,
83 };
84 if let Ok(json) = serde_json::to_string(&evento) {
85 imprimir_linha(&json);
86 }
87}
88
89pub fn imprimir_linha_health(s: &str) {
93 imprimir_linha(s);
94}
95
96pub fn simbolo_health(ok: bool) -> colored::ColoredString {
100 if ok {
101 simbolo_ou_ascii("✔", "[OK]").green()
102 } else {
103 simbolo_ou_ascii("✘", "[FAIL]").red()
104 }
105}
106
107pub fn exibir_bibliotecas_formatado(resultados: &[LibrarySearchResult]) {
114 if resultados.is_empty() {
115 imprimir_linha(
116 &t(Mensagem::NenhumaBibliotecaEncontrada)
117 .yellow()
118 .to_string(),
119 );
120 return;
121 }
122
123 imprimir_linha(
124 &t(Mensagem::BibliotecasEncontradas)
125 .green()
126 .bold()
127 .to_string(),
128 );
129 imprimir_linha(&simbolo_ou_ascii("─", "-").repeat(60).dimmed().to_string());
130
131 for (i, lib) in resultados.iter().enumerate() {
132 let numero = format!("{}.", i + 1);
133
134 let titulo = if let Some(score) = lib.trust_score {
136 format!(
137 "{} {} ({} {:.1}/10)",
138 numero.cyan(),
139 lib.title.bold(),
140 t(Mensagem::ConfiancaScore),
141 score
142 )
143 } else {
144 format!("{} {}", numero.cyan(), lib.title.bold())
145 };
146 imprimir_linha(&titulo);
147
148 imprimir_linha(&format!(" {}", lib.id.dimmed()));
150
151 if let Some(desc) = &lib.description {
152 imprimir_linha(&format!(" {}", desc.italic()));
153 }
154
155 imprimir_vazio();
156 }
157}
158
159pub fn exibir_dica_biblioteca_nao_encontrada() {
164 eprintln!("{}", t(Mensagem::BibliotecaNaoEncontradaApi).yellow());
165}
166
167pub fn exibir_documentacao_formatada(doc: &RespostaDocumentacao) {
173 let snippets = match &doc.snippets {
174 Some(s) if !s.is_empty() => s,
175 _ => {
176 imprimir_linha(
177 &t(Mensagem::NenhumaDocumentacaoEncontrada)
178 .yellow()
179 .to_string(),
180 );
181 return;
182 }
183 };
184
185 imprimir_linha(&t(Mensagem::TituloDocumentacao).green().bold().to_string());
186 imprimir_linha(&simbolo_ou_ascii("─", "-").repeat(60).dimmed().to_string());
187
188 for snippet in snippets {
189 exibir_snippet(snippet);
190 }
191}
192
193fn exibir_snippet(snippet: &DocumentationSnippet) {
197 if let Some(titulo_pagina) = &snippet.page_title {
198 imprimir_linha(&format!("## {}", titulo_pagina).green().bold().to_string());
199 }
200
201 if let Some(titulo_codigo) = &snippet.code_title {
202 imprimir_linha(
203 &format!("{} {}", simbolo_ou_ascii("▸", ">"), titulo_codigo)
204 .cyan()
205 .to_string(),
206 );
207 }
208
209 if let Some(descricao) = &snippet.code_description {
210 imprimir_linha(&format!(" {}", descricao.dimmed().italic()));
211 }
212
213 if let Some(blocos) = &snippet.code_list {
214 for bloco in blocos {
215 imprimir_linha(&format!("```{}", bloco.language));
216 imprimir_linha(&bloco.code);
217 imprimir_linha("```");
218 }
219 }
220
221 if let Some(source) = &snippet.code_id {
222 imprimir_linha(&source.blue().bold().dimmed().to_string());
223 }
224
225 imprimir_vazio();
226}
227
228pub fn exibir_chaves_mascaradas(chaves: &[ChaveArmazenada], mascarar: impl Fn(&str) -> String) {
232 imprimir_linha(
233 &format!("{} {}", chaves.len(), t(Mensagem::ContadorChaves))
234 .green()
235 .bold()
236 .to_string(),
237 );
238 imprimir_linha(&simbolo_ou_ascii("─", "-").repeat(60).dimmed().to_string());
239
240 let rotulo_adicionada = match idioma_atual() {
241 Idioma::English => "added:",
242 Idioma::Portugues => "adicionada:",
243 };
244
245 for (i, chave) in chaves.iter().enumerate() {
246 imprimir_linha(&format!(
247 " {} {} {}",
248 format!("[{}]", i + 1).cyan(),
249 mascarar(&chave.value).bold(),
250 format!(
251 "({} {})",
252 rotulo_adicionada,
253 formatar_added_at_display(&chave.added_at)
254 )
255 .dimmed()
256 ));
257 }
258}
259
260pub fn formatar_added_at_display(iso: &str) -> String {
264 chrono::DateTime::parse_from_rfc3339(iso)
265 .map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
266 .unwrap_or_else(|_| iso.to_string())
267}
268
269pub fn exibir_nenhuma_chave() {
271 imprimir_linha(&t(Mensagem::NenhumaChaveArmazenada).yellow().to_string());
272 imprimir_linha(&t(Mensagem::UsarKeysAdd).cyan().to_string());
273}
274
275pub fn exibir_nenhuma_chave_para_remover() {
277 imprimir_linha(&t(Mensagem::NenhumaChaveParaRemover).yellow().to_string());
278}
279
280pub fn exibir_indice_invalido(_indice: usize, total: usize) {
282 imprimir_linha(
283 &format!("{} {}.", t(Mensagem::IndiceInvalido), total)
284 .red()
285 .to_string(),
286 );
287}
288
289pub fn exibir_chave_adicionada(caminho: &std::path::Path) {
291 imprimir_linha(&format!(
292 "{} {}",
293 t(Mensagem::ChaveAdicionada),
294 caminho.display().to_string().green()
295 ));
296}
297
298pub fn exibir_chave_ja_existia() {
300 imprimir_linha(&t(Mensagem::ChaveJaExistia).yellow().to_string());
301}
302
303pub fn exibir_chave_invalida_vazia() {
305 eprintln!("{}", t(Mensagem::ChaveVaziaOuInvalida).red());
306}
307
308pub fn exibir_aviso_formato_chave() {
310 eprintln!("{}", t(Mensagem::AvisoFormatoChave).yellow());
311}
312
313pub fn exibir_chave_removida(chave_mascarada: &str) {
315 imprimir_linha(&format!(
316 "{} {}",
317 chave_mascarada.bold(),
318 t(Mensagem::ChaveRemovidaSucesso)
319 ));
320}
321
322pub fn exibir_operacao_cancelada() {
324 imprimir_linha(&t(Mensagem::OperacaoCancelada).yellow().to_string());
325}
326
327pub fn exibir_chaves_removidas() {
329 imprimir_linha(&t(Mensagem::TodasChavesRemovidas).green().to_string());
330}
331
332pub fn exibir_xdg_nao_suportado() {
334 imprimir_linha(&t(Mensagem::SistemaXdgNaoSuportado).red().to_string());
335}
336
337pub fn exibir_json_array_vazio() {
339 imprimir_linha("[]");
340}
341
342pub fn exibir_json_bruto(json: &str) {
344 imprimir_linha(json);
345}
346
347pub fn exibir_caminho_config(caminho: &std::path::Path) {
349 imprimir_linha(&caminho.display().to_string());
350}
351
352pub fn exibir_chave_exportada(valor: &str) {
354 imprimir_linha(&format!("CONTEXT7_API={}", valor));
355}
356
357pub fn exibir_json_resultados(json: &str) {
359 imprimir_linha(json);
360}
361
362pub fn exibir_texto_plano(texto: &str) {
364 imprimir_linha(texto);
365}
366
367pub fn confirmar_clear() -> anyhow::Result<bool> {
371 use std::io::Write;
372 if stdout_permitido() {
373 print!("{}", t(Mensagem::ConfirmarRemoverTodas));
374 std::io::stdout()
375 .flush()
376 .context("Falha ao limpar buffer de saída")?;
377 }
378
379 let mut entrada = String::new();
380 std::io::stdin()
381 .read_line(&mut entrada)
382 .context("Falha ao ler confirmação do usuário")?;
383
384 Ok(matches!(
385 entrada.trim().to_lowercase().as_str(),
386 "s" | "sim" | "y" | "yes"
387 ))
388}
389
390pub fn exibir_importacao_concluida(importadas: usize, total: usize) {
392 imprimir_linha(
393 &format!(
394 "{}/{} {}",
395 importadas,
396 total,
397 t(Mensagem::ChavesImportadasSucesso)
398 )
399 .green()
400 .to_string(),
401 );
402}
403
404#[cfg(test)]
405mod testes {
406 use super::formatar_added_at_display;
407
408 #[test]
409 fn testa_formatar_added_at_rfc3339_com_nanossegundos() {
410 let resultado = formatar_added_at_display("2026-04-09T13:34:59.060818734+00:00");
411 assert_eq!(resultado, "2026-04-09 13:34:59");
412 assert!(
413 !resultado.contains('T'),
414 "Resultado não deve conter 'T': {resultado}"
415 );
416 assert!(
417 !resultado.contains('.'),
418 "Resultado não deve conter nanossegundos: {resultado}"
419 );
420 assert!(
421 !resultado.contains("+00:00"),
422 "Resultado não deve conter offset de timezone: {resultado}"
423 );
424 }
425
426 #[test]
427 fn testa_formatar_added_at_rfc3339_sem_nanossegundos() {
428 let resultado = formatar_added_at_display("2026-01-01T00:00:00+00:00");
429 assert_eq!(resultado, "2026-01-01 00:00:00");
430 }
431
432 #[test]
433 fn testa_formatar_added_at_rfc3339_offset_nao_utc() {
434 let resultado = formatar_added_at_display("2026-04-09T10:00:00-03:00");
436 assert_eq!(resultado, "2026-04-09 10:00:00");
438 assert!(
440 !resultado.contains("-03:00"),
441 "Resultado não deve conter offset de timezone: {resultado}"
442 );
443 }
444
445 #[test]
446 fn testa_formatar_added_at_fallback_string_invalida() {
447 let resultado = formatar_added_at_display("lixo-nao-e-data");
448 assert_eq!(
449 resultado, "lixo-nao-e-data",
450 "String inválida deve ser retornada sem modificação"
451 );
452 }
453
454 #[test]
455 fn testa_formatar_added_at_string_vazia() {
456 let resultado = formatar_added_at_display("");
457 assert_eq!(
458 resultado, "",
459 "String vazia deve ser retornada sem modificação"
460 );
461 }
462
463 #[test]
464 fn testa_formatar_added_at_formato_saida_legivel() {
465 let resultado = formatar_added_at_display("2026-04-09T13:34:59.123456789+00:00");
466 assert_eq!(
468 resultado.len(),
469 19,
470 "Formato de saída deve ter 19 caracteres, obteve: '{resultado}'"
471 );
472 assert!(
473 resultado.contains(' '),
474 "Resultado deve conter espaço separando data e hora: {resultado}"
475 );
476 }
477}