diff --git a/Cargo.lock b/Cargo.lock index 7603a9ae..4588cd06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -600,7 +600,7 @@ checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" [[package]] name = "encstr" -version = "0.28.2-alpha.2" +version = "0.29.0-alpha.1" [[package]] name = "enum-map" @@ -757,7 +757,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hl" -version = "0.28.2-alpha.2" +version = "0.29.0-alpha.1" dependencies = [ "atoi", "bincode", diff --git a/Cargo.toml b/Cargo.toml index b5406168..b48ee1d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = [".", "crate/encstr"] [workspace.package] repository = "https://github.com/pamburus/hl" authors = ["Pavel Ivanov "] -version = "0.28.2-alpha.2" +version = "0.29.0-alpha.1" edition = "2021" license = "MIT" diff --git a/README.md b/README.md index 7bc9edbc..ce449b32 100644 --- a/README.md +++ b/README.md @@ -488,6 +488,7 @@ Output Options: --no-raw Disable raw source messages output, overrides --raw option --raw-fields Output field values as is, without unescaping or prettifying -h, --hide Hide or reveal fields with the specified keys, prefix with ! to reveal, specify '!*' to reveal all + --flatten Whether to flatten objects [default: always] [possible values: never, always] -t, --time-format Time format, see https://man7.org/linux/man-pages/man1/date.1.html [env: HL_TIME_FORMAT=] [default: "%b %d %T.%3N"] -Z, --time-zone Time zone name, see column "TZ identifier" at https://en.wikipedia.org/wiki/List_of_tz_database_time_zones [env: HL_TIME_ZONE=] [default: UTC] -L, --local Use local time zone, overrides --time-zone option diff --git a/src/app.rs b/src/app.rs index 2e1c821c..a778d379 100644 --- a/src/app.rs +++ b/src/app.rs @@ -74,6 +74,7 @@ pub struct Options { pub tail: u64, pub delimiter: Delimiter, pub unix_ts_unit: Option, + pub flatten: bool, } impl Options { @@ -639,7 +640,8 @@ impl App { self.options.fields.filter.clone(), self.options.formatting.clone(), ) - .with_field_unescaping(!self.options.raw_fields), + .with_field_unescaping(!self.options.raw_fields) + .with_flatten(self.options.flatten), ) } } diff --git a/src/cli.rs b/src/cli.rs index 6fbd4a5d..61d1fb77 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -153,6 +153,17 @@ pub struct Opt { )] pub hide: Vec, + /// Whether to flatten objects. + #[arg( + long, + value_name = "WHEN", + value_enum, + default_value = "always", + overrides_with = "flatten", + help_heading = heading::OUTPUT + )] + pub flatten: FlattenOption, + /// Time format, see https://man7.org/linux/man-pages/man1/date.1.html. #[arg( short, @@ -360,6 +371,12 @@ pub enum UnixTimestampUnit { Ns, } +#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)] +pub enum FlattenOption { + Never, + Always, +} + mod heading { pub const FILTERING: &str = "Filtering Options"; pub const INPUT: &str = "Input Options"; diff --git a/src/fmtx.rs b/src/fmtx.rs index 305e8dd0..fd7a0302 100644 --- a/src/fmtx.rs +++ b/src/fmtx.rs @@ -11,15 +11,109 @@ impl Push for Vec where T: Clone, { + #[inline(always)] fn push(&mut self, value: T) { Vec::push(self, value) } + #[inline(always)] fn extend_from_slice(&mut self, values: &[T]) { Vec::extend_from_slice(self, values) } } +impl Push for heapless::Vec +where + T: Clone, +{ + #[inline(always)] + fn push(&mut self, value: T) { + Self::push(self, value).ok(); + } + + #[inline(always)] + fn extend_from_slice(&mut self, values: &[T]) { + Self::extend_from_slice(self, values).ok(); + } +} + +// --- + +#[derive(Default)] +pub struct OptimizedBuf { + pub head: heapless::Vec, + pub tail: Vec, +} + +impl OptimizedBuf +where + T: Clone, +{ + #[inline(always)] + pub fn new() -> Self { + Self { + head: heapless::Vec::new(), + tail: Vec::new(), + } + } + + #[inline(always)] + pub fn len(&self) -> usize { + self.head.len() + self.tail.len() + } + + #[inline(always)] + pub fn clear(&mut self) { + self.head.clear(); + self.tail.clear(); + } + + #[inline(always)] + pub fn truncate(&mut self, len: usize) { + if len <= self.head.len() { + self.head.truncate(len); + self.tail.clear(); + } else { + self.tail.truncate(len - self.head.len()); + } + } + + #[inline(always)] + pub fn push(&mut self, value: T) { + if self.head.len() < N { + self.head.push(value).ok(); + } else { + self.tail.push(value); + } + } + + #[inline(always)] + pub fn extend_from_slice(&mut self, values: &[T]) { + if self.head.len() + values.len() <= N { + self.head.extend_from_slice(values).ok(); + } else { + let n = N - self.head.len(); + self.head.extend_from_slice(&values[..n]).ok(); + self.tail.extend_from_slice(&values[n..]); + } + } +} + +impl Push for OptimizedBuf +where + T: Clone, +{ + #[inline(always)] + fn push(&mut self, value: T) { + OptimizedBuf::push(self, value) + } + + #[inline(always)] + fn extend_from_slice(&mut self, values: &[T]) { + OptimizedBuf::extend_from_slice(self, values) + } +} + // --- pub struct Counter { @@ -27,20 +121,24 @@ pub struct Counter { } impl Counter { + #[inline(always)] pub fn new() -> Self { Self { value: 0 } } + #[inline(always)] pub fn result(&self) -> usize { self.value } } impl Push for Counter { + #[inline(always)] fn push(&mut self, _: T) { self.value += 1 } + #[inline(always)] fn extend_from_slice(&mut self, values: &[T]) { self.value += values.len() } @@ -65,6 +163,7 @@ pub struct Padding { } impl Padding { + #[inline(always)] pub fn new(pad: T, width: usize) -> Self { Self { pad, width } } @@ -79,6 +178,7 @@ pub struct Adjustment { } impl Adjustment { + #[inline(always)] pub fn new(alignment: Alignment, padding: Padding) -> Self { Self { alignment, padding } } @@ -101,6 +201,7 @@ where T: Clone, O: Push, { + #[inline(always)] fn new(out: &'a mut O, adjustment: Option>) -> Self { if let Some(adjustment) = adjustment { match adjustment.alignment { @@ -114,6 +215,7 @@ where } } + #[inline(always)] fn push(&mut self, value: T) { match self { Self::Disabled(ref mut aligner) => aligner.push(value), @@ -122,6 +224,7 @@ where } } + #[inline(always)] fn extend_from_slice(&mut self, values: &[T]) { match self { Self::Disabled(ref mut aligner) => aligner.extend_from_slice(values), @@ -136,10 +239,12 @@ where T: Clone, B: Push, { + #[inline(always)] fn push(&mut self, value: T) { Aligner::push(self, value) } + #[inline(always)] fn extend_from_slice(&mut self, values: &[T]) { Aligner::extend_from_slice(self, values) } @@ -152,6 +257,7 @@ pub struct DisabledAligner<'a, O> { } impl<'a, O> DisabledAligner<'a, O> { + #[inline(always)] pub fn new(out: &'a mut O) -> Self { Self { out } } @@ -162,10 +268,12 @@ where T: Clone, O: Push, { + #[inline(always)] fn push(&mut self, value: T) { self.out.push(value) } + #[inline(always)] fn extend_from_slice(&mut self, values: &[T]) { self.out.extend_from_slice(values) } @@ -188,10 +296,12 @@ where T: Clone, O: Push, { + #[inline(always)] pub fn new(out: &'a mut O, padding: Padding) -> Self { Self { out, padding, cur: 0 } } + #[inline(always)] pub fn push(&mut self, value: T) { if self.cur < self.padding.width { self.out.push(value); @@ -199,6 +309,7 @@ where } } + #[inline(always)] pub fn extend_from_slice(&mut self, values: &[T]) { if self.cur < self.padding.width { let n = min(self.padding.width - self.cur, values.len()); @@ -213,10 +324,12 @@ where T: Clone, O: Push, { + #[inline(always)] fn push(&mut self, value: T) { UnbufferedAligner::push(self, value) } + #[inline(always)] fn extend_from_slice(&mut self, values: &[T]) { UnbufferedAligner::extend_from_slice(self, values) } @@ -227,6 +340,7 @@ where T: Clone, O: Push, { + #[inline(always)] fn drop(&mut self) { for _ in self.cur..self.padding.width { self.out.push(self.padding.pad.clone()); @@ -252,6 +366,7 @@ where T: Clone, O: Push, { + #[inline(always)] fn new(out: &'a mut O, padding: Padding, alignment: Alignment) -> Self { Self { out, @@ -265,6 +380,7 @@ where } } + #[inline(always)] pub fn push(&mut self, value: T) { match self.buf { AlignerBuffer::Static(ref mut buf) => { @@ -280,6 +396,7 @@ where } } + #[inline(always)] pub fn extend_from_slice(&mut self, values: &[T]) { match self.buf { AlignerBuffer::Static(ref mut buf) => { @@ -299,10 +416,12 @@ where T: Clone, O: Push, { + #[inline(always)] fn push(&mut self, value: T) { BufferedAligner::push(self, value) } + #[inline(always)] fn extend_from_slice(&mut self, values: &[T]) { BufferedAligner::extend_from_slice(self, values) } @@ -313,6 +432,7 @@ where T: Clone, O: Push, { + #[inline] fn drop(&mut self) { let buf = match &self.buf { AlignerBuffer::Static(buf) => &buf[..], @@ -342,6 +462,7 @@ enum AlignerBuffer { // --- +#[inline(always)] pub fn aligned<'a, T, O, F>(out: &'a mut O, adjustment: Option>, f: F) where T: Clone, @@ -351,6 +472,7 @@ where f(Aligner::new(out, adjustment)); } +#[inline(always)] pub fn aligned_left<'a, T, O, F>(out: &'a mut O, width: usize, pad: T, f: F) where T: Clone, @@ -360,6 +482,7 @@ where f(UnbufferedAligner::new(out, Padding::new(pad, width))); } +#[inline(always)] pub fn centered<'a, T, O, F>(out: &'a mut O, width: usize, pad: T, f: F) where T: Clone, diff --git a/src/formatting.rs b/src/formatting.rs index 2a5ab049..f8e10a31 100644 --- a/src/formatting.rs +++ b/src/formatting.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use crate::{ datefmt::DateTimeFormatter, filtering::IncludeExcludeSetting, - fmtx::{aligned_left, centered, Push}, + fmtx::{aligned_left, centered, OptimizedBuf, Push}, model::{self, Caller, Level, RawValue}, settings::Formatting, theme::{Element, StylingPush, Theme}, @@ -57,6 +57,7 @@ pub struct RecordFormatter { ts_formatter: DateTimeFormatter, ts_width: usize, hide_empty_fields: bool, + flatten: bool, fields: Arc, cfg: Formatting, } @@ -76,6 +77,7 @@ impl RecordFormatter { ts_formatter, ts_width, hide_empty_fields, + flatten: false, fields, cfg, } @@ -86,7 +88,14 @@ impl RecordFormatter { self } + pub fn with_flatten(mut self, value: bool) -> Self { + self.flatten = value; + self + } + pub fn format_record(&self, buf: &mut Buf, rec: &model::Record) { + let mut fs = FormattingState::new(self.flatten && self.unescape_fields); + self.theme.apply(buf, &rec.level, |s| { // // time @@ -152,7 +161,7 @@ impl RecordFormatter { // if let Some(text) = &rec.message { s.batch(|buf| buf.push(b' ')); - s.element(Element::Message, |s| self.format_message(s, *text)); + s.element(Element::Message, |s| self.format_message(s, &mut fs, *text)); } else { s.reset(); } @@ -162,7 +171,7 @@ impl RecordFormatter { let mut some_fields_hidden = false; for (k, v) in rec.fields() { if !self.hide_empty_fields || !v.is_empty() { - some_fields_hidden |= !self.format_field(s, k, *v, Some(&self.fields)); + some_fields_hidden |= !self.format_field(s, k, *v, &mut fs, Some(&self.fields)); } } if some_fields_hidden { @@ -205,18 +214,19 @@ impl RecordFormatter { s: &mut S, key: &str, value: RawValue<'a>, + fs: &mut FormattingState, filter: Option<&IncludeExcludeKeyFilter>, ) -> bool { let mut fv = FieldFormatter::new(self); - fv.format(s, key, value, filter, IncludeExcludeSetting::Unspecified) + fv.format(s, key, value, fs, filter, IncludeExcludeSetting::Unspecified) } - fn format_value<'a, S: StylingPush>(&self, s: &mut S, value: RawValue<'a>) { + fn format_value<'a, S: StylingPush>(&self, s: &mut S, fs: &mut FormattingState, value: RawValue<'a>) { let mut fv = FieldFormatter::new(self); - fv.format_value(s, value, None, IncludeExcludeSetting::Unspecified); + fv.format_value(s, value, fs, None, IncludeExcludeSetting::Unspecified); } - fn format_message<'a, S: StylingPush>(&self, s: &mut S, value: RawValue<'a>) { + fn format_message<'a, S: StylingPush>(&self, s: &mut S, fs: &mut FormattingState, value: RawValue<'a>) { match value { RawValue::String(value) => { s.element(Element::Message, |s| { @@ -241,7 +251,7 @@ impl RecordFormatter { s.batch(|buf| buf.push(b'{')); let mut has_some = false; for (k, v) in item.fields.iter() { - has_some |= self.format_field(s, k, *v, None) + has_some |= self.format_field(s, k, *v, fs, None) } s.batch(|buf| { if has_some { @@ -287,7 +297,7 @@ impl RecordFormatter { } else { first = false; } - self.format_value(s, *v); + self.format_value(s, fs, *v); } s.batch(|buf| buf.push(b']')) }); @@ -305,6 +315,65 @@ impl RecordWithSourceFormatter for RecordFormatter { // --- +struct FormattingState { + key_prefix: KeyPrefix, + flatten: bool, +} + +impl FormattingState { + #[inline(always)] + fn new(flatten: bool) -> Self { + Self { + key_prefix: KeyPrefix::default(), + flatten, + } + } +} + +// --- + +#[derive(Default)] +struct KeyPrefix { + value: OptimizedBuf, +} + +impl KeyPrefix { + #[inline(always)] + fn len(&self) -> usize { + self.value.len() + } + + #[inline(always)] + fn format>(&self, buf: &mut B) { + buf.extend_from_slice(&self.value.head); + buf.extend_from_slice(&self.value.tail); + } + + #[inline(always)] + fn push(&mut self, key: &str) -> usize { + let len = self.len(); + if len != 0 { + self.value.push(b'.'); + } + key.key_prettify(&mut self.value); + self.len() - len + } + + #[inline(always)] + fn pop(&mut self, n: usize) { + if n != 0 { + let len = self.len(); + if n >= len { + self.value.clear(); + } else { + self.value.truncate(len - n); + } + } + } +} + +// --- + struct FieldFormatter<'a> { rf: &'a RecordFormatter, } @@ -319,6 +388,7 @@ impl<'a> FieldFormatter<'a> { s: &mut S, key: &str, value: RawValue<'a>, + fs: &mut FormattingState, filter: Option<&IncludeExcludeKeyFilter>, setting: IncludeExcludeSetting, ) -> bool { @@ -335,20 +405,15 @@ impl<'a> FieldFormatter<'a> { if setting == IncludeExcludeSetting::Exclude && leaf { return false; } - s.space(); - s.element(Element::Key, |s| { - s.batch(|buf| key.key_prettify(buf)); - }); - s.element(Element::Field, |s| { - s.batch(|buf| buf.extend_from_slice(self.rf.cfg.punctuation.field_key_value_separator.as_bytes())); - }); + let ffv = self.begin(s, key, value, fs); if self.rf.unescape_fields { - self.format_value(s, value, filter, setting); + self.format_value(s, value, fs, filter, setting); } else { s.element(Element::String, |s| { s.batch(|buf| buf.extend(value.raw_str().as_bytes())) }); } + self.end(fs, ffv); true } @@ -356,6 +421,7 @@ impl<'a> FieldFormatter<'a> { &mut self, s: &mut S, value: RawValue<'a>, + fs: &mut FormattingState, filter: Option<&IncludeExcludeKeyFilter>, setting: IncludeExcludeSetting, ) { @@ -384,22 +450,26 @@ impl<'a> FieldFormatter<'a> { RawValue::Object(value) => { let item = value.parse().unwrap(); s.element(Element::Object, |s| { - s.batch(|buf| buf.push(b'{')); + if !fs.flatten { + s.batch(|buf| buf.push(b'{')); + } let mut some_fields_hidden = false; for (k, v) in item.fields.iter() { - some_fields_hidden |= !self.format(s, k, *v, filter, setting); + some_fields_hidden |= !self.format(s, k, *v, fs, filter, setting); } if some_fields_hidden { s.element(Element::Ellipsis, |s| { s.batch(|buf| buf.extend(self.rf.cfg.punctuation.hidden_fields_indicator.as_bytes())) }); } - s.batch(|buf| { - if item.fields.len() != 0 { - buf.push(b' '); - } - buf.push(b'}'); - }); + if !fs.flatten { + s.batch(|buf| { + if item.fields.len() != 0 { + buf.push(b' '); + } + buf.push(b'}'); + }); + } }); } RawValue::Array(value) => { @@ -413,13 +483,59 @@ impl<'a> FieldFormatter<'a> { } else { first = false; } - self.format_value(s, *v, None, IncludeExcludeSetting::Unspecified); + self.format_value(s, *v, fs, None, IncludeExcludeSetting::Unspecified); } s.batch(|buf| buf.push(b']')); }); } }; } + + #[inline(always)] + fn begin>( + &mut self, + s: &mut S, + key: &str, + value: RawValue<'a>, + fs: &mut FormattingState, + ) -> FormattedFieldVariant { + if fs.flatten && matches!(value, RawValue::Object(_)) { + return FormattedFieldVariant::Flattened(fs.key_prefix.push(key)); + } + + let variant = FormattedFieldVariant::Normal { flatten: fs.flatten }; + + s.space(); + s.element(Element::Key, |s| { + s.batch(|buf| { + if fs.flatten { + fs.flatten = false; + if fs.key_prefix.len() != 0 { + fs.key_prefix.format(buf); + buf.push(b'.'); + } + } + key.key_prettify(buf); + }); + }); + s.element(Element::Field, |s| { + s.batch(|buf| buf.extend(self.rf.cfg.punctuation.field_key_value_separator.as_bytes())); + }); + + variant + } + + #[inline(always)] + fn end(&mut self, fs: &mut FormattingState, v: FormattedFieldVariant) { + match v { + FormattedFieldVariant::Normal { flatten } => { + fs.flatten = flatten; + } + FormattedFieldVariant::Flattened(n) => { + fs.key_prefix.pop(n); + } + } + } } // --- @@ -446,7 +562,7 @@ impl WithAutoTrim for Vec { // --- -pub trait KeyPrettify { +trait KeyPrettify { fn key_prettify>(&self, buf: &mut B); } @@ -466,6 +582,13 @@ impl KeyPrettify for str { // --- +enum FormattedFieldVariant { + Normal { flatten: bool }, + Flattened(usize), +} + +// --- + const HEXDIGIT: [u8; 16] = [ b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'a', b'b', b'c', b'd', b'e', b'f', ]; @@ -787,7 +910,6 @@ mod tests { use super::*; use crate::{ datefmt::LinuxDateFormat, - error::Error, model::{RawObject, Record, RecordFields}, settings::Punctuation, theme::Theme, @@ -799,9 +921,31 @@ mod tests { use encstr::EncodedString; use serde_json as json; - fn format(rec: &Record) -> Result { - let formatter = RecordFormatter::new( - Arc::new(Theme::from(testing::theme()?)), + trait FormatToVec { + fn format_to_vec(&self, rec: &Record) -> Vec; + } + + trait FormatToString { + fn format_to_string(&self, rec: &Record) -> String; + } + + impl FormatToVec for RecordFormatter { + fn format_to_vec(&self, rec: &Record) -> Vec { + let mut buf = Vec::new(); + self.format_record(&mut buf, rec); + buf + } + } + + impl FormatToString for RecordFormatter { + fn format_to_string(&self, rec: &Record) -> String { + String::from_utf8(self.format_to_vec(rec)).unwrap() + } + } + + fn formatter() -> RecordFormatter { + RecordFormatter::new( + Arc::new(Theme::from(testing::theme().unwrap())), DateTimeFormatter::new( LinuxDateFormat::new("%y-%m-%d %T.%3N").compile(), Tz::FixedOffset(Utc.fix()), @@ -811,10 +955,11 @@ mod tests { Formatting { punctuation: Punctuation::test_default(), }, - ); - let mut buf = Vec::new(); - formatter.format_record(&mut buf, rec); - Ok(String::from_utf8(buf)?) + ) + } + + fn format(rec: &Record) -> String { + formatter().format_to_string(rec) } fn json_raw_value(s: &str) -> Box { @@ -823,22 +968,28 @@ mod tests { #[test] fn test_nested_objects() { + let ka = json_raw_value(r#"{"va":{"kb":42}}"#); + let rec = Record { + ts: Some(Timestamp::new("2000-01-02T03:04:05.123Z")), + message: Some(RawValue::String(EncodedString::json(r#""tm""#))), + level: Some(Level::Debug), + logger: Some("tl"), + caller: Some(Caller::Text("tc")), + fields: RecordFields { + head: heapless::Vec::from_slice(&[("ka", RawValue::from(RawObject::Json(&ka)))]).unwrap(), + tail: Vec::default(), + }, + predefined: heapless::Vec::default(), + }; + + assert_eq!( + &format(&rec), + "\u{1b}[0;2;3m00-01-02 03:04:05.123 \u{1b}[0;36m|\u{1b}[0;95mDBG\u{1b}[0;36m|\u{1b}[0;2;3m \u{1b}[0;2;4mtl:\u{1b}[0;2;3m \u{1b}[0;1;39mtm \u{1b}[0;32mka\u{1b}[0;2m:\u{1b}[0;33m{ \u{1b}[0;32mva\u{1b}[0;2m:\u{1b}[0;33m{ \u{1b}[0;32mkb\u{1b}[0;2m:\u{1b}[0;94m42\u{1b}[0;33m } }\u{1b}[0;2;3m @ tc\u{1b}[0m", + ); + assert_eq!( - format(&Record { - ts: Some(Timestamp::new("2000-01-02T03:04:05.123Z")), - message: Some(RawValue::String(EncodedString::json(r#""tm""#))), - level: Some(Level::Debug), - logger: Some("tl"), - caller: Some(Caller::Text("tc")), - fields: RecordFields{ - head: heapless::Vec::from_slice(&[ - ("ka", RawValue::from(RawObject::Json(&json_raw_value(r#"{"va":{"kb":42}}"#)))), - ]).unwrap(), - tail: Vec::default(), - }, - predefined: heapless::Vec::default(), - }).unwrap(), - String::from("\u{1b}[0;2;3m00-01-02 03:04:05.123 \u{1b}[0;36m|\u{1b}[0;95mDBG\u{1b}[0;36m|\u{1b}[0;2;3m \u{1b}[0;2;4mtl:\u{1b}[0;2;3m \u{1b}[0;1;39mtm \u{1b}[0;32mka\u{1b}[0;2m:\u{1b}[0;33m{ \u{1b}[0;32mva\u{1b}[0;2m:\u{1b}[0;33m{ \u{1b}[0;32mkb\u{1b}[0;2m:\u{1b}[0;94m42\u{1b}[0;33m } }\u{1b}[0;2;3m @ tc\u{1b}[0m"), + &formatter().with_flatten(true).format_to_string(&rec), + "\u{1b}[0;2;3m00-01-02 03:04:05.123 \u{1b}[0;36m|\u{1b}[0;95mDBG\u{1b}[0;36m|\u{1b}[0;2;3m \u{1b}[0;2;4mtl:\u{1b}[0;2;3m \u{1b}[0;1;39mtm \u{1b}[0;32mka.va.kb\u{1b}[0;2m:\u{1b}[0;94m42\u{1b}[0;2;3m @ tc\u{1b}[0m", ); } } diff --git a/src/main.rs b/src/main.rs index 2e3df636..fe687465 100644 --- a/src/main.rs +++ b/src/main.rs @@ -227,6 +227,7 @@ fn run() -> Result<()> { cli::UnixTimestampUnit::Us => Some(app::UnixTimestampUnit::Microseconds), cli::UnixTimestampUnit::Ns => Some(app::UnixTimestampUnit::Nanoseconds), }, + flatten: opt.flatten != cli::FlattenOption::Never, }); // Configure input.