diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 913a0ae3b..27661ea04 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -52,9 +52,10 @@ - [CLI arguments](./how-to/develop-apps/cli-arguments.md) - [Configuration Directories](./how-to/develop-apps/config-directories.md) - [Tracing](./how-to/develop-apps/tracing.md) - - [Arrange your App Code](./how-to/develop-apps/arrange-your-app-code.md) + - [Arrange your App Code](./how-to/develop-apps/abstract-terminal-and-event-handler.md) - [Setup Panic Hooks](./how-to/develop-apps/setup-panic-hooks.md) - [Use `better-panic`](./how-to/develop-apps/better-panic.md) + - [Use color-eyre and human-panic](./how-to/develop-apps/setup-panic-hooks-color-eyre.md) - [Migrate from tui-rs](./how-to/develop-apps/migrate-from-tui-rs.md) - [FAQ](./faq/README.md) diff --git a/src/how-to/develop-apps/README.md b/src/how-to/develop-apps/README.md index 30aa7cec2..8d3a30e8a 100644 --- a/src/how-to/develop-apps/README.md +++ b/src/how-to/develop-apps/README.md @@ -5,6 +5,7 @@ This section covers topics on how to develop applications: - [CLI arguments](./cli-arguments.md) - [Configuration Directories](./config-directories.md) - [Tracing](./tracing.md) -- [Arrange your App Code](./arrange-your-app-code.md) +- [Arrange your App Code](./abstract-terminal-and-event-handler.md) - [Setup Panic Hooks](./setup-panic-hooks.md) - [Use `better-panic`](./better-panic.md) +- [Use `color-eyre` and `human-panic`](./setup-panic-hooks-color-eyre.md) diff --git a/src/how-to/develop-apps/arrange-your-app-code.md b/src/how-to/develop-apps/abstract-terminal-and-event-handler.md similarity index 81% rename from src/how-to/develop-apps/arrange-your-app-code.md rename to src/how-to/develop-apps/abstract-terminal-and-event-handler.md index ab751494f..6872f6777 100644 --- a/src/how-to/develop-apps/arrange-your-app-code.md +++ b/src/how-to/develop-apps/abstract-terminal-and-event-handler.md @@ -20,7 +20,9 @@ Then you'll be able write code like this: ```rust impl App { async fn run(&mut self) -> Result<()> { - let mut tui = tui::Tui::new(self.tick_rate)?; + let mut tui = tui::Tui::new()?; + tui.tick_rate(4.0); // 4 ticks per second + tui.frame_rate(30.0); // 30 frames per second tui.enter()?; // Starts event handler loop { tui.draw(|f| { // Deref allows calling `tui.draw` @@ -67,12 +69,16 @@ use tokio::{ }; use tokio_util::sync::CancellationToken; +pub type Frame<'a> = ratatui::Frame<'a, Backend>; + #[derive(Clone, Debug, Serialize, Deserialize)] pub enum Event { + Init, Quit, Error, Closed, Tick, + Render, FocusGained, FocusLost, Paste(String), @@ -87,29 +93,44 @@ pub struct Tui { pub cancellation_token: CancellationToken, pub event_rx: UnboundedReceiver, pub event_tx: UnboundedSender, - pub tick_rate: usize, + pub frame_rate: f64, + pub tick_rate: f64, } impl Tui { - pub fn new(tick_rate: usize) -> Result { + pub fn new() -> Result { + let tick_rate = 4.0; // 4 ticks per second + let frame_rate = 30.0; // 30 frames per seconds let terminal = ratatui::Terminal::new(Backend::new(std::io::stderr()))?; let (event_tx, event_rx) = mpsc::unbounded_channel(); let cancellation_token = CancellationToken::new(); let task = tokio::spawn(async {}); - Ok(Self { terminal, task, cancellation_token, event_rx, event_tx, tick_rate }) + Ok(Self { terminal, task, cancellation_token, event_rx, event_tx, frame_rate, tick_rate }) + } + + pub fn tick_rate(&mut self, tick_rate: f64) { + self.tick_rate = tick_rate; + } + + pub fn frame_rate(&mut self, frame_rate: f64) { + self.frame_rate = frame_rate; } pub fn start(&mut self) { - let tick_rate = std::time::Duration::from_millis(self.tick_rate as u64); + let tick_delay = std::time::Duration::from_secs_f64(1.0 / self.tick_rate); + let render_delay = std::time::Duration::from_secs_f64(1.0 / self.frame_rate); self.cancel(); self.cancellation_token = CancellationToken::new(); let _cancellation_token = self.cancellation_token.clone(); let _event_tx = self.event_tx.clone(); self.task = tokio::spawn(async move { let mut reader = crossterm::event::EventStream::new(); - let mut interval = tokio::time::interval(tick_rate); + let mut tick_interval = tokio::time::interval(tick_delay); + let mut render_interval = tokio::time::interval(render_delay); + _event_tx.send(Event::Init).unwrap(); loop { - let delay = interval.tick(); + let tick_delay = tick_interval.tick(); + let render_delay = render_interval.tick(); let crossterm_event = reader.next().fuse(); tokio::select! { _ = _cancellation_token.cancelled() => { @@ -147,9 +168,12 @@ impl Tui { None => {}, } }, - _ = delay => { + _ = tick_delay => { _event_tx.send(Event::Tick).unwrap(); }, + _ = render_delay => { + _event_tx.send(Event::Render).unwrap(); + }, } } }); @@ -159,12 +183,12 @@ impl Tui { self.cancel(); let mut counter = 0; while !self.task.is_finished() { - std::thread::sleep(Duration::from_secs(1)); + std::thread::sleep(Duration::from_millis(1)); counter += 1; - if counter > 5 { + if counter > 50 { self.task.abort(); } - if counter > 10 { + if counter > 100 { log::error!("Failed to abort task for unknown reason"); return Err(color_eyre::eyre::eyre!("Unable to abort task")); } diff --git a/src/how-to/develop-apps/better-panic.md b/src/how-to/develop-apps/better-panic.md index 030bff7d7..d2d101e7f 100644 --- a/src/how-to/develop-apps/better-panic.md +++ b/src/how-to/develop-apps/better-panic.md @@ -100,32 +100,3 @@ In the screenshot below, I added a `None.unwrap()` into a function that is calle that you can see what a prettier stacktrace looks like: ![](https://user-images.githubusercontent.com/1813121/252723080-18c15640-c75f-42b3-8aeb-d4e6ce323430.png) - -So far we used `crossterm` for the `Tui` and panic handling. Similarly, if you are using `termion` -you can do something like the following: - -```rust -use std::panic; -use std::error::Error; - -let panic_hook = panic::take_hook(); -panic::set_hook(Box::new(move |panic| { - let panic_cleanup = || -> Result<(), Box> { - let mut output = io::stderr(); - write!( - output, - "{}{}{}", - termion::clear::All, - termion::screen::ToMainScreen, - termion::cursor::Show - )?; - output.into_raw_mode()?.suspend_raw_mode()?; - io::stderr().flush()?; - Ok(()) - }; - panic_cleanup().expect("failed to clean up for panic"); - panic_hook(panic); -})); -``` - -This will take the original panic hook and execute it after cleaning up the terminal. diff --git a/src/how-to/develop-apps/setup-panic-hooks-color-eyre.md b/src/how-to/develop-apps/setup-panic-hooks-color-eyre.md new file mode 100644 index 000000000..8f06092d5 --- /dev/null +++ b/src/how-to/develop-apps/setup-panic-hooks-color-eyre.md @@ -0,0 +1,202 @@ +# Use color-eyre and human-panic + +### color-eyre panic hook + +One way to manage printing of stack-traces is by using +[`color-eyre`](https://github.com/eyre-rs/color-eyre): + +```console +cargo add color-eyre +``` + +You will also want to add a `repository` key to your `Cargo.toml` file: + +```toml +repository = "https://github.com/ratatui-org/ratatui-async-template" # used by env!("CARGO_PKG_REPOSITORY") +``` + +Now, let's say I added a `panic!` to my application as an example: + +```diff +diff --git a/src/components/app.rs b/src/components/app.rs +index 289e40b..de48392 100644 +--- a/src/components/app.rs ++++ b/src/components/app.rs +@@ -77,6 +77,7 @@ impl App { + } + + pub fn increment(&mut self, i: usize) { ++ panic!("At the disco"); + self.counter = self.counter.saturating_add(i); + } +``` + +Then when this function is called, I can have the application cleanly restore the terminal and print +out a nice error message like so: + +``` +The application panicked (crashed). +Message: At the disco +Location: src/components/app.rs:80 + +This is a bug. Consider reporting it at https://github.com/ratatui-org/ratatui-async-template + +Backtrace omitted. Run with RUST_BACKTRACE=1 environment variable to display it. +Run with RUST_BACKTRACE=full to include source snippets. +``` + +Users can opt to give you a more detailed stacktrace if they can reproduce the error with +`export RUST_BACKTRACE=1`: + +``` +The application panicked (crashed). +Message: At the disco +Location: src/components/app.rs:80 + +This is a bug. Consider reporting it at https://github.com/ratatui-org/ratatui-async-template + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ BACKTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + ⋮ 13 frames hidden ⋮ + 14: ratatui_async_template::components::app::App::increment::h4e8b6e0d83d3d575 + at /Users/kd/gitrepos/ratatui-async-template/src/components/app.rs:80 + 15: ::update::hc78145b4a91e06b6 + at /Users/kd/gitrepos/ratatui-async-template/src/components/app.rs:132 + 16: ratatui_async_template::runner::Runner::run::{{closure}}::h802b0d3c3413762b + at /Users/kd/gitrepos/ratatui-async-template/src/runner.rs:80 + 17: ratatui_async_template::main::{{closure}}::hd78d335f19634c3f + at /Users/kd/gitrepos/ratatui-async-template/src/main.rs:44 + 18: tokio::runtime::park::CachedParkThread::block_on::{{closure}}::hd7949515524de9f8 + at /Users/kd/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.28.2/src/runtime/park.rs:283 + 19: tokio::runtime::coop::with_budget::h39648e20808374d3 + at /Users/kd/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.28.2/src/runtime/coop.rs:107 + 20: tokio::runtime::coop::budget::h653c1593abdd982d + at /Users/kd/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.28.2/src/runtime/coop.rs:73 + 21: tokio::runtime::park::CachedParkThread::block_on::hb0a0dd4a7c3cf33b + at /Users/kd/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.28.2/src/runtime/park.rs:283 + 22: tokio::runtime::context::BlockingRegionGuard::block_on::h4d02ab23bd93d0fd + at /Users/kd/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.28.2/src/runtime/context.rs:315 + 23: tokio::runtime::scheduler::multi_thread::MultiThread::block_on::h8aaba9030519c80d + at /Users/kd/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.28.2/src/runtime/scheduler/multi_thread/mod.rs:66 + 24: tokio::runtime::runtime::Runtime::block_on::h73a6fbfba201fac9 + at /Users/kd/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.28.2/src/runtime/runtime.rs:304 + 25: ratatui_async_template::main::h6da543b193746523 + at /Users/kd/gitrepos/ratatui-async-template/src/main.rs:46 + 26: core::ops::function::FnOnce::call_once::h6cac3edc975fcef2 + at /rustc/eb26296b556cef10fb713a38f3d16b9886080f26/library/core/src/ops/function.rs:250 + ⋮ 13 frames hidden ⋮ +``` + +## human-panic + +Personally, I like to use `human-panic` to print out a user friendly message like so: + +``` +The application panicked (crashed). +Message: At the disco +Location: src/components/app.rs:80 + +This is a bug. Consider reporting it at https://github.com/ratatui-org/ratatui-async-template + +Backtrace omitted. Run with RUST_BACKTRACE=1 environment variable to display it. +Run with RUST_BACKTRACE=full to include source snippets. +Well, this is embarrassing. + +ratatui-async-template had a problem and crashed. To help us diagnose the problem you can send us a crash report. + +We have generated a report file at "/var/folders/l4/bnjjc6p15zd3jnty8c_qkrtr0000gn/T/report-ce1e29cb-c17c-4684-b9d4-92d9678242b7.toml". Submit an issue or email with the subject of "ratatui-async-template Crash Report" and include the report as an attachment. + +- Authors: Dheepak Krishnamurthy + +We take privacy seriously, and do not perform any automated error collection. In order to improve the software, we rely on people to submit reports. + +Thank you kindly! +``` + +Here's the content of the temporary report file that `human-panic` creates: + +``` +name = "ratatui-async-template" +operating_system = "Mac OS 13.5.2 [64-bit]" +crate_version = "0.1.0" +explanation = """ +Panic occurred in file 'src/components/app.rs' at line 80 +""" +cause = "At the disco" +method = "Panic" +backtrace = """ + + 0: 0x10448f5f8 - __mh_execute_header + 1: 0x1044a43c8 - __mh_execute_header + 2: 0x1044a01ac - __mh_execute_header + 3: 0x10446f8c0 - __mh_execute_header + 4: 0x1044ac850 - __mh_execute_header""" +``` + +You'll need [human-panic](https://github.com/rust-cli/human-panic) installed as a dependency for +this: + +```console +cargo add human-panic +``` + +## Configuration + +You can mix and match different panic handlers, using `better-panic` for debug builds and +`color-eyre` and `human-panic` for release builds. The code below also prints the `color-eyre` +stacktrace to `log::error!` for good measure (after striping ansi escape sequences). + +```console +cargo add color-eyre human-panic libc better-panic strip-ansi-escapes +``` + +Here's code you can copy paste into your project (if you use the +[`Tui`](./abstract-terminal-and-event-handler.md) struct to handle terminal exits): + +```rust +pub fn initialize_panic_handler() -> Result<()> { + let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default() + .panic_section(format!("This is a bug. Consider reporting it at {}", env!("CARGO_PKG_REPOSITORY"))) + .display_location_section(true) + .display_env_section(true) + .into_hooks(); + eyre_hook.install()?; + std::panic::set_hook(Box::new(move |panic_info| { + if let Ok(t) = crate::tui::Tui::new() { + if let Err(r) = t.exit() { + error!("Unable to exit Terminal: {:?}", r); + } + } + + let msg = format!("{}", panic_hook.panic_report(panic_info)); + #[cfg(not(debug_assertions))] + { + eprintln!("{}", msg); // prints color-eyre stack trace to stderr + use human_panic::{handle_dump, print_msg, Metadata}; + let meta = Metadata { + version: env!("CARGO_PKG_VERSION").into(), + name: env!("CARGO_PKG_NAME").into(), + authors: env!("CARGO_PKG_AUTHORS").replace(':', ", ").into(), + homepage: env!("CARGO_PKG_HOMEPAGE").into(), + }; + + let file_path = handle_dump(&meta, panic_info); + // prints human-panic message + print_msg(file_path, &meta).expect("human-panic: printing error message to console failed"); + } + log::error!("Error: {}", strip_ansi_escapes::strip_str(msg)); + + #[cfg(debug_assertions)] + { + // Better Panic stacktrace that is only enabled when debugging. + better_panic::Settings::auto() + .most_recent_first(false) + .lineno_suffix(true) + .verbosity(better_panic::Verbosity::Full) + .create_panic_handler()(panic_info); + } + + std::process::exit(libc::EXIT_FAILURE); + })); + Ok(()) +} +``` diff --git a/src/how-to/develop-apps/setup-panic-hooks.md b/src/how-to/develop-apps/setup-panic-hooks.md index 0cb36ad64..9bc2ed377 100644 --- a/src/how-to/develop-apps/setup-panic-hooks.md +++ b/src/how-to/develop-apps/setup-panic-hooks.md @@ -9,28 +9,66 @@ library functionality and no external dependencies. ```rust pub fn initialize_panic_handler() { - let original_hook = std::panic::take_hook(); - std::panic::set_hook(Box::new(move |panic_info| { - crossterm::execute!(std::io::stderr(), crossterm::terminal::LeaveAlternateScreen).unwrap(); - crossterm::terminal::disable_raw_mode().unwrap(); - original_hook(panic_info); - })); + let original_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |panic_info| { + crossterm::execute!(std::io::stderr(), crossterm::terminal::LeaveAlternateScreen).unwrap(); + crossterm::terminal::disable_raw_mode().unwrap(); + original_hook(panic_info); + })); } +``` + +With this function, all your need to do is call `initialize_panic_handler()` in `main()` before +running any terminal initialization code: +```rust fn main() -> Result<()> { - initialize_panic_handler(); + initialize_panic_handler(); - // Startup - crossterm::terminal::enable_raw_mode()?; - crossterm::execute!(std::io::stderr(), crossterm::terminal::EnterAlternateScreen)?; + // Startup + crossterm::terminal::enable_raw_mode()?; + crossterm::execute!(std::io::stderr(), crossterm::terminal::EnterAlternateScreen)?; - let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stderr()))?; + let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stderr()))?; - // ... + // ... - // Shutdown - crossterm::execute!(std::io::stderr(), crossterm::terminal::LeaveAlternateScreen)?; - crossterm::terminal::disable_raw_mode()?; - Ok(()) + // Shutdown + crossterm::execute!(std::io::stderr(), crossterm::terminal::LeaveAlternateScreen)?; + crossterm::terminal::disable_raw_mode()?; + Ok(()) } ``` + +We used `crossterm` for panic handling. If you are using `termion` you can do something like the +following: + +```rust +use std::panic; +use std::error::Error; + +pub fn initialize_panic_handler() { + let panic_hook = panic::take_hook(); + panic::set_hook(Box::new(move |panic| { + let panic_cleanup = || -> Result<(), Box> { + let mut output = io::stderr(); + write!( + output, + "{}{}{}", + termion::clear::All, + termion::screen::ToMainScreen, + termion::cursor::Show + )?; + output.into_raw_mode()?.suspend_raw_mode()?; + io::stderr().flush()?; + Ok(()) + }; + panic_cleanup().expect("failed to clean up for panic"); + panic_hook(panic); + })); +} +``` + +As a general rule, you want to take the original panic hook and execute it after cleaning up the +terminal. In the next sections we will discuss some third party packages that can help give better +stacktraces. diff --git a/src/how-to/handle-panics/README.md b/src/how-to/handle-panics/README.md deleted file mode 100644 index c04e90469..000000000 --- a/src/how-to/handle-panics/README.md +++ /dev/null @@ -1 +0,0 @@ -# Handle Panics diff --git a/src/how-to/handle-panics/with-better-panic.md b/src/how-to/handle-panics/with-better-panic.md deleted file mode 100644 index eca04e825..000000000 --- a/src/how-to/handle-panics/with-better-panic.md +++ /dev/null @@ -1 +0,0 @@ -# Using better-panic diff --git a/src/how-to/handle-panics/with-panic-hooks.md b/src/how-to/handle-panics/with-panic-hooks.md deleted file mode 100644 index 17f2dd1f0..000000000 --- a/src/how-to/handle-panics/with-panic-hooks.md +++ /dev/null @@ -1 +0,0 @@ -# Setup Panic Hooks diff --git a/src/tutorial/counter-app/multiple-files.md b/src/tutorial/counter-app/multiple-files.md index 17a9b4b3d..809d196e5 100644 --- a/src/tutorial/counter-app/multiple-files.md +++ b/src/tutorial/counter-app/multiple-files.md @@ -39,6 +39,13 @@ Instead of `anyhow` you can also use [`eyre`](https://github.com/eyre-rs/eyre) o + use color_eyre::eyre::Result; ``` +You'll need to add `color-eyre` and remove `anyhow`: + +```console +cargo remove anyhow +cargo add color-eyre +``` + If you are using `color_eyre`, you'll also want to add `color_eyre::install()?` to the beginning of your `main()` function: @@ -52,7 +59,10 @@ fn main() -> Result<()> { } ``` -`color_eyre` is an error report handler for colorful, consistent, and well formatted error reports for all kinds of errors. +`color_eyre` is an error report handler for colorful, consistent, and well formatted error +reports for all kinds of errors. +Check out the [section](../../how-to/develop-apps/setup-panic-hooks-color-eyre.md) for +setting up panic hooks with color-eyre. ```` Now we are ready to start refactoring our app. diff --git a/src/tutorial/counter-app/multiple-functions.md b/src/tutorial/counter-app/multiple-functions.md index 1282038b9..01183e0b2 100644 --- a/src/tutorial/counter-app/multiple-functions.md +++ b/src/tutorial/counter-app/multiple-functions.md @@ -34,7 +34,7 @@ pub type Frame<'a> = ratatui::Frame<'a, CrosstermBackend>; ``` ````admonish tip -If you use the popular [`anyhow`](https://docs.rs/anyhow/latest/anyhow/) crate, +If you use the popular [`anyhow`](https://docs.rs/anyhow/latest/anyhow/) then instead of these two lines: ```rust @@ -42,7 +42,7 @@ type Err = Box; type Result = std::result::Result; ``` -you can simply import `anyhow::Result`: +you can simply import `Result` from `anyhow`: ```rust use anyhow::Result; @@ -334,5 +334,6 @@ What do you think happens if you modify the example above to change the polling What would happen if you change the example to poll every 10 seconds? Experiment with different "tick rates" and see how that affects the user experience. -Also, monitor your CPU usage when you do this experiment. +Monitor your CPU usage when you do this experiment. +What happens to your CPU usage as you change the poll frequency? ```