use crate::color::{blend_rgb, Color, ColorSystem};
use crate::error::StyleError;
use crate::terminal_theme::TerminalTheme;
use std::fmt;
use std::fmt::Write as _;
use std::ops::Add;
const BOLD: u16 = 1 << 0;
const DIM: u16 = 1 << 1;
const ITALIC: u16 = 1 << 2;
const UNDERLINE: u16 = 1 << 3;
const BLINK: u16 = 1 << 4;
const BLINK2: u16 = 1 << 5;
const REVERSE: u16 = 1 << 6;
const CONCEAL: u16 = 1 << 7;
const STRIKE: u16 = 1 << 8;
const UNDERLINE2: u16 = 1 << 9;
const FRAME: u16 = 1 << 10;
const ENCIRCLE: u16 = 1 << 11;
const OVERLINE: u16 = 1 << 12;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum UnderlineStyle {
Single,
Double,
Curly,
Dotted,
Dashed,
}
#[derive(Clone, Debug)]
pub struct Style {
color: Option<Color>,
bgcolor: Option<Color>,
set_attributes: u16,
attributes: u16,
link: Option<String>,
underline_color: Option<Color>,
underline_style: Option<UnderlineStyle>,
}
impl Style {
#[allow(clippy::too_many_arguments)]
pub fn new(
color: Option<&str>,
bgcolor: Option<&str>,
bold: Option<bool>,
dim: Option<bool>,
italic: Option<bool>,
underline: Option<bool>,
blink: Option<bool>,
blink2: Option<bool>,
reverse: Option<bool>,
conceal: Option<bool>,
strike: Option<bool>,
underline2: Option<bool>,
frame: Option<bool>,
encircle: Option<bool>,
overline: Option<bool>,
link: Option<&str>,
) -> Result<Self, StyleError> {
let mut style = Style {
color: None,
bgcolor: None,
set_attributes: 0,
attributes: 0,
link: None,
underline_color: None,
underline_style: None,
};
if let Some(c) = color {
style.color = Some(
Color::parse(c)
.map_err(|e| StyleError::InvalidSyntax(format!("invalid color: {}", e)))?,
);
}
if let Some(bg) = bgcolor {
style.bgcolor = Some(
Color::parse(bg)
.map_err(|e| StyleError::InvalidSyntax(format!("invalid bgcolor: {}", e)))?,
);
}
style.set_attribute(BOLD, bold);
style.set_attribute(DIM, dim);
style.set_attribute(ITALIC, italic);
style.set_attribute(UNDERLINE, underline);
style.set_attribute(BLINK, blink);
style.set_attribute(BLINK2, blink2);
style.set_attribute(REVERSE, reverse);
style.set_attribute(CONCEAL, conceal);
style.set_attribute(STRIKE, strike);
style.set_attribute(UNDERLINE2, underline2);
style.set_attribute(FRAME, frame);
style.set_attribute(ENCIRCLE, encircle);
style.set_attribute(OVERLINE, overline);
if let Some(l) = link {
style.link = Some(l.to_string());
}
Ok(style)
}
pub fn null() -> Self {
Style {
color: None,
bgcolor: None,
set_attributes: 0,
attributes: 0,
link: None,
underline_color: None,
underline_style: None,
}
}
pub fn from_color(color: Option<Color>, bgcolor: Option<Color>) -> Self {
Style {
color,
bgcolor,
set_attributes: 0,
attributes: 0,
link: None,
underline_color: None,
underline_style: None,
}
}
pub fn parse(definition: &str) -> Self {
Self::parse_strict(definition).unwrap_or_else(|_| Style::null())
}
pub fn parse_strict(definition: &str) -> Result<Self, StyleError> {
let key = definition.to_lowercase();
let mut cache = get_style_cache();
if let Some(ref mut c) = *cache {
if let Some(style) = c.get(key.as_str()) {
return Ok(style.clone());
}
}
let style = Self::parse_internal(definition)?;
if let Some(ref mut c) = *cache {
c.put(key, style.clone());
}
Ok(style)
}
fn parse_internal(definition: &str) -> Result<Self, StyleError> {
let definition = definition.trim();
if definition.is_empty() || definition.eq_ignore_ascii_case("none") {
return Ok(Style::null());
}
let mut style = Style::null();
let words: Vec<&str> = definition.split_whitespace().collect();
let mut i = 0;
while i < words.len() {
let word = words[i].to_lowercase();
match word.as_str() {
"on" => {
i += 1;
if i >= words.len() {
return Err(StyleError::InvalidSyntax(
"expected color after 'on'".to_string(),
));
}
let bgcolor_str = words[i];
style.bgcolor = Some(Color::parse(bgcolor_str).map_err(|e| {
StyleError::InvalidSyntax(format!("invalid background color: {}", e))
})?);
}
"not" => {
i += 1;
if i >= words.len() {
return Err(StyleError::InvalidSyntax(
"expected attribute after 'not'".to_string(),
));
}
let attr = words[i].to_lowercase();
if let Some(bit) = parse_attribute_name(&attr) {
style.set_attribute(bit, Some(false));
} else {
return Err(StyleError::UnknownAttribute(attr));
}
}
"link" => {
i += 1;
if i >= words.len() {
return Err(StyleError::InvalidSyntax(
"expected URL after 'link'".to_string(),
));
}
style.link = Some(words[i].to_string());
}
_ => {
if word.starts_with("link=") {
let url = &words[i]["link=".len()..];
if url.is_empty() {
return Err(StyleError::InvalidSyntax(
"expected URL after 'link='".to_string(),
));
}
style.link = Some(url.to_string());
} else if let Some(ul_style) = parse_underline_style_name(&word) {
style.underline_style = Some(ul_style);
} else if word.starts_with("underline_color(") && word.ends_with(')') {
let inner = &word["underline_color(".len()..word.len() - 1];
let color = Color::parse(inner).map_err(|e| {
StyleError::InvalidSyntax(format!("invalid underline_color: {}", e))
})?;
style.underline_color = Some(color);
} else if let Some(bit) = parse_attribute_name(&word) {
style.set_attribute(bit, Some(true));
} else {
match Color::parse(&word) {
Ok(color) => style.color = Some(color),
Err(e) => {
return Err(StyleError::InvalidSyntax(format!(
"unknown attribute or color '{}': {}",
word, e
)))
}
}
}
}
}
i += 1;
}
Ok(style)
}
fn set_attribute(&mut self, bit: u16, value: Option<bool>) {
if let Some(val) = value {
self.set_attributes |= bit;
if val {
self.attributes |= bit;
} else {
self.attributes &= !bit;
}
}
}
fn get_attribute(&self, bit: u16) -> Option<bool> {
if self.set_attributes & bit != 0 {
Some(self.attributes & bit != 0)
} else {
None
}
}
pub fn bold(&self) -> Option<bool> {
self.get_attribute(BOLD)
}
pub fn dim(&self) -> Option<bool> {
self.get_attribute(DIM)
}
pub fn italic(&self) -> Option<bool> {
self.get_attribute(ITALIC)
}
pub fn underline(&self) -> Option<bool> {
self.get_attribute(UNDERLINE)
}
pub fn blink(&self) -> Option<bool> {
self.get_attribute(BLINK)
}
pub fn blink2(&self) -> Option<bool> {
self.get_attribute(BLINK2)
}
pub fn reverse(&self) -> Option<bool> {
self.get_attribute(REVERSE)
}
pub fn conceal(&self) -> Option<bool> {
self.get_attribute(CONCEAL)
}
pub fn strike(&self) -> Option<bool> {
self.get_attribute(STRIKE)
}
pub fn underline2(&self) -> Option<bool> {
self.get_attribute(UNDERLINE2)
}
pub fn frame(&self) -> Option<bool> {
self.get_attribute(FRAME)
}
pub fn encircle(&self) -> Option<bool> {
self.get_attribute(ENCIRCLE)
}
pub fn overline(&self) -> Option<bool> {
self.get_attribute(OVERLINE)
}
pub fn color(&self) -> Option<&Color> {
self.color.as_ref()
}
pub fn bgcolor(&self) -> Option<&Color> {
self.bgcolor.as_ref()
}
pub fn link(&self) -> Option<&str> {
self.link.as_deref()
}
pub fn underline_color(&self) -> Option<&Color> {
self.underline_color.as_ref()
}
pub fn underline_style(&self) -> Option<UnderlineStyle> {
self.underline_style
}
pub fn set_bold(&mut self, value: Option<bool>) {
self.set_attribute(BOLD, value);
}
pub fn set_dim(&mut self, value: Option<bool>) {
self.set_attribute(DIM, value);
}
pub fn set_italic(&mut self, value: Option<bool>) {
self.set_attribute(ITALIC, value);
}
pub fn set_underline(&mut self, value: Option<bool>) {
self.set_attribute(UNDERLINE, value);
}
pub fn set_blink(&mut self, value: Option<bool>) {
self.set_attribute(BLINK, value);
}
pub fn set_reverse(&mut self, value: Option<bool>) {
self.set_attribute(REVERSE, value);
}
pub fn set_conceal(&mut self, value: Option<bool>) {
self.set_attribute(CONCEAL, value);
}
pub fn set_strike(&mut self, value: Option<bool>) {
self.set_attribute(STRIKE, value);
}
pub fn set_underline_color(&mut self, color: Option<Color>) {
self.underline_color = color;
}
pub fn set_underline_style(&mut self, style: Option<UnderlineStyle>) {
self.underline_style = style;
}
pub fn combine(styles: &[Style]) -> Style {
styles
.iter()
.fold(Style::null(), |acc, style| acc + style.clone())
}
pub fn chain(styles: &[Style]) -> Style {
Self::combine(styles)
}
pub fn normalize(&self) -> String {
self.to_string()
}
pub fn combine_refs<'a, I>(styles: I) -> Style
where
I: IntoIterator<Item = &'a Style>,
{
let mut acc = Style::null();
for style in styles {
acc = Style {
color: style.color.or(acc.color),
bgcolor: style.bgcolor.or(acc.bgcolor),
set_attributes: acc.set_attributes | style.set_attributes,
attributes: (acc.attributes & !style.set_attributes)
| (style.attributes & style.set_attributes),
link: style.link.clone().or(acc.link),
underline_color: style.underline_color.or(acc.underline_color),
underline_style: style.underline_style.or(acc.underline_style),
};
}
acc
}
pub fn render_no_link(&self, text: &str, color_system: Option<ColorSystem>) -> String {
self.render_inner(text, color_system, false)
}
pub fn render(&self, text: &str, color_system: Option<ColorSystem>) -> String {
self.render_inner(text, color_system, true)
}
fn render_inner(
&self,
text: &str,
color_system: Option<ColorSystem>,
emit_link: bool,
) -> String {
if text.is_empty() || color_system.is_none() {
return text.to_string();
}
let mut sgr = String::with_capacity(32);
let attrs: [(u16, &str); 13] = [
(BOLD, "1"),
(DIM, "2"),
(ITALIC, "3"),
(UNDERLINE, "4"),
(BLINK, "5"),
(BLINK2, "6"),
(REVERSE, "7"),
(CONCEAL, "8"),
(STRIKE, "9"),
(UNDERLINE2, "21"),
(FRAME, "51"),
(ENCIRCLE, "52"),
(OVERLINE, "53"),
];
for (bit, code) in &attrs {
if self.attributes & bit != 0 && self.set_attributes & bit != 0 {
if !sgr.is_empty() {
sgr.push(';');
}
sgr.push_str(code);
}
}
if let Some(ul_style) = &self.underline_style {
if !sgr.is_empty() {
sgr.push(';');
}
sgr.push_str(match ul_style {
UnderlineStyle::Single => "4:1",
UnderlineStyle::Double => "4:2",
UnderlineStyle::Curly => "4:3",
UnderlineStyle::Dotted => "4:4",
UnderlineStyle::Dashed => "4:5",
});
}
if let Some(color) = &self.color {
color.write_ansi_codes(true, &mut sgr);
}
if let Some(bgcolor) = &self.bgcolor {
bgcolor.write_ansi_codes(false, &mut sgr);
}
if let Some(ul_color) = &self.underline_color {
ul_color.write_underline_color_codes(&mut sgr);
}
let mut result = String::with_capacity(text.len() + sgr.len() + 12);
if sgr.is_empty() {
result.push_str(text);
} else {
write!(result, "\x1b[{}m{}\x1b[0m", sgr, text).unwrap();
}
if emit_link {
if let Some(url) = &self.link {
let id = link_id_for(url);
let mut linked = String::with_capacity(result.len() + url.len() + 32);
write!(
linked,
"\x1b]8;id={};{}\x1b\\{}\x1b]8;;\x1b\\",
id, url, result
)
.unwrap();
return linked;
}
}
result
}
pub fn pick_first(candidates: &[Option<&Style>]) -> Style {
for s in candidates.iter().flatten() {
if !s.is_null() {
return (*s).clone();
}
}
Style::null()
}
pub fn is_null(&self) -> bool {
self.color.is_none()
&& self.bgcolor.is_none()
&& self.set_attributes == 0
&& self.link.is_none()
&& self.underline_color.is_none()
&& self.underline_style.is_none()
}
#[must_use]
pub fn fg(mut self, color: Color) -> Self {
self.color = Some(color);
self
}
#[must_use]
pub fn bg(mut self, color: Color) -> Self {
self.bgcolor = Some(color);
self
}
#[must_use]
pub fn with_underline_color(mut self, color: Color) -> Self {
self.underline_color = Some(color);
self
}
pub fn without_color(&self) -> Style {
Style {
color: None,
bgcolor: None,
set_attributes: self.set_attributes,
attributes: self.attributes,
link: self.link.clone(),
underline_color: self.underline_color,
underline_style: self.underline_style,
}
}
pub fn background_style(&self) -> Style {
Style {
color: None,
bgcolor: self.bgcolor,
set_attributes: 0,
attributes: 0,
link: None,
underline_color: None,
underline_style: None,
}
}
pub fn clear_meta_and_links(&self) -> Style {
Style {
color: self.color,
bgcolor: self.bgcolor,
set_attributes: self.set_attributes,
attributes: self.attributes,
link: None,
underline_color: self.underline_color,
underline_style: self.underline_style,
}
}
pub fn with_link(url: &str) -> Style {
Style {
color: None,
bgcolor: None,
set_attributes: 0,
attributes: 0,
link: Some(url.to_string()),
underline_color: None,
underline_style: None,
}
}
pub fn update_link(&self, link: Option<&str>) -> Style {
Style {
color: self.color,
bgcolor: self.bgcolor,
set_attributes: self.set_attributes,
attributes: self.attributes,
link: link.map(|s| s.to_string()),
underline_color: self.underline_color,
underline_style: self.underline_style,
}
}
pub fn get_html_style(&self, theme: Option<&TerminalTheme>) -> String {
let mut css = String::new();
let mut fg_color = self.color.as_ref();
let mut bg_color = self.bgcolor.as_ref();
if self.reverse() == Some(true) {
std::mem::swap(&mut fg_color, &mut bg_color);
}
let mut fg_triplet = fg_color.map(|c| c.get_truecolor(theme, true));
let bg_triplet = bg_color.map(|c| c.get_truecolor(theme, false));
if self.dim() == Some(true) {
if let (Some(fg), Some(bg)) = (fg_triplet, bg_triplet) {
fg_triplet = Some(blend_rgb(fg, bg, 0.5));
}
}
if let Some(triplet) = fg_triplet {
let hex = triplet.hex();
write!(css, "color: {}; text-decoration-color: {}", hex, hex).unwrap();
}
if let Some(triplet) = bg_triplet {
if !css.is_empty() {
css.push_str("; ");
}
write!(css, "background-color: {}", triplet.hex()).unwrap();
}
if self.bold() == Some(true) {
if !css.is_empty() {
css.push_str("; ");
}
css.push_str("font-weight: bold");
}
if self.italic() == Some(true) {
if !css.is_empty() {
css.push_str("; ");
}
css.push_str("font-style: italic");
}
let has_underline = self.underline() == Some(true);
let has_strike = self.strike() == Some(true);
let has_overline = self.overline() == Some(true);
if has_underline || has_strike || has_overline {
if !css.is_empty() {
css.push_str("; ");
}
css.push_str("text-decoration: ");
let mut first = true;
if has_underline {
css.push_str("underline");
first = false;
}
if has_strike {
if !first {
css.push(' ');
}
css.push_str("line-through");
first = false;
}
if has_overline {
if !first {
css.push(' ');
}
css.push_str("overline");
}
}
css
}
}
impl fmt::Display for Style {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut parts = Vec::new();
let attrs = [
(BOLD, "bold", "not bold"),
(DIM, "dim", "not dim"),
(ITALIC, "italic", "not italic"),
(UNDERLINE, "underline", "not underline"),
(BLINK, "blink", "not blink"),
(BLINK2, "blink2", "not blink2"),
(REVERSE, "reverse", "not reverse"),
(CONCEAL, "conceal", "not conceal"),
(STRIKE, "strike", "not strike"),
(UNDERLINE2, "underline2", "not underline2"),
(FRAME, "frame", "not frame"),
(ENCIRCLE, "encircle", "not encircle"),
(OVERLINE, "overline", "not overline"),
];
for (bit, on_name, off_name) in &attrs {
if self.set_attributes & bit != 0 {
if self.attributes & bit != 0 {
parts.push(on_name.to_string());
} else {
parts.push(off_name.to_string());
}
}
}
if let Some(color) = &self.color {
parts.push(color.name().into_owned());
}
if let Some(bgcolor) = &self.bgcolor {
parts.push("on".to_string());
parts.push(bgcolor.name().into_owned());
}
if let Some(ul_style) = &self.underline_style {
parts.push(
match ul_style {
UnderlineStyle::Single => "single",
UnderlineStyle::Double => "double",
UnderlineStyle::Curly => "curly",
UnderlineStyle::Dotted => "dotted",
UnderlineStyle::Dashed => "dashed",
}
.to_string(),
);
}
if let Some(ul_color) = &self.underline_color {
parts.push(format!("underline_color({})", ul_color.name()));
}
if let Some(link) = &self.link {
parts.push("link".to_string());
parts.push(link.clone());
}
if parts.is_empty() {
write!(f, "none")
} else {
write!(f, "{}", parts.join(" "))
}
}
}
impl PartialEq for Style {
fn eq(&self, other: &Self) -> bool {
self.color == other.color
&& self.bgcolor == other.bgcolor
&& self.set_attributes == other.set_attributes
&& self.attributes == other.attributes
&& self.link == other.link
&& self.underline_color == other.underline_color
&& self.underline_style == other.underline_style
}
}
impl std::hash::Hash for Style {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.color.hash(state);
self.bgcolor.hash(state);
self.set_attributes.hash(state);
self.attributes.hash(state);
self.link.hash(state);
self.underline_color.hash(state);
self.underline_style.hash(state);
}
}
impl Eq for Style {}
impl Add<Style> for Style {
type Output = Style;
fn add(self, rhs: Style) -> Style {
Style {
color: rhs.color.or(self.color),
bgcolor: rhs.bgcolor.or(self.bgcolor),
set_attributes: self.set_attributes | rhs.set_attributes,
attributes: (self.attributes & !rhs.set_attributes)
| (rhs.attributes & rhs.set_attributes),
link: rhs.link.or(self.link),
underline_color: rhs.underline_color.or(self.underline_color),
underline_style: rhs.underline_style.or(self.underline_style),
}
}
}
impl Add<Option<Style>> for Style {
type Output = Style;
fn add(self, rhs: Option<Style>) -> Style {
match rhs {
Some(style) => self + style,
None => self,
}
}
}
fn parse_underline_style_name(name: &str) -> Option<UnderlineStyle> {
match name {
"single" => Some(UnderlineStyle::Single),
"double" => Some(UnderlineStyle::Double),
"curly" => Some(UnderlineStyle::Curly),
"dotted" => Some(UnderlineStyle::Dotted),
"dashed" => Some(UnderlineStyle::Dashed),
_ => None,
}
}
fn parse_attribute_name(name: &str) -> Option<u16> {
match name {
"bold" | "b" => Some(BOLD),
"dim" | "d" => Some(DIM),
"italic" | "i" => Some(ITALIC),
"underline" | "u" => Some(UNDERLINE),
"blink" => Some(BLINK),
"blink2" => Some(BLINK2),
"reverse" | "r" => Some(REVERSE),
"conceal" | "c" => Some(CONCEAL),
"strike" | "s" => Some(STRIKE),
"underline2" | "uu" => Some(UNDERLINE2),
"frame" => Some(FRAME),
"encircle" => Some(ENCIRCLE),
"overline" | "o" => Some(OVERLINE),
_ => None,
}
}
#[derive(Debug, Clone)]
pub struct StyleStack {
stack: Vec<Style>,
}
impl StyleStack {
pub fn new(default: Style) -> Self {
StyleStack {
stack: vec![default],
}
}
pub fn current(&self) -> &Style {
self.stack.last().expect("StyleStack should never be empty")
}
pub fn push(&mut self, style: Style) {
let new_style = self.current().clone() + style;
self.stack.push(new_style);
}
pub fn pop(&mut self) -> Result<&Style, StyleError> {
if self.stack.len() <= 1 {
return Err(StyleError::StackError(
"cannot pop from stack with only default style".to_string(),
));
}
self.stack.pop();
Ok(self.current())
}
}
use lru::LruCache;
use std::num::NonZeroUsize;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
static LINK_ID_COUNTER: AtomicU64 = AtomicU64::new(1);
pub(crate) fn next_link_id() -> u64 {
LINK_ID_COUNTER.fetch_add(1, Ordering::Relaxed)
}
fn link_id_for(url: &str) -> u64 {
const FNV_OFFSET: u64 = 14_695_981_039_346_656_037;
const FNV_PRIME: u64 = 1_099_511_628_211;
let mut hash = FNV_OFFSET;
for byte in url.bytes() {
hash ^= byte as u64;
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
static STYLE_CACHE: Mutex<Option<LruCache<String, Style>>> = Mutex::new(None);
fn get_style_cache() -> std::sync::MutexGuard<'static, Option<LruCache<String, Style>>> {
let mut cache = STYLE_CACHE
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if cache.is_none() {
*cache = Some(LruCache::new(NonZeroUsize::new(256).unwrap()));
}
cache
}
pub fn clear_style_cache() {
if let Ok(mut cache) = STYLE_CACHE.lock() {
*cache = None;
}
}
pub fn style_cache_size() -> usize {
if let Ok(cache) = STYLE_CACHE.lock() {
cache.as_ref().map(|c| c.len()).unwrap_or(0)
} else {
0
}
}
#[cfg(feature = "json")]
impl serde::Serialize for Style {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&self.to_string())
}
}
#[cfg(feature = "json")]
impl<'de> serde::Deserialize<'de> for Style {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
use serde::de::{self, Visitor};
struct StyleVisitor;
impl<'de> Visitor<'de> for StyleVisitor {
type Value = Style;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "a style string like \"bold red\" or \"italic on blue\"")
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Style, E> {
Style::parse_strict(v).map_err(|e| de::Error::custom(e.to_string()))
}
}
d.deserialize_str(StyleVisitor)
}
}
#[cfg(test)]
#[path = "style_tests.rs"]
mod tests;