Repository: ratatui/ratatui
Pull request #1778: feat(no_std)!: replace `Backend`'s `io::Error` usages with associated `Error` type
Author: login: j-g00da
Labels: name: Type: Breaking Change
description: This change will cause application code to break and must be noted in the breaking changes docs etc.
Created: 2025-04-11T11:02:17Z | Merged: 2025-04-23T22:05:59Z

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`.


Linked issue #1775: Remove `std::io` usages from `ratatui-core`
Make `Backend` implementations provide a custom `Error` and modify built-in backends to explicitly use `std::io::Error`. This way this breaking change _should_ only affect custom backend implementations.

Diff:
diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index ecf045dbb4..3fad18dda1 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -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
diff --git a/examples/apps/demo/src/crossterm.rs b/examples/apps/demo/src/crossterm.rs
index 51a42093fe..aeaa10dd50 100644
--- a/examples/apps/demo/src/crossterm.rs
+++ b/examples/apps/demo/src/crossterm.rs
@@ -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))?;
diff --git a/examples/apps/demo/src/termion.rs b/examples/apps/demo/src/termion.rs
index fe9c7b5268..f416136692 100644
--- a/examples/apps/demo/src/termion.rs
+++ b/examples/apps/demo/src/termion.rs
@@ -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))?;
diff --git a/examples/apps/inline/src/main.rs b/examples/apps/inline/src/main.rs
index 5f77ed84f6..d5d309f2b8 100644
--- a/examples/apps/inline/src/main.rs
+++ b/examples/apps/inline/src/main.rs
@@ -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 {
diff --git a/ratatui-core/src/backend.rs b/ratatui-core/src/backend.rs
index 0f90ed013d..9eb29c283a 100644
--- a/ratatui-core/src/backend.rs
+++ b/ratatui-core/src/backend.rs
@@ -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)]
diff --git a/ratatui-core/src/backend/test.rs b/ratatui-core/src/backend/test.rs
index 12cc47f8b6..243825eb9e 100644
--- a/ratatui-core/src/backend/test.rs
+++ b/ratatui-core/src/backend/test.rs
@@ -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)>,
diff --git a/ratatui-core/src/terminal/terminal.rs b/ratatui-core/src/terminal/terminal.rs
index 72b8f4ca4d..5c85dff76b 100644
--- a/ratatui-core/src/terminal/terminal.rs
+++ b/ratatui-core/src/terminal/terminal.rs
@@ -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;
 
diff --git a/ratatui-crossterm/src/lib.rs b/ratatui-crossterm/src/lib.rs
index 6bcc6ad762..cef82be119 100644
--- a/ratatui-crossterm/src/lib.rs
+++ b/ratatui-crossterm/src/lib.rs
@@ -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)>,
diff --git a/ratatui-termion/src/lib.rs b/ratatui-termion/src/lib.rs
index bd2d5fe28a..28db36b164 100644
--- a/ratatui-termion/src/lib.rs
+++ b/ratatui-termion/src/lib.rs
@@ -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)
     }
diff --git a/ratatui-termwiz/src/lib.rs b/ratatui-termwiz/src/lib.rs
index 51b97309db..ea50246f49 100644
--- a/ratatui-termwiz/src/lib.rs
+++ b/ratatui-termwiz/src/lib.rs
@@ -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)))


Reviews:
user: login: joshka
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-11T14:29:47Z
user: login: j-g00da
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-11T15:05:46Z
user: login: j-g00da
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-11T15:08:07Z
user: login: ed-2100
body: This makes `cargo xtask ci` a little happier and adds a kind enum for common errors.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T15:44:56Z
user: login: joshka
body: Generally LGTM - one smallish question that would probably make this a smaller PR.

I think this likely doesn't cause problems if we want to introduce a proper error type for the terminal calls, but I'm not 100% sure of that. I think I'd want to break once rather than twice if we can on this. I haven't thought through the impact yet on that bit however, so this might be ok. If you've spent any time considering it, that would be great to know.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T02:58:04Z
user: login: j-g00da
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T04:13:27Z
user: login: j-g00da
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T14:06:22Z
user: login: joshka
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T21:13:50Z
user: login: joshka
body: LGTM one minor question
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-23T21:17:39Z
user: login: j-g00da
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-23T21:46:29Z

Review comments:
diff_hunk: @@ -374,10 +372,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>
path: ratatui-core/src/terminal/terminal.rs
user: login: joshka
body: I think this is worth considering making into a concrete error type possibly. (Not in this PR, but mentioned so we're thinking about this holistically)
created_at: 2025-04-11T14:17:20Z
author_association: MEMBER
diff_hunk: @@ -148,19 +147,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 + 'static;
path: ratatui-core/src/backend.rs
user: login: joshka
body: Why is static necessary?
created_at: 2025-04-11T14:17:58Z
author_association: MEMBER
diff_hunk: @@ -158,12 +158,15 @@ fn downloads() -> Downloads {
 }
 
 #[allow(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,
+{
path: examples/apps/inline/src/main.rs
user: login: joshka
body: Can this be simplified somehow?
created_at: 2025-04-11T14:19:08Z
author_association: MEMBER
diff_hunk: @@ -232,7 +232,8 @@ impl fmt::Display for TestBackend {
 }
 
 impl Backend for TestBackend {
-    fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
+    type Error = io::Error;
+    fn draw<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
path: ratatui-core/src/backend/test.rs
user: login: joshka
body: ```suggestion
    type Error = io::Error;

    fn draw<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
```

created_at: 2025-04-11T14:22:20Z
author_association: MEMBER
diff_hunk: @@ -216,18 +218,31 @@ impl Backend for TermwizBackend {
         Ok(())
     }
 
-    fn clear(&mut self) -> io::Result<()> {
+    fn clear(&mut self) -> Result<(), Self::Error> {
         self.buffered_terminal
             .add_change(Change::ClearScreen(termwiz::color::ColorAttribute::Default));
         Ok(())
     }
 
-    fn size(&self) -> io::Result<Size> {
+    fn clear_region(&mut self, clear_type: ClearType) -> Result<(), Self::Error> {
path: ratatui-termwiz/src/lib.rs
user: login: joshka
body: Termwiz probably has a few of these available. Can be a follow up PR.
created_at: 2025-04-11T14:28:20Z
author_association: MEMBER
diff_hunk: @@ -148,19 +147,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 + 'static;
path: ratatui-core/src/backend.rs
user: login: j-g00da
body: Some examples use boxed errors, which forces that. I think this bound should be handled by applications (examples), I'll move it.
created_at: 2025-04-11T15:05:46Z
author_association: MEMBER
diff_hunk: @@ -158,12 +158,15 @@ fn downloads() -> Downloads {
 }
 
 #[allow(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,
+{
path: examples/apps/inline/src/main.rs
user: login: j-g00da
body: I don't think so
created_at: 2025-04-11T15:08:07Z
author_association: MEMBER
diff_hunk: @@ -234,7 +234,9 @@ impl fmt::Display for TestBackend {
 }
 
 impl Backend for TestBackend {
-    fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
+    type Error = io::Error;
+
+    fn draw<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
path: ratatui-core/src/backend/test.rs
user: login: joshka
body: It's possible to leave these bits as-is due to the types being the same. Here's how they render in rustdoc:

<img width="664" alt="image" src="https://github.com/user-attachments/assets/d2b31901-4385-4d7f-812b-eae2433500e9" />
<img width="527" alt="image" src="https://github.com/user-attachments/assets/a7dbb3ed-af5e-4bb8-add4-7187f3209e44" />

And in VSCode:
<img width="903" alt="image" src="https://github.com/user-attachments/assets/9a7906c6-2be8-44c0-b517-e3630f6cc70c" />

<img width="920" alt="image" src="https://github.com/user-attachments/assets/c3d5b73a-b1ce-4aa0-96ba-69d7cd1dd479" />

The Result type on both links to https://doc.rust-lang.org/nightly/std/io/type.Result.html, which is a little more useful to have over linking to Self::Error (particularly for newer rust users, which we often try to take into consideration).

What do you think about keeping these on the concrete types and only using Self::Error on the trait?

(Also applies to crossterm / termion / termwiz)
created_at: 2025-04-23T02:46:40Z
author_association: MEMBER
diff_hunk: @@ -234,7 +234,9 @@ impl fmt::Display for TestBackend {
 }
 
 impl Backend for TestBackend {
-    fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
+    type Error = io::Error;
+
+    fn draw<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
path: ratatui-core/src/backend/test.rs
user: login: j-g00da
body: I thought about that, the only reason I changed these was trying to be extra explicit - but it doesn't provide any real benefit, I will revert these.
created_at: 2025-04-23T04:13:27Z
author_association: MEMBER
diff_hunk: @@ -374,10 +372,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>
path: ratatui-core/src/terminal/terminal.rs
user: login: j-g00da
body: I can implement this in followup PR, but I'm not entirely sure if I understand correctly.
Do you mean that we should provide some kind of TerminalError and Backends should `impl From<SomeBackendError> for TerminalError`?
created_at: 2025-04-23T14:06:22Z
author_association: MEMBER
diff_hunk: @@ -374,10 +372,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>
path: ratatui-core/src/terminal/terminal.rs
user: login: joshka
body: Maybe just ratatui::Error there? But yep - the backend error should be convertible to the ratatui error.
I'm thinking about this mainly from the perspective that if we release ratatui-core 0.1, we don't want to release 0.2 too soon after as the idea of -core is to be stable for longer periods than the rest of the library. So it seems like a good place to do a breaking change.

As a followup PR is the right thing - just noting that here because we're thinking about it in this PR.
created_at: 2025-04-23T21:13:50Z
author_association: MEMBER
diff_hunk: @@ -237,7 +251,7 @@ impl Backend for TermwizBackend {
             .buffered_terminal
             .terminal()
             .get_screen_size()
-            .map_err(io::Error::other)?;
+            .map_err(Self::Error::other)?;
path: ratatui-termwiz/src/lib.rs
user: login: joshka
body: Can this still be io::Error here (and below)?
created_at: 2025-04-23T21:17:21Z
author_association: MEMBER
diff_hunk: @@ -237,7 +251,7 @@ impl Backend for TermwizBackend {
             .buffered_terminal
             .terminal()
             .get_screen_size()
-            .map_err(io::Error::other)?;
+            .map_err(Self::Error::other)?;
path: ratatui-termwiz/src/lib.rs
user: login: j-g00da
body: Sure
created_at: 2025-04-23T21:46:29Z
author_association: MEMBER

PR comments:
user: login: j-g00da
created_at: 2025-04-11T11:03:59Z
body: ~~Still needs some docs changes~~*, but it works and won't break _most_ apps. 
Thanks [@ed-2100](https://github.com/ed-2100) for suggesting this approach!

*Edit: I think docs are fine.
author_association: MEMBER
user: login: codecov[bot]
created_at: 2025-04-11T11:05:57Z
body: ## [Codecov](https://app.codecov.io/gh/ratatui/ratatui/pull/1778?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ratatui) Report
Attention: Patch coverage is `56.25000%` with `14 lines` in your changes missing coverage. Please review.
> Project coverage is 93.0%. Comparing base [(`3745a67`)](https://app.codecov.io/gh/ratatui/ratatui/commit/3745a67ba071d5a52e378af34f8409cd90912eb0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ratatui) to head [(`da49856`)](https://app.codecov.io/gh/ratatui/ratatui/commit/da4985600f955dfe83d27653b19cec749b5574d8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ratatui).
> Report is 1 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/ratatui/ratatui/pull/1778?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ratatui) | Patch % | Lines |
|---|---|---|
| [ratatui-termwiz/src/lib.rs](https://app.codecov.io/gh/ratatui/ratatui/pull/1778?src=pr&el=tree&filepath=ratatui-termwiz%2Fsrc%2Flib.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ratatui#diff-cmF0YXR1aS10ZXJtd2l6L3NyYy9saWIucnM=) | 0.0% | [7 Missing :warning: ](https://app.codecov.io/gh/ratatui/ratatui/pull/1778?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ratatui) |
| [ratatui-core/src/terminal/terminal.rs](https://app.codecov.io/gh/ratatui/ratatui/pull/1778?src=pr&el=tree&filepath=ratatui-core%2Fsrc%2Fterminal%2Fterminal.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ratatui#diff-cmF0YXR1aS1jb3JlL3NyYy90ZXJtaW5hbC90ZXJtaW5hbC5ycw==) | 81.8% | [4 Missing :warning: ](https://app.codecov.io/gh/ratatui/ratatui/pull/1778?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ratatui) |
| [ratatui-core/src/backend.rs](https://app.codecov.io/gh/ratatui/ratatui/pull/1778?src=pr&el=tree&filepath=ratatui-core%2Fsrc%2Fbackend.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ratatui#diff-cmF0YXR1aS1jb3JlL3NyYy9iYWNrZW5kLnJz) | 0.0% | [3 Missing :warning: ](https://app.codecov.io/gh/ratatui/ratatui/pull/1778?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ratatui) |

<details><summary>Additional details and impacted files</summary>


```diff
@@          Coverage Diff          @@
##            main   #1778   +/-   ##
=====================================
  Coverage   93.0%   93.0%           
=====================================
  Files         80      80           
  Lines      17641   17641           
=====================================
  Hits       16413   16413           
  Misses      1228    1228           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/ratatui/ratatui/pull/1778?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ratatui).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ratatui).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: j-g00da
created_at: 2025-04-11T11:12:38Z
body: Deps check fails because of https://github.com/ratatui/ratatui/pull/1774
author_association: MEMBER
user: login: joshka
created_at: 2025-04-11T14:31:21Z
body: Reviewing on my phone. Can you please add something to the breaking changes doc?
author_association: MEMBER
user: login: j-g00da
created_at: 2025-04-11T15:30:49Z
body: > Reviewing on my phone. Can you please add something to the breaking changes doc?

Added
author_association: MEMBER
user: login: joshka
created_at: 2025-04-23T22:06:09Z
body: Thanks again :)
author_association: MEMBER