use std::io::IsTerminal;
use std::sync::OnceLock;
use anyhow::Context;
use chrono::Utc;
use colored::Colorize;
use serde::Serialize;
use crate::api::{DocumentationSnippet, LibrarySearchResult, RespostaDocumentacao};
use crate::i18n::{idioma_atual, t, Idioma, Mensagem};
use crate::storage::ChaveArmazenada;
static SILENCIOSO: OnceLock<bool> = OnceLock::new();
pub fn definir_silencioso(v: bool) {
let _ = SILENCIOSO.set(v);
}
fn stdout_permitido() -> bool {
!SILENCIOSO.get().copied().unwrap_or(false)
}
fn imprimir_linha(s: &str) {
if stdout_permitido() {
println!("{s}");
}
}
fn imprimir_vazio() {
if stdout_permitido() {
println!();
}
}
fn simbolo_ou_ascii<'a>(unicode: &'a str, ascii: &'a str) -> &'a str {
static USAR_ASCII: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
let usar_ascii = *USAR_ASCII.get_or_init(|| {
!std::io::stdout().is_terminal()
|| std::env::var("NO_COLOR").is_ok()
|| std::env::var("TERM").map(|t| t == "dumb").unwrap_or(false)
});
if usar_ascii {
ascii
} else {
unicode
}
}
#[derive(Serialize)]
struct EventoNdjson<'a, T: Serialize> {
#[serde(rename = "type")]
tipo: &'a str,
timestamp: String,
#[serde(flatten)]
dados: T,
}
pub fn emitir_ndjson<T: Serialize>(tipo: &str, dados: &T) {
let evento = EventoNdjson {
tipo,
timestamp: Utc::now().to_rfc3339(),
dados,
};
if let Ok(json) = serde_json::to_string(&evento) {
imprimir_linha(&json);
}
}
pub fn imprimir_linha_health(s: &str) {
imprimir_linha(s);
}
pub fn simbolo_health(ok: bool) -> colored::ColoredString {
if ok {
simbolo_ou_ascii("✔", "[OK]").green()
} else {
simbolo_ou_ascii("✘", "[FAIL]").red()
}
}
pub fn exibir_bibliotecas_formatado(resultados: &[LibrarySearchResult]) {
if resultados.is_empty() {
imprimir_linha(
&t(Mensagem::NenhumaBibliotecaEncontrada)
.yellow()
.to_string(),
);
return;
}
imprimir_linha(
&t(Mensagem::BibliotecasEncontradas)
.green()
.bold()
.to_string(),
);
imprimir_linha(&simbolo_ou_ascii("─", "-").repeat(60).dimmed().to_string());
for (i, lib) in resultados.iter().enumerate() {
let numero = format!("{}.", i + 1);
let titulo = if let Some(score) = lib.trust_score {
format!(
"{} {} ({} {:.1}/10)",
numero.cyan(),
lib.title.bold(),
t(Mensagem::ConfiancaScore),
score
)
} else {
format!("{} {}", numero.cyan(), lib.title.bold())
};
imprimir_linha(&titulo);
imprimir_linha(&format!(" {}", lib.id.dimmed()));
if let Some(desc) = &lib.description {
imprimir_linha(&format!(" {}", desc.italic()));
}
imprimir_vazio();
}
}
pub fn exibir_dica_biblioteca_nao_encontrada() {
eprintln!("{}", t(Mensagem::BibliotecaNaoEncontradaApi).yellow());
}
pub fn exibir_documentacao_formatada(doc: &RespostaDocumentacao) {
let snippets = match &doc.snippets {
Some(s) if !s.is_empty() => s,
_ => {
imprimir_linha(
&t(Mensagem::NenhumaDocumentacaoEncontrada)
.yellow()
.to_string(),
);
return;
}
};
imprimir_linha(&t(Mensagem::TituloDocumentacao).green().bold().to_string());
imprimir_linha(&simbolo_ou_ascii("─", "-").repeat(60).dimmed().to_string());
for snippet in snippets {
exibir_snippet(snippet);
}
}
fn exibir_snippet(snippet: &DocumentationSnippet) {
if let Some(titulo_pagina) = &snippet.page_title {
imprimir_linha(&format!("## {}", titulo_pagina).green().bold().to_string());
}
if let Some(titulo_codigo) = &snippet.code_title {
imprimir_linha(
&format!("{} {}", simbolo_ou_ascii("▸", ">"), titulo_codigo)
.cyan()
.to_string(),
);
}
if let Some(descricao) = &snippet.code_description {
imprimir_linha(&format!(" {}", descricao.dimmed().italic()));
}
if let Some(blocos) = &snippet.code_list {
for bloco in blocos {
imprimir_linha(&format!("```{}", bloco.language));
imprimir_linha(&bloco.code);
imprimir_linha("```");
}
}
if let Some(source) = &snippet.code_id {
imprimir_linha(&source.blue().bold().dimmed().to_string());
}
imprimir_vazio();
}
pub fn exibir_chaves_mascaradas(chaves: &[ChaveArmazenada], mascarar: impl Fn(&str) -> String) {
imprimir_linha(
&format!("{} {}", chaves.len(), t(Mensagem::ContadorChaves))
.green()
.bold()
.to_string(),
);
imprimir_linha(&simbolo_ou_ascii("─", "-").repeat(60).dimmed().to_string());
let rotulo_adicionada = match idioma_atual() {
Idioma::English => "added:",
Idioma::Portugues => "adicionada:",
};
for (i, chave) in chaves.iter().enumerate() {
imprimir_linha(&format!(
" {} {} {}",
format!("[{}]", i + 1).cyan(),
mascarar(&chave.value).bold(),
format!(
"({} {})",
rotulo_adicionada,
formatar_added_at_display(&chave.added_at)
)
.dimmed()
));
}
}
pub fn formatar_added_at_display(iso: &str) -> String {
chrono::DateTime::parse_from_rfc3339(iso)
.map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
.unwrap_or_else(|_| iso.to_string())
}
pub fn exibir_nenhuma_chave() {
imprimir_linha(&t(Mensagem::NenhumaChaveArmazenada).yellow().to_string());
imprimir_linha(&t(Mensagem::UsarKeysAdd).cyan().to_string());
}
pub fn exibir_nenhuma_chave_para_remover() {
imprimir_linha(&t(Mensagem::NenhumaChaveParaRemover).yellow().to_string());
}
pub fn exibir_indice_invalido(_indice: usize, total: usize) {
imprimir_linha(
&format!("{} {}.", t(Mensagem::IndiceInvalido), total)
.red()
.to_string(),
);
}
pub fn exibir_chave_adicionada(caminho: &std::path::Path) {
imprimir_linha(&format!(
"{} {}",
t(Mensagem::ChaveAdicionada),
caminho.display().to_string().green()
));
}
pub fn exibir_chave_ja_existia() {
imprimir_linha(&t(Mensagem::ChaveJaExistia).yellow().to_string());
}
pub fn exibir_chave_invalida_vazia() {
eprintln!("{}", t(Mensagem::ChaveVaziaOuInvalida).red());
}
pub fn exibir_aviso_formato_chave() {
eprintln!("{}", t(Mensagem::AvisoFormatoChave).yellow());
}
pub fn exibir_chave_removida(chave_mascarada: &str) {
imprimir_linha(&format!(
"{} {}",
chave_mascarada.bold(),
t(Mensagem::ChaveRemovidaSucesso)
));
}
pub fn exibir_operacao_cancelada() {
imprimir_linha(&t(Mensagem::OperacaoCancelada).yellow().to_string());
}
pub fn exibir_chaves_removidas() {
imprimir_linha(&t(Mensagem::TodasChavesRemovidas).green().to_string());
}
pub fn exibir_xdg_nao_suportado() {
imprimir_linha(&t(Mensagem::SistemaXdgNaoSuportado).red().to_string());
}
pub fn exibir_json_array_vazio() {
imprimir_linha("[]");
}
pub fn exibir_json_bruto(json: &str) {
imprimir_linha(json);
}
pub fn exibir_caminho_config(caminho: &std::path::Path) {
imprimir_linha(&caminho.display().to_string());
}
pub fn exibir_chave_exportada(valor: &str) {
imprimir_linha(&format!("CONTEXT7_API={}", valor));
}
pub fn exibir_json_resultados(json: &str) {
imprimir_linha(json);
}
pub fn exibir_texto_plano(texto: &str) {
imprimir_linha(texto);
}
pub fn confirmar_clear() -> anyhow::Result<bool> {
use std::io::Write;
if stdout_permitido() {
print!("{}", t(Mensagem::ConfirmarRemoverTodas));
std::io::stdout()
.flush()
.context("Falha ao limpar buffer de saída")?;
}
let mut entrada = String::new();
std::io::stdin()
.read_line(&mut entrada)
.context("Falha ao ler confirmação do usuário")?;
Ok(matches!(
entrada.trim().to_lowercase().as_str(),
"s" | "sim" | "y" | "yes"
))
}
pub fn exibir_importacao_concluida(importadas: usize, total: usize) {
imprimir_linha(
&format!(
"{}/{} {}",
importadas,
total,
t(Mensagem::ChavesImportadasSucesso)
)
.green()
.to_string(),
);
}
#[cfg(test)]
mod testes {
use super::formatar_added_at_display;
#[test]
fn testa_formatar_added_at_rfc3339_com_nanossegundos() {
let resultado = formatar_added_at_display("2026-04-09T13:34:59.060818734+00:00");
assert_eq!(resultado, "2026-04-09 13:34:59");
assert!(
!resultado.contains('T'),
"Resultado não deve conter 'T': {resultado}"
);
assert!(
!resultado.contains('.'),
"Resultado não deve conter nanossegundos: {resultado}"
);
assert!(
!resultado.contains("+00:00"),
"Resultado não deve conter offset de timezone: {resultado}"
);
}
#[test]
fn testa_formatar_added_at_rfc3339_sem_nanossegundos() {
let resultado = formatar_added_at_display("2026-01-01T00:00:00+00:00");
assert_eq!(resultado, "2026-01-01 00:00:00");
}
#[test]
fn testa_formatar_added_at_rfc3339_offset_nao_utc() {
let resultado = formatar_added_at_display("2026-04-09T10:00:00-03:00");
assert_eq!(resultado, "2026-04-09 10:00:00");
assert!(
!resultado.contains("-03:00"),
"Resultado não deve conter offset de timezone: {resultado}"
);
}
#[test]
fn testa_formatar_added_at_fallback_string_invalida() {
let resultado = formatar_added_at_display("lixo-nao-e-data");
assert_eq!(
resultado, "lixo-nao-e-data",
"String inválida deve ser retornada sem modificação"
);
}
#[test]
fn testa_formatar_added_at_string_vazia() {
let resultado = formatar_added_at_display("");
assert_eq!(
resultado, "",
"String vazia deve ser retornada sem modificação"
);
}
#[test]
fn testa_formatar_added_at_formato_saida_legivel() {
let resultado = formatar_added_at_display("2026-04-09T13:34:59.123456789+00:00");
assert_eq!(
resultado.len(),
19,
"Formato de saída deve ter 19 caracteres, obteve: '{resultado}'"
);
assert!(
resultado.contains(' '),
"Resultado deve conter espaço separando data e hora: {resultado}"
);
}
}