#[cfg(all(feature = "rich-ui", unix))]
use rich_rust::prelude::*;
#[cfg(all(feature = "rich-ui", unix))]
use rich_rust::renderables::Renderable;
use rch_common::ui::{Icons, OutputContext, RchTheme};
pub struct RchConsole {
#[cfg(all(feature = "rich-ui", unix))]
inner: Console,
context: OutputContext,
}
impl RchConsole {
#[must_use]
pub fn new() -> Self {
let context = OutputContext::detect();
Self::with_context(context)
}
#[must_use]
pub fn with_context(context: OutputContext) -> Self {
#[cfg(all(feature = "rich-ui", unix))]
let inner = Self::build_console(context);
Self {
#[cfg(all(feature = "rich-ui", unix))]
inner,
context,
}
}
#[cfg(all(feature = "rich-ui", unix))]
fn build_console(context: OutputContext) -> Console {
Console::builder()
.force_terminal(context.supports_rich())
.build()
}
#[must_use]
pub fn is_rich(&self) -> bool {
self.context.supports_rich()
}
#[must_use]
pub fn is_colored(&self) -> bool {
self.context.supports_color()
}
#[must_use]
pub fn is_machine(&self) -> bool {
self.context.is_machine()
}
#[must_use]
pub fn context(&self) -> OutputContext {
self.context
}
#[must_use]
pub fn width(&self) -> usize {
#[cfg(all(feature = "rich-ui", unix))]
{
self.inner.width()
}
#[cfg(not(all(feature = "rich-ui", unix)))]
{
terminal_size::terminal_size()
.map(|(w, _)| w.0 as usize)
.unwrap_or(80)
}
}
#[cfg(all(feature = "rich-ui", unix))]
pub fn print_rich(&self, content: &str) {
if self.context.supports_rich() {
self.inner.print(content);
}
}
#[cfg(all(feature = "rich-ui", unix))]
pub fn print_renderable<R: Renderable>(&self, renderable: &R) {
if self.context.supports_rich() {
self.inner.print_renderable(renderable);
}
}
pub fn rule(&self, title: Option<&str>) {
#[cfg(all(feature = "rich-ui", unix))]
if self.context.supports_rich() {
self.inner.rule(title);
return;
}
if !self.context.is_machine() {
let width: usize = 60;
if let Some(t) = title {
let padding = width.saturating_sub(t.len()).saturating_sub(2) / 2;
eprintln!("{} {} {}", "-".repeat(padding), t, "-".repeat(padding));
} else {
eprintln!("{}", "-".repeat(width));
}
}
}
pub fn line(&self) {
if !self.context.is_machine() {
#[cfg(all(feature = "rich-ui", unix))]
{
self.inner.line();
}
#[cfg(not(all(feature = "rich-ui", unix)))]
{
eprintln!();
}
}
}
pub fn print_or_plain(&self, _rich: &str, plain: &str) {
#[cfg(all(feature = "rich-ui", unix))]
if self.context.supports_rich() {
self.inner.print(_rich);
return;
}
if !self.context.is_machine() {
eprintln!("{plain}");
}
}
pub fn print_plain(&self, content: &str) {
if !self.context.is_machine() {
eprintln!("{content}");
}
}
#[cfg(all(feature = "rich-ui", unix))]
pub fn print_error(&self, title: &str, message: &str) {
if self.context.supports_rich() {
let icon = Icons::cross(self.context);
let title_text = format!("{icon} {title}");
let panel = Panel::from_text(message)
.title(title_text.as_str())
.border_style(RchTheme::error())
.rounded();
self.inner.print_renderable(&panel);
} else {
let icon = Icons::cross(self.context);
eprintln!("{icon} Error: {title}");
eprintln!("{message}");
}
}
#[cfg(not(all(feature = "rich-ui", unix)))]
pub fn print_error(&self, title: &str, message: &str) {
let icon = Icons::cross(self.context);
eprintln!("{icon} Error: {title}");
eprintln!("{message}");
}
pub fn print_success(&self, message: &str) {
let icon = Icons::check(self.context);
#[cfg(all(feature = "rich-ui", unix))]
if self.context.supports_rich() {
self.inner
.print(&format!("[bold {}]{icon}[/] {message}", RchTheme::SUCCESS));
return;
}
if !self.context.is_machine() {
eprintln!("{icon} {message}");
}
}
pub fn print_warning(&self, message: &str) {
let icon = Icons::warning(self.context);
#[cfg(all(feature = "rich-ui", unix))]
if self.context.supports_rich() {
self.inner
.print(&format!("[bold {}]{icon}[/] {message}", RchTheme::WARNING));
return;
}
if !self.context.is_machine() {
eprintln!("{icon} {message}");
}
}
pub fn print_info(&self, message: &str) {
let icon = Icons::info(self.context);
#[cfg(all(feature = "rich-ui", unix))]
if self.context.supports_rich() {
self.inner
.print(&format!("[bold {}]{icon}[/] {message}", RchTheme::INFO));
return;
}
if !self.context.is_machine() {
eprintln!("{icon} {message}");
}
}
pub fn print_json<T: serde::Serialize>(&self, value: &T) -> serde_json::Result<()> {
let json = serde_json::to_string_pretty(value)?;
println!("{json}");
Ok(())
}
#[cfg(all(feature = "rich-ui", unix))]
#[must_use]
pub fn inner(&self) -> &Console {
&self.inner
}
}
impl Default for RchConsole {
fn default() -> Self {
Self::new()
}
}
#[must_use]
pub fn console() -> RchConsole {
RchConsole::new()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_machine_mode_no_rich() {
let console = RchConsole::with_context(OutputContext::Machine);
console.print_plain("test"); assert!(console.is_machine());
assert!(!console.is_rich());
}
#[test]
fn test_hook_mode_is_machine() {
let console = RchConsole::with_context(OutputContext::Hook);
assert!(console.is_machine());
assert!(!console.is_rich());
assert!(!console.is_colored());
}
#[test]
fn test_plain_mode_no_markup() {
let console = RchConsole::with_context(OutputContext::Plain);
assert!(!console.is_machine());
assert!(!console.is_rich());
}
#[test]
fn test_context_queries_interactive() {
let console = RchConsole::with_context(OutputContext::Interactive);
assert!(console.is_rich());
assert!(console.is_colored());
assert!(!console.is_machine());
}
#[test]
fn test_context_queries_colored() {
let console = RchConsole::with_context(OutputContext::Colored);
assert!(!console.is_rich()); assert!(console.is_colored());
assert!(!console.is_machine());
}
#[test]
fn test_context_accessor_returns_value() {
let console = RchConsole::with_context(OutputContext::Plain);
assert_eq!(console.context(), OutputContext::Plain);
}
#[test]
fn test_with_context_all_modes() {
let cases = [
(OutputContext::Interactive, true, true, false),
(OutputContext::Colored, false, true, false),
(OutputContext::Plain, false, false, false),
(OutputContext::Machine, false, false, true),
(OutputContext::Hook, false, false, true),
];
for (context, rich, colored, machine) in cases {
let console = RchConsole::with_context(context);
assert_eq!(console.is_rich(), rich, "rich mismatch for {context:?}");
assert_eq!(
console.is_colored(),
colored,
"color mismatch for {context:?}"
);
assert_eq!(
console.is_machine(),
machine,
"machine mismatch for {context:?}"
);
}
}
#[test]
fn test_width_returns_reasonable_value() {
let console = RchConsole::new();
let width = console.width();
assert!(width >= 40, "Width should be at least 40: {width}");
assert!(width <= 500, "Width should be at most 500: {width}");
}
#[test]
fn test_default_creates_valid_console() {
let console = RchConsole::default();
let _ = console.context();
}
#[test]
fn test_console_function() {
let c1 = console();
let c2 = console();
assert_eq!(c1.context(), c2.context());
}
#[test]
fn test_print_json_succeeds() {
let console = RchConsole::with_context(OutputContext::Machine);
#[derive(serde::Serialize)]
struct TestData {
value: i32,
}
let result = console.print_json(&TestData { value: 42 });
assert!(result.is_ok());
}
#[test]
fn test_rule_plain_mode() {
let console = RchConsole::with_context(OutputContext::Plain);
console.rule(None);
console.rule(Some("Test Title"));
}
#[test]
fn test_rule_machine_mode_silent() {
let console = RchConsole::with_context(OutputContext::Machine);
console.rule(None);
console.rule(Some("Test Title"));
}
#[test]
fn test_rule_plain_long_title() {
let console = RchConsole::with_context(OutputContext::Plain);
console.rule(Some(
"A Very Long Title That Exceeds The Default Width For Rules",
));
}
#[test]
fn test_line_plain_mode() {
let console = RchConsole::with_context(OutputContext::Plain);
console.line();
}
#[test]
fn test_line_machine_mode() {
let console = RchConsole::with_context(OutputContext::Machine);
console.line();
}
#[test]
fn test_print_or_plain_modes() {
let console = RchConsole::with_context(OutputContext::Machine);
console.print_or_plain("[bold]rich[/]", "plain");
let console = RchConsole::with_context(OutputContext::Plain);
console.print_or_plain("[bold]rich[/]", "plain");
}
#[test]
fn test_print_or_plain_interactive() {
let console = RchConsole::with_context(OutputContext::Interactive);
console.print_or_plain("[bold]rich[/]", "plain");
}
#[test]
fn test_print_plain_suppressed_in_machine_mode() {
let console = RchConsole::with_context(OutputContext::Machine);
console.print_plain("plain");
}
#[test]
fn test_status_messages_machine_mode() {
let console = RchConsole::with_context(OutputContext::Machine);
console.print_success("success");
console.print_warning("warning");
console.print_info("info");
}
#[test]
fn test_status_messages_plain_and_colored() {
let console = RchConsole::with_context(OutputContext::Plain);
console.print_success("success");
console.print_warning("warning");
console.print_info("info");
let console = RchConsole::with_context(OutputContext::Colored);
console.print_success("success");
console.print_warning("warning");
console.print_info("info");
}
#[test]
fn test_print_plain_non_machine() {
let console = RchConsole::with_context(OutputContext::Plain);
console.print_plain("plain");
let console = RchConsole::with_context(OutputContext::Interactive);
console.print_plain("plain");
}
#[test]
fn test_print_json_plain_mode() {
let console = RchConsole::with_context(OutputContext::Plain);
#[derive(serde::Serialize)]
struct TestData {
label: &'static str,
}
let result = console.print_json(&TestData { label: "ok" });
assert!(result.is_ok());
}
#[cfg(all(feature = "rich-ui", unix))]
#[test]
fn test_print_rich_suppressed_in_machine_mode() {
let console = RchConsole::with_context(OutputContext::Machine);
console.print_rich("[bold]test[/]");
}
#[cfg(all(feature = "rich-ui", unix))]
#[test]
fn test_print_renderable_suppressed_in_plain_mode() {
let console = RchConsole::with_context(OutputContext::Plain);
let panel = rich_rust::renderables::Panel::from_text("renderable");
console.print_renderable(&panel);
}
#[cfg(all(feature = "rich-ui", unix))]
#[test]
fn test_inner_console_access() {
let console = RchConsole::with_context(OutputContext::Interactive);
let _inner = console.inner();
}
#[test]
fn test_print_error_always_outputs() {
let console = RchConsole::with_context(OutputContext::Machine);
console.print_error("Test Error", "This is a test error message");
}
#[test]
fn test_threaded_console_usage() {
use std::thread;
let handles: Vec<_> = (0..4)
.map(|_| {
thread::spawn(|| {
let console = RchConsole::with_context(OutputContext::Plain);
console.print_plain("test");
console.print_or_plain("[bold]rich[/]", "plain");
console.rule(None);
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
}
#[cfg(all(feature = "rich-ui", unix))]
#[test]
fn test_rich_paths_interactive() {
let console = RchConsole::with_context(OutputContext::Interactive);
console.print_rich("[bold]rich[/]");
let panel = rich_rust::renderables::Panel::from_text("renderable");
console.print_renderable(&panel);
console.rule(Some("Title"));
console.line();
console.print_error("Title", "Message");
console.print_success("success");
console.print_warning("warning");
console.print_info("info");
console.print_or_plain("[bold]rich[/]", "plain");
}
}