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::Custom(format),
388 is_timestamp_available: false,
389 }
390 }
391
392 pub fn with_timestamp(mut self) -> Self {
395 self.is_timestamp_available = true;
396 self
397 }
398
399 pub fn with_location(mut self) -> Self {
404 match self.format {
407 FormatterFormat::OneLine { with_location: _ } => {
408 self.format = FormatterFormat::OneLine {
409 with_location: true,
410 };
411 self
412 }
413 FormatterFormat::Default { with_location: _ } => {
414 self.format = FormatterFormat::Default {
415 with_location: true,
416 };
417 self
418 }
419 _ => self,
420 }
421 }
422}
423
424impl InternalFormatter {
425 fn new(config: FormatterConfig, source: Source) -> Self {
426 let format = match config.format {
427 FormatterFormat::Default { with_location } => {
428 let mut format =
429 DefaultStyle::get_string(with_location, config.is_timestamp_available)
430 .to_string();
431 if source == Source::Host {
432 format.insert_str(0, "(HOST) ");
433 }
434
435 format
436 }
437 FormatterFormat::OneLine { with_location } => {
438 let mut format =
439 OneLineStyle::get_string(with_location, config.is_timestamp_available)
440 .to_string();
441
442 if source == Source::Host {
443 format.insert_str(0, "(HOST) ");
444 }
445
446 format
447 }
448 FormatterFormat::Custom(format) => format.to_string(),
449 };
450
451 let format = parser::parse(&format).expect("log format is invalid '{format}'");
452
453 if matches!(config.format, FormatterFormat::Custom(_)) {
454 let format_has_timestamp = format_has_timestamp(&format);
455 if format_has_timestamp && !config.is_timestamp_available {
456 log::warn!(
457 "logger format contains timestamp but no timestamp implementation \
458 was provided; consider removing the timestamp (`{{t}}`) from the \
459 logger format or provide a `defmt::timestamp!` implementation"
460 );
461 } else if !format_has_timestamp && config.is_timestamp_available {
462 log::warn!(
463 "`defmt::timestamp!` implementation was found, but timestamp is not \
464 part of the log format; consider adding the timestamp (`{{t}}`) \
465 argument to the log format"
466 );
467 }
468 }
469
470 Self { format }
471 }
472
473 fn format(&self, record: &Record) -> String {
474 let mut buf = String::new();
475 if get_log_level_of_record(record).is_some() {
478 for segment in &self.format {
479 let s = self.build_segment(record, segment);
480 write!(buf, "{s}").expect("writing to String cannot fail");
481 }
482 } else {
483 let empty_format: LogFormat = Default::default();
484 let s = self.build_log(record, &empty_format);
485 write!(buf, "{s}").expect("writing to String cannot fail");
486 }
487 buf
488 }
489
490 fn build_segment(&self, record: &Record, segment: &LogSegment) -> String {
491 match &segment.metadata {
492 LogMetadata::String(s) => s.to_string(),
493 LogMetadata::Timestamp => self.build_timestamp(record, &segment.format),
494 LogMetadata::CrateName => self.build_crate_name(record, &segment.format),
495 LogMetadata::FileName(n) => self.build_file_name(record, &segment.format, *n),
496 LogMetadata::FilePath => self.build_file_path(record, &segment.format),
497 LogMetadata::ModulePath => self.build_module_path(record, &segment.format),
498 LogMetadata::LineNumber => self.build_line_number(record, &segment.format),
499 LogMetadata::LogLevel => self.build_log_level(record, &segment.format),
500 LogMetadata::Log => self.build_log(record, &segment.format),
501 LogMetadata::NestedLogSegments(segments) => {
502 self.build_nested(record, segments, &segment.format)
503 }
504 }
505 }
506
507 fn build_nested(&self, record: &Record, segments: &[LogSegment], format: &LogFormat) -> String {
508 let mut result = String::new();
509 for segment in segments {
510 let s = match &segment.metadata {
511 LogMetadata::String(s) => s.to_string(),
512 LogMetadata::Timestamp => self.build_timestamp(record, &segment.format),
513 LogMetadata::CrateName => self.build_crate_name(record, &segment.format),
514 LogMetadata::FileName(n) => self.build_file_name(record, &segment.format, *n),
515 LogMetadata::FilePath => self.build_file_path(record, &segment.format),
516 LogMetadata::ModulePath => self.build_module_path(record, &segment.format),
517 LogMetadata::LineNumber => self.build_line_number(record, &segment.format),
518 LogMetadata::LogLevel => self.build_log_level(record, &segment.format),
519 LogMetadata::Log => self.build_log(record, &segment.format),
520 LogMetadata::NestedLogSegments(segments) => {
521 self.build_nested(record, segments, &segment.format)
522 }
523 };
524 result.push_str(&s);
525 }
526
527 build_formatted_string(
528 &result,
529 format,
530 0,
531 get_log_level_of_record(record),
532 format.color,
533 )
534 }
535
536 fn build_timestamp(&self, record: &Record, format: &LogFormat) -> String {
537 let s = match record {
538 Record::Defmt(record) if !record.timestamp().is_empty() => record.timestamp(),
539 _ => "<time>",
540 }
541 .to_string();
542
543 build_formatted_string(
544 s.as_str(),
545 format,
546 0,
547 get_log_level_of_record(record),
548 format.color,
549 )
550 }
551
552 fn build_log_level(&self, record: &Record, format: &LogFormat) -> String {
553 let s = match get_log_level_of_record(record) {
554 Some(level) => level.to_string(),
555 None => "<lvl>".to_string(),
556 };
557
558 let color = format.color.unwrap_or(LogColor::SeverityLevel);
559
560 build_formatted_string(
561 s.as_str(),
562 format,
563 5,
564 get_log_level_of_record(record),
565 Some(color),
566 )
567 }
568
569 fn build_file_path(&self, record: &Record, format: &LogFormat) -> String {
570 let file_path = match record {
571 Record::Defmt(record) => record.file(),
572 Record::Host(record) => record.file(),
573 }
574 .unwrap_or("<file>");
575
576 build_formatted_string(
577 file_path,
578 format,
579 0,
580 get_log_level_of_record(record),
581 format.color,
582 )
583 }
584
585 fn build_file_name(&self, record: &Record, format: &LogFormat, level_of_detail: u8) -> String {
586 let file = match record {
587 Record::Defmt(record) => record.file(),
588 Record::Host(record) => record.file(),
589 };
590
591 let s = if let Some(file) = file {
592 let path_iter = Path::new(file).iter();
593 let number_of_components = path_iter.clone().count();
594
595 let number_of_components_to_join = number_of_components.min(level_of_detail as usize);
596
597 let number_of_elements_to_skip =
598 number_of_components.saturating_sub(number_of_components_to_join);
599 let s = path_iter
600 .skip(number_of_elements_to_skip)
601 .take(number_of_components)
602 .fold(String::new(), |mut acc, s| {
603 acc.push_str(s.to_str().unwrap_or("<?>"));
604 acc.push('/');
605 acc
606 });
607 s.strip_suffix('/').unwrap().to_string()
608 } else {
609 "<file>".to_string()
610 };
611
612 build_formatted_string(&s, format, 0, get_log_level_of_record(record), format.color)
613 }
614
615 fn build_module_path(&self, record: &Record, format: &LogFormat) -> String {
616 let s = match record {
617 Record::Defmt(record) => record.module_path(),
618 Record::Host(record) => record.module_path(),
619 }
620 .unwrap_or("<mod path>");
621
622 build_formatted_string(s, format, 0, get_log_level_of_record(record), format.color)
623 }
624
625 fn build_crate_name(&self, record: &Record, format: &LogFormat) -> String {
626 let module_path = match record {
627 Record::Defmt(record) => record.module_path(),
628 Record::Host(record) => record.module_path(),
629 };
630
631 let s = if let Some(module_path) = module_path {
632 let path = module_path.split("::").collect::<Vec<_>>();
633
634 if path.len() >= 2 {
636 path.first().unwrap()
637 } else {
638 "<crate>"
639 }
640 } else {
641 "<crate>"
642 };
643
644 build_formatted_string(s, format, 0, get_log_level_of_record(record), format.color)
645 }
646
647 fn build_line_number(&self, record: &Record, format: &LogFormat) -> String {
648 let s = match record {
649 Record::Defmt(record) => record.line(),
650 Record::Host(record) => record.line(),
651 }
652 .unwrap_or(0)
653 .to_string();
654
655 build_formatted_string(
656 s.as_str(),
657 format,
658 4,
659 get_log_level_of_record(record),
660 format.color,
661 )
662 }
663
664 fn build_log(&self, record: &Record, format: &LogFormat) -> String {
665 let log_level = get_log_level_of_record(record);
666 match record {
667 Record::Defmt(record) => match color_diff(record.args().to_string()) {
668 Ok(s) => s.to_string(),
669 Err(s) => build_formatted_string(s.as_str(), format, 0, log_level, format.color),
670 },
671 Record::Host(record) => record.args().to_string(),
672 }
673 }
674}
675
676fn get_log_level_of_record(record: &Record) -> Option<Level> {
677 match record {
678 Record::Defmt(record) => record.level(),
679 Record::Host(record) => Some(record.level()),
680 }
681}
682
683fn color_diff(text: String) -> Result<String, String> {
687 let lines = text.lines().collect::<Vec<_>>();
688 let nlines = lines.len();
689 if nlines > 2 {
690 let left = lines[nlines - 2];
691 let right = lines[nlines - 1];
692
693 const LEFT_START: &str = " left: `";
694 const RIGHT_START: &str = "right: `";
695 const END: &str = "`";
696 if left.starts_with(LEFT_START)
697 && left.ends_with(END)
698 && right.starts_with(RIGHT_START)
699 && right.ends_with(END)
700 {
701 let left = &left[LEFT_START.len()..left.len() - END.len()];
703 let right = &right[RIGHT_START.len()..right.len() - END.len()];
704
705 let mut buf = lines[..nlines - 2].join("\n").bold().to_string();
706 buf.push('\n');
707
708 let diffs = dissimilar::diff(left, right);
709
710 writeln!(
711 buf,
712 "{} {} / {}",
713 "diff".bold(),
714 "< left".red(),
715 "right >".green()
716 )
717 .ok();
718 write!(buf, "{}", "<".red()).ok();
719 for diff in &diffs {
720 match diff {
721 Chunk::Equal(s) => {
722 write!(buf, "{}", s.red()).ok();
723 }
724 Chunk::Insert(_) => continue,
725 Chunk::Delete(s) => {
726 write!(buf, "{}", s.red().bold()).ok();
727 }
728 }
729 }
730 buf.push('\n');
731
732 write!(buf, "{}", ">".green()).ok();
733 for diff in &diffs {
734 match diff {
735 Chunk::Equal(s) => {
736 write!(buf, "{}", s.green()).ok();
737 }
738 Chunk::Delete(_) => continue,
739 Chunk::Insert(s) => {
740 write!(buf, "{}", s.green().bold()).ok();
741 }
742 }
743 }
744 return Ok(buf);
745 }
746 }
747
748 Err(text)
749}
750
751fn color_for_log_level(level: Level) -> Color {
752 match level {
753 Level::Error => Color::Red,
754 Level::Warn => Color::Yellow,
755 Level::Info => Color::Green,
756 Level::Debug => Color::BrightWhite,
757 Level::Trace => Color::BrightBlack,
758 }
759}
760
761fn apply_color(
762 s: ColoredString,
763 log_color: Option<LogColor>,
764 level: Option<Level>,
765) -> ColoredString {
766 match log_color {
767 Some(color) => match color {
768 LogColor::Color(c) => s.color(c),
769 LogColor::SeverityLevel => match level {
770 Some(level) => s.color(color_for_log_level(level)),
771 None => s,
772 },
773 LogColor::WarnError => match level {
774 Some(level @ (Level::Warn | Level::Error)) => s.color(color_for_log_level(level)),
775 _ => s,
776 },
777 },
778 None => s,
779 }
780}
781
782fn apply_styles(s: ColoredString, log_style: Option<&Vec<Styles>>) -> ColoredString {
783 let Some(log_styles) = log_style else {
784 return s;
785 };
786
787 let mut stylized_string = s;
788 for style in log_styles {
789 stylized_string = match style {
790 Styles::Bold => stylized_string.bold(),
791 Styles::Italic => stylized_string.italic(),
792 Styles::Underline => stylized_string.underline(),
793 Styles::Strikethrough => stylized_string.strikethrough(),
794 Styles::Dimmed => stylized_string.dimmed(),
795 Styles::Clear => stylized_string.clear(),
796 Styles::Reversed => stylized_string.reversed(),
797 Styles::Blink => stylized_string.blink(),
798 Styles::Hidden => stylized_string.hidden(),
799 };
800 }
801
802 stylized_string
803}
804
805fn build_formatted_string(
806 s: &str,
807 format: &LogFormat,
808 default_width: usize,
809 level: Option<Level>,
810 log_color: Option<LogColor>,
811) -> String {
812 let s = ColoredString::from(s);
813 let styled_string_length = s.len();
814 let length_without_styles = string_excluding_ansi(&s).len();
815 let length_of_ansi_sequences = styled_string_length - length_without_styles;
816
817 let s = apply_color(s, log_color, level);
818 let colored_str = apply_styles(s, format.style.as_ref());
819
820 let alignment = format.alignment.unwrap_or(Alignment::Left);
821 let width = format.width.unwrap_or(default_width) + length_of_ansi_sequences;
822 let padding = format.padding.unwrap_or(Padding::Space);
823
824 let mut result = String::new();
825 match (alignment, padding) {
826 (Alignment::Left, Padding::Space) => write!(&mut result, "{colored_str:<0$}", width),
827 (Alignment::Left, Padding::Zero) => write!(&mut result, "{colored_str:0<0$}", width),
828 (Alignment::Center, Padding::Space) => write!(&mut result, "{colored_str:^0$}", width),
829 (Alignment::Center, Padding::Zero) => write!(&mut result, "{colored_str:0^0$}", width),
830 (Alignment::Right, Padding::Space) => write!(&mut result, "{colored_str:>0$}", width),
831 (Alignment::Right, Padding::Zero) => write!(&mut result, "{colored_str:0>0$}", width),
832 }
833 .expect("Failed to format string: \"{colored_str}\"");
834 result
835}
836
837fn format_has_timestamp(segments: &[LogSegment]) -> bool {
838 for segment in segments {
839 match &segment.metadata {
840 LogMetadata::Timestamp => return true,
841 LogMetadata::NestedLogSegments(s) => {
842 if format_has_timestamp(s) {
843 return true;
844 }
845 }
846 _ => continue,
847 }
848 }
849 false
850}
851
852fn string_excluding_ansi(s: &str) -> String {
854 let re = Regex::new(r"\x1b\[[0-9;]*m").unwrap();
856
857 re.replace_all(s, "").to_string()
859}
860
861#[cfg(test)]
862mod tests {
863 use super::*;
864
865 #[test]
866 fn test_left_aligned_styled_string() {
867 let format = LogFormat {
868 color: Some(LogColor::Color(Color::Green)),
869 width: Some(10),
870 alignment: Some(Alignment::Left),
871 padding: Some(Padding::Space),
872 style: Some(vec![Styles::Bold]),
873 };
874
875 let s = build_formatted_string("test", &format, 0, None, None);
876 let string_without_styles = string_excluding_ansi(&s);
877 assert_eq!(string_without_styles, "test ");
878 }
879
880 #[test]
881 fn test_right_aligned_styled_string() {
882 let format = LogFormat {
883 color: Some(LogColor::Color(Color::Green)),
884 width: Some(10),
885 alignment: Some(Alignment::Right),
886 padding: Some(Padding::Space),
887 style: Some(vec![Styles::Bold]),
888 };
889
890 let s = build_formatted_string("test", &format, 0, None, None);
891 let string_without_styles = string_excluding_ansi(&s);
892 assert_eq!(string_without_styles, " test");
893 }
894}