parse_style/
attributes.rs

1use std::fmt;
2use thiserror::Error;
3
4/// Individual effects that can be applied to text in a terminal.
5///
6/// `Attribute` values can be combined with bitwise operators to produce
7/// [`AttributeSet`]s.
8///
9/// `Attribute` values can be [parsed][std::str::FromStr] from the
10/// following case-insensitive strings:
11///
12/// - `"bold"` or `"b"` — `Bold`
13/// - `"dim"` or `"d"` — `Dim`
14/// - `"italic"` or `"i"` — `Italic`
15/// - `"underline"` or `"u"` — `Underline`
16/// - `"blink"` — `Blink`
17/// - `"blink2"` — `Blink2`
18/// - `"reverse"` or `"r"` — `Reverse`
19/// - `"conceal"` or `"c"` — `Conceal`
20/// - `"strike"` or `"s"` — `Strike`
21/// - `"underline2"` or `"uu"` — `Underline2`
22/// - `"frame"` — `Frame`
23/// - `"encircle"` — `Encircle`
24/// - `"overline"` — `Overline`
25///
26/// `Attribute` values are [displayed][std::fmt::Display] as lowercase
27/// strings from the above list; for values with two strings, the longer
28/// one is used.
29#[derive(Clone, Copy, Debug, strum::EnumIter, Eq, Hash, Ord, PartialEq, PartialOrd)]
30#[repr(u16)]
31pub enum Attribute {
32    Bold = 1 << 0,
33    Dim = 1 << 1,
34    Italic = 1 << 2,
35    Underline = 1 << 3,
36    Blink = 1 << 4,
37    /// Fast blinking
38    Blink2 = 1 << 5,
39    /// Reverse video
40    Reverse = 1 << 6,
41    /// Concealed/hidden
42    Conceal = 1 << 7,
43    /// Strikethrough
44    Strike = 1 << 8,
45    /// Double-underline
46    Underline2 = 1 << 9,
47    Frame = 1 << 10,
48    Encircle = 1 << 11,
49    Overline = 1 << 12,
50}
51
52impl Attribute {
53    const COUNT: u16 = 13;
54
55    /// Returns an iterator over all [`Attribute`] variants
56    pub fn iter() -> AttributeIter {
57        // To avoid the need for users to import the trait
58        <Attribute as strum::IntoEnumIterator>::iter()
59    }
60
61    /// Return the long name of the attribute
62    ///
63    /// # Example
64    ///
65    /// ```
66    /// use parse_style::Attribute;
67    ///
68    /// assert_eq!(Attribute::Bold.as_str(), "bold");
69    /// ```
70    pub fn as_str(self) -> &'static str {
71        match self {
72            Attribute::Bold => "bold",
73            Attribute::Dim => "dim",
74            Attribute::Italic => "italic",
75            Attribute::Underline => "underline",
76            Attribute::Blink => "blink",
77            Attribute::Blink2 => "blink2",
78            Attribute::Reverse => "reverse",
79            Attribute::Conceal => "conceal",
80            Attribute::Strike => "strike",
81            Attribute::Underline2 => "underline2",
82            Attribute::Frame => "frame",
83            Attribute::Encircle => "encircle",
84            Attribute::Overline => "overline",
85        }
86    }
87}
88
89impl fmt::Display for Attribute {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        write!(f, "{}", self.as_str())
92    }
93}
94
95impl std::str::FromStr for Attribute {
96    type Err = ParseAttributeError;
97
98    fn from_str(s: &str) -> Result<Attribute, ParseAttributeError> {
99        match s.to_ascii_lowercase().as_str() {
100            "bold" | "b" => Ok(Attribute::Bold),
101            "dim" | "d" => Ok(Attribute::Dim),
102            "italic" | "i" => Ok(Attribute::Italic),
103            "underline" | "u" => Ok(Attribute::Underline),
104            "blink" => Ok(Attribute::Blink),
105            "blink2" => Ok(Attribute::Blink2),
106            "reverse" | "r" => Ok(Attribute::Reverse),
107            "conceal" | "c" => Ok(Attribute::Conceal),
108            "strike" | "s" => Ok(Attribute::Strike),
109            "underline2" | "uu" => Ok(Attribute::Underline2),
110            "frame" => Ok(Attribute::Frame),
111            "encircle" => Ok(Attribute::Encircle),
112            "overline" => Ok(Attribute::Overline),
113            _ => Err(ParseAttributeError(s.to_owned())),
114        }
115    }
116}
117
118impl<A: Into<AttributeSet>> std::ops::BitAnd<A> for Attribute {
119    type Output = AttributeSet;
120
121    fn bitand(self, rhs: A) -> AttributeSet {
122        AttributeSet((self as u16) & rhs.into().0)
123    }
124}
125
126impl<A: Into<AttributeSet>> std::ops::BitOr<A> for Attribute {
127    type Output = AttributeSet;
128
129    fn bitor(self, rhs: A) -> AttributeSet {
130        AttributeSet((self as u16) | rhs.into().0)
131    }
132}
133
134impl<A: Into<AttributeSet>> std::ops::BitXor<A> for Attribute {
135    type Output = AttributeSet;
136
137    fn bitxor(self, rhs: A) -> AttributeSet {
138        AttributeSet((self as u16) ^ rhs.into().0)
139    }
140}
141
142impl<A: Into<AttributeSet>> std::ops::Sub<A> for Attribute {
143    type Output = AttributeSet;
144
145    fn sub(self, rhs: A) -> AttributeSet {
146        AttributeSet((self as u16) & !rhs.into().0)
147    }
148}
149
150impl std::ops::Not for Attribute {
151    type Output = AttributeSet;
152
153    fn not(self) -> AttributeSet {
154        AttributeSet::ALL - self
155    }
156}
157
158/// A set of [`Attribute`] values.
159///
160/// `AttributeSet` values can be combined with bitwise operators and can be
161/// iterated over.
162#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
163pub struct AttributeSet(u16);
164
165impl AttributeSet {
166    /// A set containing no [`Attribute`]s
167    pub const EMPTY: AttributeSet = AttributeSet(0);
168
169    /// A set containing all [`Attribute`]s
170    pub const ALL: AttributeSet = AttributeSet((1 << Attribute::COUNT) - 1);
171
172    /// Return a new set containing no [`Attribute`]s
173    pub fn new() -> AttributeSet {
174        AttributeSet(0)
175    }
176
177    /// Test whether the set is empty
178    pub fn is_empty(self) -> bool {
179        self.0 == 0
180    }
181
182    /// Test whether the set contains all [`Attribute`]s
183    pub fn is_all(self) -> bool {
184        self == Self::ALL
185    }
186
187    /// Test whether the set contains the given [`Attribute`]
188    pub fn contains(self, attr: Attribute) -> bool {
189        self.0 & (attr as u16) != 0
190    }
191}
192
193impl From<Attribute> for AttributeSet {
194    fn from(value: Attribute) -> AttributeSet {
195        AttributeSet(value as u16)
196    }
197}
198
199impl IntoIterator for AttributeSet {
200    type Item = Attribute;
201    type IntoIter = AttributeSetIter;
202
203    fn into_iter(self) -> AttributeSetIter {
204        AttributeSetIter::new(self)
205    }
206}
207
208impl FromIterator<Attribute> for AttributeSet {
209    fn from_iter<I: IntoIterator<Item = Attribute>>(iter: I) -> Self {
210        iter.into_iter()
211            .fold(AttributeSet::new(), |set, attr| set | attr)
212    }
213}
214
215impl Extend<Attribute> for AttributeSet {
216    fn extend<I: IntoIterator<Item = Attribute>>(&mut self, iter: I) {
217        for attr in iter {
218            *self |= attr;
219        }
220    }
221}
222
223#[cfg(feature = "anstyle")]
224#[cfg_attr(docsrs, doc(cfg(feature = "anstyle")))]
225impl From<AttributeSet> for anstyle::Effects {
226    /// Convert an `AttributeSet` to an [`anstyle::Effects`]
227    ///
228    /// # Data Loss
229    ///
230    /// The following attributes are discarded during conversion, as they have
231    /// no `anstyle::Effects` equivalents:
232    ///
233    /// - [`Attribute::Blink2`]
234    /// - [`Attribute::Frame`]
235    /// - [`Attribute::Encircle`]
236    /// - [`Attribute::Overline`]
237    fn from(value: AttributeSet) -> anstyle::Effects {
238        let mut efs = anstyle::Effects::new();
239        for attr in value {
240            match attr {
241                Attribute::Bold => efs |= anstyle::Effects::BOLD,
242                Attribute::Dim => efs |= anstyle::Effects::DIMMED,
243                Attribute::Italic => efs |= anstyle::Effects::ITALIC,
244                Attribute::Underline => efs |= anstyle::Effects::UNDERLINE,
245                Attribute::Blink => efs |= anstyle::Effects::BLINK,
246                Attribute::Blink2 => (),
247                Attribute::Reverse => efs |= anstyle::Effects::INVERT,
248                Attribute::Conceal => efs |= anstyle::Effects::HIDDEN,
249                Attribute::Strike => efs |= anstyle::Effects::STRIKETHROUGH,
250                Attribute::Underline2 => efs |= anstyle::Effects::DOUBLE_UNDERLINE,
251                Attribute::Frame => (),
252                Attribute::Encircle => (),
253                Attribute::Overline => (),
254            }
255        }
256        efs
257    }
258}
259
260#[cfg(feature = "anstyle")]
261#[cfg_attr(docsrs, doc(cfg(feature = "anstyle")))]
262impl From<anstyle::Effects> for AttributeSet {
263    /// Convert an [`anstyle::Effects`] to an `AttributeSet`
264    ///
265    ///
266    /// # Data Loss
267    ///
268    /// The following effects are discarded during conversion, as they have no
269    /// `Attribute` equivalents:
270    ///
271    /// - [`anstyle::Effects::CURLY_UNDERLINE`]
272    /// - [`anstyle::Effects::DOTTED_UNDERLINE`]
273    /// - [`anstyle::Effects::DASHED_UNDERLINE`]
274    fn from(value: anstyle::Effects) -> AttributeSet {
275        let mut set = AttributeSet::new();
276        for eff in value.iter() {
277            match eff {
278                anstyle::Effects::BOLD => set |= Attribute::Bold,
279                anstyle::Effects::DIMMED => set |= Attribute::Dim,
280                anstyle::Effects::ITALIC => set |= Attribute::Italic,
281                anstyle::Effects::UNDERLINE => set |= Attribute::Underline,
282                anstyle::Effects::DOUBLE_UNDERLINE => set |= Attribute::Underline2,
283                anstyle::Effects::CURLY_UNDERLINE => (),
284                anstyle::Effects::DOTTED_UNDERLINE => (),
285                anstyle::Effects::DASHED_UNDERLINE => (),
286                anstyle::Effects::BLINK => set |= Attribute::Blink,
287                anstyle::Effects::INVERT => set |= Attribute::Reverse,
288                anstyle::Effects::HIDDEN => set |= Attribute::Conceal,
289                anstyle::Effects::STRIKETHROUGH => set |= Attribute::Strike,
290                // Because an `Effects` can be either a single effect or
291                // multiple, we need a catch-all arm here, even though the
292                // iterator will only yield single effects.
293                _ => (),
294            }
295        }
296        set
297    }
298}
299
300#[cfg(feature = "crossterm")]
301#[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
302impl From<AttributeSet> for crossterm::style::Attributes {
303    /// Convert an `AttributeSet` to a [`crossterm::style::Attributes`] that
304    /// enables the input attributes
305    fn from(value: AttributeSet) -> crossterm::style::Attributes {
306        use crossterm::style::Attribute as CrossAttrib;
307        let mut attributes = crossterm::style::Attributes::none();
308        for attr in value {
309            let ca = match attr {
310                Attribute::Bold => CrossAttrib::Bold,
311                Attribute::Dim => CrossAttrib::Dim,
312                Attribute::Italic => CrossAttrib::Italic,
313                Attribute::Underline => CrossAttrib::Underlined,
314                Attribute::Blink => CrossAttrib::SlowBlink,
315                Attribute::Blink2 => CrossAttrib::RapidBlink,
316                Attribute::Reverse => CrossAttrib::Reverse,
317                Attribute::Conceal => CrossAttrib::Hidden,
318                Attribute::Strike => CrossAttrib::CrossedOut,
319                Attribute::Underline2 => CrossAttrib::DoubleUnderlined,
320                Attribute::Frame => CrossAttrib::Framed,
321                Attribute::Encircle => CrossAttrib::Encircled,
322                Attribute::Overline => CrossAttrib::OverLined,
323            };
324            attributes.set(ca);
325        }
326        attributes
327    }
328}
329
330#[cfg(feature = "ratatui")]
331#[cfg_attr(docsrs, doc(cfg(feature = "ratatui")))]
332impl From<AttributeSet> for ratatui::style::Modifier {
333    /// Convert an `AttributeSet` to an [`ratatui::style::Modifier`]
334    ///
335    /// # Data Loss
336    ///
337    /// The following attributes are discarded during conversion, as they have
338    /// no `ratatui::style::Modifier` equivalents:
339    ///
340    /// - [`Attribute::Underline2`]
341    /// - [`Attribute::Frame`]
342    /// - [`Attribute::Encircle`]
343    /// - [`Attribute::Overline`]
344    fn from(value: AttributeSet) -> ratatui::style::Modifier {
345        let mut mods = ratatui::style::Modifier::empty();
346        for attr in value {
347            match attr {
348                Attribute::Bold => mods |= ratatui::style::Modifier::BOLD,
349                Attribute::Dim => mods |= ratatui::style::Modifier::DIM,
350                Attribute::Italic => mods |= ratatui::style::Modifier::ITALIC,
351                Attribute::Underline => mods |= ratatui::style::Modifier::UNDERLINED,
352                Attribute::Blink => mods |= ratatui::style::Modifier::SLOW_BLINK,
353                Attribute::Blink2 => mods |= ratatui::style::Modifier::RAPID_BLINK,
354                Attribute::Reverse => mods |= ratatui::style::Modifier::REVERSED,
355                Attribute::Conceal => mods |= ratatui::style::Modifier::HIDDEN,
356                Attribute::Strike => mods |= ratatui::style::Modifier::CROSSED_OUT,
357                Attribute::Underline2 => (),
358                Attribute::Frame => (),
359                Attribute::Encircle => (),
360                Attribute::Overline => (),
361            }
362        }
363        mods
364    }
365}
366
367#[cfg(feature = "ratatui")]
368#[cfg_attr(docsrs, doc(cfg(feature = "ratatui")))]
369impl From<ratatui::style::Modifier> for AttributeSet {
370    /// Convert a [`ratatui::style::Modifier`] to an `AttributeSet`
371    fn from(value: ratatui::style::Modifier) -> AttributeSet {
372        let mut set = AttributeSet::new();
373        for m in value.iter() {
374            match m {
375                ratatui::style::Modifier::BOLD => set |= Attribute::Bold,
376                ratatui::style::Modifier::DIM => set |= Attribute::Dim,
377                ratatui::style::Modifier::ITALIC => set |= Attribute::Italic,
378                ratatui::style::Modifier::UNDERLINED => set |= Attribute::Underline,
379                ratatui::style::Modifier::SLOW_BLINK => set |= Attribute::Blink,
380                ratatui::style::Modifier::RAPID_BLINK => set |= Attribute::Blink,
381                ratatui::style::Modifier::REVERSED => set |= Attribute::Reverse,
382                ratatui::style::Modifier::HIDDEN => set |= Attribute::Conceal,
383                ratatui::style::Modifier::CROSSED_OUT => set |= Attribute::Strike,
384                // Because a `Modifier` can be either a single effect or
385                // multiple, we need a catch-all arm here, even though the
386                // iterator will only yield single modifiers.
387                _ => (),
388            }
389        }
390        set
391    }
392}
393
394impl<A: Into<AttributeSet>> std::ops::BitAnd<A> for AttributeSet {
395    type Output = AttributeSet;
396
397    fn bitand(self, rhs: A) -> AttributeSet {
398        AttributeSet(self.0 & rhs.into().0)
399    }
400}
401
402impl<A: Into<AttributeSet>> std::ops::BitAndAssign<A> for AttributeSet {
403    fn bitand_assign(&mut self, rhs: A) {
404        self.0 &= rhs.into().0;
405    }
406}
407
408impl<A: Into<AttributeSet>> std::ops::BitOr<A> for AttributeSet {
409    type Output = AttributeSet;
410
411    fn bitor(self, rhs: A) -> AttributeSet {
412        AttributeSet(self.0 | rhs.into().0)
413    }
414}
415
416impl<A: Into<AttributeSet>> std::ops::BitOrAssign<A> for AttributeSet {
417    fn bitor_assign(&mut self, rhs: A) {
418        self.0 |= rhs.into().0;
419    }
420}
421
422impl<A: Into<AttributeSet>> std::ops::BitXor<A> for AttributeSet {
423    type Output = AttributeSet;
424
425    fn bitxor(self, rhs: A) -> AttributeSet {
426        AttributeSet(self.0 ^ rhs.into().0)
427    }
428}
429
430impl<A: Into<AttributeSet>> std::ops::BitXorAssign<A> for AttributeSet {
431    fn bitxor_assign(&mut self, rhs: A) {
432        self.0 ^= rhs.into().0;
433    }
434}
435
436impl<A: Into<AttributeSet>> std::ops::Sub<A> for AttributeSet {
437    type Output = AttributeSet;
438
439    fn sub(self, rhs: A) -> AttributeSet {
440        AttributeSet(self.0 & !rhs.into().0)
441    }
442}
443
444impl<A: Into<AttributeSet>> std::ops::SubAssign<A> for AttributeSet {
445    fn sub_assign(&mut self, rhs: A) {
446        self.0 &= !rhs.into().0;
447    }
448}
449
450impl std::ops::Not for AttributeSet {
451    type Output = AttributeSet;
452
453    fn not(self) -> AttributeSet {
454        AttributeSet(!self.0 & ((1 << Attribute::COUNT) - 1))
455    }
456}
457
458/// An iterator over the [`Attribute`]s in an [`AttributeSet`]
459#[derive(Clone, Debug)]
460pub struct AttributeSetIter {
461    inner: AttributeIter,
462    set: AttributeSet,
463}
464
465impl AttributeSetIter {
466    fn new(set: AttributeSet) -> AttributeSetIter {
467        AttributeSetIter {
468            inner: Attribute::iter(),
469            set,
470        }
471    }
472}
473
474impl Iterator for AttributeSetIter {
475    type Item = Attribute;
476
477    fn next(&mut self) -> Option<Attribute> {
478        self.inner.by_ref().find(|&attr| self.set.contains(attr))
479    }
480
481    fn size_hint(&self) -> (usize, Option<usize>) {
482        (0, self.inner.size_hint().1)
483    }
484}
485
486impl DoubleEndedIterator for AttributeSetIter {
487    fn next_back(&mut self) -> Option<Attribute> {
488        self.inner.by_ref().rfind(|&attr| self.set.contains(attr))
489    }
490}
491
492impl std::iter::FusedIterator for AttributeSetIter {}
493
494/// Error returned when parsing an attribute fails
495#[derive(Clone, Debug, Eq, Error, PartialEq)]
496#[error("invalid attribute name: {0:?}")]
497pub struct ParseAttributeError(
498    /// The invalid attribute string
499    pub String,
500);
501
502#[cfg(test)]
503mod tests {
504    use super::*;
505
506    #[test]
507    fn double_ended_iteration() {
508        let attrs = Attribute::Bold | Attribute::Frame | Attribute::Reverse | Attribute::Strike;
509        let mut iter = attrs.into_iter();
510        assert_eq!(iter.next(), Some(Attribute::Bold));
511        assert_eq!(iter.next_back(), Some(Attribute::Frame));
512        assert_eq!(iter.next(), Some(Attribute::Reverse));
513        assert_eq!(iter.next_back(), Some(Attribute::Strike));
514        assert_eq!(iter.next(), None);
515        assert_eq!(iter.next_back(), None);
516        assert_eq!(iter.next(), None);
517        assert_eq!(iter.next_back(), None);
518    }
519}