Compare commits

...

14 Commits

Author SHA1 Message Date
Josh McKinney
37be39b15c chore(examples): simplify all examples
In <https://github.com/ratatui-org/ratatui/pull/1180> and
<https://github.com/ratatui-org/ratatui/pull/1181>, we introduced some
simpler ways to create a backend and terminal, and to setup color_eyre
to handle errors. This PR updates the examples to use these new methods,
and removes a bunch of unnecessary boilerplate code.
2024-06-17 22:55:38 -07:00
Josh McKinney
e7a4fd8fc3 fix: formatting and clippy 2024-06-17 20:22:29 -07:00
Josh McKinney
50eb504be9 fix: add std panic hook 2024-06-17 20:22:29 -07:00
Josh McKinney
8ed4a2b5a4 feat(color-eyre): Add a default color-eyre feature
Apps can now enable color-eyre hooks using the with_color_eyre_hooks
method on CrosstermBackend. This is also added to the default features
in the Cargo.toml file, and the defaults that are applied to terminals
created using CrosstermBackend::into_terminal_with_defaults.
2024-06-17 20:22:29 -07:00
Josh McKinney
706bdd6116 feat(backend): Add convenience methods for setting up terminals
Adds:
- `CrosstermBackend::stdout` and `CrosstermBackend::stderr` for creating
  backends with `std::io::stdout` and `std::io::stderr` respectively.
- `CrosstermBackend::into_terminal` for converting a backend into a
  terminal.
- `CrosstermBackend::into_terminal_with_options` for converting a
  backend into a terminal with options.
- `CrosstermBackend::into_terminal_with_defaults` for converting a
  backend into a terminal with default settings (raw mode and alternate
  screen).
- `CrosstermBackend::with_raw_mode` for enabling raw mode.
- `CrosstermBackend::with_alternate_screen` for switching to the
  alternate screen.
- `CrosstermBackend::with_mouse_support` for enabling mouse support.
- `CrosstermBackend::reset` for resetting the terminal to its default
  state.
- `Drop` implementation for restoring raw mode, mouse capture, and
  alternate screen on drop.

Most applications can now just use the `into_terminal_with_defaults`
method to set up the terminal with raw mode and the alternate screen,
and now longer need to worry about restoring the terminal to its default
state when the application exits. The reset method can be used to
restore the terminal to its default state if needed (e.g. in a panic
handler).

```rust
use ratatui::backend::CrosstermBackend;

let terminal = CrosstermBackend::stdout()
     .into_terminal_with_defaults()?;
```
2024-06-17 20:22:29 -07:00
Josh McKinney
cd525c3356 fix: example imports and added a doc comment for reset 2024-06-17 18:46:48 -07:00
Josh McKinney
0c96035c34 fix: remove terminal::self 2024-06-17 18:38:32 -07:00
Josh McKinney
ce40f4e315 fix: doc fix 2024-06-16 16:05:33 -07:00
Josh McKinney
b2baa9d0c9 fix: formatting and clippy 2024-06-16 15:28:57 -07:00
Josh McKinney
1d8bfdf7c7 fix: import 2024-06-16 15:23:32 -07:00
Josh McKinney
def85b7401 fix: move to_terminal to Backend, added other configs and review related changes 2024-06-16 15:21:32 -07:00
Josh McKinney
1d176d9307 docs(examples): use backend convenience methods for colors_rgb example 2024-06-15 18:28:09 -07:00
Josh McKinney
bf43eff17c docs(examples): use backend convenience methods for minimal example 2024-06-15 18:28:09 -07:00
Josh McKinney
09e263b4ae feat(backend): Add convenience methods for setting up terminals
Adds:
- `CrosstermBackend::stdout` and `CrosstermBackend::stderr` for creating
  backends with `std::io::stdout` and `std::io::stderr` respectively.
- `CrosstermBackend::into_terminal` for converting a backend into a
  terminal.
- `CrosstermBackend::into_terminal_with_options` for converting a
  backend into a terminal with options.
- `CrosstermBackend::into_terminal_with_defaults` for converting a
  backend into a terminal with default settings (raw mode and alternate
  screen).
- `CrosstermBackend::with_raw_mode` for enabling raw mode.
- `CrosstermBackend::with_alternate_screen` for switching to the
  alternate screen.
- `CrosstermBackend::with_mouse_support` for enabling mouse support.
- `CrosstermBackend::reset` for resetting the terminal to its default
  state.
- `Drop` implementation for restoring raw mode, mouse capture, and
  alternate screen on drop.

Most applications can now just use the `into_terminal_with_defaults`
method to set up the terminal with raw mode and the alternate screen,
and now longer need to worry about restoring the terminal to its default
state when the application exits. The reset method can be used to
restore the terminal to its default state if needed (e.g. in a panic
handler).

```rust
use ratatui::backend::CrosstermBackend;

let terminal = CrosstermBackend::stdout()
     .into_terminal_with_defaults()?;
```
2024-06-15 18:28:09 -07:00
40 changed files with 906 additions and 1458 deletions

View File

@@ -28,6 +28,7 @@ rust-version = "1.74.0"
bitflags = "2.3"
cassowary = "0.3"
compact_str = "0.7.1"
color-eyre = { version = "0.6.2", optional = true }
crossterm = { version = "0.27", optional = true }
document-features = { version = "0.2.7", optional = true }
itertools = "0.13"
@@ -106,7 +107,7 @@ use_self = "warn"
## By default, we enable the crossterm backend as this is a reasonable choice for most applications
## as it is supported on Linux/Mac/Windows systems. We also enable the `underline-color` feature
## which allows you to set the underline color of text.
default = ["crossterm", "underline-color"]
default = ["crossterm", "underline-color", "color-eyre"]
#! Generally an application will only use one backend, so you should only enable one of the following features:
## enables the [`CrosstermBackend`](backend::CrosstermBackend) backend and adds a dependency on [`crossterm`].
crossterm = ["dep:crossterm"]
@@ -116,6 +117,12 @@ termion = ["dep:termion"]
termwiz = ["dep:termwiz"]
#! The following optional features are available for all backends:
## enables the [`color-eyre`](color_eyre) crate which provides a better error handling experience.
## See [`CrosstermBackend::with_color_eyre_hooks`](crate::backend::CrosstermBackend::with_color_eyre_hooks)
## for more details.
color-eyre = ["dep:color-eyre"]
## enables serialization and deserialization of style and color types using the [`serde`] crate.
## This is useful if you want to save themes to a file.
serde = ["dep:serde", "bitflags/serde", "compact_str/serde"]

View File

@@ -13,22 +13,15 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use std::time::{Duration, Instant};
use color_eyre::Result;
use ratatui::{
backend::{Backend, CrosstermBackend},
crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
crossterm::event::{self, Event, KeyCode},
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
terminal::{Frame, Terminal},
terminal::Frame,
text::{Line, Span},
widgets::{Bar, BarChart, BarGroup, Block, Paragraph},
};
@@ -103,40 +96,13 @@ impl<'a> App<'a> {
}
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
fn main() -> Result<()> {
let mut terminal = CrosstermBackend::stdout_with_defaults()?
.with_mouse_capture()?
.to_terminal()?;
// create app and run it
let tick_rate = Duration::from_millis(250);
let app = App::new();
let res = run_app(&mut terminal, app, tick_rate);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut app = App::new();
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &app))?;

View File

@@ -13,21 +13,11 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{
error::Error,
io::{stdout, Stdout},
ops::ControlFlow,
time::Duration,
};
use color_eyre::Result;
use itertools::Itertools;
use ratatui::{
backend::CrosstermBackend,
crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
backend::{Backend, CrosstermBackend},
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Alignment, Constraint, Layout, Rect},
style::{Style, Stylize},
terminal::Frame,
@@ -38,55 +28,16 @@ use ratatui::{
},
};
// These type aliases are used to make the code more readable by reducing repetition of the generic
// types. They are not necessary for the functionality of the code.
type Terminal = ratatui::Terminal<CrosstermBackend<Stdout>>;
type Result<T> = std::result::Result<T, Box<dyn Error>>;
fn main() -> Result<()> {
let mut terminal = setup_terminal()?;
let result = run(&mut terminal);
restore_terminal(terminal)?;
if let Err(err) = result {
eprintln!("{err:?}");
}
Ok(())
}
fn setup_terminal() -> Result<Terminal> {
enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
fn restore_terminal(mut terminal: Terminal) -> Result<()> {
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
Ok(())
}
fn run(terminal: &mut Terminal) -> Result<()> {
let mut terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal()?;
loop {
terminal.draw(ui)?;
if handle_events()?.is_break() {
return Ok(());
}
}
}
fn handle_events() -> Result<ControlFlow<()>> {
if event::poll(Duration::from_millis(100))? {
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('q') {
return Ok(ControlFlow::Break(()));
if let Event::Key(event) = event::read()? {
if event.kind == KeyEventKind::Press && event.code == KeyCode::Char('q') {
return Ok(());
}
}
}
Ok(ControlFlow::Continue(()))
}
fn ui(frame: &mut Frame) {

View File

@@ -13,50 +13,31 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{error::Error, io};
use color_eyre::Result;
use crossterm::event::KeyEventKind;
use ratatui::{
backend::CrosstermBackend,
crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
backend::{Backend, CrosstermBackend},
crossterm::event::{self, Event, KeyCode},
layout::{Constraint, Layout, Rect},
style::{Color, Modifier, Style},
widgets::calendar::{CalendarEventStore, DateStyler, Monthly},
Frame, Terminal,
Frame,
};
use time::{Date, Month, OffsetDateTime};
fn main() -> Result<(), Box<dyn Error>> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
fn main() -> Result<()> {
let mut terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal()?;
loop {
let _ = terminal.draw(draw);
terminal.draw(ui)?;
if let Event::Key(key) = event::read()? {
#[allow(clippy::single_match)]
match key.code {
KeyCode::Char(_) => {
break;
}
_ => {}
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
return Ok(());
};
}
}
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
Ok(())
}
fn draw(frame: &mut Frame) {
fn ui(frame: &mut Frame) {
let app_area = frame.size();
let calarea = Rect {

View File

@@ -13,30 +13,26 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{
io::{self, stdout, Stdout},
time::{Duration, Instant},
};
use std::time::{Duration, Instant};
use color_eyre::Result;
use ratatui::{
backend::CrosstermBackend,
crossterm::{
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
},
backend::{Backend, CrosstermBackend},
crossterm::event::{self, Event, KeyCode},
layout::{Constraint, Layout, Rect},
style::{Color, Stylize},
symbols::Marker,
terminal::{Frame, Terminal},
terminal::Frame,
widgets::{
canvas::{Canvas, Circle, Map, MapResolution, Rectangle},
Block, Widget,
},
Terminal,
};
fn main() -> io::Result<()> {
App::run()
fn main() -> Result<()> {
let terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal()?;
App::new().run(terminal)
}
struct App {
@@ -69,33 +65,31 @@ impl App {
}
}
pub fn run() -> io::Result<()> {
let mut terminal = init_terminal()?;
let mut app = Self::new();
pub fn run(mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
let mut last_tick = Instant::now();
let tick_rate = Duration::from_millis(16);
loop {
let _ = terminal.draw(|frame| app.ui(frame));
let _ = terminal.draw(|frame| self.ui(frame));
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => break,
KeyCode::Down | KeyCode::Char('j') => app.y += 1.0,
KeyCode::Up | KeyCode::Char('k') => app.y -= 1.0,
KeyCode::Right | KeyCode::Char('l') => app.x += 1.0,
KeyCode::Left | KeyCode::Char('h') => app.x -= 1.0,
KeyCode::Down | KeyCode::Char('j') => self.y += 1.0,
KeyCode::Up | KeyCode::Char('k') => self.y -= 1.0,
KeyCode::Right | KeyCode::Char('l') => self.x += 1.0,
KeyCode::Left | KeyCode::Char('h') => self.x -= 1.0,
_ => {}
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
self.on_tick();
last_tick = Instant::now();
}
}
restore_terminal()
Ok(())
}
fn on_tick(&mut self) {
@@ -204,15 +198,3 @@ impl App {
})
}
}
fn init_terminal() -> io::Result<Terminal<CrosstermBackend<Stdout>>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
Terminal::new(CrosstermBackend::new(stdout()))
}
fn restore_terminal() -> io::Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}

View File

@@ -13,23 +13,16 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use std::time::{Duration, Instant};
use color_eyre::Result;
use ratatui::{
backend::{Backend, CrosstermBackend},
crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
crossterm::event::{self, Event, KeyCode},
layout::{Alignment, Constraint, Layout, Rect},
style::{Color, Modifier, Style, Stylize},
symbols::{self, Marker},
terminal::{Frame, Terminal},
terminal::Frame,
text::Span,
widgets::{block::Title, Axis, Block, Chart, Dataset, GraphType, LegendPosition},
};
@@ -97,40 +90,13 @@ impl App {
}
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
fn main() -> Result<()> {
let mut terminal = CrosstermBackend::stdout_with_defaults()?
.with_mouse_capture()?
.to_terminal()?;
// create app and run it
let tick_rate = Duration::from_millis(250);
let app = App::new();
let res = run_app(&mut terminal, app, tick_rate);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut app = App::new();
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &app))?;

View File

@@ -16,49 +16,25 @@
// This example shows all the colors supported by ratatui. It will render a grid of foreground
// and background colors with their names and indexes.
use std::{
error::Error,
io::{self, Stdout},
result,
time::Duration,
};
use color_eyre::Result;
use itertools::Itertools;
use ratatui::{
backend::{Backend, CrosstermBackend},
crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
crossterm::event::{self, Event, KeyCode},
layout::{Alignment, Constraint, Layout, Rect},
style::{Color, Style, Stylize},
terminal::{Frame, Terminal},
terminal::Frame,
text::Line,
widgets::{Block, Borders, Paragraph},
};
type Result<T> = result::Result<T, Box<dyn Error>>;
fn main() -> Result<()> {
let mut terminal = setup_terminal()?;
let res = run_app(&mut terminal);
restore_terminal(terminal)?;
if let Err(err) = res {
eprintln!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
let mut terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal()?;
loop {
terminal.draw(ui)?;
if event::poll(Duration::from_millis(250))? {
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('q') {
return Ok(());
}
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('q') {
return Ok(());
}
}
}
@@ -271,20 +247,3 @@ fn render_indexed_grayscale(frame: &mut Frame, area: Rect) {
frame.render_widget(paragraph, layout[i as usize - 232]);
}
}
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
Ok(terminal)
}
fn restore_terminal(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
Ok(())
}

View File

@@ -26,27 +26,19 @@
// is useful when the state is only used by the widget and doesn't need to be shared with
// other widgets.
use std::{
io::stdout,
panic,
time::{Duration, Instant},
};
use std::time::{Duration, Instant};
use color_eyre::{config::HookBuilder, eyre, Result};
use color_eyre::Result;
use palette::{convert::FromColorUnclamped, Okhsv, Srgb};
use ratatui::{
backend::{Backend, CrosstermBackend},
buffer::Buffer,
crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
},
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Constraint, Layout, Rect},
style::Color,
terminal::Terminal,
text::Text,
widgets::Widget,
Terminal,
};
#[derive(Debug, Default)]
@@ -100,11 +92,8 @@ struct ColorsWidget {
}
fn main() -> Result<()> {
install_error_hooks()?;
let terminal = init_terminal()?;
App::default().run(terminal)?;
restore_terminal()?;
Ok(())
let terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal()?;
App::default().run(terminal)
}
impl App {
@@ -128,9 +117,9 @@ impl App {
/// Currently, this only handles the q key to quit the app.
fn handle_events(&mut self) -> Result<()> {
// Ensure that the app only blocks for a period that allows the app to render at
// approximately 60 FPS (this doesn't account for the time to render the frame, and will
// approximately 50 FPS (this doesn't account for the time to render the frame, and will
// also update the app immediately any time an event occurs)
let timeout = Duration::from_secs_f32(1.0 / 60.0);
let timeout = Duration::from_secs_f32(1.0 / 50.0); // 50 FPS is standard for GIFs
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
@@ -263,36 +252,3 @@ impl ColorsWidget {
}
}
}
/// Install `color_eyre` panic and error hooks
///
/// The hooks restore the terminal to a usable state before printing the error message.
fn install_error_hooks() -> Result<()> {
let (panic, error) = HookBuilder::default().into_hooks();
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
eyre::set_hook(Box::new(move |e| {
let _ = restore_terminal();
error(e)
}))?;
panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
panic(info);
}));
Ok(())
}
fn init_terminal() -> Result<Terminal<impl Backend>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
terminal.clear()?;
terminal.hide_cursor()?;
Ok(terminal)
}
fn restore_terminal() -> Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}

View File

@@ -13,18 +13,12 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::io::{self, stdout};
use color_eyre::{config::HookBuilder, Result};
use color_eyre::Result;
use itertools::Itertools;
use ratatui::{
backend::{Backend, CrosstermBackend},
buffer::Buffer,
crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
},
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
Flex, Layout, Rect,
@@ -91,16 +85,13 @@ struct ConstraintBlock {
struct SpacerBlock;
fn main() -> Result<()> {
init_error_hooks()?;
let terminal = init_terminal()?;
App::default().run(terminal)?;
restore_terminal()?;
Ok(())
let terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal()?;
App::default().run(terminal)
}
// App behaviour
impl App {
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
fn run(mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
self.insert_test_defaults();
while self.is_running() {
@@ -124,7 +115,7 @@ impl App {
self.mode == AppMode::Running
}
fn draw(&self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
fn draw(&self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
terminal.draw(|frame| frame.render_widget(self, frame.size()))?;
Ok(())
}
@@ -621,32 +612,3 @@ impl ConstraintName {
}
}
}
fn init_error_hooks() -> Result<()> {
let (panic, error) = HookBuilder::default().into_hooks();
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
color_eyre::eyre::set_hook(Box::new(move |e| {
let _ = restore_terminal();
error(e)
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
panic(info);
}));
Ok(())
}
fn init_terminal() -> Result<Terminal<impl Backend>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
fn restore_terminal() -> Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}

View File

@@ -13,17 +13,11 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::io::{self, stdout};
use color_eyre::{config::HookBuilder, Result};
use color_eyre::Result;
use ratatui::{
backend::{Backend, CrosstermBackend},
buffer::Buffer,
crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
},
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
Layout, Rect,
@@ -83,21 +77,14 @@ enum AppState {
}
fn main() -> Result<()> {
init_error_hooks()?;
let terminal = init_terminal()?;
// increase the cache size to avoid flickering for indeterminate layouts
Layout::init_cache(100);
App::default().run(terminal)?;
restore_terminal()?;
Ok(())
let terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal()?;
App::default().run(terminal)
}
impl App {
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
fn run(mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
self.update_max_scroll_offset();
while self.is_running() {
self.draw(&mut terminal)?;
@@ -114,7 +101,7 @@ impl App {
self.state == AppState::Running
}
fn draw(self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
fn draw(self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
terminal.draw(|frame| frame.render_widget(self, frame.size()))?;
Ok(())
}
@@ -421,32 +408,3 @@ impl Example {
Paragraph::new(text).centered().block(block)
}
}
fn init_error_hooks() -> Result<()> {
let (panic, error) = HookBuilder::default().into_hooks();
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
color_eyre::eyre::set_hook(Box::new(move |e| {
let _ = restore_terminal();
error(e)
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
panic(info);
}));
Ok(())
}
fn init_terminal() -> Result<Terminal<impl Backend>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
fn restore_terminal() -> Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}

View File

@@ -13,19 +13,13 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{error::Error, io, ops::ControlFlow, time::Duration};
use std::{ops::ControlFlow, time::Duration};
use color_eyre::Result;
use ratatui::{
backend::{Backend, CrosstermBackend},
buffer::Buffer,
crossterm::{
event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, MouseButton, MouseEvent,
MouseEventKind,
},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
crossterm::event::{self, Event, KeyCode, MouseButton, MouseEvent, MouseEventKind},
layout::{Constraint, Layout, Rect},
style::{Color, Style},
terminal::{Frame, Terminal},
@@ -143,34 +137,14 @@ impl Button<'_> {
}
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let res = run_app(&mut terminal);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
fn main() -> Result<()> {
let terminal = CrosstermBackend::stdout_with_defaults()?
.with_mouse_capture()?
.to_terminal()?;
run_app(terminal)
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
fn run_app(mut terminal: Terminal<impl Backend>) -> Result<()> {
let mut selected_button: usize = 0;
let mut button_states = [State::Selected, State::Normal, State::Normal];
loop {
@@ -180,11 +154,10 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
}
match event::read()? {
Event::Key(key) => {
if key.kind != event::KeyEventKind::Press {
continue;
}
if handle_key_event(key, &mut button_states, &mut selected_button).is_break() {
break;
if key.kind == event::KeyEventKind::Press
&& handle_key_event(key, &mut button_states, &mut selected_button).is_break()
{
return Ok(());
}
}
Event::Mouse(mouse) => {
@@ -193,7 +166,6 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
_ => (),
}
}
Ok(())
}
fn ui(frame: &mut Frame, states: [State; 3]) {

View File

@@ -1,56 +1,26 @@
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use std::time::{Duration, Instant};
use color_eyre::Result;
use ratatui::{
backend::{Backend, CrosstermBackend},
crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
terminal::Terminal,
crossterm::event::{self, Event, KeyCode, KeyEventKind},
Terminal,
};
use crate::{app::App, ui};
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<()> {
let terminal = CrosstermBackend::stdout_with_defaults()?
.with_mouse_capture()?
.to_terminal()?;
// create app and run it
let app = App::new("Crossterm Demo", enhanced_graphics);
let res = run_app(&mut terminal, app, tick_rate);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
run_app(app, terminal, tick_rate)
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
fn run_app(mut app: App, mut terminal: Terminal<impl Backend>, tick_rate: Duration) -> Result<()> {
let mut last_tick = Instant::now();
loop {
while !app.should_quit {
terminal.draw(|f| ui::draw(f, &mut app))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
@@ -72,8 +42,6 @@ fn run_app<B: Backend>(
app.on_tick();
last_tick = Instant::now();
}
if app.should_quit {
return Ok(());
}
}
Ok(())
}

View File

@@ -13,9 +13,10 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{error::Error, time::Duration};
use std::time::Duration;
use argh::FromArgs;
use color_eyre::Result;
mod app;
#[cfg(feature = "crossterm")]
@@ -38,7 +39,7 @@ struct Cli {
enhanced_graphics: bool,
}
fn main() -> Result<(), Box<dyn Error>> {
fn main() -> Result<()> {
let cli: Cli = argh::from_env();
let tick_rate = Duration::from_millis(cli.tick_rate);
#[cfg(feature = "crossterm")]

View File

@@ -1,5 +1,6 @@
use std::{error::Error, io, sync::mpsc, thread, time::Duration};
use std::{io, sync::mpsc, thread, time::Duration};
use color_eyre::Result;
use ratatui::{
backend::{Backend, TermionBackend},
terminal::Terminal,
@@ -13,7 +14,7 @@ use ratatui::{
use crate::{app::App, ui};
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<()> {
// setup terminal
let stdout = io::stdout()
.into_raw_mode()
@@ -22,22 +23,17 @@ pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn E
.unwrap();
let stdout = MouseTerminal::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let terminal = Terminal::new(backend)?;
// create app and run it
let app = App::new("Termion demo", enhanced_graphics);
run_app(&mut terminal, app, tick_rate)?;
run_app(app, terminal, tick_rate)?;
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> Result<(), Box<dyn Error>> {
fn run_app(mut app: App, mut terminal: Terminal<impl Backend>, tick_rate: Duration) -> Result<()> {
let events = events(tick_rate);
loop {
while !app.should_quit {
terminal.draw(|f| ui::draw(f, &mut app))?;
match events.recv()? {
@@ -51,10 +47,8 @@ fn run_app<B: Backend>(
},
Event::Tick => app.on_tick(),
}
if app.should_quit {
return Ok(());
}
}
Ok(())
}
enum Event {

View File

@@ -1,8 +1,6 @@
use std::{
error::Error,
time::{Duration, Instant},
};
use std::time::{Duration, Instant};
use color_eyre::{eyre::eyre, Result};
use ratatui::{
backend::TermwizBackend,
terminal::Terminal,
@@ -14,14 +12,14 @@ use ratatui::{
use crate::{app::App, ui};
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
let backend = TermwizBackend::new()?;
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<()> {
let backend =
TermwizBackend::new().map_err(|error| eyre!("failed to init termwiz backend. {error}"))?;
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
// create app and run it
let app = App::new("Termwiz Demo", enhanced_graphics);
let res = run_app(&mut terminal, app, tick_rate);
let res = run_app(app, &mut terminal, tick_rate);
terminal.show_cursor()?;
terminal.flush()?;
@@ -34,12 +32,12 @@ pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn E
}
fn run_app(
terminal: &mut Terminal<TermwizBackend>,
mut app: App,
terminal: &mut Terminal<TermwizBackend>,
tick_rate: Duration,
) -> Result<(), Box<dyn Error>> {
) -> Result<()> {
let mut last_tick = Instant::now();
loop {
while !app.should_quit {
terminal.draw(|f| ui::draw(f, &mut app))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
@@ -72,8 +70,6 @@ fn run_app(
app.on_tick();
last_tick = Instant::now();
}
if app.should_quit {
return Ok(());
}
}
Ok(())
}

View File

@@ -1,6 +1,7 @@
use std::time::Duration;
use color_eyre::{eyre::Context, Result};
use crossterm::event;
use itertools::Itertools;
use ratatui::{
backend::Backend,
@@ -17,7 +18,7 @@ use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
use crate::{
destroy,
tabs::{AboutTab, EmailTab, RecipeTab, TracerouteTab, WeatherTab},
term, THEME,
THEME,
};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
@@ -49,15 +50,11 @@ enum Tab {
Weather,
}
pub fn run(terminal: &mut Terminal<impl Backend>) -> Result<()> {
App::default().run(terminal)
}
impl App {
/// Run the app until the user quits.
pub fn run(&mut self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
pub fn run(mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
while self.is_running() {
self.draw(terminal)?;
self.draw(&mut terminal)?;
self.handle_events()?;
}
Ok(())
@@ -86,13 +83,21 @@ impl App {
/// 1/50th of a second. This was chosen to try to match the default frame rate of a GIF in VHS.
fn handle_events(&mut self) -> Result<()> {
let timeout = Duration::from_secs_f64(1.0 / 50.0);
match term::next_event(timeout)? {
match Self::next_event(timeout)? {
Some(Event::Key(key)) if key.kind == KeyEventKind::Press => self.handle_key_press(key),
_ => {}
}
Ok(())
}
pub fn next_event(timeout: Duration) -> Result<Option<Event>> {
if !event::poll(timeout)? {
return Ok(None);
}
let event = event::read()?;
Ok(Some(event))
}
fn handle_key_press(&mut self, key: KeyEvent) {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => self.mode = Mode::Quit,

View File

@@ -1,18 +0,0 @@
use color_eyre::{config::HookBuilder, Result};
use crate::term;
pub fn init_hooks() -> Result<()> {
let (panic, error) = HookBuilder::default().into_hooks();
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
color_eyre::eyre::set_hook(Box::new(move |e| {
let _ = term::restore();
error(e)
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = term::restore();
panic(info);
}));
Ok(())
}

View File

@@ -23,12 +23,16 @@ mod app;
mod big_text;
mod colors;
mod destroy;
mod errors;
mod tabs;
mod term;
mod theme;
use app::App;
use color_eyre::Result;
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::Rect,
TerminalOptions, Viewport,
};
pub use self::{
colors::{color_from_oklab, RgbSwatch},
@@ -36,9 +40,11 @@ pub use self::{
};
fn main() -> Result<()> {
errors::init_hooks()?;
let terminal = &mut term::init()?;
app::run(terminal)?;
term::restore()?;
Ok(())
// this size is to match the size of the terminal when running the demo
// using vhs in a 1280x640 sized window (github social preview size)
let options = TerminalOptions {
viewport: Viewport::Fixed(Rect::new(0, 0, 81, 18)),
};
let terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal_with_options(options)?;
App::default().run(terminal)
}

View File

@@ -13,18 +13,13 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::io::{self, stdout};
use color_eyre::Result;
use ratatui::{
backend::CrosstermBackend,
crossterm::{
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
},
backend::{Backend, CrosstermBackend},
crossterm::event::{self, Event, KeyCode},
layout::{Constraint, Layout},
style::{Color, Modifier, Style, Stylize},
terminal::{Frame, Terminal},
terminal::Frame,
text::{Line, Span, Text},
widgets::{Block, Borders, Paragraph},
};
@@ -33,12 +28,9 @@ use ratatui::{
///
/// When cargo-rdme supports doc comments that import from code, this will be imported
/// rather than copied to the lib.rs file.
fn main() -> io::Result<()> {
fn main() -> Result<()> {
let arg = std::env::args().nth(1).unwrap_or_default();
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
let mut terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal()?;
let mut should_quit = false;
while !should_quit {
terminal.draw(match arg.as_str() {
@@ -48,9 +40,6 @@ fn main() -> io::Result<()> {
})?;
should_quit = handle_events()?;
}
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
@@ -61,7 +50,7 @@ fn hello_world(frame: &mut Frame) {
);
}
fn handle_events() -> io::Result<bool> {
fn handle_events() -> Result<bool> {
if event::poll(std::time::Duration::from_millis(50))? {
if let Event::Key(key) = event::read()? {
if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {

View File

@@ -13,17 +13,11 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::io::{self, stdout};
use color_eyre::{config::HookBuilder, Result};
use color_eyre::Result;
use ratatui::{
backend::{Backend, CrosstermBackend},
buffer::Buffer,
crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
},
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{
Alignment,
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
@@ -157,16 +151,12 @@ enum SelectedTab {
fn main() -> Result<()> {
// assuming the user changes spacing about a 100 times or so
Layout::init_cache(EXAMPLE_DATA.len() * SelectedTab::iter().len() * 100);
init_error_hooks()?;
let terminal = init_terminal()?;
App::default().run(terminal)?;
restore_terminal()?;
Ok(())
let terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal()?;
App::default().run(terminal)
}
impl App {
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
fn run(mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
self.draw(&mut terminal)?;
while self.is_running() {
self.handle_events()?;
@@ -179,7 +169,7 @@ impl App {
self.state == AppState::Running
}
fn draw(self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
fn draw(self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
terminal.draw(|frame| frame.render_widget(self, frame.size()))?;
Ok(())
}
@@ -527,35 +517,6 @@ const fn color_for_constraint(constraint: Constraint) -> Color {
}
}
fn init_error_hooks() -> Result<()> {
let (panic, error) = HookBuilder::default().into_hooks();
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
color_eyre::eyre::set_hook(Box::new(move |e| {
let _ = restore_terminal();
error(e)
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
panic(info);
}));
Ok(())
}
fn init_terminal() -> Result<Terminal<impl Backend>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
fn restore_terminal() -> Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
#[allow(clippy::cast_possible_truncation)]
fn get_description_height(s: &str) -> u16 {
if s.is_empty() {

View File

@@ -13,17 +13,13 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{io::stdout, time::Duration};
use std::time::Duration;
use color_eyre::{config::HookBuilder, Result};
use color_eyre::Result;
use ratatui::{
backend::{Backend, CrosstermBackend},
buffer::Buffer,
crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
},
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Alignment, Constraint, Layout, Rect},
style::{palette::tailwind, Color, Style, Stylize},
terminal::Terminal,
@@ -56,15 +52,12 @@ enum AppState {
}
fn main() -> Result<()> {
init_error_hooks()?;
let terminal = init_terminal()?;
App::default().run(terminal)?;
restore_terminal()?;
Ok(())
let terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal()?;
App::default().run(terminal)
}
impl App {
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
fn run(mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
while self.state != AppState::Quitting {
self.draw(&mut terminal)?;
self.handle_events()?;
@@ -213,32 +206,3 @@ fn title_block(title: &str) -> Block {
.title(title)
.fg(CUSTOM_LABEL_COLOR)
}
fn init_error_hooks() -> color_eyre::Result<()> {
let (panic, error) = HookBuilder::default().into_hooks();
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
color_eyre::eyre::set_hook(Box::new(move |e| {
let _ = restore_terminal();
error(e)
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
panic(info);
}));
Ok(())
}
fn init_terminal() -> color_eyre::Result<Terminal<impl Backend>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
fn restore_terminal() -> color_eyre::Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}

View File

@@ -13,20 +13,11 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{
io::{self, Stdout},
time::Duration,
};
use anyhow::{Context, Result};
use color_eyre::{eyre::WrapErr, Result};
use crossterm::event::KeyEventKind;
use ratatui::{
backend::CrosstermBackend,
crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
terminal::{Frame, Terminal},
backend::{Backend, CrosstermBackend},
crossterm::event::{self, Event, KeyCode},
widgets::Paragraph,
};
@@ -34,66 +25,36 @@ use ratatui::{
/// this is not meant to be prescriptive. It is only meant to demonstrate the basic setup and
/// teardown of a terminal application.
///
/// A more robust application would probably want to handle errors and ensure that the terminal is
/// restored to a sane state before exiting. This example does not do that. It also does not handle
/// events or update the application state. It just draws a greeting and exits when the user
/// presses 'q'.
/// A more robust application would probably want to handle errors more thouroughly. It also does
/// not handle events or update any application state. It just draws a greeting and exits when the
/// user presses 'q'.
fn main() -> Result<()> {
let mut terminal = setup_terminal().context("setup failed")?;
run(&mut terminal).context("app loop failed")?;
restore_terminal(&mut terminal).context("restore terminal failed")?;
Ok(())
}
let mut terminal = CrosstermBackend::stdout_with_defaults()?
.to_terminal()
.wrap_err("failed to start terminal")?;
/// Setup the terminal. This is where you would enable raw mode, enter the alternate screen, and
/// hide the cursor. This example does not handle errors. A more robust application would probably
/// want to handle errors and ensure that the terminal is restored to a sane state before exiting.
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
let mut stdout = io::stdout();
enable_raw_mode().context("failed to enable raw mode")?;
execute!(stdout, EnterAlternateScreen).context("unable to enter alternate screen")?;
Terminal::new(CrosstermBackend::new(stdout)).context("creating terminal failed")
}
/// Restore the terminal. This is where you disable raw mode, leave the alternate screen, and show
/// the cursor.
fn restore_terminal(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
disable_raw_mode().context("failed to disable raw mode")?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)
.context("unable to switch to main screen")?;
terminal.show_cursor().context("unable to show cursor")
}
/// Run the application loop. This is where you would handle events and update the application
/// state. This example exits when the user presses 'q'. Other styles of application loops are
/// possible, for example, you could have multiple application states and switch between them based
/// on events, or you could have a single application state and update it based on events.
fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
// Run the application loop. This is where you would handle events and update the application
// state. This example exits when the user presses 'q'. Other styles of application loops are
// possible, for example, you could have multiple application states and switch between them
// based on events, or you could have a single application state and update it based on events.
loop {
terminal.draw(crate::render_app)?;
if should_quit()? {
break;
terminal
.draw(|frame| {
// Render the application. This is where you would draw the application UI. This
// example just draws a greeting.
let greeting = Paragraph::new("Hello World! (press 'q' to quit)");
frame.render_widget(greeting, frame.size());
})
.wrap_err("failed to draw")?;
// Check if the user has pressed 'q'. This is where you would handle events. This example
// just checks if the user has pressed 'q' and returns true if they have. It does not
// handle any other events. This is a very basic event loop. A more robust application
// would probably want to handle more events and consider not blocking the thread when
// there are no events (by using [`crossterm::event::poll`])
if let Event::Key(key) = event::read().wrap_err("failed to read events")? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
return Ok(());
}
}
}
Ok(())
}
/// Render the application. This is where you would draw the application UI. This example just
/// draws a greeting.
fn render_app(frame: &mut Frame) {
let greeting = Paragraph::new("Hello World! (press 'q' to quit)");
frame.render_widget(greeting, frame.size());
}
/// Check if the user has pressed 'q'. This is where you would handle events. This example just
/// checks if the user has pressed 'q' and returns true if they have. It does not handle any other
/// events. There is a 250ms timeout on the event poll so that the application can exit in a timely
/// manner, and to ensure that the terminal is rendered at least once every 250ms.
fn should_quit() -> Result<bool> {
if event::poll(Duration::from_millis(250)).context("event poll failed")? {
if let Event::Key(key) = event::read().context("event read failed")? {
return Ok(KeyCode::Char('q') == key.code);
}
}
Ok(false)
}

View File

@@ -15,13 +15,12 @@
use std::{
collections::{BTreeMap, VecDeque},
error::Error,
io,
sync::mpsc,
thread,
time::{Duration, Instant},
};
use color_eyre::Result;
use rand::distributions::{Distribution, Uniform};
use ratatui::{
backend::{Backend, CrosstermBackend},
@@ -87,17 +86,11 @@ struct Worker {
tx: mpsc::Sender<Download>,
}
fn main() -> Result<(), Box<dyn Error>> {
crossterm::terminal::enable_raw_mode()?;
let stdout = io::stdout();
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::with_options(
backend,
TerminalOptions {
viewport: Viewport::Inline(8),
},
)?;
fn main() -> Result<()> {
let options = TerminalOptions {
viewport: Viewport::Inline(8),
};
let terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal_with_options(options)?;
let (tx, rx) = mpsc::channel();
input_handling(tx.clone());
let workers = workers(tx);
@@ -108,11 +101,7 @@ fn main() -> Result<(), Box<dyn Error>> {
w.tx.send(d).unwrap();
}
run_app(&mut terminal, workers, downloads, rx)?;
crossterm::terminal::disable_raw_mode()?;
terminal.clear()?;
run_app(terminal, workers, downloads, rx)?;
Ok(())
}
@@ -179,12 +168,12 @@ fn downloads() -> Downloads {
}
#[allow(clippy::needless_pass_by_value)]
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
fn run_app(
mut terminal: Terminal<impl Backend>,
workers: Vec<Worker>,
mut downloads: Downloads,
rx: mpsc::Receiver<Event>,
) -> Result<(), Box<dyn Error>> {
) -> Result<()> {
let mut redraw = true;
loop {
if redraw {

View File

@@ -13,58 +13,28 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{error::Error, io};
use color_eyre::Result;
use itertools::Itertools;
use ratatui::{
backend::{Backend, CrosstermBackend},
crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
crossterm::event::{self, Event, KeyCode},
layout::{
Constraint,
Constraint::{Length, Max, Min, Percentage, Ratio},
Layout, Rect,
},
style::{Color, Style, Stylize},
terminal::{Frame, Terminal},
terminal::Frame,
text::Line,
widgets::{Block, Paragraph},
};
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let res = run_app(&mut terminal);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
fn main() -> Result<()> {
let mut terminal = CrosstermBackend::stdout_with_defaults()?
.with_mouse_capture()?
.to_terminal()?;
loop {
terminal.draw(ui)?;
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('q') {
return Ok(());

View File

@@ -13,17 +13,13 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{io::stdout, time::Duration};
use std::time::Duration;
use color_eyre::{config::HookBuilder, Result};
use color_eyre::Result;
use ratatui::{
backend::{Backend, CrosstermBackend},
buffer::Buffer,
crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
},
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Alignment, Constraint, Layout, Rect},
style::{palette::tailwind, Color, Style, Stylize},
terminal::Terminal,
@@ -48,15 +44,12 @@ enum AppState {
}
fn main() -> Result<()> {
init_error_hooks()?;
let terminal = init_terminal()?;
App::default().run(terminal)?;
restore_terminal()?;
Ok(())
let terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal()?;
App::default().run(terminal)
}
impl App {
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
fn run(mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
while self.state != AppState::Quitting {
self.draw(&mut terminal)?;
self.handle_events()?;
@@ -187,32 +180,3 @@ fn title_block(title: &str) -> Block {
.fg(CUSTOM_LABEL_COLOR)
.padding(Padding::vertical(1))
}
fn init_error_hooks() -> color_eyre::Result<()> {
let (panic, error) = HookBuilder::default().into_hooks();
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
color_eyre::eyre::set_hook(Box::new(move |e| {
let _ = restore_terminal();
error(e)
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
panic(info);
}));
Ok(())
}
fn init_terminal() -> color_eyre::Result<Terminal<impl Backend>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
fn restore_terminal() -> color_eyre::Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}

View File

@@ -13,17 +13,11 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{error::Error, io, io::stdout};
use color_eyre::config::HookBuilder;
use color_eyre::Result;
use ratatui::{
backend::{Backend, CrosstermBackend},
buffer::Buffer,
crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
},
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Alignment, Constraint, Layout, Rect},
style::{palette::tailwind, Color, Modifier, Style, Stylize},
terminal::Terminal,
@@ -79,46 +73,9 @@ struct App {
items: TodoList,
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
init_error_hooks()?;
let terminal = init_terminal()?;
// create app and run it
App::new().run(terminal)?;
restore_terminal()?;
Ok(())
}
fn init_error_hooks() -> color_eyre::Result<()> {
let (panic, error) = HookBuilder::default().into_hooks();
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
color_eyre::eyre::set_hook(Box::new(move |e| {
let _ = restore_terminal();
error(e)
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
panic(info);
}));
Ok(())
}
fn init_terminal() -> color_eyre::Result<Terminal<impl Backend>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
fn restore_terminal() -> color_eyre::Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
fn main() -> Result<()> {
let terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal()?;
App::new().run(terminal)
}
impl App {
@@ -155,7 +112,7 @@ impl App {
}
impl App {
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> io::Result<()> {
fn run(mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
loop {
self.draw(&mut terminal)?;
@@ -178,7 +135,7 @@ impl App {
}
}
fn draw(&mut self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
fn draw(&mut self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
terminal.draw(|f| f.render_widget(self, f.size()))?;
Ok(())
}

View File

@@ -13,15 +13,11 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use color_eyre::Result;
use ratatui::{
backend::CrosstermBackend,
crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
backend::{Backend, CrosstermBackend},
crossterm::event::{self, Event, KeyCode, KeyEventKind},
text::Text,
Terminal,
};
/// This is a bare minimum example. There are many approaches to running an application loop, so
@@ -30,19 +26,15 @@ use ratatui::{
///
/// [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
/// [hello-world]: https://github.com/ratatui-org/ratatui/blob/main/examples/hello_world.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stdout()))?;
enable_raw_mode()?;
execute!(terminal.backend_mut(), EnterAlternateScreen)?;
fn main() -> Result<()> {
let mut terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal()?;
terminal.clear()?;
loop {
terminal.draw(|frame| frame.render_widget(Text::raw("Hello World!"), frame.size()))?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
break;
return Ok(());
}
}
}
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
Ok(())
}

View File

@@ -17,42 +17,22 @@
// It will render a grid of combinations of foreground and background colors with all
// modifiers applied to them.
use std::{
error::Error,
io::{self, Stdout},
iter::once,
result,
time::Duration,
};
use std::{iter::once, time::Duration};
use color_eyre::Result;
use itertools::Itertools;
use ratatui::{
backend::{Backend, CrosstermBackend},
crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
crossterm::event::{self, Event, KeyCode},
layout::{Constraint, Layout},
style::{Color, Modifier, Style, Stylize},
terminal::{Frame, Terminal},
terminal::Frame,
text::Line,
widgets::Paragraph,
};
type Result<T> = result::Result<T, Box<dyn Error>>;
fn main() -> Result<()> {
let mut terminal = setup_terminal()?;
let res = run_app(&mut terminal);
restore_terminal(terminal)?;
if let Err(err) = res {
eprintln!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
let mut terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal()?;
loop {
terminal.draw(ui)?;
@@ -114,20 +94,3 @@ fn ui(frame: &mut Frame) {
}
}
}
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
Ok(terminal)
}
fn restore_terminal(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
Ok(())
}

View File

@@ -13,21 +13,19 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
//! How to use a panic hook to reset the terminal before printing the panic to
//! the terminal.
//! How to use a panic hook to reset the terminal before printing the panic to the terminal.
//!
//! When exiting normally or when handling `Result::Err`, we can reset the
//! terminal manually at the end of `main` just before we print the error.
//! When exiting normally or when handling `Result::Err`, we can reset the terminal manually at the
//! end of `main` just before we print the error.
//!
//! Because a panic interrupts the normal control flow, manually resetting the
//! terminal at the end of `main` won't do us any good. Instead, we need to
//! make sure to set up a panic hook that first resets the terminal before
//! handling the panic. This both reuses the standard panic hook to ensure a
//! consistent panic handling UX and properly resets the terminal to not
//! distort the output.
//! Because a panic interrupts the normal control flow, manually resetting the terminal at the end
//! of `main` won't do us any good. Instead, we need to make sure to set up a panic hook that first
//! resets the terminal before handling the panic. This both reuses the standard panic hook to
//! ensure a consistent panic handling UX and properly resets the terminal to not distort the
//! output.
//!
//! That's why this example is set up to show both situations, with and without
//! the chained panic hook, to see the difference.
//! That's why this example is set up to show both situations, with and without the chained panic
//! hook, to see the difference.
use std::{error::Error, io};
@@ -44,6 +42,21 @@ use ratatui::{
type Result<T> = std::result::Result<T, Box<dyn Error>>;
fn main() -> Result<()> {
let mut terminal = init_terminal()?;
let mut app = App::default();
let res = run_tui(&mut terminal, &mut app);
reset_terminal()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
#[derive(Default)]
struct App {
hook_enabled: bool,
@@ -62,26 +75,12 @@ impl App {
}
}
fn main() -> Result<()> {
let mut terminal = init_terminal()?;
let mut app = App::default();
let res = run_tui(&mut terminal, &mut app);
reset_terminal()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
/// Initializes the terminal.
fn init_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> {
crossterm::execute!(io::stdout(), EnterAlternateScreen)?;
enable_raw_mode()?;
#[allow(deprecated)]
let backend = CrosstermBackend::new(io::stdout());
let mut terminal = Terminal::new(backend)?;
@@ -99,7 +98,7 @@ fn reset_terminal() -> Result<()> {
}
/// Runs the TUI loop.
fn run_tui<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<()> {
fn run_tui<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> Result<()> {
loop {
terminal.draw(|f| ui(f, app))?;

View File

@@ -13,30 +13,26 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{
io::{self},
time::{Duration, Instant},
};
use std::time::{Duration, Instant};
use color_eyre::Result;
use crossterm::event::KeyEventKind;
use ratatui::{
backend::{Backend, CrosstermBackend},
buffer::Buffer,
crossterm::event::{self, Event, KeyCode},
layout::{Constraint, Layout, Rect},
style::{Color, Stylize},
text::{Line, Masked, Span},
widgets::{Block, Paragraph, Widget, Wrap},
Terminal,
};
use self::common::{init_terminal, install_hooks, restore_terminal, Tui};
fn main() -> color_eyre::Result<()> {
install_hooks()?;
let mut terminal = init_terminal()?;
let mut app = App::new();
app.run(&mut terminal)?;
restore_terminal()?;
Ok(())
fn main() -> Result<()> {
let terminal = CrosstermBackend::stdout_with_defaults()?
.with_mouse_capture()?
.to_terminal()?;
App::new().run(terminal)
}
#[derive(Debug)]
@@ -60,9 +56,9 @@ impl App {
}
/// Run the app until the user exits.
fn run(&mut self, terminal: &mut Tui) -> io::Result<()> {
fn run(mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
while !self.should_exit {
self.draw(terminal)?;
self.draw(&mut terminal)?;
self.handle_events()?;
if self.last_tick.elapsed() >= Self::TICK_RATE {
self.on_tick();
@@ -73,13 +69,13 @@ impl App {
}
/// Draw the app to the terminal.
fn draw(&mut self, terminal: &mut Tui) -> io::Result<()> {
fn draw(&mut self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
terminal.draw(|frame| frame.render_widget(self, frame.size()))?;
Ok(())
}
/// Handle events from the terminal.
fn handle_events(&mut self) -> io::Result<()> {
fn handle_events(&mut self) -> Result<()> {
let timeout = Self::TICK_RATE.saturating_sub(self.last_tick.elapsed());
while event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
@@ -159,73 +155,3 @@ fn create_lines(area: Rect) -> Vec<Line<'static>> {
]),
]
}
/// A module for common functionality used in the examples.
mod common {
use std::{
io::{self, stdout, Stdout},
panic,
};
use color_eyre::{
config::{EyreHook, HookBuilder, PanicHook},
eyre,
};
use crossterm::ExecutableCommand;
use ratatui::{
backend::CrosstermBackend,
crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
},
terminal::Terminal,
};
// A simple alias for the terminal type used in this example.
pub type Tui = Terminal<CrosstermBackend<Stdout>>;
/// Initialize the terminal and enter alternate screen mode.
pub fn init_terminal() -> io::Result<Tui> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
Terminal::new(backend)
}
/// Restore the terminal to its original state.
pub fn restore_terminal() -> io::Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
/// Installs hooks for panic and error handling.
///
/// Makes the app resilient to panics and errors by restoring the terminal before printing the
/// panic or error message. This prevents error messages from being messed up by the terminal
/// state.
pub fn install_hooks() -> color_eyre::Result<()> {
let (panic_hook, eyre_hook) = HookBuilder::default().into_hooks();
install_panic_hook(panic_hook);
install_error_hook(eyre_hook)?;
Ok(())
}
/// Install a panic hook that restores the terminal before printing the panic.
fn install_panic_hook(panic_hook: PanicHook) {
let panic_hook = panic_hook.into_panic_hook();
panic::set_hook(Box::new(move |panic_info| {
let _ = restore_terminal();
panic_hook(panic_info);
}));
}
/// Install an error hook that restores the terminal before printing the error.
fn install_error_hook(eyre_hook: EyreHook) -> color_eyre::Result<()> {
let eyre_hook = eyre_hook.into_eyre_hook();
eyre::set_hook(Box::new(move |error| {
let _ = restore_terminal();
eyre_hook(error)
}))?;
Ok(())
}
}

View File

@@ -16,18 +16,13 @@
// See also https://github.com/joshka/tui-popup and
// https://github.com/sephiroth74/tui-confirm-dialog
use std::{error::Error, io};
use color_eyre::Result;
use ratatui::{
backend::{Backend, CrosstermBackend},
crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Constraint, Layout, Rect},
style::Stylize,
terminal::{Frame, Terminal},
terminal::Frame,
widgets::{Block, Clear, Paragraph, Wrap},
};
@@ -41,35 +36,12 @@ impl App {
}
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
fn main() -> Result<()> {
let mut terminal = CrosstermBackend::stdout_with_defaults()?
.with_mouse_capture()?
.to_terminal()?;
// create app and run it
let app = App::new();
let res = run_app(&mut terminal, app);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
let mut app = App::new();
loop {
terminal.draw(|f| ui(f, &app))?;

View File

@@ -13,25 +13,39 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{
io::{self, stdout},
thread::sleep,
time::Duration,
};
use color_eyre::Result;
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use indoc::indoc;
use itertools::izip;
use ratatui::{
backend::{Backend, CrosstermBackend},
crossterm::terminal::{disable_raw_mode, enable_raw_mode},
terminal::{Terminal, Viewport},
terminal::Viewport,
widgets::Paragraph,
TerminalOptions,
};
fn main() -> Result<()> {
let mut terminal =
CrosstermBackend::stdout_with_defaults()?.to_terminal_with_options(TerminalOptions {
viewport: Viewport::Inline(3),
})?;
terminal.draw(|frame| {
frame.render_widget(logo(), frame.size());
})?;
loop {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
break;
}
}
}
println!(); // necessary to avoid the cursor being on the same line as the logo
Ok(())
}
/// A fun example of using half block characters to draw a logo
#[allow(clippy::many_single_char_names)]
fn logo() -> String {
fn logo() -> Paragraph<'static> {
let r = indoc! {"
▄▄▄
█▄▄▀
@@ -57,32 +71,9 @@ fn logo() -> String {
"};
izip!(r.lines(), a.lines(), t.lines(), u.lines(), i.lines())
let lines = izip!(r.lines(), a.lines(), t.lines(), u.lines(), i.lines())
.map(|(r, a, t, u, i)| format!("{r:5}{a:5}{t:4}{a:5}{t:4}{u:5}{i:5}"))
.collect::<Vec<_>>()
.join("\n")
}
fn main() -> io::Result<()> {
let mut terminal = init()?;
terminal.draw(|frame| {
frame.render_widget(Paragraph::new(logo()), frame.size());
})?;
sleep(Duration::from_secs(5));
restore()?;
println!();
Ok(())
}
fn init() -> io::Result<Terminal<impl Backend>> {
enable_raw_mode()?;
let options = TerminalOptions {
viewport: Viewport::Inline(3),
};
Terminal::with_options(CrosstermBackend::new(stdout()), options)
}
fn restore() -> io::Result<()> {
disable_raw_mode()?;
Ok(())
.join("\n");
Paragraph::new(lines)
}

View File

@@ -15,23 +15,16 @@
#![warn(clippy::pedantic)]
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use std::time::{Duration, Instant};
use color_eyre::Result;
use ratatui::{
backend::{Backend, CrosstermBackend},
crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
crossterm::event::{self, Event, KeyCode},
layout::{Alignment, Constraint, Layout, Margin},
style::{Color, Style, Stylize},
symbols::scrollbar,
terminal::{Frame, Terminal},
terminal::Frame,
text::{Line, Masked, Span},
widgets::{Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState},
};
@@ -44,40 +37,13 @@ struct App {
pub horizontal_scroll: usize,
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
fn main() -> Result<()> {
let mut terminal = CrosstermBackend::stdout_with_defaults()?
.with_mouse_capture()?
.to_terminal()?;
// create app and run it
let tick_rate = Duration::from_millis(250);
let app = App::default();
let res = run_app(&mut terminal, app, tick_rate);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut app = App::default();
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &mut app))?;

View File

@@ -13,26 +13,19 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{
error::Error,
io,
time::{Duration, Instant},
};
use std::time::{Duration, Instant};
use color_eyre::Result;
use rand::{
distributions::{Distribution, Uniform},
rngs::ThreadRng,
};
use ratatui::{
backend::{Backend, CrosstermBackend},
crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
crossterm::event::{self, Event, KeyCode},
layout::{Constraint, Layout},
style::{Color, Style},
terminal::{Frame, Terminal},
terminal::Frame,
widgets::{Block, Borders, Sparkline},
};
@@ -92,40 +85,13 @@ impl App {
}
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
fn main() -> Result<()> {
let mut terminal = CrosstermBackend::stdout_with_defaults()?
.with_mouse_capture()?
.to_terminal()?;
// create app and run it
let tick_rate = Duration::from_millis(250);
let app = App::new();
let res = run_app(&mut terminal, app, tick_rate);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut app = App::new();
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &app))?;

View File

@@ -13,26 +13,20 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{error::Error, io};
use color_eyre::Result;
use itertools::Itertools;
use ratatui::{
backend::{Backend, CrosstermBackend},
crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Constraint, Layout, Margin, Rect},
style::{self, Color, Modifier, Style, Stylize},
terminal::{Frame, Terminal},
style::{palette::tailwind, Color, Modifier, Style, Stylize},
terminal::Frame,
text::{Line, Text},
widgets::{
Block, BorderType, Cell, HighlightSpacing, Paragraph, Row, Scrollbar, ScrollbarOrientation,
ScrollbarState, Table, TableState,
},
};
use style::palette::tailwind;
use unicode_width::UnicodeWidthStr;
const PALETTES: [tailwind::Palette; 4] = [
@@ -46,31 +40,37 @@ const INFO_TEXT: &str =
const ITEM_HEIGHT: usize = 4;
struct TableColors {
buffer_bg: Color,
header_bg: Color,
header_fg: Color,
row_fg: Color,
selected_style_fg: Color,
normal_row_color: Color,
alt_row_color: Color,
footer_border_color: Color,
}
fn main() -> Result<()> {
let mut terminal = CrosstermBackend::stdout_with_defaults()?
.with_mouse_capture()?
.to_terminal()?;
impl TableColors {
const fn new(color: &tailwind::Palette) -> Self {
Self {
buffer_bg: tailwind::SLATE.c950,
header_bg: color.c900,
header_fg: tailwind::SLATE.c200,
row_fg: tailwind::SLATE.c200,
selected_style_fg: color.c400,
normal_row_color: tailwind::SLATE.c950,
alt_row_color: tailwind::SLATE.c900,
footer_border_color: color.c400,
let mut app = App::new();
loop {
terminal.draw(|f| ui(f, &mut app))?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
KeyCode::Char('j') | KeyCode::Down => app.next(),
KeyCode::Char('k') | KeyCode::Up => app.previous(),
KeyCode::Char('l') | KeyCode::Right => app.next_color(),
KeyCode::Char('h') | KeyCode::Left => app.previous_color(),
_ => {}
}
}
}
}
}
struct App {
state: TableState,
items: Vec<Data>,
longest_item_lens: (u16, u16, u16), // order is (name, address, email)
scroll_state: ScrollbarState,
colors: TableColors,
color_index: usize,
}
struct Data {
name: String,
@@ -96,13 +96,30 @@ impl Data {
}
}
struct App {
state: TableState,
items: Vec<Data>,
longest_item_lens: (u16, u16, u16), // order is (name, address, email)
scroll_state: ScrollbarState,
colors: TableColors,
color_index: usize,
struct TableColors {
buffer_bg: Color,
header_bg: Color,
header_fg: Color,
row_fg: Color,
selected_style_fg: Color,
normal_row_color: Color,
alt_row_color: Color,
footer_border_color: Color,
}
impl TableColors {
const fn new(color: &tailwind::Palette) -> Self {
Self {
buffer_bg: tailwind::SLATE.c950,
header_bg: color.c900,
header_fg: tailwind::SLATE.c200,
row_fg: tailwind::SLATE.c200,
selected_style_fg: color.c400,
normal_row_color: tailwind::SLATE.c950,
alt_row_color: tailwind::SLATE.c900,
footer_border_color: color.c400,
}
}
}
impl App {
@@ -186,53 +203,6 @@ fn generate_fake_names() -> Vec<Data> {
.collect_vec()
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let app = App::new();
let res = run_app(&mut terminal, app);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
loop {
terminal.draw(|f| ui(f, &mut app))?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
KeyCode::Char('j') | KeyCode::Down => app.next(),
KeyCode::Char('k') | KeyCode::Up => app.previous(),
KeyCode::Char('l') | KeyCode::Right => app.next_color(),
KeyCode::Char('h') | KeyCode::Left => app.previous_color(),
_ => {}
}
}
}
}
}
fn ui(f: &mut Frame, app: &mut App) {
let rects = Layout::vertical([Constraint::Min(5), Constraint::Length(3)]).split(f.size());

View File

@@ -13,17 +13,13 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::io::stdout;
use std::fmt;
use color_eyre::{config::HookBuilder, Result};
use color_eyre::Result;
use ratatui::{
backend::{Backend, CrosstermBackend},
buffer::Buffer,
crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
},
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Constraint, Layout, Rect},
style::{palette::tailwind, Color, Stylize},
symbols,
@@ -31,7 +27,11 @@ use ratatui::{
text::Line,
widgets::{Block, Padding, Paragraph, Tabs, Widget},
};
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
fn main() -> Result<()> {
let terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal()?;
App::default().run(terminal)
}
#[derive(Default)]
struct App {
@@ -46,31 +46,30 @@ enum AppState {
Quitting,
}
#[derive(Default, Clone, Copy, Display, FromRepr, EnumIter)]
#[derive(Default, Clone, Copy)]
enum SelectedTab {
#[default]
#[strum(to_string = "Tab 1")]
Tab1,
#[strum(to_string = "Tab 2")]
Tab2,
#[strum(to_string = "Tab 3")]
Tab3,
#[strum(to_string = "Tab 4")]
Tab4,
}
fn main() -> Result<()> {
init_error_hooks()?;
let mut terminal = init_terminal()?;
App::default().run(&mut terminal)?;
restore_terminal()?;
Ok(())
impl fmt::Display for SelectedTab {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Tab1 => write!(f, "Tab 1"),
Self::Tab2 => write!(f, "Tab 2"),
Self::Tab3 => write!(f, "Tab 3"),
Self::Tab4 => write!(f, "Tab 4"),
}
}
}
impl App {
fn run(&mut self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
fn run(mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
while self.state == AppState::Running {
self.draw(terminal)?;
self.draw(&mut terminal)?;
self.handle_events()?;
}
Ok(())
@@ -108,22 +107,6 @@ impl App {
}
}
impl SelectedTab {
/// Get the previous tab, if there is no previous tab return the current tab.
fn previous(self) -> Self {
let current_index: usize = self as usize;
let previous_index = current_index.saturating_sub(1);
Self::from_repr(previous_index).unwrap_or(self)
}
/// Get the next tab, if there is no next tab return the current tab.
fn next(self) -> Self {
let current_index = self as usize;
let next_index = current_index.saturating_add(1);
Self::from_repr(next_index).unwrap_or(self)
}
}
impl Widget for &App {
fn render(self, area: Rect, buf: &mut Buffer) {
use Constraint::{Length, Min};
@@ -142,7 +125,7 @@ impl Widget for &App {
impl App {
fn render_tabs(&self, area: Rect, buf: &mut Buffer) {
let titles = SelectedTab::iter().map(SelectedTab::title);
let titles = SelectedTab::titles();
let highlight_style = (Color::default(), self.selected_tab.palette().c700);
let selected_tab_index = self.selected_tab as usize;
Tabs::new(titles)
@@ -177,6 +160,35 @@ impl Widget for SelectedTab {
}
impl SelectedTab {
fn titles() -> [Line<'static>; 4] {
[
Self::Tab1.title(),
Self::Tab2.title(),
Self::Tab3.title(),
Self::Tab4.title(),
]
}
/// Get the previous tab, if there is no previous tab return the current tab.
const fn previous(self) -> Self {
match self {
Self::Tab1 => Self::Tab4,
Self::Tab2 => Self::Tab1,
Self::Tab3 => Self::Tab2,
Self::Tab4 => Self::Tab3,
}
}
/// Get the next tab, if there is no next tab return the current tab.
const fn next(self) -> Self {
match self {
Self::Tab1 => Self::Tab2,
Self::Tab2 => Self::Tab3,
Self::Tab3 => Self::Tab4,
Self::Tab4 => Self::Tab1,
}
}
/// Return tab's name as a styled `Line`
fn title(self) -> Line<'static> {
format!(" {self} ")
@@ -226,32 +238,3 @@ impl SelectedTab {
}
}
}
fn init_error_hooks() -> color_eyre::Result<()> {
let (panic, error) = HookBuilder::default().into_hooks();
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
color_eyre::eyre::set_hook(Box::new(move |e| {
let _ = restore_terminal();
error(e)
}))?;
std::panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
panic(info);
}));
Ok(())
}
fn init_terminal() -> color_eyre::Result<Terminal<impl Backend>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
fn restore_terminal() -> color_eyre::Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}

View File

@@ -27,18 +27,13 @@
//
// See also https://github.com/rhysd/tui-textarea and https://github.com/sayanarijit/tui-input/
use std::{error::Error, io};
use color_eyre::Result;
use ratatui::{
backend::{Backend, CrosstermBackend},
crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Constraint, Layout},
style::{Color, Modifier, Style, Stylize},
terminal::{Frame, Terminal},
terminal::Frame,
text::{Line, Span, Text},
widgets::{Block, List, ListItem, Paragraph},
};
@@ -135,35 +130,12 @@ impl App {
}
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
fn main() -> Result<()> {
let mut terminal = CrosstermBackend::stdout_with_defaults()?
.with_mouse_capture()?
.to_terminal()?;
// create app and run it
let app = App::new();
let res = run_app(&mut terminal, app);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
let mut app = App::new();
loop {
terminal.draw(|f| ui(f, &app))?;

View File

@@ -10,3 +10,5 @@ Enter
Sleep 2s
Show
Sleep 2s
Hide
Type "q"

View File

@@ -104,7 +104,11 @@ use std::io;
use strum::{Display, EnumString};
use crate::{buffer::Cell, layout::Size, prelude::Rect};
use crate::{
buffer::Cell,
layout::{Rect, Size},
Terminal, TerminalOptions,
};
#[cfg(feature = "termion")]
mod termion;
@@ -298,6 +302,45 @@ pub trait Backend {
/// Flush any buffered content to the terminal screen.
fn flush(&mut self) -> io::Result<()>;
/// Converts the `Backend` into a [`Terminal`] instance.
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::backend::{Backend, CrosstermBackend};
/// let terminal = CrosstermBackend::stdout().to_terminal()?;
/// # std::io::Result::Ok(())
/// ```
fn to_terminal(self) -> io::Result<Terminal<Self>>
where
Self: Sized,
{
Terminal::new(self)
}
/// Converts the `Backend` into a [`Terminal`] instance with options.
///
/// # Example
///
/// ```rust,no_run
/// use ratatui::{
/// backend::{Backend, CrosstermBackend},
/// TerminalOptions, Viewport,
/// };
///
/// let options = TerminalOptions {
/// viewport: Viewport::Inline(10),
/// };
/// let terminal = CrosstermBackend::stdout().to_terminal_with_options(options)?;
/// # std::io::Result::Ok(())
/// ```
fn to_terminal_with_options(self, options: TerminalOptions) -> io::Result<Terminal<Self>>
where
Self: Sized,
{
Terminal::with_options(self, options)
}
}
#[cfg(test)]

View File

@@ -2,80 +2,83 @@
//! the [Crossterm] crate to interact with the terminal.
//!
//! [Crossterm]: https://crates.io/crates/crossterm
use std::io::{self, Write};
use std::io;
#[cfg(feature = "underline-color")]
use crossterm::style::SetUnderlineColor;
use crate::crossterm::style::SetUnderlineColor;
use crate::{
backend::{Backend, ClearType, WindowSize},
buffer::Cell,
crossterm::{
cursor::{Hide, MoveTo, Show},
event::{
DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
EnableFocusChange, EnableMouseCapture, KeyboardEnhancementFlags,
PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
},
execute, queue,
style::{
Attribute as CAttribute, Attributes as CAttributes, Color as CColor, Colors,
ContentStyle, Print, SetAttribute, SetBackgroundColor, SetColors, SetForegroundColor,
},
terminal::{self, Clear},
terminal::{
disable_raw_mode, enable_raw_mode, Clear, EnterAlternateScreen, LeaveAlternateScreen,
},
},
layout::Size,
prelude::Rect,
layout::{Rect, Size},
style::{Color, Modifier, Style},
};
/// A [`Backend`] implementation that uses [Crossterm] to render to the terminal.
///
/// The `CrosstermBackend` struct is a wrapper around a writer implementing [`Write`], which is
/// used to send commands to the terminal. It provides methods for drawing content, manipulating
/// the cursor, and clearing the terminal screen.
/// The `CrosstermBackend` struct is a wrapper around a writer implementing [`Write`], which is used
/// to send commands to the terminal. It provides methods for drawing content, manipulating the
/// cursor, and clearing the terminal screen.
///
/// Most applications should not call the methods on `CrosstermBackend` directly, but will instead
/// use the [`Terminal`] struct, which provides a more ergonomic interface.
/// Convenience methods ([`CrosstermBackend::stdout`] and [`CrosstermBackend::stderr`] are provided
/// to create a `CrosstermBackend` with [`std::io::stdout`] or [`std::io::stderr`] as the writer.
/// Additionally, these can be created with default settings to enable raw mode and switch to the
/// alternate screen using [`CrosstermBackend::stdout_with_defaults`] or
/// [`CrosstermBackend::stderr_with_defaults`].
///
/// Usually applications will enable raw mode and switch to alternate screen mode after creating
/// a `CrosstermBackend`. This is done by calling [`crossterm::terminal::enable_raw_mode`] and
/// [`crossterm::terminal::EnterAlternateScreen`] (and the corresponding disable/leave functions
/// when the application exits). This is not done automatically by the backend because it is
/// possible that the application may want to use the terminal for other purposes (like showing
/// help text) before entering alternate screen mode.
/// If the default settings are not desired, the `CrosstermBackend` can be configured using the
/// `with_*` methods. These methods return an [`io::Result`] containing self so that they can be
/// chained with other methods. The settings are restored when the `CrosstermBackend` is dropped.
/// - [`CrosstermBackend::with_raw_mode`] enables raw mode for the terminal.
/// - [`CrosstermBackend::with_alternate_screen`] switches to the alternate screen.
/// - [`CrosstermBackend::with_mouse_capture`] enables mouse capture.
/// - [`CrosstermBackend::with_bracketed_paste`] enables bracketed paste.
/// - [`CrosstermBackend::with_focus_change`] enables focus change.
/// - [`CrosstermBackend::with_keyboard_enhancement_flags`] enables keyboard enhancement flags.
///
/// If a backend is configured using the `with_*` methods, the settings are restored when the
/// `CrosstermBackend` is dropped.
///
/// # Example
///
/// ```rust,no_run
/// use std::io::{stderr, stdout};
///
/// use ratatui::{
/// crossterm::{
/// terminal::{
/// disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
/// },
/// ExecutableCommand,
/// },
/// prelude::*,
/// backend::{Backend, CrosstermBackend},
/// crossterm::event::KeyboardEnhancementFlags,
/// };
///
/// let mut backend = CrosstermBackend::new(stdout());
/// let mut terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal()?;
/// // or
/// let backend = CrosstermBackend::new(stderr());
/// let mut terminal = Terminal::new(backend)?;
///
/// enable_raw_mode()?;
/// stdout().execute(EnterAlternateScreen)?;
///
/// terminal.clear()?;
/// terminal.draw(|frame| {
/// // -- snip --
/// })?;
///
/// stdout().execute(LeaveAlternateScreen)?;
/// disable_raw_mode()?;
///
/// let mut terminal = CrosstermBackend::stderr_with_defaults()?.to_terminal()?;
/// // or with custom settings
/// let mut terminal = CrosstermBackend::stdout()
/// .with_raw_mode()?
/// .with_alternate_screen()?
/// .with_mouse_capture()?
/// .with_bracketed_paste()?
/// .with_focus_change()?
/// .with_keyboard_enhancement_flags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES)?
/// .to_terminal()?;
/// # std::io::Result::Ok(())
/// ```
///
/// See the the [Examples] directory for more examples. See the [`backend`] module documentation
/// for more details on raw mode and alternate screen.
/// See the the [Examples] directory for more examples. See the [`backend`] module documentation for
/// more details on raw mode and alternate screen.
///
/// [`Write`]: std::io::Write
/// [`Terminal`]: crate::terminal::Terminal
@@ -83,26 +86,41 @@ use crate::{
/// [Crossterm]: https://crates.io/crates/crossterm
/// [Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples/README.md
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct CrosstermBackend<W: Write> {
#[allow(clippy::struct_excessive_bools)]
pub struct CrosstermBackend<W: io::Write> {
/// The writer used to send commands to the terminal.
writer: W,
restore_raw_mode_on_drop: bool,
restore_alternate_screen_on_drop: bool,
restore_mouse_capture_on_drop: bool,
restore_bracketed_paste_on_drop: bool,
restore_focus_change_on_drop: bool,
restore_keyboard_enhancement_flags_on_drop: bool,
}
impl<W> CrosstermBackend<W>
where
W: Write,
{
impl<W: io::Write> CrosstermBackend<W> {
/// Creates a new `CrosstermBackend` with the given writer.
///
/// Applications will typically use [`CrosstermBackend::stdout`] or [`CrosstermBackend::stderr`]
/// to create a `CrosstermBackend` with [`std::io::stdout`] or [`std::io::stderr`] as the
/// writer.
///
/// # Example
///
/// ```rust,no_run
/// # use std::io::stdout;
/// # use ratatui::prelude::*;
/// let backend = CrosstermBackend::new(stdout());
/// # use ratatui::backend::CrosstermBackend;
/// let backend = CrosstermBackend::new(std::io::stdout());
/// ```
pub const fn new(writer: W) -> Self {
Self { writer }
Self {
writer,
restore_raw_mode_on_drop: false,
restore_alternate_screen_on_drop: false,
restore_mouse_capture_on_drop: false,
restore_bracketed_paste_on_drop: false,
restore_focus_change_on_drop: false,
restore_keyboard_enhancement_flags_on_drop: false,
}
}
/// Gets the writer.
@@ -127,10 +145,335 @@ where
}
}
impl<W> Write for CrosstermBackend<W>
where
W: Write,
{
impl CrosstermBackend<io::Stdout> {
/// Creates a new `CrosstermBackend` with `std::io::stdout`.
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::backend::CrosstermBackend;
/// let backend = CrosstermBackend::stdout();
/// ```
pub fn stdout() -> Self {
Self::new(io::stdout())
}
/// Creates a new `CrosstermBackend` with `std::io::stdout` and default settings.
///
/// This enables raw mode and switches to the alternate screen. Mouse support is not enabled.
///
/// Raw mode and alternate screen are restored when the `CrosstermBackend` is dropped.
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::backend::CrosstermBackend;
/// let backend = CrosstermBackend::stdout_with_defaults()?;
/// # std::io::Result::Ok(())
/// ```
pub fn stdout_with_defaults() -> io::Result<Self> {
Self::stdout().with_defaults()
}
}
impl CrosstermBackend<io::Stderr> {
/// Creates a new `CrosstermBackend` with `std::io::stderr`.
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::backend::CrosstermBackend;
/// let backend = CrosstermBackend::stderr();
/// ```
pub fn stderr() -> Self {
Self::new(io::stderr())
}
/// Creates a new `CrosstermBackend` with `std::io::stderr` and default settings.
///
/// This enables raw mode and switches to the alternate screen. Mouse support is not enabled.
///
/// Raw mode and alternate screen are restored when the `CrosstermBackend` is dropped.
///
/// If the `color-eyre` feature is enabled, the color-eyre panic and error report hooks are
/// installed.
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::backend::CrosstermBackend;
/// let backend = CrosstermBackend::stderr_with_defaults()?;
/// # std::io::Result::Ok(())
/// ```
pub fn stderr_with_defaults() -> io::Result<Self> {
Self::stderr().with_defaults()
}
}
impl<W: io::Write> CrosstermBackend<W> {
/// Enables default settings for the terminal backend.
///
/// This enables raw mode and switches to the alternate screen. Mouse support is not enabled.
///
/// If the `color-eyre` feature is enabled, the color-eyre panic and error report hooks are
/// installed. Otherwise, a panic hook is installed that resets the terminal to its default
/// state before panicking.
///
/// Returns an [`io::Result`] containing self so that it can be chained with other methods.
///
/// # Example
///
/// ```rust,no_run
/// use ratatui::backend::CrosstermBackend;
///
/// let backend = CrosstermBackend::stdout().with_defaults()?;
/// # std::io::Result::Ok(())
/// ```
pub fn with_defaults(self) -> io::Result<Self> {
let backend = self.with_raw_mode()?.with_alternate_screen()?;
#[cfg(feature = "color-eyre")]
let backend = backend.with_color_eyre_hooks()?;
#[cfg(not(feature = "color-eyre"))]
let backend = backend.with_panic_hook();
Ok(backend)
}
/// Enables raw mode for the terminal.
///
/// Returns an [`io::Result`] containing self so that it can be chained with other methods.
///
/// Raw mode is restored when the `CrosstermBackend` is dropped.
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::backend::CrosstermBackend;
/// let backend = CrosstermBackend::stdout().with_raw_mode()?;
/// # std::io::Result::Ok(())
/// ```
pub fn with_raw_mode(mut self) -> io::Result<Self> {
enable_raw_mode()?;
self.restore_raw_mode_on_drop = true;
Ok(self)
}
/// Enables raw mode for the terminal and switches to the alternate screen.
///
/// Returns an [`io::Result`] containing self so that it can be chained with other methods.
///
/// Alternate screen is restored when the `CrosstermBackend` is dropped.
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::backend::CrosstermBackend;
/// let backend = CrosstermBackend::stdout().with_alternate_screen()?;
/// # std::io::Result::Ok(())
/// ```
pub fn with_alternate_screen(mut self) -> io::Result<Self> {
execute!(self.writer, EnterAlternateScreen)?;
self.restore_alternate_screen_on_drop = true;
Ok(self)
}
/// Enables mouse capture for the terminal.
///
/// Returns an [`io::Result`] containing self so that it can be chained with other methods.
///
/// Mouse capture is disabled when the `CrosstermBackend` is dropped.
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::backend::CrosstermBackend;
/// let backend = CrosstermBackend::stdout().with_mouse_capture()?;
/// # std::io::Result::Ok(())
/// ```
pub fn with_mouse_capture(mut self) -> io::Result<Self> {
execute!(self.writer, EnableMouseCapture)?;
self.restore_mouse_capture_on_drop = true;
Ok(self)
}
/// Enables bracketed paste for the terminal.
///
/// Returns an [`io::Result`] containing self so that it can be chained with other methods.
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::backend::CrosstermBackend;
/// let backend = CrosstermBackend::stdout().with_bracketed_paste()?;
/// # std::io::Result::Ok(())
/// ```
pub fn with_bracketed_paste(mut self) -> io::Result<Self> {
execute!(self.writer, EnableBracketedPaste)?;
self.restore_bracketed_paste_on_drop = true;
Ok(self)
}
/// Enables focus change for the terminal.
///
/// Returns an [`io::Result`] containing self so that it can be chained with other methods.
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::backend::CrosstermBackend;
/// let backend = CrosstermBackend::stdout().with_focus_change()?;
/// # std::io::Result::Ok(())
pub fn with_focus_change(mut self) -> io::Result<Self> {
execute!(self.writer, EnableFocusChange)?;
self.restore_focus_change_on_drop = true;
Ok(self)
}
/// Enables keyboard enhancement flags for the terminal.
///
/// Returns an [`io::Result`] containing self so that it can be chained with other methods.
///
/// # Example
///
/// ```rust,no_run
/// use ratatui::{backend::CrosstermBackend, crossterm::event::KeyboardEnhancementFlags};
///
/// let backend = CrosstermBackend::stdout()
/// .with_keyboard_enhancement_flags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES)?;
/// # std::io::Result::Ok(())
/// ```
pub fn with_keyboard_enhancement_flags(
mut self,
flags: KeyboardEnhancementFlags,
) -> io::Result<Self> {
execute!(self.writer, PushKeyboardEnhancementFlags(flags))?;
self.restore_keyboard_enhancement_flags_on_drop = true;
Ok(self)
}
/// Installs a panic hook that resets the terminal to its default state before panicking.
///
/// This is a convenience method that sets up the panic hook for the terminal backend.
///
/// # Example
///
/// ```rust,no_run
/// use ratatui::backend::CrosstermBackend;
///
/// let backend = CrosstermBackend::stdout().with_panic_hook()?;
/// ```
#[cfg(not(feature = "color-eyre"))]
#[must_use]
pub fn with_panic_hook(self) -> Self {
use std::panic;
let hook = panic::take_hook();
panic::set_hook(Box::new(move |info| {
let _ = CrosstermBackend::reset(io::stderr());
hook(info);
}));
self
}
/// Installs the color-eyre panic and error report hooks.
///
/// This is a convenience method that sets up the color-eyre hooks for the terminal backend.
///
/// # Example
///
/// ```rust,no_run
/// use ratatui::backend::CrosstermBackend;
///
/// let backend = CrosstermBackend::stdout().with_color_eyre_hooks()?;
/// # std::io::Result::Ok(())
/// ```
#[cfg(feature = "color-eyre")]
pub fn with_color_eyre_hooks(self) -> io::Result<Self> {
use std::{io::stderr, panic};
use color_eyre::{config::HookBuilder, eyre};
let (panic, error) = HookBuilder::default().into_hooks();
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
eyre::set_hook(Box::new(move |e| {
// ignore errors here because we are already in an error state
let _ = CrosstermBackend::reset(stderr());
error(e)
}))
.map_err(io::Error::other)?;
panic::set_hook(Box::new(move |info| {
// ignore errors here because we are already in an error state
let _ = CrosstermBackend::reset(stderr());
panic(info);
}));
Ok(self)
}
/// Resets the terminal to its default state.
///
/// - Disables raw mode
/// - Disables mouse capture
/// - Leaves the alternate screen
/// - Disables bracketed paste
/// - Disables focus change
/// - Pops keyboard enhancement flags
///
/// This method is an associated method rather than an instance method to make it possible to
/// call without having a `CrosstermBackend` instance. This is often useful in the context of
/// error / panic handling.
///
/// If you have created a `CrosstermBackend` using the `with_*` methods, the settings are
/// restored when the `CrosstermBackend` is dropped, so you do not need to call this method
/// manually.
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::backend::CrosstermBackend;
/// CrosstermBackend::reset(std::io::stderr())?;
/// # std::io::Result::Ok(())
/// ```
pub fn reset(mut writer: W) -> io::Result<()> {
disable_raw_mode()?;
execute!(
writer,
LeaveAlternateScreen,
DisableMouseCapture,
DisableBracketedPaste,
DisableFocusChange,
PopKeyboardEnhancementFlags
)?;
writer.flush()
}
}
impl<W: io::Write> Drop for CrosstermBackend<W> {
fn drop(&mut self) {
// note that these are not checked for errors because there is nothing that can be done if
// they fail. The terminal is likely in a bad state, and the application is exiting anyway.
if self.restore_raw_mode_on_drop {
let _ = disable_raw_mode();
}
if self.restore_mouse_capture_on_drop {
let _ = execute!(self.writer, DisableMouseCapture);
}
if self.restore_alternate_screen_on_drop {
let _ = execute!(self.writer, LeaveAlternateScreen);
}
if self.restore_bracketed_paste_on_drop {
let _ = execute!(self.writer, DisableBracketedPaste);
}
if self.restore_focus_change_on_drop {
let _ = execute!(self.writer, DisableFocusChange);
}
if self.restore_keyboard_enhancement_flags_on_drop {
let _ = execute!(self.writer, PopKeyboardEnhancementFlags);
}
let _ = self.writer.flush();
}
}
impl<W: io::Write> io::Write for CrosstermBackend<W> {
/// Writes a buffer of bytes to the underlying buffer.
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.writer.write(buf)
@@ -142,10 +485,7 @@ where
}
}
impl<W> Backend for CrosstermBackend<W>
where
W: Write,
{
impl<W: io::Write> Backend for CrosstermBackend<W> {
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
@@ -247,7 +587,7 @@ where
}
fn size(&self) -> io::Result<Rect> {
let (width, height) = terminal::size()?;
let (width, height) = crossterm::terminal::size()?;
Ok(Rect::new(0, 0, width, height))
}
@@ -257,7 +597,7 @@ where
rows,
width,
height,
} = terminal::window_size()?;
} = crossterm::terminal::window_size()?;
Ok(WindowSize {
columns_rows: Size {
width: columns,
@@ -333,11 +673,7 @@ struct ModifierDiff {
}
impl ModifierDiff {
fn queue<W>(self, mut w: W) -> io::Result<()>
where
W: io::Write,
{
//use crossterm::Attribute;
fn queue<W: io::Write>(self, mut w: W) -> io::Result<()> {
let removed = self.from - self.to;
if removed.contains(Modifier::REVERSED) {
queue!(w, SetAttribute(CAttribute::NoReverse))?;