Skip to main content

cargo_shear/
output.rs

1use std::{io, io::IsTerminal, str::FromStr};
2
3use crate::{diagnostics::ShearAnalysis, output::miette::MietteRenderer};
4
5pub mod github;
6pub mod json;
7pub mod miette;
8
9/// Picks the renderer used for diagnostics on stdout.
10#[derive(Debug, Clone, Copy, Default)]
11#[non_exhaustive]
12pub enum OutputFormat {
13    /// Miette renderer with colors and unicode (resolves to `GitHub` under CI).
14    #[default]
15    Auto,
16
17    /// Newline-terminated JSON for machine consumers.
18    Json,
19
20    /// `::error file=...` workflow commands consumed by GitHub Actions.
21    GitHub,
22}
23
24impl OutputFormat {
25    /// Resolve `Auto` against the environment.
26    ///
27    /// When running in GitHub Actions (the `GITHUB_ACTIONS` env var is set),
28    /// `Auto` switches to `GitHub` so failures show up as PR annotations.
29    /// Otherwise it stays as `Auto` (the miette renderer).
30    #[must_use]
31    pub fn resolve(self) -> Self {
32        if matches!(self, Self::Auto) && std::env::var_os("GITHUB_ACTIONS").is_some() {
33            Self::GitHub
34        } else {
35            self
36        }
37    }
38}
39
40impl FromStr for OutputFormat {
41    type Err = String;
42
43    fn from_str(s: &str) -> Result<Self, Self::Err> {
44        match s.to_lowercase().as_str() {
45            "auto" => Ok(Self::Auto),
46            "json" => Ok(Self::Json),
47            "github" => Ok(Self::GitHub),
48            _ => Err(format!("unknown format: {s}, expected: auto, json, github")),
49        }
50    }
51}
52
53/// Whether the terminal renderer should emit ANSI colors.
54#[derive(Debug, Clone, Copy, Default)]
55#[non_exhaustive]
56pub enum ColorMode {
57    /// Honour `NO_COLOR` and only colourise when stdout is a TTY.
58    #[default]
59    Auto,
60
61    /// Always emit colors, even when piped.
62    Always,
63
64    /// Strip all colors from the output.
65    Never,
66}
67
68impl ColorMode {
69    /// Resolve to a concrete `bool` based on the mode plus the environment.
70    #[must_use]
71    pub fn enabled(self) -> bool {
72        match self {
73            Self::Always => true,
74            Self::Never => false,
75            Self::Auto => {
76                if std::env::var_os("NO_COLOR").is_some() {
77                    return false;
78                }
79
80                std::io::stdout().is_terminal()
81            }
82        }
83    }
84}
85
86impl FromStr for ColorMode {
87    type Err = String;
88
89    fn from_str(s: &str) -> Result<Self, Self::Err> {
90        match s.to_lowercase().as_str() {
91            "auto" => Ok(Self::Auto),
92            "always" => Ok(Self::Always),
93            "never" => Ok(Self::Never),
94            _ => Err(format!("unknown color option: {s}, expected one of: auto, always, never")),
95        }
96    }
97}
98
99pub struct Renderer<W> {
100    writer: W,
101    format: OutputFormat,
102    color: bool,
103}
104
105impl<W: io::Write> Renderer<W> {
106    pub const fn new(writer: W, format: OutputFormat, color: bool) -> Self {
107        Self { writer, format, color }
108    }
109
110    pub fn render(&mut self, analysis: &ShearAnalysis) -> io::Result<()> {
111        match self.format {
112            OutputFormat::Auto => {
113                let mut renderer = MietteRenderer::new(&mut self.writer, self.color);
114                renderer.render(analysis)
115            }
116            OutputFormat::Json => {
117                let mut renderer = json::JsonRenderer::new(&mut self.writer);
118                renderer.render(analysis)
119            }
120            OutputFormat::GitHub => {
121                let mut renderer = github::GitHubRenderer::new(&mut self.writer);
122                renderer.render(analysis)
123            }
124        }
125    }
126}