defmt_decoder/log/format/
mod.rs

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/// Representation of what a [LogSegment] can be.
12#[derive(Debug, PartialEq, Clone)]
13#[non_exhaustive]
14pub(super) enum LogMetadata {
15    /// `{c}` format specifier.
16    ///
17    /// Prints the name of the crate where the log is coming from.
18    CrateName,
19
20    /// `{f}` format specifier.
21    ///
22    /// This specifier may be repeated up to 255 times.
23    /// For a file "/path/to/crate/src/foo/bar.rs":
24    /// - `{f}` prints "bar.rs".
25    /// - `{ff}` prints "foo/bar.rs".
26    /// - `{fff}` prints "src/foo/bar.rs"
27    FileName(u8),
28
29    /// `{F}` format specifier.
30    ///
31    /// For a file "/path/to/crate/src/foo/bar.rs"
32    /// this option prints "/path/to/crate/src/foo/bar.rs".
33    FilePath,
34
35    /// `{l}` format specifier.
36    ///
37    /// Prints the line number where the log is coming from.
38    LineNumber,
39
40    /// `{s}` format specifier.
41    ///
42    /// Prints the actual log contents.
43    /// For `defmt::info!("hello")`, this prints "hello".
44    Log,
45
46    /// `{L}` format specifier.
47    ///
48    /// Prints the log level.
49    /// For `defmt::info!("hello")`, this prints "INFO".
50    LogLevel,
51
52    /// `{m}` format specifier.
53    ///
54    /// Prints the module path of the function where the log is coming from.
55    /// For the following log:
56    ///
57    /// ```ignore
58    /// // crate: my_crate
59    /// mod foo {
60    ///     fn bar() {
61    ///         defmt::info!("hello");
62    ///     }
63    /// }
64    /// ```
65    /// this prints "my_crate::foo::bar".
66    ModulePath,
67
68    /// Represents the parts of the formatting string that is not specifiers.
69    String(String),
70
71    /// `{t}` format specifier.
72    ///
73    /// Prints the timestamp at which something was logged.
74    /// For a log printed with a timestamp 123456 ms, this prints "123456".
75    Timestamp,
76
77    /// Represents formats specified within nested curly brackets in the formatting string.
78    NestedLogSegments(Vec<LogSegment>),
79}
80
81impl LogMetadata {
82    /// Checks whether this `LogMetadata` came from a specifier such as
83    /// {t}, {f}, etc.
84    fn is_metadata_specifier(&self) -> bool {
85        !matches!(
86            self,
87            LogMetadata::String(_) | LogMetadata::NestedLogSegments(_)
88        )
89    }
90}
91
92/// Coloring options for [LogSegment]s.
93#[derive(Debug, PartialEq, Clone, Copy)]
94pub(super) enum LogColor {
95    /// User-defined color.
96    ///
97    /// Use a string that can be parsed by the FromStr implementation
98    /// of [colored::Color].
99    Color(colored::Color),
100
101    /// Color matching the default color for the log level.
102    /// Use `"severity"` as a format parameter to use this option.
103    SeverityLevel,
104
105    /// Color matching the default color for the log level,
106    /// but only if the log level is WARN or ERROR.
107    ///
108    /// Use `"werror"` as a format parameter to use this option.
109    WarnError,
110}
111
112/// Alignment options for [LogSegment]s.
113#[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/// Representation of a segment of the formatting string.
127#[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
189/// A formatter for microcontroller-generated frames
190pub struct Formatter {
191    formatter: InternalFormatter,
192}
193
194impl Formatter {
195    /// Create a new formatter, using the given configuration.
196    pub fn new(config: FormatterConfig) -> Self {
197        Self {
198            formatter: InternalFormatter::new(config, Source::Defmt),
199        }
200    }
201
202    /// Format a defmt frame using this formatter.
203    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        // HACK: use match instead of let, because otherwise compilation fails
213        #[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    /// Format the given [`DefmtRecord`] (which is an internal type).
234    pub(super) fn format(&self, record: &DefmtRecord) -> String {
235        self.formatter.format(&Record::Defmt(record))
236    }
237}
238
239/// A formatter for host-generated frames
240pub struct HostFormatter {
241    formatter: InternalFormatter,
242}
243
244impl HostFormatter {
245    /// Create a new host formatter using the given config
246    pub fn new(config: FormatterConfig) -> Self {
247        Self {
248            formatter: InternalFormatter::new(config, Source::Host),
249        }
250    }
251
252    /// Format the given [`log::Record`].
253    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    /// The classic defmt two-line format.
278    ///
279    /// Looks like:
280    ///
281    /// ```text
282    /// INFO This is a log message
283    /// └─ test_lib::hello @ /Users/jonathan/Documents/knurling/test-lib/src/lib.rs:8
284    /// ```
285    Default {
286        with_location: bool,
287    },
288    /// A one-line format.
289    ///
290    /// Looks like:
291    ///
292    /// ```text
293    /// [INFO ] This is a log message (crate_name test-lib/src/lib.rs:8)
294    /// ```
295    OneLine {
296        with_location: bool,
297    },
298    Custom(&'a str),
299}
300
301impl FormatterFormat<'static> {
302    /// Parse a string into a choice of [`FormatterFormat`].
303    ///
304    /// Unknown strings return `None`.
305    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    /// Get a list of valid string names for the various format options.
314    ///
315    /// This will *not* include an entry for [`FormatterFormat::Custom`] because
316    /// that requires a format string argument.
317    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
330/// Describes one of the fixed format string sets.
331trait 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    /// Return a suitable format string, given these options.
338    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
348/// Implements the `FormatterFormat::Default` style.
349struct 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
358/// Implements the `FormatterFormat::OneLine` style.
359struct 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/// Settings that control how defmt frames should be formatted.
369#[derive(Debug, Default)]
370pub struct FormatterConfig<'a> {
371    /// The format to use
372    pub format: FormatterFormat<'a>,
373    /// If `true`, then the logs should include a timestamp.
374    ///
375    /// Not all targets can supply a timestamp, and if not, it should be
376    /// omitted.
377    pub is_timestamp_available: bool,
378}
379
380impl<'a> FormatterConfig<'a> {
381    /// Create a new custom formatter config.
382    ///
383    /// This allows the user to supply a custom log-format string. See the
384    /// "Custom Log Output" section of the defmt book for details of the format.
385    pub fn custom(format: &'a str) -> Self {
386        FormatterConfig {
387            format: FormatterFormat::Custom(format),
388            is_timestamp_available: false,
389        }
390    }
391
392    /// Modify a formatter configuration, setting the 'timestamp available' flag
393    /// to true.
394    pub fn with_timestamp(mut self) -> Self {
395        self.is_timestamp_available = true;
396        self
397    }
398
399    /// Modify a formatter configuration, setting the 'with_location' flag
400    /// to true.
401    ///
402    /// Do not use this with a custom log formatter.
403    pub fn with_location(mut self) -> Self {
404        // TODO: Should we warn the user that trying to set a location
405        //       for a custom format won't work?
406        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        // Only format logs, not printlns
476        // printlns do not have a log level
477        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            // There need to be at least two elements, the crate and the function
635            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
683// color the output of `defmt::assert_eq`
684// HACK we should not re-parse formatted output but instead directly format into a color diff
685// template; that may require specially tagging log messages that come from `defmt::assert_eq`
686fn 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            // `defmt::assert_eq!` output
702            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
852/// Returns the given string excluding ANSI control sequences.
853fn string_excluding_ansi(s: &str) -> String {
854    // Regular expression to match ANSI escape sequences
855    let re = Regex::new(r"\x1b\[[0-9;]*m").unwrap();
856
857    // Replace all ANSI sequences with an empty string
858    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}