From f05c053b9952d730ca2c4419f5e7c712548d4b99 Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Wed, 1 May 2024 20:29:10 -0700 Subject: [PATCH 01/11] docs(examples): simplify the barchart example - Simplify the rendering functions - Fix several clippy lints that were marked as allowed - Extract a common.rs for terminal initialization and error handling --- Cargo.toml | 2 +- examples/barchart.rs | 342 +++++++++++++++++-------------------------- examples/common.rs | 79 ++++++++++ 3 files changed, 212 insertions(+), 211 deletions(-) create mode 100644 examples/common.rs diff --git a/Cargo.toml b/Cargo.toml index cd5d3ad79..e3e706c26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ exclude = [ "*.log", "tags", ] -autoexamples = true +autoexamples = false # so we can use a `examples/common.rs` module edition = "2021" rust-version = "1.74.0" diff --git a/examples/barchart.rs b/examples/barchart.rs index a7763712b..9a247b237 100644 --- a/examples/barchart.rs +++ b/examples/barchart.rs @@ -32,50 +32,45 @@ use ratatui::{ text::{Line, Span}, widgets::{Bar, BarChart, BarGroup, Block, Paragraph}, }; +use unicode_width::UnicodeWidthStr; + +mod common; struct Company<'a> { - revenue: [u64; 4], + revenue: [u32; 4], label: &'a str, bar_style: Style, } struct App<'a> { - data: Vec<(&'a str, u64)>, + data: Vec>, months: [&'a str; 4], companies: [Company<'a>; 3], } const TOTAL_REVENUE: &str = "Total Revenue"; +const TICK_RATE: Duration = Duration::from_millis(250); + +fn main() -> Result<(), Box> { + common::install_hooks()?; + let mut terminal = common::init_terminal()?; + App::new().run(&mut terminal)?; + common::restore_terminal()?; + Ok(()) +} impl<'a> App<'a> { fn new() -> Self { + let mut rng = rand::thread_rng(); + let data = (1..24) + .map(|index| { + Bar::default() + .label(format!("B{index}").into()) + .value(rng.gen_range(1..15)) + }) + .collect(); App { - data: vec![ - ("B1", 9), - ("B2", 12), - ("B3", 5), - ("B4", 8), - ("B5", 2), - ("B6", 4), - ("B7", 5), - ("B8", 9), - ("B9", 14), - ("B10", 15), - ("B11", 1), - ("B12", 0), - ("B13", 4), - ("B14", 6), - ("B15", 4), - ("B16", 6), - ("B17", 4), - ("B18", 7), - ("B19", 13), - ("B20", 8), - ("B21", 11), - ("B22", 9), - ("B23", 3), - ("B24", 5), - ], + data, companies: [ Company { label: "Comp.A", @@ -97,206 +92,133 @@ impl<'a> App<'a> { } } - fn on_tick(&mut self) { - let value = self.data.pop().unwrap(); - self.data.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, terminal: &mut Terminal) -> io::Result<()> { + let mut last_tick = Instant::now(); + loop { + terminal.draw(|frame| frame.render_widget(&self, frame.size()))?; - // create app and run it - let tick_rate = Duration::from_millis(250); - let app = App::new(); - let res = run_app(&mut terminal, app, tick_rate); - - // restore terminal - disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; - terminal.show_cursor()?; - - if let Err(err) = res { - println!("{err:?}"); - } - - Ok(()) -} - -fn run_app( - 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 crossterm::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 crossterm::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.update_data(); + last_tick = Instant::now(); + } } } -} - -fn ui(frame: &mut Frame, app: &App) { - let vertical = Layout::vertical([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)]); - let horizontal = Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]); - let [top, bottom] = vertical.areas(frame.size()); - let [left, right] = horizontal.areas(bottom); - - let barchart = BarChart::default() - .block(Block::bordered().title("Data1")) - .data(&app.data) - .bar_width(9) - .bar_style(Style::default().fg(Color::Yellow)) - .value_style(Style::default().fg(Color::Black).bg(Color::Yellow)); - frame.render_widget(barchart, top); - draw_bar_with_group_labels(frame, app, left); - draw_horizontal_bars(frame, app, right); + fn update_data(&mut self) { + let value = self.data.pop().unwrap(); + self.data.insert(0, value); + } } -#[allow(clippy::cast_precision_loss)] -fn create_groups<'a>(app: &'a App, combine_values_and_labels: bool) -> Vec> { - app.months - .iter() - .enumerate() - .map(|(i, &month)| { - let bars: Vec = app - .companies - .iter() - .map(|c| { - let mut bar = Bar::default() - .value(c.revenue[i]) - .style(c.bar_style) - .value_style( - Style::default() - .bg(c.bar_style.fg.unwrap()) - .fg(Color::Black), - ); - - if combine_values_and_labels { - bar = bar.text_value(format!( - "{} ({:.1} M)", - c.label, - (c.revenue[i] as f64) / 1000. - )); - } else { - bar = bar - .text_value(format!("{:.1}", (c.revenue[i] as f64) / 1000.)) - .label(c.label.into()); - } - bar - }) - .collect(); - BarGroup::default() - .label(Line::from(month).centered()) - .bars(&bars) - }) - .collect() +impl Widget for &App<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { + let vertical = Layout::vertical([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)]); + let horizontal = + Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]); + let [top, bottom] = vertical.areas(area); + let [left, right] = horizontal.areas(bottom); + + self.main_barchart().render(top, buf); + self.group_labels_barchart().render(left, buf); + App::legend().render(App::legend_area(left), buf); + self.horizontal_barchart().render(right, buf); + App::legend().render(App::legend_area(right), buf); + } } -#[allow(clippy::cast_possible_truncation)] -fn draw_bar_with_group_labels(f: &mut Frame, app: &App, area: Rect) { - const LEGEND_HEIGHT: u16 = 6; - - let groups = create_groups(app, false); - - let mut barchart = BarChart::default() - .block(Block::bordered().title("Data1")) - .bar_width(7) - .group_gap(3); - - for group in groups { - barchart = barchart.data(group); +impl App<'_> { + fn main_barchart(&self) -> BarChart<'_> { + BarChart::default() + .block(Block::bordered().title("Data1")) + .data(BarGroup::default().bars(&self.data)) + .bar_width(9) + .bar_style(Style::default().fg(Color::Yellow)) + .value_style(Style::default().fg(Color::Black).bg(Color::Yellow)) } - f.render_widget(barchart, area); + fn group_labels_barchart(&self) -> BarChart<'_> { + let groups = self.create_groups(false); - if area.height >= LEGEND_HEIGHT && area.width >= TOTAL_REVENUE.len() as u16 + 2 { - let legend_width = TOTAL_REVENUE.len() as u16 + 2; - let legend_area = Rect { - height: LEGEND_HEIGHT, - width: legend_width, - y: area.y, - x: area.right() - legend_width, - }; - draw_legend(f, legend_area); + let mut barchart = BarChart::default() + .block(Block::default().title("Data1").borders(Borders::ALL)) + .bar_width(7) + .group_gap(3); + for group in groups { + barchart = barchart.data(group); + } + barchart } -} - -#[allow(clippy::cast_possible_truncation)] -fn draw_horizontal_bars(f: &mut Frame, app: &App, area: Rect) { - const LEGEND_HEIGHT: u16 = 6; - - let groups = create_groups(app, true); - - let mut barchart = BarChart::default() - .block(Block::bordered().title("Data1")) - .bar_width(1) - .group_gap(1) - .bar_gap(0) - .direction(Direction::Horizontal); - for group in groups { - barchart = barchart.data(group); + fn horizontal_barchart(&self) -> BarChart<'_> { + let groups = self.create_groups(true); + let mut barchart = BarChart::default() + .block(Block::default().title("Data1").borders(Borders::ALL)) + .bar_width(1) + .group_gap(1) + .bar_gap(0) + .direction(Direction::Horizontal); + for group in groups { + barchart = barchart.data(group); + } + barchart } - f.render_widget(barchart, area); - - if area.height >= LEGEND_HEIGHT && area.width >= TOTAL_REVENUE.len() as u16 + 2 { - let legend_width = TOTAL_REVENUE.len() as u16 + 2; - let legend_area = Rect { - height: LEGEND_HEIGHT, - width: legend_width, - y: area.y, - x: area.right() - legend_width, - }; - draw_legend(f, legend_area); + fn legend_area(area: Rect) -> Rect { + let height = 6; + let width = TOTAL_REVENUE.width() as u16 + 2; + Rect::new(area.right().saturating_sub(width), area.y, width, height).intersection(area) } -} -fn draw_legend(f: &mut Frame, area: Rect) { - let text = vec![ - Line::from(Span::styled( - TOTAL_REVENUE, - Style::default() - .add_modifier(Modifier::BOLD) - .fg(Color::White), - )), - Line::from(Span::styled( - "- Company A", - Style::default().fg(Color::Green), - )), - Line::from(Span::styled( - "- Company B", - Style::default().fg(Color::Yellow), - )), - Line::from(Span::styled( - "- Company C", - Style::default().fg(Color::White), - )), - ]; + fn legend() -> Paragraph<'static> { + let text = vec![ + Line::styled(TOTAL_REVENUE, (Color::White, Modifier::BOLD)), + Line::styled("- Company A", Color::Green), + Line::styled("- Company B", Color::Yellow), + Line::styled("- Company C", Color::White), + ]; + Paragraph::new(text).block(Block::bordered().fg(Color::White)) + } - let block = Block::bordered().style(Style::default().fg(Color::White)); - let paragraph = Paragraph::new(text).block(block); - f.render_widget(paragraph, area); + fn create_groups(&self, combine_values_and_labels: bool) -> Vec> { + self.months + .iter() + .enumerate() + .map(|(i, &month)| { + let bars: Vec = self + .companies + .iter() + .map(|c| { + let mut bar = Bar::default() + .value(u64::from(c.revenue[i])) + .style(c.bar_style) + .value_style((Color::Black, c.bar_style.fg.unwrap_or(Color::White))); + + if combine_values_and_labels { + bar = bar.text_value(format!( + "{} ({:.1} M)", + c.label, + f64::from(c.revenue[i]) / 1000. + )); + } else { + bar = bar + .text_value(format!("{:.1}", f64::from(c.revenue[i]) / 1000.)) + .label(c.label.into()); + } + bar + }) + .collect(); + BarGroup::default() + .label(Line::from(month).centered()) + .bars(&bars) + }) + .collect() + } } diff --git a/examples/common.rs b/examples/common.rs new file mode 100644 index 000000000..9242c51ed --- /dev/null +++ b/examples/common.rs @@ -0,0 +1,79 @@ +//! # [Ratatui] common code for use in the examples +//! +//! This module contains common code that is used in the examples. This includes functions to +//! initialize and restore the terminal, and to install the panic and error hooks. +//! +//! The latest version of this code is available in the [examples] folder in the repository. +//! +//! Please note that the examples are designed to be run against the `main` branch of the Github +//! repository. This means that you may not be able to compile with the latest release version on +//! crates.io, or the one that you have installed locally. +//! +//! See the [examples readme] for more information on finding examples that match the version of the +//! library you are using. +//! +//! [Ratatui]: https://github.com/ratatui-org/ratatui +//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples +//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md + +use std::{ + error::Error, + io::{self, stdout, Stdout}, + panic, +}; + +use color_eyre::{ + config::{EyreHook, HookBuilder, PanicHook}, + eyre, +}; +use crossterm::{ + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use ratatui::prelude::*; + +/// Initialize the terminal by enabling raw mode and entering the alternate screen. +pub fn init_terminal() -> io::Result>> { + 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. +pub fn restore_terminal() -> io::Result<()> { + disable_raw_mode()?; + execute!(stdout(), LeaveAlternateScreen,)?; + Ok(()) +} + +/// This replaces the standard `color_eyre` panic and error hooks with hooks that restore the +/// terminal before printing the panic or error. +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) { + // convert from a color_eyre PanicHook to a standard panic hook + 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) -> eyre::Result<()> { + // convert from a color_eyre EyreHook to a standard eyre hook + let eyre_hook = eyre_hook.into_eyre_hook(); + eyre::set_hook(Box::new(move |error: &(dyn Error + 'static)| { + let _ = restore_terminal(); + eyre_hook(error) + }))?; + Ok(()) +} From 508f3e958d40074d59919b13dc7511934e98df0c Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Tue, 14 May 2024 19:40:42 -0700 Subject: [PATCH 02/11] chore: more simplification, remove prelude, restructure data etc. --- examples/barchart.rs | 274 ++++++++++++++++++++++++++----------------- examples/common.rs | 22 ++-- 2 files changed, 180 insertions(+), 116 deletions(-) diff --git a/examples/barchart.rs b/examples/barchart.rs index 9a247b237..dbf7ff171 100644 --- a/examples/barchart.rs +++ b/examples/barchart.rs @@ -13,12 +13,11 @@ //! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples //! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md -use std::{ - error::Error, - io, - time::{Duration, Instant}, -}; +use std::time::{Duration, Instant}; +use crossterm::event::{self, Event, KeyCode}; +use itertools::{izip, Itertools}; +use rand::prelude::*; use ratatui::{ backend::{Backend, CrosstermBackend}, crossterm::{ @@ -34,191 +33,250 @@ use ratatui::{ }; use unicode_width::UnicodeWidthStr; +/// Contains functions to initialize / restore the terminal, and install panic / error hooks. mod common; -struct Company<'a> { - revenue: [u32; 4], - label: &'a str, - bar_style: Style, -} - struct App<'a> { data: Vec>, - months: [&'a str; 4], - companies: [Company<'a>; 3], + companies: [Company; COMPANY_COUNT], + revenues: [Revenues; PERIOD_COUNT], +} + +struct Revenues { + period: &'static str, + revenues: [u32; COMPANY_COUNT], +} + +struct Company { + name: &'static str, + label: &'static str, + color: Color, } -const TOTAL_REVENUE: &str = "Total Revenue"; +const COMPANY_COUNT: usize = 3; +const PERIOD_COUNT: usize = 4; +const TOTAL_REVENUE_LABEL: &str = "Total Revenue"; const TICK_RATE: Duration = Duration::from_millis(250); -fn main() -> Result<(), Box> { +fn main() -> color_eyre::Result<()> { common::install_hooks()?; let mut terminal = common::init_terminal()?; - App::new().run(&mut terminal)?; + let app = App::new(); + app.run(&mut terminal)?; common::restore_terminal()?; Ok(()) } impl<'a> App<'a> { + /// Create a new instance of the application fn new() -> Self { - let mut rng = rand::thread_rng(); - let data = (1..24) - .map(|index| { - Bar::default() - .label(format!("B{index}").into()) - .value(rng.gen_range(1..15)) - }) - .collect(); App { - data, - companies: [ - Company { - label: "Comp.A", - revenue: [9500, 12500, 5300, 8500], - bar_style: Style::default().fg(Color::Green), - }, - Company { - label: "Comp.B", - revenue: [1500, 2500, 3000, 500], - bar_style: Style::default().fg(Color::Yellow), - }, - Company { - label: "Comp.C", - revenue: [10500, 10600, 9000, 4200], - bar_style: Style::default().fg(Color::White), - }, - ], - months: ["Mars", "Apr", "May", "Jun"], + data: generate_main_barchart_data(), + companies: Company::companies(), + revenues: Revenues::revenues(), } } - fn run(mut self, terminal: &mut Terminal) -> io::Result<()> { + /// Run the application + fn run(mut self, terminal: &mut Terminal) -> color_eyre::Result<()> { let mut last_tick = Instant::now(); loop { terminal.draw(|frame| frame.render_widget(&self, frame.size()))?; let timeout = TICK_RATE.saturating_sub(last_tick.elapsed()); - if crossterm::event::poll(timeout)? { + if event::poll(timeout)? { if let Event::Key(key) = event::read()? { if key.code == KeyCode::Char('q') { - return Ok(()); + break; } } } if last_tick.elapsed() >= TICK_RATE { + // only update the data every 250ms self.update_data(); last_tick = Instant::now(); } } + return Ok(()); } + // Rotate the data to simulate a real-time update fn update_data(&mut self) { let value = self.data.pop().unwrap(); self.data.insert(0, value); } } +/// Generate some random data for the main bar chart +fn generate_main_barchart_data() -> Vec> { + let mut rng = rand::thread_rng(); + (1..50) + .map(|index| { + Bar::default() + .label(format!("B{index:>02}").into()) + .value(rng.gen_range(1..15)) + }) + .collect() +} + impl Widget for &App<'_> { + /// Render the application fn render(self, area: Rect, buf: &mut Buffer) { - let vertical = Layout::vertical([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)]); - let horizontal = - Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]); - let [top, bottom] = vertical.areas(area); - let [left, right] = horizontal.areas(bottom); + use Constraint::*; + let [top, bottom] = Layout::vertical([Ratio(1, 3), Ratio(2, 3)]).areas(area); + let [left, right] = Layout::horizontal([Percentage(50), Percentage(50)]).areas(bottom); + + let left_legend_area = App::legend_area(left); + let right_legend_area = App::legend_area(right); self.main_barchart().render(top, buf); - self.group_labels_barchart().render(left, buf); - App::legend().render(App::legend_area(left), buf); - self.horizontal_barchart().render(right, buf); - App::legend().render(App::legend_area(right), buf); + self.vertical_revenue_barchart().render(left, buf); + self.horizontal_revenue_barchart().render(right, buf); + self.legend().render(left_legend_area, buf); + self.legend().render(right_legend_area, buf); } } impl App<'_> { + /// Create a bar chart with the data from the `data` field. fn main_barchart(&self) -> BarChart<'_> { BarChart::default() - .block(Block::bordered().title("Data1")) + .block(Block::bordered().title("Vertical Grouped")) .data(BarGroup::default().bars(&self.data)) - .bar_width(9) + .bar_width(5) .bar_style(Style::default().fg(Color::Yellow)) .value_style(Style::default().fg(Color::Black).bg(Color::Yellow)) } - fn group_labels_barchart(&self) -> BarChart<'_> { - let groups = self.create_groups(false); - + /// Create a vertical revenue bar chart with the data from the `revenues` field. + fn vertical_revenue_barchart(&self) -> BarChart<'_> { let mut barchart = BarChart::default() - .block(Block::default().title("Data1").borders(Borders::ALL)) + .block(Block::bordered().title("Vertical Grouped")) .bar_width(7) .group_gap(3); - for group in groups { + for group in self + .revenues + .iter() + .map(|revenue| revenue.to_vertical_bar_group(&self.companies)) + { barchart = barchart.data(group); } barchart } - fn horizontal_barchart(&self) -> BarChart<'_> { - let groups = self.create_groups(true); + /// Create a horizontal revenue bar chart with the data from the `revenues` field. + fn horizontal_revenue_barchart(&self) -> BarChart<'_> { let mut barchart = BarChart::default() - .block(Block::default().title("Data1").borders(Borders::ALL)) + .block(Block::bordered().title("Horizontal Grouped")) .bar_width(1) .group_gap(1) .bar_gap(0) .direction(Direction::Horizontal); - for group in groups { + for group in self + .revenues + .iter() + .map(|revenue| revenue.to_horizontal_bar_group(&self.companies)) + { barchart = barchart.data(group); } barchart } + /// Calculate the area for the legend based on the width of the revenue bar chart. fn legend_area(area: Rect) -> Rect { let height = 6; - let width = TOTAL_REVENUE.width() as u16 + 2; + let width = TOTAL_REVENUE_LABEL.width() as u16 + 2; Rect::new(area.right().saturating_sub(width), area.y, width, height).intersection(area) } - fn legend() -> Paragraph<'static> { - let text = vec![ - Line::styled(TOTAL_REVENUE, (Color::White, Modifier::BOLD)), - Line::styled("- Company A", Color::Green), - Line::styled("- Company B", Color::Yellow), - Line::styled("- Company C", Color::White), - ]; - Paragraph::new(text).block(Block::bordered().fg(Color::White)) + /// Create a `Paragraph` widget with the legend for the revenue bar charts. + fn legend(&self) -> Paragraph<'static> { + let mut text = vec![Line::styled( + TOTAL_REVENUE_LABEL, + (Color::White, Modifier::BOLD), + )]; + for company in self.companies.iter() { + text.push(Line::styled(format!("- {}", company.name), company.color)); + } + Paragraph::new(text).block(Block::bordered().white()) } +} - fn create_groups(&self, combine_values_and_labels: bool) -> Vec> { - self.months - .iter() - .enumerate() - .map(|(i, &month)| { - let bars: Vec = self - .companies - .iter() - .map(|c| { - let mut bar = Bar::default() - .value(u64::from(c.revenue[i])) - .style(c.bar_style) - .value_style((Color::Black, c.bar_style.fg.unwrap_or(Color::White))); - - if combine_values_and_labels { - bar = bar.text_value(format!( - "{} ({:.1} M)", - c.label, - f64::from(c.revenue[i]) / 1000. - )); - } else { - bar = bar - .text_value(format!("{:.1}", f64::from(c.revenue[i]) / 1000.)) - .label(c.label.into()); - } - bar - }) - .collect(); - BarGroup::default() - .label(Line::from(month).centered()) - .bars(&bars) - }) - .collect() +impl Revenues { + /// Create a new instance of `Revenues` + fn new(period: &'static str, revenues: [u32; COMPANY_COUNT]) -> Self { + Self { period, revenues } + } + + /// Some fake revenue data + fn revenues() -> [Self; PERIOD_COUNT] { + [ + Self::new("Jan", [9500, 1500, 10500]), + Self::new("Feb", [12500, 2500, 10600]), + Self::new("Mar", [5300, 3000, 9000]), + Self::new("Apr", [8500, 500, 4200]), + ] + } + + /// Create a `BarGroup` with vertical bars for each company + fn to_vertical_bar_group<'a>(&'a self, companies: &'a [Company]) -> BarGroup<'a> { + let bars = izip!(companies, self.revenues) + .map(|(company, revenue)| company.vertical_revenue_bar(revenue)) + .collect_vec(); + BarGroup::default() + .label(Line::from(self.period).centered()) + .bars(&bars) + } + + /// Create a `BarGroup` with horizontal bars for each company + fn to_horizontal_bar_group<'a>(&'a self, companies: &'a [Company]) -> BarGroup<'a> { + let bars = izip!(companies, self.revenues) + .map(|(company, revenue)| company.horizontal_revenue_bar(revenue)) + .collect_vec(); + BarGroup::default() + .label(Line::from(self.period).centered()) + .bars(&bars) + } +} + +impl Company { + /// Create a new instance of `Company` + fn new(name: &'static str, label: &'static str, color: Color) -> Self { + Self { name, label, color } + } + + /// Generate fake company data + fn companies() -> [Self; COMPANY_COUNT] { + [ + Self::new("Company A", "Comp.A", Color::Green), + Self::new("Company B", "Comp.B", Color::Yellow), + Self::new("Company C", "Comp.C", Color::White), + ] + } + + /// Create a vertical revenue bar for the company + /// + /// The label is the short name of the company, and will be displayed under the bar + fn vertical_revenue_bar(&self, revenue: u32) -> Bar { + let text_value = format!("{:.1}", f64::from(revenue) / 1000.); + Bar::default() + .label(self.label.into()) + .value(u64::from(revenue)) + .text_value(text_value) + .style(self.color) + .value_style(Style::new().fg(Color::Black).bg(self.color)) + } + + /// Create a horizontal revenue bar for the company + /// + /// The label is the short name of the company combined with the revenue and will be displayed + /// on the bar + fn horizontal_revenue_bar(&self, revenue: u32) -> Bar { + let text_value = format!("{} ({:.1} M)", self.label, f64::from(revenue) / 1000.); + Bar::default() + .value(u64::from(revenue)) + .text_value(text_value) + .style(self.color) + .value_style(Style::new().fg(Color::Black).bg(self.color)) } } diff --git a/examples/common.rs b/examples/common.rs index 9242c51ed..1b78d2ebd 100644 --- a/examples/common.rs +++ b/examples/common.rs @@ -17,20 +17,22 @@ //! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md use std::{ - error::Error, io::{self, stdout, Stdout}, panic, }; use color_eyre::{ config::{EyreHook, HookBuilder, PanicHook}, - eyre, + eyre::{self}, }; use crossterm::{ execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + terminal::{ + disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen, + LeaveAlternateScreen, + }, }; -use ratatui::prelude::*; +use ratatui::{backend::CrosstermBackend, Terminal}; /// Initialize the terminal by enabling raw mode and entering the alternate screen. pub fn init_terminal() -> io::Result>> { @@ -42,9 +44,13 @@ pub fn init_terminal() -> io::Result>> { } /// Restore the terminal by leaving the alternate screen and disabling raw mode. -pub fn restore_terminal() -> io::Result<()> { +pub fn restore_terminal() -> color_eyre::Result<()> { disable_raw_mode()?; - execute!(stdout(), LeaveAlternateScreen,)?; + execute!( + stdout(), + LeaveAlternateScreen, + Clear(ClearType::FromCursorDown), + )?; Ok(()) } @@ -68,10 +74,10 @@ fn install_panic_hook(panic_hook: PanicHook) { } /// Install an error hook that restores the terminal before printing the error. -fn install_error_hook(eyre_hook: EyreHook) -> eyre::Result<()> { +fn install_error_hook(eyre_hook: EyreHook) -> color_eyre::Result<()> { // convert from a color_eyre EyreHook to a standard eyre hook let eyre_hook = eyre_hook.into_eyre_hook(); - eyre::set_hook(Box::new(move |error: &(dyn Error + 'static)| { + eyre::set_hook(Box::new(move |error| { let _ = restore_terminal(); eyre_hook(error) }))?; From 739f381446fb868b824e38b664655b62715b9c36 Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Tue, 14 May 2024 22:20:38 -0700 Subject: [PATCH 03/11] fix: clippy lints --- examples/barchart.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/barchart.rs b/examples/barchart.rs index dbf7ff171..392c094a6 100644 --- a/examples/barchart.rs +++ b/examples/barchart.rs @@ -72,8 +72,8 @@ impl<'a> App<'a> { fn new() -> Self { App { data: generate_main_barchart_data(), - companies: Company::companies(), - revenues: Revenues::revenues(), + companies: Company::fake_companies(), + revenues: Revenues::fake_revenues(), } } @@ -97,7 +97,7 @@ impl<'a> App<'a> { last_tick = Instant::now(); } } - return Ok(()); + Ok(()) } // Rotate the data to simulate a real-time update @@ -122,7 +122,7 @@ fn generate_main_barchart_data() -> Vec> { impl Widget for &App<'_> { /// Render the application fn render(self, area: Rect, buf: &mut Buffer) { - use Constraint::*; + use Constraint::{Percentage, Ratio}; let [top, bottom] = Layout::vertical([Ratio(1, 3), Ratio(2, 3)]).areas(area); let [left, right] = Layout::horizontal([Percentage(50), Percentage(50)]).areas(bottom); @@ -195,7 +195,7 @@ impl App<'_> { TOTAL_REVENUE_LABEL, (Color::White, Modifier::BOLD), )]; - for company in self.companies.iter() { + for company in &self.companies { text.push(Line::styled(format!("- {}", company.name), company.color)); } Paragraph::new(text).block(Block::bordered().white()) @@ -204,12 +204,12 @@ impl App<'_> { impl Revenues { /// Create a new instance of `Revenues` - fn new(period: &'static str, revenues: [u32; COMPANY_COUNT]) -> Self { + const fn new(period: &'static str, revenues: [u32; COMPANY_COUNT]) -> Self { Self { period, revenues } } /// Some fake revenue data - fn revenues() -> [Self; PERIOD_COUNT] { + fn fake_revenues() -> [Self; PERIOD_COUNT] { [ Self::new("Jan", [9500, 1500, 10500]), Self::new("Feb", [12500, 2500, 10600]), @@ -241,12 +241,12 @@ impl Revenues { impl Company { /// Create a new instance of `Company` - fn new(name: &'static str, label: &'static str, color: Color) -> Self { + const fn new(name: &'static str, label: &'static str, color: Color) -> Self { Self { name, label, color } } /// Generate fake company data - fn companies() -> [Self; COMPANY_COUNT] { + fn fake_companies() -> [Self; COMPANY_COUNT] { [ Self::new("Company A", "Comp.A", Color::Green), Self::new("Company B", "Comp.B", Color::Yellow), From 333b95f56c99333af120f7a630fbab3b4e715c13 Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Tue, 14 May 2024 22:26:33 -0700 Subject: [PATCH 04/11] fix: clippy lints --- examples/barchart.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/barchart.rs b/examples/barchart.rs index 392c094a6..0c9d08791 100644 --- a/examples/barchart.rs +++ b/examples/barchart.rs @@ -209,7 +209,7 @@ impl Revenues { } /// Some fake revenue data - fn fake_revenues() -> [Self; PERIOD_COUNT] { + const fn fake_revenues() -> [Self; PERIOD_COUNT] { [ Self::new("Jan", [9500, 1500, 10500]), Self::new("Feb", [12500, 2500, 10600]), @@ -246,7 +246,7 @@ impl Company { } /// Generate fake company data - fn fake_companies() -> [Self; COMPANY_COUNT] { + const fn fake_companies() -> [Self; COMPANY_COUNT] { [ Self::new("Company A", "Comp.A", Color::Green), Self::new("Company B", "Comp.B", Color::Yellow), From 8b703ee9632481d3f7c110bbb47e5d4133b0e5ec Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Wed, 15 May 2024 02:46:50 -0700 Subject: [PATCH 05/11] fix: use rand imports and Style::new() --- examples/barchart.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/barchart.rs b/examples/barchart.rs index 0c9d08791..ef812f4ed 100644 --- a/examples/barchart.rs +++ b/examples/barchart.rs @@ -17,7 +17,7 @@ use std::time::{Duration, Instant}; use crossterm::event::{self, Event, KeyCode}; use itertools::{izip, Itertools}; -use rand::prelude::*; +use rand::{thread_rng, Rng}; use ratatui::{ backend::{Backend, CrosstermBackend}, crossterm::{ @@ -109,7 +109,7 @@ impl<'a> App<'a> { /// Generate some random data for the main bar chart fn generate_main_barchart_data() -> Vec> { - let mut rng = rand::thread_rng(); + let mut rng = thread_rng(); (1..50) .map(|index| { Bar::default() @@ -144,8 +144,8 @@ impl App<'_> { .block(Block::bordered().title("Vertical Grouped")) .data(BarGroup::default().bars(&self.data)) .bar_width(5) - .bar_style(Style::default().fg(Color::Yellow)) - .value_style(Style::default().fg(Color::Black).bg(Color::Yellow)) + .bar_style(Style::new().fg(Color::Yellow)) + .value_style(Style::new().fg(Color::Black).bg(Color::Yellow)) } /// Create a vertical revenue bar chart with the data from the `revenues` field. From db9b93b688d1e7382a8179b60034e99f670ab20d Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Wed, 15 May 2024 17:17:14 -0700 Subject: [PATCH 06/11] fix: tweaks Move constants closer to their usage add an exit field to app extract methods from the main loop to make the flow clearer move update handling to use a field on app instead of a variable in the loop rename last_tick to last_update to make it clearer what it controls --- examples/barchart.rs | 69 +++++++++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/examples/barchart.rs b/examples/barchart.rs index ef812f4ed..92698e26e 100644 --- a/examples/barchart.rs +++ b/examples/barchart.rs @@ -13,7 +13,10 @@ //! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples //! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md -use std::time::{Duration, Instant}; +use std::{ + io, + time::{Duration, Instant}, +}; use crossterm::event::{self, Event, KeyCode}; use itertools::{izip, Itertools}; @@ -36,8 +39,13 @@ use unicode_width::UnicodeWidthStr; /// Contains functions to initialize / restore the terminal, and install panic / error hooks. mod common; +const COMPANY_COUNT: usize = 3; +const PERIOD_COUNT: usize = 4; + struct App<'a> { + exit: bool, data: Vec>, + last_update: Instant, companies: [Company; COMPANY_COUNT], revenues: [Revenues; PERIOD_COUNT], } @@ -53,11 +61,6 @@ struct Company { color: Color, } -const COMPANY_COUNT: usize = 3; -const PERIOD_COUNT: usize = 4; -const TOTAL_REVENUE_LABEL: &str = "Total Revenue"; -const TICK_RATE: Duration = Duration::from_millis(250); - fn main() -> color_eyre::Result<()> { common::install_hooks()?; let mut terminal = common::init_terminal()?; @@ -68,42 +71,54 @@ fn main() -> color_eyre::Result<()> { } impl<'a> App<'a> { + // update the data every 250ms + const UPDATE_RATE: Duration = Duration::from_millis(250); + /// Create a new instance of the application fn new() -> Self { App { + exit: false, data: generate_main_barchart_data(), + last_update: Instant::now(), companies: Company::fake_companies(), revenues: Revenues::fake_revenues(), } } /// Run the application - fn run(mut self, terminal: &mut Terminal) -> color_eyre::Result<()> { - let mut last_tick = Instant::now(); - loop { - terminal.draw(|frame| frame.render_widget(&self, frame.size()))?; - - 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') { - break; - } + fn run(mut self, terminal: &mut Terminal) -> io::Result<()> { + while !self.exit { + self.draw(terminal)?; + self.handle_events()?; + self.update_data(); + } + Ok(()) + } + + fn draw(&self, terminal: &mut Terminal) -> io::Result<()> { + terminal.draw(|frame| frame.render_widget(self, frame.size()))?; + Ok(()) + } + + fn handle_events(&mut self) -> io::Result<()> { + let timeout = Self::UPDATE_RATE.saturating_sub(self.last_update.elapsed()); + if event::poll(timeout)? { + if let Event::Key(key) = event::read()? { + if key.code == KeyCode::Char('q') { + self.exit = true; } } - if last_tick.elapsed() >= TICK_RATE { - // only update the data every 250ms - self.update_data(); - last_tick = Instant::now(); - } } Ok(()) } // Rotate the data to simulate a real-time update fn update_data(&mut self) { - let value = self.data.pop().unwrap(); - self.data.insert(0, value); + if self.last_update.elapsed() >= Self::UPDATE_RATE { + let value = self.data.pop().unwrap(); + self.data.insert(0, value); + self.last_update = Instant::now(); + } } } @@ -138,6 +153,8 @@ impl Widget for &App<'_> { } impl App<'_> { + const TOTAL_REVENUE_LABEL: &'static str = "Total Revenue"; + /// Create a bar chart with the data from the `data` field. fn main_barchart(&self) -> BarChart<'_> { BarChart::default() @@ -185,14 +202,14 @@ impl App<'_> { /// Calculate the area for the legend based on the width of the revenue bar chart. fn legend_area(area: Rect) -> Rect { let height = 6; - let width = TOTAL_REVENUE_LABEL.width() as u16 + 2; + let width = Self::TOTAL_REVENUE_LABEL.width() as u16 + 2; Rect::new(area.right().saturating_sub(width), area.y, width, height).intersection(area) } /// Create a `Paragraph` widget with the legend for the revenue bar charts. fn legend(&self) -> Paragraph<'static> { let mut text = vec![Line::styled( - TOTAL_REVENUE_LABEL, + Self::TOTAL_REVENUE_LABEL, (Color::White, Modifier::BOLD), )]; for company in &self.companies { From 04519c4c0680f549523845a0ad4f0ab13022a82f Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Thu, 16 May 2024 21:11:18 -0700 Subject: [PATCH 07/11] docs: move common stuff back into barchart, document design choices in readme --- examples/README.md | 22 ++++++++++ examples/barchart.rs | 102 ++++++++++++++++++++++++++++++++++++------- examples/common.rs | 85 ------------------------------------ 3 files changed, 109 insertions(+), 100 deletions(-) delete mode 100644 examples/common.rs diff --git a/examples/README.md b/examples/README.md index b5b7c99d2..238eefce2 100644 --- a/examples/README.md +++ b/examples/README.md @@ -23,6 +23,28 @@ This folder might use unreleased code. View the examples for the latest release > We don't keep the CHANGELOG updated with unreleased changes, check the git commit history or run > `git-cliff -u` against a cloned version of this repository. +## Design choices + +The examples contain some opinionated choices in order to make it easier for newer rustaceans to +easily be productive in creating applications: + +- Each example has an App struct, with methods that implement a main loop, handle events and drawing + the UI. +- Each App implements the Widget trait for drawing the UI. The `render` method makes a good point to + write tests against. +- We use color_eyre for handling errors and panics. See [How to use color-eyre with Ratatui] on the + website for more information about this. +- Common code is not extracted into a separate file. This makes each example self-contained and easy + to read as a whole. + +Not every example has been updated with all these points in mind yet, however over time they will +be. None of the above choices are strictly necessary for Ratatui apps, but these choices make +examples easier to run, maintain and explain. These choices are designed to help newer users fall +into the pit of success when incorporating example code into their own apps. We may also eventually +move some of these design choices into the core of Ratatui to simplify apps. + +[How to use color-eyre with Ratatui]: https://ratatui.rs/how-to/develop-apps/color_eyre/ + ## Demo2 This is the demo example from the main README and crate page. Source: [demo2](./demo2/). diff --git a/examples/barchart.rs b/examples/barchart.rs index 92698e26e..2525745dc 100644 --- a/examples/barchart.rs +++ b/examples/barchart.rs @@ -18,27 +18,19 @@ use std::{ time::{Duration, Instant}, }; +use common::Terminal; use crossterm::event::{self, Event, KeyCode}; use itertools::{izip, Itertools}; use rand::{thread_rng, Rng}; use ratatui::{ - backend::{Backend, CrosstermBackend}, - crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - }, + buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, - terminal::{Frame, Terminal}, - text::{Line, Span}, - widgets::{Bar, BarChart, BarGroup, Block, Paragraph}, + style::{Color, Modifier, Style, Stylize}, + text::Line, + widgets::{Bar, BarChart, BarGroup, Block, Paragraph, Widget}, }; use unicode_width::UnicodeWidthStr; -/// Contains functions to initialize / restore the terminal, and install panic / error hooks. -mod common; - const COMPANY_COUNT: usize = 3; const PERIOD_COUNT: usize = 4; @@ -86,7 +78,7 @@ impl<'a> App<'a> { } /// Run the application - fn run(mut self, terminal: &mut Terminal) -> io::Result<()> { + fn run(mut self, terminal: &mut Terminal) -> io::Result<()> { while !self.exit { self.draw(terminal)?; self.handle_events()?; @@ -95,7 +87,7 @@ impl<'a> App<'a> { Ok(()) } - fn draw(&self, terminal: &mut Terminal) -> io::Result<()> { + fn draw(&self, terminal: &mut Terminal) -> io::Result<()> { terminal.draw(|frame| frame.render_widget(self, frame.size()))?; Ok(()) } @@ -297,3 +289,83 @@ impl Company { .value_style(Style::new().fg(Color::Black).bg(self.color)) } } + +/// Contains functions common to all examples +mod common { + use std::{ + io::{self, stdout, Stdout}, + panic, + }; + + use color_eyre::{ + config::{EyreHook, HookBuilder, PanicHook}, + eyre::{self}, + }; + use crossterm::{ + execute, + terminal::{ + disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen, + LeaveAlternateScreen, + }, + }; + use ratatui::backend::CrosstermBackend; + + // 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_terminal() -> io::Result { + enable_raw_mode()?; + execute!(stdout(), EnterAlternateScreen)?; + let backend = CrosstermBackend::new(stdout()); + Terminal::new(backend) + } + + /// 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_terminal() -> io::Result<()> { + disable_raw_mode()?; + execute!( + stdout(), + LeaveAlternateScreen, + Clear(ClearType::FromCursorDown), + ) + } + + /// 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/common.rs b/examples/common.rs deleted file mode 100644 index 1b78d2ebd..000000000 --- a/examples/common.rs +++ /dev/null @@ -1,85 +0,0 @@ -//! # [Ratatui] common code for use in the examples -//! -//! This module contains common code that is used in the examples. This includes functions to -//! initialize and restore the terminal, and to install the panic and error hooks. -//! -//! The latest version of this code is available in the [examples] folder in the repository. -//! -//! Please note that the examples are designed to be run against the `main` branch of the Github -//! repository. This means that you may not be able to compile with the latest release version on -//! crates.io, or the one that you have installed locally. -//! -//! See the [examples readme] for more information on finding examples that match the version of the -//! library you are using. -//! -//! [Ratatui]: https://github.com/ratatui-org/ratatui -//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples -//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md - -use std::{ - io::{self, stdout, Stdout}, - panic, -}; - -use color_eyre::{ - config::{EyreHook, HookBuilder, PanicHook}, - eyre::{self}, -}; -use crossterm::{ - execute, - terminal::{ - disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen, - LeaveAlternateScreen, - }, -}; -use ratatui::{backend::CrosstermBackend, Terminal}; - -/// Initialize the terminal by enabling raw mode and entering the alternate screen. -pub fn init_terminal() -> io::Result>> { - 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. -pub fn restore_terminal() -> color_eyre::Result<()> { - disable_raw_mode()?; - execute!( - stdout(), - LeaveAlternateScreen, - Clear(ClearType::FromCursorDown), - )?; - Ok(()) -} - -/// This replaces the standard `color_eyre` panic and error hooks with hooks that restore the -/// terminal before printing the panic or error. -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) { - // convert from a color_eyre PanicHook to a standard panic hook - 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<()> { - // convert from a color_eyre EyreHook to a standard eyre hook - let eyre_hook = eyre_hook.into_eyre_hook(); - eyre::set_hook(Box::new(move |error| { - let _ = restore_terminal(); - eyre_hook(error) - }))?; - Ok(()) -} From 77f8183b77b96348ffa636d6693da83dd66d5882 Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Thu, 16 May 2024 21:30:29 -0700 Subject: [PATCH 08/11] fix: remove autoexamples setting from cargo.toml --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e3e706c26..2d7797fbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,6 @@ exclude = [ "*.log", "tags", ] -autoexamples = false # so we can use a `examples/common.rs` module edition = "2021" rust-version = "1.74.0" From 4ae646b207b1d21d7a8b15f679f1fe9387efb924 Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Wed, 29 May 2024 05:02:40 -0700 Subject: [PATCH 09/11] fix: simplify unnecessary lifetimes --- examples/barchart.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/barchart.rs b/examples/barchart.rs index 2525745dc..6fc2f2e19 100644 --- a/examples/barchart.rs +++ b/examples/barchart.rs @@ -34,9 +34,9 @@ use unicode_width::UnicodeWidthStr; const COMPANY_COUNT: usize = 3; const PERIOD_COUNT: usize = 4; -struct App<'a> { +struct App { exit: bool, - data: Vec>, + data: Vec>, last_update: Instant, companies: [Company; COMPANY_COUNT], revenues: [Revenues; PERIOD_COUNT], @@ -62,7 +62,7 @@ fn main() -> color_eyre::Result<()> { Ok(()) } -impl<'a> App<'a> { +impl App { // update the data every 250ms const UPDATE_RATE: Duration = Duration::from_millis(250); @@ -126,7 +126,7 @@ fn generate_main_barchart_data() -> Vec> { .collect() } -impl Widget for &App<'_> { +impl Widget for &App { /// Render the application fn render(self, area: Rect, buf: &mut Buffer) { use Constraint::{Percentage, Ratio}; @@ -144,7 +144,7 @@ impl Widget for &App<'_> { } } -impl App<'_> { +impl App { const TOTAL_REVENUE_LABEL: &'static str = "Total Revenue"; /// Create a bar chart with the data from the `data` field. @@ -228,7 +228,7 @@ impl Revenues { } /// Create a `BarGroup` with vertical bars for each company - fn to_vertical_bar_group<'a>(&'a self, companies: &'a [Company]) -> BarGroup<'a> { + fn to_vertical_bar_group<'a>(&self, companies: &'a [Company]) -> BarGroup<'a> { let bars = izip!(companies, self.revenues) .map(|(company, revenue)| company.vertical_revenue_bar(revenue)) .collect_vec(); From 3b826e3aab5daed11a2e26f59fa75751b70307ae Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Wed, 29 May 2024 05:31:34 -0700 Subject: [PATCH 10/11] fix:Update examples/barchart.rs Co-authored-by: EdJoPaTo --- examples/barchart.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/barchart.rs b/examples/barchart.rs index 6fc2f2e19..e486da42d 100644 --- a/examples/barchart.rs +++ b/examples/barchart.rs @@ -18,7 +18,7 @@ use std::{ time::{Duration, Instant}, }; -use common::Terminal; +use self::common::Terminal; use crossterm::event::{self, Event, KeyCode}; use itertools::{izip, Itertools}; use rand::{thread_rng, Rng}; From 335ed142315b7801a8e136f7feeed158f35ab632 Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Wed, 29 May 2024 17:15:36 -0700 Subject: [PATCH 11/11] fix: lints --- examples/barchart.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/barchart.rs b/examples/barchart.rs index e486da42d..e64887c61 100644 --- a/examples/barchart.rs +++ b/examples/barchart.rs @@ -18,7 +18,6 @@ use std::{ time::{Duration, Instant}, }; -use self::common::Terminal; use crossterm::event::{self, Event, KeyCode}; use itertools::{izip, Itertools}; use rand::{thread_rng, Rng}; @@ -31,6 +30,8 @@ use ratatui::{ }; use unicode_width::UnicodeWidthStr; +use self::common::Terminal; + const COMPANY_COUNT: usize = 3; const PERIOD_COUNT: usize = 4; @@ -68,7 +69,7 @@ impl App { /// Create a new instance of the application fn new() -> Self { - App { + Self { exit: false, data: generate_main_barchart_data(), last_update: Instant::now(),