ratatui/backend/
crossterm.rs

1//! This module provides the [`CrosstermBackend`] implementation for the [`Backend`] trait. It uses
2//! the [Crossterm] crate to interact with the terminal.
3//!
4//! [Crossterm]: https://crates.io/crates/crossterm
5use std::io::{self, Write};
6
7#[cfg(feature = "underline-color")]
8use crossterm::style::SetUnderlineColor;
9
10use crate::{
11    backend::{Backend, ClearType, WindowSize},
12    buffer::Cell,
13    crossterm::{
14        cursor::{Hide, MoveTo, Show},
15        execute, queue,
16        style::{
17            Attribute as CAttribute, Attributes as CAttributes, Color as CColor, Colors,
18            ContentStyle, Print, SetAttribute, SetBackgroundColor, SetColors, SetForegroundColor,
19        },
20        terminal::{self, Clear},
21    },
22    layout::{Position, Size},
23    style::{Color, Modifier, Style},
24};
25
26/// A [`Backend`] implementation that uses [Crossterm] to render to the terminal.
27///
28/// The `CrosstermBackend` struct is a wrapper around a writer implementing [`Write`], which is
29/// used to send commands to the terminal. It provides methods for drawing content, manipulating
30/// the cursor, and clearing the terminal screen.
31///
32/// Most applications should not call the methods on `CrosstermBackend` directly, but will instead
33/// use the [`Terminal`] struct, which provides a more ergonomic interface.
34///
35/// Usually applications will enable raw mode and switch to alternate screen mode after creating
36/// a `CrosstermBackend`. This is done by calling [`crossterm::terminal::enable_raw_mode`] and
37/// [`crossterm::terminal::EnterAlternateScreen`] (and the corresponding disable/leave functions
38/// when the application exits). This is not done automatically by the backend because it is
39/// possible that the application may want to use the terminal for other purposes (like showing
40/// help text) before entering alternate screen mode.
41///
42/// # Example
43///
44/// ```rust,no_run
45/// use std::io::{stderr, stdout};
46///
47/// use crossterm::{
48///     terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
49///     ExecutableCommand,
50/// };
51/// use ratatui::{backend::CrosstermBackend, Terminal};
52///
53/// let mut backend = CrosstermBackend::new(stdout());
54/// // or
55/// let backend = CrosstermBackend::new(stderr());
56/// let mut terminal = Terminal::new(backend)?;
57///
58/// enable_raw_mode()?;
59/// stdout().execute(EnterAlternateScreen)?;
60///
61/// terminal.clear()?;
62/// terminal.draw(|frame| {
63///     // -- snip --
64/// })?;
65///
66/// stdout().execute(LeaveAlternateScreen)?;
67/// disable_raw_mode()?;
68///
69/// # std::io::Result::Ok(())
70/// ```
71///
72/// See the the [Examples] directory for more examples. See the [`backend`] module documentation
73/// for more details on raw mode and alternate screen.
74///
75/// [`Write`]: std::io::Write
76/// [`Terminal`]: crate::terminal::Terminal
77/// [`backend`]: crate::backend
78/// [Crossterm]: https://crates.io/crates/crossterm
79/// [Examples]: https://github.com/ratatui/ratatui/tree/main/examples/README.md
80#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
81pub struct CrosstermBackend<W: Write> {
82    /// The writer used to send commands to the terminal.
83    writer: W,
84}
85
86impl<W> CrosstermBackend<W>
87where
88    W: Write,
89{
90    /// Creates a new `CrosstermBackend` with the given writer.
91    ///
92    /// Most applications will use either [`stdout`](std::io::stdout) or
93    /// [`stderr`](std::io::stderr) as writer. See the [FAQ] to determine which one to use.
94    ///
95    /// [FAQ]: https://ratatui.rs/faq/#should-i-use-stdout-or-stderr
96    ///
97    /// # Example
98    ///
99    /// ```rust,no_run
100    /// use std::io::stdout;
101    ///
102    /// use ratatui::backend::CrosstermBackend;
103    ///
104    /// let backend = CrosstermBackend::new(stdout());
105    /// ```
106    pub const fn new(writer: W) -> Self {
107        Self { writer }
108    }
109
110    /// Gets the writer.
111    #[instability::unstable(
112        feature = "backend-writer",
113        issue = "https://github.com/ratatui/ratatui/pull/991"
114    )]
115    pub const fn writer(&self) -> &W {
116        &self.writer
117    }
118
119    /// Gets the writer as a mutable reference.
120    ///
121    /// Note: writing to the writer may cause incorrect output after the write. This is due to the
122    /// way that the Terminal implements diffing Buffers.
123    #[instability::unstable(
124        feature = "backend-writer",
125        issue = "https://github.com/ratatui/ratatui/pull/991"
126    )]
127    pub fn writer_mut(&mut self) -> &mut W {
128        &mut self.writer
129    }
130}
131
132impl<W> Write for CrosstermBackend<W>
133where
134    W: Write,
135{
136    /// Writes a buffer of bytes to the underlying buffer.
137    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
138        self.writer.write(buf)
139    }
140
141    /// Flushes the underlying buffer.
142    fn flush(&mut self) -> io::Result<()> {
143        self.writer.flush()
144    }
145}
146
147impl<W> Backend for CrosstermBackend<W>
148where
149    W: Write,
150{
151    fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
152    where
153        I: Iterator<Item = (u16, u16, &'a Cell)>,
154    {
155        let mut fg = Color::Reset;
156        let mut bg = Color::Reset;
157        #[cfg(feature = "underline-color")]
158        let mut underline_color = Color::Reset;
159        let mut modifier = Modifier::empty();
160        let mut last_pos: Option<Position> = None;
161        for (x, y, cell) in content {
162            // Move the cursor if the previous location was not (x - 1, y)
163            if !matches!(last_pos, Some(p) if x == p.x + 1 && y == p.y) {
164                queue!(self.writer, MoveTo(x, y))?;
165            }
166            last_pos = Some(Position { x, y });
167            if cell.modifier != modifier {
168                let diff = ModifierDiff {
169                    from: modifier,
170                    to: cell.modifier,
171                };
172                diff.queue(&mut self.writer)?;
173                modifier = cell.modifier;
174            }
175            if cell.fg != fg || cell.bg != bg {
176                queue!(
177                    self.writer,
178                    SetColors(Colors::new(cell.fg.into(), cell.bg.into()))
179                )?;
180                fg = cell.fg;
181                bg = cell.bg;
182            }
183            #[cfg(feature = "underline-color")]
184            if cell.underline_color != underline_color {
185                let color = CColor::from(cell.underline_color);
186                queue!(self.writer, SetUnderlineColor(color))?;
187                underline_color = cell.underline_color;
188            }
189
190            queue!(self.writer, Print(cell.symbol()))?;
191        }
192
193        #[cfg(feature = "underline-color")]
194        return queue!(
195            self.writer,
196            SetForegroundColor(CColor::Reset),
197            SetBackgroundColor(CColor::Reset),
198            SetUnderlineColor(CColor::Reset),
199            SetAttribute(CAttribute::Reset),
200        );
201        #[cfg(not(feature = "underline-color"))]
202        return queue!(
203            self.writer,
204            SetForegroundColor(CColor::Reset),
205            SetBackgroundColor(CColor::Reset),
206            SetAttribute(CAttribute::Reset),
207        );
208    }
209
210    fn hide_cursor(&mut self) -> io::Result<()> {
211        execute!(self.writer, Hide)
212    }
213
214    fn show_cursor(&mut self) -> io::Result<()> {
215        execute!(self.writer, Show)
216    }
217
218    fn get_cursor_position(&mut self) -> io::Result<Position> {
219        crossterm::cursor::position()
220            .map(|(x, y)| Position { x, y })
221            .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
222    }
223
224    fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
225        let Position { x, y } = position.into();
226        execute!(self.writer, MoveTo(x, y))
227    }
228
229    fn clear(&mut self) -> io::Result<()> {
230        self.clear_region(ClearType::All)
231    }
232
233    fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
234        execute!(
235            self.writer,
236            Clear(match clear_type {
237                ClearType::All => crossterm::terminal::ClearType::All,
238                ClearType::AfterCursor => crossterm::terminal::ClearType::FromCursorDown,
239                ClearType::BeforeCursor => crossterm::terminal::ClearType::FromCursorUp,
240                ClearType::CurrentLine => crossterm::terminal::ClearType::CurrentLine,
241                ClearType::UntilNewLine => crossterm::terminal::ClearType::UntilNewLine,
242            })
243        )
244    }
245
246    fn append_lines(&mut self, n: u16) -> io::Result<()> {
247        for _ in 0..n {
248            queue!(self.writer, Print("\n"))?;
249        }
250        self.writer.flush()
251    }
252
253    fn size(&self) -> io::Result<Size> {
254        let (width, height) = terminal::size()?;
255        Ok(Size { width, height })
256    }
257
258    fn window_size(&mut self) -> io::Result<WindowSize> {
259        let crossterm::terminal::WindowSize {
260            columns,
261            rows,
262            width,
263            height,
264        } = terminal::window_size()?;
265        Ok(WindowSize {
266            columns_rows: Size {
267                width: columns,
268                height: rows,
269            },
270            pixels: Size { width, height },
271        })
272    }
273
274    fn flush(&mut self) -> io::Result<()> {
275        self.writer.flush()
276    }
277
278    #[cfg(feature = "scrolling-regions")]
279    fn scroll_region_up(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
280        queue!(
281            self.writer,
282            ScrollUpInRegion {
283                first_row: region.start,
284                last_row: region.end.saturating_sub(1),
285                lines_to_scroll: amount,
286            }
287        )?;
288        self.writer.flush()
289    }
290
291    #[cfg(feature = "scrolling-regions")]
292    fn scroll_region_down(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
293        queue!(
294            self.writer,
295            ScrollDownInRegion {
296                first_row: region.start,
297                last_row: region.end.saturating_sub(1),
298                lines_to_scroll: amount,
299            }
300        )?;
301        self.writer.flush()
302    }
303}
304
305impl From<Color> for CColor {
306    fn from(color: Color) -> Self {
307        match color {
308            Color::Reset => Self::Reset,
309            Color::Black => Self::Black,
310            Color::Red => Self::DarkRed,
311            Color::Green => Self::DarkGreen,
312            Color::Yellow => Self::DarkYellow,
313            Color::Blue => Self::DarkBlue,
314            Color::Magenta => Self::DarkMagenta,
315            Color::Cyan => Self::DarkCyan,
316            Color::Gray => Self::Grey,
317            Color::DarkGray => Self::DarkGrey,
318            Color::LightRed => Self::Red,
319            Color::LightGreen => Self::Green,
320            Color::LightBlue => Self::Blue,
321            Color::LightYellow => Self::Yellow,
322            Color::LightMagenta => Self::Magenta,
323            Color::LightCyan => Self::Cyan,
324            Color::White => Self::White,
325            Color::Indexed(i) => Self::AnsiValue(i),
326            Color::Rgb(r, g, b) => Self::Rgb { r, g, b },
327        }
328    }
329}
330
331impl From<CColor> for Color {
332    fn from(value: CColor) -> Self {
333        match value {
334            CColor::Reset => Self::Reset,
335            CColor::Black => Self::Black,
336            CColor::DarkRed => Self::Red,
337            CColor::DarkGreen => Self::Green,
338            CColor::DarkYellow => Self::Yellow,
339            CColor::DarkBlue => Self::Blue,
340            CColor::DarkMagenta => Self::Magenta,
341            CColor::DarkCyan => Self::Cyan,
342            CColor::Grey => Self::Gray,
343            CColor::DarkGrey => Self::DarkGray,
344            CColor::Red => Self::LightRed,
345            CColor::Green => Self::LightGreen,
346            CColor::Blue => Self::LightBlue,
347            CColor::Yellow => Self::LightYellow,
348            CColor::Magenta => Self::LightMagenta,
349            CColor::Cyan => Self::LightCyan,
350            CColor::White => Self::White,
351            CColor::Rgb { r, g, b } => Self::Rgb(r, g, b),
352            CColor::AnsiValue(v) => Self::Indexed(v),
353        }
354    }
355}
356
357/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
358/// values. This is useful when updating the terminal display, as it allows for more
359/// efficient updates by only sending the necessary changes.
360struct ModifierDiff {
361    pub from: Modifier,
362    pub to: Modifier,
363}
364
365impl ModifierDiff {
366    fn queue<W>(self, mut w: W) -> io::Result<()>
367    where
368        W: io::Write,
369    {
370        //use crossterm::Attribute;
371        let removed = self.from - self.to;
372        if removed.contains(Modifier::REVERSED) {
373            queue!(w, SetAttribute(CAttribute::NoReverse))?;
374        }
375        if removed.contains(Modifier::BOLD) {
376            queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
377            if self.to.contains(Modifier::DIM) {
378                queue!(w, SetAttribute(CAttribute::Dim))?;
379            }
380        }
381        if removed.contains(Modifier::ITALIC) {
382            queue!(w, SetAttribute(CAttribute::NoItalic))?;
383        }
384        if removed.contains(Modifier::UNDERLINED) {
385            queue!(w, SetAttribute(CAttribute::NoUnderline))?;
386        }
387        if removed.contains(Modifier::DIM) {
388            queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
389        }
390        if removed.contains(Modifier::CROSSED_OUT) {
391            queue!(w, SetAttribute(CAttribute::NotCrossedOut))?;
392        }
393        if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
394            queue!(w, SetAttribute(CAttribute::NoBlink))?;
395        }
396
397        let added = self.to - self.from;
398        if added.contains(Modifier::REVERSED) {
399            queue!(w, SetAttribute(CAttribute::Reverse))?;
400        }
401        if added.contains(Modifier::BOLD) {
402            queue!(w, SetAttribute(CAttribute::Bold))?;
403        }
404        if added.contains(Modifier::ITALIC) {
405            queue!(w, SetAttribute(CAttribute::Italic))?;
406        }
407        if added.contains(Modifier::UNDERLINED) {
408            queue!(w, SetAttribute(CAttribute::Underlined))?;
409        }
410        if added.contains(Modifier::DIM) {
411            queue!(w, SetAttribute(CAttribute::Dim))?;
412        }
413        if added.contains(Modifier::CROSSED_OUT) {
414            queue!(w, SetAttribute(CAttribute::CrossedOut))?;
415        }
416        if added.contains(Modifier::SLOW_BLINK) {
417            queue!(w, SetAttribute(CAttribute::SlowBlink))?;
418        }
419        if added.contains(Modifier::RAPID_BLINK) {
420            queue!(w, SetAttribute(CAttribute::RapidBlink))?;
421        }
422
423        Ok(())
424    }
425}
426
427impl From<CAttribute> for Modifier {
428    fn from(value: CAttribute) -> Self {
429        // `Attribute*s*` (note the *s*) contains multiple `Attribute`
430        // We convert `Attribute` to `Attribute*s*` (containing only 1 value) to avoid implementing
431        // the conversion again
432        Self::from(CAttributes::from(value))
433    }
434}
435
436impl From<CAttributes> for Modifier {
437    fn from(value: CAttributes) -> Self {
438        let mut res = Self::empty();
439
440        if value.has(CAttribute::Bold) {
441            res |= Self::BOLD;
442        }
443        if value.has(CAttribute::Dim) {
444            res |= Self::DIM;
445        }
446        if value.has(CAttribute::Italic) {
447            res |= Self::ITALIC;
448        }
449        if value.has(CAttribute::Underlined)
450            || value.has(CAttribute::DoubleUnderlined)
451            || value.has(CAttribute::Undercurled)
452            || value.has(CAttribute::Underdotted)
453            || value.has(CAttribute::Underdashed)
454        {
455            res |= Self::UNDERLINED;
456        }
457        if value.has(CAttribute::SlowBlink) {
458            res |= Self::SLOW_BLINK;
459        }
460        if value.has(CAttribute::RapidBlink) {
461            res |= Self::RAPID_BLINK;
462        }
463        if value.has(CAttribute::Reverse) {
464            res |= Self::REVERSED;
465        }
466        if value.has(CAttribute::Hidden) {
467            res |= Self::HIDDEN;
468        }
469        if value.has(CAttribute::CrossedOut) {
470            res |= Self::CROSSED_OUT;
471        }
472
473        res
474    }
475}
476
477impl From<ContentStyle> for Style {
478    fn from(value: ContentStyle) -> Self {
479        let mut sub_modifier = Modifier::empty();
480
481        if value.attributes.has(CAttribute::NoBold) {
482            sub_modifier |= Modifier::BOLD;
483        }
484        if value.attributes.has(CAttribute::NoItalic) {
485            sub_modifier |= Modifier::ITALIC;
486        }
487        if value.attributes.has(CAttribute::NotCrossedOut) {
488            sub_modifier |= Modifier::CROSSED_OUT;
489        }
490        if value.attributes.has(CAttribute::NoUnderline) {
491            sub_modifier |= Modifier::UNDERLINED;
492        }
493        if value.attributes.has(CAttribute::NoHidden) {
494            sub_modifier |= Modifier::HIDDEN;
495        }
496        if value.attributes.has(CAttribute::NoBlink) {
497            sub_modifier |= Modifier::RAPID_BLINK | Modifier::SLOW_BLINK;
498        }
499        if value.attributes.has(CAttribute::NoReverse) {
500            sub_modifier |= Modifier::REVERSED;
501        }
502
503        Self {
504            fg: value.foreground_color.map(Into::into),
505            bg: value.background_color.map(Into::into),
506            #[cfg(feature = "underline-color")]
507            underline_color: value.underline_color.map(Into::into),
508            add_modifier: value.attributes.into(),
509            sub_modifier,
510        }
511    }
512}
513
514/// A command that scrolls the terminal screen a given number of rows up in a specific scrolling
515/// region.
516///
517/// This will hopefully be replaced by a struct in crossterm proper. There are two outstanding
518/// crossterm PRs that will address this:
519///   - [918](https://github.com/crossterm-rs/crossterm/pull/918)
520///   - [923](https://github.com/crossterm-rs/crossterm/pull/923)
521#[cfg(feature = "scrolling-regions")]
522#[derive(Debug, Clone, Copy, PartialEq, Eq)]
523struct ScrollUpInRegion {
524    /// The first row of the scrolling region.
525    pub first_row: u16,
526
527    /// The last row of the scrolling region.
528    pub last_row: u16,
529
530    /// The number of lines to scroll up by.
531    pub lines_to_scroll: u16,
532}
533
534#[cfg(feature = "scrolling-regions")]
535impl crate::crossterm::Command for ScrollUpInRegion {
536    fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
537        if self.lines_to_scroll != 0 {
538            // Set a scrolling region that contains just the desired lines.
539            write!(
540                f,
541                crate::crossterm::csi!("{};{}r"),
542                self.first_row.saturating_add(1),
543                self.last_row.saturating_add(1)
544            )?;
545            // Scroll the region by the desired count.
546            write!(f, crate::crossterm::csi!("{}S"), self.lines_to_scroll)?;
547            // Reset the scrolling region to be the whole screen.
548            write!(f, crate::crossterm::csi!("r"))?;
549        }
550        Ok(())
551    }
552
553    #[cfg(windows)]
554    fn execute_winapi(&self) -> io::Result<()> {
555        Err(io::Error::new(
556            io::ErrorKind::Unsupported,
557            "ScrollUpInRegion command not supported for winapi",
558        ))
559    }
560}
561
562/// A command that scrolls the terminal screen a given number of rows down in a specific scrolling
563/// region.
564///
565/// This will hopefully be replaced by a struct in crossterm proper. There are two outstanding
566/// crossterm PRs that will address this:
567///   - [918](https://github.com/crossterm-rs/crossterm/pull/918)
568///   - [923](https://github.com/crossterm-rs/crossterm/pull/923)
569#[cfg(feature = "scrolling-regions")]
570#[derive(Debug, Clone, Copy, PartialEq, Eq)]
571struct ScrollDownInRegion {
572    /// The first row of the scrolling region.
573    pub first_row: u16,
574
575    /// The last row of the scrolling region.
576    pub last_row: u16,
577
578    /// The number of lines to scroll down by.
579    pub lines_to_scroll: u16,
580}
581
582#[cfg(feature = "scrolling-regions")]
583impl crate::crossterm::Command for ScrollDownInRegion {
584    fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
585        if self.lines_to_scroll != 0 {
586            // Set a scrolling region that contains just the desired lines.
587            write!(
588                f,
589                crate::crossterm::csi!("{};{}r"),
590                self.first_row.saturating_add(1),
591                self.last_row.saturating_add(1)
592            )?;
593            // Scroll the region by the desired count.
594            write!(f, crate::crossterm::csi!("{}T"), self.lines_to_scroll)?;
595            // Reset the scrolling region to be the whole screen.
596            write!(f, crate::crossterm::csi!("r"))?;
597        }
598        Ok(())
599    }
600
601    #[cfg(windows)]
602    fn execute_winapi(&self) -> io::Result<()> {
603        Err(io::Error::new(
604            io::ErrorKind::Unsupported,
605            "ScrollDownInRegion command not supported for winapi",
606        ))
607    }
608}
609
610#[cfg(test)]
611mod tests {
612    use super::*;
613
614    #[test]
615    fn from_crossterm_color() {
616        assert_eq!(Color::from(CColor::Reset), Color::Reset);
617        assert_eq!(Color::from(CColor::Black), Color::Black);
618        assert_eq!(Color::from(CColor::DarkGrey), Color::DarkGray);
619        assert_eq!(Color::from(CColor::Red), Color::LightRed);
620        assert_eq!(Color::from(CColor::DarkRed), Color::Red);
621        assert_eq!(Color::from(CColor::Green), Color::LightGreen);
622        assert_eq!(Color::from(CColor::DarkGreen), Color::Green);
623        assert_eq!(Color::from(CColor::Yellow), Color::LightYellow);
624        assert_eq!(Color::from(CColor::DarkYellow), Color::Yellow);
625        assert_eq!(Color::from(CColor::Blue), Color::LightBlue);
626        assert_eq!(Color::from(CColor::DarkBlue), Color::Blue);
627        assert_eq!(Color::from(CColor::Magenta), Color::LightMagenta);
628        assert_eq!(Color::from(CColor::DarkMagenta), Color::Magenta);
629        assert_eq!(Color::from(CColor::Cyan), Color::LightCyan);
630        assert_eq!(Color::from(CColor::DarkCyan), Color::Cyan);
631        assert_eq!(Color::from(CColor::White), Color::White);
632        assert_eq!(Color::from(CColor::Grey), Color::Gray);
633        assert_eq!(
634            Color::from(CColor::Rgb { r: 0, g: 0, b: 0 }),
635            Color::Rgb(0, 0, 0)
636        );
637        assert_eq!(
638            Color::from(CColor::Rgb {
639                r: 10,
640                g: 20,
641                b: 30
642            }),
643            Color::Rgb(10, 20, 30)
644        );
645        assert_eq!(Color::from(CColor::AnsiValue(32)), Color::Indexed(32));
646        assert_eq!(Color::from(CColor::AnsiValue(37)), Color::Indexed(37));
647    }
648
649    mod modifier {
650        use super::*;
651
652        #[test]
653        fn from_crossterm_attribute() {
654            assert_eq!(Modifier::from(CAttribute::Reset), Modifier::empty());
655            assert_eq!(Modifier::from(CAttribute::Bold), Modifier::BOLD);
656            assert_eq!(Modifier::from(CAttribute::Italic), Modifier::ITALIC);
657            assert_eq!(Modifier::from(CAttribute::Underlined), Modifier::UNDERLINED);
658            assert_eq!(
659                Modifier::from(CAttribute::DoubleUnderlined),
660                Modifier::UNDERLINED
661            );
662            assert_eq!(
663                Modifier::from(CAttribute::Underdotted),
664                Modifier::UNDERLINED
665            );
666            assert_eq!(Modifier::from(CAttribute::Dim), Modifier::DIM);
667            assert_eq!(
668                Modifier::from(CAttribute::NormalIntensity),
669                Modifier::empty()
670            );
671            assert_eq!(
672                Modifier::from(CAttribute::CrossedOut),
673                Modifier::CROSSED_OUT
674            );
675            assert_eq!(Modifier::from(CAttribute::NoUnderline), Modifier::empty());
676            assert_eq!(Modifier::from(CAttribute::OverLined), Modifier::empty());
677            assert_eq!(Modifier::from(CAttribute::SlowBlink), Modifier::SLOW_BLINK);
678            assert_eq!(
679                Modifier::from(CAttribute::RapidBlink),
680                Modifier::RAPID_BLINK
681            );
682            assert_eq!(Modifier::from(CAttribute::Hidden), Modifier::HIDDEN);
683            assert_eq!(Modifier::from(CAttribute::NoHidden), Modifier::empty());
684            assert_eq!(Modifier::from(CAttribute::Reverse), Modifier::REVERSED);
685        }
686
687        #[test]
688        fn from_crossterm_attributes() {
689            assert_eq!(
690                Modifier::from(CAttributes::from(CAttribute::Bold)),
691                Modifier::BOLD
692            );
693            assert_eq!(
694                Modifier::from(CAttributes::from(
695                    [CAttribute::Bold, CAttribute::Italic].as_ref()
696                )),
697                Modifier::BOLD | Modifier::ITALIC
698            );
699            assert_eq!(
700                Modifier::from(CAttributes::from(
701                    [CAttribute::Bold, CAttribute::NotCrossedOut].as_ref()
702                )),
703                Modifier::BOLD
704            );
705            assert_eq!(
706                Modifier::from(CAttributes::from(
707                    [CAttribute::Dim, CAttribute::Underdotted].as_ref()
708                )),
709                Modifier::DIM | Modifier::UNDERLINED
710            );
711            assert_eq!(
712                Modifier::from(CAttributes::from(
713                    [CAttribute::Dim, CAttribute::SlowBlink, CAttribute::Italic].as_ref()
714                )),
715                Modifier::DIM | Modifier::SLOW_BLINK | Modifier::ITALIC
716            );
717            assert_eq!(
718                Modifier::from(CAttributes::from(
719                    [
720                        CAttribute::Hidden,
721                        CAttribute::NoUnderline,
722                        CAttribute::NotCrossedOut
723                    ]
724                    .as_ref()
725                )),
726                Modifier::HIDDEN
727            );
728            assert_eq!(
729                Modifier::from(CAttributes::from(CAttribute::Reverse)),
730                Modifier::REVERSED
731            );
732            assert_eq!(
733                Modifier::from(CAttributes::from(CAttribute::Reset)),
734                Modifier::empty()
735            );
736            assert_eq!(
737                Modifier::from(CAttributes::from(
738                    [CAttribute::RapidBlink, CAttribute::CrossedOut].as_ref()
739                )),
740                Modifier::RAPID_BLINK | Modifier::CROSSED_OUT
741            );
742        }
743    }
744
745    #[test]
746    fn from_crossterm_content_style() {
747        assert_eq!(Style::from(ContentStyle::default()), Style::default());
748        assert_eq!(
749            Style::from(ContentStyle {
750                foreground_color: Some(CColor::DarkYellow),
751                ..Default::default()
752            }),
753            Style::default().fg(Color::Yellow)
754        );
755        assert_eq!(
756            Style::from(ContentStyle {
757                background_color: Some(CColor::DarkYellow),
758                ..Default::default()
759            }),
760            Style::default().bg(Color::Yellow)
761        );
762        assert_eq!(
763            Style::from(ContentStyle {
764                attributes: CAttributes::from(CAttribute::Bold),
765                ..Default::default()
766            }),
767            Style::default().add_modifier(Modifier::BOLD)
768        );
769        assert_eq!(
770            Style::from(ContentStyle {
771                attributes: CAttributes::from(CAttribute::NoBold),
772                ..Default::default()
773            }),
774            Style::default().remove_modifier(Modifier::BOLD)
775        );
776        assert_eq!(
777            Style::from(ContentStyle {
778                attributes: CAttributes::from(CAttribute::Italic),
779                ..Default::default()
780            }),
781            Style::default().add_modifier(Modifier::ITALIC)
782        );
783        assert_eq!(
784            Style::from(ContentStyle {
785                attributes: CAttributes::from(CAttribute::NoItalic),
786                ..Default::default()
787            }),
788            Style::default().remove_modifier(Modifier::ITALIC)
789        );
790        assert_eq!(
791            Style::from(ContentStyle {
792                attributes: CAttributes::from([CAttribute::Bold, CAttribute::Italic].as_ref()),
793                ..Default::default()
794            }),
795            Style::default()
796                .add_modifier(Modifier::BOLD)
797                .add_modifier(Modifier::ITALIC)
798        );
799        assert_eq!(
800            Style::from(ContentStyle {
801                attributes: CAttributes::from([CAttribute::NoBold, CAttribute::NoItalic].as_ref()),
802                ..Default::default()
803            }),
804            Style::default()
805                .remove_modifier(Modifier::BOLD)
806                .remove_modifier(Modifier::ITALIC)
807        );
808    }
809
810    #[test]
811    #[cfg(feature = "underline-color")]
812    fn from_crossterm_content_style_underline() {
813        assert_eq!(
814            Style::from(ContentStyle {
815                underline_color: Some(CColor::DarkRed),
816                ..Default::default()
817            }),
818            Style::default().underline_color(Color::Red)
819        );
820    }
821}