use crate::config;
use serde::{Deserialize, Serialize};
use std::{borrow::Cow, str::FromStr};
use tui::{
style::{Color, Modifier, Style},
text::Span,
};
#[derive(Debug, Clone)]
pub struct Styles {
palette: Palette,
toggles: config::ColorToggles,
pub(crate) utf8: bool,
}
#[derive(Debug, PartialEq, Eq, Copy, Clone, Deserialize, Serialize)]
#[repr(u8)]
pub enum Palette {
#[serde(rename = "off")]
NoColors,
#[serde(rename = "8")]
Ansi8,
#[serde(rename = "16")]
Ansi16,
#[serde(rename = "256")]
Ansi256,
#[serde(rename = "all")]
All,
}
fn fg_style(color: Color) -> Style {
Style::default().fg(color)
}
impl Styles {
pub fn from_config(config: config::ViewOptions) -> Self {
Self {
palette: config.determine_palette(),
toggles: config.toggles(),
utf8: config.is_utf8(),
}
}
pub fn error_init(&self, cfg: &crate::config::Config) -> color_eyre::Result<()> {
use color_eyre::{
config::{HookBuilder, Theme},
ErrorKind,
};
let mut builder = HookBuilder::new()
.issue_url(concat!(env!("CARGO_PKG_REPOSITORY"), "/issues/new"))
.issue_filter(|kind| match kind {
ErrorKind::NonRecoverable(_) => true,
ErrorKind::Recoverable(_) => false,
})
.add_default_filters()
.add_issue_metadata("version", env!("CARGO_PKG_VERSION"));
builder = cfg.add_issue_metadata(builder);
if self.palette == Palette::NoColors {
builder = builder.theme(Theme::new());
}
let (panic_hook, error_hook) = builder.into_hooks();
std::panic::set_hook(Box::new(move |panic_info| {
if let Some(location) = panic_info.location() {
tracing::error!(
message = %panic_info,
panic.file = location.file(),
panic.line = location.line(),
panic.column = location.column(),
);
} else {
tracing::error!(message = %panic_info);
}
let _ = crate::term::exit_crossterm();
eprintln!("{}", panic_hook.panic_report(panic_info));
}));
error_hook.install()?;
Ok(())
}
pub fn if_utf8<'a>(&self, utf8: &'a str, ascii: &'a str) -> &'a str {
if self.utf8 {
utf8
} else {
ascii
}
}
pub fn time_units<'a>(&self, text: impl Into<Cow<'a, str>>) -> Span<'a> {
let mut text = text.into();
if !self.toggles.color_durations() {
return Span::raw(text);
}
if !self.utf8 {
if let Some(mu_offset) = text.find("µs") {
text.to_mut().replace_range(mu_offset.., "us");
}
}
let style = match self.palette {
Palette::NoColors => return Span::raw(text),
Palette::Ansi8 | Palette::Ansi16 => match text.as_ref() {
s if s.ends_with("ps") => fg_style(Color::Blue),
s if s.ends_with("ns") => fg_style(Color::Green),
s if s.ends_with("µs") || s.ends_with("us") => fg_style(Color::Yellow),
s if s.ends_with("ms") => fg_style(Color::Red),
s if s.ends_with('s') => fg_style(Color::Magenta),
_ => Style::default(),
},
Palette::Ansi256 | Palette::All => match text.as_ref() {
s if s.ends_with("ps") => fg_style(Color::Indexed(40)), s if s.ends_with("ns") => fg_style(Color::Indexed(41)), s if s.ends_with("µs") || s.ends_with("us") => fg_style(Color::Indexed(42)), s if s.ends_with("ms") => fg_style(Color::Indexed(43)), s if s.ends_with('s') => fg_style(Color::Indexed(44)), _ => Style::default(),
},
};
Span::styled(text, style)
}
pub fn terminated(&self) -> Style {
if !self.toggles.color_terminated() {
return Style::default();
}
Style::default().add_modifier(Modifier::DIM)
}
pub fn fg(&self, color: Color) -> Style {
if let Some(color) = self.color(color) {
Style::default().fg(color)
} else {
Style::default()
}
}
pub fn warning_wide(&self) -> Span<'static> {
Span::styled(
self.if_utf8("\u{26A0} ", "/!\\ "),
self.fg(Color::LightYellow).add_modifier(Modifier::BOLD),
)
}
pub fn warning_narrow(&self) -> Span<'static> {
Span::styled(
self.if_utf8("\u{26A0} ", "! "),
self.fg(Color::LightYellow).add_modifier(Modifier::BOLD),
)
}
pub fn selected(&self, value: &str) -> Span<'static> {
let style = if let Some(cyan) = self.color(Color::Cyan) {
Style::default().fg(cyan)
} else {
Style::default().remove_modifier(Modifier::REVERSED)
};
Span::styled(value.to_string(), style)
}
pub fn ascending(&self, value: &str) -> Span<'static> {
let value = format!("{}{}", value, self.if_utf8("▵", "+"));
self.selected(&value)
}
pub fn descending(&self, value: &str) -> Span<'static> {
let value = format!("{}{}", value, self.if_utf8("▿", "-"));
self.selected(&value)
}
pub fn color(&self, color: Color) -> Option<Color> {
use Palette::*;
match (self.palette, color) {
(NoColors, _) => None,
(All, color) => Some(color),
(Ansi256, Color::Rgb(_, _, _)) => None,
(Ansi256, color) => Some(color),
(_, Color::Rgb(_, _, _)) | (Ansi16, Color::Indexed(_)) => None,
(Ansi16, color) => Some(color),
(Ansi8, Color::LightRed) => Some(Color::Red),
(Ansi8, Color::LightGreen) => Some(Color::Green),
(Ansi8, Color::LightYellow) => Some(Color::Yellow),
(Ansi8, Color::LightBlue) => Some(Color::Blue),
(Ansi8, Color::LightMagenta) => Some(Color::Magenta),
(Ansi8, Color::Cyan) => Some(Color::Cyan),
(_, _) => Some(color),
}
}
pub fn border_block(&self) -> tui::widgets::Block<'_> {
if self.utf8 {
tui::widgets::Block::default()
.borders(tui::widgets::Borders::ALL)
.border_type(tui::widgets::BorderType::Rounded)
} else {
Default::default()
}
}
}
impl FromStr for Palette {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim() {
"0" => Ok(Palette::NoColors),
"8" => Ok(Palette::Ansi8),
"16" => Ok(Palette::Ansi16),
"256" => Ok(Palette::Ansi256),
s if s.eq_ignore_ascii_case("all") => Ok(Palette::All),
s if s.eq_ignore_ascii_case("off") => Ok(Palette::NoColors),
_ => Err("invalid color palette"),
}
}
}
impl Default for Palette {
fn default() -> Self {
Self::NoColors
}
}