1use std::time::Duration;
17
18use color_eyre::Result;
19use ratatui::{
20 buffer::Buffer,
21 crossterm::event::{self, Event, KeyCode, KeyEventKind},
22 layout::{Alignment, Constraint, Layout, Rect},
23 style::{palette::tailwind, Color, Style, Stylize},
24 text::{Line, Span},
25 widgets::{Block, Borders, Gauge, Padding, Paragraph, Widget},
26 DefaultTerminal,
27};
28
29const GAUGE1_COLOR: Color = tailwind::RED.c800;
30const GAUGE2_COLOR: Color = tailwind::GREEN.c800;
31const GAUGE3_COLOR: Color = tailwind::BLUE.c800;
32const GAUGE4_COLOR: Color = tailwind::ORANGE.c800;
33const CUSTOM_LABEL_COLOR: Color = tailwind::SLATE.c200;
34
35#[derive(Debug, Default, Clone, Copy)]
36struct App {
37 state: AppState,
38 progress_columns: u16,
39 progress1: u16,
40 progress2: f64,
41 progress3: f64,
42 progress4: f64,
43}
44
45#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
46enum AppState {
47 #[default]
48 Running,
49 Started,
50 Quitting,
51}
52
53fn main() -> Result<()> {
54 color_eyre::install()?;
55 let terminal = ratatui::init();
56 let app_result = App::default().run(terminal);
57 ratatui::restore();
58 app_result
59}
60
61impl App {
62 fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
63 while self.state != AppState::Quitting {
64 terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
65 self.handle_events()?;
66 self.update(terminal.size()?.width);
67 }
68 Ok(())
69 }
70
71 fn update(&mut self, terminal_width: u16) {
72 if self.state != AppState::Started {
73 return;
74 }
75
76 self.progress_columns = (self.progress_columns + 1).clamp(0, terminal_width);
80 self.progress1 = self.progress_columns * 100 / terminal_width;
81 self.progress2 = f64::from(self.progress_columns) * 100.0 / f64::from(terminal_width);
82
83 self.progress3 = (self.progress3 + 0.1).clamp(40.0, 100.0);
86 self.progress4 = (self.progress4 + 0.1).clamp(40.0, 100.0);
87 }
88
89 fn handle_events(&mut self) -> Result<()> {
90 let timeout = Duration::from_secs_f32(1.0 / 20.0);
91 if event::poll(timeout)? {
92 if let Event::Key(key) = event::read()? {
93 if key.kind == KeyEventKind::Press {
94 match key.code {
95 KeyCode::Char(' ') | KeyCode::Enter => self.start(),
96 KeyCode::Char('q') | KeyCode::Esc => self.quit(),
97 _ => {}
98 }
99 }
100 }
101 }
102 Ok(())
103 }
104
105 fn start(&mut self) {
106 self.state = AppState::Started;
107 }
108
109 fn quit(&mut self) {
110 self.state = AppState::Quitting;
111 }
112}
113
114impl Widget for &App {
115 #[allow(clippy::similar_names)]
116 fn render(self, area: Rect, buf: &mut Buffer) {
117 use Constraint::{Length, Min, Ratio};
118 let layout = Layout::vertical([Length(2), Min(0), Length(1)]);
119 let [header_area, gauge_area, footer_area] = layout.areas(area);
120
121 let layout = Layout::vertical([Ratio(1, 4); 4]);
122 let [gauge1_area, gauge2_area, gauge3_area, gauge4_area] = layout.areas(gauge_area);
123
124 render_header(header_area, buf);
125 render_footer(footer_area, buf);
126
127 self.render_gauge1(gauge1_area, buf);
128 self.render_gauge2(gauge2_area, buf);
129 self.render_gauge3(gauge3_area, buf);
130 self.render_gauge4(gauge4_area, buf);
131 }
132}
133
134fn render_header(area: Rect, buf: &mut Buffer) {
135 Paragraph::new("Ratatui Gauge Example")
136 .bold()
137 .alignment(Alignment::Center)
138 .fg(CUSTOM_LABEL_COLOR)
139 .render(area, buf);
140}
141
142fn render_footer(area: Rect, buf: &mut Buffer) {
143 Paragraph::new("Press ENTER to start")
144 .alignment(Alignment::Center)
145 .fg(CUSTOM_LABEL_COLOR)
146 .bold()
147 .render(area, buf);
148}
149
150impl App {
151 fn render_gauge1(&self, area: Rect, buf: &mut Buffer) {
152 let title = title_block("Gauge with percentage");
153 Gauge::default()
154 .block(title)
155 .gauge_style(GAUGE1_COLOR)
156 .percent(self.progress1)
157 .render(area, buf);
158 }
159
160 fn render_gauge2(&self, area: Rect, buf: &mut Buffer) {
161 let title = title_block("Gauge with ratio and custom label");
162 let label = Span::styled(
163 format!("{:.1}/100", self.progress2),
164 Style::new().italic().bold().fg(CUSTOM_LABEL_COLOR),
165 );
166 Gauge::default()
167 .block(title)
168 .gauge_style(GAUGE2_COLOR)
169 .ratio(self.progress2 / 100.0)
170 .label(label)
171 .render(area, buf);
172 }
173
174 fn render_gauge3(&self, area: Rect, buf: &mut Buffer) {
175 let title = title_block("Gauge with ratio (no unicode)");
176 let label = format!("{:.1}%", self.progress3);
177 Gauge::default()
178 .block(title)
179 .gauge_style(GAUGE3_COLOR)
180 .ratio(self.progress3 / 100.0)
181 .label(label)
182 .render(area, buf);
183 }
184
185 fn render_gauge4(&self, area: Rect, buf: &mut Buffer) {
186 let title = title_block("Gauge with ratio (unicode)");
187 let label = format!("{:.1}%", self.progress3);
188 Gauge::default()
189 .block(title)
190 .gauge_style(GAUGE4_COLOR)
191 .ratio(self.progress4 / 100.0)
192 .label(label)
193 .use_unicode(true)
194 .render(area, buf);
195 }
196}
197
198fn title_block(title: &str) -> Block {
199 let title = Line::from(title).centered();
200 Block::new()
201 .borders(Borders::NONE)
202 .padding(Padding::vertical(1))
203 .title(title)
204 .fg(CUSTOM_LABEL_COLOR)
205}