pub mod console;
pub mod denial;
pub mod escalation;
pub mod progress;
pub mod rich_theme;
pub mod suggestions;
pub mod tables;
pub mod test;
pub mod theme;
pub mod tree;
pub use console::{DcgConsole, console, init_console};
pub use denial::DenialBox;
pub use escalation::{EscalationContext, format_escalation_message};
pub use progress::{
MaybeProgress, NoopProgress, SCAN_PROGRESS_THRESHOLD, ScanProgress, ScanProgressStyle, spinner,
spinner_if_tty,
};
#[cfg(feature = "rich-output")]
pub use progress::{RichProgressStyle, render_progress_bar_rich};
pub use rich_theme::{RichThemeExt, color_to_markup, severity_badge_markup, severity_panel_title};
pub use suggestions::{
SUGGESTION_OUTPUT_SCHEMA_VERSION, SuggestionJsonEntry, SuggestionJsonOutput,
SuggestionPathJson, SuggestionRenderOptions, render_suggestions_json, render_suggestions_text,
suggested_config_snippet, suggestions_to_json_output,
};
pub use tables::{ScanResultRow, ScanResultsTable, TableStyle};
pub use test::{AllowedReason, TestOutcome, TestResultBox};
pub use theme::{BorderStyle, Severity, SeverityColors, Theme, ThemePalette};
pub use tree::{
DEFAULT_PACK_TREE_MAX_PATTERNS, DcgTree, DcgTreeGuides, ExplainTreeBuilder, PackTreeItem,
PackTreeOptions, PackTreePattern, TreeNode, explain_trace_tree, pack_list_tree,
pack_list_tree_with_options,
};
use crate::config::Config;
use std::sync::OnceLock;
static FORCE_PLAIN: OnceLock<bool> = OnceLock::new();
static SUGGESTIONS_ENABLED: OnceLock<bool> = OnceLock::new();
pub fn init(force_plain: bool) {
let _ = FORCE_PLAIN.set(force_plain);
}
pub fn init_suggestions(enabled: bool) {
let _ = SUGGESTIONS_ENABLED.set(enabled);
}
#[must_use]
pub fn should_use_rich_output() -> bool {
should_use_rich_output_with_env(
FORCE_PLAIN.get().copied().unwrap_or(false),
::console::Term::stdout().is_term(),
|name| std::env::var_os(name),
)
}
fn should_use_rich_output_with_env(
force_plain: bool,
stdout_is_tty: bool,
mut env_var: impl FnMut(&str) -> Option<std::ffi::OsString>,
) -> bool {
if force_plain {
return false;
}
if env_flag_enabled_with(&mut env_var, "DCG_NO_RICH")
|| env_var("NO_COLOR").is_some()
|| env_flag_enabled_with(&mut env_var, "DCG_NO_COLOR")
{
return false;
}
if env_var("CI").is_some() {
return false;
}
if !stdout_is_tty {
return false;
}
if matches!(
env_var("TERM").as_deref(),
Some(term) if term == std::ffi::OsStr::new("dumb")
) {
return false;
}
true
}
#[must_use]
pub fn auto_theme() -> Theme {
if should_use_rich_output() {
if env_flag_enabled("DCG_HIGH_CONTRAST") {
Theme::high_contrast()
} else {
Theme::default()
}
} else {
Theme::no_color()
}
}
#[must_use]
pub fn auto_theme_with_config(config: &Config) -> Theme {
if !should_use_rich_output() {
return Theme::no_color();
}
let palette = if env_flag_enabled("DCG_HIGH_CONTRAST") || config.output.high_contrast_enabled()
{
ThemePalette::HighContrast
} else if let Some(palette) = config
.theme
.palette
.as_deref()
.and_then(|value| value.parse::<ThemePalette>().ok())
{
palette
} else {
ThemePalette::Default
};
let mut theme = Theme::from_palette(palette);
if let Some(use_color) = config.theme.use_color {
if !use_color {
theme = theme.without_colors();
}
}
if let Some(use_unicode) = config.theme.use_unicode {
if palette != ThemePalette::HighContrast {
theme.border_style = if use_unicode {
BorderStyle::Unicode
} else {
BorderStyle::Ascii
};
}
}
theme
}
pub fn env_flag_enabled(var: &str) -> bool {
std::env::var_os(var).is_some_and(|value| env_flag_os_value_enabled(&value))
}
fn env_flag_enabled_with(
env_var: &mut impl FnMut(&str) -> Option<std::ffi::OsString>,
var: &str,
) -> bool {
env_var(var).is_some_and(|value| env_flag_os_value_enabled(&value))
}
fn env_flag_os_value_enabled(value: &std::ffi::OsStr) -> bool {
value.to_str().is_none_or(env_flag_value_enabled)
}
pub fn env_flag_value_enabled(value: &str) -> bool {
!matches!(
value.trim().to_lowercase().as_str(),
"" | "0" | "false" | "no" | "off"
)
}
#[must_use]
pub fn robot_mode_enabled(explicit_robot_flag: bool) -> bool {
explicit_robot_flag || env_flag_enabled("DCG_ROBOT")
}
#[must_use]
pub fn supports_256_colors() -> bool {
if !should_use_rich_output() {
return false;
}
if let Ok(colorterm) = std::env::var("COLORTERM") {
if colorterm == "truecolor" || colorterm == "24bit" {
return true;
}
}
if let Ok(term) = std::env::var("TERM") {
if term.contains("256color") || term.contains("truecolor") {
return true;
}
}
true
}
#[must_use]
pub fn terminal_width() -> u16 {
::console::Term::stdout()
.size_checked()
.map_or(80, |(_, w)| w)
}
#[must_use]
pub fn terminal_height() -> u16 {
::console::Term::stdout()
.size_checked()
.map_or(24, |(h, _)| h)
}
#[must_use]
pub fn suggestions_enabled() -> bool {
suggestions_requested() && should_use_rich_output()
}
#[must_use]
pub fn suggestions_requested() -> bool {
SUGGESTIONS_ENABLED.get().copied().unwrap_or(true)
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::OsString;
fn test_env<'a>(
entries: &'a [(&'a str, &'a str)],
) -> impl FnMut(&str) -> Option<OsString> + 'a {
move |name| {
entries
.iter()
.find(|(key, _)| *key == name)
.map(|(_, value)| OsString::from(value))
}
}
#[test]
fn test_auto_theme_returns_theme() {
let theme = auto_theme();
assert!(matches!(
theme.border_style,
BorderStyle::Unicode | BorderStyle::Ascii | BorderStyle::None
));
}
#[test]
fn test_terminal_dimensions_have_defaults() {
let width = terminal_width();
let height = terminal_height();
assert!(width > 0);
assert!(height > 0);
}
#[test]
fn test_supports_256_colors_does_not_panic() {
let _ = supports_256_colors();
}
#[test]
fn test_env_flag_enabled_true_values() {
assert!(!env_flag_enabled("DCG_NONEXISTENT_TEST_VAR_12345"));
}
#[test]
fn test_env_flag_enabled_false_for_unset() {
assert!(!env_flag_enabled("DCG_DEFINITELY_NOT_SET_EVER"));
}
#[test]
fn test_env_flag_value_enabled_boolean_semantics() {
for value in ["1", "true", "yes", "on", "anything"] {
assert!(env_flag_value_enabled(value), "{value:?} should be enabled");
}
for value in ["", "0", "false", "no", "off", " FALSE "] {
assert!(
!env_flag_value_enabled(value),
"{value:?} should be disabled"
);
}
}
#[cfg(unix)]
#[test]
fn test_invalid_unicode_env_flag_value_is_enabled() {
use std::os::unix::ffi::OsStringExt;
let invalid_utf8 = OsString::from_vec(vec![0xff]);
assert!(!should_use_rich_output_with_env(false, true, |name| {
(name == "DCG_NO_RICH").then(|| invalid_utf8.clone())
}));
}
#[test]
fn test_dcg_no_rich_disables_rich_output() {
assert!(!should_use_rich_output_with_env(
false,
true,
test_env(&[("DCG_NO_RICH", "1")])
));
}
#[test]
fn test_dcg_no_rich_falsey_does_not_disable_rich_output() {
for value in ["0", "false", "no", "off", ""] {
assert!(
should_use_rich_output_with_env(false, true, test_env(&[("DCG_NO_RICH", value)])),
"DCG_NO_RICH={value:?} should not disable rich output"
);
}
}
#[test]
fn test_no_color_disables_rich_output() {
assert!(!should_use_rich_output_with_env(
false,
true,
test_env(&[("NO_COLOR", "")])
));
}
#[test]
fn test_dcg_no_color_disables_rich_output() {
assert!(!should_use_rich_output_with_env(
false,
true,
test_env(&[("DCG_NO_COLOR", "1")])
));
}
#[test]
fn test_dcg_no_color_falsey_does_not_disable_rich_output() {
for value in ["0", "false", "no", "off", ""] {
assert!(
should_use_rich_output_with_env(false, true, test_env(&[("DCG_NO_COLOR", value)])),
"DCG_NO_COLOR={value:?} should not disable rich output"
);
}
}
#[test]
fn test_ci_environment_disables_rich_output() {
assert!(!should_use_rich_output_with_env(
false,
true,
test_env(&[("CI", "true")])
));
}
#[test]
fn test_term_dumb_disables_rich_output() {
assert!(!should_use_rich_output_with_env(
false,
true,
test_env(&[("TERM", "dumb")])
));
}
#[test]
fn test_non_tty_disables_rich_output() {
assert!(!should_use_rich_output_with_env(
false,
false,
test_env(&[])
));
}
#[test]
fn test_rich_output_enabled_for_tty_without_disabling_env() {
assert!(should_use_rich_output_with_env(false, true, test_env(&[])));
}
#[test]
fn test_force_plain_disables_rich_output() {
assert!(!should_use_rich_output_with_env(true, true, test_env(&[])));
}
#[test]
fn test_robot_mode_enabled_explicit_flag_wins() {
assert!(robot_mode_enabled(true));
}
#[test]
fn test_suggestions_enabled_default() {
let result = suggestions_enabled();
assert!(!result);
}
#[test]
fn test_init_idempotent() {
init(false);
init(true);
}
#[test]
fn test_init_suggestions_idempotent() {
init_suggestions(true);
init_suggestions(false);
}
#[test]
fn test_should_use_rich_output_in_test_env() {
let result = should_use_rich_output();
assert!(!result);
}
#[test]
fn test_auto_theme_no_color_in_test_env() {
let theme = auto_theme();
assert!(!theme.colors_enabled);
}
#[test]
fn test_terminal_width_reasonable_range() {
let width = terminal_width();
assert!(width >= 1);
assert!(width <= 500);
}
#[test]
fn test_terminal_height_reasonable_range() {
let height = terminal_height();
assert!(height >= 1);
assert!(height <= 200);
}
}