Skip to content

Commit

Permalink
docs(examples): simplify paragraph example (ratatui#1169)
Browse files Browse the repository at this point in the history
Related: ratatui#1157
  • Loading branch information
joshka committed Oct 14, 2024
1 parent b71c6f9 commit cbb148c
Showing 1 changed file with 183 additions and 119 deletions.
302 changes: 183 additions & 119 deletions examples/paragraph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,154 +14,218 @@
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{
error::Error,
io,
io::{self},
time::{Duration, Instant},
};

use crossterm::event::KeyEventKind;
use ratatui::{
backend::{Backend, CrosstermBackend},
crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
layout::{Constraint, Layout},
style::{Color, Modifier, Style, Stylize},
terminal::{Frame, Terminal},
buffer::Buffer,
crossterm::event::{self, Event, KeyCode},
layout::{Constraint, Layout, Rect},
style::{Color, Stylize},
text::{Line, Masked, Span},
widgets::{Block, Paragraph, Wrap},
widgets::{Block, Paragraph, Widget, Wrap},
};

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(())
}

#[derive(Debug)]
struct App {
should_exit: bool,
scroll: u16,
last_tick: Instant,
}

impl App {
const fn new() -> Self {
Self { scroll: 0 }
/// The duration between each tick.
const TICK_RATE: Duration = Duration::from_millis(250);

/// Create a new instance of the app.
fn new() -> Self {
Self {
should_exit: false,
scroll: 0,
last_tick: Instant::now(),
}
}

fn on_tick(&mut self) {
self.scroll += 1;
self.scroll %= 10;
/// Run the app until the user exits.
fn run(&mut self, terminal: &mut Tui) -> io::Result<()> {
while !self.should_exit {
self.draw(terminal)?;
self.handle_events()?;
if self.last_tick.elapsed() >= Self::TICK_RATE {
self.on_tick();
self.last_tick = Instant::now();
}
}
Ok(())
}
}

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 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:?}");
/// Draw the app to the terminal.
fn draw(&mut self, terminal: &mut Tui) -> io::Result<()> {
terminal.draw(|frame| frame.render_widget(self, frame.size()))?;
Ok(())
}

Ok(())
}

fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &app))?;

let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
/// Handle events from the terminal.
fn handle_events(&mut self) -> io::Result<()> {
let timeout = Self::TICK_RATE.saturating_sub(self.last_tick.elapsed());
while event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('q') {
return Ok(());
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
self.should_exit = true;
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
Ok(())
}
}

fn ui(f: &mut Frame, app: &App) {
let size = f.size();

// Words made "loooong" to demonstrate line breaking.
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
long_line.push('\n');
/// Update the app state on each tick.
fn on_tick(&mut self) {
self.scroll = (self.scroll + 1) % 10;
}
}

let block = Block::new().black();
f.render_widget(block, size);
impl Widget for &mut App {
fn render(self, area: Rect, buf: &mut Buffer) {
let areas = Layout::vertical([Constraint::Max(9); 4]).split(area);
Paragraph::new(create_lines(area))
.block(title_block("Default alignment (Left), no wrap"))
.gray()
.render(areas[0], buf);
Paragraph::new(create_lines(area))
.block(title_block("Default alignment (Left), with wrap"))
.gray()
.wrap(Wrap { trim: true })
.render(areas[1], buf);
Paragraph::new(create_lines(area))
.block(title_block("Right alignment, with wrap"))
.gray()
.right_aligned()
.wrap(Wrap { trim: true })
.render(areas[2], buf);
Paragraph::new(create_lines(area))
.block(title_block("Center alignment, with wrap, with scroll"))
.gray()
.centered()
.wrap(Wrap { trim: true })
.scroll((self.scroll, 0))
.render(areas[3], buf);
}
}

let layout = Layout::vertical([Constraint::Ratio(1, 4); 4]).split(size);
/// Create a bordered block with a title.
fn title_block(title: &str) -> Block {
Block::bordered()
.gray()
.title(title.bold().into_centered_line())
}

let text = vec![
Line::from("This is a line "),
Line::from("This is a line ".red()),
Line::from("This is a line".on_blue()),
Line::from("This is a longer line".crossed_out()),
Line::from(long_line.on_green()),
Line::from("This is a line".green().italic()),
Line::from(vec![
/// Create some lines to display in the paragraph.
fn create_lines(area: Rect) -> Vec<Line<'static>> {
let short_line = "A long line to demonstrate line wrapping. ";
let long_line = short_line.repeat(usize::from(area.width) / short_line.len() + 4);
let mut styled_spans = vec![];
for span in [
"Styled".blue(),
"Spans".red().on_white(),
"Bold".bold(),
"Italic".italic(),
"Underlined".underlined(),
"Strikethrough".crossed_out(),
] {
styled_spans.push(span);
styled_spans.push(" ".into());
}
vec![
Line::raw("Unstyled Line"),
Line::raw("Styled Line").black().on_red().bold().italic(),
Line::from(styled_spans),
Line::from(long_line.green().italic()),
Line::from_iter([
"Masked text: ".into(),
Span::styled(
Masked::new("password", '*'),
Style::default().fg(Color::Red),
),
Span::styled(Masked::new("my secret password", '*'), Color::Red),
]),
];

let create_block = |title| {
Block::bordered()
.style(Style::default().fg(Color::Gray))
.title(Span::styled(
title,
Style::default().add_modifier(Modifier::BOLD),
))
]
}

/// A module for common functionality used in the examples.
mod common {
use std::{
io::{self, stdout, Stdout},
panic,
};

let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Default alignment (Left), no wrap"));
f.render_widget(paragraph, layout[0]);

let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Default alignment (Left), with wrap"))
.wrap(Wrap { trim: true });
f.render_widget(paragraph, layout[1]);

let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Right alignment, with wrap"))
.right_aligned()
.wrap(Wrap { trim: true });
f.render_widget(paragraph, layout[2]);

let paragraph = Paragraph::new(text)
.style(Style::default().fg(Color::Gray))
.block(create_block("Center alignment, with wrap, with scroll"))
.centered()
.wrap(Wrap { trim: true })
.scroll((app.scroll, 0));
f.render_widget(paragraph, layout[3]);
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(())
}
}

0 comments on commit cbb148c

Please sign in to comment.