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")]
24#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
25
26use std::fmt::{self, Write as FmtWrite};
27use std::io::{self, Write};
28
29use ratatui_core::backend::{Backend, ClearType, WindowSize};
30use ratatui_core::buffer::Cell;
31use ratatui_core::layout::{Position, Size};
32use ratatui_core::style::{Color, Modifier, Style};
33pub use termina;
34use termina::escape::csi::{
35 Csi, Cursor, DecPrivateMode, DecPrivateModeCode, Edit, EraseInDisplay, EraseInLine, Mode, Sgr,
36 SgrAttributes, SgrModifiers,
37};
38use termina::style::{Blink, ColorSpec, Intensity, RgbColor, Underline};
39use termina::{Event, OneBased, Terminal};
40
41macro_rules! decset {
42 ($mode:ident) => {{
43 let mode = DecPrivateMode::Code(DecPrivateModeCode::$mode);
44 Csi::Mode(Mode::SetDecPrivateMode(mode))
45 }};
46}
47
48macro_rules! decreset {
49 ($mode:ident) => {{
50 let mode = DecPrivateMode::Code(DecPrivateModeCode::$mode);
51 Csi::Mode(Mode::ResetDecPrivateMode(mode))
52 }};
53}
54
55pub struct TerminaBackend<T>
83where
84 T: Terminal,
85{
86 terminal: T,
87}
88
89impl<T> TerminaBackend<T>
90where
91 T: Terminal,
92{
93 pub const fn new(terminal: T) -> Self {
95 Self { terminal }
96 }
97
98 #[instability::unstable(
100 feature = "backend-writer",
101 issue = "https://github.com/ratatui/ratatui/pull/991"
102 )]
103 pub const fn terminal(&self) -> &T {
104 &self.terminal
105 }
106
107 #[instability::unstable(
112 feature = "backend-writer",
113 issue = "https://github.com/ratatui/ratatui/pull/991"
114 )]
115 pub const fn terminal_mut(&mut self) -> &mut T {
116 &mut self.terminal
117 }
118}
119
120impl<T> Write for TerminaBackend<T>
121where
122 T: Terminal,
123{
124 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
125 self.terminal.write(buf)
126 }
127
128 fn flush(&mut self) -> io::Result<()> {
129 self.terminal.flush()
130 }
131}
132
133impl<T> Backend for TerminaBackend<T>
134where
135 T: Terminal,
136{
137 type Error = io::Error;
138
139 fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
140 where
141 I: Iterator<Item = (u16, u16, &'a Cell)>,
142 {
143 let mut string = String::with_capacity(content.size_hint().0 * 3);
144 let mut fg = Color::Reset;
145 let mut bg = Color::Reset;
146 #[cfg(feature = "underline-color")]
147 let mut underline_color = Color::Reset;
148 let mut modifier = Modifier::empty();
149 let mut last_pos: Option<Position> = None;
150 for (x, y, cell) in content {
151 if !matches!(last_pos, Some(p) if x == p.x + 1 && y == p.y) {
152 let command = Csi::Cursor(cursor_position(Position { x, y })?);
153 write!(string, "{command}").unwrap();
154 }
155 last_pos = Some(Position { x, y });
156
157 let mut attributes = SgrAttributes::default();
158 if cell.fg != fg {
159 attributes.foreground = Some(cell.fg.into_termina());
160 fg = cell.fg;
161 }
162 if cell.bg != bg {
163 attributes.background = Some(cell.bg.into_termina());
164 bg = cell.bg;
165 }
166 #[cfg(feature = "underline-color")]
167 if cell.underline_color != underline_color {
168 attributes.underline_color = Some(cell.underline_color.into_termina());
169 underline_color = cell.underline_color;
170 }
171 if cell.modifier != modifier {
172 attributes.modifiers = ModifierDiff {
173 from: modifier,
174 to: cell.modifier,
175 }
176 .into_termina();
177 modifier = cell.modifier;
178 }
179 if !attributes.is_empty() {
180 write!(string, "{}", Csi::Sgr(Sgr::Attributes(attributes))).unwrap();
181 }
182
183 string.push_str(cell.symbol());
184 }
185
186 write!(self.terminal, "{string}{}", Csi::Sgr(Sgr::Reset))
187 }
188
189 fn hide_cursor(&mut self) -> io::Result<()> {
190 let command = decreset!(ShowCursor);
191 write!(self.terminal, "{command}")?;
192 self.terminal.flush()
193 }
194
195 fn show_cursor(&mut self) -> io::Result<()> {
196 let command = decset!(ShowCursor);
197 write!(self.terminal, "{command}")?;
198 self.terminal.flush()
199 }
200
201 fn get_cursor_position(&mut self) -> io::Result<Position> {
202 let command = Csi::Cursor(Cursor::RequestActivePositionReport);
203 write!(self.terminal, "{command}")?;
204 self.terminal.flush()?;
205 let event = self.terminal.read(|event| {
206 matches!(
207 event,
208 Event::Csi(Csi::Cursor(Cursor::ActivePositionReport { .. }))
209 )
210 })?;
211 let Event::Csi(Csi::Cursor(Cursor::ActivePositionReport { line, col })) = event else {
212 return Err(io::Error::other(
213 "termina returned a non-cursor-position event",
214 ));
215 };
216 Ok(Position {
217 x: col.get_zero_based(),
218 y: line.get_zero_based(),
219 })
220 }
221
222 fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
223 let command = Csi::Cursor(cursor_position(position.into())?);
224 write!(self.terminal, "{command}")?;
225 self.terminal.flush()
226 }
227
228 fn clear(&mut self) -> io::Result<()> {
229 self.clear_region(ClearType::All)
230 }
231
232 fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
233 let edit = match clear_type {
234 ClearType::All => Edit::EraseInDisplay(EraseInDisplay::EraseDisplay),
235 ClearType::AfterCursor => Edit::EraseInDisplay(EraseInDisplay::EraseToEndOfDisplay),
236 ClearType::BeforeCursor => Edit::EraseInDisplay(EraseInDisplay::EraseToStartOfDisplay),
237 ClearType::CurrentLine => Edit::EraseInLine(EraseInLine::EraseLine),
238 ClearType::UntilNewLine => Edit::EraseInLine(EraseInLine::EraseToEndOfLine),
239 };
240 let command = Csi::Edit(edit);
241 write!(self.terminal, "{command}")?;
242 self.terminal.flush()
243 }
244
245 fn append_lines(&mut self, n: u16) -> io::Result<()> {
246 for _ in 0..n {
247 writeln!(self.terminal)?;
248 }
249 self.terminal.flush()
250 }
251
252 fn size(&self) -> io::Result<Size> {
253 let size = self.terminal.get_dimensions()?;
254 Ok(Size::new(size.cols, size.rows))
255 }
256
257 fn window_size(&mut self) -> io::Result<WindowSize> {
258 let size = self.terminal.get_dimensions()?;
259 Ok(WindowSize {
260 columns_rows: Size::new(size.cols, size.rows),
261 pixels: Size::new(
262 size.pixel_width.unwrap_or_default(),
263 size.pixel_height.unwrap_or_default(),
264 ),
265 })
266 }
267
268 fn flush(&mut self) -> io::Result<()> {
269 self.terminal.flush()
270 }
271
272 #[cfg(feature = "scrolling-regions")]
273 fn scroll_region_up(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
274 let margins = Csi::Cursor(set_top_and_bottom_margins(region)?);
275 let scroll = Csi::Edit(Edit::ScrollUp(amount.into()));
276 let reset = Csi::Cursor(reset_top_and_bottom_margins());
277 write!(self.terminal, "{margins}{scroll}{reset}")?;
278 self.terminal.flush()
279 }
280
281 #[cfg(feature = "scrolling-regions")]
282 fn scroll_region_down(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
283 let margins = Csi::Cursor(set_top_and_bottom_margins(region)?);
284 let scroll = Csi::Edit(Edit::ScrollDown(amount.into()));
285 let reset = Csi::Cursor(reset_top_and_bottom_margins());
286 write!(self.terminal, "{margins}{scroll}{reset}")?;
287 self.terminal.flush()
288 }
289}
290
291fn cursor_position(position: Position) -> io::Result<Cursor> {
292 Ok(Cursor::Position {
293 line: one_based(position.y)?,
294 col: one_based(position.x)?,
295 })
296}
297
298fn one_based(n: u16) -> io::Result<OneBased> {
299 n.checked_add(1)
300 .and_then(OneBased::new)
301 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "position exceeds u16::MAX - 1"))
302}
303
304#[cfg(feature = "scrolling-regions")]
305fn set_top_and_bottom_margins(region: std::ops::Range<u16>) -> io::Result<Cursor> {
306 Ok(Cursor::SetTopAndBottomMargins {
307 top: one_based(region.start)?,
308 bottom: OneBased::new(region.end).ok_or_else(|| {
309 io::Error::new(io::ErrorKind::InvalidInput, "scroll region end cannot be 0")
310 })?,
311 })
312}
313
314#[cfg(feature = "scrolling-regions")]
315const fn reset_top_and_bottom_margins() -> Cursor {
316 Cursor::SetTopAndBottomMargins {
317 top: OneBased::from_zero_based(0),
318 bottom: OneBased::new(u16::MAX).expect("u16::MAX is non-zero"),
319 }
320}
321
322pub trait IntoTermina<T> {
324 fn into_termina(self) -> T;
326}
327
328pub trait FromTermina<T> {
330 fn from_termina(value: T) -> Self;
332}
333
334struct ModifierDiff {
335 from: Modifier,
336 to: Modifier,
337}
338
339impl IntoTermina<ColorSpec> for Color {
340 fn into_termina(self) -> ColorSpec {
341 match self {
342 Self::Reset => ColorSpec::Reset,
343 Self::Black => ColorSpec::BLACK,
344 Self::Red => ColorSpec::RED,
345 Self::Green => ColorSpec::GREEN,
346 Self::Yellow => ColorSpec::YELLOW,
347 Self::Blue => ColorSpec::BLUE,
348 Self::Magenta => ColorSpec::MAGENTA,
349 Self::Cyan => ColorSpec::CYAN,
350 Self::Gray => ColorSpec::WHITE,
351 Self::DarkGray => ColorSpec::BRIGHT_BLACK,
352 Self::LightRed => ColorSpec::BRIGHT_RED,
353 Self::LightGreen => ColorSpec::BRIGHT_GREEN,
354 Self::LightYellow => ColorSpec::BRIGHT_YELLOW,
355 Self::LightBlue => ColorSpec::BRIGHT_BLUE,
356 Self::LightMagenta => ColorSpec::BRIGHT_MAGENTA,
357 Self::LightCyan => ColorSpec::BRIGHT_CYAN,
358 Self::White => ColorSpec::BRIGHT_WHITE,
359 Self::Indexed(i) => ColorSpec::PaletteIndex(i),
360 Self::Rgb(r, g, b) => ColorSpec::TrueColor(RgbColor::new(r, g, b).into()),
361 }
362 }
363}
364
365impl IntoTermina<SgrAttributes> for Style {
366 fn into_termina(self) -> SgrAttributes {
367 SgrAttributes {
368 foreground: self.fg.map(IntoTermina::into_termina),
369 background: self.bg.map(IntoTermina::into_termina),
370 #[cfg(feature = "underline-color")]
371 underline_color: self.underline_color.map(IntoTermina::into_termina),
372 modifiers: ModifierDiff {
373 from: self.sub_modifier,
374 to: self.add_modifier,
375 }
376 .into_termina(),
377 ..Default::default()
378 }
379 }
380}
381
382impl FromTermina<ColorSpec> for Color {
383 fn from_termina(value: ColorSpec) -> Self {
384 match value {
385 ColorSpec::Reset => Self::Reset,
386 ColorSpec::PaletteIndex(i) => match i {
387 0 => Self::Black,
388 1 => Self::Red,
389 2 => Self::Green,
390 3 => Self::Yellow,
391 4 => Self::Blue,
392 5 => Self::Magenta,
393 6 => Self::Cyan,
394 7 => Self::Gray,
395 8 => Self::DarkGray,
396 9 => Self::LightRed,
397 10 => Self::LightGreen,
398 11 => Self::LightYellow,
399 12 => Self::LightBlue,
400 13 => Self::LightMagenta,
401 14 => Self::LightCyan,
402 15 => Self::White,
403 _ => Self::Indexed(i),
404 },
405 ColorSpec::TrueColor(color) => Self::Rgb(color.red, color.green, color.blue),
406 }
407 }
408}
409
410impl IntoTermina<SgrModifiers> for ModifierDiff {
411 fn into_termina(self) -> SgrModifiers {
412 let removed = self.from - self.to;
413 let added = self.to - self.from;
414 let mut modifiers = SgrModifiers::empty();
415
416 if removed.contains(Modifier::BOLD) || removed.contains(Modifier::DIM) {
417 modifiers |= SgrModifiers::INTENSITY_NORMAL;
418 }
419 if removed.contains(Modifier::ITALIC) {
420 modifiers |= SgrModifiers::NO_ITALIC;
421 }
422 if removed.contains(Modifier::UNDERLINED) {
423 modifiers |= SgrModifiers::UNDERLINE_NONE;
424 }
425 if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
426 modifiers |= SgrModifiers::BLINK_NONE;
427 }
428 if removed.contains(Modifier::REVERSED) {
429 modifiers |= SgrModifiers::NO_REVERSE;
430 }
431 if removed.contains(Modifier::HIDDEN) {
432 modifiers |= SgrModifiers::NO_INVISIBLE;
433 }
434 if removed.contains(Modifier::CROSSED_OUT) {
435 modifiers |= SgrModifiers::NO_STRIKE_THROUGH;
436 }
437
438 if added.contains(Modifier::BOLD) {
439 modifiers |= SgrModifiers::INTENSITY_BOLD;
440 }
441 if added.contains(Modifier::DIM) {
442 modifiers |= SgrModifiers::INTENSITY_DIM;
443 }
444 if added.contains(Modifier::ITALIC) {
445 modifiers |= SgrModifiers::ITALIC;
446 }
447 if added.contains(Modifier::UNDERLINED) {
448 modifiers |= SgrModifiers::UNDERLINE_SINGLE;
449 }
450 if added.contains(Modifier::SLOW_BLINK) {
451 modifiers |= SgrModifiers::BLINK_SLOW;
452 }
453 if added.contains(Modifier::RAPID_BLINK) {
454 modifiers |= SgrModifiers::BLINK_RAPID;
455 }
456 if added.contains(Modifier::REVERSED) {
457 modifiers |= SgrModifiers::REVERSE;
458 }
459 if added.contains(Modifier::HIDDEN) {
460 modifiers |= SgrModifiers::INVISIBLE;
461 }
462 if added.contains(Modifier::CROSSED_OUT) {
463 modifiers |= SgrModifiers::STRIKE_THROUGH;
464 }
465
466 modifiers
467 }
468}
469
470impl FromTermina<Intensity> for Modifier {
471 fn from_termina(value: Intensity) -> Self {
472 match value {
473 Intensity::Normal => Self::empty(),
474 Intensity::Bold => Self::BOLD,
475 Intensity::Dim => Self::DIM,
476 }
477 }
478}
479
480impl FromTermina<Underline> for Modifier {
481 fn from_termina(value: Underline) -> Self {
482 match value {
483 Underline::None => Self::empty(),
484 _ => Self::UNDERLINED,
485 }
486 }
487}
488
489impl FromTermina<Blink> for Modifier {
490 fn from_termina(value: Blink) -> Self {
491 match value {
492 Blink::None => Self::empty(),
493 Blink::Slow => Self::SLOW_BLINK,
494 Blink::Rapid => Self::RAPID_BLINK,
495 }
496 }
497}
498
499impl<T> fmt::Debug for TerminaBackend<T>
500where
501 T: Terminal + fmt::Debug,
502{
503 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
504 f.debug_struct("TerminaBackend")
505 .field("terminal", &self.terminal)
506 .finish()
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use std::time::Duration;
513
514 use ratatui_core::buffer::Cell;
515 use termina::EventReader;
516 use termina::escape::csi::Csi;
517
518 use super::*;
519
520 #[derive(Debug)]
521 struct MockTerminal {
522 output: Vec<u8>,
523 size: termina::WindowSize,
524 events: Vec<Event>,
525 }
526
527 impl MockTerminal {
528 fn new() -> Self {
529 Self {
530 output: Vec::new(),
531 size: termina::WindowSize {
532 cols: 80,
533 rows: 24,
534 pixel_width: Some(800),
535 pixel_height: Some(480),
536 },
537 events: Vec::new(),
538 }
539 }
540
541 fn with_event(mut self, event: Event) -> Self {
542 self.events.push(event);
543 self
544 }
545
546 fn output(&self) -> String {
547 String::from_utf8_lossy(&self.output).into_owned()
548 }
549 }
550
551 impl Write for MockTerminal {
552 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
553 self.output.extend_from_slice(buf);
554 Ok(buf.len())
555 }
556
557 fn flush(&mut self) -> io::Result<()> {
558 Ok(())
559 }
560 }
561
562 impl Terminal for MockTerminal {
563 fn enter_raw_mode(&mut self) -> io::Result<()> {
564 Ok(())
565 }
566
567 fn enter_cooked_mode(&mut self) -> io::Result<()> {
568 Ok(())
569 }
570
571 fn get_dimensions(&self) -> io::Result<termina::WindowSize> {
572 Ok(self.size)
573 }
574
575 fn event_reader(&self) -> EventReader {
576 unimplemented!("backend tests do not use event_reader")
577 }
578
579 fn poll<F: Fn(&Event) -> bool>(
580 &self,
581 filter: F,
582 _timeout: Option<Duration>,
583 ) -> io::Result<bool> {
584 Ok(self.events.iter().any(filter))
585 }
586
587 fn read<F: Fn(&Event) -> bool>(&self, filter: F) -> io::Result<Event> {
588 self.events
589 .iter()
590 .find(|event| filter(event))
591 .cloned()
592 .ok_or_else(|| io::Error::new(io::ErrorKind::WouldBlock, "no matching event"))
593 }
594
595 fn set_panic_hook(
596 &mut self,
597 _f: impl Fn(&mut termina::PlatformHandle) + Send + Sync + 'static,
598 ) {
599 }
600 }
601
602 fn backend() -> TerminaBackend<MockTerminal> {
603 TerminaBackend::new(MockTerminal::new())
604 }
605
606 #[test]
607 fn writes_cursor_visibility_commands() {
608 let mut backend = backend();
609 backend.hide_cursor().unwrap();
610 backend.show_cursor().unwrap();
611
612 let hide_cursor = decreset!(ShowCursor);
613 let show_cursor = decset!(ShowCursor);
614 assert_eq!(
615 backend.terminal.output(),
616 format!("{hide_cursor}{show_cursor}")
617 );
618 }
619
620 #[test]
621 fn reads_cursor_position_reports() {
622 let event = Event::Csi(Csi::Cursor(Cursor::ActivePositionReport {
623 line: OneBased::new(5).unwrap(),
624 col: OneBased::new(7).unwrap(),
625 }));
626 let mut backend = TerminaBackend::new(MockTerminal::new().with_event(event));
627
628 assert_eq!(backend.get_cursor_position().unwrap(), Position::new(6, 4));
629 let request = Csi::Cursor(Cursor::RequestActivePositionReport);
630 assert_eq!(backend.terminal.output(), request.to_string());
631 }
632
633 #[test]
634 fn rejects_non_cursor_position_reports() {
635 let event = Event::FocusIn;
636 let mut backend = TerminaBackend::new(MockTerminal::new().with_event(event));
637
638 let error = backend.get_cursor_position().unwrap_err();
639 assert_eq!(error.kind(), io::ErrorKind::WouldBlock);
640 }
641
642 #[test]
643 fn sets_cursor_position() {
644 let mut backend = backend();
645 backend.set_cursor_position(Position::new(3, 4)).unwrap();
646
647 let position = Cursor::Position {
648 line: OneBased::new(5).unwrap(),
649 col: OneBased::new(4).unwrap(),
650 };
651 assert_eq!(backend.terminal.output(), Csi::Cursor(position).to_string());
652 }
653
654 #[test]
655 fn rejects_cursor_position_overflow() {
656 let mut backend = backend();
657
658 let error = backend.set_cursor_position(Position::new(u16::MAX, 0));
659 assert_eq!(error.unwrap_err().kind(), io::ErrorKind::InvalidInput);
660 }
661
662 #[test]
663 fn clears_regions() {
664 let mut backend = backend();
665 backend.clear_region(ClearType::All).unwrap();
666 backend.clear_region(ClearType::AfterCursor).unwrap();
667 backend.clear_region(ClearType::BeforeCursor).unwrap();
668 backend.clear_region(ClearType::CurrentLine).unwrap();
669 backend.clear_region(ClearType::UntilNewLine).unwrap();
670
671 let expected = [
672 Csi::Edit(Edit::EraseInDisplay(EraseInDisplay::EraseDisplay)),
673 Csi::Edit(Edit::EraseInDisplay(EraseInDisplay::EraseToEndOfDisplay)),
674 Csi::Edit(Edit::EraseInDisplay(EraseInDisplay::EraseToStartOfDisplay)),
675 Csi::Edit(Edit::EraseInLine(EraseInLine::EraseLine)),
676 Csi::Edit(Edit::EraseInLine(EraseInLine::EraseToEndOfLine)),
677 ]
678 .into_iter()
679 .map(|command| command.to_string())
680 .collect::<String>();
681 assert_eq!(backend.terminal.output(), expected);
682 }
683
684 #[test]
685 fn reports_terminal_size() {
686 let mut backend = backend();
687
688 assert_eq!(backend.size().unwrap(), Size::new(80, 24));
689 assert_eq!(
690 backend.window_size().unwrap(),
691 WindowSize {
692 columns_rows: Size::new(80, 24),
693 pixels: Size::new(800, 480),
694 }
695 );
696 }
697
698 #[test]
699 fn appends_lines() {
700 let mut backend = backend();
701 backend.append_lines(3).unwrap();
702
703 assert_eq!(backend.terminal.output(), "\n\n\n");
704 }
705
706 #[test]
707 fn draws_cells_with_grouped_sgr_attributes() {
708 let mut backend = backend();
709 let mut cell = Cell::new("x");
710 cell.set_style(
711 Style::new()
712 .fg(Color::Red)
713 .bg(Color::Blue)
714 .add_modifier(Modifier::BOLD),
715 );
716 let content = [(2, 3, &cell)];
717
718 backend.draw(content.into_iter()).unwrap();
719
720 let output = backend.terminal.output();
721 let cursor = Csi::Cursor(cursor_position(Position::new(2, 3)).unwrap());
722 assert!(output.starts_with(&cursor.to_string()));
723 assert!(output.contains('x'));
724 assert!(output.ends_with(&Csi::Sgr(Sgr::Reset).to_string()));
725 }
726
727 #[test]
728 fn converts_ratatui_colors_to_termina_colors() {
729 assert_eq!(Color::Reset.into_termina(), ColorSpec::Reset);
730 assert_eq!(Color::Red.into_termina(), ColorSpec::RED);
731 assert_eq!(
732 Color::Indexed(42).into_termina(),
733 ColorSpec::PaletteIndex(42)
734 );
735 assert_eq!(
736 Color::Rgb(1, 2, 3).into_termina(),
737 ColorSpec::TrueColor(RgbColor::new(1, 2, 3).into())
738 );
739 }
740
741 #[test]
742 fn converts_termina_colors_to_ratatui_colors() {
743 assert_eq!(Color::from_termina(ColorSpec::Reset), Color::Reset);
744 assert_eq!(Color::from_termina(ColorSpec::PaletteIndex(1)), Color::Red);
745 assert_eq!(
746 Color::from_termina(ColorSpec::TrueColor(RgbColor::new(1, 2, 3).into())),
747 Color::Rgb(1, 2, 3)
748 );
749 }
750
751 #[test]
752 fn converts_modifier_diffs_to_sgr_modifiers() {
753 let from = Modifier::BOLD | Modifier::ITALIC | Modifier::UNDERLINED;
754 let to = Modifier::DIM | Modifier::REVERSED | Modifier::CROSSED_OUT;
755 let modifiers = ModifierDiff { from, to }.into_termina();
756
757 assert!(modifiers.contains(SgrModifiers::INTENSITY_NORMAL));
758 assert!(modifiers.contains(SgrModifiers::NO_ITALIC));
759 assert!(modifiers.contains(SgrModifiers::UNDERLINE_NONE));
760 assert!(modifiers.contains(SgrModifiers::INTENSITY_DIM));
761 assert!(modifiers.contains(SgrModifiers::REVERSE));
762 assert!(modifiers.contains(SgrModifiers::STRIKE_THROUGH));
763 }
764
765 #[test]
766 fn converts_termina_modifiers_to_ratatui_modifiers() {
767 assert_eq!(Modifier::from_termina(Intensity::Normal), Modifier::empty());
768 assert_eq!(Modifier::from_termina(Intensity::Bold), Modifier::BOLD);
769 assert_eq!(Modifier::from_termina(Underline::None), Modifier::empty());
770 assert_eq!(
771 Modifier::from_termina(Underline::Single),
772 Modifier::UNDERLINED
773 );
774 assert_eq!(Modifier::from_termina(Blink::None), Modifier::empty());
775 assert_eq!(Modifier::from_termina(Blink::Rapid), Modifier::RAPID_BLINK);
776 }
777
778 #[cfg(feature = "scrolling-regions")]
779 #[test]
780 fn scrolls_regions() {
781 let mut backend = backend();
782 backend.scroll_region_up(1..4, 2).unwrap();
783 backend.scroll_region_down(1..4, 3).unwrap();
784
785 let margins = Cursor::SetTopAndBottomMargins {
786 top: OneBased::new(2).unwrap(),
787 bottom: OneBased::new(4).unwrap(),
788 };
789 let reset = reset_top_and_bottom_margins();
790 let up = Csi::Edit(Edit::ScrollUp(2_u16.into()));
791 let down = Csi::Edit(Edit::ScrollDown(3_u16.into()));
792 let expected = format!(
793 "{}{up}{}{}{down}{}",
794 Csi::Cursor(margins.clone()),
795 Csi::Cursor(reset.clone()),
796 Csi::Cursor(margins),
797 Csi::Cursor(reset)
798 );
799 assert_eq!(backend.terminal.output(), expected);
800 }
801
802 #[cfg(feature = "scrolling-regions")]
803 #[test]
804 fn rejects_zero_ended_scroll_regions() {
805 let mut backend = backend();
806
807 let error = backend.scroll_region_up(0..0, 1).unwrap_err();
808 assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
809 }
810
811 #[test]
812 fn csi_helpers_use_one_based_coordinates() {
813 assert_eq!(
814 cursor_position(Position::new(1, 2)).unwrap(),
815 Cursor::Position {
816 line: OneBased::new(3).unwrap(),
817 col: OneBased::new(2).unwrap(),
818 }
819 );
820 assert_eq!(one_based(0).unwrap(), OneBased::new(1).unwrap());
821 }
822}