use rich::cells::cell_len;
use rich::measure::Measurement;
use rich::{Console, ConsoleOptions, Overflow, Renderable, Segment, Style, Text};
use crate::a11y::{AccessibleText, Status, SymbolSet};
pub const STYLES: &[(&str, &str)] = &[
("badge.ok", "bold black on green"),
("badge.warning", "bold black on yellow"),
("badge.error", "bold bright_white on red"),
("badge.info", "bold black on cyan"),
("badge.pending", "bright_white on blue"),
("badge.skipped", "black on bright_black"),
("badge.label", "black on white"),
("badge.link", "underline black on bright_blue"),
("badge.key", "bright_white on grey30"),
("badge.value", "black on bright_cyan"),
];
fn theme_style(console: &Console, key: &str) -> Style {
if let Some(style) = console.theme().get(key) {
return style.clone();
}
STYLES
.iter()
.find(|(name, _)| *name == key)
.and_then(|(_, spec)| Style::parse(spec).ok())
.or_else(|| Style::parse(key).ok())
.unwrap_or_default()
}
fn shows_color(console: &Console) -> bool {
console.color_system().is_some() && !console.no_color()
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum Kind {
Label,
Status(Status),
Link(String),
Meta(String),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Badge {
kind: Kind,
text: String,
style: Option<String>,
symbols: Option<SymbolSet>,
show_url: Option<bool>,
}
impl Badge {
fn new(kind: Kind, text: impl Into<String>) -> Self {
Badge {
kind,
text: text.into(),
style: None,
symbols: None,
show_url: None,
}
}
pub fn label(text: impl Into<String>) -> Self {
Self::new(Kind::Label, text)
}
pub fn status(status: Status, message: impl Into<String>) -> Self {
Self::new(Kind::Status(status), message)
}
pub fn link(text: impl Into<String>, url: impl Into<String>) -> Self {
Self::new(Kind::Link(url.into()), text)
}
pub fn meta(key: impl Into<String>, value: impl Into<String>) -> Self {
Self::new(Kind::Meta(key.into()), value)
}
pub fn style(mut self, style: impl Into<String>) -> Self {
self.style = Some(style.into());
self
}
pub fn symbols(mut self, symbols: SymbolSet) -> Self {
self.symbols = Some(symbols);
self
}
pub fn show_url(mut self, show: bool) -> Self {
self.show_url = Some(show);
self
}
pub fn status_of(&self) -> Option<Status> {
match self.kind {
Kind::Status(status) => Some(status),
_ => None,
}
}
pub fn plain(&self) -> String {
let set = self.symbols.unwrap_or(SymbolSet::Ascii);
let set = if set == SymbolSet::Unicode {
SymbolSet::Ascii
} else {
set
};
format!("[{}]", self.body(set, self.show_url != Some(false)))
}
fn body(&self, set: SymbolSet, url: bool) -> String {
match &self.kind {
Kind::Label => self.text.clone(),
Kind::Status(status) => {
let marker = marker(*status, set, self.text.is_empty());
if self.text.is_empty() {
marker
} else {
format!("{marker} {}", self.text)
}
}
Kind::Link(target) if url => format!("{} <{target}>", self.text),
Kind::Link(_) => self.text.clone(),
Kind::Meta(key) => format!("{key}: {}", self.text),
}
}
fn key(&self) -> String {
match &self.kind {
Kind::Label => "badge.label".into(),
Kind::Status(status) => format!("badge.{}", status.word()),
Kind::Link(_) => "badge.link".into(),
Kind::Meta(_) => "badge.value".into(),
}
}
fn resolved_style(&self, console: &Console) -> Style {
let style = theme_style(console, self.style.as_deref().unwrap_or(&self.key()));
match &self.kind {
Kind::Link(url) if console.color_system().is_some() => style.with_link(url.clone()),
_ => style,
}
}
pub fn to_text(&self, console: &Console) -> Text {
let style = self.resolved_style(console);
let links = console.color_system().is_some();
let url = self
.show_url
.unwrap_or(matches!(self.kind, Kind::Link(_)) && !links);
let default_set = if shows_color(console) {
SymbolSet::Unicode
} else {
SymbolSet::Ascii
};
let mut set = self.symbols.unwrap_or(default_set);
if set == SymbolSet::Unicode && console.ascii_only() {
set = SymbolSet::Ascii;
}
let mut text = Text::new("");
if !shows_color(console) {
text.append(&format!("[{}]", self.body(set, url)), Some(style.into()));
return text;
}
if let Kind::Meta(key) = &self.kind {
let key_style = theme_style(console, "badge.key");
text.append(&format!(" {key} "), Some(key_style.into()));
}
let body = match self.kind {
Kind::Meta(_) => self.text.clone(),
_ => self.body(set, url),
};
text.append(&format!(" {body} "), Some(style.into()));
text
}
}
fn marker(status: Status, set: SymbolSet, bare: bool) -> String {
let symbol = status.symbol(set);
match set {
SymbolSet::Unicode if bare => symbol.to_string(),
SymbolSet::Unicode => symbol.split(' ').next().unwrap_or(symbol).to_string(),
SymbolSet::Ascii => symbol.trim_matches(['[', ']']).to_string(),
SymbolSet::Words if bare => symbol.trim_end_matches(':').to_string(),
SymbolSet::Words => symbol.to_string(),
}
}
fn render_line(text: Text, options: &ConsoleOptions, console: &Console) -> Vec<Segment> {
text.no_wrap(true)
.overflow(Overflow::Ellipsis)
.rich_render(console, options)
}
impl Renderable for Badge {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
render_line(self.to_text(console), options, console)
}
fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
let width = self.to_text(console).cell_len().min(options.max_width);
Measurement::new(width, width)
}
}
impl AccessibleText for Badge {
fn accessible_text(&self, _width: usize) -> String {
self.plain()
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Badges {
badges: Vec<Badge>,
separator: String,
}
impl Badges {
pub fn new(badges: impl IntoIterator<Item = Badge>) -> Self {
Badges {
badges: badges.into_iter().collect(),
separator: " ".into(),
}
}
pub fn push(mut self, badge: Badge) -> Self {
self.badges.push(badge);
self
}
pub fn separator(mut self, separator: impl Into<String>) -> Self {
self.separator = separator.into();
self
}
pub fn badges(&self) -> &[Badge] {
&self.badges
}
pub fn plain(&self) -> String {
self.badges
.iter()
.map(Badge::plain)
.collect::<Vec<_>>()
.join(&self.separator)
}
pub fn to_text(&self, console: &Console) -> Text {
let mut text = Text::new("");
for (index, badge) in self.badges.iter().enumerate() {
if index > 0 {
text.append(&self.separator, None);
}
text = text.append_text(&badge.to_text(console));
}
text
}
}
impl Renderable for Badges {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
let width = options.max_width;
let separator = cell_len(&self.separator);
let mut lines: Vec<Text> = Vec::new();
let mut line = Text::new("");
for badge in &self.badges {
let chip = badge.to_text(console);
let used = line.cell_len();
if used > 0 && used + separator + chip.cell_len() > width {
lines.push(std::mem::replace(&mut line, Text::new("")));
}
if line.cell_len() > 0 {
line.append(&self.separator, None);
}
line = line.append_text(&chip);
}
lines.push(line);
let mut out = Vec::new();
for (index, line) in lines.into_iter().enumerate() {
if index > 0 {
out.push(Segment::line());
}
out.extend(render_line(line, options, console));
}
out
}
fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
let widths: Vec<usize> = self
.badges
.iter()
.map(|b| b.to_text(console).cell_len())
.collect();
let minimum = widths.iter().copied().max().unwrap_or(0);
let maximum = widths.iter().sum::<usize>()
+ cell_len(&self.separator) * widths.len().saturating_sub(1);
Measurement::new(
minimum.min(options.max_width),
maximum.min(options.max_width),
)
}
}
impl AccessibleText for Badges {
fn accessible_text(&self, _width: usize) -> String {
self.plain()
}
}