ratatui/examples/line_gauge.rs
Josh McKinney ed51c4b342
feat(terminal): Add ratatui::init() and restore() methods (#1289)
These are simple opinionated methods for creating a terminal that is
useful to use in most apps. The new init method creates a crossterm
backend writing to stdout, enables raw mode, enters the alternate
screen, and sets a panic handler that restores the terminal on panic.

A minimal hello world now looks a bit like:

```rust
use ratatui::{
    crossterm::event::{self, Event},
    text::Text,
    Frame,
};

fn main() {
    let mut terminal = ratatui::init();
    loop {
        terminal
            .draw(|frame: &mut Frame| frame.render_widget(Text::raw("Hello World!"), frame.area()))
            .expect("Failed to draw");
        if matches!(event::read().expect("failed to read event"), Event::Key(_)) {
            break;
        }
    }
    ratatui::restore();
}
```

A type alias `DefaultTerminal` is added to represent this terminal
type and to simplify any cases where applications need to pass this
terminal around. It is equivalent to:
`Terminal<CrosstermBackend<Stdout>>`

We also added `ratatui::try_init()` and `try_restore()`, for situations
where you might want to handle initialization errors yourself instead
of letting the panic handler fire and cleanup. Simple Apps should
prefer the `init` and `restore` functions over these functions.

Corresponding functions to allow passing a `TerminalOptions` with
a `Viewport` (e.g. inline, fixed) are also available
(`init_with_options`,
and `try_init_with_options`).

The existing code to create a backend and terminal will remain and
is not deprecated by this approach. This just provides a simple one
line initialization using the common options.

---------

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2024-08-22 15:16:35 +03:00

180 lines
5.5 KiB
Rust

//! # [Ratatui] Line Gauge example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui/ratatui
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use std::time::Duration;
use color_eyre::Result;
use ratatui::{
buffer::Buffer,
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Alignment, Constraint, Layout, Rect},
style::{palette::tailwind, Color, Style, Stylize},
widgets::{block::Title, Block, Borders, LineGauge, Padding, Paragraph, Widget},
DefaultTerminal,
};
const CUSTOM_LABEL_COLOR: Color = tailwind::SLATE.c200;
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::default().run(terminal);
ratatui::restore();
app_result
}
#[derive(Debug, Default, Clone, Copy)]
struct App {
state: AppState,
progress_columns: u16,
progress: f64,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
enum AppState {
#[default]
Running,
Started,
Quitting,
}
impl App {
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
while self.state != AppState::Quitting {
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
self.handle_events()?;
self.update(terminal.size()?.width);
}
Ok(())
}
fn update(&mut self, terminal_width: u16) {
if self.state != AppState::Started {
return;
}
self.progress_columns = (self.progress_columns + 1).clamp(0, terminal_width);
self.progress = f64::from(self.progress_columns) / f64::from(terminal_width);
}
fn handle_events(&mut self) -> Result<()> {
let timeout = Duration::from_secs_f32(1.0 / 20.0);
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char(' ') | KeyCode::Enter => self.start(),
KeyCode::Char('q') | KeyCode::Esc => self.quit(),
_ => {}
}
}
}
}
Ok(())
}
fn start(&mut self) {
self.state = AppState::Started;
}
fn quit(&mut self) {
self.state = AppState::Quitting;
}
}
impl Widget for &App {
fn render(self, area: Rect, buf: &mut Buffer) {
use Constraint::{Length, Min, Ratio};
let layout = Layout::vertical([Length(2), Min(0), Length(1)]);
let [header_area, main_area, footer_area] = layout.areas(area);
let layout = Layout::vertical([Ratio(1, 3); 3]);
let [gauge1_area, gauge2_area, gauge3_area] = layout.areas(main_area);
header().render(header_area, buf);
footer().render(footer_area, buf);
self.render_gauge1(gauge1_area, buf);
self.render_gauge2(gauge2_area, buf);
self.render_gauge3(gauge3_area, buf);
}
}
fn header() -> impl Widget {
Paragraph::new("Ratatui Line Gauge Example")
.bold()
.alignment(Alignment::Center)
.fg(CUSTOM_LABEL_COLOR)
}
fn footer() -> impl Widget {
Paragraph::new("Press ENTER / SPACE to start")
.alignment(Alignment::Center)
.fg(CUSTOM_LABEL_COLOR)
.bold()
}
impl App {
fn render_gauge1(&self, area: Rect, buf: &mut Buffer) {
let title = title_block("Blue / red only foreground");
LineGauge::default()
.block(title)
.filled_style(Style::default().fg(Color::Blue))
.unfilled_style(Style::default().fg(Color::Red))
.label("Foreground:")
.ratio(self.progress)
.render(area, buf);
}
fn render_gauge2(&self, area: Rect, buf: &mut Buffer) {
let title = title_block("Blue / red only background");
LineGauge::default()
.block(title)
.filled_style(Style::default().fg(Color::Blue).bg(Color::Blue))
.unfilled_style(Style::default().fg(Color::Red).bg(Color::Red))
.label("Background:")
.ratio(self.progress)
.render(area, buf);
}
fn render_gauge3(&self, area: Rect, buf: &mut Buffer) {
let title = title_block("Fully styled with background");
LineGauge::default()
.block(title)
.filled_style(
Style::default()
.fg(tailwind::BLUE.c400)
.bg(tailwind::BLUE.c600),
)
.unfilled_style(
Style::default()
.fg(tailwind::RED.c400)
.bg(tailwind::RED.c800),
)
.label("Both:")
.ratio(self.progress)
.render(area, buf);
}
}
fn title_block(title: &str) -> Block {
let title = Title::from(title).alignment(Alignment::Center);
Block::default()
.title(title)
.borders(Borders::NONE)
.fg(CUSTOM_LABEL_COLOR)
.padding(Padding::vertical(1))
}