// This is derived from `ratatui::Terminal`, which is licensed under the following terms://// The MIT License (MIT)// Copyright (c) 2016-2022 Florian Dehau// Copyright (c) 2023-2025 The Ratatui Developers//// Permission is hereby granted, free of charge, to any person obtaining a copy// of this software and associated documentation files (the "Software"), to deal// in the Software without restriction, including without limitation the rights// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell// copies of the Software, and to permit persons to whom the Software is// furnished to do so, subject to the following conditions://// The above copyright notice and this permission notice shall be included in all// copies or substantial portions of the Software.//// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE// SOFTWARE.use std::io;use std::io::Write;use crossterm::cursor::MoveTo;use crossterm::cursor::SetCursorStyle;use crossterm::queue;use crossterm::style::Colors;use crossterm::style::Print;use crossterm::style::SetAttribute;use crossterm::style::SetBackgroundColor;use crossterm::style::SetColors;use crossterm::style::SetForegroundColor;use crossterm::terminal::Clear;use derive_more::IsVariant;use ratatui::backend::Backend;use ratatui::backend::ClearType;use ratatui::buffer::Buffer;use ratatui::layout::Position;use ratatui::layout::Rect;use ratatui::layout::Size;use ratatui::style::Color;use ratatui::style::Modifier;use ratatui::widgets::WidgetRef;use unicode_width::UnicodeWidthStr;/// Returns the display width of a cell symbol, ignoring OSC escape sequences.////// OSC sequences (e.g. OSC 8 hyperlinks: `\x1B]8;;URL\x07`) are terminal/// control sequences that don't consume display columns. The standard/// `UnicodeWidthStr::width()` method incorrectly counts the printable/// characters inside OSC payloads (like `]`, `8`, `;`, and URL characters)./// This function strips them first so that only visible characters contribute/// to the width.fn display_width(s: &str) -> usize { // Fast path: no escape sequences present. if !s.contains('\x1B') { return s.width(); } // Strip OSC sequences: ESC ] ... BEL let mut visible = String::with_capacity(s.len()); let mut chars = s.chars(); while let Some(ch) = chars.next() { if ch == '\x1B' && chars.clone().next() == Some(']') { // Consume the ']' and everything up to and including BEL. chars.next(); // skip ']' for c in chars.by_ref() { if c == '\x07' { break; } } continue; } visible.push(ch); } visible.width()}pub struct Frame<'a> { /// Where should the cursor be after drawing this frame? /// /// If `None`, the cursor is hidden and its position is controlled by the backend. If `Some((x, /// y))`, the cursor is shown and placed at `(x, y)` after the call to `Terminal::draw()`. pub(crate) cursor_position: Option<Position>, /// Visible cursor shape to apply after drawing this frame. cursor_style: SetCursorStyle, /// The area of the viewport pub(crate) viewport_area: Rect, /// The buffer that is used to draw the current frame pub(crate) buffer: &'a mut Buffer,}impl Frame<'_> { /// The area of the current frame /// /// This is guaranteed not to change during rendering, so may be called multiple times. /// /// If your app listens for a resize event from the backend, it should ignore the values from /// the event for any calculations that are used to render the current frame and use this value /// instead as this is the area of the buffer that is used to render the current frame. pub const fn area(&self) -> Rect { self.viewport_area } /// Render a [`WidgetRef`] to the current buffer using [`WidgetRef::render_ref`]. /// /// Usually the area argument is the size of the current frame or a sub-area of the current /// frame (which can be obtained using [`Layout`] to split the total area). #[allow(clippy::needless_pass_by_value)] pub fn render_widget_ref<W: WidgetRef>(&mut self, widget: W, area: Rect) { widget.render_ref(area, self.buffer); } /// After drawing this frame, make the cursor visible and put it at the specified (x, y) /// coordinates. If this method is not called, the cursor will be hidden. /// /// Note that this will interfere with calls to [`Terminal::hide_cursor`], /// [`Terminal::show_cursor`], and [`Terminal::set_cursor_position`]. Pick one of the APIs and /// stick with it. /// /// [`Terminal::hide_cursor`]: crate::Terminal::hide_cursor /// [`Terminal::show_cursor`]: crate::Terminal::show_cursor /// [`Terminal::set_cursor_position`]: crate::Terminal::set_cursor_position pub fn set_cursor_position<P: Into<Position>>(&mut self, position: P) { self.cursor_position = Some(position.into()); } /// After drawing this frame, set the terminal's visible cursor style. pub fn set_cursor_style(&mut self, style: SetCursorStyle) { self.cursor_style = style; } /// Gets the buffer that this `Frame` draws into as a mutable reference. pub fn buffer_mut(&mut self) -> &mut Buffer { self.buffer }}#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]pub struct Terminal<B>where B: Backend + Write,{ /// The backend used to interface with the terminal backend: B, /// Holds the results of the current and previous draw calls. The two are compared at the end /// of each draw pass to output the necessary updates to the terminal buffers: [Buffer; 2], /// Index of the current buffer in the previous array current: usize, /// Whether the cursor is currently hidden pub hidden_cursor: bool, /// Area of the viewport pub viewport_area: Rect, /// Last known size of the terminal. Used to detect if the internal buffers have to be resized. pub last_known_screen_size: Size, /// Last known position of the cursor. Used to find the new area when the viewport is inlined /// and the terminal resized. pub last_known_cursor_pos: Position, /// Count of visible history rows rendered above the viewport in inline mode. visible_history_rows: u16,}impl<B> Drop for Terminal<B>where B: Backend, B: Write,{ #[allow(clippy::print_stderr)] fn drop(&mut self) { // Attempt to restore the cursor state if let Err(err) = self.reset_cursor_style() { eprintln!("Failed to reset the cursor style: {err}"); } if self.hidden_cursor && let Err(err) = self.show_cursor() { eprintln!("Failed to show the cursor: {err}"); } }}impl<B> Terminal<B>where B: Backend, B: Write,{ /// Creates a new [`Terminal`] with the given [`Backend`] and [`TerminalOptions`]. pub fn with_options(mut backend: B) -> io::Result<Self> { let screen_size = backend.size()?; let cursor_pos = backend.get_cursor_position().unwrap_or_else(|err| { // Some PTYs do not answer CPR (`ESC[6n`); continue with a safe default instead // of failing TUI startup. tracing::warn!("failed to read initial cursor position; defaulting to origin: {err}"); Position { x: 0, y: 0 } }); Ok(Self::with_screen_size_and_cursor_position( backend, screen_size, cursor_pos, )) } /// Creates a new [`Terminal`] from a caller-provided initial cursor position. /// /// Startup code uses this when cursor probing has already happened outside the backend, for /// example through a bounded terminal probe. Supplying a stale or synthetic position changes /// the inline viewport anchor, so callers should only use this after they have chosen the same /// fallback they want the first render to honor. pub fn with_options_and_cursor_position(backend: B, cursor_pos: Position) -> io::Result<Self> { let screen_size = backend.size()?; Ok(Self::with_screen_size_and_cursor_position( backend, screen_size, cursor_pos, )) } fn with_screen_size_and_cursor_position( backend: B, screen_size: Size, cursor_pos: Position, ) -> Self { Self { backend, buffers: [Buffer::empty(Rect::ZERO), Buffer::empty(Rect::ZERO)], current: 0, hidden_cursor: false, viewport_area: Rect::new( /*x*/ 0, cursor_pos.y, /*width*/ 0, /*height*/ 0, ), last_known_screen_size: screen_size, last_known_cursor_pos: cursor_pos, visible_history_rows: 0, } } /// Get a Frame object which provides a consistent view into the terminal state for rendering. pub fn get_frame(&mut self) -> Frame<'_> { Frame { cursor_position: None, cursor_style: SetCursorStyle::DefaultUserShape, viewport_area: self.viewport_area, buffer: self.current_buffer_mut(), } } /// Gets the current buffer as a reference. fn current_buffer(&self) -> &Buffer { &self.buffers[self.current] } /// Gets the current buffer as a mutable reference. fn current_buffer_mut(&mut self) -> &mut Buffer { &mut self.buffers[self.current] } /// Gets the previous buffer as a reference. fn previous_buffer(&self) -> &Buffer { &self.buffers[1 - self.current] } /// Gets the previous buffer as a mutable reference. fn previous_buffer_mut(&mut self) -> &mut Buffer { &mut self.buffers[1 - self.current] } /// Gets the backend pub const fn backend(&self) -> &B { &self.backend } /// Gets the backend as a mutable reference pub fn backend_mut(&mut self) -> &mut B { &mut self.backend } /// Obtains a difference between the previous and the current buffer and passes it to the /// current backend for drawing. pub fn flush(&mut self) -> io::Result<()> { let updates = diff_buffers(self.previous_buffer(), self.current_buffer()); let last_put_command = updates.iter().rfind(|command| command.is_put()); if let Some(&DrawCommand::Put { x, y, .. }) = last_put_command { self.last_known_cursor_pos = Position { x, y }; } draw(&mut self.backend, updates.into_iter()) } /// Updates the Terminal so that internal buffers match the requested area. /// /// Requested area will be saved to remain consistent when rendering. This leads to a full clear /// of the screen. pub fn resize(&mut self, screen_size: Size) -> io::Result<()> { self.last_known_screen_size = screen_size; Ok(()) } /// Sets the viewport area. pub fn set_viewport_area(&mut self, area: Rect) { self.current_buffer_mut().resize(area); self.previous_buffer_mut().resize(area); self.viewport_area = area; self.visible_history_rows = self.visible_history_rows.min(area.top()); } /// Queries the backend for size and resizes if it doesn't match the previous size. pub fn autoresize(&mut self) -> io::Result<()> { let screen_size = self.size()?; if screen_size != self.last_known_screen_size { self.resize(screen_size)?; } Ok(()) } /// Draws a single frame to the terminal. /// /// Returns a [`CompletedFrame`] if successful, otherwise a [`std::io::Error`]. /// /// If the render callback passed to this method can fail, use [`try_draw`] instead. /// /// Applications should call `draw` or [`try_draw`] in a loop to continuously render the /// terminal. These methods are the main entry points for drawing to the terminal. /// /// [`try_draw`]: Terminal::try_draw /// /// This method will: /// /// - autoresize the terminal if necessary /// - call the render callback, passing it a [`Frame`] reference to render to /// - flush the current internal state by copying the current buffer to the backend /// - move the cursor to the last known position if it was set during the rendering closure /// /// The render callback should fully render the entire frame when called, including areas that /// are unchanged from the previous frame. This is because each frame is compared to the /// previous frame to determine what has changed, and only the changes are written to the /// terminal. If the render callback does not fully render the frame, the terminal will not be /// in a consistent state. pub fn draw<F>(&mut self, render_callback: F) -> io::Result<()> where F: FnOnce(&mut Frame), { self.try_draw(|frame| { render_callback(frame); io::Result::Ok(()) }) } /// Tries to draw a single frame to the terminal. /// /// Returns [`Result::Ok`] containing a [`CompletedFrame`] if successful, otherwise /// [`Result::Err`] containing the [`std::io::Error`] that caused the failure. /// /// This is the equivalent of [`Terminal::draw`] but the render callback is a function or /// closure that returns a `Result` instead of nothing. /// /// Applications should call `try_draw` or [`draw`] in a loop to continuously render the /// terminal. These methods are the main entry points for drawing to the terminal. /// /// [`draw`]: Terminal::draw /// /// This method will: /// /// - autoresize the terminal if necessary /// - call the render callback, passing it a [`Frame`] reference to render to /// - flush the current internal state by copying the current buffer to the backend /// - move the cursor to the last known position if it was set during the rendering closure /// - return a [`CompletedFrame`] with the current buffer and the area of the terminal /// /// The render callback passed to `try_draw` can return any [`Result`] with an error type that /// can be converted into an [`std::io::Error`] using the [`Into`] trait. This makes it possible /// to use the `?` operator to propagate errors that occur during rendering. If the render /// callback returns an error, the error will be returned from `try_draw` as an /// [`std::io::Error`] and the terminal will not be updated. /// /// The [`CompletedFrame`] returned by this method can be useful for debugging or testing /// purposes, but it is often not used in regular applicationss. /// /// The render callback should fully render the entire frame when called, including areas that /// are unchanged from the previous frame. This is because each frame is compared to the /// previous frame to determine what has changed, and only the changes are written to the /// terminal. If the render function does not fully render the frame, the terminal will not be /// in a consistent state. pub fn try_draw<F, E>(&mut self, render_callback: F) -> io::Result<()> where F: FnOnce(&mut Frame) -> Result<(), E>, E: Into<io::Error>, { // Autoresize - otherwise we get glitches if shrinking or potential desync between widgets // and the terminal (if growing), which may OOB. self.autoresize()?; let mut frame = self.get_frame(); render_callback(&mut frame).map_err(Into::into)?; // We can't change the cursor position right away because we have to flush the frame to // stdout first. But we also can't keep the frame around, since it holds a &mut to // Buffer. Thus, we're taking the important data out of the Frame and dropping it. let cursor_position = frame.cursor_position; let cursor_style = frame.cursor_style; // Draw to stdout self.flush()?; match cursor_position { None => self.hide_cursor()?, Some(position) => { self.set_cursor_style(cursor_style)?; self.show_cursor()?; self.set_cursor_position(position)?; } } self.swap_buffers(); Backend::flush(&mut self.backend)?; Ok(()) } /// Hides the cursor. pub fn hide_cursor(&mut self) -> io::Result<()> { self.backend.hide_cursor()?; self.hidden_cursor = true; Ok(()) } /// Shows the cursor. pub fn show_cursor(&mut self) -> io::Result<()> { self.backend.show_cursor()?; self.hidden_cursor = false; Ok(()) } /// Sets the visible terminal cursor style. pub fn set_cursor_style(&mut self, style: SetCursorStyle) -> io::Result<()> { queue!(self.backend, style) } /// Restores the user-configured terminal cursor style. pub fn reset_cursor_style(&mut self) -> io::Result<()> { self.set_cursor_style(SetCursorStyle::DefaultUserShape) } /// Gets the current cursor position. /// /// This is the position of the cursor after the last draw call. #[allow(dead_code)] pub fn get_cursor_position(&mut self) -> io::Result<Position> { self.backend.get_cursor_position() } /// Sets the cursor position. pub fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> { let position = position.into(); self.backend.set_cursor_position(position)?; self.last_known_cursor_pos = position; Ok(()) } /// Clear the terminal and force a full redraw on the next draw call. pub fn clear(&mut self) -> io::Result<()> { if self.viewport_area.is_empty() { return Ok(()); } self.clear_after_position(self.viewport_area.as_position()) } /// Clear from `position` through the end of the visible screen and force a full redraw. pub(crate) fn clear_after_position(&mut self, position: Position) -> io::Result<()> { self.backend.set_cursor_position(position)?; self.backend.clear_region(ClearType::AfterCursor)?; // Reset the back buffer to make sure the next update will redraw everything. self.previous_buffer_mut().reset(); Ok(()) } /// Force the next draw pass to repaint the entire viewport by resetting the /// diff buffer. Call this after raw terminal operations that move screen /// content outside ratatui's knowledge. pub fn invalidate_viewport(&mut self) { self.previous_buffer_mut().reset(); } /// Clear terminal scrollback (if supported) and force a full redraw. pub fn clear_scrollback(&mut self) -> io::Result<()> { if self.viewport_area.is_empty() { return Ok(()); } let home = Position { x: 0, y: 0 }; // Use an explicit cursor-home around scrollback purge for terminals that // are sensitive to inline viewport cursor placement (e.g. Terminal.app). self.set_cursor_position(home)?; queue!(self.backend, Clear(crossterm::terminal::ClearType::Purge))?; self.set_cursor_position(home)?; std::io::Write::flush(&mut self.backend)?; self.previous_buffer_mut().reset(); Ok(()) } /// Clear the entire visible screen (not just the viewport) and force a full redraw. pub fn clear_visible_screen(&mut self) -> io::Result<()> { let home = Position { x: 0, y: 0 }; // Some terminals (notably Terminal.app) behave more reliably if we pair ED2 // with an explicit cursor-home before/after, matching the common `clear` // sequence (`CSI 2J` + `CSI H`). self.set_cursor_position(home)?; self.backend.clear_region(ClearType::All)?; self.set_cursor_position(home)?; std::io::Write::flush(&mut self.backend)?; self.visible_history_rows = 0; self.previous_buffer_mut().reset(); Ok(()) } /// Hard-reset scrollback + visible screen using an explicit ANSI sequence. /// /// Some terminals behave more reliably when purge + clear are emitted as a /// single ANSI sequence instead of separate backend commands. pub fn clear_scrollback_and_visible_screen_ansi(&mut self) -> io::Result<()> { if self.viewport_area.is_empty() { return Ok(()); } // Reset scroll region + style state, home cursor, clear screen, purge scrollback. // The order matches the common shell `clear && printf '\\e[3J'` behavior. write!(self.backend, "\x1b[r\x1b[0m\x1b[H\x1b[2J\x1b[3J\x1b[H")?; std::io::Write::flush(&mut self.backend)?; self.last_known_cursor_pos = Position { x: 0, y: 0 }; self.visible_history_rows = 0; self.previous_buffer_mut().reset(); Ok(()) } pub fn visible_history_rows(&self) -> u16 { self.visible_history_rows } pub(crate) fn note_history_rows_inserted(&mut self, inserted_rows: u16) { self.visible_history_rows = self .visible_history_rows .saturating_add(inserted_rows) .min(self.viewport_area.top()); } /// Clears the inactive buffer and swaps it with the current buffer pub fn swap_buffers(&mut self) { self.previous_buffer_mut().reset(); self.current = 1 - self.current; } /// Queries the real size of the backend. pub fn size(&self) -> io::Result<Size> { self.backend.size() }}use ratatui::buffer::Cell;#[derive(Debug, IsVariant)]enum DrawCommand { Put { x: u16, y: u16, cell: Cell }, ClearToEnd { x: u16, y: u16, bg: Color },}fn diff_buffers(a: &Buffer, b: &Buffer) -> Vec<DrawCommand> { let previous_buffer = &a.content; let next_buffer = &b.content; let mut updates = vec![]; let mut last_nonblank_columns = vec![0; a.area.height as usize]; for y in 0..a.area.height { let row_start = y as usize * a.area.width as usize; let row_end = row_start + a.area.width as usize; let row = &next_buffer[row_start..row_end]; let bg = row.last().map(|cell| cell.bg).unwrap_or(Color::Reset); // Scan the row to find the rightmost column that still matters: any non-space glyph, // any cell whose bg differs from the row’s trailing bg, or any cell with modifiers. // Multi-width glyphs extend that region through their full displayed width. // After that point the rest of the row can be cleared with a single ClearToEnd, a perf win // versus emitting multiple space Put commands. let mut last_nonblank_column = 0usize; let mut column = 0usize; while column < row.len() { let cell = &row[column]; let width = display_width(cell.symbol()); if cell.symbol() != " " || cell.bg != bg || cell.modifier != Modifier::empty() { last_nonblank_column = column + (width.saturating_sub(1)); } column += width.max(1); // treat zero-width symbols as width 1 } if last_nonblank_column + 1 < row.len() { let (x, y) = a.pos_of(row_start + last_nonblank_column + 1); updates.push(DrawCommand::ClearToEnd { x, y, bg }); } last_nonblank_columns[y as usize] = last_nonblank_column as u16; } // Cells invalidated by drawing/replacing preceding multi-width characters: let mut invalidated: usize = 0; // Cells from the current buffer to skip due to preceding multi-width characters taking // their place (the skipped cells should be blank anyway), or due to per-cell-skipping: let mut to_skip: usize = 0; for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() { if !current.skip && (current != previous || invalidated > 0) && to_skip == 0 { let (x, y) = a.pos_of(i); let row = i / a.area.width as usize; if x <= last_nonblank_columns[row] { updates.push(DrawCommand::Put { x, y, cell: next_buffer[i].clone(), }); } } to_skip = display_width(current.symbol()).saturating_sub(1); let affected_width = std::cmp::max( display_width(current.symbol()), display_width(previous.symbol()), ); invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1); } updates}fn draw<I>(writer: &mut impl Write, commands: I) -> io::Result<()>where I: Iterator<Item = DrawCommand>,{ let mut fg = Color::Reset; let mut bg = Color::Reset; let mut modifier = Modifier::empty(); let mut last_pos: Option<Position> = None; for command in commands { let (x, y) = match command { DrawCommand::Put { x, y, .. } => (x, y), DrawCommand::ClearToEnd { x, y, .. } => (x, y), }; // Move the cursor if the previous location was not (x - 1, y) if !matches!(last_pos, Some(p) if x == p.x + 1 && y == p.y) { queue!(writer, MoveTo(x, y))?; } last_pos = Some(Position { x, y }); match command { DrawCommand::Put { cell, .. } => { if cell.modifier != modifier { let diff = ModifierDiff { from: modifier, to: cell.modifier, }; diff.queue(writer)?; modifier = cell.modifier; } if cell.fg != fg || cell.bg != bg { queue!( writer, SetColors(Colors::new(cell.fg.into(), cell.bg.into())) )?; fg = cell.fg; bg = cell.bg; } queue!(writer, Print(cell.symbol()))?; } DrawCommand::ClearToEnd { bg: clear_bg, .. } => { queue!(writer, SetAttribute(crossterm::style::Attribute::Reset))?; modifier = Modifier::empty(); queue!(writer, SetBackgroundColor(clear_bg.into()))?; bg = clear_bg; queue!(writer, Clear(crossterm::terminal::ClearType::UntilNewLine))?; } } } queue!( writer, SetForegroundColor(crossterm::style::Color::Reset), SetBackgroundColor(crossterm::style::Color::Reset), SetAttribute(crossterm::style::Attribute::Reset), )?; Ok(())}/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`/// values. This is useful when updating the terminal display, as it allows for more/// efficient updates by only sending the necessary changes.struct ModifierDiff { pub from: Modifier, pub to: Modifier,}impl ModifierDiff { fn queue<W: io::Write>(self, w: &mut W) -> io::Result<()> { use crossterm::style::Attribute as CAttribute; let removed = self.from - self.to; if removed.contains(Modifier::REVERSED) { queue!(w, SetAttribute(CAttribute::NoReverse))?; } if removed.contains(Modifier::BOLD) { queue!(w, SetAttribute(CAttribute::NormalIntensity))?; if self.to.contains(Modifier::DIM) { queue!(w, SetAttribute(CAttribute::Dim))?; } } if removed.contains(Modifier::ITALIC) { queue!(w, SetAttribute(CAttribute::NoItalic))?; } if removed.contains(Modifier::UNDERLINED) { queue!(w, SetAttribute(CAttribute::NoUnderline))?; } if removed.contains(Modifier::DIM) { queue!(w, SetAttribute(CAttribute::NormalIntensity))?; } if removed.contains(Modifier::CROSSED_OUT) { queue!(w, SetAttribute(CAttribute::NotCrossedOut))?; } if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) { queue!(w, SetAttribute(CAttribute::NoBlink))?; } let added = self.to - self.from; if added.contains(Modifier::REVERSED) { queue!(w, SetAttribute(CAttribute::Reverse))?; } if added.contains(Modifier::BOLD) { queue!(w, SetAttribute(CAttribute::Bold))?; } if added.contains(Modifier::ITALIC) { queue!(w, SetAttribute(CAttribute::Italic))?; } if added.contains(Modifier::UNDERLINED) { queue!(w, SetAttribute(CAttribute::Underlined))?; } if added.contains(Modifier::DIM) { queue!(w, SetAttribute(CAttribute::Dim))?; } if added.contains(Modifier::CROSSED_OUT) { queue!(w, SetAttribute(CAttribute::CrossedOut))?; } if added.contains(Modifier::SLOW_BLINK) { queue!(w, SetAttribute(CAttribute::SlowBlink))?; } if added.contains(Modifier::RAPID_BLINK) { queue!(w, SetAttribute(CAttribute::RapidBlink))?; } Ok(()) }}#[cfg(test)]mod tests { use super::*; use pretty_assertions::assert_eq; use ratatui::backend::WindowSize; use ratatui::layout::Rect; use ratatui::style::Style; struct CaptureBackend { output: Vec<u8>, size: Size, cursor: Position, } impl CaptureBackend { fn new(width: u16, height: u16) -> Self { Self { output: Vec::new(), size: Size { width, height }, cursor: Position { x: 0, y: 0 }, } } fn output(&self) -> String { String::from_utf8_lossy(&self.output).into_owned() } } impl Write for CaptureBackend { fn write(&mut self, buf: &[u8]) -> io::Result<usize> { self.output.extend_from_slice(buf); Ok(buf.len()) } fn flush(&mut self) -> io::Result<()> { Ok(()) } } impl Backend for CaptureBackend { fn draw<'a, I>(&mut self, _content: I) -> io::Result<()> where I: Iterator<Item = (u16, u16, &'a Cell)>, { Ok(()) } fn hide_cursor(&mut self) -> io::Result<()> { Ok(()) } fn show_cursor(&mut self) -> io::Result<()> { Ok(()) } fn get_cursor_position(&mut self) -> io::Result<Position> { Ok(self.cursor) } fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> { self.cursor = position.into(); Ok(()) } fn clear(&mut self) -> io::Result<()> { Ok(()) } fn clear_region(&mut self, _clear_type: ClearType) -> io::Result<()> { Ok(()) } fn append_lines(&mut self, _line_count: u16) -> io::Result<()> { Ok(()) } fn scroll_region_up( &mut self, _region: std::ops::Range<u16>, _scroll_by: u16, ) -> io::Result<()> { Ok(()) } fn scroll_region_down( &mut self, _region: std::ops::Range<u16>, _scroll_by: u16, ) -> io::Result<()> { Ok(()) } fn size(&self) -> io::Result<Size> { Ok(self.size) } fn window_size(&mut self) -> io::Result<WindowSize> { Ok(WindowSize { columns_rows: self.size, pixels: self.size, }) } fn flush(&mut self) -> io::Result<()> { Ok(()) } } #[test] fn diff_buffers_does_not_emit_clear_to_end_for_full_width_row() { let area = Rect::new(0, 0, 3, 2); let previous = Buffer::empty(area); let mut next = Buffer::empty(area); next.cell_mut((2, 0)) .expect("cell should exist") .set_symbol("X"); let commands = diff_buffers(&previous, &next); let clear_count = commands .iter() .filter(|command| matches!(command, DrawCommand::ClearToEnd { y, .. } if *y == 0)) .count(); assert_eq!( 0, clear_count, "expected diff_buffers not to emit ClearToEnd; commands: {commands:?}", ); assert!( commands .iter() .any(|command| matches!(command, DrawCommand::Put { x: 2, y: 0, .. })), "expected diff_buffers to update the final cell; commands: {commands:?}", ); } #[test] fn diff_buffers_clear_to_end_starts_after_wide_char() { let area = Rect::new(0, 0, 10, 1); let mut previous = Buffer::empty(area); let mut next = Buffer::empty(area); previous.set_string(0, 0, "中文", Style::default()); next.set_string(0, 0, "中", Style::default()); let commands = diff_buffers(&previous, &next); assert!( commands .iter() .any(|command| matches!(command, DrawCommand::ClearToEnd { x: 2, y: 0, .. })), "expected clear-to-end to start after the remaining wide char; commands: {commands:?}" ); } #[test] fn terminal_draw_applies_requested_cursor_style() { let mut output = Vec::new(); let mut terminal = Terminal::with_options(CaptureBackend::new(/*width*/ 2, /*height*/ 1)) .expect("terminal"); terminal.set_viewport_area(Rect::new(0, 0, 2, 1)); terminal .try_draw(|frame| { frame.set_cursor_style(SetCursorStyle::SteadyBar); frame.set_cursor_position((0, 0)); io::Result::Ok(()) }) .expect("draw"); queue!(output, SetCursorStyle::SteadyBar).expect("queue style"); let expected = String::from_utf8(output).expect("utf8"); let actual = terminal.backend().output(); assert!( actual.contains(&expected), "expected terminal output to contain cursor style {expected:?}, got {actual:?}" ); } #[test] fn reset_cursor_style_emits_default_user_shape() { let mut output = Vec::new(); let mut terminal = Terminal::with_options(CaptureBackend::new(/*width*/ 2, /*height*/ 1)) .expect("terminal"); terminal.reset_cursor_style().expect("reset cursor style"); ratatui::backend::Backend::flush(terminal.backend_mut()).expect("flush backend"); queue!(output, SetCursorStyle::DefaultUserShape).expect("queue style"); let expected = String::from_utf8(output).expect("utf8"); let actual = terminal.backend().output(); assert!( actual.contains(&expected), "expected terminal output to contain cursor style reset {expected:?}, got {actual:?}" ); }}