From ed51c4b3429862201b2c5de6846fea4c237f0ffb Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Thu, 22 Aug 2024 05:16:35 -0700 Subject: [PATCH] feat(terminal): Add ratatui::init() and restore() methods (#1289) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These are simple opinionated methods for creating a terminal that is useful to use in most apps. The new init method creates a crossterm backend writing to stdout, enables raw mode, enters the alternate screen, and sets a panic handler that restores the terminal on panic. A minimal hello world now looks a bit like: ```rust use ratatui::{ crossterm::event::{self, Event}, text::Text, Frame, }; fn main() { let mut terminal = ratatui::init(); loop { terminal .draw(|frame: &mut Frame| frame.render_widget(Text::raw("Hello World!"), frame.area())) .expect("Failed to draw"); if matches!(event::read().expect("failed to read event"), Event::Key(_)) { break; } } ratatui::restore(); } ``` A type alias `DefaultTerminal` is added to represent this terminal type and to simplify any cases where applications need to pass this terminal around. It is equivalent to: `Terminal>` We also added `ratatui::try_init()` and `try_restore()`, for situations where you might want to handle initialization errors yourself instead of letting the panic handler fire and cleanup. Simple Apps should prefer the `init` and `restore` functions over these functions. Corresponding functions to allow passing a `TerminalOptions` with a `Viewport` (e.g. inline, fixed) are also available (`init_with_options`, and `try_init_with_options`). The existing code to create a backend and terminal will remain and is not deprecated by this approach. This just provides a simple one line initialization using the common options. --------- Co-authored-by: Orhun Parmaksız --- Cargo.toml | 2 +- examples/async.rs | 161 +++++--------- examples/barchart-grouped.rs | 103 ++------- examples/barchart.rs | 138 ++++-------- examples/block.rs | 70 ++---- examples/calendar.rs | 60 ++---- examples/canvas.rs | 58 ++--- examples/chart.rs | 222 +++++++++---------- examples/colors.rs | 63 ++---- examples/colors_rgb.rs | 68 ++---- examples/constraint-explorer.rs | 67 ++---- examples/constraints.rs | 70 ++---- examples/custom_widget.rs | 52 ++--- examples/demo/crossterm.rs | 6 +- examples/demo/termion.rs | 2 +- examples/demo/termwiz.rs | 6 +- examples/demo/ui.rs | 54 ++--- examples/demo2/app.rs | 38 ++-- examples/demo2/errors.rs | 18 -- examples/demo2/main.rs | 25 ++- examples/demo2/term.rs | 46 ---- examples/docsrs.rs | 51 ++--- examples/flex.rs | 83 ++------ examples/gauge.rs | 61 +----- examples/hello_world.rs | 70 ++---- examples/hyperlink.rs | 82 ++------ examples/inline.rs | 87 ++++---- examples/layout.rs | 56 ++--- examples/line_gauge.rs | 67 ++---- examples/list.rs | 67 +----- examples/minimal.rs | 42 ++-- examples/modifiers.rs | 61 ++---- examples/panic.rs | 191 +++++++---------- examples/paragraph.rs | 101 +-------- examples/popup.rs | 137 +++++------- examples/ratatui-logo.rs | 28 +-- examples/scrollbar.rs | 363 +++++++++++++++----------------- examples/sparkline.rs | 171 ++++++--------- examples/table.rs | 263 +++++++++++------------ examples/tabs.rs | 67 ++---- examples/tracing.rs | 64 +----- examples/user_input.rs | 244 +++++++++------------ src/lib.rs | 61 ++---- src/terminal.rs | 6 + src/terminal/init.rs | 244 +++++++++++++++++++++ 45 files changed, 1485 insertions(+), 2511 deletions(-) delete mode 100644 examples/demo2/errors.rs delete mode 100644 examples/demo2/term.rs create mode 100644 src/terminal/init.rs diff --git a/Cargo.toml b/Cargo.toml index 616c7159..979c52eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -295,7 +295,7 @@ doc-scrape-examples = true [[example]] name = "hyperlink" -required-features = ["crossterm", "unstable-widget-ref"] +required-features = ["crossterm"] doc-scrape-examples = true [[example]] diff --git a/examples/async.rs b/examples/async.rs index 4961ec0f..525c96a1 100644 --- a/examples/async.rs +++ b/examples/async.rs @@ -42,24 +42,21 @@ use octocrab::{ }; use ratatui::{ buffer::Buffer, - crossterm::event::{Event, EventStream, KeyCode}, - layout::{Constraint, Offset, Rect}, - style::{Modifier, Stylize}, + crossterm::event::{Event, EventStream, KeyCode, KeyEventKind}, + layout::{Constraint, Layout, Rect}, + style::{Style, Stylize}, text::Line, - widgets::{ - Block, BorderType, HighlightSpacing, Row, StatefulWidget, Table, TableState, Widget, - }, + widgets::{Block, HighlightSpacing, Row, StatefulWidget, Table, TableState, Widget}, + DefaultTerminal, Frame, }; -use self::terminal::Terminal; - #[tokio::main] async fn main() -> Result<()> { color_eyre::install()?; init_octocrab()?; - let terminal = terminal::init()?; + let terminal = ratatui::init(); let app_result = App::default().run(terminal).await; - terminal::restore(); + ratatui::restore(); app_result } @@ -78,48 +75,45 @@ fn init_octocrab() -> Result<()> { #[derive(Debug, Default)] struct App { should_quit: bool, - pulls: PullRequestsWidget, + pull_requests: PullRequestListWidget, } impl App { const FRAMES_PER_SECOND: f32 = 60.0; - pub async fn run(mut self, mut terminal: Terminal) -> Result<()> { - self.pulls.run(); + pub async fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { + self.pull_requests.run(); - let mut interval = - tokio::time::interval(Duration::from_secs_f32(1.0 / Self::FRAMES_PER_SECOND)); + let period = Duration::from_secs_f32(1.0 / Self::FRAMES_PER_SECOND); + let mut interval = tokio::time::interval(period); let mut events = EventStream::new(); while !self.should_quit { tokio::select! { - _ = interval.tick() => self.draw(&mut terminal)?, - Some(Ok(event)) = events.next() => self.handle_event(&event), + _ = interval.tick() => { terminal.draw(|frame| self.draw(frame))?; }, + Some(Ok(event)) = events.next() => self.handle_event(&event), } } Ok(()) } - fn draw(&self, terminal: &mut Terminal) -> Result<()> { - terminal.draw(|frame| { - let area = frame.area(); - frame.render_widget( - Line::from("ratatui async example").centered().cyan().bold(), - area, - ); - let area = area.offset(Offset { x: 0, y: 1 }).intersection(area); - frame.render_widget(&self.pulls, area); - })?; - Ok(()) + fn draw(&self, frame: &mut Frame) { + let vertical = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]); + let [title_area, body_area] = vertical.areas(frame.area()); + let title = Line::from("Ratatui async example").centered().bold(); + frame.render_widget(title, title_area); + frame.render_widget(&self.pull_requests, body_area); } fn handle_event(&mut self, event: &Event) { - if let Event::Key(event) = event { - match event.code { - KeyCode::Char('q') => self.should_quit = true, - KeyCode::Char('j') => self.pulls.scroll_down(), - KeyCode::Char('k') => self.pulls.scroll_up(), - _ => {} + if let Event::Key(key) = event { + if key.kind == KeyEventKind::Press { + match key.code { + KeyCode::Char('q') | KeyCode::Esc => self.should_quit = true, + KeyCode::Char('j') | KeyCode::Down => self.pull_requests.scroll_down(), + KeyCode::Char('k') | KeyCode::Up => self.pull_requests.scroll_up(), + _ => {} + } } } } @@ -128,19 +122,19 @@ impl App { /// A widget that displays a list of pull requests. /// /// This is an async widget that fetches the list of pull requests from the GitHub API. It contains -/// an inner `Arc>` that holds the state of the widget. Cloning the widget -/// will clone the Arc, so you can pass it around to other threads, and this is used to spawn a -/// background task to fetch the pull requests. +/// an inner `Arc>` that holds the state of the widget. Cloning the +/// widget will clone the Arc, so you can pass it around to other threads, and this is used to spawn +/// a background task to fetch the pull requests. #[derive(Debug, Clone, Default)] -struct PullRequestsWidget { - inner: Arc>, - selected_index: usize, // no need to lock this since it's only accessed by the main thread +struct PullRequestListWidget { + state: Arc>, } #[derive(Debug, Default)] -struct PullRequests { - pulls: Vec, +struct PullRequestListState { + pull_requests: Vec, loading_state: LoadingState, + table_state: TableState, } #[derive(Debug, Clone)] @@ -159,7 +153,7 @@ enum LoadingState { Error(String), } -impl PullRequestsWidget { +impl PullRequestListWidget { /// Start fetching the pull requests in the background. /// /// This method spawns a background task that fetches the pull requests from the GitHub API. @@ -187,9 +181,12 @@ impl PullRequestsWidget { } fn on_load(&self, page: &Page) { let prs = page.items.iter().map(Into::into); - let mut inner = self.inner.write().unwrap(); - inner.loading_state = LoadingState::Loaded; - inner.pulls.extend(prs); + let mut state = self.state.write().unwrap(); + state.loading_state = LoadingState::Loaded; + state.pull_requests.extend(prs); + if !state.pull_requests.is_empty() { + state.table_state.select(Some(0)); + } } fn on_err(&self, err: &octocrab::Error) { @@ -197,15 +194,15 @@ impl PullRequestsWidget { } fn set_loading_state(&self, state: LoadingState) { - self.inner.write().unwrap().loading_state = state; + self.state.write().unwrap().loading_state = state; } - fn scroll_down(&mut self) { - self.selected_index = self.selected_index.saturating_add(1); + fn scroll_down(&self) { + self.state.write().unwrap().table_state.scroll_down_by(1); } - fn scroll_up(&mut self) { - self.selected_index = self.selected_index.saturating_sub(1); + fn scroll_up(&self) { + self.state.write().unwrap().table_state.scroll_up_by(1); } } @@ -225,19 +222,19 @@ impl From<&OctoPullRequest> for PullRequest { } } -impl Widget for &PullRequestsWidget { +impl Widget for &PullRequestListWidget { fn render(self, area: Rect, buf: &mut Buffer) { - let inner = self.inner.read().unwrap(); + let mut state = self.state.write().unwrap(); - // a block with a right aligned title with the loading state - let loading_state = Line::from(format!("{:?}", inner.loading_state)).right_aligned(); + // a block with a right aligned title with the loading state on the right + let loading_state = Line::from(format!("{:?}", state.loading_state)).right_aligned(); let block = Block::bordered() - .border_type(BorderType::Rounded) .title("Pull Requests") - .title(loading_state); + .title(loading_state) + .title_bottom("j/k to scroll, q to quit"); // a table with the list of pull requests - let rows = inner.pulls.iter(); + let rows = state.pull_requests.iter(); let widths = [ Constraint::Length(5), Constraint::Fill(1), @@ -247,10 +244,9 @@ impl Widget for &PullRequestsWidget { .block(block) .highlight_spacing(HighlightSpacing::Always) .highlight_symbol(">>") - .highlight_style(Modifier::REVERSED); - let mut table_state = TableState::new().with_selected(self.selected_index); + .highlight_style(Style::new().on_blue()); - StatefulWidget::render(table, area, buf, &mut table_state); + StatefulWidget::render(table, area, buf, &mut state.table_state); } } @@ -260,46 +256,3 @@ impl From<&PullRequest> for Row<'_> { Row::new(vec![pr.id, pr.title, pr.url]) } } - -mod terminal { - use std::io; - - use ratatui::{ - backend::CrosstermBackend, - crossterm::{ - execute, - terminal::{ - disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, - }, - }, - }; - - /// A type alias for the terminal type used in this example. - pub type Terminal = ratatui::Terminal>; - - pub fn init() -> io::Result { - set_panic_hook(); - enable_raw_mode()?; - execute!(io::stdout(), EnterAlternateScreen)?; - let backend = CrosstermBackend::new(io::stdout()); - Terminal::new(backend) - } - - fn set_panic_hook() { - let hook = std::panic::take_hook(); - std::panic::set_hook(Box::new(move |info| { - restore(); - hook(info); - })); - } - - /// Restores the terminal to its original state. - pub fn restore() { - if let Err(err) = disable_raw_mode() { - eprintln!("error disabling raw mode: {err}"); - } - if let Err(err) = execute!(io::stdout(), LeaveAlternateScreen) { - eprintln!("error leaving alternate screen: {err}"); - } - } -} diff --git a/examples/barchart-grouped.rs b/examples/barchart-grouped.rs index 7fa445b0..c9bf44f2 100644 --- a/examples/barchart-grouped.rs +++ b/examples/barchart-grouped.rs @@ -17,15 +17,21 @@ use std::iter::zip; use color_eyre::Result; use ratatui::{ - crossterm::event::{self, Event, KeyCode}, + crossterm::event::{self, Event, KeyCode, KeyEventKind}, layout::{Constraint, Direction, Layout}, style::{Color, Style, Stylize}, text::Line, widgets::{Bar, BarChart, BarGroup, Block}, - Frame, + DefaultTerminal, Frame, }; -use self::terminal::Terminal; +fn main() -> Result<()> { + color_eyre::install()?; + let terminal = ratatui::init(); + let app_result = App::new().run(terminal); + ratatui::restore(); + app_result +} const COMPANY_COUNT: usize = 3; const PERIOD_COUNT: usize = 4; @@ -47,15 +53,6 @@ struct Company { color: Color, } -fn main() -> Result<()> { - color_eyre::install()?; - let mut terminal = terminal::init()?; - let app = App::new(); - app.run(&mut terminal)?; - terminal::restore()?; - Ok(()) -} - impl App { const fn new() -> Self { Self { @@ -65,33 +62,27 @@ impl App { } } - fn run(mut self, terminal: &mut Terminal) -> Result<()> { + fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { while !self.should_exit { - self.draw(terminal)?; + terminal.draw(|frame| self.draw(frame))?; self.handle_events()?; } Ok(()) } - fn draw(&self, terminal: &mut Terminal) -> Result<()> { - terminal.draw(|frame| self.render(frame))?; - Ok(()) - } - fn handle_events(&mut self) -> Result<()> { if let Event::Key(key) = event::read()? { - if key.code == KeyCode::Char('q') { + if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { self.should_exit = true; } } Ok(()) } - fn render(&self, frame: &mut Frame) { + fn draw(&self, frame: &mut Frame) { use Constraint::{Fill, Length, Min}; - let [title, top, bottom] = Layout::vertical([Length(1), Fill(1), Min(20)]) - .spacing(1) - .areas(frame.area()); + let vertical = Layout::vertical([Length(1), Fill(1), Min(20)]).spacing(1); + let [title, top, bottom] = vertical.areas(frame.area()); frame.render_widget("Grouped Barchart".bold().into_centered_line(), title); frame.render_widget(self.vertical_revenue_barchart(), top); @@ -100,12 +91,12 @@ impl App { /// Create a vertical revenue bar chart with the data from the `revenues` field. fn vertical_revenue_barchart(&self) -> BarChart<'_> { - let title = Line::from("Company revenues (Vertical)").centered(); let mut barchart = BarChart::default() - .block(Block::new().title(title)) + .block(Block::new().title(Line::from("Company revenues (Vertical)").centered())) .bar_gap(0) .bar_width(6) .group_gap(2); + for group in self .revenues .iter() @@ -218,63 +209,3 @@ impl Company { .value_style(Style::new().fg(Color::Black).bg(self.color)) } } - -/// Contains functions common to all examples -mod terminal { - use std::{ - io::{self, stdout, Stdout}, - panic, - }; - - use ratatui::{ - backend::CrosstermBackend, - crossterm::{ - execute, - terminal::{ - disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen, - LeaveAlternateScreen, - }, - }, - }; - - // A type alias to simplify the usage of the terminal and make it easier to change the backend - // or choice of writer. - pub type Terminal = ratatui::Terminal>; - - /// Initialize the terminal by enabling raw mode and entering the alternate screen. - /// - /// This function should be called before the program starts to ensure that the terminal is in - /// the correct state for the application. - pub fn init() -> io::Result { - install_panic_hook(); - enable_raw_mode()?; - execute!(stdout(), EnterAlternateScreen)?; - let backend = CrosstermBackend::new(stdout()); - let terminal = Terminal::new(backend)?; - Ok(terminal) - } - - /// Restore the terminal by leaving the alternate screen and disabling raw mode. - /// - /// This function should be called before the program exits to ensure that the terminal is - /// restored to its original state. - pub fn restore() -> io::Result<()> { - disable_raw_mode()?; - execute!( - stdout(), - LeaveAlternateScreen, - Clear(ClearType::FromCursorDown), - ) - } - - /// Install a panic hook that restores the terminal before printing the panic. - /// - /// This prevents error messages from being messed up by the terminal state. - fn install_panic_hook() { - let panic_hook = panic::take_hook(); - panic::set_hook(Box::new(move |panic_info| { - let _ = restore(); - panic_hook(panic_info); - })); - } -} diff --git a/examples/barchart.rs b/examples/barchart.rs index c6e6a93c..4941f38c 100644 --- a/examples/barchart.rs +++ b/examples/barchart.rs @@ -16,29 +16,27 @@ use color_eyre::Result; use rand::{thread_rng, Rng}; use ratatui::{ - crossterm::event::{self, Event, KeyCode}, + crossterm::event::{self, Event, KeyCode, KeyEventKind}, layout::{Constraint, Direction, Layout}, style::{Color, Style, Stylize}, text::Line, widgets::{Bar, BarChart, BarGroup, Block}, + DefaultTerminal, Frame, }; -use self::terminal::Terminal; +fn main() -> Result<()> { + color_eyre::install()?; + let terminal = ratatui::init(); + let app_result = App::new().run(terminal); + ratatui::restore(); + app_result +} struct App { should_exit: bool, temperatures: Vec, } -fn main() -> Result<()> { - color_eyre::install()?; - let mut terminal = terminal::init()?; - let app = App::new(); - app.run(&mut terminal)?; - terminal::restore()?; - Ok(()) -} - impl App { fn new() -> Self { let mut rng = thread_rng(); @@ -49,29 +47,24 @@ impl App { } } - fn run(mut self, terminal: &mut Terminal) -> Result<()> { + fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { while !self.should_exit { - self.draw(terminal)?; + terminal.draw(|frame| self.draw(frame))?; self.handle_events()?; } Ok(()) } - fn draw(&self, terminal: &mut Terminal) -> Result<()> { - terminal.draw(|frame| self.render(frame))?; - Ok(()) - } - fn handle_events(&mut self) -> Result<()> { if let Event::Key(key) = event::read()? { - if key.code == KeyCode::Char('q') { + if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { self.should_exit = true; } } Ok(()) } - fn render(&self, frame: &mut ratatui::Frame) { + fn draw(&self, frame: &mut Frame) { let [title, vertical, horizontal] = Layout::vertical([ Constraint::Length(1), Constraint::Fill(1), @@ -79,6 +72,7 @@ impl App { ]) .spacing(1) .areas(frame.area()); + frame.render_widget("Barchart".bold().into_centered_line(), title); frame.render_widget(vertical_barchart(&self.temperatures), vertical); frame.render_widget(horizontal_barchart(&self.temperatures), horizontal); @@ -89,16 +83,8 @@ impl App { fn vertical_barchart(temperatures: &[u8]) -> BarChart { let bars: Vec = temperatures .iter() - .map(|v| u64::from(*v)) .enumerate() - .map(|(i, value)| { - Bar::default() - .value(value) - .label(Line::from(format!("{i:>02}:00"))) - .text_value(format!("{value:>3}°")) - .style(temperature_style(value)) - .value_style(temperature_style(value).reversed()) - }) + .map(|(hour, value)| vertical_bar(hour, value)) .collect(); let title = Line::from("Weather (Vertical)").centered(); BarChart::default() @@ -107,21 +93,21 @@ fn vertical_barchart(temperatures: &[u8]) -> BarChart { .bar_width(5) } +fn vertical_bar(hour: usize, temperature: &u8) -> Bar { + Bar::default() + .value(u64::from(*temperature)) + .label(Line::from(format!("{hour:>02}:00"))) + .text_value(format!("{temperature:>3}°")) + .style(temperature_style(*temperature)) + .value_style(temperature_style(*temperature).reversed()) +} + /// Create a horizontal bar chart from the temperatures data. fn horizontal_barchart(temperatures: &[u8]) -> BarChart { let bars: Vec = temperatures .iter() - .map(|v| u64::from(*v)) .enumerate() - .map(|(i, value)| { - let style = temperature_style(value); - Bar::default() - .value(value) - .label(Line::from(format!("{i:>02}:00"))) - .text_value(format!("{value:>3}°")) - .style(style) - .value_style(style.reversed()) - }) + .map(|(hour, value)| horizontal_bar(hour, value)) .collect(); let title = Line::from("Weather (Horizontal)").centered(); BarChart::default() @@ -132,69 +118,19 @@ fn horizontal_barchart(temperatures: &[u8]) -> BarChart { .direction(Direction::Horizontal) } +fn horizontal_bar(hour: usize, temperature: &u8) -> Bar { + let style = temperature_style(*temperature); + Bar::default() + .value(u64::from(*temperature)) + .label(Line::from(format!("{hour:>02}:00"))) + .text_value(format!("{temperature:>3}°")) + .style(style) + .value_style(style.reversed()) +} + /// create a yellow to red value based on the value (50-90) -fn temperature_style(value: u64) -> Style { - let green = (255.0 * (1.0 - (value - 50) as f64 / 40.0)) as u8; +fn temperature_style(value: u8) -> Style { + let green = (255.0 * (1.0 - f64::from(value - 50) / 40.0)) as u8; let color = Color::Rgb(255, green, 0); Style::new().fg(color) } - -/// Contains functions common to all examples -mod terminal { - use std::{ - io::{self, stdout, Stdout}, - panic, - }; - - use ratatui::{ - backend::CrosstermBackend, - crossterm::{ - execute, - terminal::{ - disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen, - LeaveAlternateScreen, - }, - }, - }; - - // A type alias to simplify the usage of the terminal and make it easier to change the backend - // or choice of writer. - pub type Terminal = ratatui::Terminal>; - - /// Initialize the terminal by enabling raw mode and entering the alternate screen. - /// - /// This function should be called before the program starts to ensure that the terminal is in - /// the correct state for the application. - pub fn init() -> io::Result { - install_panic_hook(); - enable_raw_mode()?; - execute!(stdout(), EnterAlternateScreen)?; - let backend = CrosstermBackend::new(stdout()); - let terminal = Terminal::new(backend)?; - Ok(terminal) - } - - /// Restore the terminal by leaving the alternate screen and disabling raw mode. - /// - /// This function should be called before the program exits to ensure that the terminal is - /// restored to its original state. - pub fn restore() -> io::Result<()> { - disable_raw_mode()?; - execute!( - stdout(), - LeaveAlternateScreen, - Clear(ClearType::FromCursorDown), - ) - } - - /// Install a panic hook that restores the terminal before printing the panic. - /// - /// This prevents error messages from being messed up by the terminal state. - fn install_panic_hook() { - let panic_hook = panic::take_hook(); - panic::set_hook(Box::new(move |panic_info| { - let _ = restore(); - panic_hook(panic_info); - })); - } -} diff --git a/examples/block.rs b/examples/block.rs index cd27ac7d..c3102941 100644 --- a/examples/block.rs +++ b/examples/block.rs @@ -13,21 +13,10 @@ //! [examples]: https://github.com/ratatui/ratatui/blob/main/examples //! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md -use std::{ - error::Error, - io::{stdout, Stdout}, - ops::ControlFlow, - time::Duration, -}; - +use color_eyre::Result; use itertools::Itertools; use ratatui::{ - backend::CrosstermBackend, - crossterm::{ - event::{self, Event, KeyCode}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - }, + crossterm::event::{self, Event, KeyCode, KeyEventKind}, layout::{Alignment, Constraint, Layout, Rect}, style::{Style, Stylize}, text::Line, @@ -35,61 +24,29 @@ use ratatui::{ block::{Position, Title}, Block, BorderType, Borders, Padding, Paragraph, Wrap, }, - Frame, + DefaultTerminal, Frame, }; -// 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>; -type Result = std::result::Result>; - fn main() -> Result<()> { - let mut terminal = setup_terminal()?; - let result = run(&mut terminal); - restore_terminal(terminal)?; - - if let Err(err) = result { - eprintln!("{err:?}"); - } - Ok(()) + color_eyre::install()?; + let terminal = ratatui::init(); + let result = run(terminal); + ratatui::restore(); + result } -fn setup_terminal() -> Result { - 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<()> { +fn run(mut terminal: DefaultTerminal) -> Result<()> { loop { - terminal.draw(ui)?; - if handle_events()?.is_break() { - return Ok(()); - } - } -} - -fn handle_events() -> Result> { - if event::poll(Duration::from_millis(100))? { + terminal.draw(draw)?; if let Event::Key(key) = event::read()? { - if key.code == KeyCode::Char('q') { - return Ok(ControlFlow::Break(())); + if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { + break Ok(()); } } } - Ok(ControlFlow::Continue(())) } -fn ui(frame: &mut Frame) { +fn draw(frame: &mut Frame) { let (title_area, layout) = calculate_layout(frame.area()); render_title(frame, title_area); @@ -183,7 +140,6 @@ fn render_styled_block(paragraph: &Paragraph, frame: &mut Frame, area: Rect) { frame.render_widget(paragraph.clone().block(block), area); } -// Note: this currently renders incorrectly, see https://github.com/ratatui/ratatui/issues/349 fn render_styled_title(paragraph: &Paragraph, frame: &mut Frame, area: Rect) { let block = Block::bordered() .title("Styled title") diff --git a/examples/calendar.rs b/examples/calendar.rs index 2e52b8e9..29fc2c50 100644 --- a/examples/calendar.rs +++ b/examples/calendar.rs @@ -13,58 +13,40 @@ //! [examples]: https://github.com/ratatui/ratatui/blob/main/examples //! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md -use std::{error::Error, io}; - +use color_eyre::Result; use ratatui::{ - backend::CrosstermBackend, - crossterm::{ - event::{self, Event, KeyCode}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - }, - layout::{Constraint, Layout, Rect}, + crossterm::event::{self, Event, KeyCode, KeyEventKind}, + layout::{Constraint, Layout, Margin}, style::{Color, Modifier, Style}, widgets::calendar::{CalendarEventStore, DateStyler, Monthly}, - Frame, Terminal, + DefaultTerminal, Frame, }; use time::{Date, Month, OffsetDateTime}; -fn main() -> Result<(), Box> { - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen)?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; +fn main() -> Result<()> { + color_eyre::install()?; + let terminal = ratatui::init(); + let result = run(terminal); + ratatui::restore(); + result +} +fn run(mut terminal: DefaultTerminal) -> Result<()> { loop { - let _ = terminal.draw(draw); - + terminal.draw(draw)?; if let Event::Key(key) = event::read()? { - #[allow(clippy::single_match)] - match key.code { - KeyCode::Char(_) => { - break; - } - _ => {} - }; + if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { + break Ok(()); + } } } - - disable_raw_mode()?; - execute!(terminal.backend_mut(), LeaveAlternateScreen)?; - terminal.show_cursor()?; - Ok(()) } fn draw(frame: &mut Frame) { - let app_area = frame.area(); - - let calarea = Rect { - x: app_area.x + 1, - y: app_area.y + 1, - height: app_area.height - 1, - width: app_area.width - 1, - }; + let area = frame.area().inner(Margin { + vertical: 1, + horizontal: 1, + }); let mut start = OffsetDateTime::now_local() .unwrap() @@ -76,7 +58,7 @@ fn draw(frame: &mut Frame) { let list = make_dates(start.year()); - let rows = Layout::vertical([Constraint::Ratio(1, 3); 3]).split(calarea); + let rows = Layout::vertical([Constraint::Ratio(1, 3); 3]).split(area); let cols = rows.iter().flat_map(|row| { Layout::horizontal([Constraint::Ratio(1, 4); 4]) .split(*row) diff --git a/examples/canvas.rs b/examples/canvas.rs index 9390cc73..6a1228c0 100644 --- a/examples/canvas.rs +++ b/examples/canvas.rs @@ -13,18 +13,11 @@ //! [examples]: https://github.com/ratatui/ratatui/blob/main/examples //! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md -use std::{ - io::{self, stdout, Stdout}, - time::{Duration, Instant}, -}; +use std::time::{Duration, Instant}; +use color_eyre::Result; use ratatui::{ - backend::CrosstermBackend, - crossterm::{ - event::{self, Event, KeyCode}, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - ExecutableCommand, - }, + crossterm::event::{self, Event, KeyCode}, layout::{Constraint, Layout, Rect}, style::{Color, Stylize}, symbols::Marker, @@ -32,11 +25,15 @@ use ratatui::{ canvas::{Canvas, Circle, Map, MapResolution, Rectangle}, Block, Widget, }, - Frame, Terminal, + DefaultTerminal, Frame, }; -fn main() -> io::Result<()> { - App::run() +fn main() -> Result<()> { + color_eyre::install()?; + let terminal = ratatui::init(); + let app_result = App::new().run(terminal); + ratatui::restore(); + app_result } struct App { @@ -69,33 +66,30 @@ impl App { } } - pub fn run() -> io::Result<()> { - let mut terminal = init_terminal()?; - let mut app = Self::new(); - let mut last_tick = Instant::now(); + pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { let tick_rate = Duration::from_millis(16); + let mut last_tick = Instant::now(); loop { - let _ = terminal.draw(|frame| app.ui(frame)); + terminal.draw(|frame| self.draw(frame))?; let timeout = tick_rate.saturating_sub(last_tick.elapsed()); if event::poll(timeout)? { if let Event::Key(key) = event::read()? { match key.code { - KeyCode::Char('q') => break, - KeyCode::Down | KeyCode::Char('j') => app.y += 1.0, - KeyCode::Up | KeyCode::Char('k') => app.y -= 1.0, - KeyCode::Right | KeyCode::Char('l') => app.x += 1.0, - KeyCode::Left | KeyCode::Char('h') => app.x -= 1.0, + KeyCode::Char('q') => break Ok(()), + KeyCode::Down | KeyCode::Char('j') => self.y += 1.0, + KeyCode::Up | KeyCode::Char('k') => self.y -= 1.0, + KeyCode::Right | KeyCode::Char('l') => self.x += 1.0, + KeyCode::Left | KeyCode::Char('h') => self.x -= 1.0, _ => {} } } } if last_tick.elapsed() >= tick_rate { - app.on_tick(); + self.on_tick(); last_tick = Instant::now(); } } - restore_terminal() } fn on_tick(&mut self) { @@ -128,7 +122,7 @@ impl App { self.ball.y += self.vy; } - fn ui(&self, frame: &mut Frame) { + fn draw(&self, frame: &mut Frame) { let horizontal = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]); let vertical = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]); @@ -204,15 +198,3 @@ impl App { }) } } - -fn init_terminal() -> io::Result>> { - enable_raw_mode()?; - stdout().execute(EnterAlternateScreen)?; - Terminal::new(CrosstermBackend::new(stdout())) -} - -fn restore_terminal() -> io::Result<()> { - disable_raw_mode()?; - stdout().execute(LeaveAlternateScreen)?; - Ok(()) -} diff --git a/examples/chart.rs b/examples/chart.rs index ec3c2ef4..18dec88c 100644 --- a/examples/chart.rs +++ b/examples/chart.rs @@ -13,27 +13,35 @@ //! [examples]: https://github.com/ratatui/ratatui/blob/main/examples //! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md -use std::{ - error::Error, - io, - time::{Duration, Instant}, -}; +use std::time::{Duration, Instant}; +use color_eyre::Result; use ratatui::{ - backend::{Backend, CrosstermBackend}, - crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - }, + crossterm::event::{self, Event, KeyCode}, layout::{Alignment, Constraint, Layout, Rect}, style::{Color, Modifier, Style, Stylize}, symbols::{self, Marker}, text::Span, widgets::{block::Title, Axis, Block, Chart, Dataset, GraphType, LegendPosition}, - Frame, Terminal, + DefaultTerminal, Frame, }; +fn main() -> Result<()> { + color_eyre::install()?; + let terminal = ratatui::init(); + let app_result = App::new().run(terminal); + ratatui::restore(); + app_result +} + +struct App { + signal1: SinSignal, + data1: Vec<(f64, f64)>, + signal2: SinSignal, + data2: Vec<(f64, f64)>, + window: [f64; 2], +} + #[derive(Clone)] struct SinSignal { x: f64, @@ -62,14 +70,6 @@ impl Iterator for SinSignal { } } -struct App { - signal1: SinSignal, - data1: Vec<(f64, f64)>, - signal2: SinSignal, - data2: Vec<(f64, f64)>, - window: [f64; 2], -} - impl App { fn new() -> Self { let mut signal1 = SinSignal::new(0.2, 3.0, 18.0); @@ -85,6 +85,27 @@ impl App { } } + fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { + let tick_rate = Duration::from_millis(250); + let mut last_tick = Instant::now(); + loop { + terminal.draw(|frame| self.draw(frame))?; + + let timeout = tick_rate.saturating_sub(last_tick.elapsed()); + if event::poll(timeout)? { + if let Event::Key(key) = event::read()? { + if key.code == KeyCode::Char('q') { + return Ok(()); + } + } + } + if last_tick.elapsed() >= tick_rate { + self.on_tick(); + last_tick = Instant::now(); + } + } + } + fn on_tick(&mut self) { self.data1.drain(0..5); self.data1.extend(self.signal1.by_ref().take(5)); @@ -95,118 +116,65 @@ impl App { self.window[0] += 1.0; self.window[1] += 1.0; } -} -fn main() -> Result<(), Box> { - // setup terminal - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; + fn draw(&self, frame: &mut Frame) { + let [top, bottom] = Layout::vertical([Constraint::Fill(1); 2]).areas(frame.area()); + let [animated_chart, bar_chart] = + Layout::horizontal([Constraint::Fill(1), Constraint::Length(29)]).areas(top); + let [line_chart, scatter] = Layout::horizontal([Constraint::Fill(1); 2]).areas(bottom); - // create app and run it - let tick_rate = Duration::from_millis(250); - let app = App::new(); - let res = run_app(&mut terminal, app, tick_rate); - - // restore terminal - disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; - terminal.show_cursor()?; - - if let Err(err) = res { - println!("{err:?}"); + self.render_animated_chart(frame, animated_chart); + render_barchart(frame, bar_chart); + render_line_chart(frame, line_chart); + render_scatter(frame, scatter); } - Ok(()) -} + fn render_animated_chart(&self, frame: &mut Frame, area: Rect) { + let x_labels = vec![ + Span::styled( + format!("{}", self.window[0]), + Style::default().add_modifier(Modifier::BOLD), + ), + Span::raw(format!("{}", (self.window[0] + self.window[1]) / 2.0)), + Span::styled( + format!("{}", self.window[1]), + Style::default().add_modifier(Modifier::BOLD), + ), + ]; + let datasets = vec![ + Dataset::default() + .name("data2") + .marker(symbols::Marker::Dot) + .style(Style::default().fg(Color::Cyan)) + .data(&self.data1), + Dataset::default() + .name("data3") + .marker(symbols::Marker::Braille) + .style(Style::default().fg(Color::Yellow)) + .data(&self.data2), + ]; -fn run_app( - terminal: &mut Terminal, - mut app: App, - tick_rate: Duration, -) -> io::Result<()> { - let mut last_tick = Instant::now(); - loop { - terminal.draw(|f| ui(f, &app))?; + let chart = Chart::new(datasets) + .block(Block::bordered()) + .x_axis( + Axis::default() + .title("X Axis") + .style(Style::default().fg(Color::Gray)) + .labels(x_labels) + .bounds(self.window), + ) + .y_axis( + Axis::default() + .title("Y Axis") + .style(Style::default().fg(Color::Gray)) + .labels(["-20".bold(), "0".into(), "20".bold()]) + .bounds([-20.0, 20.0]), + ); - let timeout = tick_rate.saturating_sub(last_tick.elapsed()); - if event::poll(timeout)? { - if let Event::Key(key) = event::read()? { - if key.code == KeyCode::Char('q') { - return Ok(()); - } - } - } - if last_tick.elapsed() >= tick_rate { - app.on_tick(); - last_tick = Instant::now(); - } + frame.render_widget(chart, area); } } -fn ui(frame: &mut Frame, app: &App) { - let [top, bottom] = Layout::vertical([Constraint::Fill(1); 2]).areas(frame.area()); - let [animated_chart, bar_chart] = - Layout::horizontal([Constraint::Fill(1), Constraint::Length(29)]).areas(top); - let [line_chart, scatter] = Layout::horizontal([Constraint::Fill(1); 2]).areas(bottom); - - render_animated_chart(frame, animated_chart, app); - render_barchart(frame, bar_chart); - render_line_chart(frame, line_chart); - render_scatter(frame, scatter); -} - -fn render_animated_chart(f: &mut Frame, area: Rect, app: &App) { - let x_labels = vec![ - Span::styled( - format!("{}", app.window[0]), - Style::default().add_modifier(Modifier::BOLD), - ), - Span::raw(format!("{}", (app.window[0] + app.window[1]) / 2.0)), - Span::styled( - format!("{}", app.window[1]), - Style::default().add_modifier(Modifier::BOLD), - ), - ]; - let datasets = vec![ - Dataset::default() - .name("data2") - .marker(symbols::Marker::Dot) - .style(Style::default().fg(Color::Cyan)) - .data(&app.data1), - Dataset::default() - .name("data3") - .marker(symbols::Marker::Braille) - .style(Style::default().fg(Color::Yellow)) - .data(&app.data2), - ]; - - let chart = Chart::new(datasets) - .block(Block::bordered()) - .x_axis( - Axis::default() - .title("X Axis") - .style(Style::default().fg(Color::Gray)) - .labels(x_labels) - .bounds(app.window), - ) - .y_axis( - Axis::default() - .title("Y Axis") - .style(Style::default().fg(Color::Gray)) - .labels(["-20".bold(), "0".into(), "20".bold()]) - .bounds([-20.0, 20.0]), - ); - - f.render_widget(chart, area); -} - fn render_barchart(frame: &mut Frame, bar_chart: Rect) { let dataset = Dataset::default() .marker(symbols::Marker::HalfBlock) @@ -252,7 +220,7 @@ fn render_barchart(frame: &mut Frame, bar_chart: Rect) { frame.render_widget(chart, bar_chart); } -fn render_line_chart(f: &mut Frame, area: Rect) { +fn render_line_chart(frame: &mut Frame, area: Rect) { let datasets = vec![Dataset::default() .name("Line from only 2 points".italic()) .marker(symbols::Marker::Braille) @@ -285,10 +253,10 @@ fn render_line_chart(f: &mut Frame, area: Rect) { .legend_position(Some(LegendPosition::TopLeft)) .hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2))); - f.render_widget(chart, area); + frame.render_widget(chart, area); } -fn render_scatter(f: &mut Frame, area: Rect) { +fn render_scatter(frame: &mut Frame, area: Rect) { let datasets = vec![ Dataset::default() .name("Heavy") @@ -334,7 +302,7 @@ fn render_scatter(f: &mut Frame, area: Rect) { ) .hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2))); - f.render_widget(chart, area); + frame.render_widget(chart, area); } // Data from https://ourworldindata.org/space-exploration-satellites diff --git a/examples/colors.rs b/examples/colors.rs index 542293eb..12cba415 100644 --- a/examples/colors.rs +++ b/examples/colors.rs @@ -16,55 +16,37 @@ // This example shows all the colors supported by ratatui. It will render a grid of foreground // and background colors with their names and indexes. -use std::{ - error::Error, - io::{self, Stdout}, - result, - time::Duration, -}; - +use color_eyre::Result; use itertools::Itertools; use ratatui::{ - backend::{Backend, CrosstermBackend}, - crossterm::{ - event::{self, Event, KeyCode}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - }, + crossterm::event::{self, Event, KeyCode, KeyEventKind}, layout::{Alignment, Constraint, Layout, Rect}, style::{Color, Style, Stylize}, text::Line, widgets::{Block, Borders, Paragraph}, - Frame, Terminal, + DefaultTerminal, Frame, }; -type Result = result::Result>; - fn main() -> Result<()> { - let mut terminal = setup_terminal()?; - let res = run_app(&mut terminal); - restore_terminal(terminal)?; - if let Err(err) = res { - eprintln!("{err:?}"); - } - Ok(()) + color_eyre::install()?; + let terminal = ratatui::init(); + let app_result = run(terminal); + ratatui::restore(); + app_result } -fn run_app(terminal: &mut Terminal) -> io::Result<()> { +fn run(mut terminal: DefaultTerminal) -> Result<()> { loop { - terminal.draw(ui)?; - - if event::poll(Duration::from_millis(250))? { - if let Event::Key(key) = event::read()? { - if key.code == KeyCode::Char('q') { - return Ok(()); - } + terminal.draw(draw)?; + if let Event::Key(key) = event::read()? { + if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { + return Ok(()); } } } } -fn ui(frame: &mut Frame) { +fn draw(frame: &mut Frame) { let layout = Layout::vertical([ Constraint::Length(30), Constraint::Length(17), @@ -271,20 +253,3 @@ fn render_indexed_grayscale(frame: &mut Frame, area: Rect) { frame.render_widget(paragraph, layout[i as usize - 232]); } } - -fn setup_terminal() -> Result>> { - 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>) -> Result<()> { - disable_raw_mode()?; - execute!(terminal.backend_mut(), LeaveAlternateScreen)?; - terminal.show_cursor()?; - Ok(()) -} diff --git a/examples/colors_rgb.rs b/examples/colors_rgb.rs index 016a2aed..7557a4be 100644 --- a/examples/colors_rgb.rs +++ b/examples/colors_rgb.rs @@ -26,29 +26,28 @@ // is useful when the state is only used by the widget and doesn't need to be shared with // other widgets. -use std::{ - io::stdout, - panic, - time::{Duration, Instant}, -}; +use std::time::{Duration, Instant}; -use color_eyre::{config::HookBuilder, eyre, Result}; +use color_eyre::Result; use palette::{convert::FromColorUnclamped, Okhsv, Srgb}; use ratatui::{ - backend::{Backend, CrosstermBackend}, buffer::Buffer, - crossterm::{ - event::{self, Event, KeyCode, KeyEventKind}, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - ExecutableCommand, - }, + crossterm::event::{self, Event, KeyCode, KeyEventKind}, layout::{Constraint, Layout, Position, Rect}, style::Color, text::Text, widgets::Widget, - Terminal, + DefaultTerminal, }; +fn main() -> Result<()> { + color_eyre::install()?; + let terminal = ratatui::init(); + let app_result = App::default().run(terminal); + ratatui::restore(); + app_result +} + #[derive(Debug, Default)] struct App { /// The current state of the app (running or quit) @@ -99,19 +98,11 @@ struct ColorsWidget { frame_count: usize, } -fn main() -> Result<()> { - install_error_hooks()?; - let terminal = init_terminal()?; - App::default().run(terminal)?; - restore_terminal()?; - Ok(()) -} - impl App { /// Run the app /// /// This is the main event loop for the app. - pub fn run(mut self, mut terminal: Terminal) -> Result<()> { + pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { while self.is_running() { terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?; self.handle_events()?; @@ -263,36 +254,3 @@ impl ColorsWidget { } } } - -/// Install `color_eyre` panic and error hooks -/// -/// The hooks restore the terminal to a usable state before printing the error message. -fn install_error_hooks() -> Result<()> { - let (panic, error) = HookBuilder::default().into_hooks(); - let panic = panic.into_panic_hook(); - let error = error.into_eyre_hook(); - eyre::set_hook(Box::new(move |e| { - let _ = restore_terminal(); - error(e) - }))?; - panic::set_hook(Box::new(move |info| { - let _ = restore_terminal(); - panic(info); - })); - Ok(()) -} - -fn init_terminal() -> Result> { - enable_raw_mode()?; - stdout().execute(EnterAlternateScreen)?; - let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?; - terminal.clear()?; - terminal.hide_cursor()?; - Ok(terminal) -} - -fn restore_terminal() -> Result<()> { - disable_raw_mode()?; - stdout().execute(LeaveAlternateScreen)?; - Ok(()) -} diff --git a/examples/constraint-explorer.rs b/examples/constraint-explorer.rs index b89efc9e..c1736c8c 100644 --- a/examples/constraint-explorer.rs +++ b/examples/constraint-explorer.rs @@ -13,18 +13,11 @@ //! [examples]: https://github.com/ratatui/ratatui/blob/main/examples //! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md -use std::io::{self, stdout}; - -use color_eyre::{config::HookBuilder, Result}; +use color_eyre::Result; use itertools::Itertools; use ratatui::{ - backend::{Backend, CrosstermBackend}, buffer::Buffer, - crossterm::{ - event::{self, Event, KeyCode, KeyEventKind}, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - ExecutableCommand, - }, + crossterm::event::{self, Event, KeyCode, KeyEventKind}, layout::{ Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio}, Flex, Layout, Rect, @@ -36,10 +29,18 @@ use ratatui::{ symbols::{self, line}, text::{Line, Span, Text}, widgets::{Block, Paragraph, Widget, Wrap}, - Terminal, + DefaultTerminal, }; use strum::{Display, EnumIter, FromRepr}; +fn main() -> Result<()> { + color_eyre::install()?; + let terminal = ratatui::init(); + let app_result = App::default().run(terminal); + ratatui::restore(); + app_result +} + #[derive(Default)] struct App { mode: AppMode, @@ -90,21 +91,13 @@ struct ConstraintBlock { /// ``` struct SpacerBlock; -fn main() -> Result<()> { - init_error_hooks()?; - let terminal = init_terminal()?; - App::default().run(terminal)?; - restore_terminal()?; - Ok(()) -} - // App behaviour impl App { - fn run(&mut self, mut terminal: Terminal) -> Result<()> { + fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { self.insert_test_defaults(); while self.is_running() { - self.draw(&mut terminal)?; + terminal.draw(|frame| frame.render_widget(&self, frame.area()))?; self.handle_events()?; } Ok(()) @@ -124,11 +117,6 @@ impl App { self.mode == AppMode::Running } - fn draw(&self, terminal: &mut Terminal) -> io::Result<()> { - terminal.draw(|frame| frame.render_widget(self, frame.area()))?; - Ok(()) - } - fn handle_events(&mut self) -> Result<()> { match event::read()? { Event::Key(key) if key.kind == KeyEventKind::Press => match key.code { @@ -621,32 +609,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> { - 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(()) -} diff --git a/examples/constraints.rs b/examples/constraints.rs index d64f82c6..9f1aa58b 100644 --- a/examples/constraints.rs +++ b/examples/constraints.rs @@ -13,17 +13,10 @@ //! [examples]: https://github.com/ratatui/ratatui/blob/main/examples //! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md -use std::io::{self, stdout}; - -use color_eyre::{config::HookBuilder, Result}; +use color_eyre::Result; use ratatui::{ - backend::{Backend, CrosstermBackend}, buffer::Buffer, - crossterm::{ - event::{self, Event, KeyCode, KeyEventKind}, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - ExecutableCommand, - }, + crossterm::event::{self, Event, KeyCode, KeyEventKind}, layout::{ Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio}, Layout, Rect, @@ -35,7 +28,7 @@ use ratatui::{ Block, Padding, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Tabs, Widget, }, - Terminal, + DefaultTerminal, }; use strum::{Display, EnumIter, FromRepr, IntoEnumIterator}; @@ -53,6 +46,14 @@ const RATIO_COLOR: Color = tailwind::SLATE.c900; // priority 4 const FILL_COLOR: Color = tailwind::SLATE.c950; +fn main() -> Result<()> { + color_eyre::install()?; + let terminal = ratatui::init(); + let app_result = App::default().run(terminal); + ratatui::restore(); + app_result +} + #[derive(Default, Clone, Copy)] struct App { selected_tab: SelectedTab, @@ -82,22 +83,11 @@ enum AppState { Quit, } -fn main() -> Result<()> { - init_error_hooks()?; - let terminal = init_terminal()?; - - App::default().run(terminal)?; - - restore_terminal()?; - - Ok(()) -} - impl App { - fn run(&mut self, mut terminal: Terminal) -> Result<()> { + fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { self.update_max_scroll_offset(); while self.is_running() { - self.draw(&mut terminal)?; + terminal.draw(|frame| frame.render_widget(self, frame.area()))?; self.handle_events()?; } Ok(()) @@ -111,11 +101,6 @@ impl App { self.state == AppState::Running } - fn draw(self, terminal: &mut Terminal) -> io::Result<()> { - terminal.draw(|frame| frame.render_widget(self, frame.area()))?; - Ok(()) - } - fn handle_events(&mut self) -> Result<()> { if let Event::Key(key) = event::read()? { if key.kind != KeyEventKind::Press { @@ -418,32 +403,3 @@ impl Example { Paragraph::new(text).centered().block(block) } } - -fn init_error_hooks() -> Result<()> { - let (panic, error) = HookBuilder::default().into_hooks(); - let panic = panic.into_panic_hook(); - let error = error.into_eyre_hook(); - color_eyre::eyre::set_hook(Box::new(move |e| { - let _ = restore_terminal(); - error(e) - }))?; - std::panic::set_hook(Box::new(move |info| { - let _ = restore_terminal(); - panic(info); - })); - Ok(()) -} - -fn init_terminal() -> Result> { - 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(()) -} diff --git a/examples/custom_widget.rs b/examples/custom_widget.rs index 071319ef..faa07059 100644 --- a/examples/custom_widget.rs +++ b/examples/custom_widget.rs @@ -13,10 +13,10 @@ //! [examples]: https://github.com/ratatui/ratatui/blob/main/examples //! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md -use std::{error::Error, io, ops::ControlFlow, time::Duration}; +use std::{io::stdout, ops::ControlFlow, time::Duration}; +use color_eyre::Result; use ratatui::{ - backend::{Backend, CrosstermBackend}, buffer::Buffer, crossterm::{ event::{ @@ -24,15 +24,26 @@ use ratatui::{ MouseEventKind, }, execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }, layout::{Constraint, Layout, Rect}, style::{Color, Style}, text::Line, widgets::{Paragraph, Widget}, - Frame, Terminal, + DefaultTerminal, Frame, }; +fn main() -> Result<()> { + color_eyre::install()?; + let terminal = ratatui::init(); + execute!(stdout(), EnableMouseCapture)?; + let app_result = run(terminal); + ratatui::restore(); + if let Err(err) = execute!(stdout(), DisableMouseCapture) { + eprintln!("Error disabling mouse capture: {err}"); + } + app_result +} + /// A custom widget that renders a button with a label, theme and state. #[derive(Debug, Clone)] struct Button<'a> { @@ -143,38 +154,11 @@ impl Button<'_> { } } -fn main() -> Result<(), Box> { - // setup terminal - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - - // create app and run it - let res = run_app(&mut terminal); - - // restore terminal - disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; - terminal.show_cursor()?; - - if let Err(err) = res { - println!("{err:?}"); - } - - Ok(()) -} - -fn run_app(terminal: &mut Terminal) -> io::Result<()> { +fn run(mut terminal: DefaultTerminal) -> Result<()> { let mut selected_button: usize = 0; let mut button_states = [State::Selected, State::Normal, State::Normal]; loop { - terminal.draw(|frame| ui(frame, button_states))?; + terminal.draw(|frame| draw(frame, button_states))?; if !event::poll(Duration::from_millis(100))? { continue; } @@ -196,7 +180,7 @@ fn run_app(terminal: &mut Terminal) -> io::Result<()> { Ok(()) } -fn ui(frame: &mut Frame, states: [State; 3]) { +fn draw(frame: &mut Frame, states: [State; 3]) { let vertical = Layout::vertical([ Constraint::Length(1), Constraint::Max(3), diff --git a/examples/demo/crossterm.rs b/examples/demo/crossterm.rs index daaeb665..004ede93 100644 --- a/examples/demo/crossterm.rs +++ b/examples/demo/crossterm.rs @@ -26,7 +26,7 @@ pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box Result<(), Box( ) -> io::Result<()> { let mut last_tick = Instant::now(); loop { - terminal.draw(|f| ui::draw(f, &mut app))?; + terminal.draw(|frame| ui::draw(frame, &mut app))?; let timeout = tick_rate.saturating_sub(last_tick.elapsed()); if event::poll(timeout)? { diff --git a/examples/demo/termion.rs b/examples/demo/termion.rs index 87deaafb..7d75dc91 100644 --- a/examples/demo/termion.rs +++ b/examples/demo/termion.rs @@ -39,7 +39,7 @@ fn run_app( ) -> Result<(), Box> { let events = events(tick_rate); loop { - terminal.draw(|f| ui::draw(f, &mut app))?; + terminal.draw(|frame| ui::draw(frame, &mut app))?; match events.recv()? { Event::Input(key) => match key { diff --git a/examples/demo/termwiz.rs b/examples/demo/termwiz.rs index 18110ed6..f1a3226f 100644 --- a/examples/demo/termwiz.rs +++ b/examples/demo/termwiz.rs @@ -22,12 +22,12 @@ pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box Result<(), Box> { let mut last_tick = Instant::now(); loop { - terminal.draw(|f| ui::draw(f, &mut app))?; + terminal.draw(|frame| ui::draw(frame, &mut app))?; let timeout = tick_rate.saturating_sub(last_tick.elapsed()); if let Some(input) = terminal diff --git a/examples/demo/ui.rs b/examples/demo/ui.rs index 40958068..03991ed7 100644 --- a/examples/demo/ui.rs +++ b/examples/demo/ui.rs @@ -13,8 +13,8 @@ use ratatui::{ use crate::app::App; -pub fn draw(f: &mut Frame, app: &mut App) { - let chunks = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).split(f.area()); +pub fn draw(frame: &mut Frame, app: &mut App) { + let chunks = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).split(frame.area()); let tabs = app .tabs .titles @@ -24,28 +24,28 @@ pub fn draw(f: &mut Frame, app: &mut App) { .block(Block::bordered().title(app.title)) .highlight_style(Style::default().fg(Color::Yellow)) .select(app.tabs.index); - f.render_widget(tabs, chunks[0]); + frame.render_widget(tabs, chunks[0]); match app.tabs.index { - 0 => draw_first_tab(f, app, chunks[1]), - 1 => draw_second_tab(f, app, chunks[1]), - 2 => draw_third_tab(f, app, chunks[1]), + 0 => draw_first_tab(frame, app, chunks[1]), + 1 => draw_second_tab(frame, app, chunks[1]), + 2 => draw_third_tab(frame, app, chunks[1]), _ => {} }; } -fn draw_first_tab(f: &mut Frame, app: &mut App, area: Rect) { +fn draw_first_tab(frame: &mut Frame, app: &mut App, area: Rect) { let chunks = Layout::vertical([ Constraint::Length(9), Constraint::Min(8), Constraint::Length(7), ]) .split(area); - draw_gauges(f, app, chunks[0]); - draw_charts(f, app, chunks[1]); - draw_text(f, chunks[2]); + draw_gauges(frame, app, chunks[0]); + draw_charts(frame, app, chunks[1]); + draw_text(frame, chunks[2]); } -fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) { +fn draw_gauges(frame: &mut Frame, app: &mut App, area: Rect) { let chunks = Layout::vertical([ Constraint::Length(2), Constraint::Length(3), @@ -54,7 +54,7 @@ fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) { .margin(1) .split(area); let block = Block::bordered().title("Graphs"); - f.render_widget(block, area); + frame.render_widget(block, area); let label = format!("{:.2}%", app.progress * 100.0); let gauge = Gauge::default() @@ -68,7 +68,7 @@ fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) { .use_unicode(app.enhanced_graphics) .label(label) .ratio(app.progress); - f.render_widget(gauge, chunks[0]); + frame.render_widget(gauge, chunks[0]); let sparkline = Sparkline::default() .block(Block::new().title("Sparkline:")) @@ -79,7 +79,7 @@ fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) { } else { symbols::bar::THREE_LEVELS }); - f.render_widget(sparkline, chunks[1]); + frame.render_widget(sparkline, chunks[1]); let line_gauge = LineGauge::default() .block(Block::new().title("LineGauge:")) @@ -90,11 +90,11 @@ fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) { symbols::line::NORMAL }) .ratio(app.progress); - f.render_widget(line_gauge, chunks[2]); + frame.render_widget(line_gauge, chunks[2]); } #[allow(clippy::too_many_lines)] -fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) { +fn draw_charts(frame: &mut Frame, app: &mut App, area: Rect) { let constraints = if app.show_chart { vec![Constraint::Percentage(50), Constraint::Percentage(50)] } else { @@ -120,7 +120,7 @@ fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) { .block(Block::bordered().title("List")) .highlight_style(Style::default().add_modifier(Modifier::BOLD)) .highlight_symbol("> "); - f.render_stateful_widget(tasks, chunks[0], &mut app.tasks.state); + frame.render_stateful_widget(tasks, chunks[0], &mut app.tasks.state); // Draw logs let info_style = Style::default().fg(Color::Blue); @@ -146,7 +146,7 @@ fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) { }) .collect(); let logs = List::new(logs).block(Block::bordered().title("List")); - f.render_stateful_widget(logs, chunks[1], &mut app.logs.state); + frame.render_stateful_widget(logs, chunks[1], &mut app.logs.state); } let barchart = BarChart::default() @@ -167,7 +167,7 @@ fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) { ) .label_style(Style::default().fg(Color::Yellow)) .bar_style(Style::default().fg(Color::Green)); - f.render_widget(barchart, chunks[1]); + frame.render_widget(barchart, chunks[1]); } if app.show_chart { let x_labels = vec![ @@ -227,11 +227,11 @@ fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) { Span::styled("20", Style::default().add_modifier(Modifier::BOLD)), ]), ); - f.render_widget(chart, chunks[1]); + frame.render_widget(chart, chunks[1]); } } -fn draw_text(f: &mut Frame, area: Rect) { +fn draw_text(frame: &mut Frame, area: Rect) { let text = vec![ text::Line::from("This is a paragraph with several lines. You can change style your text the way you want"), text::Line::from(""), @@ -266,10 +266,10 @@ fn draw_text(f: &mut Frame, area: Rect) { .add_modifier(Modifier::BOLD), )); let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true }); - f.render_widget(paragraph, area); + frame.render_widget(paragraph, area); } -fn draw_second_tab(f: &mut Frame, app: &mut App, area: Rect) { +fn draw_second_tab(frame: &mut Frame, app: &mut App, area: Rect) { let chunks = Layout::horizontal([Constraint::Percentage(30), Constraint::Percentage(70)]).split(area); let up_style = Style::default().fg(Color::Green); @@ -298,7 +298,7 @@ fn draw_second_tab(f: &mut Frame, app: &mut App, area: Rect) { .bottom_margin(1), ) .block(Block::bordered().title("Servers")); - f.render_widget(table, chunks[0]); + frame.render_widget(table, chunks[0]); let map = Canvas::default() .block(Block::bordered().title("World")) @@ -352,10 +352,10 @@ fn draw_second_tab(f: &mut Frame, app: &mut App, area: Rect) { }) .x_bounds([-180.0, 180.0]) .y_bounds([-90.0, 90.0]); - f.render_widget(map, chunks[1]); + frame.render_widget(map, chunks[1]); } -fn draw_third_tab(f: &mut Frame, _app: &mut App, area: Rect) { +fn draw_third_tab(frame: &mut Frame, _app: &mut App, area: Rect) { let chunks = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).split(area); let colors = [ Color::Reset, @@ -396,5 +396,5 @@ fn draw_third_tab(f: &mut Frame, _app: &mut App, area: Rect) { ], ) .block(Block::bordered().title("Colors")); - f.render_widget(table, chunks[0]); + frame.render_widget(table, chunks[0]); } diff --git a/examples/demo2/app.rs b/examples/demo2/app.rs index 99f4ec32..fc01e595 100644 --- a/examples/demo2/app.rs +++ b/examples/demo2/app.rs @@ -1,23 +1,23 @@ use std::time::Duration; use color_eyre::{eyre::Context, Result}; +use crossterm::event; use itertools::Itertools; use ratatui::{ - backend::Backend, buffer::Buffer, crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind}, layout::{Constraint, Layout, Rect}, style::Color, text::{Line, Span}, widgets::{Block, Tabs, Widget}, - Terminal, + DefaultTerminal, Frame, }; use strum::{Display, EnumIter, FromRepr, IntoEnumIterator}; use crate::{ destroy, tabs::{AboutTab, EmailTab, RecipeTab, TracerouteTab, WeatherTab}, - term, THEME, + THEME, }; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] @@ -49,15 +49,13 @@ enum Tab { Weather, } -pub fn run(terminal: &mut Terminal) -> Result<()> { - App::default().run(terminal) -} - impl App { /// Run the app until the user quits. - pub fn run(&mut self, terminal: &mut Terminal) -> Result<()> { + pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { while self.is_running() { - self.draw(terminal)?; + terminal + .draw(|frame| self.draw(frame)) + .wrap_err("terminal.draw")?; self.handle_events()?; } Ok(()) @@ -68,16 +66,11 @@ impl App { } /// Draw a single frame of the app. - fn draw(&self, terminal: &mut Terminal) -> Result<()> { - terminal - .draw(|frame| { - frame.render_widget(self, frame.area()); - if self.mode == Mode::Destroy { - destroy::destroy(frame); - } - }) - .wrap_err("terminal.draw")?; - Ok(()) + fn draw(&self, frame: &mut Frame) { + frame.render_widget(self, frame.area()); + if self.mode == Mode::Destroy { + destroy::destroy(frame); + } } /// Handle events from the terminal. @@ -86,8 +79,11 @@ impl App { /// 1/50th of a second. This was chosen to try to match the default frame rate of a GIF in VHS. fn handle_events(&mut self) -> Result<()> { let timeout = Duration::from_secs_f64(1.0 / 50.0); - match term::next_event(timeout)? { - Some(Event::Key(key)) if key.kind == KeyEventKind::Press => self.handle_key_press(key), + if !event::poll(timeout)? { + return Ok(()); + } + match event::read()? { + Event::Key(key) if key.kind == KeyEventKind::Press => self.handle_key_press(key), _ => {} } Ok(()) diff --git a/examples/demo2/errors.rs b/examples/demo2/errors.rs deleted file mode 100644 index d868e38a..00000000 --- a/examples/demo2/errors.rs +++ /dev/null @@ -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(()) -} diff --git a/examples/demo2/main.rs b/examples/demo2/main.rs index 1a3e9655..63506b1b 100644 --- a/examples/demo2/main.rs +++ b/examples/demo2/main.rs @@ -22,12 +22,18 @@ mod app; mod colors; mod destroy; -mod errors; mod tabs; -mod term; mod theme; +use std::io::stdout; + +use app::App; use color_eyre::Result; +use crossterm::{ + execute, + terminal::{EnterAlternateScreen, LeaveAlternateScreen}, +}; +use ratatui::{layout::Rect, TerminalOptions, Viewport}; pub use self::{ colors::{color_from_oklab, RgbSwatch}, @@ -35,9 +41,14 @@ pub use self::{ }; fn main() -> Result<()> { - errors::init_hooks()?; - let terminal = &mut term::init()?; - app::run(terminal)?; - term::restore()?; - Ok(()) + color_eyre::install()?; + // this size is to match the size of the terminal when running the demo + // using vhs in a 1280x640 sized window (github social preview size) + let viewport = Viewport::Fixed(Rect::new(0, 0, 81, 18)); + let terminal = ratatui::init_with_options(TerminalOptions { viewport }); + execute!(stdout(), EnterAlternateScreen).expect("failed to enter alternate screen"); + let app_result = App::default().run(terminal); + execute!(stdout(), LeaveAlternateScreen).expect("failed to leave alternate screen"); + ratatui::restore(); + app_result } diff --git a/examples/demo2/term.rs b/examples/demo2/term.rs deleted file mode 100644 index c9c54a9d..00000000 --- a/examples/demo2/term.rs +++ /dev/null @@ -1,46 +0,0 @@ -use std::{ - io::{self, stdout}, - time::Duration, -}; - -use color_eyre::{eyre::WrapErr, Result}; -use ratatui::{ - backend::{Backend, CrosstermBackend}, - crossterm::{ - event::{self, Event}, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - ExecutableCommand, - }, - layout::Rect, - Terminal, TerminalOptions, Viewport, -}; - -pub fn init() -> Result> { - // this size is to match the size of the terminal when running the demo - // using vhs in a 1280x640 sized window (github social preview size) - let options = TerminalOptions { - viewport: Viewport::Fixed(Rect::new(0, 0, 81, 18)), - }; - let terminal = Terminal::with_options(CrosstermBackend::new(io::stdout()), options)?; - enable_raw_mode().context("enable raw mode")?; - stdout() - .execute(EnterAlternateScreen) - .wrap_err("enter alternate screen")?; - Ok(terminal) -} - -pub fn restore() -> Result<()> { - disable_raw_mode().context("disable raw mode")?; - stdout() - .execute(LeaveAlternateScreen) - .wrap_err("leave alternate screen")?; - Ok(()) -} - -pub fn next_event(timeout: Duration) -> Result> { - if !event::poll(timeout)? { - return Ok(None); - } - let event = event::read()?; - Ok(Some(event)) -} diff --git a/examples/docsrs.rs b/examples/docsrs.rs index 38642646..460d915c 100644 --- a/examples/docsrs.rs +++ b/examples/docsrs.rs @@ -13,47 +13,51 @@ //! [examples]: https://github.com/ratatui/ratatui/blob/main/examples //! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md -use std::io::{self, stdout}; - +use color_eyre::Result; use ratatui::{ - backend::CrosstermBackend, - crossterm::{ - event::{self, Event, KeyCode}, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - ExecutableCommand, - }, + crossterm::event::{self, Event, KeyCode}, layout::{Constraint, Layout}, style::{Color, Modifier, Style, Stylize}, text::{Line, Span, Text}, widgets::{Block, Borders, Paragraph}, - Frame, Terminal, + DefaultTerminal, Frame, }; /// Example code for lib.rs /// /// When cargo-rdme supports doc comments that import from code, this will be imported /// rather than copied to the lib.rs file. -fn main() -> io::Result<()> { - let arg = std::env::args().nth(1).unwrap_or_default(); - enable_raw_mode()?; - stdout().execute(EnterAlternateScreen)?; - let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?; +fn main() -> Result<()> { + color_eyre::install()?; + let first_arg = std::env::args().nth(1).unwrap_or_default(); + let terminal = ratatui::init(); + let app_result = run(terminal, &first_arg); + ratatui::restore(); + app_result +} +fn run(mut terminal: DefaultTerminal, first_arg: &str) -> Result<()> { let mut should_quit = false; while !should_quit { - terminal.draw(match arg.as_str() { + terminal.draw(match first_arg { "layout" => layout, "styling" => styling, _ => hello_world, })?; should_quit = handle_events()?; } - - disable_raw_mode()?; - stdout().execute(LeaveAlternateScreen)?; Ok(()) } +fn handle_events() -> std::io::Result { + if let Event::Key(key) = event::read()? { + if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') { + return Ok(true); + } + } + Ok(false) +} + fn hello_world(frame: &mut Frame) { frame.render_widget( Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")), @@ -61,17 +65,6 @@ fn hello_world(frame: &mut Frame) { ); } -fn handle_events() -> io::Result { - if event::poll(std::time::Duration::from_millis(50))? { - if let Event::Key(key) = event::read()? { - if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') { - return Ok(true); - } - } - } - Ok(false) -} - fn layout(frame: &mut Frame) { let vertical = Layout::vertical([ Constraint::Length(1), diff --git a/examples/flex.rs b/examples/flex.rs index ff3a3cc3..bfa2dcc9 100644 --- a/examples/flex.rs +++ b/examples/flex.rs @@ -13,20 +13,12 @@ //! [examples]: https://github.com/ratatui/ratatui/blob/main/examples //! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md -use std::{ - io::{self, stdout}, - num::NonZeroUsize, -}; +use std::num::NonZeroUsize; -use color_eyre::{config::HookBuilder, Result}; +use color_eyre::Result; use ratatui::{ - backend::{Backend, CrosstermBackend}, buffer::Buffer, - crossterm::{ - event::{self, Event, KeyCode, KeyEventKind}, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - ExecutableCommand, - }, + crossterm::event::{self, Event, KeyCode, KeyEventKind}, layout::{ Alignment, Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio}, @@ -39,10 +31,18 @@ use ratatui::{ block::Title, Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Tabs, Widget, }, - Terminal, + DefaultTerminal, }; use strum::{Display, EnumIter, FromRepr, IntoEnumIterator}; +fn main() -> Result<()> { + color_eyre::install()?; + let terminal = ratatui::init(); + let app_result = App::default().run(terminal); + ratatui::restore(); + app_result +} + const EXAMPLE_DATA: &[(&str, &[Constraint])] = &[ ( "Min(u16) takes any excess space always", @@ -157,25 +157,18 @@ enum SelectedTab { SpaceBetween, } -fn main() -> Result<()> { - // assuming the user changes spacing about a 100 times or so - Layout::init_cache( - NonZeroUsize::new(EXAMPLE_DATA.len() * SelectedTab::iter().len() * 100).unwrap(), - ); - init_error_hooks()?; - let terminal = init_terminal()?; - App::default().run(terminal)?; - - restore_terminal()?; - Ok(()) -} - impl App { - fn run(&mut self, mut terminal: Terminal) -> Result<()> { - self.draw(&mut terminal)?; + fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { + // increase the layout cache to account for the number of layout events. This ensures that + // layout is not generally reprocessed on every frame (which would lead to possible janky + // results when there are more than one possible solution to the requested layout). This + // assumes the user changes spacing about a 100 times or so. + let cache_size = EXAMPLE_DATA.len() * SelectedTab::iter().len() * 100; + Layout::init_cache(NonZeroUsize::new(cache_size).unwrap()); + while self.is_running() { + terminal.draw(|frame| frame.render_widget(self, frame.area()))?; self.handle_events()?; - self.draw(&mut terminal)?; } Ok(()) } @@ -184,11 +177,6 @@ impl App { self.state == AppState::Running } - fn draw(self, terminal: &mut Terminal) -> io::Result<()> { - terminal.draw(|frame| frame.render_widget(self, frame.area()))?; - Ok(()) - } - fn handle_events(&mut self) -> Result<()> { match event::read()? { Event::Key(key) if key.kind == KeyEventKind::Press => match key.code { @@ -532,35 +520,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> { - enable_raw_mode()?; - stdout().execute(EnterAlternateScreen)?; - let backend = CrosstermBackend::new(stdout()); - let terminal = Terminal::new(backend)?; - Ok(terminal) -} - -fn restore_terminal() -> Result<()> { - disable_raw_mode()?; - stdout().execute(LeaveAlternateScreen)?; - Ok(()) -} - #[allow(clippy::cast_possible_truncation)] fn get_description_height(s: &str) -> u16 { if s.is_empty() { diff --git a/examples/gauge.rs b/examples/gauge.rs index 0c09f53a..f72916cb 100644 --- a/examples/gauge.rs +++ b/examples/gauge.rs @@ -13,22 +13,17 @@ //! [examples]: https://github.com/ratatui/ratatui/blob/main/examples //! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md -use std::{io::stdout, time::Duration}; +use std::time::Duration; -use color_eyre::{config::HookBuilder, Result}; +use color_eyre::Result; use ratatui::{ - backend::{Backend, CrosstermBackend}, buffer::Buffer, - crossterm::{ - event::{self, Event, KeyCode, KeyEventKind}, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - ExecutableCommand, - }, + crossterm::event::{self, Event, KeyCode, KeyEventKind}, layout::{Alignment, Constraint, Layout, Rect}, style::{palette::tailwind, Color, Style, Stylize}, text::Span, widgets::{block::Title, Block, Borders, Gauge, Padding, Paragraph, Widget}, - Terminal, + DefaultTerminal, }; const GAUGE1_COLOR: Color = tailwind::RED.c800; @@ -56,28 +51,23 @@ enum AppState { } fn main() -> Result<()> { - init_error_hooks()?; - let terminal = init_terminal()?; - App::default().run(terminal)?; - restore_terminal()?; - Ok(()) + color_eyre::install()?; + let terminal = ratatui::init(); + let app_result = App::default().run(terminal); + ratatui::restore(); + app_result } impl App { - fn run(&mut self, mut terminal: Terminal) -> Result<()> { + fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { while self.state != AppState::Quitting { - self.draw(&mut terminal)?; + terminal.draw(|frame| frame.render_widget(&self, frame.area()))?; self.handle_events()?; self.update(terminal.size()?.width); } Ok(()) } - fn draw(&self, terminal: &mut Terminal) -> Result<()> { - terminal.draw(|f| f.render_widget(self, f.area()))?; - Ok(()) - } - fn update(&mut self, terminal_width: u16) { if self.state != AppState::Started { return; @@ -213,32 +203,3 @@ fn title_block(title: &str) -> Block { .title(title) .fg(CUSTOM_LABEL_COLOR) } - -fn init_error_hooks() -> color_eyre::Result<()> { - let (panic, error) = HookBuilder::default().into_hooks(); - let panic = panic.into_panic_hook(); - let error = error.into_eyre_hook(); - color_eyre::eyre::set_hook(Box::new(move |e| { - let _ = restore_terminal(); - error(e) - }))?; - std::panic::set_hook(Box::new(move |info| { - let _ = restore_terminal(); - panic(info); - })); - Ok(()) -} - -fn init_terminal() -> color_eyre::Result> { - 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(()) -} diff --git a/examples/hello_world.rs b/examples/hello_world.rs index ca68a619..9e814620 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -13,21 +13,13 @@ //! [examples]: https://github.com/ratatui/ratatui/blob/main/examples //! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md -use std::{ - io::{self, Stdout}, - time::Duration, -}; +use std::time::Duration; use color_eyre::{eyre::Context, Result}; use ratatui::{ - backend::CrosstermBackend, - crossterm::{ - event::{self, Event, KeyCode}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - }, + crossterm::event::{self, Event, KeyCode}, widgets::Paragraph, - Frame, Terminal, + DefaultTerminal, Frame, }; /// This is a bare minimum example. There are many approaches to running an application loop, so @@ -38,50 +30,19 @@ use ratatui::{ /// and exits when the user presses 'q'. fn main() -> Result<()> { color_eyre::install()?; // augment errors / panics with easy to read messages - let mut terminal = init_terminal().context("setup failed")?; - let result = run(&mut terminal).context("app loop failed"); - restore_terminal(); - result -} - -/// Setup the terminal. This is where you would enable raw mode, enter the alternate screen, and -/// hide the cursor. This example does not handle errors. -fn init_terminal() -> Result>> { - set_panic_hook(); - 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() { - // There's not a lot we can do if these fail, so we just print an error message. - if let Err(err) = disable_raw_mode() { - eprintln!("Error disabling raw mode: {err}"); - } - if let Err(err) = execute!(io::stdout(), LeaveAlternateScreen) { - eprintln!("Error leaving alternate screen: {err}"); - } -} - -/// Replace the default panic hook with one that restores the terminal before panicking. -fn set_panic_hook() { - let hook = std::panic::take_hook(); - std::panic::set_hook(Box::new(move |panic_info| { - restore_terminal(); - hook(panic_info); - })); + let terminal = ratatui::init(); + let app_result = run(terminal).context("app loop failed"); + ratatui::restore(); + app_result } /// Run the application loop. This is where you would handle events and update the application /// state. This example exits when the user presses 'q'. Other styles of application loops are /// possible, for example, you could have multiple application states and switch between them based /// on events, or you could have a single application state and update it based on events. -fn run(terminal: &mut Terminal>) -> Result<()> { +fn run(mut terminal: DefaultTerminal) -> Result<()> { loop { - terminal.draw(render_app)?; + terminal.draw(draw)?; if should_quit()? { break; } @@ -89,19 +50,18 @@ fn run(terminal: &mut Terminal>) -> Result<()> { 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) { +/// Render the application. This is where you would draw the application UI. This example draws a +/// greeting. +fn draw(frame: &mut Frame) { let greeting = Paragraph::new("Hello World! (press 'q' to quit)"); frame.render_widget(greeting, frame.area()); } /// 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. This allows you -/// to do other work in the application loop, such as updating the application state, without -/// blocking the event loop for too long. +/// events. There is a 250ms timeout on the event poll to ensure that the terminal is rendered at +/// least once every 250ms. This allows you to do other work in the application loop, such as +/// updating the application state, without blocking the event loop for too long. fn should_quit() -> Result { if event::poll(Duration::from_millis(250)).context("event poll failed")? { if let Event::Key(key) = event::read().context("event read failed")? { diff --git a/examples/hyperlink.rs b/examples/hyperlink.rs index 36bf401a..e093f8da 100644 --- a/examples/hyperlink.rs +++ b/examples/hyperlink.rs @@ -16,39 +16,24 @@ //! [examples]: https://github.com/ratatui/ratatui/blob/main/examples //! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md -use std::{ - io::{self, stdout, Stdout}, - panic, -}; - -use color_eyre::{ - config::{EyreHook, HookBuilder, PanicHook}, - eyre, Result, -}; +use color_eyre::Result; use itertools::Itertools; use ratatui::{ - backend::CrosstermBackend, buffer::Buffer, - crossterm::{ - event::{self, Event, KeyCode}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - ExecutableCommand, - }, + crossterm::event::{self, Event, KeyCode}, layout::Rect, style::Stylize, text::{Line, Text}, - widgets::WidgetRef, - Terminal, + widgets::Widget, + DefaultTerminal, }; fn main() -> Result<()> { - init_error_handling()?; - let mut terminal = init_terminal()?; - let app = App::new(); - app.run(&mut terminal)?; - restore_terminal()?; - Ok(()) + color_eyre::install()?; + let terminal = ratatui::init(); + let app_result = App::new().run(terminal); + ratatui::restore(); + app_result } struct App { @@ -62,7 +47,7 @@ impl App { Self { hyperlink } } - fn run(self, terminal: &mut Terminal>) -> io::Result<()> { + fn run(self, mut terminal: DefaultTerminal) -> Result<()> { loop { terminal.draw(|frame| frame.render_widget(&self.hyperlink, frame.area()))?; if let Event::Key(key) = event::read()? { @@ -92,9 +77,9 @@ impl<'content> Hyperlink<'content> { } } -impl WidgetRef for Hyperlink<'_> { - fn render_ref(&self, area: Rect, buffer: &mut Buffer) { - self.text.render_ref(area, buffer); +impl Widget for &Hyperlink<'_> { + fn render(self, area: Rect, buffer: &mut Buffer) { + (&self.text).render(area, buffer); // this is a hacky workaround for https://github.com/ratatui/ratatui/issues/902, a bug // in the terminal code that incorrectly calculates the width of ANSI escape sequences. It @@ -114,44 +99,3 @@ impl WidgetRef for Hyperlink<'_> { } } } - -/// Initialize the terminal with raw mode and alternate screen. -fn init_terminal() -> io::Result>> { - enable_raw_mode()?; - stdout().execute(EnterAlternateScreen)?; - Terminal::new(CrosstermBackend::new(stdout())) -} - -/// Restore the terminal to its original state. -fn restore_terminal() -> io::Result<()> { - disable_raw_mode()?; - execute!(stdout(), LeaveAlternateScreen)?; - Ok(()) -} - -/// Initialize error handling with color-eyre. -pub fn init_error_handling() -> Result<()> { - let (panic_hook, eyre_hook) = HookBuilder::default().into_hooks(); - set_panic_hook(panic_hook); - set_error_hook(eyre_hook)?; - Ok(()) -} - -/// Install a panic hook that restores the terminal before printing the panic. -fn set_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 set_error_hook(eyre_hook: EyreHook) -> Result<()> { - let eyre_hook = eyre_hook.into_eyre_hook(); - eyre::set_hook(Box::new(move |error| { - let _ = restore_terminal(); - eyre_hook(error) - }))?; - Ok(()) -} diff --git a/examples/inline.rs b/examples/inline.rs index d5786528..5e21e980 100644 --- a/examples/inline.rs +++ b/examples/inline.rs @@ -15,20 +15,16 @@ use std::{ collections::{BTreeMap, VecDeque}, - error::Error, - io, sync::mpsc, thread, time::{Duration, Instant}, }; +use color_eyre::Result; use rand::distributions::{Distribution, Uniform}; use ratatui::{ - backend::{Backend, CrosstermBackend}, - crossterm::{ - event, - terminal::{disable_raw_mode, enable_raw_mode}, - }, + backend::Backend, + crossterm::event, layout::{Alignment, Constraint, Layout, Rect}, style::{Color, Modifier, Style}, symbols, @@ -37,11 +33,33 @@ use ratatui::{ Frame, Terminal, TerminalOptions, Viewport, }; +fn main() -> Result<()> { + color_eyre::install()?; + let mut terminal = ratatui::init_with_options(TerminalOptions { + viewport: Viewport::Inline(8), + }); + + let (tx, rx) = mpsc::channel(); + input_handling(tx.clone()); + let workers = workers(tx); + let mut downloads = downloads(); + + for w in &workers { + let d = downloads.next(w.id).unwrap(); + w.tx.send(d).unwrap(); + } + + let app_result = run(&mut terminal, workers, downloads, rx); + + ratatui::restore(); + + app_result +} + const NUM_DOWNLOADS: usize = 10; type DownloadId = usize; type WorkerId = usize; - enum Event { Input(event::KeyEvent), Tick, @@ -49,7 +67,6 @@ enum Event { DownloadUpdate(WorkerId, DownloadId, f64), DownloadDone(WorkerId, DownloadId), } - struct Downloads { pending: VecDeque, in_progress: BTreeMap, @@ -73,52 +90,20 @@ impl Downloads { } } } - struct DownloadInProgress { id: DownloadId, started_at: Instant, progress: f64, } - struct Download { id: DownloadId, size: usize, } - struct Worker { id: WorkerId, tx: mpsc::Sender, } -fn main() -> Result<(), Box> { - enable_raw_mode()?; - let stdout = io::stdout(); - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::with_options( - backend, - TerminalOptions { - viewport: Viewport::Inline(8), - }, - )?; - - let (tx, rx) = mpsc::channel(); - input_handling(tx.clone()); - let workers = workers(tx); - let mut downloads = downloads(); - - for w in &workers { - let d = downloads.next(w.id).unwrap(); - w.tx.send(d).unwrap(); - } - - run_app(&mut terminal, workers, downloads, rx)?; - - disable_raw_mode()?; - terminal.clear()?; - - Ok(()) -} - fn input_handling(tx: mpsc::Sender) { let tick_rate = Duration::from_millis(200); thread::spawn(move || { @@ -182,16 +167,16 @@ fn downloads() -> Downloads { } #[allow(clippy::needless_pass_by_value)] -fn run_app( - terminal: &mut Terminal, +fn run( + terminal: &mut Terminal, workers: Vec, mut downloads: Downloads, rx: mpsc::Receiver, -) -> Result<(), Box> { +) -> Result<()> { let mut redraw = true; loop { if redraw { - terminal.draw(|f| ui(f, &downloads))?; + terminal.draw(|frame| draw(frame, &downloads))?; } redraw = true; @@ -243,11 +228,11 @@ fn run_app( Ok(()) } -fn ui(f: &mut Frame, downloads: &Downloads) { - let area = f.area(); +fn draw(frame: &mut Frame, downloads: &Downloads) { + let area = frame.area(); let block = Block::new().title(block::Title::from("Progress").alignment(Alignment::Center)); - f.render_widget(block, area); + frame.render_widget(block, area); let vertical = Layout::vertical([Constraint::Length(2), Constraint::Length(4)]).margin(1); let horizontal = Layout::horizontal([Constraint::Percentage(20), Constraint::Percentage(80)]); @@ -261,7 +246,7 @@ fn ui(f: &mut Frame, downloads: &Downloads) { .filled_style(Style::default().fg(Color::Blue)) .label(format!("{done}/{NUM_DOWNLOADS}")) .ratio(done as f64 / NUM_DOWNLOADS as f64); - f.render_widget(progress, progress_area); + frame.render_widget(progress, progress_area); // in progress downloads let items: Vec = downloads @@ -284,7 +269,7 @@ fn ui(f: &mut Frame, downloads: &Downloads) { }) .collect(); let list = List::new(items); - f.render_widget(list, list_area); + frame.render_widget(list, list_area); #[allow(clippy::cast_possible_truncation)] for (i, (_, download)) in downloads.in_progress.iter().enumerate() { @@ -294,7 +279,7 @@ fn ui(f: &mut Frame, downloads: &Downloads) { if gauge_area.top().saturating_add(i as u16) > area.bottom() { continue; } - f.render_widget( + frame.render_widget( gauge, Rect { x: gauge_area.left(), diff --git a/examples/layout.rs b/examples/layout.rs index d40338fd..f7ae8842 100644 --- a/examples/layout.rs +++ b/examples/layout.rs @@ -13,68 +13,40 @@ //! [examples]: https://github.com/ratatui/ratatui/blob/main/examples //! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md -use std::{error::Error, io}; - use itertools::Itertools; use ratatui::{ - backend::{Backend, CrosstermBackend}, - crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - }, + crossterm::event::{self, Event, KeyCode, KeyEventKind}, layout::{ - Constraint, - Constraint::{Length, Max, Min, Percentage, Ratio}, + Constraint::{self, Length, Max, Min, Percentage, Ratio}, Layout, Rect, }, style::{Color, Style, Stylize}, text::Line, widgets::{Block, Paragraph}, - Frame, Terminal, + DefaultTerminal, Frame, }; -fn main() -> Result<(), Box> { - // setup terminal - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - - // create app and run it - let res = run_app(&mut terminal); - - // restore terminal - disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; - terminal.show_cursor()?; - - if let Err(err) = res { - println!("{err:?}"); - } - - Ok(()) +fn main() -> color_eyre::Result<()> { + color_eyre::install()?; + let terminal = ratatui::init(); + let app_result = run(terminal); + ratatui::restore(); + app_result } -fn run_app(terminal: &mut Terminal) -> io::Result<()> { +fn run(mut terminal: DefaultTerminal) -> color_eyre::Result<()> { loop { - terminal.draw(ui)?; - + terminal.draw(draw)?; if let Event::Key(key) = event::read()? { - if key.code == KeyCode::Char('q') { - return Ok(()); + if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { + break Ok(()); } } } } #[allow(clippy::too_many_lines)] -fn ui(frame: &mut Frame) { +fn draw(frame: &mut Frame) { let vertical = Layout::vertical([ Length(4), // text Length(50), // examples diff --git a/examples/line_gauge.rs b/examples/line_gauge.rs index ca3a4b59..ee40696f 100644 --- a/examples/line_gauge.rs +++ b/examples/line_gauge.rs @@ -13,25 +13,28 @@ //! [examples]: https://github.com/ratatui/ratatui/blob/main/examples //! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md -use std::{io::stdout, time::Duration}; +use std::time::Duration; -use color_eyre::{config::HookBuilder, Result}; +use color_eyre::Result; use ratatui::{ - backend::{Backend, CrosstermBackend}, buffer::Buffer, - crossterm::{ - event::{self, Event, KeyCode, KeyEventKind}, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - ExecutableCommand, - }, + crossterm::event::{self, Event, KeyCode, KeyEventKind}, layout::{Alignment, Constraint, Layout, Rect}, style::{palette::tailwind, Color, Style, Stylize}, widgets::{block::Title, Block, Borders, LineGauge, Padding, Paragraph, Widget}, - Terminal, + DefaultTerminal, }; const CUSTOM_LABEL_COLOR: Color = tailwind::SLATE.c200; +fn main() -> Result<()> { + color_eyre::install()?; + let terminal = ratatui::init(); + let app_result = App::default().run(terminal); + ratatui::restore(); + app_result +} + #[derive(Debug, Default, Clone, Copy)] struct App { state: AppState, @@ -47,29 +50,16 @@ enum AppState { Quitting, } -fn main() -> Result<()> { - init_error_hooks()?; - let terminal = init_terminal()?; - App::default().run(terminal)?; - restore_terminal()?; - Ok(()) -} - impl App { - fn run(&mut self, mut terminal: Terminal) -> Result<()> { + fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { while self.state != AppState::Quitting { - self.draw(&mut terminal)?; + terminal.draw(|frame| frame.render_widget(&self, frame.area()))?; self.handle_events()?; self.update(terminal.size()?.width); } Ok(()) } - fn draw(&self, terminal: &mut Terminal) -> Result<()> { - terminal.draw(|f| f.render_widget(self, f.area()))?; - Ok(()) - } - fn update(&mut self, terminal_width: u16) { if self.state != AppState::Started { return; @@ -187,32 +177,3 @@ fn title_block(title: &str) -> Block { .fg(CUSTOM_LABEL_COLOR) .padding(Padding::vertical(1)) } - -fn init_error_hooks() -> color_eyre::Result<()> { - let (panic, error) = HookBuilder::default().into_hooks(); - let panic = panic.into_panic_hook(); - let error = error.into_eyre_hook(); - color_eyre::eyre::set_hook(Box::new(move |e| { - let _ = restore_terminal(); - error(e) - }))?; - std::panic::set_hook(Box::new(move |info| { - let _ = restore_terminal(); - panic(info); - })); - Ok(()) -} - -fn init_terminal() -> color_eyre::Result> { - 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(()) -} diff --git a/examples/list.rs b/examples/list.rs index e29903f9..d74a6c8f 100644 --- a/examples/list.rs +++ b/examples/list.rs @@ -13,10 +13,8 @@ //! [examples]: https://github.com/ratatui/ratatui/blob/main/examples //! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md -use std::{error::Error, io}; - +use color_eyre::Result; use ratatui::{ - backend::Backend, buffer::Buffer, crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind}, layout::{Constraint, Layout, Rect}, @@ -30,7 +28,7 @@ use ratatui::{ Block, Borders, HighlightSpacing, List, ListItem, ListState, Padding, Paragraph, StatefulWidget, Widget, Wrap, }, - Terminal, + DefaultTerminal, }; const TODO_HEADER_STYLE: Style = Style::new().fg(SLATE.c100).bg(BLUE.c800); @@ -40,15 +38,12 @@ const SELECTED_STYLE: Style = Style::new().bg(SLATE.c800).add_modifier(Modifier: const TEXT_FG_COLOR: Color = SLATE.c200; const COMPLETED_TEXT_FG_COLOR: Color = GREEN.c500; -fn main() -> Result<(), Box> { - tui::init_error_hooks()?; - let terminal = tui::init_terminal()?; - - let mut app = App::default(); - app.run(terminal)?; - - tui::restore_terminal()?; - Ok(()) +fn main() -> Result<()> { + color_eyre::install()?; + let terminal = ratatui::init(); + let app_result = App::default().run(terminal); + ratatui::restore(); + app_result } /// This struct holds the current state of the app. In particular, it has the `todo_list` field @@ -118,9 +113,9 @@ impl TodoItem { } impl App { - fn run(&mut self, mut terminal: Terminal) -> io::Result<()> { + fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { while !self.should_exit { - terminal.draw(|f| f.render_widget(&mut *self, f.area()))?; + terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?; if let Event::Key(key) = event::read()? { self.handle_key(key); }; @@ -290,45 +285,3 @@ impl From<&TodoItem> for ListItem<'_> { ListItem::new(line) } } - -mod tui { - use std::{io, io::stdout}; - - use color_eyre::config::HookBuilder; - use ratatui::{ - backend::{Backend, CrosstermBackend}, - crossterm::{ - terminal::{ - disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, - }, - ExecutableCommand, - }, - Terminal, - }; - - pub 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(()) - } - - pub fn init_terminal() -> io::Result> { - stdout().execute(EnterAlternateScreen)?; - enable_raw_mode()?; - Terminal::new(CrosstermBackend::new(stdout())) - } - - pub fn restore_terminal() -> io::Result<()> { - stdout().execute(LeaveAlternateScreen)?; - disable_raw_mode() - } -} diff --git a/examples/minimal.rs b/examples/minimal.rs index 0f3e527b..7e88e4ba 100644 --- a/examples/minimal.rs +++ b/examples/minimal.rs @@ -1,5 +1,12 @@ //! # [Ratatui] Minimal example //! +//! This is a bare minimum example. There are many approaches to running an application loop, so +//! this is not meant to be prescriptive. See the [examples] folder for more complete examples. +//! In particular, the [hello-world] example is a good starting point. +//! +//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples +//! [hello-world]: https://github.com/ratatui-org/ratatui/blob/main/examples/hello_world.rs +//! //! The latest version of this example is available in the [examples] folder in the repository. //! //! Please note that the examples are designed to be run against the `main` branch of the Github @@ -14,35 +21,20 @@ //! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md use ratatui::{ - backend::CrosstermBackend, - crossterm::{ - event::{self, Event, KeyCode, KeyEventKind}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - }, + crossterm::event::{self, Event}, text::Text, - Terminal, + Frame, }; -/// This is a bare minimum example. There are many approaches to running an application loop, so -/// this is not meant to be prescriptive. See the [examples] folder for more complete examples. -/// In particular, the [hello-world] example is a good starting point. -/// -/// [examples]: https://github.com/ratatui/ratatui/blob/main/examples -/// [hello-world]: https://github.com/ratatui/ratatui/blob/main/examples/hello_world.rs -fn main() -> Result<(), Box> { - let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stdout()))?; - enable_raw_mode()?; - execute!(terminal.backend_mut(), EnterAlternateScreen)?; +fn main() { + let mut terminal = ratatui::init(); loop { - terminal.draw(|frame| frame.render_widget(Text::raw("Hello World!"), frame.area()))?; - if let Event::Key(key) = event::read()? { - if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { - break; - } + terminal + .draw(|frame: &mut Frame| frame.render_widget(Text::raw("Hello World!"), frame.area())) + .expect("Failed to draw"); + if matches!(event::read().expect("failed to read event"), Event::Key(_)) { + break; } } - disable_raw_mode()?; - execute!(terminal.backend_mut(), LeaveAlternateScreen)?; - Ok(()) + ratatui::restore(); } diff --git a/examples/modifiers.rs b/examples/modifiers.rs index bb058228..74555ef1 100644 --- a/examples/modifiers.rs +++ b/examples/modifiers.rs @@ -17,56 +17,40 @@ // It will render a grid of combinations of foreground and background colors with all // modifiers applied to them. -use std::{ - error::Error, - io::{self, Stdout}, - iter::once, - result, - time::Duration, -}; +use std::{error::Error, iter::once, result}; use itertools::Itertools; use ratatui::{ - backend::{Backend, CrosstermBackend}, - crossterm::{ - event::{self, Event, KeyCode}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - }, + crossterm::event::{self, Event, KeyCode, KeyEventKind}, layout::{Constraint, Layout}, style::{Color, Modifier, Style, Stylize}, text::Line, widgets::Paragraph, - Frame, Terminal, + DefaultTerminal, Frame, }; type Result = result::Result>; fn main() -> Result<()> { - let mut terminal = setup_terminal()?; - let res = run_app(&mut terminal); - restore_terminal(terminal)?; - if let Err(err) = res { - eprintln!("{err:?}"); - } - Ok(()) + color_eyre::install()?; + let terminal = ratatui::init(); + let app_result = run(terminal); + ratatui::restore(); + app_result } -fn run_app(terminal: &mut Terminal) -> io::Result<()> { +fn run(mut terminal: DefaultTerminal) -> Result<()> { loop { - terminal.draw(ui)?; - - if event::poll(Duration::from_millis(250))? { - if let Event::Key(key) = event::read()? { - if key.code == KeyCode::Char('q') { - return Ok(()); - } + terminal.draw(draw)?; + if let Event::Key(key) = event::read()? { + if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { + return Ok(()); } } } } -fn ui(frame: &mut Frame) { +fn draw(frame: &mut Frame) { let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]); let [text_area, main_area] = vertical.areas(frame.area()); frame.render_widget( @@ -114,20 +98,3 @@ fn ui(frame: &mut Frame) { } } } - -fn setup_terminal() -> Result>> { - 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>) -> Result<()> { - disable_raw_mode()?; - execute!(terminal.backend_mut(), LeaveAlternateScreen)?; - terminal.show_cursor()?; - Ok(()) -} diff --git a/examples/panic.rs b/examples/panic.rs index 628b2805..a0f91501 100644 --- a/examples/panic.rs +++ b/examples/panic.rs @@ -12,142 +12,99 @@ //! [Ratatui]: https://github.com/ratatui/ratatui //! [examples]: https://github.com/ratatui/ratatui/blob/main/examples //! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md - -//! How to use a panic hook to reset the terminal before printing the panic to -//! the terminal. //! -//! When exiting normally or when handling `Result::Err`, we can reset the -//! terminal manually at the end of `main` just before we print the error. +//! Prior to Ratatui 0.28.1, a panic hook had to be manually set up to ensure that the terminal was +//! reset when a panic occurred. This was necessary because a panic would interrupt the normal +//! control flow and leave the terminal in a distorted state. //! -//! Because a panic interrupts the normal control flow, manually resetting the -//! terminal at the end of `main` won't do us any good. Instead, we need to -//! make sure to set up a panic hook that first resets the terminal before -//! handling the panic. This both reuses the standard panic hook to ensure a -//! consistent panic handling UX and properly resets the terminal to not -//! distort the output. +//! Starting with Ratatui 0.28.1, the panic hook is automatically set up by the new `ratatui::init` +//! function, so you no longer need to manually set up the panic hook. This example now demonstrates +//! how the panic hook acts when it is enabled by default. //! -//! That's why this example is set up to show both situations, with and without -//! the chained panic hook, to see the difference. - -use std::{error::Error, io}; +//! When exiting normally or when handling `Result::Err`, we can reset the terminal manually at the +//! end of `main` just before we print the error. +//! +//! Because a panic interrupts the normal control flow, manually resetting the terminal at the end +//! of `main` won't do us any good. Instead, we need to make sure to set up a panic hook that first +//! resets the terminal before handling the panic. This both reuses the standard panic hook to +//! ensure a consistent panic handling UX and properly resets the terminal to not distort the +//! output. +//! +//! That's why this example is set up to show both situations, with and without the panic hook, to +//! see the difference. +//! +//! For more information on how to set this up manually, see the [Color Eyre recipe] in the Ratatui +//! website. +//! +//! [Color Eyre recipe]: https://ratatui.rs/recipes/apps/color-eyre +use color_eyre::{eyre::bail, Result}; use ratatui::{ - backend::{Backend, CrosstermBackend}, - crossterm::{ - event::{self, Event, KeyCode}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - }, + crossterm::event::{self, Event, KeyCode}, text::Line, widgets::{Block, Paragraph}, - Frame, Terminal, + DefaultTerminal, Frame, }; -type Result = std::result::Result>; - -#[derive(Default)] +fn main() -> Result<()> { + color_eyre::install()?; + let terminal = ratatui::init(); + let app_result = App::new().run(terminal); + ratatui::restore(); + app_result +} struct App { hook_enabled: bool, } impl App { - fn chain_hook(&mut self) { - let original_hook = std::panic::take_hook(); - - std::panic::set_hook(Box::new(move |panic| { - reset_terminal().unwrap(); - original_hook(panic); - })); - - self.hook_enabled = true; - } -} - -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:?}"); + const fn new() -> Self { + Self { hook_enabled: true } } - Ok(()) -} + fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { + loop { + terminal.draw(|frame| self.draw(frame))?; -/// Initializes the terminal. -fn init_terminal() -> Result>> { - execute!(io::stdout(), EnterAlternateScreen)?; - enable_raw_mode()?; - - let backend = CrosstermBackend::new(io::stdout()); - - let mut terminal = Terminal::new(backend)?; - terminal.hide_cursor()?; - - Ok(terminal) -} - -/// Resets the terminal. -fn reset_terminal() -> Result<()> { - disable_raw_mode()?; - execute!(io::stdout(), LeaveAlternateScreen)?; - - Ok(()) -} - -/// Runs the TUI loop. -fn run_tui(terminal: &mut Terminal, app: &mut App) -> io::Result<()> { - loop { - terminal.draw(|f| ui(f, app))?; - - if let Event::Key(key) = event::read()? { - match key.code { - KeyCode::Char('p') => { - panic!("intentional demo panic"); - } - - KeyCode::Char('e') => { - app.chain_hook(); - } - - _ => { - return Ok(()); + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('p') => panic!("intentional demo panic"), + KeyCode::Char('e') => bail!("intentional demo error"), + KeyCode::Char('h') => { + let _ = std::panic::take_hook(); + self.hook_enabled = false; + } + KeyCode::Char('q') => return Ok(()), + _ => {} } } } } -} - -/// Render the TUI. -fn ui(f: &mut Frame, app: &App) { - let text = vec![ - if app.hook_enabled { - Line::from("HOOK IS CURRENTLY **ENABLED**") - } else { - Line::from("HOOK IS CURRENTLY **DISABLED**") - }, - Line::from(""), - Line::from("press `p` to panic"), - Line::from("press `e` to enable the terminal-resetting panic hook"), - Line::from("press any other key to quit without panic"), - Line::from(""), - Line::from("when you panic without the chained hook,"), - Line::from("you will likely have to reset your terminal afterwards"), - Line::from("with the `reset` command"), - Line::from(""), - Line::from("with the chained panic hook enabled,"), - Line::from("you should see the panic report as you would without ratatui"), - Line::from(""), - Line::from("try first without the panic handler to see the difference"), - ]; - - let paragraph = Paragraph::new(text) - .block(Block::bordered().title("Panic Handler Demo")) - .centered(); - - f.render_widget(paragraph, f.area()); + + fn draw(&self, frame: &mut Frame) { + let text = vec![ + if self.hook_enabled { + Line::from("HOOK IS CURRENTLY **ENABLED**") + } else { + Line::from("HOOK IS CURRENTLY **DISABLED**") + }, + Line::from(""), + Line::from("Press `p` to cause a panic"), + Line::from("Press `e` to cause an error"), + Line::from("Press `h` to disable the panic hook"), + Line::from("Press `q` to quit"), + Line::from(""), + Line::from("When your app panics without a panic hook, you will likely have to"), + Line::from("reset your terminal afterwards with the `reset` command"), + Line::from(""), + Line::from("Try first with the panic handler enabled, and then with it disabled"), + Line::from("to see the difference"), + ]; + + let paragraph = Paragraph::new(text) + .block(Block::bordered().title("Panic Handler Demo")) + .centered(); + + frame.render_widget(paragraph, frame.area()); + } } diff --git a/examples/paragraph.rs b/examples/paragraph.rs index 3f6a208a..115f02d5 100644 --- a/examples/paragraph.rs +++ b/examples/paragraph.rs @@ -18,6 +18,7 @@ use std::{ time::{Duration, Instant}, }; +use color_eyre::Result; use ratatui::{ buffer::Buffer, crossterm::event::{self, Event, KeyCode, KeyEventKind}, @@ -25,17 +26,15 @@ use ratatui::{ style::{Color, Stylize}, text::{Line, Masked, Span}, widgets::{Block, Paragraph, Widget, Wrap}, + DefaultTerminal, }; -use self::common::{init_terminal, install_hooks, restore_terminal, Tui}; - -fn main() -> color_eyre::Result<()> { - install_hooks()?; - let mut terminal = init_terminal()?; - let mut app = App::new(); - app.run(&mut terminal)?; - restore_terminal()?; - Ok(()) +fn main() -> Result<()> { + color_eyre::install()?; + let terminal = ratatui::init(); + let app_result = App::new().run(terminal); + ratatui::restore(); + app_result } #[derive(Debug)] @@ -59,9 +58,9 @@ impl App { } /// Run the app until the user exits. - fn run(&mut self, terminal: &mut Tui) -> io::Result<()> { + fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { while !self.should_exit { - self.draw(terminal)?; + terminal.draw(|frame| frame.render_widget(&self, frame.area()))?; self.handle_events()?; if self.last_tick.elapsed() >= Self::TICK_RATE { self.on_tick(); @@ -71,12 +70,6 @@ impl App { Ok(()) } - /// Draw the app to the terminal. - fn draw(&mut self, terminal: &mut Tui) -> io::Result<()> { - terminal.draw(|frame| frame.render_widget(self, frame.area()))?; - Ok(()) - } - /// Handle events from the terminal. fn handle_events(&mut self) -> io::Result<()> { let timeout = Self::TICK_RATE.saturating_sub(self.last_tick.elapsed()); @@ -96,7 +89,7 @@ impl App { } } -impl Widget for &mut App { +impl Widget for &App { fn render(self, area: Rect, buf: &mut Buffer) { let areas = Layout::vertical([Constraint::Max(9); 4]).split(area); Paragraph::new(create_lines(area)) @@ -158,75 +151,3 @@ fn create_lines(area: Rect) -> Vec> { ]), ] } - -/// 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 ratatui::{ - backend::CrosstermBackend, - crossterm::{ - terminal::{ - disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, - }, - ExecutableCommand, - }, - Terminal, - }; - - // A simple alias for the terminal type used in this example. - pub type Tui = Terminal>; - - /// Initialize the terminal and enter alternate screen mode. - pub fn init_terminal() -> io::Result { - 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(()) - } -} diff --git a/examples/popup.rs b/examples/popup.rs index 0cbb6164..2289154d 100644 --- a/examples/popup.rs +++ b/examples/popup.rs @@ -16,115 +16,78 @@ // See also https://github.com/joshka/tui-popup and // https://github.com/sephiroth74/tui-confirm-dialog -use std::{error::Error, io}; - +use color_eyre::Result; use ratatui::{ - backend::{Backend, CrosstermBackend}, - crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - }, - layout::{Constraint, Layout, Rect}, + crossterm::event::{self, Event, KeyCode, KeyEventKind}, + layout::{Constraint, Flex, Layout, Rect}, style::Stylize, widgets::{Block, Clear, Paragraph, Wrap}, - Frame, Terminal, + DefaultTerminal, Frame, }; +fn main() -> Result<()> { + color_eyre::install()?; + let terminal = ratatui::init(); + let app_result = App::default().run(terminal); + ratatui::restore(); + app_result +} + +#[derive(Default)] struct App { show_popup: bool, } impl App { - const fn new() -> Self { - Self { show_popup: false } - } -} + fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { + loop { + terminal.draw(|frame| self.draw(frame))?; -fn main() -> Result<(), Box> { - // 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(terminal: &mut Terminal, mut app: App) -> io::Result<()> { - loop { - terminal.draw(|f| ui(f, &app))?; - - if let Event::Key(key) = event::read()? { - if key.kind == KeyEventKind::Press { - match key.code { - KeyCode::Char('q') => return Ok(()), - KeyCode::Char('p') => app.show_popup = !app.show_popup, - _ => {} + if let Event::Key(key) = event::read()? { + if key.kind == KeyEventKind::Press { + match key.code { + KeyCode::Char('q') => return Ok(()), + KeyCode::Char('p') => self.show_popup = !self.show_popup, + _ => {} + } } } } } -} -fn ui(f: &mut Frame, app: &App) { - let area = f.area(); + fn draw(&self, frame: &mut Frame) { + let area = frame.area(); - let vertical = Layout::vertical([Constraint::Percentage(20), Constraint::Percentage(80)]); - let [instructions, content] = vertical.areas(area); + let vertical = Layout::vertical([Constraint::Percentage(20), Constraint::Percentage(80)]); + let [instructions, content] = vertical.areas(area); - let text = if app.show_popup { - "Press p to close the popup" - } else { - "Press p to show the popup" - }; - let paragraph = Paragraph::new(text.slow_blink()) - .centered() - .wrap(Wrap { trim: true }); - f.render_widget(paragraph, instructions); + let text = if self.show_popup { + "Press p to close the popup" + } else { + "Press p to show the popup" + }; + let paragraph = Paragraph::new(text.slow_blink()) + .centered() + .wrap(Wrap { trim: true }); + frame.render_widget(paragraph, instructions); - let block = Block::bordered().title("Content").on_blue(); - f.render_widget(block, content); + let block = Block::bordered().title("Content").on_blue(); + frame.render_widget(block, content); - if app.show_popup { - let block = Block::bordered().title("Popup"); - let area = centered_rect(60, 20, area); - f.render_widget(Clear, area); //this clears out the background - f.render_widget(block, area); + if self.show_popup { + let block = Block::bordered().title("Popup"); + let area = popup_area(area, 60, 20); + frame.render_widget(Clear, area); //this clears out the background + frame.render_widget(block, area); + } } } /// helper function to create a centered rect using up certain percentage of the available rect `r` -fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { - let popup_layout = Layout::vertical([ - Constraint::Percentage((100 - percent_y) / 2), - Constraint::Percentage(percent_y), - Constraint::Percentage((100 - percent_y) / 2), - ]) - .split(r); - - Layout::horizontal([ - Constraint::Percentage((100 - percent_x) / 2), - Constraint::Percentage(percent_x), - Constraint::Percentage((100 - percent_x) / 2), - ]) - .split(popup_layout[1])[1] +fn popup_area(area: Rect, percent_x: u16, percent_y: u16) -> Rect { + let vertical = Layout::vertical([Constraint::Percentage(percent_y)]).flex(Flex::Center); + let horizontal = Layout::horizontal([Constraint::Percentage(percent_x)]).flex(Flex::Center); + let [area] = vertical.areas(area); + let [area] = horizontal.areas(area); + area } diff --git a/examples/ratatui-logo.rs b/examples/ratatui-logo.rs index 0f479e67..675662e0 100644 --- a/examples/ratatui-logo.rs +++ b/examples/ratatui-logo.rs @@ -14,19 +14,14 @@ //! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md use std::{ - io::{self, stdout}, + io::{self}, thread::sleep, time::Duration, }; use indoc::indoc; use itertools::izip; -use ratatui::{ - backend::{Backend, CrosstermBackend}, - crossterm::terminal::{disable_raw_mode, enable_raw_mode}, - widgets::Paragraph, - Terminal, TerminalOptions, Viewport, -}; +use ratatui::{widgets::Paragraph, TerminalOptions, Viewport}; /// A fun example of using half block characters to draw a logo #[allow(clippy::many_single_char_names)] @@ -63,23 +58,12 @@ fn logo() -> String { } fn main() -> io::Result<()> { - let mut terminal = init()?; + let mut terminal = ratatui::init_with_options(TerminalOptions { + viewport: Viewport::Inline(3), + }); terminal.draw(|frame| frame.render_widget(Paragraph::new(logo()), frame.area()))?; sleep(Duration::from_secs(5)); - restore()?; + ratatui::restore(); println!(); Ok(()) } - -fn init() -> io::Result> { - 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(()) -} diff --git a/examples/scrollbar.rs b/examples/scrollbar.rs index 48ed50d0..d6e7bbae 100644 --- a/examples/scrollbar.rs +++ b/examples/scrollbar.rs @@ -15,25 +15,17 @@ #![warn(clippy::pedantic)] -use std::{ - error::Error, - io, - time::{Duration, Instant}, -}; +use std::time::{Duration, Instant}; +use color_eyre::Result; use ratatui::{ - backend::{Backend, CrosstermBackend}, - crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - }, + crossterm::event::{self, Event, KeyCode}, layout::{Alignment, Constraint, Layout, Margin}, style::{Color, Style, Stylize}, symbols::scrollbar, text::{Line, Masked, Span}, widgets::{Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState}, - Frame, Terminal, + DefaultTerminal, Frame, }; #[derive(Default)] @@ -44,195 +36,176 @@ struct App { pub horizontal_scroll: usize, } -fn main() -> Result<(), Box> { - // setup terminal - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - - // create app and run it - let tick_rate = Duration::from_millis(250); - let app = App::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 main() -> Result<()> { + color_eyre::install()?; + let terminal = ratatui::init(); + let app_result = App::default().run(terminal); + ratatui::restore(); + app_result } -fn run_app( - terminal: &mut Terminal, - mut app: App, - tick_rate: Duration, -) -> io::Result<()> { - let mut last_tick = Instant::now(); - loop { - terminal.draw(|f| ui(f, &mut app))?; +impl App { + fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { + let tick_rate = Duration::from_millis(250); + let mut last_tick = Instant::now(); + loop { + terminal.draw(|frame| self.draw(frame))?; - let timeout = tick_rate.saturating_sub(last_tick.elapsed()); - if event::poll(timeout)? { - if let Event::Key(key) = event::read()? { - match key.code { - KeyCode::Char('q') => return Ok(()), - KeyCode::Char('j') | KeyCode::Down => { - app.vertical_scroll = app.vertical_scroll.saturating_add(1); - app.vertical_scroll_state = - app.vertical_scroll_state.position(app.vertical_scroll); + let timeout = tick_rate.saturating_sub(last_tick.elapsed()); + if event::poll(timeout)? { + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => return Ok(()), + KeyCode::Char('j') | KeyCode::Down => { + self.vertical_scroll = self.vertical_scroll.saturating_add(1); + self.vertical_scroll_state = + self.vertical_scroll_state.position(self.vertical_scroll); + } + KeyCode::Char('k') | KeyCode::Up => { + self.vertical_scroll = self.vertical_scroll.saturating_sub(1); + self.vertical_scroll_state = + self.vertical_scroll_state.position(self.vertical_scroll); + } + KeyCode::Char('h') | KeyCode::Left => { + self.horizontal_scroll = self.horizontal_scroll.saturating_sub(1); + self.horizontal_scroll_state = self + .horizontal_scroll_state + .position(self.horizontal_scroll); + } + KeyCode::Char('l') | KeyCode::Right => { + self.horizontal_scroll = self.horizontal_scroll.saturating_add(1); + self.horizontal_scroll_state = self + .horizontal_scroll_state + .position(self.horizontal_scroll); + } + _ => {} } - KeyCode::Char('k') | KeyCode::Up => { - app.vertical_scroll = app.vertical_scroll.saturating_sub(1); - app.vertical_scroll_state = - app.vertical_scroll_state.position(app.vertical_scroll); - } - KeyCode::Char('h') | KeyCode::Left => { - app.horizontal_scroll = app.horizontal_scroll.saturating_sub(1); - app.horizontal_scroll_state = - app.horizontal_scroll_state.position(app.horizontal_scroll); - } - KeyCode::Char('l') | KeyCode::Right => { - app.horizontal_scroll = app.horizontal_scroll.saturating_add(1); - app.horizontal_scroll_state = - app.horizontal_scroll_state.position(app.horizontal_scroll); - } - _ => {} } } - } - if last_tick.elapsed() >= tick_rate { - last_tick = Instant::now(); + if last_tick.elapsed() >= tick_rate { + last_tick = Instant::now(); + } } } -} - -#[allow(clippy::too_many_lines, clippy::cast_possible_truncation)] -fn ui(f: &mut Frame, app: &mut App) { - let area = f.area(); - - // Words made "loooong" to demonstrate line breaking. - let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. "; - let mut long_line = s.repeat(usize::from(area.width) / s.len() + 4); - long_line.push('\n'); - - let chunks = Layout::vertical([ - Constraint::Min(1), - Constraint::Percentage(25), - Constraint::Percentage(25), - Constraint::Percentage(25), - Constraint::Percentage(25), - ]) - .split(area); - - let text = vec![ - Line::from("This is a line "), - Line::from("This is a line ".red()), - Line::from("This is a line".on_dark_gray()), - Line::from("This is a longer line".crossed_out()), - Line::from(long_line.clone()), - Line::from("This is a line".reset()), - Line::from(vec![ - Span::raw("Masked text: "), - Span::styled(Masked::new("password", '*'), Style::new().fg(Color::Red)), - ]), - Line::from("This is a line "), - Line::from("This is a line ".red()), - Line::from("This is a line".on_dark_gray()), - Line::from("This is a longer line".crossed_out()), - Line::from(long_line.clone()), - Line::from("This is a line".reset()), - Line::from(vec![ - Span::raw("Masked text: "), - Span::styled(Masked::new("password", '*'), Style::new().fg(Color::Red)), - ]), - ]; - app.vertical_scroll_state = app.vertical_scroll_state.content_length(text.len()); - app.horizontal_scroll_state = app.horizontal_scroll_state.content_length(long_line.len()); - - let create_block = |title: &'static str| Block::bordered().gray().title(title.bold()); - - let title = Block::new() - .title_alignment(Alignment::Center) - .title("Use h j k l or ◄ ▲ ▼ ► to scroll ".bold()); - f.render_widget(title, chunks[0]); - - let paragraph = Paragraph::new(text.clone()) - .gray() - .block(create_block("Vertical scrollbar with arrows")) - .scroll((app.vertical_scroll as u16, 0)); - f.render_widget(paragraph, chunks[1]); - f.render_stateful_widget( - Scrollbar::new(ScrollbarOrientation::VerticalRight) - .begin_symbol(Some("↑")) - .end_symbol(Some("↓")), - chunks[1], - &mut app.vertical_scroll_state, - ); - - let paragraph = Paragraph::new(text.clone()) - .gray() - .block(create_block( - "Vertical scrollbar without arrows, without track symbol and mirrored", - )) - .scroll((app.vertical_scroll as u16, 0)); - f.render_widget(paragraph, chunks[2]); - f.render_stateful_widget( - Scrollbar::new(ScrollbarOrientation::VerticalLeft) - .symbols(scrollbar::VERTICAL) - .begin_symbol(None) - .track_symbol(None) - .end_symbol(None), - chunks[2].inner(Margin { - vertical: 1, - horizontal: 0, - }), - &mut app.vertical_scroll_state, - ); - - let paragraph = Paragraph::new(text.clone()) - .gray() - .block(create_block( - "Horizontal scrollbar with only begin arrow & custom thumb symbol", - )) - .scroll((0, app.horizontal_scroll as u16)); - f.render_widget(paragraph, chunks[3]); - f.render_stateful_widget( - Scrollbar::new(ScrollbarOrientation::HorizontalBottom) - .thumb_symbol("🬋") - .end_symbol(None), - chunks[3].inner(Margin { - vertical: 0, - horizontal: 1, - }), - &mut app.horizontal_scroll_state, - ); - - let paragraph = Paragraph::new(text.clone()) - .gray() - .block(create_block( - "Horizontal scrollbar without arrows & custom thumb and track symbol", - )) - .scroll((0, app.horizontal_scroll as u16)); - f.render_widget(paragraph, chunks[4]); - f.render_stateful_widget( - Scrollbar::new(ScrollbarOrientation::HorizontalBottom) - .thumb_symbol("░") - .track_symbol(Some("─")), - chunks[4].inner(Margin { - vertical: 0, - horizontal: 1, - }), - &mut app.horizontal_scroll_state, - ); + + #[allow(clippy::too_many_lines, clippy::cast_possible_truncation)] + fn draw(&mut self, frame: &mut Frame) { + let area = frame.area(); + + // Words made "loooong" to demonstrate line breaking. + let s = + "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. "; + let mut long_line = s.repeat(usize::from(area.width) / s.len() + 4); + long_line.push('\n'); + + let chunks = Layout::vertical([ + Constraint::Min(1), + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + ]) + .split(area); + + let text = vec![ + Line::from("This is a line "), + Line::from("This is a line ".red()), + Line::from("This is a line".on_dark_gray()), + Line::from("This is a longer line".crossed_out()), + Line::from(long_line.clone()), + Line::from("This is a line".reset()), + Line::from(vec![ + Span::raw("Masked text: "), + Span::styled(Masked::new("password", '*'), Style::new().fg(Color::Red)), + ]), + Line::from("This is a line "), + Line::from("This is a line ".red()), + Line::from("This is a line".on_dark_gray()), + Line::from("This is a longer line".crossed_out()), + Line::from(long_line.clone()), + Line::from("This is a line".reset()), + Line::from(vec![ + Span::raw("Masked text: "), + Span::styled(Masked::new("password", '*'), Style::new().fg(Color::Red)), + ]), + ]; + self.vertical_scroll_state = self.vertical_scroll_state.content_length(text.len()); + self.horizontal_scroll_state = self.horizontal_scroll_state.content_length(long_line.len()); + + let create_block = |title: &'static str| Block::bordered().gray().title(title.bold()); + + let title = Block::new() + .title_alignment(Alignment::Center) + .title("Use h j k l or ◄ ▲ ▼ ► to scroll ".bold()); + frame.render_widget(title, chunks[0]); + + let paragraph = Paragraph::new(text.clone()) + .gray() + .block(create_block("Vertical scrollbar with arrows")) + .scroll((self.vertical_scroll as u16, 0)); + frame.render_widget(paragraph, chunks[1]); + frame.render_stateful_widget( + Scrollbar::new(ScrollbarOrientation::VerticalRight) + .begin_symbol(Some("↑")) + .end_symbol(Some("↓")), + chunks[1], + &mut self.vertical_scroll_state, + ); + + let paragraph = Paragraph::new(text.clone()) + .gray() + .block(create_block( + "Vertical scrollbar without arrows, without track symbol and mirrored", + )) + .scroll((self.vertical_scroll as u16, 0)); + frame.render_widget(paragraph, chunks[2]); + frame.render_stateful_widget( + Scrollbar::new(ScrollbarOrientation::VerticalLeft) + .symbols(scrollbar::VERTICAL) + .begin_symbol(None) + .track_symbol(None) + .end_symbol(None), + chunks[2].inner(Margin { + vertical: 1, + horizontal: 0, + }), + &mut self.vertical_scroll_state, + ); + + let paragraph = Paragraph::new(text.clone()) + .gray() + .block(create_block( + "Horizontal scrollbar with only begin arrow & custom thumb symbol", + )) + .scroll((0, self.horizontal_scroll as u16)); + frame.render_widget(paragraph, chunks[3]); + frame.render_stateful_widget( + Scrollbar::new(ScrollbarOrientation::HorizontalBottom) + .thumb_symbol("🬋") + .end_symbol(None), + chunks[3].inner(Margin { + vertical: 0, + horizontal: 1, + }), + &mut self.horizontal_scroll_state, + ); + + let paragraph = Paragraph::new(text.clone()) + .gray() + .block(create_block( + "Horizontal scrollbar without arrows & custom thumb and track symbol", + )) + .scroll((0, self.horizontal_scroll as u16)); + frame.render_widget(paragraph, chunks[4]); + frame.render_stateful_widget( + Scrollbar::new(ScrollbarOrientation::HorizontalBottom) + .thumb_symbol("░") + .track_symbol(Some("─")), + chunks[4].inner(Margin { + vertical: 0, + horizontal: 1, + }), + &mut self.horizontal_scroll_state, + ); + } } diff --git a/examples/sparkline.rs b/examples/sparkline.rs index 2f7d2522..13780c05 100644 --- a/examples/sparkline.rs +++ b/examples/sparkline.rs @@ -13,29 +13,36 @@ //! [examples]: https://github.com/ratatui/ratatui/blob/main/examples //! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md -use std::{ - error::Error, - io, - time::{Duration, Instant}, -}; +use std::time::{Duration, Instant}; +use color_eyre::Result; use rand::{ distributions::{Distribution, Uniform}, rngs::ThreadRng, }; use ratatui::{ - backend::{Backend, CrosstermBackend}, - crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - }, + crossterm::event::{self, Event, KeyCode}, layout::{Constraint, Layout}, style::{Color, Style}, widgets::{Block, Borders, Sparkline}, - Frame, Terminal, + DefaultTerminal, Frame, }; +fn main() -> Result<()> { + color_eyre::install()?; + let terminal = ratatui::init(); + let app_result = App::new().run(terminal); + ratatui::restore(); + app_result +} + +struct App { + signal: RandomSignal, + data1: Vec, + data2: Vec, + data3: Vec, +} + #[derive(Clone)] struct RandomSignal { distribution: Uniform, @@ -58,13 +65,6 @@ impl Iterator for RandomSignal { } } -struct App { - signal: RandomSignal, - data1: Vec, - data2: Vec, - data3: Vec, -} - impl App { fn new() -> Self { let mut signal = RandomSignal::new(0, 100); @@ -90,94 +90,63 @@ impl App { self.data3.pop(); self.data3.insert(0, value); } -} -fn main() -> Result<(), Box> { - // setup terminal - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; + fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { + let tick_rate = Duration::from_millis(250); - // create app and run it - let tick_rate = Duration::from_millis(250); - let app = App::new(); - let res = run_app(&mut terminal, app, tick_rate); + let mut last_tick = Instant::now(); + loop { + terminal.draw(|frame| self.draw(frame))?; - // 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( - terminal: &mut Terminal, - mut app: App, - tick_rate: Duration, -) -> io::Result<()> { - let mut last_tick = Instant::now(); - loop { - terminal.draw(|f| ui(f, &app))?; - - let timeout = tick_rate.saturating_sub(last_tick.elapsed()); - if event::poll(timeout)? { - if let Event::Key(key) = event::read()? { - if key.code == KeyCode::Char('q') { - return Ok(()); + let timeout = tick_rate.saturating_sub(last_tick.elapsed()); + if event::poll(timeout)? { + if let Event::Key(key) = event::read()? { + if key.code == KeyCode::Char('q') { + return Ok(()); + } } } - } - if last_tick.elapsed() >= tick_rate { - app.on_tick(); - last_tick = Instant::now(); + if last_tick.elapsed() >= tick_rate { + self.on_tick(); + last_tick = Instant::now(); + } } } -} -fn ui(f: &mut Frame, app: &App) { - let chunks = Layout::vertical([ - Constraint::Length(3), - Constraint::Length(3), - Constraint::Min(0), - ]) - .split(f.area()); - let sparkline = Sparkline::default() - .block( - Block::new() - .borders(Borders::LEFT | Borders::RIGHT) - .title("Data1"), - ) - .data(&app.data1) - .style(Style::default().fg(Color::Yellow)); - f.render_widget(sparkline, chunks[0]); - let sparkline = Sparkline::default() - .block( - Block::new() - .borders(Borders::LEFT | Borders::RIGHT) - .title("Data2"), - ) - .data(&app.data2) - .style(Style::default().bg(Color::Green)); - f.render_widget(sparkline, chunks[1]); - // Multiline - let sparkline = Sparkline::default() - .block( - Block::new() - .borders(Borders::LEFT | Borders::RIGHT) - .title("Data3"), - ) - .data(&app.data3) - .style(Style::default().fg(Color::Red)); - f.render_widget(sparkline, chunks[2]); + fn draw(&self, frame: &mut Frame) { + let chunks = Layout::vertical([ + Constraint::Length(3), + Constraint::Length(3), + Constraint::Min(0), + ]) + .split(frame.area()); + let sparkline = Sparkline::default() + .block( + Block::new() + .borders(Borders::LEFT | Borders::RIGHT) + .title("Data1"), + ) + .data(&self.data1) + .style(Style::default().fg(Color::Yellow)); + frame.render_widget(sparkline, chunks[0]); + let sparkline = Sparkline::default() + .block( + Block::new() + .borders(Borders::LEFT | Borders::RIGHT) + .title("Data2"), + ) + .data(&self.data2) + .style(Style::default().bg(Color::Green)); + frame.render_widget(sparkline, chunks[1]); + // Multiline + let sparkline = Sparkline::default() + .block( + Block::new() + .borders(Borders::LEFT | Borders::RIGHT) + .title("Data3"), + ) + .data(&self.data3) + .style(Style::default().fg(Color::Red)); + frame.render_widget(sparkline, chunks[2]); + } } diff --git a/examples/table.rs b/examples/table.rs index efaecf57..b50c5203 100644 --- a/examples/table.rs +++ b/examples/table.rs @@ -13,16 +13,10 @@ //! [examples]: https://github.com/ratatui/ratatui/blob/main/examples //! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md -use std::{error::Error, io}; - +use color_eyre::Result; use itertools::Itertools; use ratatui::{ - backend::{Backend, CrosstermBackend}, - crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - }, + crossterm::event::{self, Event, KeyCode, KeyEventKind}, layout::{Constraint, Layout, Margin, Rect}, style::{self, Color, Modifier, Style, Stylize}, text::{Line, Text}, @@ -30,7 +24,7 @@ use ratatui::{ Block, BorderType, Cell, HighlightSpacing, Paragraph, Row, Scrollbar, ScrollbarOrientation, ScrollbarState, Table, TableState, }, - Frame, Terminal, + DefaultTerminal, Frame, }; use style::palette::tailwind; use unicode_width::UnicodeWidthStr; @@ -46,6 +40,13 @@ const INFO_TEXT: &str = const ITEM_HEIGHT: usize = 4; +fn main() -> Result<()> { + color_eyre::install()?; + let terminal = ratatui::init(); + let app_result = App::new().run(terminal); + ratatui::restore(); + app_result +} struct TableColors { buffer_bg: Color, header_bg: Color, @@ -117,6 +118,7 @@ impl App { items: data_vec, } } + pub fn next(&mut self) { let i = match self.state.selected() { Some(i) => { @@ -159,6 +161,115 @@ impl App { pub fn set_colors(&mut self) { self.colors = TableColors::new(&PALETTES[self.color_index]); } + + fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { + loop { + terminal.draw(|frame| self.draw(frame))?; + + 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 => self.next(), + KeyCode::Char('k') | KeyCode::Up => self.previous(), + KeyCode::Char('l') | KeyCode::Right => self.next_color(), + KeyCode::Char('h') | KeyCode::Left => self.previous_color(), + _ => {} + } + } + } + } + } + + fn draw(&mut self, frame: &mut Frame) { + let vertical = &Layout::vertical([Constraint::Min(5), Constraint::Length(3)]); + let rects = vertical.split(frame.area()); + + self.set_colors(); + + self.render_table(frame, rects[0]); + self.render_scrollbar(frame, rects[0]); + self.render_footer(frame, rects[1]); + } + + fn render_table(&mut self, frame: &mut Frame, area: Rect) { + let header_style = Style::default() + .fg(self.colors.header_fg) + .bg(self.colors.header_bg); + let selected_style = Style::default() + .add_modifier(Modifier::REVERSED) + .fg(self.colors.selected_style_fg); + + let header = ["Name", "Address", "Email"] + .into_iter() + .map(Cell::from) + .collect::() + .style(header_style) + .height(1); + let rows = self.items.iter().enumerate().map(|(i, data)| { + let color = match i % 2 { + 0 => self.colors.normal_row_color, + _ => self.colors.alt_row_color, + }; + let item = data.ref_array(); + item.into_iter() + .map(|content| Cell::from(Text::from(format!("\n{content}\n")))) + .collect::() + .style(Style::new().fg(self.colors.row_fg).bg(color)) + .height(4) + }); + let bar = " █ "; + let t = Table::new( + rows, + [ + // + 1 is for padding. + Constraint::Length(self.longest_item_lens.0 + 1), + Constraint::Min(self.longest_item_lens.1 + 1), + Constraint::Min(self.longest_item_lens.2), + ], + ) + .header(header) + .highlight_style(selected_style) + .highlight_symbol(Text::from(vec![ + "".into(), + bar.into(), + bar.into(), + "".into(), + ])) + .bg(self.colors.buffer_bg) + .highlight_spacing(HighlightSpacing::Always); + frame.render_stateful_widget(t, area, &mut self.state); + } + + fn render_scrollbar(&mut self, frame: &mut Frame, area: Rect) { + frame.render_stateful_widget( + Scrollbar::default() + .orientation(ScrollbarOrientation::VerticalRight) + .begin_symbol(None) + .end_symbol(None), + area.inner(Margin { + vertical: 1, + horizontal: 1, + }), + &mut self.scroll_state, + ); + } + + fn render_footer(&self, frame: &mut Frame, area: Rect) { + let info_footer = Paragraph::new(Line::from(INFO_TEXT)) + .style( + Style::new() + .fg(self.colors.row_fg) + .bg(self.colors.buffer_bg), + ) + .centered() + .block( + Block::bordered() + .border_type(BorderType::Double) + .border_style(Style::new().fg(self.colors.footer_border_color)), + ); + frame.render_widget(info_footer, area); + } } fn generate_fake_names() -> Vec { @@ -186,114 +297,6 @@ fn generate_fake_names() -> Vec { .collect_vec() } -fn main() -> Result<(), Box> { - // 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(terminal: &mut Terminal, mut app: App) -> io::Result<()> { - loop { - terminal.draw(|f| ui(f, &mut app))?; - - if let Event::Key(key) = event::read()? { - if key.kind == KeyEventKind::Press { - match key.code { - KeyCode::Char('q') | KeyCode::Esc => return Ok(()), - KeyCode::Char('j') | KeyCode::Down => app.next(), - KeyCode::Char('k') | KeyCode::Up => app.previous(), - KeyCode::Char('l') | KeyCode::Right => app.next_color(), - KeyCode::Char('h') | KeyCode::Left => app.previous_color(), - _ => {} - } - } - } - } -} - -fn ui(f: &mut Frame, app: &mut App) { - let rects = Layout::vertical([Constraint::Min(5), Constraint::Length(3)]).split(f.area()); - - app.set_colors(); - - render_table(f, app, rects[0]); - - render_scrollbar(f, app, rects[0]); - - render_footer(f, app, rects[1]); -} - -fn render_table(f: &mut Frame, app: &mut App, area: Rect) { - let header_style = Style::default() - .fg(app.colors.header_fg) - .bg(app.colors.header_bg); - let selected_style = Style::default() - .add_modifier(Modifier::REVERSED) - .fg(app.colors.selected_style_fg); - - let header = ["Name", "Address", "Email"] - .into_iter() - .map(Cell::from) - .collect::() - .style(header_style) - .height(1); - let rows = app.items.iter().enumerate().map(|(i, data)| { - let color = match i % 2 { - 0 => app.colors.normal_row_color, - _ => app.colors.alt_row_color, - }; - let item = data.ref_array(); - item.into_iter() - .map(|content| Cell::from(Text::from(format!("\n{content}\n")))) - .collect::() - .style(Style::new().fg(app.colors.row_fg).bg(color)) - .height(4) - }); - let bar = " █ "; - let t = Table::new( - rows, - [ - // + 1 is for padding. - Constraint::Length(app.longest_item_lens.0 + 1), - Constraint::Min(app.longest_item_lens.1 + 1), - Constraint::Min(app.longest_item_lens.2), - ], - ) - .header(header) - .highlight_style(selected_style) - .highlight_symbol(Text::from(vec![ - "".into(), - bar.into(), - bar.into(), - "".into(), - ])) - .bg(app.colors.buffer_bg) - .highlight_spacing(HighlightSpacing::Always); - f.render_stateful_widget(t, area, &mut app.state); -} - fn constraint_len_calculator(items: &[Data]) -> (u16, u16, u16) { let name_len = items .iter() @@ -319,32 +322,6 @@ fn constraint_len_calculator(items: &[Data]) -> (u16, u16, u16) { (name_len as u16, address_len as u16, email_len as u16) } -fn render_scrollbar(f: &mut Frame, app: &mut App, area: Rect) { - f.render_stateful_widget( - Scrollbar::default() - .orientation(ScrollbarOrientation::VerticalRight) - .begin_symbol(None) - .end_symbol(None), - area.inner(Margin { - vertical: 1, - horizontal: 1, - }), - &mut app.scroll_state, - ); -} - -fn render_footer(f: &mut Frame, app: &App, area: Rect) { - let info_footer = Paragraph::new(Line::from(INFO_TEXT)) - .style(Style::new().fg(app.colors.row_fg).bg(app.colors.buffer_bg)) - .centered() - .block( - Block::bordered() - .border_type(BorderType::Double) - .border_style(Style::new().fg(app.colors.footer_border_color)), - ); - f.render_widget(info_footer, area); -} - #[cfg(test)] mod tests { use crate::Data; diff --git a/examples/tabs.rs b/examples/tabs.rs index c90b1dc7..96768c6b 100644 --- a/examples/tabs.rs +++ b/examples/tabs.rs @@ -13,26 +13,27 @@ //! [examples]: https://github.com/ratatui/ratatui/blob/main/examples //! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md -use std::io::stdout; - -use color_eyre::{config::HookBuilder, Result}; +use color_eyre::Result; use ratatui::{ - backend::{Backend, CrosstermBackend}, buffer::Buffer, - crossterm::{ - event::{self, Event, KeyCode, KeyEventKind}, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - ExecutableCommand, - }, + crossterm::event::{self, Event, KeyCode, KeyEventKind}, layout::{Constraint, Layout, Rect}, style::{palette::tailwind, Color, Stylize}, symbols, text::Line, widgets::{Block, Padding, Paragraph, Tabs, Widget}, - Terminal, + DefaultTerminal, }; use strum::{Display, EnumIter, FromRepr, IntoEnumIterator}; +fn main() -> Result<()> { + color_eyre::install()?; + let terminal = ratatui::init(); + let app_result = App::default().run(terminal); + ratatui::restore(); + app_result +} + #[derive(Default)] struct App { state: AppState, @@ -59,28 +60,15 @@ enum SelectedTab { Tab4, } -fn main() -> Result<()> { - init_error_hooks()?; - let mut terminal = init_terminal()?; - App::default().run(&mut terminal)?; - restore_terminal()?; - Ok(()) -} - impl App { - fn run(&mut self, terminal: &mut Terminal) -> Result<()> { + fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { while self.state == AppState::Running { - self.draw(terminal)?; + terminal.draw(|frame| frame.render_widget(&self, frame.area()))?; self.handle_events()?; } Ok(()) } - fn draw(&self, terminal: &mut Terminal) -> Result<()> { - terminal.draw(|frame| frame.render_widget(self, frame.area()))?; - Ok(()) - } - fn handle_events(&mut self) -> std::io::Result<()> { if let Event::Key(key) = event::read()? { if key.kind == KeyEventKind::Press { @@ -226,32 +214,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> { - 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(()) -} diff --git a/examples/tracing.rs b/examples/tracing.rs index 5ff1bd34..57bc0e17 100644 --- a/examples/tracing.rs +++ b/examples/tracing.rs @@ -27,39 +27,32 @@ // [tracing]: https://crates.io/crates/tracing // [tui-logger]: https://crates.io/crates/tui-logger -use std::{fs::File, io::stdout, panic, time::Duration}; +use std::{fs::File, time::Duration}; -use color_eyre::{ - config::HookBuilder, - eyre::{self, Context}, - Result, -}; +use color_eyre::{eyre::Context, Result}; use ratatui::{ - backend::{Backend, CrosstermBackend}, - crossterm::{ - event::{self, Event, KeyCode}, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - ExecutableCommand, - }, + crossterm::event::{self, Event, KeyCode}, widgets::{Block, Paragraph}, - Terminal, + Frame, }; use tracing::{debug, info, instrument, trace, Level}; use tracing_appender::{non_blocking, non_blocking::WorkerGuard}; use tracing_subscriber::EnvFilter; fn main() -> Result<()> { - init_error_hooks()?; + color_eyre::install()?; + let _guard = init_tracing()?; info!("Starting tracing example"); - let mut terminal = init_terminal()?; + let mut terminal = ratatui::init(); let mut events = vec![]; // a buffer to store the recent events to display in the UI while !should_exit(&events) { handle_events(&mut events)?; - terminal.draw(|frame| ui(frame, &events))?; + terminal.draw(|frame| draw(frame, &events))?; } - restore_terminal()?; + ratatui::restore(); + info!("Exiting tracing example"); println!("See the tracing.log file for the logs"); Ok(()) @@ -85,7 +78,7 @@ fn handle_events(events: &mut Vec) -> Result<()> { } #[instrument(skip_all)] -fn ui(frame: &mut ratatui::Frame, events: &[Event]) { +fn draw(frame: &mut Frame, events: &[Event]) { // To view this event, run the example with `RUST_LOG=tracing=debug cargo run --example tracing` trace!(frame_count = frame.count(), event_count = events.len()); let events = events.iter().map(|e| format!("{e:?}")).collect::>(); @@ -116,38 +109,3 @@ fn init_tracing() -> Result { .init(); Ok(guard) } - -/// Initialize the error hooks to ensure that the terminal is restored to a sane state before -/// exiting -fn init_error_hooks() -> Result<()> { - let (panic, error) = HookBuilder::default().into_hooks(); - let panic = panic.into_panic_hook(); - let error = error.into_eyre_hook(); - eyre::set_hook(Box::new(move |e| { - let _ = restore_terminal(); - error(e) - }))?; - panic::set_hook(Box::new(move |info| { - let _ = restore_terminal(); - panic(info); - })); - Ok(()) -} - -#[instrument] -fn init_terminal() -> Result> { - enable_raw_mode()?; - stdout().execute(EnterAlternateScreen)?; - let backend = CrosstermBackend::new(stdout()); - let terminal = Terminal::new(backend)?; - debug!("terminal initialized"); - Ok(terminal) -} - -#[instrument] -fn restore_terminal() -> Result<()> { - disable_raw_mode()?; - stdout().execute(LeaveAlternateScreen)?; - debug!("terminal restored"); - Ok(()) -} diff --git a/examples/user_input.rs b/examples/user_input.rs index 4268220f..24f9dc91 100644 --- a/examples/user_input.rs +++ b/examples/user_input.rs @@ -27,25 +27,22 @@ // // See also https://github.com/rhysd/tui-textarea and https://github.com/sayanarijit/tui-input/ -use std::{error::Error, io}; - +use color_eyre::Result; use ratatui::{ - backend::{Backend, CrosstermBackend}, - crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - }, + crossterm::event::{self, Event, KeyCode, KeyEventKind}, layout::{Constraint, Layout, Position}, style::{Color, Modifier, Style, Stylize}, text::{Line, Span, Text}, widgets::{Block, List, ListItem, Paragraph}, - Frame, Terminal, + DefaultTerminal, Frame, }; -enum InputMode { - Normal, - Editing, +fn main() -> Result<()> { + color_eyre::install()?; + let terminal = ratatui::init(); + let app_result = App::new().run(terminal); + ratatui::restore(); + app_result } /// App holds the state of the application @@ -60,6 +57,11 @@ struct App { messages: Vec, } +enum InputMode { + Normal, + Editing, +} + impl App { const fn new() -> Self { Self { @@ -133,142 +135,104 @@ impl App { self.input.clear(); self.reset_cursor(); } -} -fn main() -> Result<(), Box> { - // setup terminal - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; + fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { + loop { + terminal.draw(|frame| self.draw(frame))?; - // 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(terminal: &mut Terminal, mut app: App) -> io::Result<()> { - loop { - terminal.draw(|f| ui(f, &app))?; - - if let Event::Key(key) = event::read()? { - match app.input_mode { - InputMode::Normal => match key.code { - KeyCode::Char('e') => { - app.input_mode = InputMode::Editing; - } - KeyCode::Char('q') => { - return Ok(()); - } - _ => {} - }, - InputMode::Editing if key.kind == KeyEventKind::Press => match key.code { - KeyCode::Enter => app.submit_message(), - KeyCode::Char(to_insert) => { - app.enter_char(to_insert); - } - KeyCode::Backspace => { - app.delete_char(); - } - KeyCode::Left => { - app.move_cursor_left(); - } - KeyCode::Right => { - app.move_cursor_right(); - } - KeyCode::Esc => { - app.input_mode = InputMode::Normal; - } - _ => {} - }, - InputMode::Editing => {} + if let Event::Key(key) = event::read()? { + match self.input_mode { + InputMode::Normal => match key.code { + KeyCode::Char('e') => { + self.input_mode = InputMode::Editing; + } + KeyCode::Char('q') => { + return Ok(()); + } + _ => {} + }, + InputMode::Editing if key.kind == KeyEventKind::Press => match key.code { + KeyCode::Enter => self.submit_message(), + KeyCode::Char(to_insert) => self.enter_char(to_insert), + KeyCode::Backspace => self.delete_char(), + KeyCode::Left => self.move_cursor_left(), + KeyCode::Right => self.move_cursor_right(), + KeyCode::Esc => self.input_mode = InputMode::Normal, + _ => {} + }, + InputMode::Editing => {} + } } } } -} -fn ui(f: &mut Frame, app: &App) { - let vertical = Layout::vertical([ - Constraint::Length(1), - Constraint::Length(3), - Constraint::Min(1), - ]); - let [help_area, input_area, messages_area] = vertical.areas(f.area()); + fn draw(&self, frame: &mut Frame) { + let vertical = Layout::vertical([ + Constraint::Length(1), + Constraint::Length(3), + Constraint::Min(1), + ]); + let [help_area, input_area, messages_area] = vertical.areas(frame.area()); - let (msg, style) = match app.input_mode { - InputMode::Normal => ( - vec![ - "Press ".into(), - "q".bold(), - " to exit, ".into(), - "e".bold(), - " to start editing.".bold(), - ], - Style::default().add_modifier(Modifier::RAPID_BLINK), - ), - InputMode::Editing => ( - vec![ - "Press ".into(), - "Esc".bold(), - " to stop editing, ".into(), - "Enter".bold(), - " to record the message".into(), - ], - Style::default(), - ), - }; - let text = Text::from(Line::from(msg)).patch_style(style); - let help_message = Paragraph::new(text); - f.render_widget(help_message, help_area); + let (msg, style) = match self.input_mode { + InputMode::Normal => ( + vec![ + "Press ".into(), + "q".bold(), + " to exit, ".into(), + "e".bold(), + " to start editing.".bold(), + ], + Style::default().add_modifier(Modifier::RAPID_BLINK), + ), + InputMode::Editing => ( + vec![ + "Press ".into(), + "Esc".bold(), + " to stop editing, ".into(), + "Enter".bold(), + " to record the message".into(), + ], + Style::default(), + ), + }; + let text = Text::from(Line::from(msg)).patch_style(style); + let help_message = Paragraph::new(text); + frame.render_widget(help_message, help_area); - let input = Paragraph::new(app.input.as_str()) - .style(match app.input_mode { - InputMode::Normal => Style::default(), - InputMode::Editing => Style::default().fg(Color::Yellow), - }) - .block(Block::bordered().title("Input")); - f.render_widget(input, input_area); - match app.input_mode { - // Hide the cursor. `Frame` does this by default, so we don't need to do anything here - InputMode::Normal => {} + let input = Paragraph::new(self.input.as_str()) + .style(match self.input_mode { + InputMode::Normal => Style::default(), + InputMode::Editing => Style::default().fg(Color::Yellow), + }) + .block(Block::bordered().title("Input")); + frame.render_widget(input, input_area); + match self.input_mode { + // Hide the cursor. `Frame` does this by default, so we don't need to do anything here + InputMode::Normal => {} - // Make the cursor visible and ask ratatui to put it at the specified coordinates after - // rendering - #[allow(clippy::cast_possible_truncation)] - InputMode::Editing => f.set_cursor_position(Position::new( - // Draw the cursor at the current position in the input field. - // This position is can be controlled via the left and right arrow key - input_area.x + app.character_index as u16 + 1, - // Move one line down, from the border to the input line - input_area.y + 1, - )), + // Make the cursor visible and ask ratatui to put it at the specified coordinates after + // rendering + #[allow(clippy::cast_possible_truncation)] + InputMode::Editing => frame.set_cursor_position(Position::new( + // Draw the cursor at the current position in the input field. + // This position is can be controlled via the left and right arrow key + input_area.x + self.character_index as u16 + 1, + // Move one line down, from the border to the input line + input_area.y + 1, + )), + } + + let messages: Vec = self + .messages + .iter() + .enumerate() + .map(|(i, m)| { + let content = Line::from(Span::raw(format!("{i}: {m}"))); + ListItem::new(content) + }) + .collect(); + let messages = List::new(messages).block(Block::bordered().title("Messages")); + frame.render_widget(messages, messages_area); } - - let messages: Vec = app - .messages - .iter() - .enumerate() - .map(|(i, m)| { - let content = Line::from(Span::raw(format!("{i}: {m}"))); - ListItem::new(content) - }) - .collect(); - let messages = List::new(messages).block(Block::bordered().title("Messages")); - f.render_widget(messages, messages_area); } diff --git a/src/lib.rs b/src/lib.rs index f4f85024..ca257ec7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -102,53 +102,28 @@ //! ### Example //! //! ```rust,no_run -//! use std::io::{self, stdout}; -//! //! use ratatui::{ -//! backend::CrosstermBackend, -//! crossterm::{ -//! event::{self, Event, KeyCode}, -//! terminal::{ -//! disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, -//! }, -//! ExecutableCommand, -//! }, +//! crossterm::event::{self, Event, KeyCode, KeyEventKind}, //! widgets::{Block, Paragraph}, -//! Frame, Terminal, //! }; //! -//! fn main() -> io::Result<()> { -//! enable_raw_mode()?; -//! stdout().execute(EnterAlternateScreen)?; -//! let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?; -//! -//! let mut should_quit = false; -//! while !should_quit { -//! terminal.draw(ui)?; -//! should_quit = handle_events()?; -//! } -//! -//! disable_raw_mode()?; -//! stdout().execute(LeaveAlternateScreen)?; -//! Ok(()) -//! } -//! -//! fn handle_events() -> io::Result { -//! if event::poll(std::time::Duration::from_millis(50))? { +//! fn main() -> std::io::Result<()> { +//! let mut terminal = ratatui::init(); +//! loop { +//! terminal.draw(|frame| { +//! frame.render_widget( +//! Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")), +//! frame.area(), +//! ); +//! })?; //! if let Event::Key(key) = event::read()? { -//! if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') { -//! return Ok(true); +//! if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { +//! break; //! } //! } //! } -//! Ok(false) -//! } -//! -//! fn ui(frame: &mut Frame) { -//! frame.render_widget( -//! Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")), -//! frame.size(), -//! ); +//! ratatui::restore(); +//! Ok(()) //! } //! ``` //! @@ -170,7 +145,7 @@ //! Frame, //! }; //! -//! fn ui(frame: &mut Frame) { +//! fn draw(frame: &mut Frame) { //! let [title_area, main_area, status_area] = Layout::vertical([ //! Constraint::Length(1), //! Constraint::Min(0), @@ -213,7 +188,7 @@ //! Frame, //! }; //! -//! fn ui(frame: &mut Frame) { +//! fn draw(frame: &mut Frame) { //! let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.size()); //! //! let line = Line::from(vec![ @@ -320,6 +295,10 @@ /// re-export the `crossterm` crate so that users don't have to add it as a dependency #[cfg(feature = "crossterm")] pub use crossterm; +#[cfg(feature = "crossterm")] +pub use terminal::{ + init, init_with_options, restore, try_init, try_init_with_options, try_restore, DefaultTerminal, +}; pub use terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport}; /// re-export the `termion` crate so that users don't have to add it as a dependency #[cfg(all(not(windows), feature = "termion"))] diff --git a/src/terminal.rs b/src/terminal.rs index 00e22f50..3fe5ccf4 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -32,9 +32,15 @@ //! [`Buffer`]: crate::buffer::Buffer mod frame; +#[cfg(feature = "crossterm")] +mod init; mod terminal; mod viewport; pub use frame::{CompletedFrame, Frame}; +#[cfg(feature = "crossterm")] +pub use init::{ + init, init_with_options, restore, try_init, try_init_with_options, try_restore, DefaultTerminal, +}; pub use terminal::{Options as TerminalOptions, Terminal}; pub use viewport::Viewport; diff --git a/src/terminal/init.rs b/src/terminal/init.rs new file mode 100644 index 00000000..3d090f6a --- /dev/null +++ b/src/terminal/init.rs @@ -0,0 +1,244 @@ +use std::io::{self, stdout, Stdout}; + +use crossterm::{ + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; + +use super::TerminalOptions; +use crate::{backend::CrosstermBackend, Terminal}; + +/// A type alias for the default terminal type. +/// +/// This is a [`Terminal`] using the [`CrosstermBackend`] which writes to [`Stdout`]. This is a +/// reasonable default for most applications. To use a different backend or output stream, instead +/// use [`Terminal`] and a [backend][`crate::backend`] of your choice directly. +pub type DefaultTerminal = Terminal>; + +/// Initialize a terminal with reasonable defaults for most applications. +/// +/// This will create a new [`DefaultTerminal`] and initialize it with the following defaults: +/// +/// - Backend: [`CrosstermBackend`] writing to [`Stdout`] +/// - Raw mode is enabled +/// - Alternate screen buffer enabled +/// - A panic hook is installed that restores the terminal before panicking. Ensure that this method +/// is called after any other panic hooks that may be installed to ensure that the terminal is +/// restored before those hooks are called. +/// +/// For more control over the terminal initialization, use [`Terminal::new`] or +/// [`Terminal::with_options`]. +/// +/// Ensure that this method is called *after* your app installs any other panic hooks to ensure the +/// terminal is restored before the other hooks are called. +/// +/// Generally, use this function instead of [`try_init`] to ensure that the terminal is restored +/// correctly if any of the initialization steps fail. If you need to handle the error yourself, use +/// [`try_init`] instead. +/// +/// # Panics +/// +/// This function will panic if any of the following steps fail: +/// +/// - Enabling raw mode +/// - Entering the alternate screen buffer +/// - Creating the terminal fails due to being unable to calculate the terminal size +/// +/// # Examples +/// +/// ```rust,no_run +/// let terminal = ratatui::init(); +/// ``` +pub fn init() -> DefaultTerminal { + try_init().expect("failed to initialize terminal") +} + +/// Try to initialize a terminal using reasonable defaults for most applications. +/// +/// This function will attempt to create a [`DefaultTerminal`] and initialize it with the following +/// defaults: +/// +/// - Raw mode is enabled +/// - Alternate screen buffer enabled +/// - A panic hook is installed that restores the terminal before panicking. +/// - A [`Terminal`] is created using [`CrosstermBackend`] writing to [`Stdout`] +/// +/// If any of these steps fail, the error is returned. +/// +/// Ensure that this method is called *after* your app installs any other panic hooks to ensure the +/// terminal is restored before the other hooks are called. +/// +/// Generally, you should use [`init`] instead of this function, as the panic hook installed by this +/// function will ensure that any failures during initialization will restore the terminal before +/// panicking. This function is provided for cases where you need to handle the error yourself. +/// +/// # Examples +/// +/// ```no_run +/// let terminal = ratatui::try_init()?; +/// # Ok::<(), std::io::Error>(()) +/// ``` +pub fn try_init() -> io::Result { + set_panic_hook(); + enable_raw_mode()?; + execute!(stdout(), EnterAlternateScreen)?; + let backend = CrosstermBackend::new(stdout()); + Terminal::new(backend) +} + +/// Initialize a terminal with the given options and reasonable defaults. +/// +/// This function allows the caller to specify a custom [`Viewport`] via the [`TerminalOptions`]. It +/// will create a new [`DefaultTerminal`] and initialize it with the given options and the following +/// defaults: +/// +/// [`Viewport`]: crate::Viewport +/// +/// - Raw mode is enabled +/// - A panic hook is installed that restores the terminal before panicking. +/// +/// Unlike [`init`], this function does not enter the alternate screen buffer as this may not be +/// desired in all cases. If you need the alternate screen buffer, you should enable it manually +/// after calling this function. +/// +/// For more control over the terminal initialization, use [`Terminal::with_options`]. +/// +/// Ensure that this method is called *after* your app installs any other panic hooks to ensure the +/// terminal is restored before the other hooks are called. +/// +/// Generally, use this function instead of [`try_init_with_options`] to ensure that the terminal is +/// restored correctly if any of the initialization steps fail. If you need to handle the error +/// yourself, use [`try_init_with_options`] instead. +/// +/// # Panics +/// +/// This function will panic if any of the following steps fail: +/// +/// - Enabling raw mode +/// - Creating the terminal fails due to being unable to calculate the terminal size +/// +/// # Examples +/// +/// ```rust,no_run +/// use ratatui::{TerminalOptions, Viewport}; +/// +/// let options = TerminalOptions { +/// viewport: Viewport::Inline(5), +/// }; +/// let terminal = ratatui::init_with_options(options); +/// ``` +pub fn init_with_options(options: TerminalOptions) -> DefaultTerminal { + try_init_with_options(options).expect("failed to initialize terminal") +} + +/// Try to initialize a terminal with the given options and reasonable defaults. +/// +/// This function allows the caller to specify a custom [`Viewport`] via the [`TerminalOptions`]. It +/// will attempt to create a [`DefaultTerminal`] and initialize it with the given options and the +/// following defaults: +/// +/// [`Viewport`]: crate::Viewport +/// +/// - Raw mode is enabled +/// - A panic hook is installed that restores the terminal before panicking. +/// +/// Unlike [`try_init`], this function does not enter the alternate screen buffer as this may not be +/// desired in all cases. If you need the alternate screen buffer, you should enable it manually +/// after calling this function. +/// +/// If any of these steps fail, the error is returned. +/// +/// Ensure that this method is called *after* your app installs any other panic hooks to ensure the +/// terminal is restored before the other hooks are called. +/// +/// Generally, you should use [`init_with_options`] instead of this function, as the panic hook +/// installed by this function will ensure that any failures during initialization will restore the +/// terminal before panicking. This function is provided for cases where you need to handle the +/// error yourself. +/// +/// # Examples +/// +/// ```no_run +/// use ratatui::{TerminalOptions, Viewport}; +/// +/// let options = TerminalOptions { +/// viewport: Viewport::Inline(5), +/// }; +/// let terminal = ratatui::try_init_with_options(options)?; +/// # Ok::<(), std::io::Error>(()) +/// ``` +pub fn try_init_with_options(options: TerminalOptions) -> io::Result { + set_panic_hook(); + enable_raw_mode()?; + let backend = CrosstermBackend::new(stdout()); + Terminal::with_options(backend, options) +} + +/// Restores the terminal to its original state. +/// +/// This function should be called before the program exits to ensure that the terminal is +/// restored to its original state. +/// +/// This function will attempt to restore the terminal to its original state by performing the +/// following steps: +/// +/// 1. Raw mode is disabled. +/// 2. The alternate screen buffer is left. +/// +/// If either of these steps fail, the error is printed to stderr and ignored. +/// +/// Use this function over [`try_restore`] when you don't need to handle the error yourself, as +/// ignoring the error is generally the correct behavior when cleaning up before exiting. If you +/// need to handle the error yourself, use [`try_restore`] instead. +/// +/// # Examples +/// +/// ```rust,no_run +/// ratatui::restore(); +/// ``` +pub fn restore() { + if let Err(err) = try_restore() { + // There's not much we can do if restoring the terminal fails, so we just print the error + eprintln!("Failed to restore terminal: {err}"); + } +} + +/// Restore the terminal to its original state. +/// +/// This function will attempt to restore the terminal to its original state by performing the +/// following steps: +/// +/// 1. Raw mode is disabled. +/// 2. The alternate screen buffer is left. +/// +/// If either of these steps fail, the error is returned. +/// +/// Use [`restore`] instead of this function when you don't need to handle the error yourself, as +/// ignoring the error is generally the correct behavior when cleaning up before exiting. If you +/// need to handle the error yourself, use this function instead. +/// +/// # Examples +/// +/// ```no_run +/// ratatui::try_restore()?; +/// # Ok::<(), std::io::Error>(()) +/// ``` +pub fn try_restore() -> io::Result<()> { + // disabling raw mode first is important as it has more side effects than leaving the alternate + // screen buffer + disable_raw_mode()?; + execute!(stdout(), LeaveAlternateScreen)?; + Ok(()) +} + +/// Sets a panic hook that restores the terminal before panicking. +/// +/// Replaces the panic hook with a one that will restore the terminal state before calling the +/// original panic hook. This ensures that the terminal is left in a good state when a panic occurs. +fn set_panic_hook() { + let hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + restore(); + hook(info); + })); +}