oma_console/
print.rs

1use std::{borrow::Cow, collections::BTreeMap, time::Duration};
2
3use console::{Color, StyledObject, style};
4use termbg::Theme;
5use tracing::{Level, debug, field::Field};
6use tracing_subscriber::Layer;
7
8pub use termbg;
9
10use crate::writer::{Writeln, Writer};
11
12#[derive(Clone)]
13enum StyleFollow {
14    OmaTheme,
15    TermTheme,
16}
17
18pub enum Action {
19    Emphasis,
20    Foreground,
21    Secondary,
22    EmphasisSecondary,
23    WARN,
24    Purple,
25    Note,
26    UpgradeTips,
27    PendingBg,
28}
29
30impl Action {
31    fn dark(&self) -> u8 {
32        match self {
33            Action::Emphasis => 148,
34            Action::Foreground => 72,
35            Action::Secondary => 182,
36            Action::EmphasisSecondary => 114,
37            Action::WARN => 214,
38            Action::Purple => 141,
39            Action::Note => 178,
40            Action::UpgradeTips => 87,
41            Action::PendingBg => 25,
42        }
43    }
44
45    fn light(&self) -> u8 {
46        match self {
47            Action::Emphasis => 142,
48            Action::Foreground => 72,
49            Action::Secondary => 167,
50            Action::EmphasisSecondary => 106,
51            Action::WARN => 208,
52            Action::Purple => 141,
53            Action::Note => 172,
54            Action::UpgradeTips => 63,
55            Action::PendingBg => 189,
56        }
57    }
58}
59/// OmaColorFormat
60///
61/// `OmaColorFormat` is a structure that defines the color format and theme settings for oma.
62pub struct OmaColorFormat {
63    /// A `StyleFollow` enum that indicates whether to follow the terminal theme or use the oma-defined theme.
64    follow: StyleFollow,
65    /// An optional `Theme` object that defined by oma.
66    pub theme: Option<Theme>,
67}
68
69impl OmaColorFormat {
70    pub fn new(follow: bool, duration: Duration) -> Self {
71        Self {
72            follow: if follow {
73                StyleFollow::TermTheme
74            } else {
75                StyleFollow::OmaTheme
76            },
77            theme: if !follow {
78                termbg::theme(duration)
79                    .map_err(|e| {
80                        debug!(
81                            "Failed to apply oma color schemes, falling back to default terminal colors: {e:?}."
82                        );
83                        e
84                    })
85                    .ok()
86            } else {
87                None
88            },
89        }
90    }
91    /// Convert input into StyledObject
92    ///
93    /// This function applies a color scheme to the given input string based on the specified action and the current terminal color schemes.
94    ///
95    /// # Arguments
96    ///
97    /// * `input` - The input data to be themed.
98    /// * `color` - An `Action` enum value that specifies the color to be applied.
99    ///
100    /// # Returns
101    ///
102    /// Returns a `StyledObject` that contains the styled input data.
103    pub fn color_str<D>(&self, input: D, color: Action) -> StyledObject<D> {
104        match self.follow {
105            StyleFollow::OmaTheme => match self.theme {
106                Some(Theme::Dark) => match color {
107                    x @ Action::PendingBg => style(input).bg(Color::Color256(x.dark())).bold(),
108                    x => style(input).color256(x.dark()),
109                },
110                Some(Theme::Light) => match color {
111                    x @ Action::PendingBg => style(input).bg(Color::Color256(x.light())).bold(),
112                    x => style(input).color256(x.light()),
113                },
114                None => term_color(input, color),
115            },
116            StyleFollow::TermTheme => term_color(input, color),
117        }
118    }
119}
120
121fn term_color<D>(input: D, color: Action) -> StyledObject<D> {
122    match color {
123        Action::Emphasis => style(input).green(),
124        Action::Secondary => style(input).dim(),
125        Action::EmphasisSecondary => style(input).cyan(),
126        Action::WARN => style(input).yellow().bold(),
127        Action::Purple => style(input).magenta(),
128        Action::Note => style(input).yellow(),
129        Action::Foreground => style(input).cyan().bold(),
130        Action::UpgradeTips => style(input).blue().bold(),
131        Action::PendingBg => style(input).bg(Color::Blue).bold(),
132    }
133}
134/// OmaLayer
135/// `OmaLayer` is used for outputting oma-style logs to `tracing`
136///
137/// # Example:
138/// ```
139/// use tracing_subscriber::prelude::*;
140/// use oma_console::OmaLayer;
141/// use tracing::info;
142///
143/// tracing_subscriber::registry()
144///     .with(OmaLayer::new())
145///     .init();
146///
147/// info!("My name is oma!");
148/// ```
149///
150pub struct OmaLayer {
151    /// Display result with ansi
152    with_ansi: bool,
153    /// A Terminal writer to print oma-style message
154    writer: Writer,
155}
156
157impl Default for OmaLayer {
158    fn default() -> Self {
159        Self {
160            with_ansi: true,
161            writer: Writer::default(),
162        }
163    }
164}
165
166impl OmaLayer {
167    pub fn new() -> Self {
168        OmaLayer::default()
169    }
170
171    /// Display with ANSI colors
172    ///
173    /// Set to false to disable ANSI color sequences.
174    pub fn with_ansi(mut self, with_ansi: bool) -> Self {
175        self.with_ansi = with_ansi;
176        self
177    }
178}
179
180impl<S> Layer<S> for OmaLayer
181where
182    S: tracing::Subscriber,
183    S: for<'lookup> tracing_subscriber::registry::LookupSpan<'lookup>,
184{
185    fn on_event(
186        &self,
187        event: &tracing::Event<'_>,
188        _ctx: tracing_subscriber::layer::Context<'_, S>,
189    ) {
190        let level = *event.metadata().level();
191
192        let prefix = if self.with_ansi {
193            Cow::Owned(match level {
194                Level::DEBUG => console::style("DEBUG").dim().to_string(),
195                Level::INFO => console::style("INFO").blue().bold().to_string(),
196                Level::WARN => console::style("WARNING").yellow().bold().to_string(),
197                Level::ERROR => console::style("ERROR").red().bold().to_string(),
198                Level::TRACE => console::style("TRACE").dim().to_string(),
199            })
200        } else {
201            Cow::Borrowed(match level {
202                Level::DEBUG => "DEBUG",
203                Level::INFO => "INFO",
204                Level::WARN => "WARNING",
205                Level::ERROR => "ERROR",
206                Level::TRACE => "TRACE",
207            })
208        };
209
210        let mut visitor = OmaRecorder(BTreeMap::new());
211        event.record(&mut visitor);
212
213        for (k, v) in visitor.0 {
214            if k == "message" {
215                if self.with_ansi {
216                    self.writer.writeln(&prefix, &v).ok();
217                } else {
218                    self.writer
219                        .writeln(&prefix, &console::strip_ansi_codes(&v))
220                        .ok();
221                }
222            }
223        }
224    }
225}
226/// OmaRecorder
227/// `OmaRecorder` is used for recording oma-style logs.
228///
229/// # Example:
230/// ```ignore
231/// let mut visitor = OmaRecorder(BTreeMap::new());
232/// event.record(&mut visitor);
233/// for (k, v) in visitor.0 {
234///     if k == "message" {
235///         self.writer.writeln(&prefix, &v).ok();
236///     }
237/// }
238/// ```
239struct OmaRecorder<'a>(BTreeMap<&'a str, String>);
240
241impl tracing::field::Visit for OmaRecorder<'_> {
242    fn record_f64(&mut self, field: &Field, value: f64) {
243        self.0.insert(field.name(), value.to_string());
244    }
245
246    fn record_i64(&mut self, field: &Field, value: i64) {
247        self.0.insert(field.name(), value.to_string());
248    }
249
250    fn record_u64(&mut self, field: &Field, value: u64) {
251        self.0.insert(field.name(), value.to_string());
252    }
253
254    fn record_bool(&mut self, field: &Field, value: bool) {
255        self.0.insert(field.name(), value.to_string());
256    }
257
258    fn record_str(&mut self, field: &Field, value: &str) {
259        self.0.insert(field.name(), value.to_string());
260    }
261
262    fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static)) {
263        self.0.insert(field.name(), format!("{value:#?}"));
264    }
265
266    fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
267        self.0.insert(field.name(), format!("{value:#?}"));
268    }
269}