1#![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#![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 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#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
153pub struct CrosstermBackend<W: Write> {
154 writer: W,
156}
157
158impl<W> CrosstermBackend<W>
159where
160 W: Write,
161{
162 pub const fn new(writer: W) -> Self {
179 Self { writer }
180 }
181
182 #[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 #[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 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
210 self.writer.write(buf)
211 }
212
213 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 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
382pub trait IntoCrossterm<C> {
387 fn into_crossterm(self) -> C;
389}
390
391pub trait FromCrossterm<C> {
396 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
452struct 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 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 queue!(w, SetAttribute(CrosstermAttribute::NormalIntensity))?;
473
474 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 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#[cfg(feature = "scrolling-regions")]
618#[derive(Debug, Clone, Copy, PartialEq, Eq)]
619struct ScrollUpInRegion {
620 pub first_row: u16,
622
623 pub last_row: u16,
625
626 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 write!(
636 f,
637 crate::crossterm::csi!("{};{}r"),
638 self.first_row.saturating_add(1),
639 self.last_row.saturating_add(1)
640 )?;
641 write!(f, crate::crossterm::csi!("{}S"), self.lines_to_scroll)?;
643 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#[cfg(feature = "scrolling-regions")]
666#[derive(Debug, Clone, Copy, PartialEq, Eq)]
667struct ScrollDownInRegion {
668 pub first_row: u16,
670
671 pub last_row: u16,
673
674 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 write!(
684 f,
685 crate::crossterm::csi!("{};{}r"),
686 self.first_row.saturating_add(1),
687 self.last_row.saturating_add(1)
688 )?;
689 write!(f, crate::crossterm::csi!("{}T"), self.lines_to_scroll)?;
691 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}