use std::ops::{BitOr, BitOrAssign};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct StyledFrame {
pub lines: Vec<StyledLine>,
}
impl StyledFrame {
#[must_use]
pub fn unstyled(s: String) -> Self {
if s.is_empty() {
return Self { lines: Vec::new() };
}
let lines = s.split('\n').map(StyledLine::unstyled).collect();
Self { lines }
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.lines.is_empty()
}
#[must_use]
pub fn plain_text(&self) -> String {
let mut out = String::new();
for (i, line) in self.lines.iter().enumerate() {
if i > 0 {
out.push('\n');
}
for span in &line.spans {
out.push_str(&span.text);
}
}
out
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct StyledLine {
pub spans: Vec<StyledSpan>,
}
impl StyledLine {
#[must_use]
pub fn unstyled(s: impl Into<String>) -> Self {
Self {
spans: vec![StyledSpan {
text: s.into(),
style: SpanStyle::default(),
}],
}
}
#[must_use]
pub fn from_spans<I: IntoIterator<Item = StyledSpan>>(spans: I) -> Self {
Self {
spans: spans.into_iter().collect(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StyledSpan {
pub text: String,
pub style: SpanStyle,
}
impl StyledSpan {
#[must_use]
pub fn new(text: impl Into<String>, style: SpanStyle) -> Self {
Self {
text: text.into(),
style,
}
}
#[must_use]
pub fn raw(text: impl Into<String>) -> Self {
Self::new(text, SpanStyle::default())
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SpanStyle {
pub fg: Option<Color>,
pub bg: Option<Color>,
pub modifier: Modifier,
}
impl SpanStyle {
#[must_use]
pub const fn fg(mut self, c: Color) -> Self {
self.fg = Some(c);
self
}
#[must_use]
pub const fn bg(mut self, c: Color) -> Self {
self.bg = Some(c);
self
}
#[must_use]
pub const fn bold(mut self) -> Self {
self.modifier = self.modifier.union(Modifier::BOLD);
self
}
#[must_use]
pub const fn dim(mut self) -> Self {
self.modifier = self.modifier.union(Modifier::DIM);
self
}
#[must_use]
pub const fn italic(mut self) -> Self {
self.modifier = self.modifier.union(Modifier::ITALIC);
self
}
#[must_use]
pub const fn underlined(mut self) -> Self {
self.modifier = self.modifier.union(Modifier::UNDERLINED);
self
}
#[must_use]
pub const fn reversed(mut self) -> Self {
self.modifier = self.modifier.union(Modifier::REVERSED);
self
}
#[must_use]
pub const fn with_modifier(mut self, m: Modifier) -> Self {
self.modifier = self.modifier.union(m);
self
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Color {
Reset,
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
Gray,
DarkGray,
LightRed,
LightGreen,
LightYellow,
LightBlue,
LightMagenta,
LightCyan,
White,
Indexed(u8),
}
#[derive(Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Modifier(u8);
impl Modifier {
pub const EMPTY: Self = Self(0);
pub const BOLD: Self = Self(1 << 0);
pub const DIM: Self = Self(1 << 1);
pub const ITALIC: Self = Self(1 << 2);
pub const UNDERLINED: Self = Self(1 << 3);
pub const REVERSED: Self = Self(1 << 4);
#[must_use]
pub const fn contains(self, other: Self) -> bool {
(self.0 & other.0) == other.0
}
#[must_use]
pub const fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}
#[must_use]
pub const fn intersection(self, other: Self) -> Self {
Self(self.0 & other.0)
}
pub const fn insert(&mut self, other: Self) {
self.0 |= other.0;
}
pub const fn remove(&mut self, other: Self) {
self.0 &= !other.0;
}
#[must_use]
pub const fn is_empty(self) -> bool {
self.0 == 0
}
#[must_use]
pub const fn bits(self) -> u8 {
self.0
}
}
impl BitOr for Modifier {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
self.union(rhs)
}
}
impl BitOrAssign for Modifier {
fn bitor_assign(&mut self, rhs: Self) {
self.insert(rhs);
}
}
impl std::fmt::Debug for Modifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut names: Vec<&'static str> = Vec::new();
if self.contains(Self::BOLD) {
names.push("BOLD");
}
if self.contains(Self::DIM) {
names.push("DIM");
}
if self.contains(Self::ITALIC) {
names.push("ITALIC");
}
if self.contains(Self::UNDERLINED) {
names.push("UNDERLINED");
}
if self.contains(Self::REVERSED) {
names.push("REVERSED");
}
if names.is_empty() {
write!(f, "Modifier::EMPTY")
} else {
write!(f, "Modifier({})", names.join(" | "))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn styled_frame_unstyled_round_trip() {
let s = String::from("hello\nworld");
let frame = StyledFrame::unstyled(s.clone());
assert_eq!(frame.lines.len(), 2);
assert_eq!(frame.plain_text(), s);
}
#[test]
fn styled_frame_unstyled_empty() {
let frame = StyledFrame::unstyled(String::new());
assert!(frame.is_empty());
assert_eq!(frame.plain_text(), "");
}
#[test]
fn styled_frame_unstyled_single_line() {
let frame = StyledFrame::unstyled(String::from("solo"));
assert_eq!(frame.lines.len(), 1);
assert_eq!(frame.plain_text(), "solo");
}
#[test]
fn modifier_bitops() {
let bold_italic = Modifier::BOLD | Modifier::ITALIC;
assert!(bold_italic.contains(Modifier::BOLD));
assert!(bold_italic.contains(Modifier::ITALIC));
assert!(!bold_italic.contains(Modifier::REVERSED));
let intersection = bold_italic.intersection(Modifier::BOLD);
assert!(intersection.contains(Modifier::BOLD));
assert!(!intersection.contains(Modifier::ITALIC));
let mut m = Modifier::EMPTY;
m |= Modifier::UNDERLINED;
assert!(m.contains(Modifier::UNDERLINED));
m.remove(Modifier::UNDERLINED);
assert!(m.is_empty());
}
#[test]
fn spanstyle_builder_chain() {
let style: SpanStyle = SpanStyle::default()
.fg(Color::Cyan)
.bg(Color::Black)
.bold()
.reversed();
assert_eq!(style.fg, Some(Color::Cyan));
assert_eq!(style.bg, Some(Color::Black));
assert!(style.modifier.contains(Modifier::BOLD));
assert!(style.modifier.contains(Modifier::REVERSED));
assert!(!style.modifier.contains(Modifier::ITALIC));
}
#[test]
fn modifier_debug_lists_names() {
let m: Modifier = Modifier::BOLD | Modifier::REVERSED;
let s = format!("{m:?}");
assert!(s.contains("BOLD"));
assert!(s.contains("REVERSED"));
}
#[test]
fn modifier_debug_empty() {
let m = Modifier::EMPTY;
assert_eq!(format!("{m:?}"), "Modifier::EMPTY");
}
}