use std::fmt::Debug;
use std::io::IsTerminal;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
static SKIP_ALL: AtomicBool = AtomicBool::new(false);
static BREAK_COUNT: AtomicUsize = AtomicUsize::new(0);
const MAX_LINES: usize = 50;
#[doc(hidden)]
pub fn is_enabled() -> bool {
if SKIP_ALL.load(Ordering::Relaxed) {
return false;
}
match std::env::var("PRINT_BREAK") {
Ok(val) => !matches!(val.as_str(), "0" | "false" | "no" | "off"),
Err(_) => true, }
}
#[doc(hidden)]
pub fn is_tty() -> bool {
std::io::stderr().is_terminal() && std::io::stdin().is_terminal()
}
#[doc(hidden)]
pub fn next_break_id() -> usize {
BREAK_COUNT.fetch_add(1, Ordering::Relaxed) + 1
}
#[doc(hidden)]
pub fn color(code: &str) -> &str {
if is_tty() { code } else { "" }
}
#[doc(hidden)]
pub fn reset() -> &'static str {
if is_tty() { "\x1b[0m" } else { "" }
}
#[doc(hidden)]
pub fn set_skip_all(skip: bool) {
SKIP_ALL.store(skip, Ordering::Relaxed);
}
#[doc(hidden)]
pub fn format_value<T: Debug>(value: &T) -> String {
let debug_str = format!("{:?}", value);
let raw_output;
if let Some(inner) = debug_str.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
let unescaped = inner
.replace("\\\"", "\"")
.replace("\\n", "\n")
.replace("\\t", "\t")
.replace("\\\\", "\\");
let trimmed = unescaped.trim();
if trimmed.starts_with('{') || trimmed.starts_with('[') {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&unescaped) {
if let Ok(pretty) = serde_json::to_string_pretty(&json) {
raw_output = format!("\x1b[90m(json)\x1b[0m\n{}", pretty);
return truncate_output(&raw_output);
}
}
}
if trimmed.contains(" = ") || trimmed.contains("]\n") || trimmed.starts_with('[') {
if let Ok(toml_val) = toml::from_str::<toml::Value>(&unescaped) {
if let Ok(pretty) = toml::to_string_pretty(&toml_val) {
raw_output = format!("\x1b[90m(toml)\x1b[0m\n{}", pretty);
return truncate_output(&raw_output);
}
}
}
if trimmed.contains(": ") || trimmed.contains(":\n") {
if let Ok(yaml_val) = serde_yaml::from_str::<serde_yaml::Value>(&unescaped) {
if yaml_val.is_mapping() || yaml_val.is_sequence() {
if let Ok(pretty) = serde_yaml::to_string(&yaml_val) {
raw_output = format!("\x1b[90m(yaml)\x1b[0m\n{}", pretty.trim());
return truncate_output(&raw_output);
}
}
}
}
raw_output = format!("\x1b[90m(string, {} chars)\x1b[0m\n{}", unescaped.len(), word_wrap(&unescaped, 80));
return truncate_output(&raw_output);
}
let debug_output = format!("{:#?}", value);
raw_output = colorize_debug(&debug_output);
truncate_output(&raw_output)
}
#[doc(hidden)]
pub fn format_value_full<T: Debug>(value: &T) -> String {
let debug_str = format!("{:?}", value);
if let Some(inner) = debug_str.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
let unescaped = inner
.replace("\\\"", "\"")
.replace("\\n", "\n")
.replace("\\t", "\t")
.replace("\\\\", "\\");
let trimmed = unescaped.trim();
if trimmed.starts_with('{') || trimmed.starts_with('[') {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&unescaped) {
if let Ok(pretty) = serde_json::to_string_pretty(&json) {
return pretty;
}
}
}
if trimmed.contains(" = ") || trimmed.contains("]\n") || trimmed.starts_with('[') {
if let Ok(toml_val) = toml::from_str::<toml::Value>(&unescaped) {
if let Ok(pretty) = toml::to_string_pretty(&toml_val) {
return pretty;
}
}
}
if trimmed.contains(": ") || trimmed.contains(":\n") {
if let Ok(yaml_val) = serde_yaml::from_str::<serde_yaml::Value>(&unescaped) {
if yaml_val.is_mapping() || yaml_val.is_sequence() {
if let Ok(pretty) = serde_yaml::to_string(&yaml_val) {
return pretty.trim().to_string();
}
}
}
}
return word_wrap(&unescaped, 100);
}
colorize_debug(&format!("{:#?}", value))
}
const DEFAULT_MAX_DEPTH: usize = 4;
fn max_depth() -> usize {
std::env::var("PRINT_BREAK_DEPTH")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(DEFAULT_MAX_DEPTH)
}
fn colorize_debug(s: &str) -> String {
let tty = is_tty();
if !tty {
return s.to_string();
}
let (green, cyan, yellow, magenta, white, gray, reset) = (
"\x1b[1;32m", "\x1b[36m", "\x1b[33m", "\x1b[35m", "\x1b[37m", "\x1b[90m", "\x1b[0m",
);
let mut result = String::new();
let lines: Vec<&str> = s.lines().collect();
let mut current_depth: usize = 0;
let mut skip_until_depth: Option<usize> = None;
for line in lines {
let trimmed = line.trim_start();
let indent_count = line.len() - trimmed.len();
let indent_level = indent_count / 4;
let opens = trimmed.ends_with('{') || trimmed.ends_with('[') || trimmed.ends_with("({");
let closes = trimmed.starts_with('}') || trimmed.starts_with(']') || trimmed.starts_with(')');
if closes {
current_depth = current_depth.saturating_sub(1);
}
if let Some(skip_depth) = skip_until_depth {
if current_depth < skip_depth {
skip_until_depth = None;
} else {
if opens {
current_depth += 1;
}
continue;
}
}
if opens && current_depth >= max_depth() {
for _ in 0..indent_level {
result.push_str(&format!("{}│{} ", gray, reset));
}
let name = trimmed.trim_end_matches(|c| c == '{' || c == '[' || c == '(' || c == ' ');
if !name.is_empty() {
result.push_str(&format!("{}{}{} {}{{ ... }}{}", green, name, reset, gray, reset));
} else {
result.push_str(&format!("{}[ ... ]{}", gray, reset));
}
result.push('\n');
skip_until_depth = Some(current_depth);
current_depth += 1;
continue;
}
for _ in 0..indent_level {
result.push_str(&format!("{}│{} ", gray, reset));
}
if opens {
let name = trimmed.trim_end_matches(|c| c == '{' || c == '[' || c == '(' || c == ' ');
let bracket = trimmed.chars().last().unwrap_or(' ');
if !name.is_empty() {
result.push_str(&format!("{}{}{} {}{}{}", green, name, reset, gray, bracket, reset));
} else {
result.push_str(&format!("{}{}{}", gray, bracket, reset));
}
current_depth += 1;
} else if closes || trimmed.ends_with("},") || trimmed.ends_with("],") || trimmed.ends_with("),") {
result.push_str(&format!("{}{}{}", gray, trimmed, reset));
} else if trimmed.contains(": ") {
if let Some(colon_pos) = trimmed.find(": ") {
let field = &trimmed[..colon_pos];
let value = &trimmed[colon_pos + 2..];
let colored_value = colorize_value(value, yellow, magenta, white, gray, reset);
result.push_str(&format!("{}{}{}{}: {}", cyan, field, reset, gray, colored_value));
} else {
result.push_str(trimmed);
}
} else {
let colored = colorize_value(trimmed, yellow, magenta, white, gray, reset);
result.push_str(&colored);
}
result.push('\n');
}
result.trim_end().to_string()
}
fn colorize_value(s: &str, yellow: &str, magenta: &str, white: &str, gray: &str, reset: &str) -> String {
let trimmed = s.trim_end_matches(',');
let has_comma = s.ends_with(',');
let comma = if has_comma { format!("{},{}", gray, reset) } else { String::new() };
if trimmed.starts_with('"') {
format!("{}{}{}{}", magenta, trimmed, reset, comma)
} else if trimmed.parse::<f64>().is_ok() || trimmed.starts_with('-') && trimmed[1..].parse::<f64>().is_ok() {
format!("{}{}{}{}", yellow, trimmed, reset, comma)
} else if trimmed == "true" || trimmed == "false" {
format!("{}{}{}{}", yellow, trimmed, reset, comma)
} else if trimmed == "None" || trimmed.starts_with("Some(") {
format!("{}{}{}{}", white, trimmed, reset, comma)
} else {
format!("{}{}{}{}", white, trimmed, reset, comma)
}
}
fn word_wrap(s: &str, width: usize) -> String {
let mut result = String::new();
for line in s.lines() {
if line.len() <= width {
result.push_str(line);
result.push('\n');
} else {
let mut current_line = String::new();
for word in line.split_whitespace() {
if current_line.is_empty() {
current_line = word.to_string();
} else if current_line.len() + 1 + word.len() <= width {
current_line.push(' ');
current_line.push_str(word);
} else {
result.push_str(¤t_line);
result.push('\n');
current_line = word.to_string();
}
}
if !current_line.is_empty() {
result.push_str(¤t_line);
result.push('\n');
}
}
}
result.trim_end().to_string()
}
fn truncate_output(s: &str) -> String {
let lines: Vec<&str> = s.lines().collect();
if lines.len() > MAX_LINES {
let mut result: Vec<&str> = lines[..MAX_LINES].to_vec();
result.push(&format!("\x1b[90m... ({} more lines)\x1b[0m", lines.len() - MAX_LINES));
let truncated: String = lines[..MAX_LINES].join("\n");
format!("{}\n\x1b[90m... ({} more lines)\x1b[0m", truncated, lines.len() - MAX_LINES)
} else {
s.to_string()
}
}
static LAST_FULL_OUTPUT: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
#[doc(hidden)]
pub fn store_full_output(output: String) {
if let Ok(mut guard) = LAST_FULL_OUTPUT.lock() {
*guard = Some(output);
}
}
#[doc(hidden)]
pub fn handle_input() -> bool {
use std::io::{self, BufRead, Write};
if !is_tty() {
eprintln!("(non-interactive mode, continuing...)");
return true;
}
loop {
eprint!("\x1b[90m[Enter=continue, m=more, s=skip all, q=quit]\x1b[0m ");
io::stderr().flush().unwrap();
let stdin = io::stdin();
let mut line = String::new();
if stdin.lock().read_line(&mut line).is_ok() {
let input = line.trim().to_lowercase();
match input.as_str() {
"q" | "quit" => {
eprintln!("\x1b[1;31mQuitting...\x1b[0m");
std::process::exit(0);
}
"s" | "skip" => {
eprintln!("\x1b[1;33mSkipping remaining breakpoints...\x1b[0m");
set_skip_all(true);
break;
}
"m" | "more" => {
if let Ok(guard) = LAST_FULL_OUTPUT.lock() {
if let Some(ref full) = *guard {
eprintln!("\n\x1b[1;33m─── Full Output ───\x1b[0m");
for line in full.lines() {
eprintln!("\x1b[37m{}\x1b[0m", line);
}
eprintln!("\x1b[1;33m───────────────────\x1b[0m\n");
} else {
eprintln!("\x1b[90m(no truncated output to show)\x1b[0m");
}
}
continue; }
_ => break }
} else {
break;
}
}
eprintln!();
true
}
#[macro_export]
#[cfg(debug_assertions)]
macro_rules! print_break {
() => {{
if $crate::is_enabled() {
use std::io::Write;
let break_id = $crate::next_break_id();
let location = format!("{}:{}", file!(), line!());
let width = 50;
let tty = $crate::is_tty();
let (yellow, cyan, reset) = if tty {
("\x1b[1;33m", "\x1b[36m", "\x1b[0m")
} else {
("", "", "")
};
eprintln!();
eprintln!("{}┌─ BREAK #{} {}{}", yellow, break_id, "─".repeat(width - 12 - break_id.to_string().len()), reset);
eprintln!("{}│{} {}{:<width$}{}{}│{}", yellow, reset, cyan, location, reset, yellow, reset, width = width - 2);
eprintln!("{}└{}{}", yellow, "─".repeat(width), reset);
$crate::handle_input();
}
}};
($($var:expr),+ $(,)?) => {{
if $crate::is_enabled() {
use std::io::Write;
let break_id = $crate::next_break_id();
let location = format!("{}:{}", file!(), line!());
let width = 50;
let tty = $crate::is_tty();
let (yellow, cyan, green, white, reset) = if tty {
("\x1b[1;33m", "\x1b[36m", "\x1b[1;32m", "\x1b[37m", "\x1b[0m")
} else {
("", "", "", "", "")
};
let mut full_output = String::new();
eprintln!();
eprintln!("{}┌─ BREAK #{} {}{}", yellow, break_id, "─".repeat(width - 12 - break_id.to_string().len()), reset);
eprintln!("{}│{} {}{:<width$}{}{}│{}", yellow, reset, cyan, location, reset, yellow, reset, width = width - 2);
eprintln!("{}├{}{}", yellow, "─".repeat(width), reset);
$(
let formatted = $crate::format_value(&$var);
let name = stringify!($var);
full_output.push_str(&format!("{} = {}\n\n", name, $crate::format_value_full(&$var)));
if formatted.contains('\n') {
eprintln!("{}│{} {}{}{}=", yellow, reset, green, name, reset);
for line in formatted.lines() {
eprintln!("{}│{} {}{}{}", yellow, reset, white, line, reset);
}
} else {
eprintln!("{}│{} {}{}{} = {}{}{}", yellow, reset, green, name, reset, white, formatted, reset);
}
)+
$crate::store_full_output(full_output);
eprintln!("{}└{}{}", yellow, "─".repeat(width), reset);
$crate::handle_input();
}
}};
}
#[macro_export]
#[cfg(debug_assertions)]
macro_rules! print_break_if {
($cond:expr) => {{
if $cond {
$crate::print_break!();
}
}};
($cond:expr, $($var:expr),+ $(,)?) => {{
if $cond {
$crate::print_break!($($var),+);
}
}};
}
#[macro_export]
#[cfg(not(debug_assertions))]
macro_rules! print_break_if {
($cond:expr) => {{}};
($cond:expr, $($var:expr),+ $(,)?) => {{}};
}
#[macro_export]
#[cfg(not(debug_assertions))]
macro_rules! print_break {
() => {{}};
($($var:expr),+ $(,)?) => {{}};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn format_json_string() {
let json = r#"{"name": "test", "value": 42}"#;
let formatted = format_value(&json);
assert!(formatted.contains("\"name\": \"test\""));
assert!(formatted.contains('\n')); }
#[test]
fn format_non_json() {
let x = 42;
let formatted = format_value(&x);
assert_eq!(formatted, "42");
}
#[test]
fn format_struct() {
#[derive(Debug)]
struct Test { a: i32, b: String }
let t = Test { a: 1, b: "hello".to_string() };
let formatted = format_value(&t);
assert!(formatted.contains("Test"));
}
#[test]
fn truncation_works() {
let long_vec: Vec<i32> = (0..1000).collect();
let formatted = format_value(&long_vec);
assert!(formatted.contains("more lines"));
}
#[test]
fn env_var_disable() {
std::env::set_var("PRINT_BREAK", "0");
assert!(!is_enabled());
std::env::set_var("PRINT_BREAK", "1");
assert!(is_enabled());
std::env::remove_var("PRINT_BREAK");
}
}