1use super::{DefmtRecord, Payload};
2use crate::Frame;
3use colored::{Color, ColoredString, Colorize, Styles};
4use dissimilar::Chunk;
5use log::{Level, Record as LogRecord};
6use regex::Regex;
7use std::{fmt::Write, path::Path};
8
9mod parser;
10
11#[derive(Debug, PartialEq, Clone)]
13#[non_exhaustive]
14pub(super) enum LogMetadata {
15 CrateName,
19
20 FileName(u8),
28
29 FilePath,
34
35 LineNumber,
39
40 Log,
45
46 LogLevel,
51
52 ModulePath,
67
68 String(String),
70
71 Timestamp,
76
77 NestedLogSegments(Vec<LogSegment>),
79}
80
81impl LogMetadata {
82 fn is_metadata_specifier(&self) -> bool {
85 !matches!(
86 self,
87 LogMetadata::String(_) | LogMetadata::NestedLogSegments(_)
88 )
89 }
90}
91
92#[derive(Debug, PartialEq, Clone, Copy)]
94pub(super) enum LogColor {
95 Color(colored::Color),
100
101 SeverityLevel,
104
105 WarnError,
110}
111
112#[derive(Debug, PartialEq, Clone, Copy)]
114pub(super) enum Alignment {
115 Center,
116 Left,
117 Right,
118}
119
120#[derive(Debug, PartialEq, Clone, Copy)]
121pub(super) enum Padding {
122 Space,
123 Zero,
124}
125
126#[derive(Debug, PartialEq, Clone)]
128pub(super) struct LogSegment {
129 pub(super) metadata: LogMetadata,
130 pub(super) format: LogFormat,
131}
132
133#[derive(Debug, PartialEq, Clone, Default)]
134pub(super) struct LogFormat {
135 pub(super) width: Option<usize>,
136 pub(super) color: Option<LogColor>,
137 pub(super) style: Option<Vec<colored::Styles>>,
138 pub(super) alignment: Option<Alignment>,
139 pub(super) padding: Option<Padding>,
140}
141
142impl LogSegment {
143 pub(super) const fn new(metadata: LogMetadata) -> Self {
144 Self {
145 metadata,
146 format: LogFormat {
147 color: None,
148 style: None,
149 width: None,
150 alignment: None,
151 padding: None,
152 },
153 }
154 }
155
156 #[cfg(test)]
157 pub(crate) const fn with_color(mut self, color: LogColor) -> Self {
158 self.format.color = Some(color);
159 self
160 }
161
162 #[cfg(test)]
163 pub(crate) fn with_style(mut self, style: colored::Styles) -> Self {
164 let mut styles = self.format.style.unwrap_or_default();
165 styles.push(style);
166 self.format.style = Some(styles);
167 self
168 }
169
170 #[cfg(test)]
171 pub(crate) const fn with_width(mut self, width: usize) -> Self {
172 self.format.width = Some(width);
173 self
174 }
175
176 #[cfg(test)]
177 pub(crate) const fn with_alignment(mut self, alignment: Alignment) -> Self {
178 self.format.alignment = Some(alignment);
179 self
180 }
181
182 #[cfg(test)]
183 pub(crate) const fn with_padding(mut self, padding: Padding) -> Self {
184 self.format.padding = Some(padding);
185 self
186 }
187}
188
189pub struct Formatter {
191 formatter: InternalFormatter,
192}
193
194impl Formatter {
195 pub fn new(config: FormatterConfig) -> Self {
197 Self {
198 formatter: InternalFormatter::new(config, Source::Defmt),
199 }
200 }
201
202 pub fn format_frame<'a>(
204 &self,
205 frame: Frame<'a>,
206 file: Option<&'a str>,
207 line: Option<u32>,
208 module_path: Option<&str>,
209 ) -> String {
210 let (timestamp, level) = super::timestamp_and_level_from_frame(&frame);
211
212 #[allow(clippy::match_single_binding)]
214 match format_args!("{}", frame.display_message()) {
215 args => {
216 let log_record = &LogRecord::builder()
217 .args(args)
218 .module_path(module_path)
219 .file(file)
220 .line(line)
221 .build();
222
223 let record = DefmtRecord {
224 log_record,
225 payload: Payload { level, timestamp },
226 };
227
228 self.format(&record)
229 }
230 }
231 }
232
233 pub(super) fn format(&self, record: &DefmtRecord) -> String {
235 self.formatter.format(&Record::Defmt(record))
236 }
237}
238
239pub struct HostFormatter {
241 formatter: InternalFormatter,
242}
243
244impl HostFormatter {
245 pub fn new(config: FormatterConfig) -> Self {
247 Self {
248 formatter: InternalFormatter::new(config, Source::Host),
249 }
250 }
251
252 pub fn format(&self, record: &LogRecord) -> String {
254 self.formatter.format(&Record::Host(record))
255 }
256}
257
258#[derive(Debug)]
259struct InternalFormatter {
260 format: Vec<LogSegment>,
261}
262
263#[derive(Clone, Copy, PartialEq)]
264enum Source {
265 Defmt,
266 Host,
267}
268
269enum Record<'a> {
270 Defmt(&'a DefmtRecord<'a>),
271 Host(&'a LogRecord<'a>),
272}
273
274#[derive(Debug)]
275#[non_exhaustive]
276pub enum FormatterFormat<'a> {
277 Default {
286 with_location: bool,
287 },
288 OneLine {
296 with_location: bool,
297 },
298 Custom(&'a str),
299}
300
301impl FormatterFormat<'static> {
302 pub fn from_string(s: &str, with_location: bool) -> Option<FormatterFormat<'static>> {
306 match s {
307 "default" => Some(FormatterFormat::Default { with_location }),
308 "oneline" => Some(FormatterFormat::OneLine { with_location }),
309 _ => None,
310 }
311 }
312
313 pub fn get_options() -> impl Iterator<Item = &'static str> {
318 ["default", "oneline"].iter().cloned()
319 }
320}
321
322impl Default for FormatterFormat<'_> {
323 fn default() -> Self {
324 FormatterFormat::Default {
325 with_location: false,
326 }
327 }
328}
329
330trait Style {
332 const FORMAT: &'static str;
333 const FORMAT_WITH_TS: &'static str;
334 const FORMAT_WITH_LOC: &'static str;
335 const FORMAT_WITH_TS_LOC: &'static str;
336
337 fn get_string(with_location: bool, has_timestamp: bool) -> &'static str {
339 match (with_location, has_timestamp) {
340 (false, false) => Self::FORMAT,
341 (false, true) => Self::FORMAT_WITH_TS,
342 (true, false) => Self::FORMAT_WITH_LOC,
343 (true, true) => Self::FORMAT_WITH_TS_LOC,
344 }
345 }
346}
347
348struct DefaultStyle;
350
351impl Style for DefaultStyle {
352 const FORMAT: &'static str = "{L} {s}";
353 const FORMAT_WITH_LOC: &'static str = "{L} {s}\n└─ {m} @ {F}:{l}";
354 const FORMAT_WITH_TS: &'static str = "{t} {L} {s}";
355 const FORMAT_WITH_TS_LOC: &'static str = "{t} {L} {s}\n└─ {m} @ {F}:{l}";
356}
357
358struct OneLineStyle;
360
361impl Style for OneLineStyle {
362 const FORMAT: &'static str = "{[{L}]%bold} {s}";
363 const FORMAT_WITH_LOC: &'static str = "{[{L}]%bold} {s} {({c:bold} {fff}:{l:1})%dimmed}";
364 const FORMAT_WITH_TS: &'static str = "{t} {[{L}]%bold} {s}";
365 const FORMAT_WITH_TS_LOC: &'static str = "{t} {[{L}]%bold} {s} {({c:bold} {fff}:{l:1})%dimmed}";
366}
367
368#[derive(Debug, Default)]
370pub struct FormatterConfig<'a> {
371 pub format: FormatterFormat<'a>,
373 pub is_timestamp_available: bool,
378}
379
380impl<'a> FormatterConfig<'a> {
381 pub fn custom(format: &'a str) -> Self {
386 FormatterConfig {
387 format: FormatterFormat::from_string(format, true)
388 .unwrap_or(FormatterFormat::Custom(format)),
389 is_timestamp_available: false,
390 }
391 }
392
393 pub fn with_timestamp(mut self) -> Self {
396 self.is_timestamp_available = true;
397 self
398 }
399
400 pub fn with_location(mut self) -> Self {
405 match self.format {
408 FormatterFormat::OneLine { with_location: _ } => {
409 self.format = FormatterFormat::OneLine {
410 with_location: true,
411 };
412 self
413 }
414 FormatterFormat::Default { with_location: _ } => {
415 self.format = FormatterFormat::Default {
416 with_location: true,
417 };
418 self
419 }
420 _ => self,
421 }
422 }
423}
424
425impl InternalFormatter {
426 fn new(config: FormatterConfig, source: Source) -> Self {
427 let format = match config.format {
428 FormatterFormat::Default { with_location } => {
429 let mut format =
430 DefaultStyle::get_string(with_location, config.is_timestamp_available)
431 .to_string();
432 if source == Source::Host {
433 format.push_str(" (HOST)");
434 }
435
436 format
437 }
438 FormatterFormat::OneLine { with_location } => {
439 let mut format =
440 OneLineStyle::get_string(with_location, config.is_timestamp_available)
441 .to_string();
442
443 if source == Source::Host {
444 format.push_str(" (HOST)");
445 }
446
447 format
448 }
449 FormatterFormat::Custom(format) => format.to_string(),
450 };
451
452 let format = parser::parse(&format).expect("log format is invalid '{format}'");
453
454 if matches!(config.format, FormatterFormat::Custom(_)) {
455 let format_has_timestamp = format_has_timestamp(&format);
456 if format_has_timestamp && !config.is_timestamp_available {
457 log::warn!(
458 "logger format contains timestamp but no timestamp implementation \
459 was provided; consider removing the timestamp (`{{t}}`) from the \
460 logger format or provide a `defmt::timestamp!` implementation"
461 );
462 } else if !format_has_timestamp && config.is_timestamp_available {
463 log::warn!(
464 "`defmt::timestamp!` implementation was found, but timestamp is not \
465 part of the log format; consider adding the timestamp (`{{t}}`) \
466 argument to the log format"
467 );
468 }
469 }
470
471 Self { format }
472 }
473
474 fn format(&self, record: &Record) -> String {
475 let mut buf = String::new();
476 if get_log_level_of_record(record).is_some() {
479 for segment in &self.format {
480 let s = self.build_segment(record, segment);
481 write!(buf, "{s}").expect("writing to String cannot fail");
482 }
483 } else {
484 let empty_format: LogFormat = Default::default();
485 let s = self.build_log(record, &empty_format);
486 write!(buf, "{s}").expect("writing to String cannot fail");
487 }
488 buf
489 }
490
491 fn build_segment(&self, record: &Record, segment: &LogSegment) -> String {
492 match &segment.metadata {
493 LogMetadata::String(s) => s.to_string(),
494 LogMetadata::Timestamp => self.build_timestamp(record, &segment.format),
495 LogMetadata::CrateName => self.build_crate_name(record, &segment.format),
496 LogMetadata::FileName(n) => self.build_file_name(record, &segment.format, *n),
497 LogMetadata::FilePath => self.build_file_path(record, &segment.format),
498 LogMetadata::ModulePath => self.build_module_path(record, &segment.format),
499 LogMetadata::LineNumber => self.build_line_number(record, &segment.format),
500 LogMetadata::LogLevel => self.build_log_level(record, &segment.format),
501 LogMetadata::Log => self.build_log(record, &segment.format),
502 LogMetadata::NestedLogSegments(segments) => {
503 self.build_nested(record, segments, &segment.format)
504 }
505 }
506 }
507
508 fn build_nested(&self, record: &Record, segments: &[LogSegment], format: &LogFormat) -> String {
509 let mut result = String::new();
510 for segment in segments {
511 let s = match &segment.metadata {
512 LogMetadata::String(s) => s.to_string(),
513 LogMetadata::Timestamp => self.build_timestamp(record, &segment.format),
514 LogMetadata::CrateName => self.build_crate_name(record, &segment.format),
515 LogMetadata::FileName(n) => self.build_file_name(record, &segment.format, *n),
516 LogMetadata::FilePath => self.build_file_path(record, &segment.format),
517 LogMetadata::ModulePath => self.build_module_path(record, &segment.format),
518 LogMetadata::LineNumber => self.build_line_number(record, &segment.format),
519 LogMetadata::LogLevel => self.build_log_level(record, &segment.format),
520 LogMetadata::Log => self.build_log(record, &segment.format),
521 LogMetadata::NestedLogSegments(segments) => {
522 self.build_nested(record, segments, &segment.format)
523 }
524 };
525 result.push_str(&s);
526 }
527
528 build_formatted_string(
529 &result,
530 format,
531 0,
532 get_log_level_of_record(record),
533 format.color,
534 )
535 }
536
537 fn build_timestamp(&self, record: &Record, format: &LogFormat) -> String {
538 let s = match record {
539 Record::Defmt(record) if !record.timestamp().is_empty() => record.timestamp(),
540 _ => "<time>",
541 }
542 .to_string();
543
544 build_formatted_string(
545 s.as_str(),
546 format,
547 0,
548 get_log_level_of_record(record),
549 format.color,
550 )
551 }
552
553 fn build_log_level(&self, record: &Record, format: &LogFormat) -> String {
554 let s = match get_log_level_of_record(record) {
555 Some(level) => level.to_string(),
556 None => "<lvl>".to_string(),
557 };
558
559 let color = format.color.unwrap_or(LogColor::SeverityLevel);
560
561 build_formatted_string(
562 s.as_str(),
563 format,
564 5,
565 get_log_level_of_record(record),
566 Some(color),
567 )
568 }
569
570 fn build_file_path(&self, record: &Record, format: &LogFormat) -> String {
571 let file_path = match record {
572 Record::Defmt(record) => record.file(),
573 Record::Host(record) => record.file(),
574 }
575 .unwrap_or("<file>");
576
577 build_formatted_string(
578 file_path,
579 format,
580 0,
581 get_log_level_of_record(record),
582 format.color,
583 )
584 }
585
586 fn build_file_name(&self, record: &Record, format: &LogFormat, level_of_detail: u8) -> String {
587 let file = match record {
588 Record::Defmt(record) => record.file(),
589 Record::Host(record) => record.file(),
590 };
591
592 let s = if let Some(file) = file {
593 let path_iter = Path::new(file).iter();
594 let number_of_components = path_iter.clone().count();
595
596 let number_of_components_to_join = number_of_components.min(level_of_detail as usize);
597
598 let number_of_elements_to_skip =
599 number_of_components.saturating_sub(number_of_components_to_join);
600 let s = path_iter
601 .skip(number_of_elements_to_skip)
602 .take(number_of_components)
603 .fold(String::new(), |mut acc, s| {
604 acc.push_str(s.to_str().unwrap_or("<?>"));
605 acc.push('/');
606 acc
607 });
608 s.strip_suffix('/').unwrap().to_string()
609 } else {
610 "<file>".to_string()
611 };
612
613 build_formatted_string(&s, format, 0, get_log_level_of_record(record), format.color)
614 }
615
616 fn build_module_path(&self, record: &Record, format: &LogFormat) -> String {
617 let s = match record {
618 Record::Defmt(record) => record.module_path(),
619 Record::Host(record) => record.module_path(),
620 }
621 .unwrap_or("<mod path>");
622
623 build_formatted_string(s, format, 0, get_log_level_of_record(record), format.color)
624 }
625
626 fn build_crate_name(&self, record: &Record, format: &LogFormat) -> String {
627 let module_path = match record {
628 Record::Defmt(record) => record.module_path(),
629 Record::Host(record) => record.module_path(),
630 };
631
632 let s = if let Some(module_path) = module_path {
633 let path = module_path.split("::").collect::<Vec<_>>();
634
635 if path.len() >= 2 {
637 path.first().unwrap()
638 } else {
639 "<crate>"
640 }
641 } else {
642 "<crate>"
643 };
644
645 build_formatted_string(s, format, 0, get_log_level_of_record(record), format.color)
646 }
647
648 fn build_line_number(&self, record: &Record, format: &LogFormat) -> String {
649 let s = match record {
650 Record::Defmt(record) => record.line(),
651 Record::Host(record) => record.line(),
652 }
653 .unwrap_or(0)
654 .to_string();
655
656 build_formatted_string(
657 s.as_str(),
658 format,
659 4,
660 get_log_level_of_record(record),
661 format.color,
662 )
663 }
664
665 fn build_log(&self, record: &Record, format: &LogFormat) -> String {
666 let log_level = get_log_level_of_record(record);
667 match record {
668 Record::Defmt(record) => match color_diff(record.args().to_string()) {
669 Ok(s) => s.to_string(),
670 Err(s) => build_formatted_string(s.as_str(), format, 0, log_level, format.color),
671 },
672 Record::Host(record) => record.args().to_string(),
673 }
674 }
675}
676
677fn get_log_level_of_record(record: &Record) -> Option<Level> {
678 match record {
679 Record::Defmt(record) => record.level(),
680 Record::Host(record) => Some(record.level()),
681 }
682}
683
684fn color_diff(text: String) -> Result<String, String> {
688 let lines = text.lines().collect::<Vec<_>>();
689 let nlines = lines.len();
690 if nlines > 2 {
691 let left = lines[nlines - 2];
692 let right = lines[nlines - 1];
693
694 const LEFT_START: &str = " left: `";
695 const RIGHT_START: &str = "right: `";
696 const END: &str = "`";
697 if left.starts_with(LEFT_START)
698 && left.ends_with(END)
699 && right.starts_with(RIGHT_START)
700 && right.ends_with(END)
701 {
702 let left = &left[LEFT_START.len()..left.len() - END.len()];
704 let right = &right[RIGHT_START.len()..right.len() - END.len()];
705
706 let mut buf = lines[..nlines - 2].join("\n").bold().to_string();
707 buf.push('\n');
708
709 let diffs = dissimilar::diff(left, right);
710
711 writeln!(
712 buf,
713 "{} {} / {}",
714 "diff".bold(),
715 "< left".red(),
716 "right >".green()
717 )
718 .ok();
719 write!(buf, "{}", "<".red()).ok();
720 for diff in &diffs {
721 match diff {
722 Chunk::Equal(s) => {
723 write!(buf, "{}", s.red()).ok();
724 }
725 Chunk::Insert(_) => continue,
726 Chunk::Delete(s) => {
727 write!(buf, "{}", s.red().bold()).ok();
728 }
729 }
730 }
731 buf.push('\n');
732
733 write!(buf, "{}", ">".green()).ok();
734 for diff in &diffs {
735 match diff {
736 Chunk::Equal(s) => {
737 write!(buf, "{}", s.green()).ok();
738 }
739 Chunk::Delete(_) => continue,
740 Chunk::Insert(s) => {
741 write!(buf, "{}", s.green().bold()).ok();
742 }
743 }
744 }
745 return Ok(buf);
746 }
747 }
748
749 Err(text)
750}
751
752fn color_for_log_level(level: Level) -> Color {
753 match level {
754 Level::Error => Color::Red,
755 Level::Warn => Color::Yellow,
756 Level::Info => Color::Green,
757 Level::Debug => Color::BrightWhite,
758 Level::Trace => Color::BrightBlack,
759 }
760}
761
762fn apply_color(
763 s: ColoredString,
764 log_color: Option<LogColor>,
765 level: Option<Level>,
766) -> ColoredString {
767 match log_color {
768 Some(color) => match color {
769 LogColor::Color(c) => s.color(c),
770 LogColor::SeverityLevel => match level {
771 Some(level) => s.color(color_for_log_level(level)),
772 None => s,
773 },
774 LogColor::WarnError => match level {
775 Some(level @ (Level::Warn | Level::Error)) => s.color(color_for_log_level(level)),
776 _ => s,
777 },
778 },
779 None => s,
780 }
781}
782
783fn apply_styles(s: ColoredString, log_style: Option<&Vec<Styles>>) -> ColoredString {
784 let Some(log_styles) = log_style else {
785 return s;
786 };
787
788 let mut stylized_string = s;
789 for style in log_styles {
790 stylized_string = match style {
791 Styles::Bold => stylized_string.bold(),
792 Styles::Italic => stylized_string.italic(),
793 Styles::Underline => stylized_string.underline(),
794 Styles::Strikethrough => stylized_string.strikethrough(),
795 Styles::Dimmed => stylized_string.dimmed(),
796 Styles::Clear => stylized_string.clear(),
797 Styles::Reversed => stylized_string.reversed(),
798 Styles::Blink => stylized_string.blink(),
799 Styles::Hidden => stylized_string.hidden(),
800 };
801 }
802
803 stylized_string
804}
805
806fn build_formatted_string(
807 s: &str,
808 format: &LogFormat,
809 default_width: usize,
810 level: Option<Level>,
811 log_color: Option<LogColor>,
812) -> String {
813 let s = ColoredString::from(s);
814 let styled_string_length = s.len();
815 let length_without_styles = string_excluding_ansi(&s).len();
816 let length_of_ansi_sequences = styled_string_length - length_without_styles;
817
818 let s = apply_color(s, log_color, level);
819 let colored_str = apply_styles(s, format.style.as_ref());
820
821 let alignment = format.alignment.unwrap_or(Alignment::Left);
822 let width = format.width.unwrap_or(default_width) + length_of_ansi_sequences;
823 let padding = format.padding.unwrap_or(Padding::Space);
824
825 let mut result = String::new();
826 match (alignment, padding) {
827 (Alignment::Left, Padding::Space) => write!(&mut result, "{colored_str:<0$}", width),
828 (Alignment::Left, Padding::Zero) => write!(&mut result, "{colored_str:0<0$}", width),
829 (Alignment::Center, Padding::Space) => write!(&mut result, "{colored_str:^0$}", width),
830 (Alignment::Center, Padding::Zero) => write!(&mut result, "{colored_str:0^0$}", width),
831 (Alignment::Right, Padding::Space) => write!(&mut result, "{colored_str:>0$}", width),
832 (Alignment::Right, Padding::Zero) => write!(&mut result, "{colored_str:0>0$}", width),
833 }
834 .expect("Failed to format string: \"{colored_str}\"");
835 result
836}
837
838fn format_has_timestamp(segments: &[LogSegment]) -> bool {
839 for segment in segments {
840 match &segment.metadata {
841 LogMetadata::Timestamp => return true,
842 LogMetadata::NestedLogSegments(s) => {
843 if format_has_timestamp(s) {
844 return true;
845 }
846 }
847 _ => continue,
848 }
849 }
850 false
851}
852
853fn string_excluding_ansi(s: &str) -> String {
855 let re = Regex::new(r"\x1b\[[0-9;]*m").unwrap();
857
858 re.replace_all(s, "").to_string()
860}
861
862#[cfg(test)]
863mod tests {
864 use super::*;
865
866 #[test]
867 fn test_left_aligned_styled_string() {
868 let format = LogFormat {
869 color: Some(LogColor::Color(Color::Green)),
870 width: Some(10),
871 alignment: Some(Alignment::Left),
872 padding: Some(Padding::Space),
873 style: Some(vec![Styles::Bold]),
874 };
875
876 let s = build_formatted_string("test", &format, 0, None, None);
877 let string_without_styles = string_excluding_ansi(&s);
878 assert_eq!(string_without_styles, "test ");
879 }
880
881 #[test]
882 fn test_right_aligned_styled_string() {
883 let format = LogFormat {
884 color: Some(LogColor::Color(Color::Green)),
885 width: Some(10),
886 alignment: Some(Alignment::Right),
887 padding: Some(Padding::Space),
888 style: Some(vec![Styles::Bold]),
889 };
890
891 let s = build_formatted_string("test", &format, 0, None, None);
892 let string_without_styles = string_excluding_ansi(&s);
893 assert_eq!(string_without_styles, " test");
894 }
895}