Skip to main content

ssh_cli/
terminal.rs

1//! Configuração de output colorido e detecção de terminal interativo.
2//!
3//! Gerencia a escolha de cores via `termcolor` respeitando a precedência:
4//! 1. Flag `--no-color` da CLI (maior prioridade).
5//! 2. Variável de ambiente `NO_COLOR` (padrão <https://no-color.org>).
6//! 3. Variável de ambiente `CLICOLOR_FORCE=1` (forçar cores mesmo sem TTY).
7//! 4. Detecção de TTY (cores apenas se stdout for terminal interativo).
8//! 5. Fallback: sem cor.
9
10use anyhow::Result;
11use std::sync::OnceLock;
12use termcolor::ColorChoice;
13
14/// Cache da escolha de cor (definida uma vez na inicialização).
15static COR_CACHE: OnceLock<ColorChoice> = OnceLock::new();
16
17/// Inicializa a configuração de cor do terminal.
18///
19/// Deve ser chamada uma única vez após o parsing dos argumentos CLI.
20/// O parâmetro `sem_cor` corresponde à flag `--no-color` da CLI.
21pub fn inicializar(sem_cor: bool) -> Result<()> {
22    let escolha = determinar_cor(sem_cor);
23    let _ = COR_CACHE.set(escolha);
24    tracing::debug!("configuração de cor do terminal: {:?}", escolha);
25    Ok(())
26}
27
28/// Retorna a escolha de cor configurada.
29///
30/// Se [`inicializar`] não foi chamada, retorna [`ColorChoice::Never`] como
31/// fallback seguro.
32#[must_use]
33pub fn cor_escolha() -> ColorChoice {
34    *COR_CACHE.get().unwrap_or(&ColorChoice::Never)
35}
36
37/// Retorna `true` se o processo está rodando em um terminal interativo (TTY).
38///
39/// Usa [`std::io::IsTerminal`] (estabilizado no Rust 1.70) para detecção
40/// cross-platform sem dependências externas.
41#[must_use]
42pub fn e_interativo() -> bool {
43    use std::io::IsTerminal;
44
45    // Se TERM=dumb, não é interativo independente do TTY
46    if std::env::var("TERM").as_deref() == Ok("dumb") {
47        return false;
48    }
49
50    std::io::stdout().is_terminal()
51}
52
53/// Determina a escolha de cor com base nas regras de precedência.
54fn determinar_cor(sem_cor_cli: bool) -> ColorChoice {
55    // 1. Flag --no-color da CLI (maior prioridade)
56    if sem_cor_cli {
57        return ColorChoice::Never;
58    }
59
60    // 2. Variável de ambiente NO_COLOR (qualquer valor)
61    if std::env::var("NO_COLOR").is_ok() {
62        return ColorChoice::Never;
63    }
64
65    // 3. CLICOLOR_FORCE=1 força cores mesmo sem TTY
66    if std::env::var("CLICOLOR_FORCE").as_deref() == Ok("1") {
67        return ColorChoice::Always;
68    }
69
70    // 4. Detecção de TTY: cores apenas em terminal interativo
71    if e_interativo() {
72        ColorChoice::Auto
73    } else {
74        ColorChoice::Never
75    }
76}
77
78#[cfg(test)]
79mod testes {
80    use super::*;
81
82    #[test]
83    fn sem_cor_cli_retorna_never() {
84        let escolha = determinar_cor(true);
85        assert!(matches!(escolha, ColorChoice::Never));
86    }
87
88    #[test]
89    fn no_color_env_retorna_never() {
90        // Salva e restaura o estado da variável de ambiente
91        let anterior = std::env::var("NO_COLOR").ok();
92        let anterior_force = std::env::var("CLICOLOR_FORCE").ok();
93
94        std::env::set_var("NO_COLOR", "1");
95        std::env::remove_var("CLICOLOR_FORCE");
96
97        let escolha = determinar_cor(false);
98        assert!(matches!(escolha, ColorChoice::Never));
99
100        // Restaura
101        match anterior {
102            Some(v) => std::env::set_var("NO_COLOR", v),
103            None => std::env::remove_var("NO_COLOR"),
104        }
105        match anterior_force {
106            Some(v) => std::env::set_var("CLICOLOR_FORCE", v),
107            None => std::env::remove_var("CLICOLOR_FORCE"),
108        }
109    }
110
111    #[test]
112    fn clicolor_force_retorna_always() {
113        let anterior = std::env::var("NO_COLOR").ok();
114        let anterior_force = std::env::var("CLICOLOR_FORCE").ok();
115
116        std::env::remove_var("NO_COLOR");
117        std::env::set_var("CLICOLOR_FORCE", "1");
118
119        let escolha = determinar_cor(false);
120        assert!(matches!(escolha, ColorChoice::Always));
121
122        // Restaura
123        match anterior {
124            Some(v) => std::env::set_var("NO_COLOR", v),
125            None => std::env::remove_var("NO_COLOR"),
126        }
127        match anterior_force {
128            Some(v) => std::env::set_var("CLICOLOR_FORCE", v),
129            None => std::env::remove_var("CLICOLOR_FORCE"),
130        }
131    }
132
133    #[test]
134    fn cor_escolha_retorna_never_sem_inicializar() {
135        // Sem inicializar, o fallback é Never
136        // NOTA: em testes paralelos o OnceLock pode já ter valor.
137        // Apenas verificamos que não panic.
138        let _ = cor_escolha();
139    }
140
141    #[test]
142    fn e_interativo_retorna_bool() {
143        // Apenas verifica que não panic
144        let _ = e_interativo();
145    }
146}