Skip to content
This repository was archived by the owner on Aug 6, 2023. It is now read-only.

Commit a6b25a4

Browse files
chore: add panic hook example (#593)
Without a terminal-resetting panic hook there are two main problems when an application panics: 1. The report of the panic is distorted because the terminal has not properly left the alternate screen and is still in raw mode. 2. The terminal needs to be manually reset with the `reset` command. To avoid this, the standard panic hook can be extended to first reset the terminal.
1 parent 90d8cb6 commit a6b25a4

File tree

2 files changed

+146
-0
lines changed

2 files changed

+146
-0
lines changed

Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ required-features = ["crossterm"]
6464
name = "list"
6565
required-features = ["crossterm"]
6666

67+
[[example]]
68+
name = "panic"
69+
required-features = ["crossterm"]
70+
6771
[[example]]
6872
name = "paragraph"
6973
required-features = ["crossterm"]

examples/panic.rs

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
//! How to use a panic hook to reset the terminal before printing the panic to
2+
//! the terminal.
3+
//!
4+
//! When exiting normally or when handling `Result::Err`, we can reset the
5+
//! terminal manually at the end of `main` just before we print the error.
6+
//!
7+
//! Because a panic interrupts the normal control flow, manually resetting the
8+
//! terminal at the end of `main` won't do us any good. Instead, we need to
9+
//! make sure to set up a panic hook that first resets the terminal before
10+
//! handling the panic. This both reuses the standard panic hook to ensure a
11+
//! consistent panic handling UX and properly resets the terminal to not
12+
//! distort the output.
13+
//!
14+
//! That's why this example is set up to show both situations, with and without
15+
//! the chained panic hook, to see the difference.
16+
17+
#![deny(clippy::all)]
18+
#![warn(clippy::pedantic, clippy::nursery)]
19+
20+
use std::error::Error;
21+
use std::io;
22+
23+
use crossterm::event::{self, Event, KeyCode};
24+
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
25+
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
26+
27+
use tui::backend::{Backend, CrosstermBackend};
28+
use tui::layout::Alignment;
29+
use tui::text::Spans;
30+
use tui::widgets::{Block, Borders, Paragraph};
31+
use tui::{Frame, Terminal};
32+
33+
type Result<T> = std::result::Result<T, Box<dyn Error>>;
34+
35+
#[derive(Default)]
36+
struct App {
37+
hook_enabled: bool,
38+
}
39+
40+
impl App {
41+
fn chain_hook(&mut self) {
42+
let original_hook = std::panic::take_hook();
43+
44+
std::panic::set_hook(Box::new(move |panic| {
45+
reset_terminal().unwrap();
46+
original_hook(panic);
47+
}));
48+
49+
self.hook_enabled = true;
50+
}
51+
}
52+
53+
fn main() -> Result<()> {
54+
let mut terminal = init_terminal()?;
55+
56+
let mut app = App::default();
57+
let res = run_tui(&mut terminal, &mut app);
58+
59+
reset_terminal()?;
60+
61+
if let Err(err) = res {
62+
println!("{:?}", err);
63+
}
64+
65+
Ok(())
66+
}
67+
68+
/// Initializes the terminal.
69+
fn init_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> {
70+
crossterm::execute!(io::stdout(), EnterAlternateScreen)?;
71+
enable_raw_mode()?;
72+
73+
let backend = CrosstermBackend::new(io::stdout());
74+
75+
let mut terminal = Terminal::new(backend)?;
76+
terminal.hide_cursor()?;
77+
78+
Ok(terminal)
79+
}
80+
81+
/// Resets the terminal.
82+
fn reset_terminal() -> Result<()> {
83+
disable_raw_mode()?;
84+
crossterm::execute!(io::stdout(), LeaveAlternateScreen)?;
85+
86+
Ok(())
87+
}
88+
89+
/// Runs the TUI loop.
90+
fn run_tui<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<()> {
91+
loop {
92+
terminal.draw(|f| ui(f, app))?;
93+
94+
if let Event::Key(key) = event::read()? {
95+
match key.code {
96+
KeyCode::Char('p') => {
97+
panic!("intentional demo panic");
98+
}
99+
100+
KeyCode::Char('e') => {
101+
app.chain_hook();
102+
}
103+
104+
_ => {
105+
return Ok(());
106+
}
107+
}
108+
}
109+
}
110+
}
111+
112+
/// Render the TUI.
113+
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
114+
let text = vec![
115+
if app.hook_enabled {
116+
Spans::from("HOOK IS CURRENTLY **ENABLED**")
117+
} else {
118+
Spans::from("HOOK IS CURRENTLY **DISABLED**")
119+
},
120+
Spans::from(""),
121+
Spans::from("press `p` to panic"),
122+
Spans::from("press `e` to enable the terminal-resetting panic hook"),
123+
Spans::from("press any other key to quit without panic"),
124+
Spans::from(""),
125+
Spans::from("when you panic without the chained hook,"),
126+
Spans::from("you will likely have to reset your terminal afterwards"),
127+
Spans::from("with the `reset` command"),
128+
Spans::from(""),
129+
Spans::from("with the chained panic hook enabled,"),
130+
Spans::from("you should see the panic report as you would without tui"),
131+
Spans::from(""),
132+
Spans::from("try first without the panic handler to see the difference"),
133+
];
134+
135+
let b = Block::default()
136+
.title("Panic Handler Demo")
137+
.borders(Borders::ALL);
138+
139+
let p = Paragraph::new(text).block(b).alignment(Alignment::Center);
140+
141+
f.render_widget(p, f.size());
142+
}

0 commit comments

Comments
 (0)