use std::io;
#[cfg(not(feature = "rich-output"))]
use std::io::Write;
use std::sync::OnceLock;
static USE_RICH: OnceLock<bool> = OnceLock::new();
#[derive(Debug, Clone, Copy)]
pub struct DcgConsole {
force_plain: bool,
}
impl DcgConsole {
#[must_use]
pub const fn new() -> Self {
Self { force_plain: false }
}
#[must_use]
pub const fn plain() -> Self {
Self { force_plain: true }
}
#[cfg(feature = "rich-output")]
pub fn print(&self, text: &str) {
let console = self.create_inner_console();
if self.force_plain {
console.print_plain(text);
} else {
console.print(text);
}
}
#[cfg(not(feature = "rich-output"))]
pub fn print(&self, text: &str) {
let plain_text = strip_markup(text);
let _ = writeln!(io::stderr(), "{plain_text}");
}
#[cfg(feature = "rich-output")]
pub fn print_renderable<R>(&self, renderable: &R)
where
R: rich_rust::renderables::Renderable,
{
let console = self.create_inner_console();
console.print_renderable(renderable);
}
#[cfg(feature = "rich-output")]
pub fn rule(&self, title: Option<&str>) {
let console = self.create_inner_console();
console.rule(title);
}
#[cfg(not(feature = "rich-output"))]
pub fn rule(&self, title: Option<&str>) {
let width = self.width();
let line = if let Some(t) = title {
let padding = width.saturating_sub(t.len() + 4) / 2;
format!("{} {} {}", "-".repeat(padding), t, "-".repeat(padding))
} else {
"-".repeat(width)
};
let _ = writeln!(io::stderr(), "{line}");
}
#[cfg(feature = "rich-output")]
#[must_use]
pub fn width(&self) -> usize {
let console = self.create_inner_console();
console.width()
}
#[cfg(not(feature = "rich-output"))]
#[must_use]
pub fn width(&self) -> usize {
crate::output::terminal_width() as usize
}
#[must_use]
pub const fn is_plain(&self) -> bool {
self.force_plain
}
#[cfg(feature = "rich-output")]
fn create_inner_console(&self) -> rich_rust::console::Console {
let mut builder = rich_rust::console::Console::builder().file(Box::new(io::stderr()));
if self.force_plain {
builder = builder.no_color();
}
builder.build()
}
}
impl Default for DcgConsole {
fn default() -> Self {
Self::new()
}
}
#[must_use]
pub fn console() -> DcgConsole {
let use_rich = *USE_RICH.get_or_init(|| {
if std::env::var("DCG_NO_RICH").is_ok() {
return false;
}
crate::output::should_use_rich_output()
});
if use_rich {
DcgConsole::new()
} else {
DcgConsole::plain()
}
}
pub fn init_console(force_plain: bool) {
let _ = USE_RICH.set(!force_plain);
}
#[cfg(not(feature = "rich-output"))]
fn strip_markup(text: &str) -> String {
let mut result = String::with_capacity(text.len());
let mut in_bracket = false;
for c in text.chars() {
match c {
'[' => in_bracket = true,
']' if in_bracket => in_bracket = false,
_ if !in_bracket => result.push(c),
_ => {}
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_console_returns_valid_width() {
let console = DcgConsole::plain();
assert!(console.width() > 0);
}
#[test]
fn test_plain_console_is_plain() {
let console = DcgConsole::plain();
assert!(console.is_plain());
}
#[test]
fn test_new_console_default() {
let console = DcgConsole::new();
let _ = console.width();
}
#[test]
fn test_new_console_not_plain() {
let console = DcgConsole::new();
assert!(!console.is_plain());
}
#[test]
fn test_default_trait_matches_new() {
let default_console = DcgConsole::default();
let new_console = DcgConsole::new();
assert_eq!(default_console.is_plain(), new_console.is_plain());
assert!(!default_console.is_plain());
}
#[test]
fn test_init_console_does_not_panic() {
init_console(true);
init_console(false);
}
#[test]
fn test_plain_console_print_does_not_panic() {
let console = DcgConsole::plain();
console.print("simple text");
console.print("[bold]markup text[/]");
console.print("");
}
#[test]
fn test_new_console_print_does_not_panic() {
let console = DcgConsole::new();
console.print("simple text");
console.print("[bold]markup text[/]");
console.print("");
}
#[test]
fn test_plain_console_rule_does_not_panic() {
let console = DcgConsole::plain();
console.rule(None);
console.rule(Some("Title"));
console.rule(Some(""));
}
#[test]
fn test_console_function_returns_valid_console() {
let c = console();
assert!(c.width() > 0);
}
#[cfg(not(feature = "rich-output"))]
#[test]
fn test_strip_markup() {
assert_eq!(strip_markup("[bold]hello[/]"), "hello");
assert_eq!(strip_markup("[red]error[/]: message"), "error: message");
assert_eq!(strip_markup("no markup here"), "no markup here");
assert_eq!(strip_markup("[a][b][c]"), "");
}
#[cfg(not(feature = "rich-output"))]
#[test]
fn test_strip_markup_nested() {
assert_eq!(strip_markup("[bold [red]]text[/]"), "]text");
}
#[cfg(not(feature = "rich-output"))]
#[test]
fn test_strip_markup_empty() {
assert_eq!(strip_markup(""), "");
}
#[cfg(not(feature = "rich-output"))]
#[test]
fn test_strip_markup_no_close() {
assert_eq!(strip_markup("[bold"), "");
}
}