Skip to main content

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::from_string(format, true)
388                .unwrap_or(FormatterFormat::Custom(format)),
389            is_timestamp_available: false,
390        }
391    }
392
393    /// Modify a formatter configuration, setting the 'timestamp available' flag
394    /// to true.
395    pub fn with_timestamp(mut self) -> Self {
396        self.is_timestamp_available = true;
397        self
398    }
399
400    /// Modify a formatter configuration, setting the 'with_location' flag
401    /// to true.
402    ///
403    /// Do not use this with a custom log formatter.
404    pub fn with_location(mut self) -> Self {
405        // TODO: Should we warn the user that trying to set a location
406        //       for a custom format won't work?
407        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        // Only format logs, not printlns
477        // printlns do not have a log level
478        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            // There need to be at least two elements, the crate and the function
636            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
684// color the output of `defmt::assert_eq`
685// HACK we should not re-parse formatted output but instead directly format into a color diff
686// template; that may require specially tagging log messages that come from `defmt::assert_eq`
687fn 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            // `defmt::assert_eq!` output
703            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
853/// Returns the given string excluding ANSI control sequences.
854fn string_excluding_ansi(s: &str) -> String {
855    // Regular expression to match ANSI escape sequences
856    let re = Regex::new(r"\x1b\[[0-9;]*m").unwrap();
857
858    // Replace all ANSI sequences with an empty string
859    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}