1use std::io::{self, Write};
6
7#[cfg(feature = "underline-color")]
8use crossterm::style::SetUnderlineColor;
9
10use crate::{
11 backend::{Backend, ClearType, WindowSize},
12 buffer::Cell,
13 crossterm::{
14 cursor::{Hide, MoveTo, Show},
15 execute, queue,
16 style::{
17 Attribute as CAttribute, Attributes as CAttributes, Color as CColor, Colors,
18 ContentStyle, Print, SetAttribute, SetBackgroundColor, SetColors, SetForegroundColor,
19 },
20 terminal::{self, Clear},
21 },
22 layout::{Position, Size},
23 style::{Color, Modifier, Style},
24};
25
26#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
81pub struct CrosstermBackend<W: Write> {
82 writer: W,
84}
85
86impl<W> CrosstermBackend<W>
87where
88 W: Write,
89{
90 pub const fn new(writer: W) -> Self {
107 Self { writer }
108 }
109
110 #[instability::unstable(
112 feature = "backend-writer",
113 issue = "https://github.com/ratatui/ratatui/pull/991"
114 )]
115 pub const fn writer(&self) -> &W {
116 &self.writer
117 }
118
119 #[instability::unstable(
124 feature = "backend-writer",
125 issue = "https://github.com/ratatui/ratatui/pull/991"
126 )]
127 pub fn writer_mut(&mut self) -> &mut W {
128 &mut self.writer
129 }
130}
131
132impl<W> Write for CrosstermBackend<W>
133where
134 W: Write,
135{
136 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
138 self.writer.write(buf)
139 }
140
141 fn flush(&mut self) -> io::Result<()> {
143 self.writer.flush()
144 }
145}
146
147impl<W> Backend for CrosstermBackend<W>
148where
149 W: Write,
150{
151 fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
152 where
153 I: Iterator<Item = (u16, u16, &'a Cell)>,
154 {
155 let mut fg = Color::Reset;
156 let mut bg = Color::Reset;
157 #[cfg(feature = "underline-color")]
158 let mut underline_color = Color::Reset;
159 let mut modifier = Modifier::empty();
160 let mut last_pos: Option<Position> = None;
161 for (x, y, cell) in content {
162 if !matches!(last_pos, Some(p) if x == p.x + 1 && y == p.y) {
164 queue!(self.writer, MoveTo(x, y))?;
165 }
166 last_pos = Some(Position { x, y });
167 if cell.modifier != modifier {
168 let diff = ModifierDiff {
169 from: modifier,
170 to: cell.modifier,
171 };
172 diff.queue(&mut self.writer)?;
173 modifier = cell.modifier;
174 }
175 if cell.fg != fg || cell.bg != bg {
176 queue!(
177 self.writer,
178 SetColors(Colors::new(cell.fg.into(), cell.bg.into()))
179 )?;
180 fg = cell.fg;
181 bg = cell.bg;
182 }
183 #[cfg(feature = "underline-color")]
184 if cell.underline_color != underline_color {
185 let color = CColor::from(cell.underline_color);
186 queue!(self.writer, SetUnderlineColor(color))?;
187 underline_color = cell.underline_color;
188 }
189
190 queue!(self.writer, Print(cell.symbol()))?;
191 }
192
193 #[cfg(feature = "underline-color")]
194 return queue!(
195 self.writer,
196 SetForegroundColor(CColor::Reset),
197 SetBackgroundColor(CColor::Reset),
198 SetUnderlineColor(CColor::Reset),
199 SetAttribute(CAttribute::Reset),
200 );
201 #[cfg(not(feature = "underline-color"))]
202 return queue!(
203 self.writer,
204 SetForegroundColor(CColor::Reset),
205 SetBackgroundColor(CColor::Reset),
206 SetAttribute(CAttribute::Reset),
207 );
208 }
209
210 fn hide_cursor(&mut self) -> io::Result<()> {
211 execute!(self.writer, Hide)
212 }
213
214 fn show_cursor(&mut self) -> io::Result<()> {
215 execute!(self.writer, Show)
216 }
217
218 fn get_cursor_position(&mut self) -> io::Result<Position> {
219 crossterm::cursor::position()
220 .map(|(x, y)| Position { x, y })
221 .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
222 }
223
224 fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
225 let Position { x, y } = position.into();
226 execute!(self.writer, MoveTo(x, y))
227 }
228
229 fn clear(&mut self) -> io::Result<()> {
230 self.clear_region(ClearType::All)
231 }
232
233 fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
234 execute!(
235 self.writer,
236 Clear(match clear_type {
237 ClearType::All => crossterm::terminal::ClearType::All,
238 ClearType::AfterCursor => crossterm::terminal::ClearType::FromCursorDown,
239 ClearType::BeforeCursor => crossterm::terminal::ClearType::FromCursorUp,
240 ClearType::CurrentLine => crossterm::terminal::ClearType::CurrentLine,
241 ClearType::UntilNewLine => crossterm::terminal::ClearType::UntilNewLine,
242 })
243 )
244 }
245
246 fn append_lines(&mut self, n: u16) -> io::Result<()> {
247 for _ in 0..n {
248 queue!(self.writer, Print("\n"))?;
249 }
250 self.writer.flush()
251 }
252
253 fn size(&self) -> io::Result<Size> {
254 let (width, height) = terminal::size()?;
255 Ok(Size { width, height })
256 }
257
258 fn window_size(&mut self) -> io::Result<WindowSize> {
259 let crossterm::terminal::WindowSize {
260 columns,
261 rows,
262 width,
263 height,
264 } = terminal::window_size()?;
265 Ok(WindowSize {
266 columns_rows: Size {
267 width: columns,
268 height: rows,
269 },
270 pixels: Size { width, height },
271 })
272 }
273
274 fn flush(&mut self) -> io::Result<()> {
275 self.writer.flush()
276 }
277
278 #[cfg(feature = "scrolling-regions")]
279 fn scroll_region_up(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
280 queue!(
281 self.writer,
282 ScrollUpInRegion {
283 first_row: region.start,
284 last_row: region.end.saturating_sub(1),
285 lines_to_scroll: amount,
286 }
287 )?;
288 self.writer.flush()
289 }
290
291 #[cfg(feature = "scrolling-regions")]
292 fn scroll_region_down(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
293 queue!(
294 self.writer,
295 ScrollDownInRegion {
296 first_row: region.start,
297 last_row: region.end.saturating_sub(1),
298 lines_to_scroll: amount,
299 }
300 )?;
301 self.writer.flush()
302 }
303}
304
305impl From<Color> for CColor {
306 fn from(color: Color) -> Self {
307 match color {
308 Color::Reset => Self::Reset,
309 Color::Black => Self::Black,
310 Color::Red => Self::DarkRed,
311 Color::Green => Self::DarkGreen,
312 Color::Yellow => Self::DarkYellow,
313 Color::Blue => Self::DarkBlue,
314 Color::Magenta => Self::DarkMagenta,
315 Color::Cyan => Self::DarkCyan,
316 Color::Gray => Self::Grey,
317 Color::DarkGray => Self::DarkGrey,
318 Color::LightRed => Self::Red,
319 Color::LightGreen => Self::Green,
320 Color::LightBlue => Self::Blue,
321 Color::LightYellow => Self::Yellow,
322 Color::LightMagenta => Self::Magenta,
323 Color::LightCyan => Self::Cyan,
324 Color::White => Self::White,
325 Color::Indexed(i) => Self::AnsiValue(i),
326 Color::Rgb(r, g, b) => Self::Rgb { r, g, b },
327 }
328 }
329}
330
331impl From<CColor> for Color {
332 fn from(value: CColor) -> Self {
333 match value {
334 CColor::Reset => Self::Reset,
335 CColor::Black => Self::Black,
336 CColor::DarkRed => Self::Red,
337 CColor::DarkGreen => Self::Green,
338 CColor::DarkYellow => Self::Yellow,
339 CColor::DarkBlue => Self::Blue,
340 CColor::DarkMagenta => Self::Magenta,
341 CColor::DarkCyan => Self::Cyan,
342 CColor::Grey => Self::Gray,
343 CColor::DarkGrey => Self::DarkGray,
344 CColor::Red => Self::LightRed,
345 CColor::Green => Self::LightGreen,
346 CColor::Blue => Self::LightBlue,
347 CColor::Yellow => Self::LightYellow,
348 CColor::Magenta => Self::LightMagenta,
349 CColor::Cyan => Self::LightCyan,
350 CColor::White => Self::White,
351 CColor::Rgb { r, g, b } => Self::Rgb(r, g, b),
352 CColor::AnsiValue(v) => Self::Indexed(v),
353 }
354 }
355}
356
357struct ModifierDiff {
361 pub from: Modifier,
362 pub to: Modifier,
363}
364
365impl ModifierDiff {
366 fn queue<W>(self, mut w: W) -> io::Result<()>
367 where
368 W: io::Write,
369 {
370 let removed = self.from - self.to;
372 if removed.contains(Modifier::REVERSED) {
373 queue!(w, SetAttribute(CAttribute::NoReverse))?;
374 }
375 if removed.contains(Modifier::BOLD) {
376 queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
377 if self.to.contains(Modifier::DIM) {
378 queue!(w, SetAttribute(CAttribute::Dim))?;
379 }
380 }
381 if removed.contains(Modifier::ITALIC) {
382 queue!(w, SetAttribute(CAttribute::NoItalic))?;
383 }
384 if removed.contains(Modifier::UNDERLINED) {
385 queue!(w, SetAttribute(CAttribute::NoUnderline))?;
386 }
387 if removed.contains(Modifier::DIM) {
388 queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
389 }
390 if removed.contains(Modifier::CROSSED_OUT) {
391 queue!(w, SetAttribute(CAttribute::NotCrossedOut))?;
392 }
393 if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
394 queue!(w, SetAttribute(CAttribute::NoBlink))?;
395 }
396
397 let added = self.to - self.from;
398 if added.contains(Modifier::REVERSED) {
399 queue!(w, SetAttribute(CAttribute::Reverse))?;
400 }
401 if added.contains(Modifier::BOLD) {
402 queue!(w, SetAttribute(CAttribute::Bold))?;
403 }
404 if added.contains(Modifier::ITALIC) {
405 queue!(w, SetAttribute(CAttribute::Italic))?;
406 }
407 if added.contains(Modifier::UNDERLINED) {
408 queue!(w, SetAttribute(CAttribute::Underlined))?;
409 }
410 if added.contains(Modifier::DIM) {
411 queue!(w, SetAttribute(CAttribute::Dim))?;
412 }
413 if added.contains(Modifier::CROSSED_OUT) {
414 queue!(w, SetAttribute(CAttribute::CrossedOut))?;
415 }
416 if added.contains(Modifier::SLOW_BLINK) {
417 queue!(w, SetAttribute(CAttribute::SlowBlink))?;
418 }
419 if added.contains(Modifier::RAPID_BLINK) {
420 queue!(w, SetAttribute(CAttribute::RapidBlink))?;
421 }
422
423 Ok(())
424 }
425}
426
427impl From<CAttribute> for Modifier {
428 fn from(value: CAttribute) -> Self {
429 Self::from(CAttributes::from(value))
433 }
434}
435
436impl From<CAttributes> for Modifier {
437 fn from(value: CAttributes) -> Self {
438 let mut res = Self::empty();
439
440 if value.has(CAttribute::Bold) {
441 res |= Self::BOLD;
442 }
443 if value.has(CAttribute::Dim) {
444 res |= Self::DIM;
445 }
446 if value.has(CAttribute::Italic) {
447 res |= Self::ITALIC;
448 }
449 if value.has(CAttribute::Underlined)
450 || value.has(CAttribute::DoubleUnderlined)
451 || value.has(CAttribute::Undercurled)
452 || value.has(CAttribute::Underdotted)
453 || value.has(CAttribute::Underdashed)
454 {
455 res |= Self::UNDERLINED;
456 }
457 if value.has(CAttribute::SlowBlink) {
458 res |= Self::SLOW_BLINK;
459 }
460 if value.has(CAttribute::RapidBlink) {
461 res |= Self::RAPID_BLINK;
462 }
463 if value.has(CAttribute::Reverse) {
464 res |= Self::REVERSED;
465 }
466 if value.has(CAttribute::Hidden) {
467 res |= Self::HIDDEN;
468 }
469 if value.has(CAttribute::CrossedOut) {
470 res |= Self::CROSSED_OUT;
471 }
472
473 res
474 }
475}
476
477impl From<ContentStyle> for Style {
478 fn from(value: ContentStyle) -> Self {
479 let mut sub_modifier = Modifier::empty();
480
481 if value.attributes.has(CAttribute::NoBold) {
482 sub_modifier |= Modifier::BOLD;
483 }
484 if value.attributes.has(CAttribute::NoItalic) {
485 sub_modifier |= Modifier::ITALIC;
486 }
487 if value.attributes.has(CAttribute::NotCrossedOut) {
488 sub_modifier |= Modifier::CROSSED_OUT;
489 }
490 if value.attributes.has(CAttribute::NoUnderline) {
491 sub_modifier |= Modifier::UNDERLINED;
492 }
493 if value.attributes.has(CAttribute::NoHidden) {
494 sub_modifier |= Modifier::HIDDEN;
495 }
496 if value.attributes.has(CAttribute::NoBlink) {
497 sub_modifier |= Modifier::RAPID_BLINK | Modifier::SLOW_BLINK;
498 }
499 if value.attributes.has(CAttribute::NoReverse) {
500 sub_modifier |= Modifier::REVERSED;
501 }
502
503 Self {
504 fg: value.foreground_color.map(Into::into),
505 bg: value.background_color.map(Into::into),
506 #[cfg(feature = "underline-color")]
507 underline_color: value.underline_color.map(Into::into),
508 add_modifier: value.attributes.into(),
509 sub_modifier,
510 }
511 }
512}
513
514#[cfg(feature = "scrolling-regions")]
522#[derive(Debug, Clone, Copy, PartialEq, Eq)]
523struct ScrollUpInRegion {
524 pub first_row: u16,
526
527 pub last_row: u16,
529
530 pub lines_to_scroll: u16,
532}
533
534#[cfg(feature = "scrolling-regions")]
535impl crate::crossterm::Command for ScrollUpInRegion {
536 fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
537 if self.lines_to_scroll != 0 {
538 write!(
540 f,
541 crate::crossterm::csi!("{};{}r"),
542 self.first_row.saturating_add(1),
543 self.last_row.saturating_add(1)
544 )?;
545 write!(f, crate::crossterm::csi!("{}S"), self.lines_to_scroll)?;
547 write!(f, crate::crossterm::csi!("r"))?;
549 }
550 Ok(())
551 }
552
553 #[cfg(windows)]
554 fn execute_winapi(&self) -> io::Result<()> {
555 Err(io::Error::new(
556 io::ErrorKind::Unsupported,
557 "ScrollUpInRegion command not supported for winapi",
558 ))
559 }
560}
561
562#[cfg(feature = "scrolling-regions")]
570#[derive(Debug, Clone, Copy, PartialEq, Eq)]
571struct ScrollDownInRegion {
572 pub first_row: u16,
574
575 pub last_row: u16,
577
578 pub lines_to_scroll: u16,
580}
581
582#[cfg(feature = "scrolling-regions")]
583impl crate::crossterm::Command for ScrollDownInRegion {
584 fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
585 if self.lines_to_scroll != 0 {
586 write!(
588 f,
589 crate::crossterm::csi!("{};{}r"),
590 self.first_row.saturating_add(1),
591 self.last_row.saturating_add(1)
592 )?;
593 write!(f, crate::crossterm::csi!("{}T"), self.lines_to_scroll)?;
595 write!(f, crate::crossterm::csi!("r"))?;
597 }
598 Ok(())
599 }
600
601 #[cfg(windows)]
602 fn execute_winapi(&self) -> io::Result<()> {
603 Err(io::Error::new(
604 io::ErrorKind::Unsupported,
605 "ScrollDownInRegion command not supported for winapi",
606 ))
607 }
608}
609
610#[cfg(test)]
611mod tests {
612 use super::*;
613
614 #[test]
615 fn from_crossterm_color() {
616 assert_eq!(Color::from(CColor::Reset), Color::Reset);
617 assert_eq!(Color::from(CColor::Black), Color::Black);
618 assert_eq!(Color::from(CColor::DarkGrey), Color::DarkGray);
619 assert_eq!(Color::from(CColor::Red), Color::LightRed);
620 assert_eq!(Color::from(CColor::DarkRed), Color::Red);
621 assert_eq!(Color::from(CColor::Green), Color::LightGreen);
622 assert_eq!(Color::from(CColor::DarkGreen), Color::Green);
623 assert_eq!(Color::from(CColor::Yellow), Color::LightYellow);
624 assert_eq!(Color::from(CColor::DarkYellow), Color::Yellow);
625 assert_eq!(Color::from(CColor::Blue), Color::LightBlue);
626 assert_eq!(Color::from(CColor::DarkBlue), Color::Blue);
627 assert_eq!(Color::from(CColor::Magenta), Color::LightMagenta);
628 assert_eq!(Color::from(CColor::DarkMagenta), Color::Magenta);
629 assert_eq!(Color::from(CColor::Cyan), Color::LightCyan);
630 assert_eq!(Color::from(CColor::DarkCyan), Color::Cyan);
631 assert_eq!(Color::from(CColor::White), Color::White);
632 assert_eq!(Color::from(CColor::Grey), Color::Gray);
633 assert_eq!(
634 Color::from(CColor::Rgb { r: 0, g: 0, b: 0 }),
635 Color::Rgb(0, 0, 0)
636 );
637 assert_eq!(
638 Color::from(CColor::Rgb {
639 r: 10,
640 g: 20,
641 b: 30
642 }),
643 Color::Rgb(10, 20, 30)
644 );
645 assert_eq!(Color::from(CColor::AnsiValue(32)), Color::Indexed(32));
646 assert_eq!(Color::from(CColor::AnsiValue(37)), Color::Indexed(37));
647 }
648
649 mod modifier {
650 use super::*;
651
652 #[test]
653 fn from_crossterm_attribute() {
654 assert_eq!(Modifier::from(CAttribute::Reset), Modifier::empty());
655 assert_eq!(Modifier::from(CAttribute::Bold), Modifier::BOLD);
656 assert_eq!(Modifier::from(CAttribute::Italic), Modifier::ITALIC);
657 assert_eq!(Modifier::from(CAttribute::Underlined), Modifier::UNDERLINED);
658 assert_eq!(
659 Modifier::from(CAttribute::DoubleUnderlined),
660 Modifier::UNDERLINED
661 );
662 assert_eq!(
663 Modifier::from(CAttribute::Underdotted),
664 Modifier::UNDERLINED
665 );
666 assert_eq!(Modifier::from(CAttribute::Dim), Modifier::DIM);
667 assert_eq!(
668 Modifier::from(CAttribute::NormalIntensity),
669 Modifier::empty()
670 );
671 assert_eq!(
672 Modifier::from(CAttribute::CrossedOut),
673 Modifier::CROSSED_OUT
674 );
675 assert_eq!(Modifier::from(CAttribute::NoUnderline), Modifier::empty());
676 assert_eq!(Modifier::from(CAttribute::OverLined), Modifier::empty());
677 assert_eq!(Modifier::from(CAttribute::SlowBlink), Modifier::SLOW_BLINK);
678 assert_eq!(
679 Modifier::from(CAttribute::RapidBlink),
680 Modifier::RAPID_BLINK
681 );
682 assert_eq!(Modifier::from(CAttribute::Hidden), Modifier::HIDDEN);
683 assert_eq!(Modifier::from(CAttribute::NoHidden), Modifier::empty());
684 assert_eq!(Modifier::from(CAttribute::Reverse), Modifier::REVERSED);
685 }
686
687 #[test]
688 fn from_crossterm_attributes() {
689 assert_eq!(
690 Modifier::from(CAttributes::from(CAttribute::Bold)),
691 Modifier::BOLD
692 );
693 assert_eq!(
694 Modifier::from(CAttributes::from(
695 [CAttribute::Bold, CAttribute::Italic].as_ref()
696 )),
697 Modifier::BOLD | Modifier::ITALIC
698 );
699 assert_eq!(
700 Modifier::from(CAttributes::from(
701 [CAttribute::Bold, CAttribute::NotCrossedOut].as_ref()
702 )),
703 Modifier::BOLD
704 );
705 assert_eq!(
706 Modifier::from(CAttributes::from(
707 [CAttribute::Dim, CAttribute::Underdotted].as_ref()
708 )),
709 Modifier::DIM | Modifier::UNDERLINED
710 );
711 assert_eq!(
712 Modifier::from(CAttributes::from(
713 [CAttribute::Dim, CAttribute::SlowBlink, CAttribute::Italic].as_ref()
714 )),
715 Modifier::DIM | Modifier::SLOW_BLINK | Modifier::ITALIC
716 );
717 assert_eq!(
718 Modifier::from(CAttributes::from(
719 [
720 CAttribute::Hidden,
721 CAttribute::NoUnderline,
722 CAttribute::NotCrossedOut
723 ]
724 .as_ref()
725 )),
726 Modifier::HIDDEN
727 );
728 assert_eq!(
729 Modifier::from(CAttributes::from(CAttribute::Reverse)),
730 Modifier::REVERSED
731 );
732 assert_eq!(
733 Modifier::from(CAttributes::from(CAttribute::Reset)),
734 Modifier::empty()
735 );
736 assert_eq!(
737 Modifier::from(CAttributes::from(
738 [CAttribute::RapidBlink, CAttribute::CrossedOut].as_ref()
739 )),
740 Modifier::RAPID_BLINK | Modifier::CROSSED_OUT
741 );
742 }
743 }
744
745 #[test]
746 fn from_crossterm_content_style() {
747 assert_eq!(Style::from(ContentStyle::default()), Style::default());
748 assert_eq!(
749 Style::from(ContentStyle {
750 foreground_color: Some(CColor::DarkYellow),
751 ..Default::default()
752 }),
753 Style::default().fg(Color::Yellow)
754 );
755 assert_eq!(
756 Style::from(ContentStyle {
757 background_color: Some(CColor::DarkYellow),
758 ..Default::default()
759 }),
760 Style::default().bg(Color::Yellow)
761 );
762 assert_eq!(
763 Style::from(ContentStyle {
764 attributes: CAttributes::from(CAttribute::Bold),
765 ..Default::default()
766 }),
767 Style::default().add_modifier(Modifier::BOLD)
768 );
769 assert_eq!(
770 Style::from(ContentStyle {
771 attributes: CAttributes::from(CAttribute::NoBold),
772 ..Default::default()
773 }),
774 Style::default().remove_modifier(Modifier::BOLD)
775 );
776 assert_eq!(
777 Style::from(ContentStyle {
778 attributes: CAttributes::from(CAttribute::Italic),
779 ..Default::default()
780 }),
781 Style::default().add_modifier(Modifier::ITALIC)
782 );
783 assert_eq!(
784 Style::from(ContentStyle {
785 attributes: CAttributes::from(CAttribute::NoItalic),
786 ..Default::default()
787 }),
788 Style::default().remove_modifier(Modifier::ITALIC)
789 );
790 assert_eq!(
791 Style::from(ContentStyle {
792 attributes: CAttributes::from([CAttribute::Bold, CAttribute::Italic].as_ref()),
793 ..Default::default()
794 }),
795 Style::default()
796 .add_modifier(Modifier::BOLD)
797 .add_modifier(Modifier::ITALIC)
798 );
799 assert_eq!(
800 Style::from(ContentStyle {
801 attributes: CAttributes::from([CAttribute::NoBold, CAttribute::NoItalic].as_ref()),
802 ..Default::default()
803 }),
804 Style::default()
805 .remove_modifier(Modifier::BOLD)
806 .remove_modifier(Modifier::ITALIC)
807 );
808 }
809
810 #[test]
811 #[cfg(feature = "underline-color")]
812 fn from_crossterm_content_style_underline() {
813 assert_eq!(
814 Style::from(ContentStyle {
815 underline_color: Some(CColor::DarkRed),
816 ..Default::default()
817 }),
818 Style::default().underline_color(Color::Red)
819 );
820 }
821}