thag_rs 0.2.1

A versatile cross-platform playground and REPL for Rust snippets, expressions and programs. Accepts a script file or dynamic options.
Documentation
/// Published example from `tui-scrollview` crate. Toml entries from crate's Cargo.toml.
///
/// Not suitable for running from a URL.
//# Purpose: Explore TUI editing
//# Categories: crates, exploration, technique, tui
use std::io::{self, stdout};

use color_eyre::{config::HookBuilder, Result};
use ratatui::{
    crossterm::{
        event::{self, Event, KeyCode, KeyEventKind},
        terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
        ExecutableCommand,
    },
    layout::Size,
    prelude::*,
    style::palette::tailwind,
    widgets::*,
};
use tui_scrollview::{ScrollView, ScrollViewState};

fn main() -> Result<()> {
    init_error_hooks()?;
    let terminal = init_terminal()?;
    App::new().run(terminal)?;
    restore_terminal()?;
    Ok(())
}

#[derive(Debug, Default, Clone)]
struct App {
    text: [String; 3],
    scroll_view_state: ScrollViewState,
    state: AppState,
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
enum AppState {
    #[default]
    Running,
    Quit,
}

impl App {
    fn new() -> Self {
        Self {
            text: [
                lipsum::lipsum(10_000),
                lipsum::lipsum(10_000),
                lipsum::lipsum(10_000),
            ],
            ..Default::default()
        }
    }

    fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
        self.draw(&mut terminal)?;
        while self.is_running() {
            self.handle_events()?;
            self.draw(&mut terminal)?;
        }
        Ok(())
    }

    fn is_running(&self) -> bool {
        self.state == AppState::Running
    }

    fn draw(&mut self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
        terminal.draw(|frame| frame.render_widget(self, frame.area()))?;
        Ok(())
    }

    fn handle_events(&mut self) -> Result<()> {
        use KeyCode::*;
        match event::read()? {
            Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
                Char('q') | Esc => self.quit(),
                Char('j') | Down => self.scroll_view_state.scroll_down(),
                Char('k') | Up => self.scroll_view_state.scroll_up(),
                Char('f') | PageDown => self.scroll_view_state.scroll_page_down(),
                Char('b') | PageUp => self.scroll_view_state.scroll_page_up(),
                Char('g') | Home => self.scroll_view_state.scroll_to_top(),
                Char('G') | End => self.scroll_view_state.scroll_to_bottom(),
                _ => (),
            },
            _ => {}
        }
        Ok(())
    }

    fn quit(&mut self) {
        self.state = AppState::Quit;
    }
}

const SCROLLVIEW_HEIGHT: u16 = 100;

impl Widget for &mut App {
    fn render(self, area: Rect, buf: &mut Buffer) {
        let layout = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]);
        let [title, body] = layout.areas(area);

        self.title().render(title, buf);
        let width = if buf.area.height < SCROLLVIEW_HEIGHT {
            buf.area.width - 1
        } else {
            buf.area.width
        };
        let mut scroll_view = ScrollView::new(Size::new(width, SCROLLVIEW_HEIGHT));
        self.render_widgets_into_scrollview(scroll_view.buf_mut());
        scroll_view.render(body, buf, &mut self.scroll_view_state)
    }
}

impl App {
    fn title(&self) -> impl Widget {
        let palette = tailwind::SLATE;
        let fg = palette.c900;
        let bg = palette.c300;
        let keys_fg = palette.c50;
        let keys_bg = palette.c600;
        Line::from(vec![
            "Tui-scrollview  ".into(),
            "  ↓ | ↑ | PageDown | PageUp | Home | End  "
                .fg(keys_fg)
                .bg(keys_bg),
            "  Quit: ".into(),
            " Esc ".fg(keys_fg).bg(keys_bg),
        ])
        .style((fg, bg))
    }

    fn render_widgets_into_scrollview(&self, buf: &mut Buffer) {
        use Constraint::*;
        let area = buf.area;
        let [numbers, widgets] = Layout::horizontal([Length(5), Fill(1)]).areas(area);
        let [bar_charts, text_0, text_1, text_2] =
            Layout::vertical([Length(7), Fill(1), Fill(2), Fill(4)]).areas(widgets);
        let [left_bar, right_bar] = Layout::horizontal([Length(20), Fill(1)]).areas(bar_charts);

        self.line_numbers(area.height).render(numbers, buf);
        self.vertical_bar_chart().render(left_bar, buf);
        self.horizontal_bar_chart().render(right_bar, buf);
        self.text(0).render(text_0, buf);
        self.text(1).render(text_1, buf);
        self.text(2).render(text_2, buf);
    }

    fn line_numbers(&self, height: u16) -> impl Widget {
        use std::fmt::Write;
        let line_numbers = (1..=height).fold(String::new(), |mut output, n| {
            let _ = writeln!(output, "{n:>4} ");
            output
        });
        Text::from(line_numbers).dim()
    }

    fn vertical_bar_chart(&self) -> impl Widget {
        let block = Block::bordered().title("Vertical Bar Chart");
        BarChart::default()
            .direction(Direction::Vertical)
            .block(block)
            .bar_width(5)
            .bar_gap(1)
            .data(bars())
    }

    fn horizontal_bar_chart(&self) -> impl Widget {
        let block = Block::bordered().title("Horizontal Bar Chart");
        BarChart::default()
            .direction(Direction::Horizontal)
            .block(block)
            .bar_width(1)
            .bar_gap(1)
            .data(bars())
    }

    fn text(&self, index: usize) -> impl Widget {
        let block = Block::bordered().title(format!("Text {}", index));
        Paragraph::new(self.text[index].clone())
            .wrap(Wrap { trim: false })
            .block(block)
    }
}

const CHART_DATA: [(&str, u64, Color); 3] = [
    ("Red", 2, Color::Red),
    ("Green", 7, Color::Green),
    ("Blue", 11, Color::Blue),
];

fn bars() -> BarGroup<'static> {
    let data = CHART_DATA
        .map(|(label, value, color)| Bar::default().label(label.into()).value(value).style(color));
    BarGroup::default().bars(&data)
}

fn init_error_hooks() -> Result<()> {
    let (panic, error) = HookBuilder::default().into_hooks();
    let panic = panic.into_panic_hook();
    let error = error.into_eyre_hook();
    color_eyre::eyre::set_hook(Box::new(move |e| {
        let _ = restore_terminal();
        error(e)
    }))?;
    std::panic::set_hook(Box::new(move |info| {
        let _ = restore_terminal();
        panic(info)
    }));
    Ok(())
}

fn init_terminal() -> Result<Terminal<impl Backend>> {
    enable_raw_mode()?;
    stdout().execute(EnterAlternateScreen)?;
    let backend = CrosstermBackend::new(stdout());
    let terminal = Terminal::new(backend)?;
    Ok(terminal)
}

fn restore_terminal() -> Result<()> {
    disable_raw_mode()?;
    stdout().execute(LeaveAlternateScreen)?;
    Ok(())
}