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.
This commit is contained in:
Josh McKinney 2024-06-17 22:55:38 -07:00
parent e7a4fd8fc3
commit 37be39b15c
No known key found for this signature in database
GPG key ID: 722287396A903BC5
38 changed files with 443 additions and 1330 deletions

View file

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

View file

@ -13,21 +13,11 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples //! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md //! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{ use color_eyre::Result;
error::Error,
io::{stdout, Stdout},
ops::ControlFlow,
time::Duration,
};
use itertools::Itertools; use itertools::Itertools;
use ratatui::{ use ratatui::{
backend::CrosstermBackend, backend::{Backend, CrosstermBackend},
crossterm::{ crossterm::event::{self, Event, KeyCode, KeyEventKind},
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
layout::{Alignment, Constraint, Layout, Rect}, layout::{Alignment, Constraint, Layout, Rect},
style::{Style, Stylize}, style::{Style, Stylize},
terminal::Frame, 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<()> { fn main() -> Result<()> {
let mut terminal = setup_terminal()?; let mut terminal = CrosstermBackend::stdout_with_defaults()?.to_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<()> {
loop { loop {
terminal.draw(ui)?; terminal.draw(ui)?;
if handle_events()?.is_break() { if let Event::Key(event) = event::read()? {
return Ok(()); if event.kind == KeyEventKind::Press && event.code == KeyCode::Char('q') {
} 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(()));
} }
} }
} }
Ok(ControlFlow::Continue(()))
} }
fn ui(frame: &mut Frame) { fn ui(frame: &mut Frame) {

View file

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

View file

@ -13,30 +13,26 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples //! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md //! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{ use std::time::{Duration, Instant};
io::{self, stdout, Stdout},
time::{Duration, Instant},
};
use color_eyre::Result;
use ratatui::{ use ratatui::{
backend::CrosstermBackend, backend::{Backend, CrosstermBackend},
crossterm::{ crossterm::event::{self, Event, KeyCode},
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
},
layout::{Constraint, Layout, Rect}, layout::{Constraint, Layout, Rect},
style::{Color, Stylize}, style::{Color, Stylize},
symbols::Marker, symbols::Marker,
terminal::{Frame, Terminal}, terminal::Frame,
widgets::{ widgets::{
canvas::{Canvas, Circle, Map, MapResolution, Rectangle}, canvas::{Canvas, Circle, Map, MapResolution, Rectangle},
Block, Widget, Block, Widget,
}, },
Terminal,
}; };
fn main() -> io::Result<()> { fn main() -> Result<()> {
App::run() let terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal()?;
App::new().run(terminal)
} }
struct App { struct App {
@ -69,33 +65,31 @@ impl App {
} }
} }
pub fn run() -> io::Result<()> { pub fn run(mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
let mut terminal = init_terminal()?;
let mut app = Self::new();
let mut last_tick = Instant::now(); let mut last_tick = Instant::now();
let tick_rate = Duration::from_millis(16); let tick_rate = Duration::from_millis(16);
loop { loop {
let _ = terminal.draw(|frame| app.ui(frame)); let _ = terminal.draw(|frame| self.ui(frame));
let timeout = tick_rate.saturating_sub(last_tick.elapsed()); let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if event::poll(timeout)? { if event::poll(timeout)? {
if let Event::Key(key) = event::read()? { if let Event::Key(key) = event::read()? {
match key.code { match key.code {
KeyCode::Char('q') => break, KeyCode::Char('q') => break,
KeyCode::Down | KeyCode::Char('j') => app.y += 1.0, KeyCode::Down | KeyCode::Char('j') => self.y += 1.0,
KeyCode::Up | KeyCode::Char('k') => app.y -= 1.0, KeyCode::Up | KeyCode::Char('k') => self.y -= 1.0,
KeyCode::Right | KeyCode::Char('l') => app.x += 1.0, KeyCode::Right | KeyCode::Char('l') => self.x += 1.0,
KeyCode::Left | KeyCode::Char('h') => app.x -= 1.0, KeyCode::Left | KeyCode::Char('h') => self.x -= 1.0,
_ => {} _ => {}
} }
} }
} }
if last_tick.elapsed() >= tick_rate { if last_tick.elapsed() >= tick_rate {
app.on_tick(); self.on_tick();
last_tick = Instant::now(); last_tick = Instant::now();
} }
} }
restore_terminal() Ok(())
} }
fn on_tick(&mut self) { 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]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md //! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{ use std::time::{Duration, Instant};
error::Error,
io,
time::{Duration, Instant},
};
use color_eyre::Result;
use ratatui::{ use ratatui::{
backend::{Backend, CrosstermBackend}, backend::{Backend, CrosstermBackend},
crossterm::{ crossterm::event::{self, Event, KeyCode},
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
layout::{Alignment, Constraint, Layout, Rect}, layout::{Alignment, Constraint, Layout, Rect},
style::{Color, Modifier, Style, Stylize}, style::{Color, Modifier, Style, Stylize},
symbols::{self, Marker}, symbols::{self, Marker},
terminal::{Frame, Terminal}, terminal::Frame,
text::Span, text::Span,
widgets::{block::Title, Axis, Block, Chart, Dataset, GraphType, LegendPosition}, widgets::{block::Title, Axis, Block, Chart, Dataset, GraphType, LegendPosition},
}; };
@ -97,40 +90,13 @@ impl App {
} }
} }
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<()> {
// setup terminal let mut terminal = CrosstermBackend::stdout_with_defaults()?
enable_raw_mode()?; .with_mouse_capture()?
let mut stdout = io::stdout(); .to_terminal()?;
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 tick_rate = Duration::from_millis(250);
let app = App::new(); let mut 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 last_tick = Instant::now(); let mut last_tick = Instant::now();
loop { loop {
terminal.draw(|f| ui(f, &app))?; 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 // This example shows all the colors supported by ratatui. It will render a grid of foreground
// and background colors with their names and indexes. // and background colors with their names and indexes.
use std::{ use color_eyre::Result;
error::Error,
io::{self, Stdout},
result,
time::Duration,
};
use itertools::Itertools; use itertools::Itertools;
use ratatui::{ use ratatui::{
backend::{Backend, CrosstermBackend}, backend::{Backend, CrosstermBackend},
crossterm::{ crossterm::event::{self, Event, KeyCode},
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
layout::{Alignment, Constraint, Layout, Rect}, layout::{Alignment, Constraint, Layout, Rect},
style::{Color, Style, Stylize}, style::{Color, Style, Stylize},
terminal::{Frame, Terminal}, terminal::Frame,
text::Line, text::Line,
widgets::{Block, Borders, Paragraph}, widgets::{Block, Borders, Paragraph},
}; };
type Result<T> = result::Result<T, Box<dyn Error>>;
fn main() -> Result<()> { fn main() -> Result<()> {
let mut terminal = setup_terminal()?; let mut terminal = CrosstermBackend::stdout_with_defaults()?.to_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<()> {
loop { loop {
terminal.draw(ui)?; terminal.draw(ui)?;
if let Event::Key(key) = event::read()? {
if event::poll(Duration::from_millis(250))? { if key.code == KeyCode::Char('q') {
if let Event::Key(key) = event::read()? { return Ok(());
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]); 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

@ -93,8 +93,7 @@ struct ColorsWidget {
fn main() -> Result<()> { fn main() -> Result<()> {
let terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal()?; let terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal()?;
App::default().run(terminal)?; App::default().run(terminal)
Ok(())
} }
impl App { impl App {
@ -118,9 +117,9 @@ impl App {
/// Currently, this only handles the q key to quit the app. /// Currently, this only handles the q key to quit the app.
fn handle_events(&mut self) -> Result<()> { fn handle_events(&mut self) -> Result<()> {
// Ensure that the app only blocks for a period that allows the app to render at // 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) // 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 event::poll(timeout)? {
if let Event::Key(key) = event::read()? { if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {

View file

@ -13,18 +13,12 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples //! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md //! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::io::{self, stdout}; use color_eyre::Result;
use color_eyre::{config::HookBuilder, Result};
use itertools::Itertools; use itertools::Itertools;
use ratatui::{ use ratatui::{
backend::{Backend, CrosstermBackend}, backend::{Backend, CrosstermBackend},
buffer::Buffer, buffer::Buffer,
crossterm::{ crossterm::event::{self, Event, KeyCode, KeyEventKind},
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
},
layout::{ layout::{
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio}, Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
Flex, Layout, Rect, Flex, Layout, Rect,
@ -91,16 +85,13 @@ struct ConstraintBlock {
struct SpacerBlock; struct SpacerBlock;
fn main() -> Result<()> { fn main() -> Result<()> {
init_error_hooks()?; let terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal()?;
let terminal = init_terminal()?; App::default().run(terminal)
App::default().run(terminal)?;
restore_terminal()?;
Ok(())
} }
// App behaviour // App behaviour
impl App { 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(); self.insert_test_defaults();
while self.is_running() { while self.is_running() {
@ -124,7 +115,7 @@ impl App {
self.mode == AppMode::Running 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()))?; terminal.draw(|frame| frame.render_widget(self, frame.size()))?;
Ok(()) 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]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md //! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::io::{self, stdout}; use color_eyre::Result;
use color_eyre::{config::HookBuilder, Result};
use ratatui::{ use ratatui::{
backend::{Backend, CrosstermBackend}, backend::{Backend, CrosstermBackend},
buffer::Buffer, buffer::Buffer,
crossterm::{ crossterm::event::{self, Event, KeyCode, KeyEventKind},
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
},
layout::{ layout::{
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio}, Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
Layout, Rect, Layout, Rect,
@ -83,21 +77,14 @@ enum AppState {
} }
fn main() -> Result<()> { fn main() -> Result<()> {
init_error_hooks()?;
let terminal = init_terminal()?;
// increase the cache size to avoid flickering for indeterminate layouts // increase the cache size to avoid flickering for indeterminate layouts
Layout::init_cache(100); Layout::init_cache(100);
let terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal()?;
App::default().run(terminal)?; App::default().run(terminal)
restore_terminal()?;
Ok(())
} }
impl App { 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(); self.update_max_scroll_offset();
while self.is_running() { while self.is_running() {
self.draw(&mut terminal)?; self.draw(&mut terminal)?;
@ -114,7 +101,7 @@ impl App {
self.state == AppState::Running 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()))?; terminal.draw(|frame| frame.render_widget(self, frame.size()))?;
Ok(()) Ok(())
} }
@ -421,32 +408,3 @@ impl Example {
Paragraph::new(text).centered().block(block) 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]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md //! [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::{ use ratatui::{
backend::{Backend, CrosstermBackend}, backend::{Backend, CrosstermBackend},
buffer::Buffer, buffer::Buffer,
crossterm::{ crossterm::event::{self, Event, KeyCode, MouseButton, MouseEvent, MouseEventKind},
event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, MouseButton, MouseEvent,
MouseEventKind,
},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
layout::{Constraint, Layout, Rect}, layout::{Constraint, Layout, Rect},
style::{Color, Style}, style::{Color, Style},
terminal::{Frame, Terminal}, terminal::{Frame, Terminal},
@ -143,34 +137,14 @@ impl Button<'_> {
} }
} }
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<()> {
// setup terminal let terminal = CrosstermBackend::stdout_with_defaults()?
enable_raw_mode()?; .with_mouse_capture()?
let mut stdout = io::stdout(); .to_terminal()?;
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; run_app(terminal)
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 run_app(mut terminal: Terminal<impl Backend>) -> Result<()> {
let mut selected_button: usize = 0; let mut selected_button: usize = 0;
let mut button_states = [State::Selected, State::Normal, State::Normal]; let mut button_states = [State::Selected, State::Normal, State::Normal];
loop { loop {
@ -180,11 +154,10 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
} }
match event::read()? { match event::read()? {
Event::Key(key) => { Event::Key(key) => {
if key.kind != event::KeyEventKind::Press { if key.kind == event::KeyEventKind::Press
continue; && handle_key_event(key, &mut button_states, &mut selected_button).is_break()
} {
if handle_key_event(key, &mut button_states, &mut selected_button).is_break() { return Ok(());
break;
} }
} }
Event::Mouse(mouse) => { 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]) { fn ui(frame: &mut Frame, states: [State; 3]) {

View file

@ -1,56 +1,26 @@
use std::{ use std::time::{Duration, Instant};
error::Error,
io,
time::{Duration, Instant},
};
use color_eyre::Result;
use ratatui::{ use ratatui::{
backend::{Backend, CrosstermBackend}, backend::{Backend, CrosstermBackend},
crossterm::{ crossterm::event::{self, Event, KeyCode, KeyEventKind},
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}, Terminal,
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
terminal::Terminal,
}; };
use crate::{app::App, ui}; 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 terminal = CrosstermBackend::stdout_with_defaults()?
enable_raw_mode()?; .with_mouse_capture()?
let mut stdout = io::stdout(); .to_terminal()?;
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let app = App::new("Crossterm Demo", enhanced_graphics); let app = App::new("Crossterm Demo", enhanced_graphics);
let res = run_app(&mut terminal, app, tick_rate); run_app(app, terminal, 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>( fn run_app(mut app: App, mut terminal: Terminal<impl Backend>, tick_rate: Duration) -> Result<()> {
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now(); let mut last_tick = Instant::now();
loop { while !app.should_quit {
terminal.draw(|f| ui::draw(f, &mut app))?; terminal.draw(|f| ui::draw(f, &mut app))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed()); let timeout = tick_rate.saturating_sub(last_tick.elapsed());
@ -72,8 +42,6 @@ fn run_app<B: Backend>(
app.on_tick(); app.on_tick();
last_tick = Instant::now(); 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]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md //! [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 argh::FromArgs;
use color_eyre::Result;
mod app; mod app;
#[cfg(feature = "crossterm")] #[cfg(feature = "crossterm")]
@ -38,7 +39,7 @@ struct Cli {
enhanced_graphics: bool, enhanced_graphics: bool,
} }
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<()> {
let cli: Cli = argh::from_env(); let cli: Cli = argh::from_env();
let tick_rate = Duration::from_millis(cli.tick_rate); let tick_rate = Duration::from_millis(cli.tick_rate);
#[cfg(feature = "crossterm")] #[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::{ use ratatui::{
backend::{Backend, TermionBackend}, backend::{Backend, TermionBackend},
terminal::Terminal, terminal::Terminal,
@ -13,7 +14,7 @@ use ratatui::{
use crate::{app::App, ui}; 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 // setup terminal
let stdout = io::stdout() let stdout = io::stdout()
.into_raw_mode() .into_raw_mode()
@ -22,22 +23,17 @@ pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn E
.unwrap(); .unwrap();
let stdout = MouseTerminal::from(stdout); let stdout = MouseTerminal::from(stdout);
let backend = TermionBackend::new(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); let app = App::new("Termion demo", enhanced_graphics);
run_app(&mut terminal, app, tick_rate)?; run_app(app, terminal, tick_rate)?;
Ok(()) Ok(())
} }
fn run_app<B: Backend>( fn run_app(mut app: App, mut terminal: Terminal<impl Backend>, tick_rate: Duration) -> Result<()> {
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> Result<(), Box<dyn Error>> {
let events = events(tick_rate); let events = events(tick_rate);
loop { while !app.should_quit {
terminal.draw(|f| ui::draw(f, &mut app))?; terminal.draw(|f| ui::draw(f, &mut app))?;
match events.recv()? { match events.recv()? {
@ -51,10 +47,8 @@ fn run_app<B: Backend>(
}, },
Event::Tick => app.on_tick(), Event::Tick => app.on_tick(),
} }
if app.should_quit {
return Ok(());
}
} }
Ok(())
} }
enum Event { enum Event {

View file

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

View file

@ -1,6 +1,7 @@
use std::time::Duration; use std::time::Duration;
use color_eyre::{eyre::Context, Result}; use color_eyre::{eyre::Context, Result};
use crossterm::event;
use itertools::Itertools; use itertools::Itertools;
use ratatui::{ use ratatui::{
backend::Backend, backend::Backend,
@ -17,7 +18,7 @@ use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
use crate::{ use crate::{
destroy, destroy,
tabs::{AboutTab, EmailTab, RecipeTab, TracerouteTab, WeatherTab}, tabs::{AboutTab, EmailTab, RecipeTab, TracerouteTab, WeatherTab},
term, THEME, THEME,
}; };
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
@ -49,15 +50,11 @@ enum Tab {
Weather, Weather,
} }
pub fn run(terminal: &mut Terminal<impl Backend>) -> Result<()> {
App::default().run(terminal)
}
impl App { impl App {
/// Run the app until the user quits. /// 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() { while self.is_running() {
self.draw(terminal)?; self.draw(&mut terminal)?;
self.handle_events()?; self.handle_events()?;
} }
Ok(()) 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. /// 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<()> { fn handle_events(&mut self) -> Result<()> {
let timeout = Duration::from_secs_f64(1.0 / 50.0); 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), Some(Event::Key(key)) if key.kind == KeyEventKind::Press => self.handle_key_press(key),
_ => {} _ => {}
} }
Ok(()) 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) { fn handle_key_press(&mut self, key: KeyEvent) {
match key.code { match key.code {
KeyCode::Char('q') | KeyCode::Esc => self.mode = Mode::Quit, 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 big_text;
mod colors; mod colors;
mod destroy; mod destroy;
mod errors;
mod tabs; mod tabs;
mod term;
mod theme; mod theme;
use app::App;
use color_eyre::Result; use color_eyre::Result;
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::Rect,
TerminalOptions, Viewport,
};
pub use self::{ pub use self::{
colors::{color_from_oklab, RgbSwatch}, colors::{color_from_oklab, RgbSwatch},
@ -36,9 +40,11 @@ pub use self::{
}; };
fn main() -> Result<()> { fn main() -> Result<()> {
errors::init_hooks()?; // this size is to match the size of the terminal when running the demo
let terminal = &mut term::init()?; // using vhs in a 1280x640 sized window (github social preview size)
app::run(terminal)?; let options = TerminalOptions {
term::restore()?; viewport: Viewport::Fixed(Rect::new(0, 0, 81, 18)),
Ok(()) };
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]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md //! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::io::{self, stdout}; use color_eyre::Result;
use ratatui::{ use ratatui::{
backend::CrosstermBackend, backend::{Backend, CrosstermBackend},
crossterm::{ crossterm::event::{self, Event, KeyCode},
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
},
layout::{Constraint, Layout}, layout::{Constraint, Layout},
style::{Color, Modifier, Style, Stylize}, style::{Color, Modifier, Style, Stylize},
terminal::{Frame, Terminal}, terminal::Frame,
text::{Line, Span, Text}, text::{Line, Span, Text},
widgets::{Block, Borders, Paragraph}, widgets::{Block, Borders, Paragraph},
}; };
@ -33,12 +28,9 @@ use ratatui::{
/// ///
/// When cargo-rdme supports doc comments that import from code, this will be imported /// When cargo-rdme supports doc comments that import from code, this will be imported
/// rather than copied to the lib.rs file. /// 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(); let arg = std::env::args().nth(1).unwrap_or_default();
enable_raw_mode()?; let mut terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal()?;
stdout().execute(EnterAlternateScreen)?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
let mut should_quit = false; let mut should_quit = false;
while !should_quit { while !should_quit {
terminal.draw(match arg.as_str() { terminal.draw(match arg.as_str() {
@ -48,9 +40,6 @@ fn main() -> io::Result<()> {
})?; })?;
should_quit = handle_events()?; should_quit = handle_events()?;
} }
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(()) 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 event::poll(std::time::Duration::from_millis(50))? {
if let Event::Key(key) = event::read()? { if let Event::Key(key) = event::read()? {
if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') { 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]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md //! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::io::{self, stdout}; use color_eyre::Result;
use color_eyre::{config::HookBuilder, Result};
use ratatui::{ use ratatui::{
backend::{Backend, CrosstermBackend}, backend::{Backend, CrosstermBackend},
buffer::Buffer, buffer::Buffer,
crossterm::{ crossterm::event::{self, Event, KeyCode, KeyEventKind},
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
},
layout::{ layout::{
Alignment, Alignment,
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio}, Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
@ -157,16 +151,12 @@ enum SelectedTab {
fn main() -> Result<()> { fn main() -> Result<()> {
// assuming the user changes spacing about a 100 times or so // assuming the user changes spacing about a 100 times or so
Layout::init_cache(EXAMPLE_DATA.len() * SelectedTab::iter().len() * 100); Layout::init_cache(EXAMPLE_DATA.len() * SelectedTab::iter().len() * 100);
init_error_hooks()?; let terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal()?;
let terminal = init_terminal()?; App::default().run(terminal)
App::default().run(terminal)?;
restore_terminal()?;
Ok(())
} }
impl App { 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)?; self.draw(&mut terminal)?;
while self.is_running() { while self.is_running() {
self.handle_events()?; self.handle_events()?;
@ -179,7 +169,7 @@ impl App {
self.state == AppState::Running 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()))?; terminal.draw(|frame| frame.render_widget(self, frame.size()))?;
Ok(()) 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)] #[allow(clippy::cast_possible_truncation)]
fn get_description_height(s: &str) -> u16 { fn get_description_height(s: &str) -> u16 {
if s.is_empty() { if s.is_empty() {

View file

@ -13,17 +13,13 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples //! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md //! [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::{ use ratatui::{
backend::{Backend, CrosstermBackend}, backend::{Backend, CrosstermBackend},
buffer::Buffer, buffer::Buffer,
crossterm::{ crossterm::event::{self, Event, KeyCode, KeyEventKind},
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
},
layout::{Alignment, Constraint, Layout, Rect}, layout::{Alignment, Constraint, Layout, Rect},
style::{palette::tailwind, Color, Style, Stylize}, style::{palette::tailwind, Color, Style, Stylize},
terminal::Terminal, terminal::Terminal,
@ -56,15 +52,12 @@ enum AppState {
} }
fn main() -> Result<()> { fn main() -> Result<()> {
init_error_hooks()?; let terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal()?;
let terminal = init_terminal()?; App::default().run(terminal)
App::default().run(terminal)?;
restore_terminal()?;
Ok(())
} }
impl App { 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 { while self.state != AppState::Quitting {
self.draw(&mut terminal)?; self.draw(&mut terminal)?;
self.handle_events()?; self.handle_events()?;
@ -213,32 +206,3 @@ fn title_block(title: &str) -> Block {
.title(title) .title(title)
.fg(CUSTOM_LABEL_COLOR) .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]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md //! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{ use color_eyre::{eyre::WrapErr, Result};
io::{self, Stdout}, use crossterm::event::KeyEventKind;
time::Duration,
};
use anyhow::{Context, Result};
use ratatui::{ use ratatui::{
backend::CrosstermBackend, backend::{Backend, CrosstermBackend},
crossterm::{ crossterm::event::{self, Event, KeyCode},
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
terminal::{Frame, Terminal},
widgets::Paragraph, 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 /// this is not meant to be prescriptive. It is only meant to demonstrate the basic setup and
/// teardown of a terminal application. /// teardown of a terminal application.
/// ///
/// A more robust application would probably want to handle errors and ensure that the terminal is /// A more robust application would probably want to handle errors more thouroughly. It also does
/// restored to a sane state before exiting. This example does not do that. It also does not handle /// not handle events or update any application state. It just draws a greeting and exits when the
/// events or update the application state. It just draws a greeting and exits when the user /// user presses 'q'.
/// presses 'q'.
fn main() -> Result<()> { fn main() -> Result<()> {
let mut terminal = setup_terminal().context("setup failed")?; let mut terminal = CrosstermBackend::stdout_with_defaults()?
run(&mut terminal).context("app loop failed")?; .to_terminal()
restore_terminal(&mut terminal).context("restore terminal failed")?; .wrap_err("failed to start terminal")?;
Ok(())
}
/// Setup the terminal. This is where you would enable raw mode, enter the alternate screen, and // Run the application loop. This is where you would handle events and update the application
/// hide the cursor. This example does not handle errors. A more robust application would probably // state. This example exits when the user presses 'q'. Other styles of application loops are
/// want to handle errors and ensure that the terminal is restored to a sane state before exiting. // possible, for example, you could have multiple application states and switch between them
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> { // based on events, or you could have a single application state and update it based on events.
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<()> {
loop { loop {
terminal.draw(crate::render_app)?; terminal
if should_quit()? { .draw(|frame| {
break; // 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::{ use std::{
collections::{BTreeMap, VecDeque}, collections::{BTreeMap, VecDeque},
error::Error,
io,
sync::mpsc, sync::mpsc,
thread, thread,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use color_eyre::Result;
use rand::distributions::{Distribution, Uniform}; use rand::distributions::{Distribution, Uniform};
use ratatui::{ use ratatui::{
backend::{Backend, CrosstermBackend}, backend::{Backend, CrosstermBackend},
@ -87,17 +86,11 @@ struct Worker {
tx: mpsc::Sender<Download>, tx: mpsc::Sender<Download>,
} }
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<()> {
crossterm::terminal::enable_raw_mode()?; let options = TerminalOptions {
let stdout = io::stdout(); viewport: Viewport::Inline(8),
let backend = CrosstermBackend::new(stdout); };
let mut terminal = Terminal::with_options( let terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal_with_options(options)?;
backend,
TerminalOptions {
viewport: Viewport::Inline(8),
},
)?;
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
input_handling(tx.clone()); input_handling(tx.clone());
let workers = workers(tx); let workers = workers(tx);
@ -108,11 +101,7 @@ fn main() -> Result<(), Box<dyn Error>> {
w.tx.send(d).unwrap(); w.tx.send(d).unwrap();
} }
run_app(&mut terminal, workers, downloads, rx)?; run_app(terminal, workers, downloads, rx)?;
crossterm::terminal::disable_raw_mode()?;
terminal.clear()?;
Ok(()) Ok(())
} }
@ -179,12 +168,12 @@ fn downloads() -> Downloads {
} }
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
fn run_app<B: Backend>( fn run_app(
terminal: &mut Terminal<B>, mut terminal: Terminal<impl Backend>,
workers: Vec<Worker>, workers: Vec<Worker>,
mut downloads: Downloads, mut downloads: Downloads,
rx: mpsc::Receiver<Event>, rx: mpsc::Receiver<Event>,
) -> Result<(), Box<dyn Error>> { ) -> Result<()> {
let mut redraw = true; let mut redraw = true;
loop { loop {
if redraw { if redraw {

View file

@ -13,58 +13,28 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples //! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md //! [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 itertools::Itertools;
use ratatui::{ use ratatui::{
backend::{Backend, CrosstermBackend}, backend::{Backend, CrosstermBackend},
crossterm::{ crossterm::event::{self, Event, KeyCode},
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
layout::{ layout::{
Constraint, Constraint,
Constraint::{Length, Max, Min, Percentage, Ratio}, Constraint::{Length, Max, Min, Percentage, Ratio},
Layout, Rect, Layout, Rect,
}, },
style::{Color, Style, Stylize}, style::{Color, Style, Stylize},
terminal::{Frame, Terminal}, terminal::Frame,
text::Line, text::Line,
widgets::{Block, Paragraph}, widgets::{Block, Paragraph},
}; };
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<()> {
// setup terminal let mut terminal = CrosstermBackend::stdout_with_defaults()?
enable_raw_mode()?; .with_mouse_capture()?
let mut stdout = io::stdout(); .to_terminal()?;
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<()> {
loop { loop {
terminal.draw(ui)?; terminal.draw(ui)?;
if let Event::Key(key) = event::read()? { if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('q') { if key.code == KeyCode::Char('q') {
return Ok(()); return Ok(());

View file

@ -13,17 +13,13 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples //! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md //! [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::{ use ratatui::{
backend::{Backend, CrosstermBackend}, backend::{Backend, CrosstermBackend},
buffer::Buffer, buffer::Buffer,
crossterm::{ crossterm::event::{self, Event, KeyCode, KeyEventKind},
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
},
layout::{Alignment, Constraint, Layout, Rect}, layout::{Alignment, Constraint, Layout, Rect},
style::{palette::tailwind, Color, Style, Stylize}, style::{palette::tailwind, Color, Style, Stylize},
terminal::Terminal, terminal::Terminal,
@ -48,15 +44,12 @@ enum AppState {
} }
fn main() -> Result<()> { fn main() -> Result<()> {
init_error_hooks()?; let terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal()?;
let terminal = init_terminal()?; App::default().run(terminal)
App::default().run(terminal)?;
restore_terminal()?;
Ok(())
} }
impl App { 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 { while self.state != AppState::Quitting {
self.draw(&mut terminal)?; self.draw(&mut terminal)?;
self.handle_events()?; self.handle_events()?;
@ -187,32 +180,3 @@ fn title_block(title: &str) -> Block {
.fg(CUSTOM_LABEL_COLOR) .fg(CUSTOM_LABEL_COLOR)
.padding(Padding::vertical(1)) .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]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md //! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{error::Error, io, io::stdout}; use color_eyre::Result;
use color_eyre::config::HookBuilder;
use ratatui::{ use ratatui::{
backend::{Backend, CrosstermBackend}, backend::{Backend, CrosstermBackend},
buffer::Buffer, buffer::Buffer,
crossterm::{ crossterm::event::{self, Event, KeyCode, KeyEventKind},
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
},
layout::{Alignment, Constraint, Layout, Rect}, layout::{Alignment, Constraint, Layout, Rect},
style::{palette::tailwind, Color, Modifier, Style, Stylize}, style::{palette::tailwind, Color, Modifier, Style, Stylize},
terminal::Terminal, terminal::Terminal,
@ -79,46 +73,9 @@ struct App {
items: TodoList, items: TodoList,
} }
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<()> {
// setup terminal let terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal()?;
init_error_hooks()?; App::new().run(terminal)
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(())
} }
impl App { impl App {
@ -155,7 +112,7 @@ impl App {
} }
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 { loop {
self.draw(&mut terminal)?; 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()))?; terminal.draw(|f| f.render_widget(self, f.size()))?;
Ok(()) Ok(())
} }

View file

@ -13,6 +13,7 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples //! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md //! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use color_eyre::Result;
use ratatui::{ use ratatui::{
backend::{Backend, CrosstermBackend}, backend::{Backend, CrosstermBackend},
crossterm::event::{self, Event, KeyCode, KeyEventKind}, crossterm::event::{self, Event, KeyCode, KeyEventKind},
@ -25,16 +26,15 @@ use ratatui::{
/// ///
/// [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples /// [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
/// [hello-world]: https://github.com/ratatui-org/ratatui/blob/main/examples/hello_world.rs /// [hello-world]: https://github.com/ratatui-org/ratatui/blob/main/examples/hello_world.rs
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<()> {
let mut terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal()?; let mut terminal = CrosstermBackend::stdout_with_defaults()?.to_terminal()?;
terminal.clear()?; terminal.clear()?;
loop { loop {
terminal.draw(|frame| frame.render_widget(Text::raw("Hello World!"), frame.size()))?; terminal.draw(|frame| frame.render_widget(Text::raw("Hello World!"), frame.size()))?;
if let Event::Key(key) = event::read()? { if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
break; return Ok(());
} }
} }
} }
Ok(())
} }

View file

@ -17,42 +17,22 @@
// It will render a grid of combinations of foreground and background colors with all // It will render a grid of combinations of foreground and background colors with all
// modifiers applied to them. // modifiers applied to them.
use std::{ use std::{iter::once, time::Duration};
error::Error,
io::{self, Stdout},
iter::once,
result,
time::Duration,
};
use color_eyre::Result;
use itertools::Itertools; use itertools::Itertools;
use ratatui::{ use ratatui::{
backend::{Backend, CrosstermBackend}, backend::{Backend, CrosstermBackend},
crossterm::{ crossterm::event::{self, Event, KeyCode},
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
layout::{Constraint, Layout}, layout::{Constraint, Layout},
style::{Color, Modifier, Style, Stylize}, style::{Color, Modifier, Style, Stylize},
terminal::{Frame, Terminal}, terminal::Frame,
text::Line, text::Line,
widgets::Paragraph, widgets::Paragraph,
}; };
type Result<T> = result::Result<T, Box<dyn Error>>;
fn main() -> Result<()> { fn main() -> Result<()> {
let mut terminal = setup_terminal()?; let mut terminal = CrosstermBackend::stdout_with_defaults()?.to_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<()> {
loop { loop {
terminal.draw(ui)?; 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]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md //! [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 //! How to use a panic hook to reset the terminal before printing the panic to the terminal.
//! the terminal.
//! //!
//! When exiting normally or when handling `Result::Err`, we can reset the //! When exiting normally or when handling `Result::Err`, we can reset the terminal manually at the
//! terminal manually at the end of `main` just before we print the error. //! end of `main` just before we print the error.
//! //!
//! Because a panic interrupts the normal control flow, manually resetting the //! Because a panic interrupts the normal control flow, manually resetting the terminal at the end
//! terminal at the end of `main` won't do us any good. Instead, we need to //! of `main` won't do us any good. Instead, we need to make sure to set up a panic hook that first
//! make sure to set up a panic hook that first resets the terminal before //! resets the terminal before handling the panic. This both reuses the standard panic hook to
//! handling the panic. This both reuses the standard panic hook to ensure a //! ensure a consistent panic handling UX and properly resets the terminal to not distort the
//! consistent panic handling UX and properly resets the terminal to not //! output.
//! distort the output.
//! //!
//! That's why this example is set up to show both situations, with and without //! That's why this example is set up to show both situations, with and without the chained panic
//! the chained panic hook, to see the difference. //! hook, to see the difference.
use std::{error::Error, io}; use std::{error::Error, io};
@ -44,6 +42,21 @@ use ratatui::{
type Result<T> = std::result::Result<T, Box<dyn Error>>; 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)] #[derive(Default)]
struct App { struct App {
hook_enabled: bool, 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. /// Initializes the terminal.
fn init_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> { fn init_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> {
crossterm::execute!(io::stdout(), EnterAlternateScreen)?; crossterm::execute!(io::stdout(), EnterAlternateScreen)?;
enable_raw_mode()?; enable_raw_mode()?;
#[allow(deprecated)]
let backend = CrosstermBackend::new(io::stdout()); let backend = CrosstermBackend::new(io::stdout());
let mut terminal = Terminal::new(backend)?; let mut terminal = Terminal::new(backend)?;
@ -99,7 +98,7 @@ fn reset_terminal() -> Result<()> {
} }
/// Runs the TUI loop. /// 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 { loop {
terminal.draw(|f| ui(f, app))?; terminal.draw(|f| ui(f, app))?;

View file

@ -13,30 +13,26 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples //! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md //! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{ use std::time::{Duration, Instant};
io::{self},
time::{Duration, Instant},
};
use color_eyre::Result;
use crossterm::event::KeyEventKind; use crossterm::event::KeyEventKind;
use ratatui::{ use ratatui::{
backend::{Backend, CrosstermBackend},
buffer::Buffer, buffer::Buffer,
crossterm::event::{self, Event, KeyCode}, crossterm::event::{self, Event, KeyCode},
layout::{Constraint, Layout, Rect}, layout::{Constraint, Layout, Rect},
style::{Color, Stylize}, style::{Color, Stylize},
text::{Line, Masked, Span}, text::{Line, Masked, Span},
widgets::{Block, Paragraph, Widget, Wrap}, widgets::{Block, Paragraph, Widget, Wrap},
Terminal,
}; };
use self::common::{init_terminal, install_hooks, restore_terminal, Tui}; fn main() -> Result<()> {
let terminal = CrosstermBackend::stdout_with_defaults()?
fn main() -> color_eyre::Result<()> { .with_mouse_capture()?
install_hooks()?; .to_terminal()?;
let mut terminal = init_terminal()?; App::new().run(terminal)
let mut app = App::new();
app.run(&mut terminal)?;
restore_terminal()?;
Ok(())
} }
#[derive(Debug)] #[derive(Debug)]
@ -60,9 +56,9 @@ impl App {
} }
/// Run the app until the user exits. /// 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 { while !self.should_exit {
self.draw(terminal)?; self.draw(&mut terminal)?;
self.handle_events()?; self.handle_events()?;
if self.last_tick.elapsed() >= Self::TICK_RATE { if self.last_tick.elapsed() >= Self::TICK_RATE {
self.on_tick(); self.on_tick();
@ -73,13 +69,13 @@ impl App {
} }
/// Draw the app to the terminal. /// 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()))?; terminal.draw(|frame| frame.render_widget(self, frame.size()))?;
Ok(()) Ok(())
} }
/// Handle events from the terminal. /// 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()); let timeout = Self::TICK_RATE.saturating_sub(self.last_tick.elapsed());
while event::poll(timeout)? { while event::poll(timeout)? {
if let Event::Key(key) = event::read()? { 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 // See also https://github.com/joshka/tui-popup and
// https://github.com/sephiroth74/tui-confirm-dialog // https://github.com/sephiroth74/tui-confirm-dialog
use std::{error::Error, io}; use color_eyre::Result;
use ratatui::{ use ratatui::{
backend::{Backend, CrosstermBackend}, backend::{Backend, CrosstermBackend},
crossterm::{ crossterm::event::{self, Event, KeyCode, KeyEventKind},
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
layout::{Constraint, Layout, Rect}, layout::{Constraint, Layout, Rect},
style::Stylize, style::Stylize,
terminal::{Frame, Terminal}, terminal::Frame,
widgets::{Block, Clear, Paragraph, Wrap}, widgets::{Block, Clear, Paragraph, Wrap},
}; };
@ -41,35 +36,12 @@ impl App {
} }
} }
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<()> {
// setup terminal let mut terminal = CrosstermBackend::stdout_with_defaults()?
enable_raw_mode()?; .with_mouse_capture()?
let mut stdout = io::stdout(); .to_terminal()?;
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it let mut app = App::new();
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 { loop {
terminal.draw(|f| ui(f, &app))?; terminal.draw(|f| ui(f, &app))?;

View file

@ -13,25 +13,39 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples //! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md //! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{ use color_eyre::Result;
io::{self, stdout}, use crossterm::event::{self, Event, KeyCode, KeyEventKind};
thread::sleep,
time::Duration,
};
use indoc::indoc; use indoc::indoc;
use itertools::izip; use itertools::izip;
use ratatui::{ use ratatui::{
backend::{Backend, CrosstermBackend}, backend::{Backend, CrosstermBackend},
crossterm::terminal::{disable_raw_mode, enable_raw_mode}, terminal::Viewport,
terminal::{Terminal, Viewport},
widgets::Paragraph, widgets::Paragraph,
TerminalOptions, 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 /// A fun example of using half block characters to draw a logo
#[allow(clippy::many_single_char_names)] #[allow(clippy::many_single_char_names)]
fn logo() -> String { fn logo() -> Paragraph<'static> {
let r = indoc! {" 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}")) .map(|(r, a, t, u, i)| format!("{r:5}{a:5}{t:4}{a:5}{t:4}{u:5}{i:5}"))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("\n") .join("\n");
} Paragraph::new(lines)
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(())
} }

View file

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

View file

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

View file

@ -13,26 +13,20 @@
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples //! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md //! [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 itertools::Itertools;
use ratatui::{ use ratatui::{
backend::{Backend, CrosstermBackend}, backend::{Backend, CrosstermBackend},
crossterm::{ crossterm::event::{self, Event, KeyCode, KeyEventKind},
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
layout::{Constraint, Layout, Margin, Rect}, layout::{Constraint, Layout, Margin, Rect},
style::{self, Color, Modifier, Style, Stylize}, style::{palette::tailwind, Color, Modifier, Style, Stylize},
terminal::{Frame, Terminal}, terminal::Frame,
text::{Line, Text}, text::{Line, Text},
widgets::{ widgets::{
Block, BorderType, Cell, HighlightSpacing, Paragraph, Row, Scrollbar, ScrollbarOrientation, Block, BorderType, Cell, HighlightSpacing, Paragraph, Row, Scrollbar, ScrollbarOrientation,
ScrollbarState, Table, TableState, ScrollbarState, Table, TableState,
}, },
}; };
use style::palette::tailwind;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
const PALETTES: [tailwind::Palette; 4] = [ const PALETTES: [tailwind::Palette; 4] = [
@ -46,31 +40,37 @@ const INFO_TEXT: &str =
const ITEM_HEIGHT: usize = 4; const ITEM_HEIGHT: usize = 4;
struct TableColors { fn main() -> Result<()> {
buffer_bg: Color, let mut terminal = CrosstermBackend::stdout_with_defaults()?
header_bg: Color, .with_mouse_capture()?
header_fg: Color, .to_terminal()?;
row_fg: Color,
selected_style_fg: Color,
normal_row_color: Color,
alt_row_color: Color,
footer_border_color: Color,
}
impl TableColors { let mut app = App::new();
const fn new(color: &tailwind::Palette) -> Self { loop {
Self { terminal.draw(|f| ui(f, &mut app))?;
buffer_bg: tailwind::SLATE.c950,
header_bg: color.c900, if let Event::Key(key) = event::read()? {
header_fg: tailwind::SLATE.c200, if key.kind == KeyEventKind::Press {
row_fg: tailwind::SLATE.c200, match key.code {
selected_style_fg: color.c400, KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
normal_row_color: tailwind::SLATE.c950, KeyCode::Char('j') | KeyCode::Down => app.next(),
alt_row_color: tailwind::SLATE.c900, KeyCode::Char('k') | KeyCode::Up => app.previous(),
footer_border_color: color.c400, 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 { struct Data {
name: String, name: String,
@ -96,13 +96,30 @@ impl Data {
} }
} }
struct App { struct TableColors {
state: TableState, buffer_bg: Color,
items: Vec<Data>, header_bg: Color,
longest_item_lens: (u16, u16, u16), // order is (name, address, email) header_fg: Color,
scroll_state: ScrollbarState, row_fg: Color,
colors: TableColors, selected_style_fg: Color,
color_index: usize, 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 { impl App {
@ -186,53 +203,6 @@ fn generate_fake_names() -> Vec<Data> {
.collect_vec() .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) { fn ui(f: &mut Frame, app: &mut App) {
let rects = Layout::vertical([Constraint::Min(5), Constraint::Length(3)]).split(f.size()); 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]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md //! [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::{ use ratatui::{
backend::{Backend, CrosstermBackend}, backend::{Backend, CrosstermBackend},
buffer::Buffer, buffer::Buffer,
crossterm::{ crossterm::event::{self, Event, KeyCode, KeyEventKind},
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
},
layout::{Constraint, Layout, Rect}, layout::{Constraint, Layout, Rect},
style::{palette::tailwind, Color, Stylize}, style::{palette::tailwind, Color, Stylize},
symbols, symbols,
@ -31,7 +27,11 @@ use ratatui::{
text::Line, text::Line,
widgets::{Block, Padding, Paragraph, Tabs, Widget}, 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)] #[derive(Default)]
struct App { struct App {
@ -46,31 +46,30 @@ enum AppState {
Quitting, Quitting,
} }
#[derive(Default, Clone, Copy, Display, FromRepr, EnumIter)] #[derive(Default, Clone, Copy)]
enum SelectedTab { enum SelectedTab {
#[default] #[default]
#[strum(to_string = "Tab 1")]
Tab1, Tab1,
#[strum(to_string = "Tab 2")]
Tab2, Tab2,
#[strum(to_string = "Tab 3")]
Tab3, Tab3,
#[strum(to_string = "Tab 4")]
Tab4, Tab4,
} }
fn main() -> Result<()> { impl fmt::Display for SelectedTab {
init_error_hooks()?; fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut terminal = init_terminal()?; match self {
App::default().run(&mut terminal)?; Self::Tab1 => write!(f, "Tab 1"),
restore_terminal()?; Self::Tab2 => write!(f, "Tab 2"),
Ok(()) Self::Tab3 => write!(f, "Tab 3"),
Self::Tab4 => write!(f, "Tab 4"),
}
}
} }
impl App { 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 { while self.state == AppState::Running {
self.draw(terminal)?; self.draw(&mut terminal)?;
self.handle_events()?; self.handle_events()?;
} }
Ok(()) 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 { impl Widget for &App {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
use Constraint::{Length, Min}; use Constraint::{Length, Min};
@ -142,7 +125,7 @@ impl Widget for &App {
impl App { impl App {
fn render_tabs(&self, area: Rect, buf: &mut Buffer) { 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 highlight_style = (Color::default(), self.selected_tab.palette().c700);
let selected_tab_index = self.selected_tab as usize; let selected_tab_index = self.selected_tab as usize;
Tabs::new(titles) Tabs::new(titles)
@ -177,6 +160,35 @@ impl Widget for SelectedTab {
} }
impl 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` /// Return tab's name as a styled `Line`
fn title(self) -> Line<'static> { fn title(self) -> Line<'static> {
format!(" {self} ") 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/ // 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::{ use ratatui::{
backend::{Backend, CrosstermBackend}, backend::{Backend, CrosstermBackend},
crossterm::{ crossterm::event::{self, Event, KeyCode, KeyEventKind},
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
layout::{Constraint, Layout}, layout::{Constraint, Layout},
style::{Color, Modifier, Style, Stylize}, style::{Color, Modifier, Style, Stylize},
terminal::{Frame, Terminal}, terminal::Frame,
text::{Line, Span, Text}, text::{Line, Span, Text},
widgets::{Block, List, ListItem, Paragraph}, widgets::{Block, List, ListItem, Paragraph},
}; };
@ -135,35 +130,12 @@ impl App {
} }
} }
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<()> {
// setup terminal let mut terminal = CrosstermBackend::stdout_with_defaults()?
enable_raw_mode()?; .with_mouse_capture()?
let mut stdout = io::stdout(); .to_terminal()?;
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it let mut app = App::new();
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 { loop {
terminal.draw(|f| ui(f, &app))?; terminal.draw(|f| ui(f, &app))?;

View file

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

View file

@ -400,7 +400,7 @@ impl<W: io::Write> CrosstermBackend<W> {
let _ = CrosstermBackend::reset(stderr()); let _ = CrosstermBackend::reset(stderr());
error(e) error(e)
})) }))
.map_err(|error| io::Error::other(error))?; .map_err(io::Error::other)?;
panic::set_hook(Box::new(move |info| { panic::set_hook(Box::new(move |info| {
// ignore errors here because we are already in an error state // ignore errors here because we are already in an error state
let _ = CrosstermBackend::reset(stderr()); let _ = CrosstermBackend::reset(stderr());