1use std::io::IsTerminal;
7
8use anyhow::Context;
9use chrono::Utc;
10use colored::Colorize;
11use serde::Serialize;
12
13use crate::api::{DocumentationSnippet, LibrarySearchResult, RespostaDocumentacao};
14use crate::i18n::{idioma_atual, t, Idioma, Mensagem};
15use crate::storage::ChaveArmazenada;
16
17fn simbolo_ou_ascii<'a>(unicode: &'a str, ascii: &'a str) -> &'a str {
22 static USAR_ASCII: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
23 let usar_ascii = *USAR_ASCII.get_or_init(|| {
24 !std::io::stdout().is_terminal()
25 || std::env::var("NO_COLOR").is_ok()
26 || std::env::var("TERM").map(|t| t == "dumb").unwrap_or(false)
27 });
28 if usar_ascii {
29 ascii
30 } else {
31 unicode
32 }
33}
34
35#[derive(Serialize)]
41struct EventoNdjson<'a, T: Serialize> {
42 #[serde(rename = "type")]
43 tipo: &'a str,
44 timestamp: String,
45 #[serde(flatten)]
46 dados: T,
47}
48
49pub fn emitir_ndjson<T: Serialize>(tipo: &str, dados: &T) {
51 let evento = EventoNdjson {
52 tipo,
53 timestamp: Utc::now().to_rfc3339(),
54 dados,
55 };
56 if let Ok(json) = serde_json::to_string(&evento) {
57 println!("{json}");
58 }
59}
60
61pub fn exibir_bibliotecas_formatado(resultados: &[LibrarySearchResult]) {
68 if resultados.is_empty() {
69 println!("{}", t(Mensagem::NenhumaBibliotecaEncontrada).yellow());
70 return;
71 }
72
73 println!("{}", t(Mensagem::BibliotecasEncontradas).green().bold());
74 println!("{}", simbolo_ou_ascii("─", "-").repeat(60).dimmed());
75
76 for (i, lib) in resultados.iter().enumerate() {
77 let numero = format!("{}.", i + 1);
78
79 let titulo = if let Some(score) = lib.trust_score {
81 format!(
82 "{} {} ({} {:.1}/10)",
83 numero.cyan(),
84 lib.title.bold(),
85 t(Mensagem::ConfiancaScore),
86 score
87 )
88 } else {
89 format!("{} {}", numero.cyan(), lib.title.bold())
90 };
91 println!("{}", titulo);
92
93 println!(" {}", lib.id.dimmed());
95
96 if let Some(desc) = &lib.description {
97 println!(" {}", desc.italic());
98 }
99
100 println!();
101 }
102}
103
104pub fn exibir_dica_biblioteca_nao_encontrada() {
109 eprintln!("{}", t(Mensagem::BibliotecaNaoEncontradaApi).yellow());
110}
111
112pub fn exibir_documentacao_formatada(doc: &RespostaDocumentacao) {
118 let snippets = match &doc.snippets {
119 Some(s) if !s.is_empty() => s,
120 _ => {
121 println!("{}", t(Mensagem::NenhumaDocumentacaoEncontrada).yellow());
122 return;
123 }
124 };
125
126 println!("{}", t(Mensagem::TituloDocumentacao).green().bold());
127 println!("{}", simbolo_ou_ascii("─", "-").repeat(60).dimmed());
128
129 for snippet in snippets {
130 exibir_snippet(snippet);
131 }
132}
133
134fn exibir_snippet(snippet: &DocumentationSnippet) {
138 if let Some(titulo_pagina) = &snippet.page_title {
139 println!("{}", format!("## {}", titulo_pagina).green().bold());
140 }
141
142 if let Some(titulo_codigo) = &snippet.code_title {
143 println!(
144 "{}",
145 format!("{} {}", simbolo_ou_ascii("▸", ">"), titulo_codigo).cyan()
146 );
147 }
148
149 if let Some(descricao) = &snippet.code_description {
150 println!(" {}", descricao.dimmed().italic());
151 }
152
153 if let Some(blocos) = &snippet.code_list {
154 for bloco in blocos {
155 println!("```{}", bloco.language);
156 println!("{}", bloco.code);
157 println!("```");
158 }
159 }
160
161 if let Some(source) = &snippet.code_id {
162 println!("{}", source.blue().bold().dimmed());
163 }
164
165 println!();
166}
167
168pub fn exibir_chaves_mascaradas(chaves: &[ChaveArmazenada], mascarar: impl Fn(&str) -> String) {
172 println!(
173 "{}",
174 format!("{} {}", chaves.len(), t(Mensagem::ContadorChaves))
175 .green()
176 .bold()
177 );
178 println!("{}", simbolo_ou_ascii("─", "-").repeat(60).dimmed());
179
180 let rotulo_adicionada = match idioma_atual() {
181 Idioma::English => "added:",
182 Idioma::Portugues => "adicionada:",
183 };
184
185 for (i, chave) in chaves.iter().enumerate() {
186 println!(
187 " {} {} {}",
188 format!("[{}]", i + 1).cyan(),
189 mascarar(&chave.value).bold(),
190 format!(
191 "({} {})",
192 rotulo_adicionada,
193 formatar_added_at_display(&chave.added_at)
194 )
195 .dimmed()
196 );
197 }
198}
199
200pub fn formatar_added_at_display(iso: &str) -> String {
204 chrono::DateTime::parse_from_rfc3339(iso)
205 .map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
206 .unwrap_or_else(|_| iso.to_string())
207}
208
209pub fn exibir_nenhuma_chave() {
211 println!("{}", t(Mensagem::NenhumaChaveArmazenada).yellow());
212 println!("{}", t(Mensagem::UsarKeysAdd).cyan());
213}
214
215pub fn exibir_nenhuma_chave_para_remover() {
217 println!("{}", t(Mensagem::NenhumaChaveParaRemover).yellow());
218}
219
220pub fn exibir_indice_invalido(_indice: usize, total: usize) {
222 println!(
223 "{}",
224 format!("{} {}.", t(Mensagem::IndiceInvalido), total).red()
225 );
226}
227
228pub fn exibir_chave_adicionada(caminho: &std::path::Path) {
230 println!(
231 "{} {}",
232 t(Mensagem::ChaveAdicionada),
233 caminho.display().to_string().green()
234 );
235}
236
237pub fn exibir_chave_ja_existia() {
239 println!("{}", t(Mensagem::ChaveJaExistia).yellow());
240}
241
242pub fn exibir_chave_invalida_vazia() {
244 eprintln!("{}", t(Mensagem::ChaveVaziaOuInvalida).red());
245}
246
247pub fn exibir_aviso_formato_chave() {
249 eprintln!("{}", t(Mensagem::AvisoFormatoChave).yellow());
250}
251
252pub fn exibir_chave_removida(chave_mascarada: &str) {
254 println!(
255 "{} {}",
256 chave_mascarada.bold(),
257 t(Mensagem::ChaveRemovidaSucesso)
258 );
259}
260
261pub fn exibir_operacao_cancelada() {
263 println!("{}", t(Mensagem::OperacaoCancelada).yellow());
264}
265
266pub fn exibir_chaves_removidas() {
268 println!("{}", t(Mensagem::TodasChavesRemovidas).green());
269}
270
271pub fn exibir_xdg_nao_suportado() {
273 println!("{}", t(Mensagem::SistemaXdgNaoSuportado).red());
274}
275
276pub fn exibir_json_array_vazio() {
278 println!("[]");
279}
280
281pub fn exibir_json_bruto(json: &str) {
283 println!("{}", json);
284}
285
286pub fn exibir_caminho_config(caminho: &std::path::Path) {
288 println!("{}", caminho.display());
289}
290
291pub fn exibir_chave_exportada(valor: &str) {
293 println!("CONTEXT7_API={}", valor);
294}
295
296pub fn exibir_json_resultados(json: &str) {
298 println!("{}", json);
299}
300
301pub fn exibir_texto_plano(texto: &str) {
303 println!("{}", texto);
304}
305
306pub fn confirmar_clear() -> anyhow::Result<bool> {
310 use std::io::Write;
311 print!("{}", t(Mensagem::ConfirmarRemoverTodas));
312 std::io::stdout()
313 .flush()
314 .context("Falha ao limpar buffer de saída")?;
315
316 let mut entrada = String::new();
317 std::io::stdin()
318 .read_line(&mut entrada)
319 .context("Falha ao ler confirmação do usuário")?;
320
321 Ok(matches!(
322 entrada.trim().to_lowercase().as_str(),
323 "s" | "sim" | "y" | "yes"
324 ))
325}
326
327pub fn exibir_importacao_concluida(importadas: usize, total: usize) {
329 println!(
330 "{}",
331 format!(
332 "{}/{} {}",
333 importadas,
334 total,
335 t(Mensagem::ChavesImportadasSucesso)
336 )
337 .green()
338 );
339}
340
341#[cfg(test)]
342mod testes {
343 use super::formatar_added_at_display;
344
345 #[test]
346 fn testa_formatar_added_at_rfc3339_com_nanossegundos() {
347 let resultado = formatar_added_at_display("2026-04-09T13:34:59.060818734+00:00");
348 assert_eq!(resultado, "2026-04-09 13:34:59");
349 assert!(
350 !resultado.contains('T'),
351 "Resultado não deve conter 'T': {resultado}"
352 );
353 assert!(
354 !resultado.contains('.'),
355 "Resultado não deve conter nanossegundos: {resultado}"
356 );
357 assert!(
358 !resultado.contains("+00:00"),
359 "Resultado não deve conter offset de timezone: {resultado}"
360 );
361 }
362
363 #[test]
364 fn testa_formatar_added_at_rfc3339_sem_nanossegundos() {
365 let resultado = formatar_added_at_display("2026-01-01T00:00:00+00:00");
366 assert_eq!(resultado, "2026-01-01 00:00:00");
367 }
368
369 #[test]
370 fn testa_formatar_added_at_rfc3339_offset_nao_utc() {
371 let resultado = formatar_added_at_display("2026-04-09T10:00:00-03:00");
373 assert_eq!(resultado, "2026-04-09 10:00:00");
375 assert!(
377 !resultado.contains("-03:00"),
378 "Resultado não deve conter offset de timezone: {resultado}"
379 );
380 }
381
382 #[test]
383 fn testa_formatar_added_at_fallback_string_invalida() {
384 let resultado = formatar_added_at_display("lixo-nao-e-data");
385 assert_eq!(
386 resultado, "lixo-nao-e-data",
387 "String inválida deve ser retornada sem modificação"
388 );
389 }
390
391 #[test]
392 fn testa_formatar_added_at_string_vazia() {
393 let resultado = formatar_added_at_display("");
394 assert_eq!(
395 resultado, "",
396 "String vazia deve ser retornada sem modificação"
397 );
398 }
399
400 #[test]
401 fn testa_formatar_added_at_formato_saida_legivel() {
402 let resultado = formatar_added_at_display("2026-04-09T13:34:59.123456789+00:00");
403 assert_eq!(
405 resultado.len(),
406 19,
407 "Formato de saída deve ter 19 caracteres, obteve: '{resultado}'"
408 );
409 assert!(
410 resultado.contains(' '),
411 "Resultado deve conter espaço separando data e hora: {resultado}"
412 );
413 }
414}