8000 feat: add ratatui::run() method by joshka · Pull Request #1707 · ratatui/ratatui · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat: add ratatui::run() method #1707

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open

feat: add ratatui::run() method #1707

wants to merge 2 commits into from

Conversation

joshka
Copy link
Member
@joshka joshka commented Mar 7, 2025

This introduces a new ratatui::run() method which runs a closure with
a terminal initialized with reasonable defaults for most applications.
This calls ratatui::init() before running the closure and
ratatui::restore() after the closure completes, and returns the result
of the closure.

A minimal hello world example using the new ratatui::run() method:

use crossterm::event;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    ratatui::run(|terminal| loop {
        terminal.draw(|frame| frame.render_widget("Hello, world!", frame.area()))?;
        if matches!(event::read()?, event::Event::Key(_)) {
            break Ok(());
        }
    })
}

Of course, this also works both with apps that use free methods and structs:

fn run(terminal: &mut DefaultTerminal) -> Result<(), AppError> { ... }

ratatui::run(run)?;
struct App { ... }

impl App {
    fn new() -> Self { ... }
    fn run(mut self, terminal: &mut DefaultTerminal) -> Result<(), AppError> { ... }
}

ratatui::run(|terminal| App::new().run(terminal))?;

@joshka joshka requested a review from a team as a code owner March 7, 2025 00:58
Copy link
codecov bot commented Mar 7, 2025

Codecov Report

Attention: Patch coverage is 0% with 9 lines in your changes missing coverage. Please review.

Project coverage is 92.9%. Comparing base (255e466) to head (b2010c7).
Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
ratatui/src/init.rs 0.0% 9 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##            main   #1707     +/-   ##
=======================================
- Coverage   93.0%   92.9%   -0.1%     
=======================================
  Files         80      80             
  Lines      17625   17634      +9     
=======================================
  Hits       16395   16395             
- Misses      1230    1239      +9     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the main change to review.

joshka added 2 commits March 6, 2025 17:38
This introduces a new `ratatui::run()` method which runs a closure with
a terminal initialized with reasonable defaults for most applications.
This calls `ratatui::init()` before running the closure and
`ratatui::restore()` after the closure completes, and returns the result
of the closure.

A minimal hello world example using the new `ratatui::run()` method:

```rust
use crossterm::event;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    ratatui::run(|terminal| loop {
        terminal.draw(|frame| frame.render_widget("Hello, world!", frame.area()))?;
        if matches!(event::read()?, event::Event::Key(_)) {
            break Ok(());
        }
    })
}
```
Also simplify a few of the examples
@kdheepak
Copy link
Collaborator
kdheepak commented Mar 7, 2025

I like it! Thanks for making it easy to review by splitting the commits + pointing us to the exact diff. You make this too easy :)

Would it be useful to get some user feedback on this? I'm assuming we are going to be advocating for this to be the new way users are introduced to ratatui.

@joshka
Copy link
Member Author
joshka commented Mar 7, 2025

Would it be useful to get some user feedback on this? I'm assuming we are going to be advocating for this to be the new way users are introduced to ratatui.

Yup. Mentioned it in discord. This started named with_terminal, but I think run works much better.

@TadoTheMiner
Copy link
Contributor

Perfect, but why take a mutable reference and not give ownership?

Copy link
Member
@orhun orhun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

I'm okay with this being crossterm specific, like the other convenience functions such as init etc.

For the naming, maybe we can do something like run_with_terminal but I mean... what else are we going to run it with? Oh! Maybe run_with_web in ratzilla in the future. I think run should be good for now.

P.S. 0.30.0 release is going to be packed 🫠

@TadoTheMiner
Copy link
Contributor

I don't see an advantage of giving a mutable reference over ownership. I think it makes more sense to give ownership to give the user more control over the terminal.

@joshka
Copy link
Member Author
joshka commented Mar 7, 2025

I don't see an advantage of giving a mutable reference over ownership. I think it makes more sense to give ownership to give the user more control over the terminal.

Choosing to move the terminal rather than pass a mutable reference here cedes control over when the terminal value is dropped to the closure. Passing it as a mutable reference means that it will be dropped after the closure completes, just before the terminal is restored.

Also, none of the methods on Terminal require ownership, so there's no real upside to doing so that I can see.

Some questions to think about to help clarify this:

  • Is there something specific you're looking for that would need ownership of the terminal?
  • Is there some specific downside you're trying to avoid with not wanting this to be a mutable ref?
  • Are the problematic cases advanced enough that you might generally want to avoid the simple method? If you grab a copy of this PR locally, there's around 7 examples where I didn't update the examples to use ratatui::run() because those places did more interesting things than could be easily represented with this method (enabling mouse capture, running with inline options, etc.). They might inform real world cases where this method isn't useful. Are there others you know about?
  • Are there other libraries you can point to that have similar approaches where a closure is used to give temporary access to an initialized value that will be restored after the call where this is provided as an owned value rather than a mutable ref?

@joshka
Copy link
Member Author
joshka commented Mar 7, 2025

From discord:

8:37 PM]raylu: seems great? is there any downside? if you don't want it you can just use whatever you were doing before, right?
[10:30 PM]joshka: The main downsides I can think of are:
this is crossterm specific (like init and restore)
a closure as the primary interface is a little more complex than other approaches
the generic name (run vs run_with_terminal or something like that)
the lack of an approach that would be additive to the use case

For each of those I really come up on the side of "yeah, but so what?":
we've already past that mile marker for init and restore, this is harmonious with the previous decision
it's 2025, closures are part of normal developer mindset
run is simple and obvious
there might be better ideas, show me...

@coolreader18
Copy link

Would it make sense to have restore be called in a drop guard? That way a panic won't leave the terminal in raw mode.

@joshka
Copy link
Member Author
joshka commented Mar 7, 2025

Would it make sense to have restore be called in a drop guard? That way a panic won't leave the terminal in raw mode.

Doing the right thing in a terminal on panics, errors, and normal flow is surprisingly difficult to get right, and you end up choosing an option that's not necessarily ideal, but good enough. ratatui::init() is sort of that compromise. AI coding tools like CoPilot regularly tell me that I might have been a little wrong on that one by completing it as ratatui::init()? implying that the method intuitively should have returned a result. But we do have a try_init() method for that use case.

The major problem with this idea is that the panic hooks which display the error information run before the drop methods. In general apps that crash should disable raw mode and leave the alternate screen before displaying the panic messages. ratatui::init() sets up a panic handler that calls ratatui::restore() before handling a panic.

The second (less impactful) problem is that Terminal doesn't own the application's raw/alternate screen state. The user's app does. Users can still create apps where they choose not to use the ratatui::init() and ratatui::restore() methods. The expectation there should be that the user gets to decide when to enable / disable these states. This is meaningful for the narrow use cases where apps call out to other apps (e.g. an editor) and want to be in full control of these states before and after.

Third, the code to actually do the restore can in some circumstances itself crash, so we want that code to be intentionally part of the app's flow rather than part of the drop scope handling. Otherwise the error messages are a pain.

The metric for validating how well init() / restore() works comes down to looking at how often we get people asking about why their terminal is not being correctly restored. The only messages I recall seeing about those since these methods were introduced is by people not following the gilded path which this PR codifies into a small run method.

@TadoTheMiner
Copy link
Contributor

I personally store the terminal in a separate struct in my still in development xkcd viewer. It being a mutable reference wouldn't imply much except me adding a few &muts and 'as. It would be a minor annoyance but a minor annoyance is still worse than no annoyance

let frame_timeout = Duration::from_secs_f64(1.0 / 60.0); // run at 60 FPS
ratatui::run(|terminal| loop {
terminal.draw(render)?;
if event::poll(frame_timeout)? && matches!(event::read()?, Event::Key(_)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if event::poll(frame_timeout)? && matches!(event::read()?, Event::Key(_)) {
if event::poll(frame_timeout)? && matches!(event::read()?, Event::Key(x) if x.kind == KeyEventKind::Press ) {

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only real concern that really require this is that I have heard it rumored that sometimes windows terminals will pass through the key release event of the enter key that was used to run the command that runs the app. I don't use windows often enough to have seen this personally though, so it's a hypothetical problem right now.

I've seen this actually happen a few times since then, but the change would be to check event::read()?.is_key_press() which was introduced in crossterm 0.29 instead.

@orhun
Copy link
Member
orhun commented May 14, 2025

Should we rebase and finalize this PR?

@joshka
Copy link
Member Author
joshka commented May 14, 2025

Yep. This is on my list for today. The examples changed enough that it doesn't clearly rebase easily, so it's a bit of work to do this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants
0