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