use color_eyre::eyre::Result;
use ratatui::buffer::Buffer;
use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind};
use ratatui::layout::{Alignment, Margin, Rect, Size};
use ratatui::symbols::scrollbar;
use ratatui::text::Text;
use ratatui::widgets::{
Block, BorderType, Padding, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, Widget,
Wrap,
};
use ratatui::{DefaultTerminal, Frame};
pub struct Viewer<'a>
{
exit: bool,
docs: Paragraph<'a>,
viewport_size: Size,
line_count: usize,
max_scroll: usize,
scroll_position: usize,
}
impl<'a> Viewer<'a>
{
pub fn display(title: &'a str, docs: &'a str) -> Result<()>
{
let mut terminal = ratatui::init();
let mut viewer = Self::new(title, docs, terminal.size()?);
let result = viewer.run(&mut terminal);
ratatui::restore();
result
}
fn new(title: &'a str, docs: &'a str, viewport_size: Size) -> Self
{
let docs = Paragraph::new(tui_markdown::from_str(docs))
.wrap(Wrap { trim: false })
.block(
Block::bordered()
.title(title)
.title_alignment(Alignment::Left)
.border_type(BorderType::Rounded)
.padding(Padding::horizontal(1)),
);
let line_count = docs.line_count(viewport_size.width);
Self {
exit: false,
docs,
viewport_size,
line_count,
max_scroll: line_count.saturating_sub(viewport_size.height.into()),
scroll_position: 0,
}
}
fn run(&mut self, terminal: &mut DefaultTerminal) -> Result<()>
{
while !self.exit {
terminal.draw(|frame| self.draw(frame))?;
self.handle_events()?;
}
Ok(())
}
fn draw(&mut self, frame: &mut Frame)
{
frame.render_widget(self, frame.area())
}
fn handle_events(&mut self) -> Result<()>
{
match event::read()? {
Event::Key(key) => {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q' | 'Q') => self.quit(),
KeyCode::Up => self.scroll_up(),
KeyCode::Down => self.scroll_down(),
KeyCode::PageUp => self.scroll_page_up(),
KeyCode::PageDown => self.scroll_page_down(),
_ => {},
}
}
},
Event::Resize(width, height) => self.handle_resize(width, height),
_ => {},
}
Ok(())
}
fn quit(&mut self)
{
self.exit = true
}
fn handle_resize(&mut self, width: u16, height: u16)
{
self.viewport_size = Size::new(width, height);
self.line_count = self.docs.line_count(width);
let max_scroll = self.line_count.saturating_sub(height.into());
if self.scroll_position > max_scroll {
self.scroll_position = max_scroll
}
self.max_scroll = max_scroll;
}
fn scroll_up(&mut self)
{
self.scroll_position = self.scroll_position.saturating_sub(1)
}
fn scroll_down(&mut self)
{
let new_position = self.scroll_position + 1;
if new_position <= self.max_scroll {
self.scroll_position = new_position;
}
}
fn scroll_page_up(&mut self)
{
self.scroll_position = self.scroll_position.saturating_sub(self.viewport_size.height.into())
}
fn scroll_page_down(&mut self)
{
let viewport_height: usize = self.viewport_size.height.into();
let new_position = self.scroll_position + viewport_height;
if new_position > self.max_scroll {
self.scroll_position = self.max_scroll;
} else {
self.scroll_position = new_position;
}
}
}
impl Widget for &mut Viewer<'_>
{
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
self.docs
.clone()
.scroll((self.scroll_position as u16, 0))
.render(area, buf);
let mut scroll_state = ScrollbarState::new(self.max_scroll).position(self.scroll_position);
StatefulWidget::render(
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.symbols(scrollbar::VERTICAL)
.begin_symbol(None)
.end_symbol(None),
area.inner(Margin::new(0, 1)),
buf,
&mut scroll_state,
);
Text::from(" ⋏⋎: scroll, ⊼⊻: scroll page, q: quit to menu ")
.centered()
.render(area.rows().next_back().unwrap(), buf);
}
}