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 let mut context_stack: Vec<bool> = Vec::new();
219
220 while let Some(c) = chars.next() {
221 match c {
222 '"' if !in_string => {
223 in_string = true;
224 let color = if is_key { cyan } else { magenta };
225 result.push_str(color);
226 result.push('"');
227 }
228 '"' if in_string => {
229 result.push('"');
230 result.push_str(reset);
231 in_string = false;
232 }
233 ':' if !in_string => {
234 result.push_str(gray);
235 result.push(':');
236 result.push_str(reset);
237 is_key = false;
238 }
239 ',' if !in_string => {
240 result.push_str(gray);
241 result.push(',');
242 result.push_str(reset);
243 is_key = context_stack.last().copied().unwrap_or(true);
245 }
246 '{' | '[' if !in_string => {
247 result.push_str(gray);
248 result.push(c);
249 result.push_str(reset);
250 let is_object = c == '{';
251 context_stack.push(is_object);
252 is_key = is_object;
253 }
254 '}' | ']' if !in_string => {
255 result.push_str(gray);
256 result.push(c);
257 result.push_str(reset);
258 context_stack.pop();
259 }
260 '0'..='9' | '-' | '.' if !in_string => {
261 result.push_str(yellow);
262 result.push(c);
263 while let Some(&next) = chars.peek() {
265 if next.is_ascii_digit() || next == '.' || next == 'e' || next == 'E' || next == '+' || next == '-' {
266 result.push(chars.next().unwrap());
267 } else {
268 break;
269 }
270 }
271 result.push_str(reset);
272 }
273 't' if !in_string => {
274 let rest: String = chars.by_ref().take(3).collect();
276 if rest == "rue" {
277 result.push_str(yellow);
278 result.push_str("true");
279 result.push_str(reset);
280 } else {
281 result.push('t');
282 result.push_str(&rest);
283 }
284 }
285 'f' if !in_string => {
286 let rest: String = chars.by_ref().take(4).collect();
288 if rest == "alse" {
289 result.push_str(yellow);
290 result.push_str("false");
291 result.push_str(reset);
292 } else {
293 result.push('f');
294 result.push_str(&rest);
295 }
296 }
297 'n' if !in_string => {
298 let rest: String = chars.by_ref().take(3).collect();
300 if rest == "ull" {
301 result.push_str(yellow);
302 result.push_str("null");
303 result.push_str(reset);
304 } else {
305 result.push('n');
306 result.push_str(&rest);
307 }
308 }
309 _ => result.push(c),
310 }
311 }
312
313 result
314}
315
316fn colorize_toml(s: &str) -> String {
318 let c = Colors::get();
319 if c.cyan.is_empty() {
320 return s.to_string();
321 }
322
323 let (green, cyan, magenta, yellow, gray, reset) =
324 (c.green, c.cyan, c.magenta, c.yellow, c.gray, c.reset);
325
326 let mut result = String::new();
327
328 for line in s.lines() {
329 let trimmed = line.trim();
330
331 if trimmed.starts_with('[') && trimmed.ends_with(']') {
332 result.push_str(green);
334 result.push_str(line);
335 result.push_str(reset);
336 } else if let Some(eq_pos) = trimmed.find(" = ") {
337 let indent = &line[..line.len() - trimmed.len()];
339 let key = &trimmed[..eq_pos];
340 let value = &trimmed[eq_pos + 3..];
341
342 result.push_str(indent);
343 result.push_str(cyan);
344 result.push_str(key);
345 result.push_str(reset);
346 result.push_str(gray);
347 result.push_str(" = ");
348 result.push_str(reset);
349 result.push_str(&colorize_toml_value(value, magenta, yellow, gray, reset));
350 } else {
351 result.push_str(line);
352 }
353 result.push('\n');
354 }
355
356 result.trim_end().to_string()
357}
358
359fn colorize_toml_value(s: &str, magenta: &str, yellow: &str, gray: &str, reset: &str) -> String {
360 let trimmed = s.trim();
361
362 if trimmed.starts_with('"') && trimmed.ends_with('"') {
363 format!("{}{}{}", magenta, trimmed, reset)
364 } else if trimmed == "true" || trimmed == "false" || trimmed.parse::<f64>().is_ok() {
365 format!("{}{}{}", yellow, trimmed, reset)
366 } else if trimmed.starts_with('[') {
367 let mut result = format!("{}[{}", gray, reset);
369 let inner = &trimmed[1..trimmed.len()-1];
370 let parts: Vec<&str> = inner.split(", ").collect();
371 for (i, part) in parts.iter().enumerate() {
372 if i > 0 {
373 result.push_str(&format!("{}, {}", gray, reset));
374 }
375 result.push_str(&colorize_toml_value(part, magenta, yellow, gray, reset));
376 }
377 result.push_str(&format!("{}]{}", gray, reset));
378 result
379 } else {
380 s.to_string()
381 }
382}
383
384fn colorize_yaml(s: &str) -> String {
386 let c = Colors::get();
387 if c.cyan.is_empty() {
388 return s.to_string();
389 }
390
391 let (cyan, magenta, yellow, gray, reset) = (c.cyan, c.magenta, c.yellow, c.gray, c.reset);
392
393 let mut result = String::new();
394
395 for line in s.lines() {
396 if let Some(colon_pos) = line.find(':') {
397 let before_colon = &line[..colon_pos];
398 let after_colon = &line[colon_pos + 1..];
399
400 let trimmed_before = before_colon.trim_start_matches([' ', '-']);
402
403 if !trimmed_before.is_empty() && !trimmed_before.starts_with('#') {
404 let indent = &before_colon[..before_colon.len() - trimmed_before.len()];
405
406 if indent.contains('-') {
408 let dash_pos = indent.find('-').unwrap();
409 result.push_str(&indent[..dash_pos]);
410 result.push_str(gray);
411 result.push('-');
412 result.push_str(reset);
413 result.push_str(&indent[dash_pos + 1..]);
414 } else {
415 result.push_str(indent);
416 }
417
418 result.push_str(cyan);
419 result.push_str(trimmed_before);
420 result.push_str(reset);
421 result.push_str(gray);
422 result.push(':');
423 result.push_str(reset);
424
425 let value = after_colon.trim();
427 if !value.is_empty() {
428 result.push(' ');
429 result.push_str(&colorize_yaml_value(value, magenta, yellow, reset));
430 }
431 } else {
432 result.push_str(line);
433 }
434 } else if line.trim().starts_with('-') {
435 let trimmed = line.trim();
437 let indent = &line[..line.len() - trimmed.len()];
438 result.push_str(indent);
439 result.push_str(gray);
440 result.push('-');
441 result.push_str(reset);
442 result.push_str(&colorize_yaml_value(trimmed[1..].trim(), magenta, yellow, reset));
443 } else {
444 result.push_str(line);
445 }
446 result.push('\n');
447 }
448
449 result.trim_end().to_string()
450}
451
452fn colorize_yaml_value(s: &str, magenta: &str, yellow: &str, reset: &str) -> String {
453 let trimmed = s.trim();
454
455 if trimmed.starts_with('"') || trimmed.starts_with('\'') {
456 format!("{}{}{}", magenta, trimmed, reset)
457 } else if matches!(trimmed, "true" | "false" | "null" | "~") || trimmed.parse::<f64>().is_ok() {
458 format!("{}{}{}", yellow, trimmed, reset)
459 } else if !trimmed.is_empty() && !trimmed.contains(':') {
460 format!("{}{}{}", magenta, trimmed, reset)
462 } else {
463 s.to_string()
464 }
465}
466
467#[doc(hidden)]
469pub fn is_enabled() -> bool {
470 if SKIP_ALL.load(Ordering::Relaxed) {
471 return false;
472 }
473 match std::env::var("PRINT_BREAK") {
474 Ok(val) => !matches!(val.as_str(), "0" | "false" | "no" | "off"),
475 Err(_) => true, }
477}
478
479#[doc(hidden)]
481pub fn is_tty() -> bool {
482 std::io::stderr().is_terminal() && std::io::stdin().is_terminal()
483}
484
485#[doc(hidden)]
487pub fn next_break_id() -> usize {
488 BREAK_COUNT.fetch_add(1, Ordering::Relaxed) + 1
489}
490
491#[doc(hidden)]
493pub fn set_skip_all(skip: bool) {
494 SKIP_ALL.store(skip, Ordering::Relaxed);
495}
496
497#[doc(hidden)]
501pub fn format_value<T: Debug>(value: &T) -> String {
502 let debug_str = format!("{:?}", value);
503 let raw_output;
504
505 if let Some(inner) = debug_str.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
507 let unescaped = inner
509 .replace("\\\"", "\"")
510 .replace("\\n", "\n")
511 .replace("\\t", "\t")
512 .replace("\\\\", "\\");
513
514 let trimmed = unescaped.trim();
515
516 let c = Colors::get();
517 let (gray, reset) = (c.gray, c.reset);
518
519 if trimmed.starts_with('{') || trimmed.starts_with('[') {
521 if let Ok(json) = serde_json::from_str::<serde_json::Value>(&unescaped) {
522 if let Ok(pretty) = serde_json::to_string_pretty(&json) {
523 let colorized = colorize_json(&pretty);
524 raw_output = format!("{}(json){}\n{}", gray, reset, colorized);
525 return truncate_output(&raw_output);
526 }
527 }
528 }
529
530 if trimmed.contains(" = ") || trimmed.contains("]\n") || trimmed.starts_with('[') {
532 if let Ok(toml_val) = toml::from_str::<toml::Value>(&unescaped) {
533 if let Ok(pretty) = toml::to_string_pretty(&toml_val) {
534 let colorized = colorize_toml(&pretty);
535 raw_output = format!("{}(toml){}\n{}", gray, reset, colorized);
536 return truncate_output(&raw_output);
537 }
538 }
539 }
540
541 if trimmed.contains(": ") || trimmed.contains(":\n") {
543 if let Ok(yaml_val) = serde_yaml::from_str::<serde_yaml::Value>(&unescaped) {
544 if yaml_val.is_mapping() || yaml_val.is_sequence() {
546 if let Ok(pretty) = serde_yaml::to_string(&yaml_val) {
547 let colorized = colorize_yaml(pretty.trim());
548 raw_output = format!("{}(yaml){}\n{}", gray, reset, colorized);
549 return truncate_output(&raw_output);
550 }
551 }
552 }
553 }
554
555 raw_output = format!("{}(string, {} chars){}\n{}", gray, unescaped.len(), reset, word_wrap(&unescaped, 80));
557 return truncate_output(&raw_output);
558 }
559
560 let debug_output = format!("{:#?}", value);
562 raw_output = colorize_debug(&debug_output);
563 truncate_output(&raw_output)
564}
565
566#[doc(hidden)]
568pub fn format_value_full<T: Debug>(value: &T) -> String {
569 let debug_str = format!("{:?}", value);
570
571 if let Some(inner) = debug_str.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
573 let unescaped = inner
575 .replace("\\\"", "\"")
576 .replace("\\n", "\n")
577 .replace("\\t", "\t")
578 .replace("\\\\", "\\");
579
580 let trimmed = unescaped.trim();
581
582 if trimmed.starts_with('{') || trimmed.starts_with('[') {
584 if let Ok(json) = serde_json::from_str::<serde_json::Value>(&unescaped) {
585 if let Ok(pretty) = serde_json::to_string_pretty(&json) {
586 return pretty;
587 }
588 }
589 }
590
591 if trimmed.contains(" = ") || trimmed.contains("]\n") || trimmed.starts_with('[') {
593 if let Ok(toml_val) = toml::from_str::<toml::Value>(&unescaped) {
594 if let Ok(pretty) = toml::to_string_pretty(&toml_val) {
595 return pretty;
596 }
597 }
598 }
599
600 if trimmed.contains(": ") || trimmed.contains(":\n") {
602 if let Ok(yaml_val) = serde_yaml::from_str::<serde_yaml::Value>(&unescaped) {
603 if yaml_val.is_mapping() || yaml_val.is_sequence() {
604 if let Ok(pretty) = serde_yaml::to_string(&yaml_val) {
605 return pretty.trim().to_string();
606 }
607 }
608 }
609 }
610
611 return word_wrap(&unescaped, 100);
613 }
614
615 colorize_debug(&format!("{:#?}", value))
617}
618
619const DEFAULT_MAX_DEPTH: usize = 4;
621
622fn max_depth() -> usize {
624 std::env::var("PRINT_BREAK_DEPTH")
625 .ok()
626 .and_then(|v| v.parse().ok())
627 .unwrap_or(DEFAULT_MAX_DEPTH)
628}
629
630fn colorize_debug(s: &str) -> String {
632 let c = Colors::get();
633 if c.cyan.is_empty() {
634 return s.to_string();
635 }
636
637 let (green, cyan, yellow, magenta, white, gray, reset) =
638 (c.green, c.cyan, c.yellow, c.magenta, c.white, c.gray, c.reset);
639
640 let mut result = String::new();
641 let lines: Vec<&str> = s.lines().collect();
642 let mut current_depth: usize = 0;
643 let mut skip_until_depth: Option<usize> = None;
644
645 for line in lines {
646 let trimmed = line.trim_start();
647 let indent_count = line.len() - trimmed.len();
648 let indent_level = indent_count / 4;
649
650 let opens = trimmed.ends_with('{') || trimmed.ends_with('[') || trimmed.ends_with("({");
652 let closes = trimmed.starts_with('}') || trimmed.starts_with(']') || trimmed.starts_with(')');
653
654 if closes {
655 current_depth = current_depth.saturating_sub(1);
656 }
657
658 if let Some(skip_depth) = skip_until_depth {
660 if current_depth < skip_depth {
661 skip_until_depth = None;
662 } else {
663 if opens {
664 current_depth += 1;
665 }
666 continue;
667 }
668 }
669
670 if opens && current_depth >= max_depth() {
672 for _ in 0..indent_level {
674 result.push_str(&format!("{}│{} ", gray, reset));
675 }
676
677 let name = trimmed.trim_end_matches(['{', '[', '(', ' ']);
679 if !name.is_empty() {
680 result.push_str(&format!("{}{}{} {}{{ ... }}{}", green, name, reset, gray, reset));
681 } else {
682 result.push_str(&format!("{}[ ... ]{}", gray, reset));
683 }
684 result.push('\n');
685
686 skip_until_depth = Some(current_depth);
687 current_depth += 1;
688 continue;
689 }
690
691 for _ in 0..indent_level {
693 result.push_str(&format!("{}│{} ", gray, reset));
694 }
695
696 if opens {
698 let name = trimmed.trim_end_matches(['{', '[', '(', ' ']);
700 let bracket = trimmed.chars().last().unwrap_or(' ');
701 if !name.is_empty() {
702 result.push_str(&format!("{}{}{} {}{}{}", green, name, reset, gray, bracket, reset));
703 } else {
704 result.push_str(&format!("{}{}{}", gray, bracket, reset));
705 }
706 current_depth += 1;
707 } else if closes || trimmed.ends_with("},") || trimmed.ends_with("],") || trimmed.ends_with("),") {
708 result.push_str(&format!("{}{}{}", gray, trimmed, reset));
710 } else if trimmed.contains(": ") {
711 if let Some(colon_pos) = trimmed.find(": ") {
713 let field = &trimmed[..colon_pos];
714 let value = &trimmed[colon_pos + 2..];
715 let colored_value = colorize_value(value, yellow, magenta, white, gray, reset);
716 result.push_str(&format!("{}{}{}{}: {}", cyan, field, reset, gray, colored_value));
717 } else {
718 result.push_str(trimmed);
719 }
720 } else {
721 let colored = colorize_value(trimmed, yellow, magenta, white, gray, reset);
723 result.push_str(&colored);
724 }
725 result.push('\n');
726 }
727
728 result.trim_end().to_string()
729}
730
731fn colorize_value(s: &str, yellow: &str, magenta: &str, white: &str, gray: &str, reset: &str) -> String {
733 let trimmed = s.trim_end_matches(',');
734 let has_comma = s.ends_with(',');
735 let comma = if has_comma { format!("{},{}", gray, reset) } else { String::new() };
736
737 if trimmed.starts_with('"') {
738 format!("{}{}{}{}", magenta, trimmed, reset, comma)
740 } else if trimmed.parse::<f64>().is_ok() || trimmed.starts_with('-') && trimmed[1..].parse::<f64>().is_ok() {
741 format!("{}{}{}{}", yellow, trimmed, reset, comma)
743 } else if trimmed == "true" || trimmed == "false" {
744 format!("{}{}{}{}", yellow, trimmed, reset, comma)
746 } else if trimmed == "None" || trimmed.starts_with("Some(") {
747 format!("{}{}{}{}", white, trimmed, reset, comma)
749 } else {
750 format!("{}{}{}{}", white, trimmed, reset, comma)
751 }
752}
753
754fn word_wrap(s: &str, width: usize) -> String {
756 let mut result = String::new();
757 for line in s.lines() {
758 if line.len() <= width {
759 result.push_str(line);
760 result.push('\n');
761 } else {
762 let mut current_line = String::new();
763 for word in line.split_whitespace() {
764 if current_line.is_empty() {
765 current_line = word.to_string();
766 } else if current_line.len() + 1 + word.len() <= width {
767 current_line.push(' ');
768 current_line.push_str(word);
769 } else {
770 result.push_str(¤t_line);
771 result.push('\n');
772 current_line = word.to_string();
773 }
774 }
775 if !current_line.is_empty() {
776 result.push_str(¤t_line);
777 result.push('\n');
778 }
779 }
780 }
781 result.trim_end().to_string()
782}
783
784fn truncate_output(s: &str) -> String {
786 let lines: Vec<&str> = s.lines().collect();
787 if lines.len() > MAX_LINES {
788 let c = Colors::get();
789 let truncated = lines[..MAX_LINES].join("\n");
790 format!("{}\n{}... ({} more lines){}", truncated, c.gray, lines.len() - MAX_LINES, c.reset)
791 } else {
792 s.to_string()
793 }
794}
795
796static LAST_FULL_OUTPUT: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
798
799#[doc(hidden)]
801pub fn store_full_output(output: String) {
802 if let Ok(mut guard) = LAST_FULL_OUTPUT.lock() {
803 *guard = Some(output);
804 }
805}
806
807fn show_help() {
809 eprintln!("\n\x1b[1;33m─── print-break Help ───\x1b[0m");
810 eprintln!("\x1b[36mEnter\x1b[0m Continue to next breakpoint");
811 eprintln!("\x1b[36mm\x1b[0m Show full output (if truncated)");
812 eprintln!("\x1b[36mt\x1b[0m Show stack trace");
813 eprintln!("\x1b[36mc\x1b[0m Copy last value to clipboard");
814 eprintln!("\x1b[36ms\x1b[0m Skip all remaining breakpoints");
815 eprintln!("\x1b[36mq\x1b[0m Quit the program");
816 eprintln!("\x1b[36mh / ?\x1b[0m Show this help");
817 eprintln!();
818 eprintln!("\x1b[90mEnvironment variables:\x1b[0m");
819 eprintln!(" \x1b[36mPRINT_BREAK=0\x1b[0m Disable all breakpoints");
820 eprintln!(" \x1b[36mPRINT_BREAK_DEPTH=N\x1b[0m Max nesting depth (default: 4)");
821 eprintln!(" \x1b[36mPRINT_BREAK_STYLE=X\x1b[0m Border style: rounded, sharp, double, ascii");
822 eprintln!("\x1b[1;33m─────────────────────────\x1b[0m\n");
823}
824
825fn show_stack_trace() {
827 eprintln!("\n\x1b[1;33m─── Stack Trace ───\x1b[0m");
828
829 let bt = backtrace::Backtrace::new();
830 let mut in_relevant = false;
831 let mut count = 0;
832
833 for frame in bt.frames() {
834 for symbol in frame.symbols() {
835 if let Some(name) = symbol.name() {
836 let name_str = name.to_string();
837
838 if name_str.contains("print_break::") || name_str.contains("backtrace::") {
840 continue;
841 }
842
843 if !in_relevant && !name_str.contains("print_break") {
845 in_relevant = true;
846 }
847
848 if in_relevant {
849 let file = symbol.filename()
850 .map(|p| p.display().to_string())
851 .unwrap_or_default();
852 let line = symbol.lineno().unwrap_or(0);
853
854 let short_file = file.rsplit('/').next().unwrap_or(&file);
856
857 if !name_str.contains("std::") && !name_str.contains("core::") && !name_str.contains("__rust") {
858 eprintln!("\x1b[90m{:>3}.\x1b[0m \x1b[36m{}\x1b[0m", count, name_str);
859 if !file.is_empty() && line > 0 {
860 eprintln!(" \x1b[90mat {}:{}\x1b[0m", short_file, line);
861 }
862 count += 1;
863
864 if count >= 15 {
865 eprintln!("\x1b[90m ... (truncated)\x1b[0m");
866 break;
867 }
868 }
869 }
870 }
871 }
872 if count >= 15 {
873 break;
874 }
875 }
876
877 eprintln!("\x1b[1;33m───────────────────\x1b[0m\n");
878}
879
880fn copy_to_clipboard(text: &str) -> bool {
882 use std::process::{Command, Stdio};
883 use std::io::Write as IoWrite;
884
885 let commands = if cfg!(target_os = "macos") {
887 vec![("pbcopy", vec![])]
888 } else if cfg!(target_os = "windows") {
889 vec![("clip", vec![])]
890 } else {
891 vec![
893 ("xclip", vec!["-selection", "clipboard"]),
894 ("xsel", vec!["--clipboard", "--input"]),
895 ("wl-copy", vec![]),
896 ]
897 };
898
899 for (cmd, args) in commands {
900 if let Ok(mut child) = Command::new(cmd)
901 .args(&args)
902 .stdin(Stdio::piped())
903 .stdout(Stdio::null())
904 .stderr(Stdio::null())
905 .spawn()
906 {
907 if let Some(mut stdin) = child.stdin.take() {
908 if stdin.write_all(text.as_bytes()).is_ok() {
909 drop(stdin);
910 if child.wait().map(|s| s.success()).unwrap_or(false) {
911 return true;
912 }
913 }
914 }
915 }
916 }
917 false
918}
919
920#[doc(hidden)]
922pub fn handle_input() -> bool {
923 use std::io::{self, BufRead, Write};
924
925 if !is_tty() {
927 eprintln!("(non-interactive mode, continuing...)");
928 return true;
929 }
930
931 loop {
932 eprint!("\x1b[90m[Enter, m=more, t=trace, c=copy, s=skip, q=quit, h=help]\x1b[0m ");
933 io::stderr().flush().unwrap();
934
935 let stdin = io::stdin();
936 let mut line = String::new();
937 if stdin.lock().read_line(&mut line).is_ok() {
938 let input = line.trim().to_lowercase();
939 match input.as_str() {
940 "q" | "quit" => {
941 eprintln!("\x1b[1;31mQuitting...\x1b[0m");
942 std::process::exit(0);
943 }
944 "s" | "skip" => {
945 eprintln!("\x1b[1;33mSkipping remaining breakpoints...\x1b[0m");
946 set_skip_all(true);
947 break;
948 }
949 "m" | "more" => {
950 if let Ok(guard) = LAST_FULL_OUTPUT.lock() {
952 if let Some(ref full) = *guard {
953 eprintln!("\n\x1b[1;33m─── Full Output ───\x1b[0m");
954 for line in full.lines() {
955 eprintln!("\x1b[37m{}\x1b[0m", line);
956 }
957 eprintln!("\x1b[1;33m───────────────────\x1b[0m\n");
958 } else {
959 eprintln!("\x1b[90m(no truncated output to show)\x1b[0m");
960 }
961 }
962 continue;
963 }
964 "t" | "trace" => {
965 show_stack_trace();
966 continue;
967 }
968 "c" | "copy" => {
969 if let Ok(guard) = LAST_FULL_OUTPUT.lock() {
970 if let Some(ref full) = *guard {
971 let clean = strip_ansi_codes(full);
973 if copy_to_clipboard(&clean) {
974 eprintln!("\x1b[1;32mCopied to clipboard!\x1b[0m");
975 } else {
976 eprintln!("\x1b[1;31mFailed to copy (install xclip or xsel)\x1b[0m");
977 }
978 } else {
979 eprintln!("\x1b[90m(nothing to copy)\x1b[0m");
980 }
981 }
982 continue;
983 }
984 "h" | "?" | "help" => {
985 show_help();
986 continue;
987 }
988 _ => break }
990 } else {
991 break;
992 }
993 }
994 eprintln!();
995 true
996}
997
998fn strip_ansi_codes(s: &str) -> String {
1000 let mut result = String::new();
1001 let mut in_escape = false;
1002
1003 for c in s.chars() {
1004 if c == '\x1b' {
1005 in_escape = true;
1006 } else if in_escape {
1007 if c == 'm' {
1008 in_escape = false;
1009 }
1010 } else {
1011 result.push(c);
1012 }
1013 }
1014
1015 result
1016}
1017
1018#[macro_export]
1045#[cfg(debug_assertions)]
1046macro_rules! print_break {
1047 () => {{
1048 if $crate::is_enabled() {
1049 let break_id = $crate::next_break_id();
1050 let elapsed_str = $crate::get_elapsed().map($crate::format_elapsed).unwrap_or_default();
1051 $crate::update_break_time();
1052
1053 let location = format!("{}:{}", file!(), line!());
1054 let width = 50;
1055 let border = $crate::get_border_style();
1056 let c = $crate::Colors::get();
1057
1058 let h = border.horizontal.to_string();
1059
1060 eprintln!();
1061 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);
1062 eprintln!("{}{}{} {}{}{}", c.yellow, border.vertical, c.reset, c.cyan, location, c.reset);
1063 eprintln!("{}{}{}{}", c.yellow, border.bottom_left, h.repeat(width), c.reset);
1064
1065 $crate::handle_input();
1066 }
1067 }};
1068 ($($var:expr),+ $(,)?) => {{
1069 if $crate::is_enabled() {
1070 let break_id = $crate::next_break_id();
1071 let elapsed_str = $crate::get_elapsed().map($crate::format_elapsed).unwrap_or_default();
1072 $crate::update_break_time();
1073
1074 let location = format!("{}:{}", file!(), line!());
1075 let width = 50;
1076 let border = $crate::get_border_style();
1077 let c = $crate::Colors::get();
1078
1079 let mut full_output = String::new();
1081
1082 let h = border.horizontal.to_string();
1083
1084 eprintln!();
1085 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);
1086 eprintln!("{}{}{} {}{}{}", c.yellow, border.vertical, c.reset, c.cyan, location, c.reset);
1087 eprintln!("{}{}{}{}", c.yellow, border.tee_right, h.repeat(width), c.reset);
1088
1089 $(
1090 let formatted = $crate::format_value(&$var);
1091 let name = stringify!($var);
1092
1093 full_output.push_str(&format!("{} = {}\n\n", name, $crate::format_value_full(&$var)));
1095
1096 if formatted.contains('\n') {
1097 eprintln!("{}{}{} {}{}{}=", c.yellow, border.vertical, c.reset, c.green, name, c.reset);
1098 for line in formatted.lines() {
1099 eprintln!("{}{}{} {}{}{}", c.yellow, border.vertical, c.reset, c.white, line, c.reset);
1100 }
1101 } else {
1102 eprintln!("{}{}{} {}{}{} = {}{}{}", c.yellow, border.vertical, c.reset, c.green, name, c.reset, c.white, formatted, c.reset);
1103 }
1104 )+
1105
1106 $crate::store_full_output(full_output);
1107
1108 eprintln!("{}{}{}{}", c.yellow, border.bottom_left, h.repeat(width), c.reset);
1109 $crate::handle_input();
1110 }
1111 }};
1112}
1113
1114#[macro_export]
1129#[cfg(debug_assertions)]
1130macro_rules! print_break_if {
1131 ($cond:expr) => {{
1132 if $cond {
1133 $crate::print_break!();
1134 }
1135 }};
1136 ($cond:expr, $($var:expr),+ $(,)?) => {{
1137 if $cond {
1138 $crate::print_break!($($var),+);
1139 }
1140 }};
1141}
1142
1143#[macro_export]
1145#[cfg(not(debug_assertions))]
1146macro_rules! print_break_if {
1147 ($cond:expr) => {{}};
1148 ($cond:expr, $($var:expr),+ $(,)?) => {{}};
1149}
1150
1151#[macro_export]
1153#[cfg(not(debug_assertions))]
1154macro_rules! print_break {
1155 () => {{}};
1156 ($($var:expr),+ $(,)?) => {{}};
1157}
1158
1159#[cfg(test)]
1160mod tests {
1161 use super::*;
1162
1163 #[test]
1164 fn format_json_string() {
1165 let json = r#"{"name": "test", "value": 42}"#;
1166 let formatted = format_value(&json);
1167 assert!(formatted.contains("\"name\": \"test\""));
1168 assert!(formatted.contains('\n')); }
1170
1171 #[test]
1172 fn format_non_json() {
1173 let x = 42;
1174 let formatted = format_value(&x);
1175 assert_eq!(formatted, "42");
1176 }
1177
1178 #[test]
1179 fn format_struct() {
1180 #[derive(Debug)]
1181 struct Test { a: i32, b: String }
1182
1183 let t = Test { a: 1, b: "hello".to_string() };
1184 let formatted = format_value(&t);
1185 assert!(formatted.contains("Test"));
1186 }
1187
1188 #[test]
1189 fn truncation_works() {
1190 let long_vec: Vec<i32> = (0..1000).collect();
1191 let formatted = format_value(&long_vec);
1192 assert!(formatted.contains("more lines"));
1193 }
1194
1195 #[test]
1196 fn env_var_disable() {
1197 std::env::set_var("PRINT_BREAK", "0");
1198 assert!(!is_enabled());
1199 std::env::set_var("PRINT_BREAK", "1");
1200 assert!(is_enabled());
1201 std::env::remove_var("PRINT_BREAK");
1202 }
1203}