Skip to main content

ratatui_crossterm/
lib.rs

1// show the feature flags in the generated documentation
2#![cfg_attr(docsrs, feature(doc_cfg))]
3#![doc(
4    html_logo_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/logo.png",
5    html_favicon_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/favicon.ico"
6)]
7#![warn(missing_docs)]
8//! This crate provides [`CrosstermBackend`], an implementation of the [`Backend`] trait for the
9//! [Ratatui] library. It uses the [Crossterm] library for all terminal manipulation.
10//!
11//! Most application authors should start with the main [`ratatui`] crate, which re-exports this
12//! backend and provides higher-level setup helpers. Reach for `ratatui-crossterm` directly when
13//! you need to depend on the backend crate itself, choose the Crossterm version explicitly, or
14//! integrate with Crossterm APIs beyond Ratatui's higher-level surface.
15//!
16//! # Crossterm Version and Re-export
17//!
18//! `ratatui-crossterm` requires you to specify a version of the [Crossterm] library to be used.
19//! This is managed via feature flags. The highest enabled feature flag of the available
20//! `crossterm_0_xx` features (e.g., `crossterm_0_28`, `crossterm_0_29`) takes precedence. These
21//! features determine which version of Crossterm is compiled and used by the backend. Feature
22//! unification may mean that any crate in your dependency graph that chooses to depend on a
23//! specific version of Crossterm may be affected by the feature flags you enable.
24//!
25//! Ratatui will support at least the two most recent versions of Crossterm (though we may increase
26//! this if crossterm release cadence increases). We will remove support for older versions in major
27//! (0.x) releases of `ratatui-crossterm`, and we may add support for newer versions in minor
28//! (0.x.y) releases.
29//!
30//! To promote interoperability within the [Ratatui] ecosystem, the selected Crossterm crate is
31//! re-exported as `ratatui_crossterm::crossterm`. This re-export is essential for authors of widget
32//! libraries or any applications that need to perform direct Crossterm operations while ensuring
33//! compatibility with the version used by `ratatui-crossterm`. By using
34//! `ratatui_crossterm::crossterm` for such operations, developers can avoid version conflicts and
35//! ensure that all parts of their application use a consistent set of Crossterm types and
36//! functions.
37//!
38//! For example, if your application's `Cargo.toml` enables the `crossterm_0_29` feature for
39//! `ratatui-crossterm`, then any code using `ratatui_crossterm::crossterm` will refer to the 0.29
40//! version of Crossterm.
41//!
42//! For more information on how to use the backend, see the documentation for the
43//! [`CrosstermBackend`] struct.
44//!
45//! [Ratatui]: https://ratatui.rs
46//! [Crossterm]: https://crates.io/crates/crossterm
47//! [`Backend`]: ratatui_core::backend::Backend
48//!
49//! # Crate Organization
50//!
51//! `ratatui-crossterm` is part of the Ratatui workspace that was modularized in version 0.30.0.
52//! This crate provides the [Crossterm] backend implementation for Ratatui.
53//!
54//! **When to use `ratatui-crossterm`:**
55//!
56//! - You want to depend on the Crossterm backend crate directly
57//! - You need fine-grained control over the selected Crossterm version
58//! - You integrate with Crossterm APIs alongside Ratatui and want the re-exported
59//!   `ratatui_crossterm::crossterm` path
60//!
61//! **When to use the main [`ratatui`] crate:**
62//!
63//! - Building applications
64//! - You want the common Ratatui path that already includes the Crossterm backend by default
65//! - You want the backend and higher-level terminal setup in one crate
66//!
67//! For detailed information about the workspace organization, see [ARCHITECTURE.md].
68//!
69//! [`ratatui`]: https://crates.io/crates/ratatui
70//! [ARCHITECTURE.md]: https://github.com/ratatui/ratatui/blob/main/ARCHITECTURE.md
71#![cfg_attr(feature = "document-features", doc = "\n## Features")]
72#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
73
74use std::io::{self, Write};
75
76use crossterm::cursor::{Hide, MoveTo, Show};
77#[cfg(feature = "underline-color")]
78use crossterm::style::SetUnderlineColor;
79use crossterm::style::{
80    Attribute as CrosstermAttribute, Attributes as CrosstermAttributes, Color as CrosstermColor,
81    Colors as CrosstermColors, ContentStyle, Print, SetAttribute, SetBackgroundColor, SetColors,
82    SetForegroundColor,
83};
84use crossterm::terminal::{self, Clear};
85use crossterm::{execute, queue};
86cfg_if::cfg_if! {
87    // Re-export the selected Crossterm crate making sure to choose the latest version. We do this
88    // to make it possible to easily enable all features when compiling `ratatui-crossterm`.
89    if #[cfg(feature = "crossterm_0_29")] {
90        pub use crossterm_0_29 as crossterm;
91    } else if #[cfg(feature = "crossterm_0_28")] {
92        pub use crossterm_0_28 as crossterm;
93    } else {
94        compile_error!(
95            "At least one crossterm feature must be enabled. See the crate docs for more information."
96        );
97    }
98}
99use ratatui_core::backend::{Backend, ClearType, WindowSize};
100use ratatui_core::buffer::Cell;
101use ratatui_core::layout::{Position, Size};
102use ratatui_core::style::{Color, Modifier, Style};
103
104/// A [`Backend`] implementation that uses [Crossterm] to render to the terminal.
105///
106/// The `CrosstermBackend` struct is a wrapper around a writer implementing [`Write`], which is
107/// used to send commands to the terminal. It provides methods for drawing content, manipulating
108/// the cursor, and clearing the terminal screen.
109///
110/// Most applications should not call the methods on `CrosstermBackend` directly, but will instead
111/// use the [`Terminal`] struct, which provides a more ergonomic interface.
112///
113/// Usually applications will enable raw mode and switch to alternate screen mode after creating
114/// a `CrosstermBackend`. This is done by calling [`crossterm::terminal::enable_raw_mode`] and
115/// [`crossterm::terminal::EnterAlternateScreen`] (and the corresponding disable/leave functions
116/// when the application exits). This is not done automatically by the backend because it is
117/// possible that the application may want to use the terminal for other purposes (like showing
118/// help text) before entering alternate screen mode.
119///
120/// # Example
121///
122/// ```rust,ignore
123/// use std::io::{stderr, stdout};
124///
125/// use crossterm::ExecutableCommand;
126/// use crossterm::terminal::{
127///     EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
128/// };
129/// use ratatui::Terminal;
130/// use ratatui::backend::CrosstermBackend;
131///
132/// let mut backend = CrosstermBackend::new(stdout());
133/// // or
134/// let backend = CrosstermBackend::new(stderr());
135/// let mut terminal = Terminal::new(backend)?;
136///
137/// enable_raw_mode()?;
138/// stdout().execute(EnterAlternateScreen)?;
139///
140/// terminal.clear()?;
141/// terminal.draw(|frame| {
142///     // -- snip --
143/// })?;
144///
145/// stdout().execute(LeaveAlternateScreen)?;
146/// disable_raw_mode()?;
147///
148/// # std::io::Result::Ok(())
149/// ```
150///
151/// See the [Examples] directory for more examples. See the [`backend`] module documentation
152/// for more details on raw mode and alternate screen.
153///
154/// [`Write`]: std::io::Write
155/// [`Terminal`]: https://docs.rs/ratatui/latest/ratatui/struct.Terminal.html
156/// [`backend`]: ratatui_core::backend
157/// [Crossterm]: https://crates.io/crates/crossterm
158/// [Examples]: https://github.com/ratatui/ratatui/tree/main/ratatui/examples/README.md
159#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
160pub struct CrosstermBackend<W: Write> {
161    /// The writer used to send commands to the terminal.
162    writer: W,
163}
164
165impl<W> CrosstermBackend<W>
166where
167    W: Write,
168{
169    /// Creates a new `CrosstermBackend` with the given writer.
170    ///
171    /// Most applications will use either [`stdout`](std::io::stdout) or
172    /// [`stderr`](std::io::stderr) as writer. See the [FAQ] to determine which one to use.
173    ///
174    /// [FAQ]: https://ratatui.rs/faq/#should-i-use-stdout-or-stderr
175    ///
176    /// # Example
177    ///
178    /// ```rust,ignore
179    /// use std::io::stdout;
180    ///
181    /// use ratatui::backend::CrosstermBackend;
182    ///
183    /// let backend = CrosstermBackend::new(stdout());
184    /// ```
185    pub const fn new(writer: W) -> Self {
186        Self { writer }
187    }
188
189    /// Gets the writer.
190    #[instability::unstable(
191        feature = "backend-writer",
192        issue = "https://github.com/ratatui/ratatui/pull/991"
193    )]
194    pub const fn writer(&self) -> &W {
195        &self.writer
196    }
197
198    /// Gets the writer as a mutable reference.
199    ///
200    /// Note: writing to the writer may cause incorrect output after the write. This is due to the
201    /// way that the Terminal implements diffing Buffers.
202    #[instability::unstable(
203        feature = "backend-writer",
204        issue = "https://github.com/ratatui/ratatui/pull/991"
205    )]
206    pub const fn writer_mut(&mut self) -> &mut W {
207        &mut self.writer
208    }
209}
210
211impl<W> Write for CrosstermBackend<W>
212where
213    W: Write,
214{
215    /// Writes a buffer of bytes to the underlying buffer.
216    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
217        self.writer.write(buf)
218    }
219
220    /// Flushes the underlying buffer.
221    fn flush(&mut self) -> io::Result<()> {
222        self.writer.flush()
223    }
224}
225
226impl<W> Backend for CrosstermBackend<W>
227where
228    W: Write,
229{
230    type Error = io::Error;
231
232    fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
233    where
234        I: Iterator<Item = (u16, u16, &'a Cell)>,
235    {
236        let mut fg = Color::Reset;
237        let mut bg = Color::Reset;
238        #[cfg(feature = "underline-color")]
239        let mut underline_color = Color::Reset;
240        let mut modifier = Modifier::empty();
241        let mut last_pos: Option<Position> = None;
242        for (x, y, cell) in content {
243            // Move the cursor if the previous location was not (x - 1, y)
244            if !matches!(last_pos, Some(p) if x == p.x + 1 && y == p.y) {
245                queue!(self.writer, MoveTo(x, y))?;
246            }
247            last_pos = Some(Position { x, y });
248            if cell.modifier != modifier {
249                let diff = ModifierDiff {
250                    from: modifier,
251                    to: cell.modifier,
252                };
253                diff.queue(&mut self.writer)?;
254                modifier = cell.modifier;
255            }
256            if cell.fg != fg || cell.bg != bg {
257                queue!(
258                    self.writer,
259                    SetColors(CrosstermColors::new(
260                        cell.fg.into_crossterm(),
261                        cell.bg.into_crossterm(),
262                    ))
263                )?;
264                fg = cell.fg;
265                bg = cell.bg;
266            }
267            #[cfg(feature = "underline-color")]
268            if cell.underline_color != underline_color {
269                let color = cell.underline_color.into_crossterm();
270                queue!(self.writer, SetUnderlineColor(color))?;
271                underline_color = cell.underline_color;
272            }
273
274            queue!(self.writer, Print(cell.symbol()))?;
275        }
276
277        #[cfg(feature = "underline-color")]
278        return queue!(
279            self.writer,
280            SetForegroundColor(CrosstermColor::Reset),
281            SetBackgroundColor(CrosstermColor::Reset),
282            SetUnderlineColor(CrosstermColor::Reset),
283            SetAttribute(CrosstermAttribute::Reset),
284        );
285        #[cfg(not(feature = "underline-color"))]
286        return queue!(
287            self.writer,
288            SetForegroundColor(CrosstermColor::Reset),
289            SetBackgroundColor(CrosstermColor::Reset),
290            SetAttribute(CrosstermAttribute::Reset),
291        );
292    }
293
294    fn hide_cursor(&mut self) -> io::Result<()> {
295        execute!(self.writer, Hide)
296    }
297
298    fn show_cursor(&mut self) -> io::Result<()> {
299        execute!(self.writer, Show)
300    }
301
302    fn get_cursor_position(&mut self) -> io::Result<Position> {
303        crossterm::cursor::position()
304            .map(|(x, y)| Position { x, y })
305            .map_err(io::Error::other)
306    }
307
308    fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
309        let Position { x, y } = position.into();
310        execute!(self.writer, MoveTo(x, y))
311    }
312
313    fn clear(&mut self) -> io::Result<()> {
314        self.clear_region(ClearType::All)
315    }
316
317    fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
318        execute!(
319            self.writer,
320            Clear(match clear_type {
321                ClearType::All => crossterm::terminal::ClearType::All,
322                ClearType::AfterCursor => crossterm::terminal::ClearType::FromCursorDown,
323                ClearType::BeforeCursor => crossterm::terminal::ClearType::FromCursorUp,
324                ClearType::CurrentLine => crossterm::terminal::ClearType::CurrentLine,
325                ClearType::UntilNewLine => crossterm::terminal::ClearType::UntilNewLine,
326            })
327        )
328    }
329
330    fn append_lines(&mut self, n: u16) -> io::Result<()> {
331        for _ in 0..n {
332            queue!(self.writer, Print("\n"))?;
333        }
334        self.writer.flush()
335    }
336
337    fn size(&self) -> io::Result<Size> {
338        let (width, height) = terminal::size()?;
339        Ok(Size { width, height })
340    }
341
342    fn window_size(&mut self) -> io::Result<WindowSize> {
343        let crossterm::terminal::WindowSize {
344            columns,
345            rows,
346            width,
347            height,
348        } = terminal::window_size()?;
349        Ok(WindowSize {
350            columns_rows: Size {
351                width: columns,
352                height: rows,
353            },
354            pixels: Size { width, height },
355        })
356    }
357
358    fn flush(&mut self) -> io::Result<()> {
359        self.writer.flush()
360    }
361
362    #[cfg(feature = "scrolling-regions")]
363    fn scroll_region_up(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
364        queue!(
365            self.writer,
366            ScrollUpInRegion {
367                first_row: region.start,
368                last_row: region.end.saturating_sub(1),
369                lines_to_scroll: amount,
370            }
371        )?;
372        self.writer.flush()
373    }
374
375    #[cfg(feature = "scrolling-regions")]
376    fn scroll_region_down(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
377        queue!(
378            self.writer,
379            ScrollDownInRegion {
380                first_row: region.start,
381                last_row: region.end.saturating_sub(1),
382                lines_to_scroll: amount,
383            }
384        )?;
385        self.writer.flush()
386    }
387}
388
389/// A trait for converting a Ratatui type to a Crossterm type.
390///
391/// This trait is needed for avoiding the orphan rule when implementing `From` for crossterm types
392/// once these are moved to a separate crate.
393pub trait IntoCrossterm<C> {
394    /// Converts the ratatui type to a crossterm type.
395    fn into_crossterm(self) -> C;
396}
397
398/// A trait for converting a Crossterm type to a Ratatui type.
399///
400/// This trait is needed for avoiding the orphan rule when implementing `From` for crossterm types
401/// once these are moved to a separate crate.
402pub trait FromCrossterm<C> {
403    /// Converts the crossterm type to a ratatui type.
404    fn from_crossterm(value: C) -> Self;
405}
406
407impl IntoCrossterm<CrosstermColor> for Color {
408    fn into_crossterm(self) -> CrosstermColor {
409        match self {
410            Self::Reset => CrosstermColor::Reset,
411            Self::Black => CrosstermColor::Black,
412            Self::Red => CrosstermColor::DarkRed,
413            Self::Green => CrosstermColor::DarkGreen,
414            Self::Yellow => CrosstermColor::DarkYellow,
415            Self::Blue => CrosstermColor::DarkBlue,
416            Self::Magenta => CrosstermColor::DarkMagenta,
417            Self::Cyan => CrosstermColor::DarkCyan,
418            Self::Gray => CrosstermColor::Grey,
419            Self::DarkGray => CrosstermColor::DarkGrey,
420            Self::LightRed => CrosstermColor::Red,
421            Self::LightGreen => CrosstermColor::Green,
422            Self::LightBlue => CrosstermColor::Blue,
423            Self::LightYellow => CrosstermColor::Yellow,
424            Self::LightMagenta => CrosstermColor::Magenta,
425            Self::LightCyan => CrosstermColor::Cyan,
426            Self::White => CrosstermColor::White,
427            Self::Indexed(i) => CrosstermColor::AnsiValue(i),
428            Self::Rgb(r, g, b) => CrosstermColor::Rgb { r, g, b },
429        }
430    }
431}
432
433impl IntoCrossterm<ContentStyle> for Style {
434    fn into_crossterm(self) -> ContentStyle {
435        let mut attributes = CrosstermAttributes::default();
436
437        // Add modifiers
438        if self.add_modifier.contains(Modifier::BOLD) {
439            attributes.set(CrosstermAttribute::Bold);
440        }
441        if self.add_modifier.contains(Modifier::DIM) {
442            attributes.set(CrosstermAttribute::Dim);
443        }
444        if self.add_modifier.contains(Modifier::ITALIC) {
445            attributes.set(CrosstermAttribute::Italic);
446        }
447        if self.add_modifier.contains(Modifier::UNDERLINED) {
448            attributes.set(CrosstermAttribute::Underlined);
449        }
450        if self.add_modifier.contains(Modifier::SLOW_BLINK) {
451            attributes.set(CrosstermAttribute::SlowBlink);
452        }
453        if self.add_modifier.contains(Modifier::RAPID_BLINK) {
454            attributes.set(CrosstermAttribute::RapidBlink);
455        }
456        if self.add_modifier.contains(Modifier::REVERSED) {
457            attributes.set(CrosstermAttribute::Reverse);
458        }
459        if self.add_modifier.contains(Modifier::HIDDEN) {
460            attributes.set(CrosstermAttribute::Hidden);
461        }
462        if self.add_modifier.contains(Modifier::CROSSED_OUT) {
463            attributes.set(CrosstermAttribute::CrossedOut);
464        }
465
466        // Sub modifiers (remove modifiers)
467        if self.sub_modifier.contains(Modifier::BOLD) {
468            attributes.set(CrosstermAttribute::NoBold);
469        }
470        if self.sub_modifier.contains(Modifier::DIM) {
471            attributes.set(CrosstermAttribute::NormalIntensity);
472        }
473        if self.sub_modifier.contains(Modifier::ITALIC) {
474            attributes.set(CrosstermAttribute::NoItalic);
475        }
476        if self.sub_modifier.contains(Modifier::UNDERLINED) {
477            attributes.set(CrosstermAttribute::NoUnderline);
478        }
479        if self.sub_modifier.contains(Modifier::SLOW_BLINK)
480            || self.sub_modifier.contains(Modifier::RAPID_BLINK)
481        {
482            attributes.set(CrosstermAttribute::NoBlink);
483        }
484        if self.sub_modifier.contains(Modifier::REVERSED) {
485            attributes.set(CrosstermAttribute::NoReverse);
486        }
487        if self.sub_modifier.contains(Modifier::HIDDEN) {
488            attributes.set(CrosstermAttribute::NoHidden);
489        }
490        if self.sub_modifier.contains(Modifier::CROSSED_OUT) {
491            attributes.set(CrosstermAttribute::NotCrossedOut);
492        }
493
494        ContentStyle {
495            foreground_color: self.fg.map(IntoCrossterm::into_crossterm),
496            background_color: self.bg.map(IntoCrossterm::into_crossterm),
497            #[cfg(feature = "underline-color")]
498            underline_color: self.underline_color.map(IntoCrossterm::into_crossterm),
499            #[cfg(not(feature = "underline-color"))]
500            underline_color: None,
501            attributes,
502        }
503    }
504}
505
506impl FromCrossterm<CrosstermColor> for Color {
507    fn from_crossterm(value: CrosstermColor) -> Self {
508        match value {
509            CrosstermColor::Reset => Self::Reset,
510            CrosstermColor::Black => Self::Black,
511            CrosstermColor::DarkRed => Self::Red,
512            CrosstermColor::DarkGreen => Self::Green,
513            CrosstermColor::DarkYellow => Self::Yellow,
514            CrosstermColor::DarkBlue => Self::Blue,
515            CrosstermColor::DarkMagenta => Self::Magenta,
516            CrosstermColor::DarkCyan => Self::Cyan,
517            CrosstermColor::Grey => Self::Gray,
518            CrosstermColor::DarkGrey => Self::DarkGray,
519            CrosstermColor::Red => Self::LightRed,
520            CrosstermColor::Green => Self::LightGreen,
521            CrosstermColor::Blue => Self::LightBlue,
522            CrosstermColor::Yellow => Self::LightYellow,
523            CrosstermColor::Magenta => Self::LightMagenta,
524            CrosstermColor::Cyan => Self::LightCyan,
525            CrosstermColor::White => Self::White,
526            CrosstermColor::Rgb { r, g, b } => Self::Rgb(r, g, b),
527            CrosstermColor::AnsiValue(v) => Self::Indexed(v),
528        }
529    }
530}
531
532/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
533/// values. This is useful when updating the terminal display, as it allows for more
534/// efficient updates by only sending the necessary changes.
535struct ModifierDiff {
536    pub from: Modifier,
537    pub to: Modifier,
538}
539
540impl ModifierDiff {
541    fn queue<W>(self, mut w: W) -> io::Result<()>
542    where
543        W: io::Write,
544    {
545        let removed = self.from - self.to;
546        if removed.contains(Modifier::REVERSED) {
547            queue!(w, SetAttribute(CrosstermAttribute::NoReverse))?;
548        }
549
550        let reset_intensity = removed.contains(Modifier::BOLD) || removed.contains(Modifier::DIM);
551        if reset_intensity {
552            // Bold and Dim are both reset by applying the Normal intensity
553            queue!(w, SetAttribute(CrosstermAttribute::NormalIntensity))?;
554
555            // The remaining Bold and Dim attributes must be
556            // reapplied after the intensity reset above.
557            if self.to.contains(Modifier::DIM) {
558                queue!(w, SetAttribute(CrosstermAttribute::Dim))?;
559            }
560
561            if self.to.contains(Modifier::BOLD) {
562                queue!(w, SetAttribute(CrosstermAttribute::Bold))?;
563            }
564        }
565
566        if removed.contains(Modifier::ITALIC) {
567            queue!(w, SetAttribute(CrosstermAttribute::NoItalic))?;
568        }
569        if removed.contains(Modifier::UNDERLINED) {
570            queue!(w, SetAttribute(CrosstermAttribute::NoUnderline))?;
571        }
572        if removed.contains(Modifier::CROSSED_OUT) {
573            queue!(w, SetAttribute(CrosstermAttribute::NotCrossedOut))?;
574        }
575        if removed.contains(Modifier::HIDDEN) {
576            queue!(w, SetAttribute(CrosstermAttribute::NoHidden))?;
577        }
578        if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
579            queue!(w, SetAttribute(CrosstermAttribute::NoBlink))?;
580        }
581
582        let added = self.to - self.from;
583        if added.contains(Modifier::REVERSED) {
584            queue!(w, SetAttribute(CrosstermAttribute::Reverse))?;
585        }
586        if added.contains(Modifier::BOLD) && !reset_intensity {
587            queue!(w, SetAttribute(CrosstermAttribute::Bold))?;
588        }
589        if added.contains(Modifier::ITALIC) {
590            queue!(w, SetAttribute(CrosstermAttribute::Italic))?;
591        }
592        if added.contains(Modifier::UNDERLINED) {
593            queue!(w, SetAttribute(CrosstermAttribute::Underlined))?;
594        }
595        if added.contains(Modifier::DIM) && !reset_intensity {
596            queue!(w, SetAttribute(CrosstermAttribute::Dim))?;
597        }
598        if added.contains(Modifier::CROSSED_OUT) {
599            queue!(w, SetAttribute(CrosstermAttribute::CrossedOut))?;
600        }
601        if added.contains(Modifier::HIDDEN) {
602            queue!(w, SetAttribute(CrosstermAttribute::Hidden))?;
603        }
604        if added.contains(Modifier::SLOW_BLINK) {
605            queue!(w, SetAttribute(CrosstermAttribute::SlowBlink))?;
606        }
607        if added.contains(Modifier::RAPID_BLINK) {
608            queue!(w, SetAttribute(CrosstermAttribute::RapidBlink))?;
609        }
610
611        Ok(())
612    }
613}
614
615impl FromCrossterm<CrosstermAttribute> for Modifier {
616    fn from_crossterm(value: CrosstermAttribute) -> Self {
617        // `Attribute*s*` (note the *s*) contains multiple `Attribute` We convert `Attribute` to
618        // `Attribute*s*` (containing only 1 value) to avoid implementing the conversion again
619        Self::from_crossterm(CrosstermAttributes::from(value))
620    }
621}
622
623impl FromCrossterm<CrosstermAttributes> for Modifier {
624    fn from_crossterm(value: CrosstermAttributes) -> Self {
625        let mut res = Self::empty();
626        if value.has(CrosstermAttribute::Bold) {
627            res |= Self::BOLD;
628        }
629        if value.has(CrosstermAttribute::Dim) {
630            res |= Self::DIM;
631        }
632        if value.has(CrosstermAttribute::Italic) {
633            res |= Self::ITALIC;
634        }
635        if value.has(CrosstermAttribute::Underlined)
636            || value.has(CrosstermAttribute::DoubleUnderlined)
637            || value.has(CrosstermAttribute::Undercurled)
638            || value.has(CrosstermAttribute::Underdotted)
639            || value.has(CrosstermAttribute::Underdashed)
640        {
641            res |= Self::UNDERLINED;
642        }
643        if value.has(CrosstermAttribute::SlowBlink) {
644            res |= Self::SLOW_BLINK;
645        }
646        if value.has(CrosstermAttribute::RapidBlink) {
647            res |= Self::RAPID_BLINK;
648        }
649        if value.has(CrosstermAttribute::Reverse) {
650            res |= Self::REVERSED;
651        }
652        if value.has(CrosstermAttribute::Hidden) {
653            res |= Self::HIDDEN;
654        }
655        if value.has(CrosstermAttribute::CrossedOut) {
656            res |= Self::CROSSED_OUT;
657        }
658        res
659    }
660}
661
662impl FromCrossterm<ContentStyle> for Style {
663    fn from_crossterm(value: ContentStyle) -> Self {
664        let mut sub_modifier = Modifier::empty();
665        if value.attributes.has(CrosstermAttribute::NoBold) {
666            sub_modifier |= Modifier::BOLD;
667        }
668        if value.attributes.has(CrosstermAttribute::NoItalic) {
669            sub_modifier |= Modifier::ITALIC;
670        }
671        if value.attributes.has(CrosstermAttribute::NotCrossedOut) {
672            sub_modifier |= Modifier::CROSSED_OUT;
673        }
674        if value.attributes.has(CrosstermAttribute::NoUnderline) {
675            sub_modifier |= Modifier::UNDERLINED;
676        }
677        if value.attributes.has(CrosstermAttribute::NoHidden) {
678            sub_modifier |= Modifier::HIDDEN;
679        }
680        if value.attributes.has(CrosstermAttribute::NoBlink) {
681            sub_modifier |= Modifier::RAPID_BLINK | Modifier::SLOW_BLINK;
682        }
683        if value.attributes.has(CrosstermAttribute::NoReverse) {
684            sub_modifier |= Modifier::REVERSED;
685        }
686
687        Self {
688            fg: value.foreground_color.map(FromCrossterm::from_crossterm),
689            bg: value.background_color.map(FromCrossterm::from_crossterm),
690            #[cfg(feature = "underline-color")]
691            underline_color: value.underline_color.map(FromCrossterm::from_crossterm),
692            add_modifier: Modifier::from_crossterm(value.attributes),
693            sub_modifier,
694        }
695    }
696}
697
698/// A command that scrolls the terminal screen a given number of rows up in a specific scrolling
699/// region.
700///
701/// This will hopefully be replaced by a struct in crossterm proper. There are two outstanding
702/// crossterm PRs that will address this:
703///   - [918](https://github.com/crossterm-rs/crossterm/pull/918)
704///   - [923](https://github.com/crossterm-rs/crossterm/pull/923)
705#[cfg(feature = "scrolling-regions")]
706#[derive(Debug, Clone, Copy, PartialEq, Eq)]
707struct ScrollUpInRegion {
708    /// The first row of the scrolling region.
709    pub first_row: u16,
710
711    /// The last row of the scrolling region.
712    pub last_row: u16,
713
714    /// The number of lines to scroll up by.
715    pub lines_to_scroll: u16,
716}
717
718#[cfg(feature = "scrolling-regions")]
719impl crate::crossterm::Command for ScrollUpInRegion {
720    fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
721        if self.lines_to_scroll != 0 {
722            // Set a scrolling region that contains just the desired lines.
723            write!(
724                f,
725                crate::crossterm::csi!("{};{}r"),
726                self.first_row.saturating_add(1),
727                self.last_row.saturating_add(1)
728            )?;
729            // Scroll the region by the desired count.
730            write!(f, crate::crossterm::csi!("{}S"), self.lines_to_scroll)?;
731            // Reset the scrolling region to be the whole screen.
732            write!(f, crate::crossterm::csi!("r"))?;
733        }
734        Ok(())
735    }
736
737    #[cfg(windows)]
738    fn execute_winapi(&self) -> io::Result<()> {
739        Err(io::Error::new(
740            io::ErrorKind::Unsupported,
741            "ScrollUpInRegion command not supported for winapi",
742        ))
743    }
744}
745
746/// A command that scrolls the terminal screen a given number of rows down in a specific scrolling
747/// region.
748///
749/// This will hopefully be replaced by a struct in crossterm proper. There are two outstanding
750/// crossterm PRs that will address this:
751///   - [918](https://github.com/crossterm-rs/crossterm/pull/918)
752///   - [923](https://github.com/crossterm-rs/crossterm/pull/923)
753#[cfg(feature = "scrolling-regions")]
754#[derive(Debug, Clone, Copy, PartialEq, Eq)]
755struct ScrollDownInRegion {
756    /// The first row of the scrolling region.
757    pub first_row: u16,
758
759    /// The last row of the scrolling region.
760    pub last_row: u16,
761
762    /// The number of lines to scroll down by.
763    pub lines_to_scroll: u16,
764}
765
766#[cfg(feature = "scrolling-regions")]
767impl crate::crossterm::Command for ScrollDownInRegion {
768    fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
769        if self.lines_to_scroll != 0 {
770            // Set a scrolling region that contains just the desired lines.
771            write!(
772                f,
773                crate::crossterm::csi!("{};{}r"),
774                self.first_row.saturating_add(1),
775                self.last_row.saturating_add(1)
776            )?;
777            // Scroll the region by the desired count.
778            write!(f, crate::crossterm::csi!("{}T"), self.lines_to_scroll)?;
779            // Reset the scrolling region to be the whole screen.
780            write!(f, crate::crossterm::csi!("r"))?;
781        }
782        Ok(())
783    }
784
785    #[cfg(windows)]
786    fn execute_winapi(&self) -> io::Result<()> {
787        Err(io::Error::new(
788            io::ErrorKind::Unsupported,
789            "ScrollDownInRegion command not supported for winapi",
790        ))
791    }
792}
793
794#[cfg(test)]
795mod tests {
796    use rstest::rstest;
797
798    use super::*;
799
800    #[rstest]
801    #[case(CrosstermColor::Reset, Color::Reset)]
802    #[case(CrosstermColor::Black, Color::Black)]
803    #[case(CrosstermColor::DarkGrey, Color::DarkGray)]
804    #[case(CrosstermColor::Red, Color::LightRed)]
805    #[case(CrosstermColor::DarkRed, Color::Red)]
806    #[case(CrosstermColor::Green, Color::LightGreen)]
807    #[case(CrosstermColor::DarkGreen, Color::Green)]
808    #[case(CrosstermColor::Yellow, Color::LightYellow)]
809    #[case(CrosstermColor::DarkYellow, Color::Yellow)]
810    #[case(CrosstermColor::Blue, Color::LightBlue)]
811    #[case(CrosstermColor::DarkBlue, Color::Blue)]
812    #[case(CrosstermColor::Magenta, Color::LightMagenta)]
813    #[case(CrosstermColor::DarkMagenta, Color::Magenta)]
814    #[case(CrosstermColor::Cyan, Color::LightCyan)]
815    #[case(CrosstermColor::DarkCyan, Color::Cyan)]
816    #[case(CrosstermColor::White, Color::White)]
817    #[case(CrosstermColor::Grey, Color::Gray)]
818    #[case(CrosstermColor::Rgb { r: 0, g: 0, b: 0 }, Color::Rgb(0, 0, 0) )]
819    #[case(CrosstermColor::Rgb { r: 10, g: 20, b: 30 }, Color::Rgb(10, 20, 30) )]
820    #[case(CrosstermColor::AnsiValue(32), Color::Indexed(32))]
821    #[case(CrosstermColor::AnsiValue(37), Color::Indexed(37))]
822    fn from_crossterm_color(#[case] crossterm_color: CrosstermColor, #[case] color: Color) {
823        assert_eq!(Color::from_crossterm(crossterm_color), color);
824    }
825
826    #[rstest]
827    #[case(Modifier::BOLD, Modifier::BOLD | Modifier::HIDDEN, &[CrosstermAttribute::Hidden])]
828    #[case(Modifier::BOLD, Modifier::DIM, &[CrosstermAttribute::NormalIntensity, CrosstermAttribute::Dim])]
829    #[case(Modifier::CROSSED_OUT, Modifier::empty(), &[CrosstermAttribute::NotCrossedOut])]
830    #[case(Modifier::DIM, Modifier::BOLD, &[CrosstermAttribute::NormalIntensity, CrosstermAttribute::Bold])]
831    #[case(Modifier::HIDDEN | Modifier::CROSSED_OUT, Modifier::CROSSED_OUT, &[CrosstermAttribute::NoHidden])]
832    #[case(Modifier::HIDDEN | Modifier::DIM, Modifier::BOLD | Modifier::DIM, &[CrosstermAttribute::NoHidden, CrosstermAttribute::Bold])]
833    #[case(Modifier::HIDDEN, Modifier::HIDDEN, &[])]
834    #[case(Modifier::HIDDEN, Modifier::empty(), &[CrosstermAttribute::NoHidden])]
835    #[case(Modifier::REVERSED, Modifier::empty(), &[CrosstermAttribute::NoReverse])]
836    #[case(Modifier::SLOW_BLINK, Modifier::RAPID_BLINK, &[CrosstermAttribute::NoBlink, CrosstermAttribute::RapidBlink])]
837    #[case(Modifier::empty(), Modifier::CROSSED_OUT, &[CrosstermAttribute::CrossedOut])]
838    #[case(Modifier::empty(), Modifier::HIDDEN, &[CrosstermAttribute::Hidden])]
839    #[case(Modifier::empty(), Modifier::REVERSED, &[CrosstermAttribute::Reverse])]
840    fn queue_modifier_diff(
841        #[case] from: Modifier,
842        #[case] to: Modifier,
843        #[case] expected_attributes: &[CrosstermAttribute],
844    ) -> io::Result<()> {
845        let mut actual = Vec::new();
846        ModifierDiff { from, to }.queue(&mut actual)?;
847
848        let mut expected = Vec::new();
849        for attribute in expected_attributes {
850            queue!(&mut expected, SetAttribute(*attribute))?;
851        }
852
853        assert_eq!(actual, expected);
854
855        Ok(())
856    }
857
858    mod modifier {
859        use super::*;
860
861        #[rstest]
862        #[case(CrosstermAttribute::Reset, Modifier::empty())]
863        #[case(CrosstermAttribute::Bold, Modifier::BOLD)]
864        #[case(CrosstermAttribute::NoBold, Modifier::empty())]
865        #[case(CrosstermAttribute::Italic, Modifier::ITALIC)]
866        #[case(CrosstermAttribute::NoItalic, Modifier::empty())]
867        #[case(CrosstermAttribute::Underlined, Modifier::UNDERLINED)]
868        #[case(CrosstermAttribute::NoUnderline, Modifier::empty())]
869        #[case(CrosstermAttribute::OverLined, Modifier::empty())]
870        #[case(CrosstermAttribute::NotOverLined, Modifier::empty())]
871        #[case(CrosstermAttribute::DoubleUnderlined, Modifier::UNDERLINED)]
872        #[case(CrosstermAttribute::Undercurled, Modifier::UNDERLINED)]
873        #[case(CrosstermAttribute::Underdotted, Modifier::UNDERLINED)]
874        #[case(CrosstermAttribute::Underdashed, Modifier::UNDERLINED)]
875        #[case(CrosstermAttribute::Dim, Modifier::DIM)]
876        #[case(CrosstermAttribute::NormalIntensity, Modifier::empty())]
877        #[case(CrosstermAttribute::CrossedOut, Modifier::CROSSED_OUT)]
878        #[case(CrosstermAttribute::NotCrossedOut, Modifier::empty())]
879        #[case(CrosstermAttribute::NoUnderline, Modifier::empty())]
880        #[case(CrosstermAttribute::SlowBlink, Modifier::SLOW_BLINK)]
881        #[case(CrosstermAttribute::RapidBlink, Modifier::RAPID_BLINK)]
882        #[case(CrosstermAttribute::Hidden, Modifier::HIDDEN)]
883        #[case(CrosstermAttribute::NoHidden, Modifier::empty())]
884        #[case(CrosstermAttribute::Reverse, Modifier::REVERSED)]
885        #[case(CrosstermAttribute::NoReverse, Modifier::empty())]
886        fn from_crossterm_attribute(
887            #[case] crossterm_attribute: CrosstermAttribute,
888            #[case] ratatui_modifier: Modifier,
889        ) {
890            assert_eq!(
891                Modifier::from_crossterm(crossterm_attribute),
892                ratatui_modifier
893            );
894        }
895
896        #[rstest]
897        #[case(&[CrosstermAttribute::Bold], Modifier::BOLD)]
898        #[case(&[CrosstermAttribute::Bold, CrosstermAttribute::Italic], Modifier::BOLD | Modifier::ITALIC)]
899        #[case(&[CrosstermAttribute::Bold, CrosstermAttribute::NotCrossedOut], Modifier::BOLD)]
900        #[case(&[CrosstermAttribute::Dim, CrosstermAttribute::Underdotted], Modifier::DIM | Modifier::UNDERLINED)]
901        #[case(&[CrosstermAttribute::Dim, CrosstermAttribute::SlowBlink, CrosstermAttribute::Italic], Modifier::DIM | Modifier::SLOW_BLINK | Modifier::ITALIC)]
902        #[case(&[CrosstermAttribute::Hidden, CrosstermAttribute::NoUnderline, CrosstermAttribute::NotCrossedOut], Modifier::HIDDEN)]
903        #[case(&[CrosstermAttribute::Reverse], Modifier::REVERSED)]
904        #[case(&[CrosstermAttribute::Reset], Modifier::empty())]
905        #[case(&[CrosstermAttribute::RapidBlink, CrosstermAttribute::CrossedOut], Modifier::RAPID_BLINK | Modifier::CROSSED_OUT)]
906        fn from_crossterm_attributes(
907            #[case] crossterm_attributes: &[CrosstermAttribute],
908            #[case] ratatui_modifier: Modifier,
909        ) {
910            assert_eq!(
911                Modifier::from_crossterm(CrosstermAttributes::from(crossterm_attributes)),
912                ratatui_modifier
913            );
914        }
915    }
916
917    #[rstest]
918    #[case(ContentStyle::default(), Style::default())]
919    #[case(
920        ContentStyle {
921            foreground_color: Some(CrosstermColor::DarkYellow),
922            ..Default::default()
923        },
924        Style::default().fg(Color::Yellow)
925    )]
926    #[case(
927        ContentStyle {
928            background_color: Some(CrosstermColor::DarkYellow),
929            ..Default::default()
930        },
931        Style::default().bg(Color::Yellow)
932    )]
933    #[case(
934        ContentStyle {
935            attributes: CrosstermAttributes::from(CrosstermAttribute::Bold),
936            ..Default::default()
937        },
938        Style::default().add_modifier(Modifier::BOLD)
939    )]
940    #[case(
941        ContentStyle {
942            attributes: CrosstermAttributes::from(CrosstermAttribute::NoBold),
943            ..Default::default()
944        },
945        Style::default().remove_modifier(Modifier::BOLD)
946    )]
947    #[case(
948        ContentStyle {
949            attributes: CrosstermAttributes::from(CrosstermAttribute::Italic),
950            ..Default::default()
951        },
952        Style::default().add_modifier(Modifier::ITALIC)
953    )]
954    #[case(
955        ContentStyle {
956            attributes: CrosstermAttributes::from(CrosstermAttribute::NoItalic),
957            ..Default::default()
958        },
959        Style::default().remove_modifier(Modifier::ITALIC)
960    )]
961    #[case(
962        ContentStyle {
963            attributes: CrosstermAttributes::from(
964                [CrosstermAttribute::Bold, CrosstermAttribute::Italic].as_ref()
965            ),
966            ..Default::default()
967        },
968        Style::default()
969            .add_modifier(Modifier::BOLD)
970            .add_modifier(Modifier::ITALIC)
971    )]
972    #[case(
973        ContentStyle {
974            attributes: CrosstermAttributes::from(
975                [CrosstermAttribute::NoBold, CrosstermAttribute::NoItalic].as_ref()
976            ),
977            ..Default::default()
978        },
979        Style::default()
980            .remove_modifier(Modifier::BOLD)
981            .remove_modifier(Modifier::ITALIC)
982    )]
983    fn from_crossterm_content_style(#[case] content_style: ContentStyle, #[case] style: Style) {
984        assert_eq!(Style::from_crossterm(content_style), style);
985    }
986
987    #[test]
988    #[cfg(feature = "underline-color")]
989    fn from_crossterm_content_style_underline() {
990        let content_style = ContentStyle {
991            underline_color: Some(CrosstermColor::DarkRed),
992            ..Default::default()
993        };
994        assert_eq!(
995            Style::from_crossterm(content_style),
996            Style::default().underline_color(Color::Red)
997        );
998    }
999
1000    #[rstest]
1001    #[case(Style::default(), ContentStyle::default())]
1002    #[case(
1003        Style::default().fg(Color::Yellow),
1004        ContentStyle {
1005            foreground_color: Some(CrosstermColor::DarkYellow),
1006            ..Default::default()
1007        }
1008    )]
1009    #[case(
1010        Style::default().bg(Color::Yellow),
1011        ContentStyle {
1012            background_color: Some(CrosstermColor::DarkYellow),
1013            ..Default::default()
1014        }
1015    )]
1016    #[case(
1017        Style::default().add_modifier(Modifier::BOLD),
1018        ContentStyle {
1019            attributes: CrosstermAttributes::from(CrosstermAttribute::Bold),
1020            ..Default::default()
1021        }
1022    )]
1023    #[case(
1024        Style::default().remove_modifier(Modifier::BOLD),
1025        ContentStyle {
1026            attributes: CrosstermAttributes::from(CrosstermAttribute::NoBold),
1027            ..Default::default()
1028        }
1029    )]
1030    #[case(
1031        Style::default().add_modifier(Modifier::ITALIC),
1032        ContentStyle {
1033            attributes: CrosstermAttributes::from(CrosstermAttribute::Italic),
1034            ..Default::default()
1035        }
1036    )]
1037    #[case(
1038        Style::default().remove_modifier(Modifier::ITALIC),
1039        ContentStyle {
1040            attributes: CrosstermAttributes::from(CrosstermAttribute::NoItalic),
1041            ..Default::default()
1042        }
1043    )]
1044    #[case(
1045        Style::default().add_modifier(Modifier::UNDERLINED),
1046        ContentStyle {
1047            attributes: CrosstermAttributes::from(CrosstermAttribute::Underlined),
1048            ..Default::default()
1049        }
1050    )]
1051    #[case(
1052        Style::default().remove_modifier(Modifier::UNDERLINED),
1053        ContentStyle {
1054            attributes: CrosstermAttributes::from(CrosstermAttribute::NoUnderline),
1055            ..Default::default()
1056        }
1057    )]
1058    #[case(
1059        Style::default().add_modifier(Modifier::DIM),
1060        ContentStyle {
1061            attributes: CrosstermAttributes::from(CrosstermAttribute::Dim),
1062            ..Default::default()
1063        }
1064    )]
1065    #[case(
1066        Style::default().remove_modifier(Modifier::DIM),
1067        ContentStyle {
1068            attributes: CrosstermAttributes::from(CrosstermAttribute::NormalIntensity),
1069            ..Default::default()
1070        }
1071    )]
1072    #[case(
1073        Style::default().add_modifier(Modifier::SLOW_BLINK),
1074        ContentStyle {
1075            attributes: CrosstermAttributes::from(CrosstermAttribute::SlowBlink),
1076            ..Default::default()
1077        }
1078    )]
1079    #[case(
1080        Style::default().add_modifier(Modifier::RAPID_BLINK),
1081        ContentStyle {
1082            attributes: CrosstermAttributes::from(CrosstermAttribute::RapidBlink),
1083            ..Default::default()
1084        }
1085    )]
1086    #[case(
1087        Style::default().remove_modifier(Modifier::SLOW_BLINK),
1088        ContentStyle {
1089            attributes: CrosstermAttributes::from(CrosstermAttribute::NoBlink),
1090            ..Default::default()
1091        }
1092    )]
1093    #[case(
1094        Style::default().add_modifier(Modifier::REVERSED),
1095        ContentStyle {
1096            attributes: CrosstermAttributes::from(CrosstermAttribute::Reverse),
1097            ..Default::default()
1098        }
1099    )]
1100    #[case(
1101        Style::default().remove_modifier(Modifier::REVERSED),
1102        ContentStyle {
1103            attributes: CrosstermAttributes::from(CrosstermAttribute::NoReverse),
1104            ..Default::default()
1105        }
1106    )]
1107    #[case(
1108        Style::default().add_modifier(Modifier::HIDDEN),
1109        ContentStyle {
1110            attributes: CrosstermAttributes::from(CrosstermAttribute::Hidden),
1111            ..Default::default()
1112        }
1113    )]
1114    #[case(
1115        Style::default().remove_modifier(Modifier::HIDDEN),
1116        ContentStyle {
1117            attributes: CrosstermAttributes::from(CrosstermAttribute::NoHidden),
1118            ..Default::default()
1119        }
1120    )]
1121    #[case(
1122        Style::default().add_modifier(Modifier::CROSSED_OUT),
1123        ContentStyle {
1124            attributes: CrosstermAttributes::from(CrosstermAttribute::CrossedOut),
1125            ..Default::default()
1126        }
1127    )]
1128    #[case(
1129        Style::default().remove_modifier(Modifier::CROSSED_OUT),
1130        ContentStyle {
1131            attributes: CrosstermAttributes::from(CrosstermAttribute::NotCrossedOut),
1132            ..Default::default()
1133        }
1134    )]
1135    #[case(
1136        Style::default()
1137            .add_modifier(Modifier::BOLD)
1138            .add_modifier(Modifier::ITALIC),
1139        ContentStyle {
1140            attributes: CrosstermAttributes::from(
1141                [CrosstermAttribute::Bold, CrosstermAttribute::Italic].as_ref()
1142            ),
1143            ..Default::default()
1144        }
1145    )]
1146    #[case(
1147        Style::default()
1148            .remove_modifier(Modifier::BOLD)
1149            .remove_modifier(Modifier::ITALIC),
1150        ContentStyle {
1151            attributes: CrosstermAttributes::from(
1152                [CrosstermAttribute::NoBold, CrosstermAttribute::NoItalic].as_ref()
1153            ),
1154            ..Default::default()
1155        }
1156    )]
1157    fn into_crossterm_content_style(#[case] style: Style, #[case] content_style: ContentStyle) {
1158        assert_eq!(style.into_crossterm(), content_style);
1159    }
1160
1161    #[test]
1162    #[cfg(feature = "underline-color")]
1163    fn into_crossterm_content_style_underline() {
1164        let style = Style::default().underline_color(Color::Red);
1165        let content_style = ContentStyle {
1166            underline_color: Some(CrosstermColor::DarkRed),
1167            ..Default::default()
1168        };
1169        assert_eq!(style.into_crossterm(), content_style);
1170    }
1171}