mirror of
https://github.com/ratatui-org/ratatui
synced 2025-02-16 22:18:51 +00:00
Simplified a bunch of the logic in the demo2 example - Moved destroy mode to its own file. - Moved error handling to its own file. - Removed AppContext - Implemented Widget for &App. The app state is small enough that it doesn't matter here and we could just copy or clone the app state on every frame, but for larger apps this can be a significant performance improvement. - Made the tabs stateful - Made the term module just a collection of functions rather than a struct. - Changed to use color_eyre for error handling. - Changed keyboard shortcuts and rearranged the bottom bar. - Use strum for the tabs enum.
223 lines
6.6 KiB
Rust
223 lines
6.6 KiB
Rust
use std::time::Duration;
|
|
|
|
use color_eyre::{eyre::Context, Result};
|
|
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind};
|
|
use itertools::Itertools;
|
|
use ratatui::{prelude::*, widgets::*};
|
|
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
|
|
|
use crate::{destroy, tabs::*, term, THEME};
|
|
|
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
|
pub struct App {
|
|
mode: Mode,
|
|
tab: Tab,
|
|
about_tab: AboutTab,
|
|
recipe_tab: RecipeTab,
|
|
email_tab: EmailTab,
|
|
traceroute_tab: TracerouteTab,
|
|
weather_tab: WeatherTab,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
|
enum Mode {
|
|
#[default]
|
|
Running,
|
|
Destroy,
|
|
Quit,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Default, Display, EnumIter, FromRepr, PartialEq, Eq)]
|
|
enum Tab {
|
|
#[default]
|
|
About,
|
|
Recipe,
|
|
Email,
|
|
Traceroute,
|
|
Weather,
|
|
}
|
|
|
|
pub fn run(terminal: &mut Terminal<impl Backend>) -> Result<()> {
|
|
App::new().run(terminal)
|
|
}
|
|
|
|
impl App {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Run the app until the user quits.
|
|
pub fn run(&mut self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
|
|
while self.is_running() {
|
|
self.draw(terminal)?;
|
|
self.handle_events()?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn is_running(&self) -> bool {
|
|
self.mode != Mode::Quit
|
|
}
|
|
|
|
/// Draw a single frame of the app.
|
|
fn draw(&self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
|
|
terminal
|
|
.draw(|frame| {
|
|
frame.render_widget(self, frame.size());
|
|
if self.mode == Mode::Destroy {
|
|
destroy::destroy(frame);
|
|
}
|
|
})
|
|
.wrap_err("terminal.draw")?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Handle events from the terminal.
|
|
///
|
|
/// This function is called once per frame, The events are polled from the stdin with timeout of
|
|
/// 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),
|
|
_ => Ok(()),
|
|
}
|
|
}
|
|
|
|
fn handle_key_press(&mut self, key: KeyEvent) -> Result<()> {
|
|
use KeyCode::*;
|
|
match key.code {
|
|
Char('q') | Esc => self.mode = Mode::Quit,
|
|
Char('h') | Left => self.prev_tab(),
|
|
Char('l') | Right => self.next_tab(),
|
|
Char('k') | Up => self.prev(),
|
|
Char('j') | Down => self.next(),
|
|
Char('d') | Delete => self.destroy(),
|
|
|
|
_ => {}
|
|
};
|
|
Ok(())
|
|
}
|
|
|
|
fn prev(&mut self) {
|
|
match self.tab {
|
|
Tab::About => self.about_tab.prev_row(),
|
|
Tab::Recipe => self.recipe_tab.prev(),
|
|
Tab::Email => self.email_tab.prev(),
|
|
Tab::Traceroute => self.traceroute_tab.prev_row(),
|
|
Tab::Weather => self.weather_tab.prev(),
|
|
}
|
|
}
|
|
|
|
fn next(&mut self) {
|
|
match self.tab {
|
|
Tab::About => self.about_tab.next_row(),
|
|
Tab::Recipe => self.recipe_tab.next(),
|
|
Tab::Email => self.email_tab.next(),
|
|
Tab::Traceroute => self.traceroute_tab.next_row(),
|
|
Tab::Weather => self.weather_tab.next(),
|
|
}
|
|
}
|
|
|
|
fn prev_tab(&mut self) {
|
|
self.tab = self.tab.prev();
|
|
}
|
|
|
|
fn next_tab(&mut self) {
|
|
self.tab = self.tab.next()
|
|
}
|
|
|
|
fn destroy(&mut self) {
|
|
self.mode = Mode::Destroy
|
|
}
|
|
}
|
|
|
|
/// Implement Widget for &App rather than for App as we would otherwise have to clone or copy the
|
|
/// entire app state on every frame. For this example, the app state is small enough that it doesn't
|
|
/// matter, but for larger apps this can be a significant performance improvement.
|
|
impl Widget for &App {
|
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
|
let vertical = Layout::vertical([
|
|
Constraint::Length(1),
|
|
Constraint::Min(0),
|
|
Constraint::Length(1),
|
|
]);
|
|
let [title_bar, tab, bottom_bar] = area.split(&vertical);
|
|
|
|
Block::new().style(THEME.root).render(area, buf);
|
|
self.render_title_bar(title_bar, buf);
|
|
self.render_selected_tab(tab, buf);
|
|
self.render_bottom_bar(bottom_bar, buf);
|
|
}
|
|
}
|
|
|
|
impl App {
|
|
fn render_title_bar(&self, area: Rect, buf: &mut Buffer) {
|
|
let layout = Layout::horizontal([Constraint::Min(0), Constraint::Length(43)]);
|
|
let [title, tabs] = area.split(&layout);
|
|
|
|
Span::styled("Ratatui", THEME.app_title).render(title, buf);
|
|
let titles = Tab::iter().map(|tab| tab.title());
|
|
Tabs::new(titles)
|
|
.style(THEME.tabs)
|
|
.highlight_style(THEME.tabs_selected)
|
|
.select(self.tab as usize)
|
|
.divider("")
|
|
.padding("", "")
|
|
.render(tabs, buf);
|
|
}
|
|
|
|
fn render_selected_tab(&self, area: Rect, buf: &mut Buffer) {
|
|
match self.tab {
|
|
Tab::About => self.about_tab.render(area, buf),
|
|
Tab::Recipe => self.recipe_tab.render(area, buf),
|
|
Tab::Email => self.email_tab.render(area, buf),
|
|
Tab::Traceroute => self.traceroute_tab.render(area, buf),
|
|
Tab::Weather => self.weather_tab.render(area, buf),
|
|
};
|
|
}
|
|
|
|
fn render_bottom_bar(&self, area: Rect, buf: &mut Buffer) {
|
|
let keys = [
|
|
("H/←", "Left"),
|
|
("L/→", "Right"),
|
|
("K/↑", "Up"),
|
|
("J/↓", "Down"),
|
|
("D/Del", "Destroy"),
|
|
("Q/Esc", "Quit"),
|
|
];
|
|
let spans = keys
|
|
.iter()
|
|
.flat_map(|(key, desc)| {
|
|
let key = Span::styled(format!(" {} ", key), THEME.key_binding.key);
|
|
let desc = Span::styled(format!(" {} ", desc), THEME.key_binding.description);
|
|
[key, desc]
|
|
})
|
|
.collect_vec();
|
|
Line::from(spans)
|
|
.centered()
|
|
.style((Color::Indexed(236), Color::Indexed(232)))
|
|
.render(area, buf);
|
|
}
|
|
}
|
|
|
|
impl Tab {
|
|
fn next(&self) -> Self {
|
|
let current_index = *self as usize;
|
|
let next_index = current_index.saturating_add(1);
|
|
Self::from_repr(next_index).unwrap_or(*self)
|
|
}
|
|
|
|
fn prev(&self) -> Self {
|
|
let current_index = *self as usize;
|
|
let prev_index = current_index.saturating_sub(1);
|
|
Self::from_repr(prev_index).unwrap_or(*self)
|
|
}
|
|
|
|
fn title(&self) -> String {
|
|
match self {
|
|
Tab::About => "".to_string(),
|
|
tab => format!(" {} ", tab),
|
|
}
|
|
}
|
|
}
|