1use std::fmt::Debug;
47use std::io::IsTerminal;
48use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
49use std::sync::Mutex;
50use std::time::Instant;
51
52static SKIP_ALL: AtomicBool = AtomicBool::new(false);
54
55static BREAK_COUNT: AtomicUsize = AtomicUsize::new(0);
57
58static LAST_BREAK_TIME: Mutex<Option<Instant>> = Mutex::new(None);
60
61#[derive(Clone, Copy)]
63pub struct BorderStyle {
64 pub top_left: char,
65 pub top_right: char,
66 pub bottom_left: char,
67 pub bottom_right: char,
68 pub horizontal: char,
69 pub vertical: char,
70 pub tee_right: char,
71}
72
73impl BorderStyle {
74 pub const ROUNDED: Self = Self {
75 top_left: '╭',
76 top_right: '╮',
77 bottom_left: '╰',
78 bottom_right: '╯',
79 horizontal: '─',
80 vertical: '│',
81 tee_right: '├',
82 };
83
84 pub const SHARP: Self = Self {
85 top_left: '┌',
86 top_right: '┐',
87 bottom_left: '└',
88 bottom_right: '┘',
89 horizontal: '─',
90 vertical: '│',
91 tee_right: '├',
92 };
93
94 pub const DOUBLE: Self = Self {
95 top_left: '╔',
96 top_right: '╗',
97 bottom_left: '╚',
98 bottom_right: '╝',
99 horizontal: '═',
100 vertical: '║',
101 tee_right: '╠',
102 };
103
104 pub const ASCII: Self = Self {
105 top_left: '+',
106 top_right: '+',
107 bottom_left: '+',
108 bottom_right: '+',
109 horizontal: '-',
110 vertical: '|',
111 tee_right: '+',
112 };
113}
114
115#[doc(hidden)]
117pub fn get_border_style() -> BorderStyle {
118 match std::env::var("PRINT_BREAK_STYLE").as_deref() {
119 Ok("round") | Ok("rounded") => BorderStyle::ROUNDED,
120 Ok("sharp") => BorderStyle::SHARP,
121 Ok("double") => BorderStyle::DOUBLE,
122 Ok("ascii") => BorderStyle::ASCII,
123 _ => BorderStyle::ROUNDED,
124 }
125}
126
127#[derive(Clone, Copy)]
133pub struct Colors {
134 pub green: &'static str,
135 pub cyan: &'static str,
136 pub yellow: &'static str,
137 pub magenta: &'static str,
138 pub white: &'static str,
139 pub gray: &'static str,
140 pub red: &'static str,
141 pub reset: &'static str,
142}
143
144impl Colors {
145 const TTY: Self = Self {
146 green: "\x1b[1;32m",
147 cyan: "\x1b[36m",
148 yellow: "\x1b[1;33m",
149 magenta: "\x1b[35m",
150 white: "\x1b[37m",
151 gray: "\x1b[90m",
152 red: "\x1b[1;31m",
153 reset: "\x1b[0m",
154 };
155
156 const PLAIN: Self = Self {
157 green: "",
158 cyan: "",
159 yellow: "",
160 magenta: "",
161 white: "",
162 gray: "",
163 red: "",
164 reset: "",
165 };
166
167 #[inline]
169 pub fn get() -> Self {
170 if is_tty() { Self::TTY } else { Self::PLAIN }
171 }
172}
173
174#[doc(hidden)]
176pub fn format_elapsed(d: std::time::Duration) -> String {
177 let c = Colors::get();
178 let micros = d.as_micros();
179 if micros < 1000 {
180 format!(" {}+{}µs{}", c.gray, micros, c.reset)
181 } else if micros < 1_000_000 {
182 format!(" {}+{:.1}ms{}", c.gray, micros as f64 / 1000.0, c.reset)
183 } else {
184 format!(" {}+{:.2}s{}", c.gray, micros as f64 / 1_000_000.0, c.reset)
185 }
186}
187
188#[doc(hidden)]
190pub fn get_elapsed() -> Option<std::time::Duration> {
191 if let Ok(guard) = LAST_BREAK_TIME.lock() {
192 guard.map(|t| t.elapsed())
193 } else {
194 None
195 }
196}
197
198#[doc(hidden)]
200pub fn update_break_time() {
201 if let Ok(mut guard) = LAST_BREAK_TIME.lock() {
202 *guard = Some(Instant::now());
203 }
204}
205
206const MAX_LINES: usize = 50;
208
209fn colorize_json(s: &str) -> String {
211 let c = Colors::get();
212 if c.cyan.is_empty() {
213 return s.to_string();
214 }
215
216 let (cyan, magenta, yellow, gray, reset) = (c.cyan, c.magenta, c.yellow, c.gray, c.reset);
217
218 let mut result = String::new();
219 let mut in_string = false;
220 let mut is_key = true;
221 let mut chars = s.chars().peekable();
222 let mut context_stack: Vec<bool> = Vec::new();
224
225 while let Some(c) = chars.next() {
226 match c {
227 '"' if !in_string => {
228 in_string = true;
229 let color = if is_key { cyan } else { magenta };
230 result.push_str(color);
231 result.push('"');
232 }
233 '"' if in_string => {
234 result.push('"');
235 result.push_str(reset);
236 in_string = false;
237 }
238 ':' if !in_string => {
239 result.push_str(gray);
240 result.push(':');
241 result.push_str(reset);
242 is_key = false;
243 }
244 ',' if !in_string => {
245 result.push_str(gray);
246 result.push(',');
247 result.push_str(reset);
248 is_key = context_stack.last().copied().unwrap_or(true);
250 }
251 '{' | '[' if !in_string => {
252 result.push_str(gray);
253 result.push(c);
254 result.push_str(reset);
255 let is_object = c == '{';
256 context_stack.push(is_object);
257 is_key = is_object;
258 }
259 '}' | ']' if !in_string => {
260 result.push_str(gray);
261 result.push(c);
262 result.push_str(reset);
263 context_stack.pop();
264 }
265 '0'..='9' | '-' | '.' if !in_string => {
266 result.push_str(yellow);
267 result.push(c);
268 while let Some(&next) = chars.peek() {
270 if next.is_ascii_digit() || next == '.' || next == 'e' || next == 'E' || next == '+' || next == '-' {
271 result.push(chars.next().unwrap());
272 } else {
273 break;
274 }
275 }
276 result.push_str(reset);
277 }
278 't' if !in_string => {
279 let rest: String = chars.by_ref().take(3).collect();
281 if rest == "rue" {
282 result.push_str(yellow);
283 result.push_str("true");
284 result.push_str(reset);
285 } else {
286 result.push('t');
287 result.push_str(&rest);
288 }
289 }
290 'f' if !in_string => {
291 let rest: String = chars.by_ref().take(4).collect();
293 if rest == "alse" {
294 result.push_str(yellow);
295 result.push_str("false");
296 result.push_str(reset);
297 } else {
298 result.push('f');
299 result.push_str(&rest);
300 }
301 }
302 'n' if !in_string => {
303 let rest: String = chars.by_ref().take(3).collect();
305 if rest == "ull" {
306 result.push_str(yellow);
307 result.push_str("null");
308 result.push_str(reset);
309 } else {
310 result.push('n');
311 result.push_str(&rest);
312 }
313 }
314 _ => result.push(c),
315 }
316 }
317
318 result
319}
320
321fn colorize_toml(s: &str) -> String {
323 let c = Colors::get();
324 if c.cyan.is_empty() {
325 return s.to_string();
326 }
327
328 let (green, cyan, magenta, yellow, gray, reset) =
329 (c.green, c.cyan, c.magenta, c.yellow, c.gray, c.reset);
330
331 let mut result = String::new();
332
333 for line in s.lines() {
334 let trimmed = line.trim();
335
336 if trimmed.starts_with('[') && trimmed.ends_with(']') {
337 result.push_str(green);
339 result.push_str(line);
340 result.push_str(reset);
341 } else if let Some(eq_pos) = trimmed.find(" = ") {
342 let indent = &line[..line.len() - trimmed.len()];
344 let key = &trimmed[..eq_pos];
345 let value = &trimmed[eq_pos + 3..];
346
347 result.push_str(indent);
348 result.push_str(cyan);
349 result.push_str(key);
350 result.push_str(reset);
351 result.push_str(gray);
352 result.push_str(" = ");
353 result.push_str(reset);
354 result.push_str(&colorize_toml_value(value, magenta, yellow, gray, reset));
355 } else {
356 result.push_str(line);
357 }
358 result.push('\n');
359 }
360
361 result.trim_end().to_string()
362}
363
364fn colorize_toml_value(s: &str, magenta: &str, yellow: &str, gray: &str, reset: &str) -> String {
365 let trimmed = s.trim();
366
367 if trimmed.starts_with('"') && trimmed.ends_with('"') {
368 format!("{}{}{}", magenta, trimmed, reset)
369 } else if trimmed == "true" || trimmed == "false" || trimmed.parse::<f64>().is_ok() {
370 format!("{}{}{}", yellow, trimmed, reset)
371 } else if trimmed.starts_with('[') {
372 let mut result = format!("{}[{}", gray, reset);
374 let inner = &trimmed[1..trimmed.len()-1];
375 let parts: Vec<&str> = inner.split(", ").collect();
376 for (i, part) in parts.iter().enumerate() {
377 if i > 0 {
378 result.push_str(&format!("{}, {}", gray, reset));
379 }
380 result.push_str(&colorize_toml_value(part, magenta, yellow, gray, reset));
381 }
382 result.push_str(&format!("{}]{}", gray, reset));
383 result
384 } else {
385 s.to_string()
386 }
387}
388
389fn colorize_yaml(s: &str) -> String {
391 let c = Colors::get();
392 if c.cyan.is_empty() {
393 return s.to_string();
394 }
395
396 let (cyan, magenta, yellow, gray, reset) = (c.cyan, c.magenta, c.yellow, c.gray, c.reset);
397
398 let mut result = String::new();
399
400 for line in s.lines() {
401 if let Some(colon_pos) = line.find(':') {
402 let before_colon = &line[..colon_pos];
403 let after_colon = &line[colon_pos + 1..];
404
405 let trimmed_before = before_colon.trim_start_matches([' ', '-']);
407
408 if !trimmed_before.is_empty() && !trimmed_before.starts_with('#') {
409 let indent = &before_colon[..before_colon.len() - trimmed_before.len()];
410
411 if indent.contains('-') {
413 let dash_pos = indent.find('-').unwrap();
414 result.push_str(&indent[..dash_pos]);
415 result.push_str(gray);
416 result.push('-');
417 result.push_str(reset);
418 result.push_str(&indent[dash_pos + 1..]);
419 } else {
420 result.push_str(indent);
421 }
422
423 result.push_str(cyan);
424 result.push_str(trimmed_before);
425 result.push_str(reset);
426 result.push_str(gray);
427 result.push(':');
428 result.push_str(reset);
429
430 let value = after_colon.trim();
432 if !value.is_empty() {
433 result.push(' ');
434 result.push_str(&colorize_yaml_value(value, magenta, yellow, reset));
435 }
436 } else {
437 result.push_str(line);
438 }
439 } else if line.trim().starts_with('-') {
440 let trimmed = line.trim();
442 let indent = &line[..line.len() - trimmed.len()];
443 result.push_str(indent);
444 result.push_str(gray);
445 result.push('-');
446 result.push_str(reset);
447 result.push_str(&colorize_yaml_value(trimmed[1..].trim(), magenta, yellow, reset));
448 } else {
449 result.push_str(line);
450 }
451 result.push('\n');
452 }
453
454 result.trim_end().to_string()
455}
456
457fn colorize_yaml_value(s: &str, magenta: &str, yellow: &str, reset: &str) -> String {
458 let trimmed = s.trim();
459
460 if trimmed.starts_with('"') || trimmed.starts_with('\'') {
461 format!("{}{}{}", magenta, trimmed, reset)
462 } else if matches!(trimmed, "true" | "false" | "null" | "~") || trimmed.parse::<f64>().is_ok() {
463 format!("{}{}{}", yellow, trimmed, reset)
464 } else if !trimmed.is_empty() && !trimmed.contains(':') {
465 format!("{}{}{}", magenta, trimmed, reset)
467 } else {
468 s.to_string()
469 }
470}
471
472#[doc(hidden)]
474pub fn is_enabled() -> bool {
475 if SKIP_ALL.load(Ordering::Relaxed) {
476 return false;
477 }
478 match std::env::var("PRINT_BREAK") {
479 Ok(val) => !matches!(val.as_str(), "0" | "false" | "no" | "off"),
480 Err(_) => true, }
482}
483
484#[doc(hidden)]
486pub fn is_tty() -> bool {
487 std::io::stderr().is_terminal() && std::io::stdin().is_terminal()
488}
489
490#[doc(hidden)]
492pub fn next_break_id() -> usize {
493 BREAK_COUNT.fetch_add(1, Ordering::Relaxed) + 1
494}
495
496#[doc(hidden)]
498pub fn set_skip_all(skip: bool) {
499 SKIP_ALL.store(skip, Ordering::Relaxed);
500}
501
502#[doc(hidden)]
506pub fn format_value<T: Debug>(value: &T) -> String {
507 let debug_str = format!("{:?}", value);
508 let raw_output;
509
510 if let Some(inner) = debug_str.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
512 let unescaped = inner
514 .replace("\\\"", "\"")
515 .replace("\\n", "\n")
516 .replace("\\t", "\t")
517 .replace("\\\\", "\\");
518
519 let trimmed = unescaped.trim();
520
521 let c = Colors::get();
522 let (gray, reset) = (c.gray, c.reset);
523
524 if trimmed.starts_with('{') || trimmed.starts_with('[') {
526 if let Ok(json) = serde_json::from_str::<serde_json::Value>(&unescaped) {
527 if let Ok(pretty) = serde_json::to_string_pretty(&json) {
528 let colorized = colorize_json(&pretty);
529 raw_output = format!("{}(json){}\n{}", gray, reset, colorized);
530 return truncate_output(&raw_output);
531 }
532 }
533 }
534
535 if trimmed.contains(" = ") || trimmed.contains("]\n") || trimmed.starts_with('[') {
537 if let Ok(toml_val) = toml::from_str::<toml::Value>(&unescaped) {
538 if let Ok(pretty) = toml::to_string_pretty(&toml_val) {
539 let colorized = colorize_toml(&pretty);
540 raw_output = format!("{}(toml){}\n{}", gray, reset, colorized);
541 return truncate_output(&raw_output);
542 }
543 }
544 }
545
546 if trimmed.contains(": ") || trimmed.contains(":\n") {
548 if let Ok(yaml_val) = serde_yaml::from_str::<serde_yaml::Value>(&unescaped) {
549 if yaml_val.is_mapping() || yaml_val.is_sequence() {
551 if let Ok(pretty) = serde_yaml::to_string(&yaml_val) {
552 let colorized = colorize_yaml(pretty.trim());
553 raw_output = format!("{}(yaml){}\n{}", gray, reset, colorized);
554 return truncate_output(&raw_output);
555 }
556 }
557 }
558 }
559
560 raw_output = format!("{}(string, {} chars){}\n{}", gray, unescaped.len(), reset, word_wrap(&unescaped, 80));
562 return truncate_output(&raw_output);
563 }
564
565 let debug_output = format!("{:#?}", value);
567 raw_output = colorize_debug(&debug_output);
568 truncate_output(&raw_output)
569}
570
571#[doc(hidden)]
573pub fn format_value_full<T: Debug>(value: &T) -> String {
574 let debug_str = format!("{:?}", value);
575
576 if let Some(inner) = debug_str.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
578 let unescaped = inner
580 .replace("\\\"", "\"")
581 .replace("\\n", "\n")
582 .replace("\\t", "\t")
583 .replace("\\\\", "\\");
584
585 let trimmed = unescaped.trim();
586
587 if trimmed.starts_with('{') || trimmed.starts_with('[') {
589 if let Ok(json) = serde_json::from_str::<serde_json::Value>(&unescaped) {
590 if let Ok(pretty) = serde_json::to_string_pretty(&json) {
591 return pretty;
592 }
593 }
594 }
595
596 if trimmed.contains(" = ") || trimmed.contains("]\n") || trimmed.starts_with('[') {
598 if let Ok(toml_val) = toml::from_str::<toml::Value>(&unescaped) {
599 if let Ok(pretty) = toml::to_string_pretty(&toml_val) {
600 return pretty;
601 }
602 }
603 }
604
605 if trimmed.contains(": ") || trimmed.contains(":\n") {
607 if let Ok(yaml_val) = serde_yaml::from_str::<serde_yaml::Value>(&unescaped) {
608 if yaml_val.is_mapping() || yaml_val.is_sequence() {
609 if let Ok(pretty) = serde_yaml::to_string(&yaml_val) {
610 return pretty.trim().to_string();
611 }
612 }
613 }
614 }
615
616 return word_wrap(&unescaped, 100);
618 }
619
620 colorize_debug(&format!("{:#?}", value))
622}
623
624const DEFAULT_MAX_DEPTH: usize = 4;
626
627fn max_depth() -> usize {
629 std::env::var("PRINT_BREAK_DEPTH")
630 .ok()
631 .and_then(|v| v.parse().ok())
632 .unwrap_or(DEFAULT_MAX_DEPTH)
633}
634
635fn colorize_debug(s: &str) -> String {
637 let c = Colors::get();
638 if c.cyan.is_empty() {
639 return s.to_string();
640 }
641
642 let (green, cyan, yellow, magenta, white, gray, reset) =
643 (c.green, c.cyan, c.yellow, c.magenta, c.white, c.gray, c.reset);
644
645 let mut result = String::new();
646 let lines: Vec<&str> = s.lines().collect();
647 let mut current_depth: usize = 0;
648 let mut skip_until_depth: Option<usize> = None;
649
650 for line in lines {
651 let trimmed = line.trim_start();
652 let indent_count = line.len() - trimmed.len();
653 let indent_level = indent_count / 4;
654
655 let opens = trimmed.ends_with('{') || trimmed.ends_with('[') || trimmed.ends_with('(');
657 let closes = trimmed.starts_with('}') || trimmed.starts_with(']') || trimmed.starts_with(')');
658
659 if closes {
660 current_depth = current_depth.saturating_sub(1);
661 }
662
663 if let Some(skip_depth) = skip_until_depth {
665 if current_depth < skip_depth {
666 skip_until_depth = None;
667 } else {
668 if opens {
669 current_depth += 1;
670 }
671 continue;
672 }
673 }
674
675 if opens && current_depth >= max_depth() {
677 for _ in 0..indent_level {
679 result.push_str(&format!("{}│{} ", gray, reset));
680 }
681
682 let name = trimmed.trim_end_matches(['{', '[', '(', ' ']);
684 if !name.is_empty() {
685 result.push_str(&format!("{}{}{} {}{{ ... }}{}", green, name, reset, gray, reset));
686 } else {
687 result.push_str(&format!("{}[ ... ]{}", gray, reset));
688 }
689 result.push('\n');
690
691 skip_until_depth = Some(current_depth);
692 current_depth += 1;
693 continue;
694 }
695
696 for _ in 0..indent_level {
698 result.push_str(&format!("{}│{} ", gray, reset));
699 }
700
701 if opens {
703 let name = trimmed.trim_end_matches(['{', '[', '(', ' ']);
705 let bracket = trimmed.chars().last().unwrap_or(' ');
706 if !name.is_empty() {
707 result.push_str(&format!("{}{}{} {}{}{}", green, name, reset, gray, bracket, reset));
708 } else {
709 result.push_str(&format!("{}{}{}", gray, bracket, reset));
710 }
711 current_depth += 1;
712 } else if closes || trimmed.ends_with("},") || trimmed.ends_with("],") || trimmed.ends_with("),") {
713 result.push_str(&format!("{}{}{}", gray, trimmed, reset));
715 } else if trimmed.contains(": ") {
716 if let Some(colon_pos) = trimmed.find(": ") {
718 let field = &trimmed[..colon_pos];
719 let value = &trimmed[colon_pos + 2..];
720 let colored_value = colorize_value(value, yellow, magenta, white, gray, reset);
721 result.push_str(&format!("{}{}{}{}: {}", cyan, field, reset, gray, colored_value));
722 } else {
723 result.push_str(trimmed);
724 }
725 } else {
726 let colored = colorize_value(trimmed, yellow, magenta, white, gray, reset);
728 result.push_str(&colored);
729 }
730 result.push('\n');
731 }
732
733 result.trim_end().to_string()
734}
735
736fn colorize_value(s: &str, yellow: &str, magenta: &str, white: &str, gray: &str, reset: &str) -> String {
738 let trimmed = s.trim_end_matches(',');
739 let has_comma = s.ends_with(',');
740 let comma = if has_comma { format!("{},{}", gray, reset) } else { String::new() };
741
742 if trimmed.starts_with('"') {
743 format!("{}{}{}{}", magenta, trimmed, reset, comma)
745 } else if trimmed.parse::<f64>().is_ok() || trimmed.starts_with('-') && trimmed[1..].parse::<f64>().is_ok() {
746 format!("{}{}{}{}", yellow, trimmed, reset, comma)
748 } else if trimmed == "true" || trimmed == "false" {
749 format!("{}{}{}{}", yellow, trimmed, reset, comma)
751 } else if trimmed == "None" || trimmed.starts_with("Some(") {
752 format!("{}{}{}{}", white, trimmed, reset, comma)
754 } else {
755 format!("{}{}{}{}", white, trimmed, reset, comma)
756 }
757}
758
759fn word_wrap(s: &str, width: usize) -> String {
761 let mut result = String::new();
762 for line in s.lines() {
763 if line.len() <= width {
764 result.push_str(line);
765 result.push('\n');
766 } else {
767 let mut current_line = String::new();
768 for word in line.split_whitespace() {
769 if current_line.is_empty() {
770 current_line = word.to_string();
771 } else if current_line.len() + 1 + word.len() <= width {
772 current_line.push(' ');
773 current_line.push_str(word);
774 } else {
775 result.push_str(¤t_line);
776 result.push('\n');
777 current_line = word.to_string();
778 }
779 }
780 if !current_line.is_empty() {
781 result.push_str(¤t_line);
782 result.push('\n');
783 }
784 }
785 }
786 result.trim_end().to_string()
787}
788
789fn truncate_output(s: &str) -> String {
791 let lines: Vec<&str> = s.lines().collect();
792 if lines.len() > MAX_LINES {
793 let c = Colors::get();
794 let truncated = lines[..MAX_LINES].join("\n");
795 format!("{}\n{}... ({} more lines){}", truncated, c.gray, lines.len() - MAX_LINES, c.reset)
796 } else {
797 s.to_string()
798 }
799}
800
801static LAST_FULL_OUTPUT: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
803
804#[doc(hidden)]
806pub fn store_full_output(output: String) {
807 if let Ok(mut guard) = LAST_FULL_OUTPUT.lock() {
808 *guard = Some(output);
809 }
810}
811
812fn show_help() {
814 eprintln!("\n\x1b[1;33m─── print-break Help ───\x1b[0m");
815 eprintln!("\x1b[36mEnter\x1b[0m Continue to next breakpoint");
816 eprintln!("\x1b[36mm\x1b[0m Show full output (if truncated)");
817 eprintln!("\x1b[36mt\x1b[0m Show stack trace");
818 eprintln!("\x1b[36mc\x1b[0m Copy last value to clipboard");
819 eprintln!("\x1b[36ms\x1b[0m Skip all remaining breakpoints");
820 eprintln!("\x1b[36mq\x1b[0m Quit the program");
821 eprintln!("\x1b[36mh / ?\x1b[0m Show this help");
822 eprintln!();
823 eprintln!("\x1b[90mEnvironment variables:\x1b[0m");
824 eprintln!(" \x1b[36mPRINT_BREAK=0\x1b[0m Disable all breakpoints");
825 eprintln!(" \x1b[36mPRINT_BREAK_DEPTH=N\x1b[0m Max nesting depth (default: 4)");
826 eprintln!(" \x1b[36mPRINT_BREAK_STYLE=X\x1b[0m Border style: rounded, sharp, double, ascii");
827 eprintln!("\x1b[1;33m─────────────────────────\x1b[0m\n");
828}
829
830fn show_stack_trace() {
832 eprintln!("\n\x1b[1;33m─── Stack Trace ───\x1b[0m");
833
834 let bt = backtrace::Backtrace::new();
835 let mut in_relevant = false;
836 let mut count = 0;
837
838 for frame in bt.frames() {
839 for symbol in frame.symbols() {
840 if let Some(name) = symbol.name() {
841 let name_str = name.to_string();
842
843 if name_str.contains("print_break::") || name_str.contains("backtrace::") {
845 continue;
846 }
847
848 if !in_relevant && !name_str.contains("print_break") {
850 in_relevant = true;
851 }
852
853 if in_relevant {
854 let file = symbol.filename()
855 .map(|p| p.display().to_string())
856 .unwrap_or_default();
857 let line = symbol.lineno().unwrap_or(0);
858
859 let short_file = file.rsplit('/').next().unwrap_or(&file);
861
862 if !name_str.contains("std::") && !name_str.contains("core::") && !name_str.contains("__rust") {
863 eprintln!("\x1b[90m{:>3}.\x1b[0m \x1b[36m{}\x1b[0m", count, name_str);
864 if !file.is_empty() && line > 0 {
865 eprintln!(" \x1b[90mat {}:{}\x1b[0m", short_file, line);
866 }
867 count += 1;
868
869 if count >= 15 {
870 eprintln!("\x1b[90m ... (truncated)\x1b[0m");
871 break;
872 }
873 }
874 }
875 }
876 }
877 if count >= 15 {
878 break;
879 }
880 }
881
882 eprintln!("\x1b[1;33m───────────────────\x1b[0m\n");
883}
884
885fn copy_to_clipboard(text: &str) -> bool {
887 use std::process::{Command, Stdio};
888 use std::io::Write as IoWrite;
889
890 let commands = if cfg!(target_os = "macos") {
892 vec![("pbcopy", vec![])]
893 } else if cfg!(target_os = "windows") {
894 vec![("clip", vec![])]
895 } else {
896 vec![
898 ("xclip", vec!["-selection", "clipboard"]),
899 ("xsel", vec!["--clipboard", "--input"]),
900 ("wl-copy", vec![]),
901 ]
902 };
903
904 for (cmd, args) in commands {
905 if let Ok(mut child) = Command::new(cmd)
906 .args(&args)
907 .stdin(Stdio::piped())
908 .stdout(Stdio::null())
909 .stderr(Stdio::null())
910 .spawn()
911 {
912 if let Some(mut stdin) = child.stdin.take() {
913 if stdin.write_all(text.as_bytes()).is_ok() {
914 drop(stdin);
915 if child.wait().map(|s| s.success()).unwrap_or(false) {
916 return true;
917 }
918 }
919 }
920 }
921 }
922 false
923}
924
925#[doc(hidden)]
927pub fn handle_input() -> bool {
928 use std::io::{self, BufRead, Write};
929
930 if !is_tty() {
932 eprintln!("(non-interactive mode, continuing...)");
933 return true;
934 }
935
936 loop {
937 eprint!("\x1b[90m[Enter, m=more, t=trace, c=copy, s=skip, q=quit, h=help]\x1b[0m ");
938 io::stderr().flush().unwrap();
939
940 let stdin = io::stdin();
941 let mut line = String::new();
942 if stdin.lock().read_line(&mut line).is_ok() {
943 let input = line.trim().to_lowercase();
944 match input.as_str() {
945 "q" | "quit" => {
946 eprintln!("\x1b[1;31mQuitting...\x1b[0m");
947 std::process::exit(0);
948 }
949 "s" | "skip" => {
950 eprintln!("\x1b[1;33mSkipping remaining breakpoints...\x1b[0m");
951 set_skip_all(true);
952 break;
953 }
954 "m" | "more" => {
955 if let Ok(guard) = LAST_FULL_OUTPUT.lock() {
957 if let Some(ref full) = *guard {
958 eprintln!("\n\x1b[1;33m─── Full Output ───\x1b[0m");
959 for line in full.lines() {
960 eprintln!("\x1b[37m{}\x1b[0m", line);
961 }
962 eprintln!("\x1b[1;33m───────────────────\x1b[0m\n");
963 } else {
964 eprintln!("\x1b[90m(no truncated output to show)\x1b[0m");
965 }
966 }
967 continue;
968 }
969 "t" | "trace" => {
970 show_stack_trace();
971 continue;
972 }
973 "c" | "copy" => {
974 if let Ok(guard) = LAST_FULL_OUTPUT.lock() {
975 if let Some(ref full) = *guard {
976 let clean = strip_ansi_codes(full);
978 if copy_to_clipboard(&clean) {
979 eprintln!("\x1b[1;32mCopied to clipboard!\x1b[0m");
980 } else {
981 eprintln!("\x1b[1;31mFailed to copy (install xclip or xsel)\x1b[0m");
982 }
983 } else {
984 eprintln!("\x1b[90m(nothing to copy)\x1b[0m");
985 }
986 }
987 continue;
988 }
989 "h" | "?" | "help" => {
990 show_help();
991 continue;
992 }
993 _ => break }
995 } else {
996 break;
997 }
998 }
999 eprintln!();
1000 true
1001}
1002
1003fn strip_ansi_codes(s: &str) -> String {
1005 let mut result = String::new();
1006 let mut in_escape = false;
1007
1008 for c in s.chars() {
1009 if c == '\x1b' {
1010 in_escape = true;
1011 } else if in_escape {
1012 if c == 'm' {
1013 in_escape = false;
1014 }
1015 } else {
1016 result.push(c);
1017 }
1018 }
1019
1020 result
1021}
1022
1023#[macro_export]
1050#[cfg(debug_assertions)]
1051macro_rules! print_break {
1052 () => {{
1053 if $crate::is_enabled() {
1054 let break_id = $crate::next_break_id();
1055 let elapsed_str = $crate::get_elapsed().map($crate::format_elapsed).unwrap_or_default();
1056 $crate::update_break_time();
1057
1058 let location = format!("{}:{}", file!(), line!());
1059 let width = 50;
1060 let border = $crate::get_border_style();
1061 let c = $crate::Colors::get();
1062
1063 let h = border.horizontal.to_string();
1064
1065 eprintln!();
1066 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);
1067 eprintln!("{}{}{} {}{}{}", c.yellow, border.vertical, c.reset, c.cyan, location, c.reset);
1068 eprintln!("{}{}{}{}", c.yellow, border.bottom_left, h.repeat(width), c.reset);
1069
1070 $crate::handle_input();
1071 }
1072 }};
1073 ($($var:expr),+ $(,)?) => {{
1074 if $crate::is_enabled() {
1075 let break_id = $crate::next_break_id();
1076 let elapsed_str = $crate::get_elapsed().map($crate::format_elapsed).unwrap_or_default();
1077 $crate::update_break_time();
1078
1079 let location = format!("{}:{}", file!(), line!());
1080 let width = 50;
1081 let border = $crate::get_border_style();
1082 let c = $crate::Colors::get();
1083
1084 let mut full_output = String::new();
1086
1087 let h = border.horizontal.to_string();
1088
1089 eprintln!();
1090 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);
1091 eprintln!("{}{}{} {}{}{}", c.yellow, border.vertical, c.reset, c.cyan, location, c.reset);
1092 eprintln!("{}{}{}{}", c.yellow, border.tee_right, h.repeat(width), c.reset);
1093
1094 $(
1095 let formatted = $crate::format_value(&$var);
1096 let name = stringify!($var);
1097
1098 full_output.push_str(&format!("{} = {}\n\n", name, $crate::format_value_full(&$var)));
1100
1101 if formatted.contains('\n') {
1102 eprintln!("{}{}{} {}{}{}=", c.yellow, border.vertical, c.reset, c.green, name, c.reset);
1103 for line in formatted.lines() {
1104 eprintln!("{}{}{} {}{}{}", c.yellow, border.vertical, c.reset, c.white, line, c.reset);
1105 }
1106 } else {
1107 eprintln!("{}{}{} {}{}{} = {}{}{}", c.yellow, border.vertical, c.reset, c.green, name, c.reset, c.white, formatted, c.reset);
1108 }
1109 )+
1110
1111 $crate::store_full_output(full_output);
1112
1113 eprintln!("{}{}{}{}", c.yellow, border.bottom_left, h.repeat(width), c.reset);
1114 $crate::handle_input();
1115 }
1116 }};
1117}
1118
1119#[macro_export]
1134#[cfg(debug_assertions)]
1135macro_rules! print_break_if {
1136 ($cond:expr) => {{
1137 if $cond {
1138 $crate::print_break!();
1139 }
1140 }};
1141 ($cond:expr, $($var:expr),+ $(,)?) => {{
1142 if $cond {
1143 $crate::print_break!($($var),+);
1144 }
1145 }};
1146}
1147
1148#[macro_export]
1150#[cfg(not(debug_assertions))]
1151macro_rules! print_break_if {
1152 ($cond:expr) => {{}};
1153 ($cond:expr, $($var:expr),+ $(,)?) => {{}};
1154}
1155
1156#[macro_export]
1158#[cfg(not(debug_assertions))]
1159macro_rules! print_break {
1160 () => {{}};
1161 ($($var:expr),+ $(,)?) => {{}};
1162}
1163
1164#[cfg(test)]
1165mod tests {
1166 use super::*;
1167
1168 #[test]
1169 fn format_json_string() {
1170 let json = r#"{"name": "test", "value": 42}"#;
1171 let formatted = format_value(&json);
1172 assert!(formatted.contains("\"name\": \"test\""));
1173 assert!(formatted.contains('\n')); }
1175
1176 #[test]
1177 fn format_non_json() {
1178 let x = 42;
1179 let formatted = format_value(&x);
1180 assert_eq!(formatted, "42");
1181 }
1182
1183 #[test]
1184 fn format_struct() {
1185 #[derive(Debug)]
1186 struct Test { a: i32, b: String }
1187
1188 let t = Test { a: 1, b: "hello".to_string() };
1189 let formatted = format_value(&t);
1190 assert!(formatted.contains("Test"));
1191 }
1192
1193 #[test]
1194 fn truncation_works() {
1195 let long_vec: Vec<i32> = (0..1000).collect();
1196 let formatted = format_value(&long_vec);
1197 assert!(formatted.contains("more lines"));
1198 }
1199
1200 #[test]
1201 fn env_var_disable() {
1202 std::env::set_var("PRINT_BREAK", "0");
1203 assert!(!is_enabled());
1204 std::env::set_var("PRINT_BREAK", "1");
1205 assert!(is_enabled());
1206 std::env::remove_var("PRINT_BREAK");
1207 }
1208
1209 #[test]
1210 fn json_array_elements_same_color() {
1211 let json = r#"["a", "b", "c"]"#;
1214 let parsed: serde_json::Value = serde_json::from_str(json).unwrap();
1216 let pretty = serde_json::to_string_pretty(&parsed).unwrap();
1217 let formatted = colorize_json(&pretty);
1218
1219 assert!(formatted.contains("\"a\""));
1222 assert!(formatted.contains("\"b\""));
1223 assert!(formatted.contains("\"c\""));
1224 }
1225}