use std::fmt::Debug;
use std::io::IsTerminal;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Mutex;
use std::time::Instant;
static SKIP_ALL: AtomicBool = AtomicBool::new(false);
static BREAK_COUNT: AtomicUsize = AtomicUsize::new(0);
static LAST_BREAK_TIME: Mutex<Option<Instant>> = Mutex::new(None);
#[derive(Clone, Copy)]
pub struct BorderStyle {
pub top_left: char,
pub top_right: char,
pub bottom_left: char,
pub bottom_right: char,
pub horizontal: char,
pub vertical: char,
pub tee_right: char,
}
impl BorderStyle {
pub const ROUNDED: Self = Self {
top_left: '╭',
top_right: '╮',
bottom_left: '╰',
bottom_right: '╯',
horizontal: '─',
vertical: '│',
tee_right: '├',
};
pub const SHARP: Self = Self {
top_left: '┌',
top_right: '┐',
bottom_left: '└',
bottom_right: '┘',
horizontal: '─',
vertical: '│',
tee_right: '├',
};
pub const DOUBLE: Self = Self {
top_left: '╔',
top_right: '╗',
bottom_left: '╚',
bottom_right: '╝',
horizontal: '═',
vertical: '║',
tee_right: '╠',
};
pub const ASCII: Self = Self {
top_left: '+',
top_right: '+',
bottom_left: '+',
bottom_right: '+',
horizontal: '-',
vertical: '|',
tee_right: '+',
};
}
#[doc(hidden)]
pub fn get_border_style() -> BorderStyle {
match std::env::var("PRINT_BREAK_STYLE").as_deref() {
Ok("round") | Ok("rounded") => BorderStyle::ROUNDED,
Ok("sharp") => BorderStyle::SHARP,
Ok("double") => BorderStyle::DOUBLE,
Ok("ascii") => BorderStyle::ASCII,
_ => BorderStyle::ROUNDED,
}
}
#[derive(Clone, Copy)]
pub struct Colors {
pub green: &'static str,
pub cyan: &'static str,
pub yellow: &'static str,
pub magenta: &'static str,
pub white: &'static str,
pub gray: &'static str,
pub red: &'static str,
pub reset: &'static str,
}
impl Colors {
const TTY: Self = Self {
green: "\x1b[1;32m",
cyan: "\x1b[36m",
yellow: "\x1b[1;33m",
magenta: "\x1b[35m",
white: "\x1b[37m",
gray: "\x1b[90m",
red: "\x1b[1;31m",
reset: "\x1b[0m",
};
const PLAIN: Self = Self {
green: "",
cyan: "",
yellow: "",
magenta: "",
white: "",
gray: "",
red: "",
reset: "",
};
#[inline]
pub fn get() -> Self {
if is_tty() { Self::TTY } else { Self::PLAIN }
}
}
#[doc(hidden)]
pub fn format_elapsed(d: std::time::Duration) -> String {
let c = Colors::get();
let micros = d.as_micros();
if micros < 1000 {
format!(" {}+{}µs{}", c.gray, micros, c.reset)
} else if micros < 1_000_000 {
format!(" {}+{:.1}ms{}", c.gray, micros as f64 / 1000.0, c.reset)
} else {
format!(" {}+{:.2}s{}", c.gray, micros as f64 / 1_000_000.0, c.reset)
}
}
#[doc(hidden)]
pub fn get_elapsed() -> Option<std::time::Duration> {
if let Ok(guard) = LAST_BREAK_TIME.lock() {
guard.map(|t| t.elapsed())
} else {
None
}
}
#[doc(hidden)]
pub fn update_break_time() {
if let Ok(mut guard) = LAST_BREAK_TIME.lock() {
*guard = Some(Instant::now());
}
}
const MAX_LINES: usize = 50;
fn colorize_json(s: &str) -> String {
let c = Colors::get();
if c.cyan.is_empty() {
return s.to_string();
}
let (cyan, magenta, yellow, gray, reset) = (c.cyan, c.magenta, c.yellow, c.gray, c.reset);
let mut result = String::new();
let mut in_string = false;
let mut is_key = true;
let mut chars = s.chars().peekable();
let mut context_stack: Vec<bool> = Vec::new();
while let Some(c) = chars.next() {
match c {
'"' if !in_string => {
in_string = true;
let color = if is_key { cyan } else { magenta };
result.push_str(color);
result.push('"');
}
'"' if in_string => {
result.push('"');
result.push_str(reset);
in_string = false;
}
':' if !in_string => {
result.push_str(gray);
result.push(':');
result.push_str(reset);
is_key = false;
}
',' if !in_string => {
result.push_str(gray);
result.push(',');
result.push_str(reset);
is_key = context_stack.last().copied().unwrap_or(true);
}
'{' | '[' if !in_string => {
result.push_str(gray);
result.push(c);
result.push_str(reset);
let is_object = c == '{';
context_stack.push(is_object);
is_key = is_object;
}
'}' | ']' if !in_string => {
result.push_str(gray);
result.push(c);
result.push_str(reset);
context_stack.pop();
}
'0'..='9' | '-' | '.' if !in_string => {
result.push_str(yellow);
result.push(c);
while let Some(&next) = chars.peek() {
if next.is_ascii_digit() || next == '.' || next == 'e' || next == 'E' || next == '+' || next == '-' {
result.push(chars.next().unwrap());
} else {
break;
}
}
result.push_str(reset);
}
't' if !in_string => {
let rest: String = chars.by_ref().take(3).collect();
if rest == "rue" {
result.push_str(yellow);
result.push_str("true");
result.push_str(reset);
} else {
result.push('t');
result.push_str(&rest);
}
}
'f' if !in_string => {
let rest: String = chars.by_ref().take(4).collect();
if rest == "alse" {
result.push_str(yellow);
result.push_str("false");
result.push_str(reset);
} else {
result.push('f');
result.push_str(&rest);
}
}
'n' if !in_string => {
let rest: String = chars.by_ref().take(3).collect();
if rest == "ull" {
result.push_str(yellow);
result.push_str("null");
result.push_str(reset);
} else {
result.push('n');
result.push_str(&rest);
}
}
_ => result.push(c),
}
}
result
}
fn colorize_toml(s: &str) -> String {
let c = Colors::get();
if c.cyan.is_empty() {
return s.to_string();
}
let (green, cyan, magenta, yellow, gray, reset) =
(c.green, c.cyan, c.magenta, c.yellow, c.gray, c.reset);
let mut result = String::new();
for line in s.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') && trimmed.ends_with(']') {
result.push_str(green);
result.push_str(line);
result.push_str(reset);
} else if let Some(eq_pos) = trimmed.find(" = ") {
let indent = &line[..line.len() - trimmed.len()];
let key = &trimmed[..eq_pos];
let value = &trimmed[eq_pos + 3..];
result.push_str(indent);
result.push_str(cyan);
result.push_str(key);
result.push_str(reset);
result.push_str(gray);
result.push_str(" = ");
result.push_str(reset);
result.push_str(&colorize_toml_value(value, magenta, yellow, gray, reset));
} else {
result.push_str(line);
}
result.push('\n');
}
result.trim_end().to_string()
}
fn colorize_toml_value(s: &str, magenta: &str, yellow: &str, gray: &str, reset: &str) -> String {
let trimmed = s.trim();
if trimmed.starts_with('"') && trimmed.ends_with('"') {
format!("{}{}{}", magenta, trimmed, reset)
} else if trimmed == "true" || trimmed == "false" || trimmed.parse::<f64>().is_ok() {
format!("{}{}{}", yellow, trimmed, reset)
} else if trimmed.starts_with('[') {
let mut result = format!("{}[{}", gray, reset);
let inner = &trimmed[1..trimmed.len()-1];
let parts: Vec<&str> = inner.split(", ").collect();
for (i, part) in parts.iter().enumerate() {
if i > 0 {
result.push_str(&format!("{}, {}", gray, reset));
}
result.push_str(&colorize_toml_value(part, magenta, yellow, gray, reset));
}
result.push_str(&format!("{}]{}", gray, reset));
result
} else {
s.to_string()
}
}
fn colorize_yaml(s: &str) -> String {
let c = Colors::get();
if c.cyan.is_empty() {
return s.to_string();
}
let (cyan, magenta, yellow, gray, reset) = (c.cyan, c.magenta, c.yellow, c.gray, c.reset);
let mut result = String::new();
for line in s.lines() {
if let Some(colon_pos) = line.find(':') {
let before_colon = &line[..colon_pos];
let after_colon = &line[colon_pos + 1..];
let trimmed_before = before_colon.trim_start_matches([' ', '-']);
if !trimmed_before.is_empty() && !trimmed_before.starts_with('#') {
let indent = &before_colon[..before_colon.len() - trimmed_before.len()];
if indent.contains('-') {
let dash_pos = indent.find('-').unwrap();
result.push_str(&indent[..dash_pos]);
result.push_str(gray);
result.push('-');
result.push_str(reset);
result.push_str(&indent[dash_pos + 1..]);
} else {
result.push_str(indent);
}
result.push_str(cyan);
result.push_str(trimmed_before);
result.push_str(reset);
result.push_str(gray);
result.push(':');
result.push_str(reset);
let value = after_colon.trim();
if !value.is_empty() {
result.push(' ');
result.push_str(&colorize_yaml_value(value, magenta, yellow, reset));
}
} else {
result.push_str(line);
}
} else if line.trim().starts_with('-') {
let trimmed = line.trim();
let indent = &line[..line.len() - trimmed.len()];
result.push_str(indent);
result.push_str(gray);
result.push('-');
result.push_str(reset);
result.push_str(&colorize_yaml_value(trimmed[1..].trim(), magenta, yellow, reset));
} else {
result.push_str(line);
}
result.push('\n');
}
result.trim_end().to_string()
}
fn colorize_yaml_value(s: &str, magenta: &str, yellow: &str, reset: &str) -> String {
let trimmed = s.trim();
if trimmed.starts_with('"') || trimmed.starts_with('\'') {
format!("{}{}{}", magenta, trimmed, reset)
} else if matches!(trimmed, "true" | "false" | "null" | "~") || trimmed.parse::<f64>().is_ok() {
format!("{}{}{}", yellow, trimmed, reset)
} else if !trimmed.is_empty() && !trimmed.contains(':') {
format!("{}{}{}", magenta, trimmed, reset)
} else {
s.to_string()
}
}
#[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 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();
let c = Colors::get();
let (gray, reset) = (c.gray, c.reset);
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) {
let colorized = colorize_json(&pretty);
raw_output = format!("{}(json){}\n{}", gray, reset, colorized);
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) {
let colorized = colorize_toml(&pretty);
raw_output = format!("{}(toml){}\n{}", gray, reset, colorized);
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) {
let colorized = colorize_yaml(pretty.trim());
raw_output = format!("{}(yaml){}\n{}", gray, reset, colorized);
return truncate_output(&raw_output);
}
}
}
}
raw_output = format!("{}(string, {} chars){}\n{}", gray, unescaped.len(), reset, 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 c = Colors::get();
if c.cyan.is_empty() {
return s.to_string();
}
let (green, cyan, yellow, magenta, white, gray, reset) =
(c.green, c.cyan, c.yellow, c.magenta, c.white, c.gray, c.reset);
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(['{', '[', '(', ' ']);
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(['{', '[', '(', ' ']);
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 c = Colors::get();
let truncated = lines[..MAX_LINES].join("\n");
format!("{}\n{}... ({} more lines){}", truncated, c.gray, lines.len() - MAX_LINES, c.reset)
} 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);
}
}
fn show_help() {
eprintln!("\n\x1b[1;33m─── print-break Help ───\x1b[0m");
eprintln!("\x1b[36mEnter\x1b[0m Continue to next breakpoint");
eprintln!("\x1b[36mm\x1b[0m Show full output (if truncated)");
eprintln!("\x1b[36mt\x1b[0m Show stack trace");
eprintln!("\x1b[36mc\x1b[0m Copy last value to clipboard");
eprintln!("\x1b[36ms\x1b[0m Skip all remaining breakpoints");
eprintln!("\x1b[36mq\x1b[0m Quit the program");
eprintln!("\x1b[36mh / ?\x1b[0m Show this help");
eprintln!();
eprintln!("\x1b[90mEnvironment variables:\x1b[0m");
eprintln!(" \x1b[36mPRINT_BREAK=0\x1b[0m Disable all breakpoints");
eprintln!(" \x1b[36mPRINT_BREAK_DEPTH=N\x1b[0m Max nesting depth (default: 4)");
eprintln!(" \x1b[36mPRINT_BREAK_STYLE=X\x1b[0m Border style: rounded, sharp, double, ascii");
eprintln!("\x1b[1;33m─────────────────────────\x1b[0m\n");
}
fn show_stack_trace() {
eprintln!("\n\x1b[1;33m─── Stack Trace ───\x1b[0m");
let bt = backtrace::Backtrace::new();
let mut in_relevant = false;
let mut count = 0;
for frame in bt.frames() {
for symbol in frame.symbols() {
if let Some(name) = symbol.name() {
let name_str = name.to_string();
if name_str.contains("print_break::") || name_str.contains("backtrace::") {
continue;
}
if !in_relevant && !name_str.contains("print_break") {
in_relevant = true;
}
if in_relevant {
let file = symbol.filename()
.map(|p| p.display().to_string())
.unwrap_or_default();
let line = symbol.lineno().unwrap_or(0);
let short_file = file.rsplit('/').next().unwrap_or(&file);
if !name_str.contains("std::") && !name_str.contains("core::") && !name_str.contains("__rust") {
eprintln!("\x1b[90m{:>3}.\x1b[0m \x1b[36m{}\x1b[0m", count, name_str);
if !file.is_empty() && line > 0 {
eprintln!(" \x1b[90mat {}:{}\x1b[0m", short_file, line);
}
count += 1;
if count >= 15 {
eprintln!("\x1b[90m ... (truncated)\x1b[0m");
break;
}
}
}
}
}
if count >= 15 {
break;
}
}
eprintln!("\x1b[1;33m───────────────────\x1b[0m\n");
}
fn copy_to_clipboard(text: &str) -> bool {
use std::process::{Command, Stdio};
use std::io::Write as IoWrite;
let commands = if cfg!(target_os = "macos") {
vec![("pbcopy", vec![])]
} else if cfg!(target_os = "windows") {
vec![("clip", vec![])]
} else {
vec![
("xclip", vec!["-selection", "clipboard"]),
("xsel", vec!["--clipboard", "--input"]),
("wl-copy", vec![]),
]
};
for (cmd, args) in commands {
if let Ok(mut child) = Command::new(cmd)
.args(&args)
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
{
if let Some(mut stdin) = child.stdin.take() {
if stdin.write_all(text.as_bytes()).is_ok() {
drop(stdin);
if child.wait().map(|s| s.success()).unwrap_or(false) {
return true;
}
}
}
}
}
false
}
#[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, m=more, t=trace, c=copy, s=skip, q=quit, h=help]\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;
}
"t" | "trace" => {
show_stack_trace();
continue;
}
"c" | "copy" => {
if let Ok(guard) = LAST_FULL_OUTPUT.lock() {
if let Some(ref full) = *guard {
let clean = strip_ansi_codes(full);
if copy_to_clipboard(&clean) {
eprintln!("\x1b[1;32mCopied to clipboard!\x1b[0m");
} else {
eprintln!("\x1b[1;31mFailed to copy (install xclip or xsel)\x1b[0m");
}
} else {
eprintln!("\x1b[90m(nothing to copy)\x1b[0m");
}
}
continue;
}
"h" | "?" | "help" => {
show_help();
continue;
}
_ => break }
} else {
break;
}
}
eprintln!();
true
}
fn strip_ansi_codes(s: &str) -> String {
let mut result = String::new();
let mut in_escape = false;
for c in s.chars() {
if c == '\x1b' {
in_escape = true;
} else if in_escape {
if c == 'm' {
in_escape = false;
}
} else {
result.push(c);
}
}
result
}
#[macro_export]
#[cfg(debug_assertions)]
macro_rules! print_break {
() => {{
if $crate::is_enabled() {
let break_id = $crate::next_break_id();
let elapsed_str = $crate::get_elapsed().map($crate::format_elapsed).unwrap_or_default();
$crate::update_break_time();
let location = format!("{}:{}", file!(), line!());
let width = 50;
let border = $crate::get_border_style();
let c = $crate::Colors::get();
let h = border.horizontal.to_string();
eprintln!();
eprintln!("{}{}{} BREAK #{} {}{}{}", c.yellow, border.top_left, h, break_id, elapsed_str, h.repeat(width - 14 - break_id.to_string().len() - elapsed_str.len() / 3), c.reset);
eprintln!("{}{}{} {}{}{}", c.yellow, border.vertical, c.reset, c.cyan, location, c.reset);
eprintln!("{}{}{}{}", c.yellow, border.bottom_left, h.repeat(width), c.reset);
$crate::handle_input();
}
}};
($($var:expr),+ $(,)?) => {{
if $crate::is_enabled() {
let break_id = $crate::next_break_id();
let elapsed_str = $crate::get_elapsed().map($crate::format_elapsed).unwrap_or_default();
$crate::update_break_time();
let location = format!("{}:{}", file!(), line!());
let width = 50;
let border = $crate::get_border_style();
let c = $crate::Colors::get();
let mut full_output = String::new();
let h = border.horizontal.to_string();
eprintln!();
eprintln!("{}{}{} BREAK #{} {}{}{}", c.yellow, border.top_left, h, break_id, elapsed_str, h.repeat(width - 14 - break_id.to_string().len() - elapsed_str.len() / 3), c.reset);
eprintln!("{}{}{} {}{}{}", c.yellow, border.vertical, c.reset, c.cyan, location, c.reset);
eprintln!("{}{}{}{}", c.yellow, border.tee_right, h.repeat(width), c.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!("{}{}{} {}{}{}=", c.yellow, border.vertical, c.reset, c.green, name, c.reset);
for line in formatted.lines() {
eprintln!("{}{}{} {}{}{}", c.yellow, border.vertical, c.reset, c.white, line, c.reset);
}
} else {
eprintln!("{}{}{} {}{}{} = {}{}{}", c.yellow, border.vertical, c.reset, c.green, name, c.reset, c.white, formatted, c.reset);
}
)+
$crate::store_full_output(full_output);
eprintln!("{}{}{}{}", c.yellow, border.bottom_left, h.repeat(width), c.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");
}
}