use std::fmt;
use std::io::Write;
use cabin_config::{
ConfigDiscoveryInputs, EffectiveConfig, discover_config_files, merge_loaded_files,
};
use cabin_core::{Verbosity, VerbosityEnvError};
pub(crate) fn discover_early_config_verbosity() -> EffectiveConfig {
let inputs = ConfigDiscoveryInputs::from_process(None);
match discover_config_files(&inputs) {
Ok(discovery) => merge_loaded_files(discovery.loaded_files),
Err(_) => EffectiveConfig::default(),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct CliVerbosity {
pub(crate) verbose_count: u8,
pub(crate) quiet: bool,
}
impl CliVerbosity {
fn into_verbosity(self) -> Option<Verbosity> {
if self.quiet {
return Some(Verbosity::Quiet);
}
if self.verbose_count > 0 {
return Some(Verbosity::from_verbose_count(self.verbose_count));
}
None
}
}
pub(crate) fn resolve_verbosity<F>(
cli: CliVerbosity,
env: F,
config: &EffectiveConfig,
) -> Result<Verbosity, VerbosityEnvError>
where
F: Fn(&str) -> Option<String>,
{
if let Some(level) = cli.into_verbosity() {
return Ok(level);
}
let env_quiet = read_bool_env(&env, cabin_env::CABIN_TERM_QUIET)?;
let env_verbose = read_bool_env(&env, cabin_env::CABIN_TERM_VERBOSE)?;
if env_quiet || env_verbose {
let combined =
Verbosity::from_config_pair(env_verbose.then_some(true), env_quiet.then_some(true))
.map_err(|_| VerbosityEnvError {
variable: cabin_env::CABIN_TERM_QUIET,
value: "1".to_owned(),
})?;
if let Some(level) = combined {
return Ok(level);
}
}
if let Some(setting) = &config.term.verbosity {
return Ok(setting.level);
}
Ok(Verbosity::default())
}
fn read_bool_env<F>(env: &F, key: &'static str) -> Result<bool, VerbosityEnvError>
where
F: Fn(&str) -> Option<String>,
{
match env(key) {
None => Ok(false),
Some(raw) => Verbosity::parse_bool_env(key, &raw),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Stream {
Stdout,
Stderr,
}
const COLUMN_WIDTH: usize = 12;
#[derive(Debug, Clone, Copy)]
pub(crate) struct Reporter {
verbosity: Verbosity,
styled: bool,
}
impl Reporter {
pub(crate) fn new(verbosity: Verbosity) -> Self {
Self {
verbosity,
styled: false,
}
}
pub(crate) fn with_color(verbosity: Verbosity, color: cabin_core::ColorChoice) -> Self {
let styled = match color {
cabin_core::ColorChoice::Always => true,
cabin_core::ColorChoice::Never => false,
cabin_core::ColorChoice::Auto => std::io::IsTerminal::is_terminal(&std::io::stdout()),
};
Self { verbosity, styled }
}
pub(crate) fn verbosity(self) -> Verbosity {
self.verbosity
}
pub(crate) fn status(self, verb: &str, args: fmt::Arguments<'_>) {
if !self.verbosity.shows_status() {
return;
}
let padding = COLUMN_WIDTH.saturating_sub(verb.len());
let stdout = std::io::stdout();
let mut handle = stdout.lock();
let _ = if self.styled {
writeln!(
handle,
"{:padding$}\x1b[1;32m{verb}\x1b[0m {args}",
"",
padding = padding,
)
} else {
writeln!(handle, "{:padding$}{verb} {args}", "", padding = padding)
};
}
pub(crate) fn note(self, args: fmt::Arguments<'_>) {
if self.verbosity.shows_status() {
self.write(Stream::Stdout, args);
}
}
pub(crate) fn warning(self, args: fmt::Arguments<'_>) {
let stderr = std::io::stderr();
let mut handle = stderr.lock();
let _ = writeln!(handle, "cabin: warning: {args}");
}
pub(crate) fn tool_stderr(self, body: &str) {
let stderr = std::io::stderr();
let mut handle = stderr.lock();
let _ = writeln!(handle, "{body}");
}
pub(crate) fn help(self, body: &str) {
let stderr = std::io::stderr();
let mut handle = stderr.lock();
let _ = writeln!(handle);
let mut lines = body.lines();
let Some(first) = lines.next() else {
return;
};
if self.styled {
let _ = writeln!(handle, "\x1b[1;36mhelp:\x1b[0m {first}");
} else {
let _ = writeln!(handle, "help: {first}");
}
for line in lines {
if line.is_empty() {
let _ = writeln!(handle);
} else {
let _ = writeln!(handle, " {line}");
}
}
}
pub(crate) fn verbose(self, args: fmt::Arguments<'_>) {
if self.verbosity.shows_verbose() {
self.write(Stream::Stdout, args);
}
}
pub(crate) fn aux_verbose(self, args: fmt::Arguments<'_>) {
if self.verbosity.shows_verbose() {
self.write(Stream::Stderr, args);
}
}
pub(crate) fn very_verbose(self, args: fmt::Arguments<'_>) {
if self.verbosity.shows_very_verbose() {
self.write(Stream::Stdout, args);
}
}
pub(crate) fn aux_very_verbose(self, args: fmt::Arguments<'_>) {
if self.verbosity.shows_very_verbose() {
self.write(Stream::Stderr, args);
}
}
fn write(self, stream: Stream, args: fmt::Arguments<'_>) {
match stream {
Stream::Stdout => {
let stdout = std::io::stdout();
let mut handle = stdout.lock();
let _ = writeln!(handle, "{args}");
}
Stream::Stderr => {
let stderr = std::io::stderr();
let mut handle = stderr.lock();
let _ = writeln!(handle, "{args}");
}
}
}
}
impl Default for Reporter {
fn default() -> Self {
Self::new(Verbosity::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn no_env(_: &str) -> Option<String> {
None
}
fn env_with<'a>(pairs: &'a [(&'a str, &'a str)]) -> impl Fn(&str) -> Option<String> + 'a {
move |key| {
pairs
.iter()
.find(|(k, _)| *k == key)
.map(|(_, v)| (*v).to_owned())
}
}
fn cfg() -> EffectiveConfig {
EffectiveConfig::default()
}
fn cfg_with_verbosity(level: Verbosity) -> EffectiveConfig {
let mut effective = EffectiveConfig::default();
effective.term.verbosity = Some(cabin_config::EffectiveVerbosity {
level,
source: cabin_config::ConfigSource::User,
});
effective
}
fn cli(verbose_count: u8, quiet: bool) -> CliVerbosity {
CliVerbosity {
verbose_count,
quiet,
}
}
#[test]
fn defaults_to_normal_with_no_inputs() {
let resolved = resolve_verbosity(cli(0, false), no_env, &cfg()).unwrap();
assert_eq!(resolved, Verbosity::Normal);
}
#[test]
fn cli_verbose_count_one_yields_verbose() {
let resolved = resolve_verbosity(cli(1, false), no_env, &cfg()).unwrap();
assert_eq!(resolved, Verbosity::Verbose);
}
#[test]
fn cli_verbose_count_two_or_more_yields_very_verbose() {
let resolved = resolve_verbosity(cli(2, false), no_env, &cfg()).unwrap();
assert_eq!(resolved, Verbosity::VeryVerbose);
let resolved = resolve_verbosity(cli(7, false), no_env, &cfg()).unwrap();
assert_eq!(resolved, Verbosity::VeryVerbose);
}
#[test]
fn cli_quiet_overrides_config_verbose() {
let resolved = resolve_verbosity(
cli(0, true),
no_env,
&cfg_with_verbosity(Verbosity::Verbose),
)
.unwrap();
assert_eq!(resolved, Verbosity::Quiet);
}
#[test]
fn cli_verbose_overrides_config_quiet() {
let resolved =
resolve_verbosity(cli(1, false), no_env, &cfg_with_verbosity(Verbosity::Quiet))
.unwrap();
assert_eq!(resolved, Verbosity::Verbose);
}
#[test]
fn env_verbose_applies_when_cli_silent() {
let resolved = resolve_verbosity(
cli(0, false),
env_with(&[(cabin_env::CABIN_TERM_VERBOSE, "1")]),
&cfg(),
)
.unwrap();
assert_eq!(resolved, Verbosity::Verbose);
}
#[test]
fn env_quiet_applies_when_cli_silent() {
let resolved = resolve_verbosity(
cli(0, false),
env_with(&[(cabin_env::CABIN_TERM_QUIET, "true")]),
&cfg(),
)
.unwrap();
assert_eq!(resolved, Verbosity::Quiet);
}
#[test]
fn env_overrides_config() {
let resolved = resolve_verbosity(
cli(0, false),
env_with(&[(cabin_env::CABIN_TERM_VERBOSE, "1")]),
&cfg_with_verbosity(Verbosity::Quiet),
)
.unwrap();
assert_eq!(resolved, Verbosity::Verbose);
}
#[test]
fn env_both_truthy_is_rejected() {
let err = resolve_verbosity(
cli(0, false),
env_with(&[
(cabin_env::CABIN_TERM_VERBOSE, "1"),
(cabin_env::CABIN_TERM_QUIET, "1"),
]),
&cfg(),
)
.unwrap_err();
assert!(
err.variable == cabin_env::CABIN_TERM_QUIET
|| err.variable == cabin_env::CABIN_TERM_VERBOSE
);
}
#[test]
fn invalid_env_value_bubbles_up_as_typed_error() {
let err = resolve_verbosity(
cli(0, false),
env_with(&[(cabin_env::CABIN_TERM_VERBOSE, "loud")]),
&cfg(),
)
.unwrap_err();
assert_eq!(err.variable, cabin_env::CABIN_TERM_VERBOSE);
assert_eq!(err.value, "loud");
}
#[test]
fn config_applies_when_cli_and_env_silent() {
let resolved = resolve_verbosity(
cli(0, false),
no_env,
&cfg_with_verbosity(Verbosity::Verbose),
)
.unwrap();
assert_eq!(resolved, Verbosity::Verbose);
}
#[test]
fn empty_env_falls_through_to_config() {
let resolved = resolve_verbosity(
cli(0, false),
env_with(&[(cabin_env::CABIN_TERM_VERBOSE, "")]),
&cfg_with_verbosity(Verbosity::Quiet),
)
.unwrap();
assert_eq!(resolved, Verbosity::Quiet);
}
}