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")]
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 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#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
160pub struct CrosstermBackend<W: Write> {
161 writer: W,
163}
164
165impl<W> CrosstermBackend<W>
166where
167 W: Write,
168{
169 pub const fn new(writer: W) -> Self {
186 Self { writer }
187 }
188
189 #[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 #[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 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
217 self.writer.write(buf)
218 }
219
220 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 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
389pub trait IntoCrossterm<C> {
394 fn into_crossterm(self) -> C;
396}
397
398pub trait FromCrossterm<C> {
403 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 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 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
532struct 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 queue!(w, SetAttribute(CrosstermAttribute::NormalIntensity))?;
554
555 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 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#[cfg(feature = "scrolling-regions")]
706#[derive(Debug, Clone, Copy, PartialEq, Eq)]
707struct ScrollUpInRegion {
708 pub first_row: u16,
710
711 pub last_row: u16,
713
714 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 write!(
724 f,
725 crate::crossterm::csi!("{};{}r"),
726 self.first_row.saturating_add(1),
727 self.last_row.saturating_add(1)
728 )?;
729 write!(f, crate::crossterm::csi!("{}S"), self.lines_to_scroll)?;
731 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#[cfg(feature = "scrolling-regions")]
754#[derive(Debug, Clone, Copy, PartialEq, Eq)]
755struct ScrollDownInRegion {
756 pub first_row: u16,
758
759 pub last_row: u16,
761
762 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 write!(
772 f,
773 crate::crossterm::csi!("{};{}r"),
774 self.first_row.saturating_add(1),
775 self.last_row.saturating_add(1)
776 )?;
777 write!(f, crate::crossterm::csi!("{}T"), self.lines_to_scroll)?;
779 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}