feat(no_std)!: replace Backend's io::Error usages with associated Error type (#1778)

Resolves #1775 

BREAKING CHANGE: Custom backends now have to implement `Backend::Error`
and `Backend::clear_region`. Additionally some generic `Backend` usage
will have to explicitly set trait bounds for `Backend::Error`.
This commit is contained in:
Jagoda Estera Ślązak 2025-04-24 00:05:59 +02:00 committed by GitHub
parent 3745a67ba0
commit 007713e50a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 82 additions and 59 deletions

View File

@ -16,6 +16,9 @@ This is a quick summary of the sections below:
- `List::highlight_symbol` now accepts `Into<Line>` instead of `&str`
- 'layout::Alignment' is renamed to 'layout::HorizontalAlignment'
- The MSRV is now 1.81.0
- `Backend` now requires an associated `Error` type and `clear_region` method
- `Backend` now uses `Self::Error` for error handling instead of `std::io::Error`
- `Terminal<B>` now uses `B::Error` for error handling instead of `std::io::Error`
- [v0.29.0](#v0290)
- `Sparkline::data` takes `IntoIterator<Item = SparklineBar>` instead of `&[u64]` and is no longer const
- Removed public fields from `Rect` iterators

View File

@ -47,7 +47,10 @@ fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
) -> Result<(), Box<dyn Error>>
where
B::Error: 'static,
{
let mut last_tick = Instant::now();
loop {
terminal.draw(|frame| ui::draw(frame, &mut app))?;

View File

@ -36,7 +36,10 @@ fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> Result<(), Box<dyn Error>> {
) -> Result<(), Box<dyn Error>>
where
B::Error: 'static,
{
let events = events(tick_rate);
loop {
terminal.draw(|frame| ui::draw(frame, &mut app))?;

View File

@ -158,12 +158,15 @@ fn downloads() -> Downloads {
}
#[expect(clippy::needless_pass_by_value)]
fn run(
terminal: &mut Terminal<impl Backend>,
fn run<B: Backend>(
terminal: &mut Terminal<B>,
workers: Vec<Worker>,
mut downloads: Downloads,
rx: mpsc::Receiver<Event>,
) -> Result<()> {
) -> Result<()>
where
B::Error: Send + Sync + 'static,
{
let mut redraw = true;
loop {
if redraw {

View File

@ -100,8 +100,6 @@
//! [Examples]: https://github.com/ratatui/ratatui/tree/main/ratatui/examples/README.md
//! [Backend Comparison]: https://ratatui.rs/concepts/backends/comparison/
//! [Ratatui Website]: https://ratatui.rs
use alloc::format;
use std::io;
use strum::{Display, EnumString};
@ -148,19 +146,22 @@ pub struct WindowSize {
///
/// [`Terminal`]: https://docs.rs/ratatui/latest/ratatui/struct.Terminal.html
pub trait Backend {
/// Error type associated with this Backend.
type Error: core::error::Error;
/// Draw the given content to the terminal screen.
///
/// The content is provided as an iterator over `(u16, u16, &Cell)` tuples, where the first two
/// elements represent the x and y coordinates, and the third element is a reference to the
/// [`Cell`] to be drawn.
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
fn draw<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
where
I: Iterator<Item = (u16, u16, &'a Cell)>;
/// Insert `n` line breaks to the terminal screen.
///
/// This method is optional and may not be implemented by all backends.
fn append_lines(&mut self, _n: u16) -> io::Result<()> {
fn append_lines(&mut self, _n: u16) -> Result<(), Self::Error> {
Ok(())
}
@ -182,14 +183,14 @@ pub trait Backend {
/// ```
///
/// [`show_cursor`]: Self::show_cursor
fn hide_cursor(&mut self) -> io::Result<()>;
fn hide_cursor(&mut self) -> Result<(), Self::Error>;
/// Show the cursor on the terminal screen.
///
/// See [`hide_cursor`] for an example.
///
/// [`hide_cursor`]: Self::hide_cursor
fn show_cursor(&mut self) -> io::Result<()>;
fn show_cursor(&mut self) -> Result<(), Self::Error>;
/// Get the current cursor position on the terminal screen.
///
@ -199,7 +200,7 @@ pub trait Backend {
/// See [`set_cursor_position`] for an example.
///
/// [`set_cursor_position`]: Self::set_cursor_position
fn get_cursor_position(&mut self) -> io::Result<Position>;
fn get_cursor_position(&mut self) -> Result<Position, Self::Error>;
/// Set the cursor position on the terminal screen to the given x and y coordinates.
///
@ -216,14 +217,14 @@ pub trait Backend {
/// assert_eq!(backend.get_cursor_position()?, Position { x: 10, y: 20 });
/// # std::io::Result::Ok(())
/// ```
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()>;
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> Result<(), Self::Error>;
/// Get the current cursor position on the terminal screen.
///
/// The returned tuple contains the x and y coordinates of the cursor. The origin
/// (0, 0) is at the top left corner of the screen.
#[deprecated = "use `get_cursor_position()` instead which returns `Result<Position>`"]
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
fn get_cursor(&mut self) -> Result<(u16, u16), Self::Error> {
let Position { x, y } = self.get_cursor_position()?;
Ok((x, y))
}
@ -232,7 +233,7 @@ pub trait Backend {
///
/// The origin (0, 0) is at the top left corner of the screen.
#[deprecated = "use `set_cursor_position((x, y))` instead which takes `impl Into<Position>`"]
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), Self::Error> {
self.set_cursor_position(Position { x, y })
}
@ -248,7 +249,7 @@ pub trait Backend {
/// backend.clear()?;
/// # std::io::Result::Ok(())
/// ```
fn clear(&mut self) -> io::Result<()>;
fn clear(&mut self) -> Result<(), Self::Error>;
/// Clears a specific region of the terminal specified by the [`ClearType`] parameter
///
@ -273,17 +274,7 @@ pub trait Backend {
/// return an error if the `clear_type` is not supported by the backend.
///
/// [`clear`]: Self::clear
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
match clear_type {
ClearType::All => self.clear(),
ClearType::AfterCursor
| ClearType::BeforeCursor
| ClearType::CurrentLine
| ClearType::UntilNewLine => Err(io::Error::other(format!(
"clear_type [{clear_type:?}] not supported with this backend"
))),
}
}
fn clear_region(&mut self, clear_type: ClearType) -> Result<(), Self::Error>;
/// Get the size of the terminal screen in columns/rows as a [`Size`].
///
@ -297,19 +288,19 @@ pub trait Backend {
/// use ratatui::{backend::Backend, layout::Size};
///
/// assert_eq!(backend.size()?, Size::new(80, 25));
/// # std::io::Result::Ok(())
/// # Result::Ok(())
/// ```
fn size(&self) -> io::Result<Size>;
fn size(&self) -> Result<Size, Self::Error>;
/// Get the size of the terminal screen in columns/rows and pixels as a [`WindowSize`].
///
/// The reason for this not returning only the pixel size, given the redundancy with the
/// `size()` method, is that the underlying backends most likely get both values with one
/// syscall, and the user is also most likely to need columns and rows along with pixel size.
fn window_size(&mut self) -> io::Result<WindowSize>;
fn window_size(&mut self) -> Result<WindowSize, Self::Error>;
/// Flush any buffered content to the terminal screen.
fn flush(&mut self) -> io::Result<()>;
fn flush(&mut self) -> Result<(), Self::Error>;
/// Scroll a region of the screen upwards, where a region is specified by a (half-open) range
/// of rows.
@ -345,7 +336,7 @@ pub trait Backend {
&mut self,
region: core::ops::Range<u16>,
line_count: u16,
) -> io::Result<()>;
) -> Result<(), Self::Error>;
/// Scroll a region of the screen downwards, where a region is specified by a (half-open) range
/// of rows.
@ -370,7 +361,7 @@ pub trait Backend {
&mut self,
region: core::ops::Range<u16>,
line_count: u16,
) -> io::Result<()>;
) -> Result<(), Self::Error>;
}
#[cfg(test)]

View File

@ -234,6 +234,8 @@ impl fmt::Display for TestBackend {
}
impl Backend for TestBackend {
type Error = io::Error;
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,

View File

@ -1,4 +1,4 @@
use std::{eprintln, io};
use std::eprintln;
use crate::backend::{Backend, ClearType};
use crate::buffer::{Buffer, Cell};
@ -115,7 +115,7 @@ where
/// let terminal = Terminal::new(backend)?;
/// # std::io::Result::Ok(())
/// ```
pub fn new(backend: B) -> io::Result<Self> {
pub fn new(backend: B) -> Result<Self, B::Error> {
Self::with_options(
backend,
TerminalOptions {
@ -138,7 +138,7 @@ where
/// let terminal = Terminal::with_options(backend, TerminalOptions { viewport })?;
/// # std::io::Result::Ok(())
/// ```
pub fn with_options(mut backend: B, options: TerminalOptions) -> io::Result<Self> {
pub fn with_options(mut backend: B, options: TerminalOptions) -> Result<Self, B::Error> {
let area = match options.viewport {
Viewport::Fullscreen | Viewport::Inline(_) => {
Rect::from((Position::ORIGIN, backend.size()?))
@ -193,7 +193,7 @@ where
/// 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<()> {
pub fn flush(&mut self) -> Result<(), B::Error> {
let previous_buffer = &self.buffers[1 - self.current];
let current_buffer = &self.buffers[self.current];
let updates = previous_buffer.diff(current_buffer);
@ -207,7 +207,7 @@ where
///
/// Requested area will be saved to remain consistent when rendering. This leads to a full clear
/// of the screen.
pub fn resize(&mut self, area: Rect) -> io::Result<()> {
pub fn resize(&mut self, area: Rect) -> Result<(), B::Error> {
let next_area = match self.viewport {
Viewport::Inline(height) => {
let offset_in_previous_viewport = self
@ -238,7 +238,7 @@ where
}
/// Queries the backend for size and resizes if it doesn't match the previous size.
pub fn autoresize(&mut self) -> io::Result<()> {
pub fn autoresize(&mut self) -> Result<(), B::Error> {
// fixed viewports do not get autoresized
if matches!(self.viewport, Viewport::Fullscreen | Viewport::Inline(_)) {
let area = Rect::from((Position::ORIGIN, self.size()?));
@ -299,13 +299,13 @@ where
/// }
/// # std::io::Result::Ok(())
/// ```
pub fn draw<F>(&mut self, render_callback: F) -> io::Result<CompletedFrame>
pub fn draw<F>(&mut self, render_callback: F) -> Result<CompletedFrame, B::Error>
where
F: FnOnce(&mut Frame),
{
self.try_draw(|frame| {
render_callback(frame);
io::Result::Ok(())
Ok::<(), B::Error>(())
})
}
@ -374,10 +374,10 @@ where
/// }
/// # io::Result::Ok(())
/// ```
pub fn try_draw<F, E>(&mut self, render_callback: F) -> io::Result<CompletedFrame>
pub fn try_draw<F, E>(&mut self, render_callback: F) -> Result<CompletedFrame, B::Error>
where
F: FnOnce(&mut Frame) -> Result<(), E>,
E: Into<io::Error>,
E: Into<B::Error>,
{
// Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
// and the terminal (if growing), which may OOB.
@ -421,14 +421,14 @@ where
}
/// Hides the cursor.
pub fn hide_cursor(&mut self) -> io::Result<()> {
pub fn hide_cursor(&mut self) -> Result<(), B::Error> {
self.backend.hide_cursor()?;
self.hidden_cursor = true;
Ok(())
}
/// Shows the cursor.
pub fn show_cursor(&mut self) -> io::Result<()> {
pub fn show_cursor(&mut self) -> Result<(), B::Error> {
self.backend.show_cursor()?;
self.hidden_cursor = false;
Ok(())
@ -439,26 +439,26 @@ where
/// This is the position of the cursor after the last draw call and is returned as a tuple of
/// `(x, y)` coordinates.
#[deprecated = "use `get_cursor_position()` instead which returns `Result<Position>`"]
pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
pub fn get_cursor(&mut self) -> Result<(u16, u16), B::Error> {
let Position { x, y } = self.get_cursor_position()?;
Ok((x, y))
}
/// Sets the cursor position.
#[deprecated = "use `set_cursor_position((x, y))` instead which takes `impl Into<Position>`"]
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
pub fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), B::Error> {
self.set_cursor_position(Position { x, y })
}
/// Gets the current cursor position.
///
/// This is the position of the cursor after the last draw call.
pub fn get_cursor_position(&mut self) -> io::Result<Position> {
pub fn get_cursor_position(&mut self) -> Result<Position, B::Error> {
self.backend.get_cursor_position()
}
/// Sets the cursor position.
pub fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
pub fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> Result<(), B::Error> {
let position = position.into();
self.backend.set_cursor_position(position)?;
self.last_known_cursor_pos = position;
@ -466,7 +466,7 @@ where
}
/// Clear the terminal and force a full redraw on the next draw call.
pub fn clear(&mut self) -> io::Result<()> {
pub fn clear(&mut self) -> Result<(), B::Error> {
match self.viewport {
Viewport::Fullscreen => self.backend.clear_region(ClearType::All)?,
Viewport::Inline(_) => {
@ -494,7 +494,7 @@ where
}
/// Queries the real size of the backend.
pub fn size(&self) -> io::Result<Size> {
pub fn size(&self) -> Result<Size, B::Error> {
self.backend.size()
}
@ -574,7 +574,7 @@ where
/// .render(buf.area, buf);
/// });
/// ```
pub fn insert_before<F>(&mut self, height: u16, draw_fn: F) -> io::Result<()>
pub fn insert_before<F>(&mut self, height: u16, draw_fn: F) -> Result<(), B::Error>
where
F: FnOnce(&mut Buffer),
{
@ -593,7 +593,7 @@ where
&mut self,
height: u16,
draw_fn: impl FnOnce(&mut Buffer),
) -> io::Result<()> {
) -> Result<(), B::Error> {
// The approach of this function is to first render all of the lines to insert into a
// temporary buffer, and then to loop drawing chunks from the buffer to the screen. drawing
// this buffer onto the screen.
@ -694,7 +694,7 @@ where
&mut self,
mut height: u16,
draw_fn: impl FnOnce(&mut Buffer),
) -> io::Result<()> {
) -> Result<(), B::Error> {
// The approach of this function is to first render all of the lines to insert into a
// temporary buffer, and then to loop drawing chunks from the buffer to the screen. drawing
// this buffer onto the screen.
@ -766,7 +766,7 @@ where
y_offset: u16,
lines_to_draw: u16,
cells: &'a [Cell],
) -> io::Result<&'a [Cell]> {
) -> Result<&'a [Cell], B::Error> {
let width: usize = self.last_known_area.width.into();
let (to_draw, remainder) = cells.split_at(width * lines_to_draw as usize);
if lines_to_draw > 0 {
@ -789,7 +789,7 @@ where
y_offset: u16,
lines_to_draw: u16,
cells: &'a [Cell],
) -> io::Result<&'a [Cell]> {
) -> Result<&'a [Cell], B::Error> {
let width: usize = self.last_known_area.width.into();
let (to_draw, remainder) = cells.split_at(width * lines_to_draw as usize);
if lines_to_draw > 0 {
@ -807,7 +807,7 @@ where
/// Scroll the whole screen up by the given number of lines.
#[cfg(not(feature = "scrolling-regions"))]
fn scroll_up(&mut self, lines_to_scroll: u16) -> io::Result<()> {
fn scroll_up(&mut self, lines_to_scroll: u16) -> Result<(), B::Error> {
if lines_to_scroll > 0 {
self.set_cursor_position(Position::new(
0,
@ -824,7 +824,7 @@ fn compute_inline_size<B: Backend>(
height: u16,
size: Size,
offset_in_previous_viewport: u16,
) -> io::Result<(Rect, Position)> {
) -> Result<(Rect, Position), B::Error> {
let pos = backend.get_cursor_position()?;
let mut row = pos.y;

View File

@ -157,6 +157,8 @@ impl<W> Backend for CrosstermBackend<W>
where
W: Write,
{
type Error = io::Error;
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,

View File

@ -140,6 +140,8 @@ impl<W> Backend for TermionBackend<W>
where
W: Write,
{
type Error = io::Error;
fn clear(&mut self) -> io::Result<()> {
self.clear_region(ClearType::All)
}

View File

@ -17,7 +17,7 @@
use std::error::Error;
use std::io;
use ratatui_core::backend::{Backend, WindowSize};
use ratatui_core::backend::{Backend, ClearType, WindowSize};
use ratatui_core::buffer::Cell;
use ratatui_core::layout::{Position, Size};
use ratatui_core::style::{Color, Modifier, Style};
@ -119,6 +119,8 @@ impl TermwizBackend {
}
impl Backend for TermwizBackend {
type Error = io::Error;
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
@ -222,6 +224,18 @@ impl Backend for TermwizBackend {
Ok(())
}
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
match clear_type {
ClearType::All => self.clear(),
ClearType::AfterCursor
| ClearType::BeforeCursor
| ClearType::CurrentLine
| ClearType::UntilNewLine => Err(io::Error::other(format!(
"clear_type [{clear_type:?}] not supported with this backend"
))),
}
}
fn size(&self) -> io::Result<Size> {
let (cols, rows) = self.buffered_terminal.dimensions();
Ok(Size::new(u16_max(cols), u16_max(rows)))