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