mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-29 16:10:34 +00:00
feat(terminal): Add ratatui::init() and restore() methods (#1289)
These are simple opinionated methods for creating a terminal that is useful to use in most apps. The new init method creates a crossterm backend writing to stdout, enables raw mode, enters the alternate screen, and sets a panic handler that restores the terminal on panic. A minimal hello world now looks a bit like: ```rust use ratatui::{ crossterm::event::{self, Event}, text::Text, Frame, }; fn main() { let mut terminal = ratatui::init(); loop { terminal .draw(|frame: &mut Frame| frame.render_widget(Text::raw("Hello World!"), frame.area())) .expect("Failed to draw"); if matches!(event::read().expect("failed to read event"), Event::Key(_)) { break; } } ratatui::restore(); } ``` A type alias `DefaultTerminal` is added to represent this terminal type and to simplify any cases where applications need to pass this terminal around. It is equivalent to: `Terminal<CrosstermBackend<Stdout>>` We also added `ratatui::try_init()` and `try_restore()`, for situations where you might want to handle initialization errors yourself instead of letting the panic handler fire and cleanup. Simple Apps should prefer the `init` and `restore` functions over these functions. Corresponding functions to allow passing a `TerminalOptions` with a `Viewport` (e.g. inline, fixed) are also available (`init_with_options`, and `try_init_with_options`). The existing code to create a backend and terminal will remain and is not deprecated by this approach. This just provides a simple one line initialization using the common options. --------- Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
This commit is contained in:
parent
23516bce76
commit
ed51c4b342
45 changed files with 1485 additions and 2511 deletions
|
@ -295,7 +295,7 @@ doc-scrape-examples = true
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "hyperlink"
|
name = "hyperlink"
|
||||||
required-features = ["crossterm", "unstable-widget-ref"]
|
required-features = ["crossterm"]
|
||||||
doc-scrape-examples = true
|
doc-scrape-examples = true
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
|
|
|
@ -42,24 +42,21 @@ use octocrab::{
|
||||||
};
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
crossterm::event::{Event, EventStream, KeyCode},
|
crossterm::event::{Event, EventStream, KeyCode, KeyEventKind},
|
||||||
layout::{Constraint, Offset, Rect},
|
layout::{Constraint, Layout, Rect},
|
||||||
style::{Modifier, Stylize},
|
style::{Style, Stylize},
|
||||||
text::Line,
|
text::Line,
|
||||||
widgets::{
|
widgets::{Block, HighlightSpacing, Row, StatefulWidget, Table, TableState, Widget},
|
||||||
Block, BorderType, HighlightSpacing, Row, StatefulWidget, Table, TableState, Widget,
|
DefaultTerminal, Frame,
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::terminal::Terminal;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
color_eyre::install()?;
|
color_eyre::install()?;
|
||||||
init_octocrab()?;
|
init_octocrab()?;
|
||||||
let terminal = terminal::init()?;
|
let terminal = ratatui::init();
|
||||||
let app_result = App::default().run(terminal).await;
|
let app_result = App::default().run(terminal).await;
|
||||||
terminal::restore();
|
ratatui::restore();
|
||||||
app_result
|
app_result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,69 +75,66 @@ fn init_octocrab() -> Result<()> {
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
struct App {
|
struct App {
|
||||||
should_quit: bool,
|
should_quit: bool,
|
||||||
pulls: PullRequestsWidget,
|
pull_requests: PullRequestListWidget,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
const FRAMES_PER_SECOND: f32 = 60.0;
|
const FRAMES_PER_SECOND: f32 = 60.0;
|
||||||
|
|
||||||
pub async fn run(mut self, mut terminal: Terminal) -> Result<()> {
|
pub async fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
self.pulls.run();
|
self.pull_requests.run();
|
||||||
|
|
||||||
let mut interval =
|
let period = Duration::from_secs_f32(1.0 / Self::FRAMES_PER_SECOND);
|
||||||
tokio::time::interval(Duration::from_secs_f32(1.0 / Self::FRAMES_PER_SECOND));
|
let mut interval = tokio::time::interval(period);
|
||||||
let mut events = EventStream::new();
|
let mut events = EventStream::new();
|
||||||
|
|
||||||
while !self.should_quit {
|
while !self.should_quit {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = interval.tick() => self.draw(&mut terminal)?,
|
_ = interval.tick() => { terminal.draw(|frame| self.draw(frame))?; },
|
||||||
Some(Ok(event)) = events.next() => self.handle_event(&event),
|
Some(Ok(event)) = events.next() => self.handle_event(&event),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(&self, terminal: &mut Terminal) -> Result<()> {
|
fn draw(&self, frame: &mut Frame) {
|
||||||
terminal.draw(|frame| {
|
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]);
|
||||||
let area = frame.area();
|
let [title_area, body_area] = vertical.areas(frame.area());
|
||||||
frame.render_widget(
|
let title = Line::from("Ratatui async example").centered().bold();
|
||||||
Line::from("ratatui async example").centered().cyan().bold(),
|
frame.render_widget(title, title_area);
|
||||||
area,
|
frame.render_widget(&self.pull_requests, body_area);
|
||||||
);
|
|
||||||
let area = area.offset(Offset { x: 0, y: 1 }).intersection(area);
|
|
||||||
frame.render_widget(&self.pulls, area);
|
|
||||||
})?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_event(&mut self, event: &Event) {
|
fn handle_event(&mut self, event: &Event) {
|
||||||
if let Event::Key(event) = event {
|
if let Event::Key(key) = event {
|
||||||
match event.code {
|
if key.kind == KeyEventKind::Press {
|
||||||
KeyCode::Char('q') => self.should_quit = true,
|
match key.code {
|
||||||
KeyCode::Char('j') => self.pulls.scroll_down(),
|
KeyCode::Char('q') | KeyCode::Esc => self.should_quit = true,
|
||||||
KeyCode::Char('k') => self.pulls.scroll_up(),
|
KeyCode::Char('j') | KeyCode::Down => self.pull_requests.scroll_down(),
|
||||||
|
KeyCode::Char('k') | KeyCode::Up => self.pull_requests.scroll_up(),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A widget that displays a list of pull requests.
|
/// A widget that displays a list of pull requests.
|
||||||
///
|
///
|
||||||
/// This is an async widget that fetches the list of pull requests from the GitHub API. It contains
|
/// This is an async widget that fetches the list of pull requests from the GitHub API. It contains
|
||||||
/// an inner `Arc<RwLock<PullRequests>>` that holds the state of the widget. Cloning the widget
|
/// an inner `Arc<RwLock<PullRequestListState>>` that holds the state of the widget. Cloning the
|
||||||
/// will clone the Arc, so you can pass it around to other threads, and this is used to spawn a
|
/// widget will clone the Arc, so you can pass it around to other threads, and this is used to spawn
|
||||||
/// background task to fetch the pull requests.
|
/// a background task to fetch the pull requests.
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
struct PullRequestsWidget {
|
struct PullRequestListWidget {
|
||||||
inner: Arc<RwLock<PullRequests>>,
|
state: Arc<RwLock<PullRequestListState>>,
|
||||||
selected_index: usize, // no need to lock this since it's only accessed by the main thread
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
struct PullRequests {
|
struct PullRequestListState {
|
||||||
pulls: Vec<PullRequest>,
|
pull_requests: Vec<PullRequest>,
|
||||||
loading_state: LoadingState,
|
loading_state: LoadingState,
|
||||||
|
table_state: TableState,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -159,7 +153,7 @@ enum LoadingState {
|
||||||
Error(String),
|
Error(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PullRequestsWidget {
|
impl PullRequestListWidget {
|
||||||
/// Start fetching the pull requests in the background.
|
/// Start fetching the pull requests in the background.
|
||||||
///
|
///
|
||||||
/// This method spawns a background task that fetches the pull requests from the GitHub API.
|
/// This method spawns a background task that fetches the pull requests from the GitHub API.
|
||||||
|
@ -187,9 +181,12 @@ impl PullRequestsWidget {
|
||||||
}
|
}
|
||||||
fn on_load(&self, page: &Page<OctoPullRequest>) {
|
fn on_load(&self, page: &Page<OctoPullRequest>) {
|
||||||
let prs = page.items.iter().map(Into::into);
|
let prs = page.items.iter().map(Into::into);
|
||||||
let mut inner = self.inner.write().unwrap();
|
let mut state = self.state.write().unwrap();
|
||||||
inner.loading_state = LoadingState::Loaded;
|
state.loading_state = LoadingState::Loaded;
|
||||||
inner.pulls.extend(prs);
|
state.pull_requests.extend(prs);
|
||||||
|
if !state.pull_requests.is_empty() {
|
||||||
|
state.table_state.select(Some(0));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_err(&self, err: &octocrab::Error) {
|
fn on_err(&self, err: &octocrab::Error) {
|
||||||
|
@ -197,15 +194,15 @@ impl PullRequestsWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_loading_state(&self, state: LoadingState) {
|
fn set_loading_state(&self, state: LoadingState) {
|
||||||
self.inner.write().unwrap().loading_state = state;
|
self.state.write().unwrap().loading_state = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scroll_down(&mut self) {
|
fn scroll_down(&self) {
|
||||||
self.selected_index = self.selected_index.saturating_add(1);
|
self.state.write().unwrap().table_state.scroll_down_by(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scroll_up(&mut self) {
|
fn scroll_up(&self) {
|
||||||
self.selected_index = self.selected_index.saturating_sub(1);
|
self.state.write().unwrap().table_state.scroll_up_by(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,19 +222,19 @@ impl From<&OctoPullRequest> for PullRequest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for &PullRequestsWidget {
|
impl Widget for &PullRequestListWidget {
|
||||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
let inner = self.inner.read().unwrap();
|
let mut state = self.state.write().unwrap();
|
||||||
|
|
||||||
// a block with a right aligned title with the loading state
|
// a block with a right aligned title with the loading state on the right
|
||||||
let loading_state = Line::from(format!("{:?}", inner.loading_state)).right_aligned();
|
let loading_state = Line::from(format!("{:?}", state.loading_state)).right_aligned();
|
||||||
let block = Block::bordered()
|
let block = Block::bordered()
|
||||||
.border_type(BorderType::Rounded)
|
|
||||||
.title("Pull Requests")
|
.title("Pull Requests")
|
||||||
.title(loading_state);
|
.title(loading_state)
|
||||||
|
.title_bottom("j/k to scroll, q to quit");
|
||||||
|
|
||||||
// a table with the list of pull requests
|
// a table with the list of pull requests
|
||||||
let rows = inner.pulls.iter();
|
let rows = state.pull_requests.iter();
|
||||||
let widths = [
|
let widths = [
|
||||||
Constraint::Length(5),
|
Constraint::Length(5),
|
||||||
Constraint::Fill(1),
|
Constraint::Fill(1),
|
||||||
|
@ -247,10 +244,9 @@ impl Widget for &PullRequestsWidget {
|
||||||
.block(block)
|
.block(block)
|
||||||
.highlight_spacing(HighlightSpacing::Always)
|
.highlight_spacing(HighlightSpacing::Always)
|
||||||
.highlight_symbol(">>")
|
.highlight_symbol(">>")
|
||||||
.highlight_style(Modifier::REVERSED);
|
.highlight_style(Style::new().on_blue());
|
||||||
let mut table_state = TableState::new().with_selected(self.selected_index);
|
|
||||||
|
|
||||||
StatefulWidget::render(table, area, buf, &mut table_state);
|
StatefulWidget::render(table, area, buf, &mut state.table_state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -260,46 +256,3 @@ impl From<&PullRequest> for Row<'_> {
|
||||||
Row::new(vec![pr.id, pr.title, pr.url])
|
Row::new(vec![pr.id, pr.title, pr.url])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mod terminal {
|
|
||||||
use std::io;
|
|
||||||
|
|
||||||
use ratatui::{
|
|
||||||
backend::CrosstermBackend,
|
|
||||||
crossterm::{
|
|
||||||
execute,
|
|
||||||
terminal::{
|
|
||||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A type alias for the terminal type used in this example.
|
|
||||||
pub type Terminal = ratatui::Terminal<CrosstermBackend<io::Stdout>>;
|
|
||||||
|
|
||||||
pub fn init() -> io::Result<Terminal> {
|
|
||||||
set_panic_hook();
|
|
||||||
enable_raw_mode()?;
|
|
||||||
execute!(io::stdout(), EnterAlternateScreen)?;
|
|
||||||
let backend = CrosstermBackend::new(io::stdout());
|
|
||||||
Terminal::new(backend)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_panic_hook() {
|
|
||||||
let hook = std::panic::take_hook();
|
|
||||||
std::panic::set_hook(Box::new(move |info| {
|
|
||||||
restore();
|
|
||||||
hook(info);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Restores the terminal to its original state.
|
|
||||||
pub fn restore() {
|
|
||||||
if let Err(err) = disable_raw_mode() {
|
|
||||||
eprintln!("error disabling raw mode: {err}");
|
|
||||||
}
|
|
||||||
if let Err(err) = execute!(io::stdout(), LeaveAlternateScreen) {
|
|
||||||
eprintln!("error leaving alternate screen: {err}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -17,15 +17,21 @@ use std::iter::zip;
|
||||||
|
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
crossterm::event::{self, Event, KeyCode},
|
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||||
layout::{Constraint, Direction, Layout},
|
layout::{Constraint, Direction, Layout},
|
||||||
style::{Color, Style, Stylize},
|
style::{Color, Style, Stylize},
|
||||||
text::Line,
|
text::Line,
|
||||||
widgets::{Bar, BarChart, BarGroup, Block},
|
widgets::{Bar, BarChart, BarGroup, Block},
|
||||||
Frame,
|
DefaultTerminal, Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::terminal::Terminal;
|
fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
let terminal = ratatui::init();
|
||||||
|
let app_result = App::new().run(terminal);
|
||||||
|
ratatui::restore();
|
||||||
|
app_result
|
||||||
|
}
|
||||||
|
|
||||||
const COMPANY_COUNT: usize = 3;
|
const COMPANY_COUNT: usize = 3;
|
||||||
const PERIOD_COUNT: usize = 4;
|
const PERIOD_COUNT: usize = 4;
|
||||||
|
@ -47,15 +53,6 @@ struct Company {
|
||||||
color: Color,
|
color: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
|
||||||
color_eyre::install()?;
|
|
||||||
let mut terminal = terminal::init()?;
|
|
||||||
let app = App::new();
|
|
||||||
app.run(&mut terminal)?;
|
|
||||||
terminal::restore()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
const fn new() -> Self {
|
const fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -65,33 +62,27 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(mut self, terminal: &mut Terminal) -> Result<()> {
|
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
while !self.should_exit {
|
while !self.should_exit {
|
||||||
self.draw(terminal)?;
|
terminal.draw(|frame| self.draw(frame))?;
|
||||||
self.handle_events()?;
|
self.handle_events()?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(&self, terminal: &mut Terminal) -> Result<()> {
|
|
||||||
terminal.draw(|frame| self.render(frame))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_events(&mut self) -> Result<()> {
|
fn handle_events(&mut self) -> Result<()> {
|
||||||
if let Event::Key(key) = event::read()? {
|
if let Event::Key(key) = event::read()? {
|
||||||
if key.code == KeyCode::Char('q') {
|
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||||
self.should_exit = true;
|
self.should_exit = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(&self, frame: &mut Frame) {
|
fn draw(&self, frame: &mut Frame) {
|
||||||
use Constraint::{Fill, Length, Min};
|
use Constraint::{Fill, Length, Min};
|
||||||
let [title, top, bottom] = Layout::vertical([Length(1), Fill(1), Min(20)])
|
let vertical = Layout::vertical([Length(1), Fill(1), Min(20)]).spacing(1);
|
||||||
.spacing(1)
|
let [title, top, bottom] = vertical.areas(frame.area());
|
||||||
.areas(frame.area());
|
|
||||||
|
|
||||||
frame.render_widget("Grouped Barchart".bold().into_centered_line(), title);
|
frame.render_widget("Grouped Barchart".bold().into_centered_line(), title);
|
||||||
frame.render_widget(self.vertical_revenue_barchart(), top);
|
frame.render_widget(self.vertical_revenue_barchart(), top);
|
||||||
|
@ -100,12 +91,12 @@ impl App {
|
||||||
|
|
||||||
/// Create a vertical revenue bar chart with the data from the `revenues` field.
|
/// Create a vertical revenue bar chart with the data from the `revenues` field.
|
||||||
fn vertical_revenue_barchart(&self) -> BarChart<'_> {
|
fn vertical_revenue_barchart(&self) -> BarChart<'_> {
|
||||||
let title = Line::from("Company revenues (Vertical)").centered();
|
|
||||||
let mut barchart = BarChart::default()
|
let mut barchart = BarChart::default()
|
||||||
.block(Block::new().title(title))
|
.block(Block::new().title(Line::from("Company revenues (Vertical)").centered()))
|
||||||
.bar_gap(0)
|
.bar_gap(0)
|
||||||
.bar_width(6)
|
.bar_width(6)
|
||||||
.group_gap(2);
|
.group_gap(2);
|
||||||
|
|
||||||
for group in self
|
for group in self
|
||||||
.revenues
|
.revenues
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -218,63 +209,3 @@ impl Company {
|
||||||
.value_style(Style::new().fg(Color::Black).bg(self.color))
|
.value_style(Style::new().fg(Color::Black).bg(self.color))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Contains functions common to all examples
|
|
||||||
mod terminal {
|
|
||||||
use std::{
|
|
||||||
io::{self, stdout, Stdout},
|
|
||||||
panic,
|
|
||||||
};
|
|
||||||
|
|
||||||
use ratatui::{
|
|
||||||
backend::CrosstermBackend,
|
|
||||||
crossterm::{
|
|
||||||
execute,
|
|
||||||
terminal::{
|
|
||||||
disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen,
|
|
||||||
LeaveAlternateScreen,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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<CrosstermBackend<Stdout>>;
|
|
||||||
|
|
||||||
/// 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() -> io::Result<Terminal> {
|
|
||||||
install_panic_hook();
|
|
||||||
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.
|
|
||||||
///
|
|
||||||
/// This function should be called before the program exits to ensure that the terminal is
|
|
||||||
/// restored to its original state.
|
|
||||||
pub fn restore() -> io::Result<()> {
|
|
||||||
disable_raw_mode()?;
|
|
||||||
execute!(
|
|
||||||
stdout(),
|
|
||||||
LeaveAlternateScreen,
|
|
||||||
Clear(ClearType::FromCursorDown),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Install a panic hook that restores the terminal before printing the panic.
|
|
||||||
///
|
|
||||||
/// This prevents error messages from being messed up by the terminal state.
|
|
||||||
fn install_panic_hook() {
|
|
||||||
let panic_hook = panic::take_hook();
|
|
||||||
panic::set_hook(Box::new(move |panic_info| {
|
|
||||||
let _ = restore();
|
|
||||||
panic_hook(panic_info);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -16,29 +16,27 @@
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use rand::{thread_rng, Rng};
|
use rand::{thread_rng, Rng};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
crossterm::event::{self, Event, KeyCode},
|
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||||
layout::{Constraint, Direction, Layout},
|
layout::{Constraint, Direction, Layout},
|
||||||
style::{Color, Style, Stylize},
|
style::{Color, Style, Stylize},
|
||||||
text::Line,
|
text::Line,
|
||||||
widgets::{Bar, BarChart, BarGroup, Block},
|
widgets::{Bar, BarChart, BarGroup, Block},
|
||||||
|
DefaultTerminal, Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::terminal::Terminal;
|
fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
let terminal = ratatui::init();
|
||||||
|
let app_result = App::new().run(terminal);
|
||||||
|
ratatui::restore();
|
||||||
|
app_result
|
||||||
|
}
|
||||||
|
|
||||||
struct App {
|
struct App {
|
||||||
should_exit: bool,
|
should_exit: bool,
|
||||||
temperatures: Vec<u8>,
|
temperatures: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
|
||||||
color_eyre::install()?;
|
|
||||||
let mut terminal = terminal::init()?;
|
|
||||||
let app = App::new();
|
|
||||||
app.run(&mut terminal)?;
|
|
||||||
terminal::restore()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
let mut rng = thread_rng();
|
let mut rng = thread_rng();
|
||||||
|
@ -49,29 +47,24 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(mut self, terminal: &mut Terminal) -> Result<()> {
|
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
while !self.should_exit {
|
while !self.should_exit {
|
||||||
self.draw(terminal)?;
|
terminal.draw(|frame| self.draw(frame))?;
|
||||||
self.handle_events()?;
|
self.handle_events()?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(&self, terminal: &mut Terminal) -> Result<()> {
|
|
||||||
terminal.draw(|frame| self.render(frame))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_events(&mut self) -> Result<()> {
|
fn handle_events(&mut self) -> Result<()> {
|
||||||
if let Event::Key(key) = event::read()? {
|
if let Event::Key(key) = event::read()? {
|
||||||
if key.code == KeyCode::Char('q') {
|
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||||
self.should_exit = true;
|
self.should_exit = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(&self, frame: &mut ratatui::Frame) {
|
fn draw(&self, frame: &mut Frame) {
|
||||||
let [title, vertical, horizontal] = Layout::vertical([
|
let [title, vertical, horizontal] = Layout::vertical([
|
||||||
Constraint::Length(1),
|
Constraint::Length(1),
|
||||||
Constraint::Fill(1),
|
Constraint::Fill(1),
|
||||||
|
@ -79,6 +72,7 @@ impl App {
|
||||||
])
|
])
|
||||||
.spacing(1)
|
.spacing(1)
|
||||||
.areas(frame.area());
|
.areas(frame.area());
|
||||||
|
|
||||||
frame.render_widget("Barchart".bold().into_centered_line(), title);
|
frame.render_widget("Barchart".bold().into_centered_line(), title);
|
||||||
frame.render_widget(vertical_barchart(&self.temperatures), vertical);
|
frame.render_widget(vertical_barchart(&self.temperatures), vertical);
|
||||||
frame.render_widget(horizontal_barchart(&self.temperatures), horizontal);
|
frame.render_widget(horizontal_barchart(&self.temperatures), horizontal);
|
||||||
|
@ -89,16 +83,8 @@ impl App {
|
||||||
fn vertical_barchart(temperatures: &[u8]) -> BarChart {
|
fn vertical_barchart(temperatures: &[u8]) -> BarChart {
|
||||||
let bars: Vec<Bar> = temperatures
|
let bars: Vec<Bar> = temperatures
|
||||||
.iter()
|
.iter()
|
||||||
.map(|v| u64::from(*v))
|
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, value)| {
|
.map(|(hour, value)| vertical_bar(hour, value))
|
||||||
Bar::default()
|
|
||||||
.value(value)
|
|
||||||
.label(Line::from(format!("{i:>02}:00")))
|
|
||||||
.text_value(format!("{value:>3}°"))
|
|
||||||
.style(temperature_style(value))
|
|
||||||
.value_style(temperature_style(value).reversed())
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
let title = Line::from("Weather (Vertical)").centered();
|
let title = Line::from("Weather (Vertical)").centered();
|
||||||
BarChart::default()
|
BarChart::default()
|
||||||
|
@ -107,21 +93,21 @@ fn vertical_barchart(temperatures: &[u8]) -> BarChart {
|
||||||
.bar_width(5)
|
.bar_width(5)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn vertical_bar(hour: usize, temperature: &u8) -> Bar {
|
||||||
|
Bar::default()
|
||||||
|
.value(u64::from(*temperature))
|
||||||
|
.label(Line::from(format!("{hour:>02}:00")))
|
||||||
|
.text_value(format!("{temperature:>3}°"))
|
||||||
|
.style(temperature_style(*temperature))
|
||||||
|
.value_style(temperature_style(*temperature).reversed())
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a horizontal bar chart from the temperatures data.
|
/// Create a horizontal bar chart from the temperatures data.
|
||||||
fn horizontal_barchart(temperatures: &[u8]) -> BarChart {
|
fn horizontal_barchart(temperatures: &[u8]) -> BarChart {
|
||||||
let bars: Vec<Bar> = temperatures
|
let bars: Vec<Bar> = temperatures
|
||||||
.iter()
|
.iter()
|
||||||
.map(|v| u64::from(*v))
|
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, value)| {
|
.map(|(hour, value)| horizontal_bar(hour, value))
|
||||||
let style = temperature_style(value);
|
|
||||||
Bar::default()
|
|
||||||
.value(value)
|
|
||||||
.label(Line::from(format!("{i:>02}:00")))
|
|
||||||
.text_value(format!("{value:>3}°"))
|
|
||||||
.style(style)
|
|
||||||
.value_style(style.reversed())
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
let title = Line::from("Weather (Horizontal)").centered();
|
let title = Line::from("Weather (Horizontal)").centered();
|
||||||
BarChart::default()
|
BarChart::default()
|
||||||
|
@ -132,69 +118,19 @@ fn horizontal_barchart(temperatures: &[u8]) -> BarChart {
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn horizontal_bar(hour: usize, temperature: &u8) -> Bar {
|
||||||
|
let style = temperature_style(*temperature);
|
||||||
|
Bar::default()
|
||||||
|
.value(u64::from(*temperature))
|
||||||
|
.label(Line::from(format!("{hour:>02}:00")))
|
||||||
|
.text_value(format!("{temperature:>3}°"))
|
||||||
|
.style(style)
|
||||||
|
.value_style(style.reversed())
|
||||||
|
}
|
||||||
|
|
||||||
/// create a yellow to red value based on the value (50-90)
|
/// create a yellow to red value based on the value (50-90)
|
||||||
fn temperature_style(value: u64) -> Style {
|
fn temperature_style(value: u8) -> Style {
|
||||||
let green = (255.0 * (1.0 - (value - 50) as f64 / 40.0)) as u8;
|
let green = (255.0 * (1.0 - f64::from(value - 50) / 40.0)) as u8;
|
||||||
let color = Color::Rgb(255, green, 0);
|
let color = Color::Rgb(255, green, 0);
|
||||||
Style::new().fg(color)
|
Style::new().fg(color)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Contains functions common to all examples
|
|
||||||
mod terminal {
|
|
||||||
use std::{
|
|
||||||
io::{self, stdout, Stdout},
|
|
||||||
panic,
|
|
||||||
};
|
|
||||||
|
|
||||||
use ratatui::{
|
|
||||||
backend::CrosstermBackend,
|
|
||||||
crossterm::{
|
|
||||||
execute,
|
|
||||||
terminal::{
|
|
||||||
disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen,
|
|
||||||
LeaveAlternateScreen,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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<CrosstermBackend<Stdout>>;
|
|
||||||
|
|
||||||
/// 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() -> io::Result<Terminal> {
|
|
||||||
install_panic_hook();
|
|
||||||
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.
|
|
||||||
///
|
|
||||||
/// This function should be called before the program exits to ensure that the terminal is
|
|
||||||
/// restored to its original state.
|
|
||||||
pub fn restore() -> io::Result<()> {
|
|
||||||
disable_raw_mode()?;
|
|
||||||
execute!(
|
|
||||||
stdout(),
|
|
||||||
LeaveAlternateScreen,
|
|
||||||
Clear(ClearType::FromCursorDown),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Install a panic hook that restores the terminal before printing the panic.
|
|
||||||
///
|
|
||||||
/// This prevents error messages from being messed up by the terminal state.
|
|
||||||
fn install_panic_hook() {
|
|
||||||
let panic_hook = panic::take_hook();
|
|
||||||
panic::set_hook(Box::new(move |panic_info| {
|
|
||||||
let _ = restore();
|
|
||||||
panic_hook(panic_info);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -13,21 +13,10 @@
|
||||||
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||||
|
|
||||||
use std::{
|
use color_eyre::Result;
|
||||||
error::Error,
|
|
||||||
io::{stdout, Stdout},
|
|
||||||
ops::ControlFlow,
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::CrosstermBackend,
|
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||||
crossterm::{
|
|
||||||
event::{self, Event, KeyCode},
|
|
||||||
execute,
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
},
|
|
||||||
layout::{Alignment, Constraint, Layout, Rect},
|
layout::{Alignment, Constraint, Layout, Rect},
|
||||||
style::{Style, Stylize},
|
style::{Style, Stylize},
|
||||||
text::Line,
|
text::Line,
|
||||||
|
@ -35,61 +24,29 @@ use ratatui::{
|
||||||
block::{Position, Title},
|
block::{Position, Title},
|
||||||
Block, BorderType, Borders, Padding, Paragraph, Wrap,
|
Block, BorderType, Borders, Padding, Paragraph, Wrap,
|
||||||
},
|
},
|
||||||
Frame,
|
DefaultTerminal, Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
// These type aliases are used to make the code more readable by reducing repetition of the generic
|
|
||||||
// types. They are not necessary for the functionality of the code.
|
|
||||||
type Terminal = ratatui::Terminal<CrosstermBackend<Stdout>>;
|
|
||||||
type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let mut terminal = setup_terminal()?;
|
color_eyre::install()?;
|
||||||
let result = run(&mut terminal);
|
let terminal = ratatui::init();
|
||||||
restore_terminal(terminal)?;
|
let result = run(terminal);
|
||||||
|
ratatui::restore();
|
||||||
if let Err(err) = result {
|
result
|
||||||
eprintln!("{err:?}");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_terminal() -> Result<Terminal> {
|
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
enable_raw_mode()?;
|
|
||||||
let mut stdout = stdout();
|
|
||||||
execute!(stdout, EnterAlternateScreen)?;
|
|
||||||
let backend = CrosstermBackend::new(stdout);
|
|
||||||
let terminal = Terminal::new(backend)?;
|
|
||||||
Ok(terminal)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn restore_terminal(mut terminal: Terminal) -> Result<()> {
|
|
||||||
disable_raw_mode()?;
|
|
||||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(terminal: &mut Terminal) -> Result<()> {
|
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(ui)?;
|
terminal.draw(draw)?;
|
||||||
if handle_events()?.is_break() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_events() -> Result<ControlFlow<()>> {
|
|
||||||
if event::poll(Duration::from_millis(100))? {
|
|
||||||
if let Event::Key(key) = event::read()? {
|
if let Event::Key(key) = event::read()? {
|
||||||
if key.code == KeyCode::Char('q') {
|
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||||
return Ok(ControlFlow::Break(()));
|
break Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(ControlFlow::Continue(()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ui(frame: &mut Frame) {
|
fn draw(frame: &mut Frame) {
|
||||||
let (title_area, layout) = calculate_layout(frame.area());
|
let (title_area, layout) = calculate_layout(frame.area());
|
||||||
|
|
||||||
render_title(frame, title_area);
|
render_title(frame, title_area);
|
||||||
|
@ -183,7 +140,6 @@ fn render_styled_block(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||||
frame.render_widget(paragraph.clone().block(block), area);
|
frame.render_widget(paragraph.clone().block(block), area);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: this currently renders incorrectly, see https://github.com/ratatui/ratatui/issues/349
|
|
||||||
fn render_styled_title(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
fn render_styled_title(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||||
let block = Block::bordered()
|
let block = Block::bordered()
|
||||||
.title("Styled title")
|
.title("Styled title")
|
||||||
|
|
|
@ -13,58 +13,40 @@
|
||||||
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||||
|
|
||||||
use std::{error::Error, io};
|
use color_eyre::Result;
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::CrosstermBackend,
|
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||||
crossterm::{
|
layout::{Constraint, Layout, Margin},
|
||||||
event::{self, Event, KeyCode},
|
|
||||||
execute,
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
},
|
|
||||||
layout::{Constraint, Layout, Rect},
|
|
||||||
style::{Color, Modifier, Style},
|
style::{Color, Modifier, Style},
|
||||||
widgets::calendar::{CalendarEventStore, DateStyler, Monthly},
|
widgets::calendar::{CalendarEventStore, DateStyler, Monthly},
|
||||||
Frame, Terminal,
|
DefaultTerminal, Frame,
|
||||||
};
|
};
|
||||||
use time::{Date, Month, OffsetDateTime};
|
use time::{Date, Month, OffsetDateTime};
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
fn main() -> Result<()> {
|
||||||
enable_raw_mode()?;
|
color_eyre::install()?;
|
||||||
let mut stdout = io::stdout();
|
let terminal = ratatui::init();
|
||||||
execute!(stdout, EnterAlternateScreen)?;
|
let result = run(terminal);
|
||||||
let backend = CrosstermBackend::new(stdout);
|
ratatui::restore();
|
||||||
let mut terminal = Terminal::new(backend)?;
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
loop {
|
loop {
|
||||||
let _ = terminal.draw(draw);
|
terminal.draw(draw)?;
|
||||||
|
|
||||||
if let Event::Key(key) = event::read()? {
|
if let Event::Key(key) = event::read()? {
|
||||||
#[allow(clippy::single_match)]
|
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||||
match key.code {
|
break Ok(());
|
||||||
KeyCode::Char(_) => {
|
}
|
||||||
break;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
disable_raw_mode()?;
|
|
||||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
|
||||||
terminal.show_cursor()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(frame: &mut Frame) {
|
fn draw(frame: &mut Frame) {
|
||||||
let app_area = frame.area();
|
let area = frame.area().inner(Margin {
|
||||||
|
vertical: 1,
|
||||||
let calarea = Rect {
|
horizontal: 1,
|
||||||
x: app_area.x + 1,
|
});
|
||||||
y: app_area.y + 1,
|
|
||||||
height: app_area.height - 1,
|
|
||||||
width: app_area.width - 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut start = OffsetDateTime::now_local()
|
let mut start = OffsetDateTime::now_local()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -76,7 +58,7 @@ fn draw(frame: &mut Frame) {
|
||||||
|
|
||||||
let list = make_dates(start.year());
|
let list = make_dates(start.year());
|
||||||
|
|
||||||
let rows = Layout::vertical([Constraint::Ratio(1, 3); 3]).split(calarea);
|
let rows = Layout::vertical([Constraint::Ratio(1, 3); 3]).split(area);
|
||||||
let cols = rows.iter().flat_map(|row| {
|
let cols = rows.iter().flat_map(|row| {
|
||||||
Layout::horizontal([Constraint::Ratio(1, 4); 4])
|
Layout::horizontal([Constraint::Ratio(1, 4); 4])
|
||||||
.split(*row)
|
.split(*row)
|
||||||
|
|
|
@ -13,18 +13,11 @@
|
||||||
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||||
|
|
||||||
use std::{
|
use std::time::{Duration, Instant};
|
||||||
io::{self, stdout, Stdout},
|
|
||||||
time::{Duration, Instant},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
use color_eyre::Result;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::CrosstermBackend,
|
crossterm::event::{self, Event, KeyCode},
|
||||||
crossterm::{
|
|
||||||
event::{self, Event, KeyCode},
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
ExecutableCommand,
|
|
||||||
},
|
|
||||||
layout::{Constraint, Layout, Rect},
|
layout::{Constraint, Layout, Rect},
|
||||||
style::{Color, Stylize},
|
style::{Color, Stylize},
|
||||||
symbols::Marker,
|
symbols::Marker,
|
||||||
|
@ -32,11 +25,15 @@ use ratatui::{
|
||||||
canvas::{Canvas, Circle, Map, MapResolution, Rectangle},
|
canvas::{Canvas, Circle, Map, MapResolution, Rectangle},
|
||||||
Block, Widget,
|
Block, Widget,
|
||||||
},
|
},
|
||||||
Frame, Terminal,
|
DefaultTerminal, Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn main() -> io::Result<()> {
|
fn main() -> Result<()> {
|
||||||
App::run()
|
color_eyre::install()?;
|
||||||
|
let terminal = ratatui::init();
|
||||||
|
let app_result = App::new().run(terminal);
|
||||||
|
ratatui::restore();
|
||||||
|
app_result
|
||||||
}
|
}
|
||||||
|
|
||||||
struct App {
|
struct App {
|
||||||
|
@ -69,33 +66,30 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run() -> io::Result<()> {
|
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
let mut terminal = init_terminal()?;
|
|
||||||
let mut app = Self::new();
|
|
||||||
let mut last_tick = Instant::now();
|
|
||||||
let tick_rate = Duration::from_millis(16);
|
let tick_rate = Duration::from_millis(16);
|
||||||
|
let mut last_tick = Instant::now();
|
||||||
loop {
|
loop {
|
||||||
let _ = terminal.draw(|frame| app.ui(frame));
|
terminal.draw(|frame| self.draw(frame))?;
|
||||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||||
if event::poll(timeout)? {
|
if event::poll(timeout)? {
|
||||||
if let Event::Key(key) = event::read()? {
|
if let Event::Key(key) = event::read()? {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Char('q') => break,
|
KeyCode::Char('q') => break Ok(()),
|
||||||
KeyCode::Down | KeyCode::Char('j') => app.y += 1.0,
|
KeyCode::Down | KeyCode::Char('j') => self.y += 1.0,
|
||||||
KeyCode::Up | KeyCode::Char('k') => app.y -= 1.0,
|
KeyCode::Up | KeyCode::Char('k') => self.y -= 1.0,
|
||||||
KeyCode::Right | KeyCode::Char('l') => app.x += 1.0,
|
KeyCode::Right | KeyCode::Char('l') => self.x += 1.0,
|
||||||
KeyCode::Left | KeyCode::Char('h') => app.x -= 1.0,
|
KeyCode::Left | KeyCode::Char('h') => self.x -= 1.0,
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if last_tick.elapsed() >= tick_rate {
|
if last_tick.elapsed() >= tick_rate {
|
||||||
app.on_tick();
|
self.on_tick();
|
||||||
last_tick = Instant::now();
|
last_tick = Instant::now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
restore_terminal()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_tick(&mut self) {
|
fn on_tick(&mut self) {
|
||||||
|
@ -128,7 +122,7 @@ impl App {
|
||||||
self.ball.y += self.vy;
|
self.ball.y += self.vy;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ui(&self, frame: &mut Frame) {
|
fn draw(&self, frame: &mut Frame) {
|
||||||
let horizontal =
|
let horizontal =
|
||||||
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
|
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
|
||||||
let vertical = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]);
|
let vertical = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]);
|
||||||
|
@ -204,15 +198,3 @@ impl App {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_terminal() -> io::Result<Terminal<CrosstermBackend<Stdout>>> {
|
|
||||||
enable_raw_mode()?;
|
|
||||||
stdout().execute(EnterAlternateScreen)?;
|
|
||||||
Terminal::new(CrosstermBackend::new(stdout()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn restore_terminal() -> io::Result<()> {
|
|
||||||
disable_raw_mode()?;
|
|
||||||
stdout().execute(LeaveAlternateScreen)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
|
@ -13,27 +13,35 @@
|
||||||
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||||
|
|
||||||
use std::{
|
use std::time::{Duration, Instant};
|
||||||
error::Error,
|
|
||||||
io,
|
|
||||||
time::{Duration, Instant},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
use color_eyre::Result;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::{Backend, CrosstermBackend},
|
crossterm::event::{self, Event, KeyCode},
|
||||||
crossterm::{
|
|
||||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
|
||||||
execute,
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
},
|
|
||||||
layout::{Alignment, Constraint, Layout, Rect},
|
layout::{Alignment, Constraint, Layout, Rect},
|
||||||
style::{Color, Modifier, Style, Stylize},
|
style::{Color, Modifier, Style, Stylize},
|
||||||
symbols::{self, Marker},
|
symbols::{self, Marker},
|
||||||
text::Span,
|
text::Span,
|
||||||
widgets::{block::Title, Axis, Block, Chart, Dataset, GraphType, LegendPosition},
|
widgets::{block::Title, Axis, Block, Chart, Dataset, GraphType, LegendPosition},
|
||||||
Frame, Terminal,
|
DefaultTerminal, Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
let terminal = ratatui::init();
|
||||||
|
let app_result = App::new().run(terminal);
|
||||||
|
ratatui::restore();
|
||||||
|
app_result
|
||||||
|
}
|
||||||
|
|
||||||
|
struct App {
|
||||||
|
signal1: SinSignal,
|
||||||
|
data1: Vec<(f64, f64)>,
|
||||||
|
signal2: SinSignal,
|
||||||
|
data2: Vec<(f64, f64)>,
|
||||||
|
window: [f64; 2],
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct SinSignal {
|
struct SinSignal {
|
||||||
x: f64,
|
x: f64,
|
||||||
|
@ -62,14 +70,6 @@ impl Iterator for SinSignal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct App {
|
|
||||||
signal1: SinSignal,
|
|
||||||
data1: Vec<(f64, f64)>,
|
|
||||||
signal2: SinSignal,
|
|
||||||
data2: Vec<(f64, f64)>,
|
|
||||||
window: [f64; 2],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
let mut signal1 = SinSignal::new(0.2, 3.0, 18.0);
|
let mut signal1 = SinSignal::new(0.2, 3.0, 18.0);
|
||||||
|
@ -85,55 +85,11 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_tick(&mut self) {
|
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
self.data1.drain(0..5);
|
|
||||||
self.data1.extend(self.signal1.by_ref().take(5));
|
|
||||||
|
|
||||||
self.data2.drain(0..10);
|
|
||||||
self.data2.extend(self.signal2.by_ref().take(10));
|
|
||||||
|
|
||||||
self.window[0] += 1.0;
|
|
||||||
self.window[1] += 1.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
|
||||||
// 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)?;
|
|
||||||
|
|
||||||
// create app and run it
|
|
||||||
let tick_rate = Duration::from_millis(250);
|
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<B: Backend>(
|
|
||||||
terminal: &mut Terminal<B>,
|
|
||||||
mut app: App,
|
|
||||||
tick_rate: Duration,
|
|
||||||
) -> io::Result<()> {
|
|
||||||
let mut last_tick = Instant::now();
|
let mut last_tick = Instant::now();
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(|f| ui(f, &app))?;
|
terminal.draw(|frame| self.draw(frame))?;
|
||||||
|
|
||||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||||
if event::poll(timeout)? {
|
if event::poll(timeout)? {
|
||||||
|
@ -144,33 +100,44 @@ fn run_app<B: Backend>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if last_tick.elapsed() >= tick_rate {
|
if last_tick.elapsed() >= tick_rate {
|
||||||
app.on_tick();
|
self.on_tick();
|
||||||
last_tick = Instant::now();
|
last_tick = Instant::now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ui(frame: &mut Frame, app: &App) {
|
fn on_tick(&mut self) {
|
||||||
|
self.data1.drain(0..5);
|
||||||
|
self.data1.extend(self.signal1.by_ref().take(5));
|
||||||
|
|
||||||
|
self.data2.drain(0..10);
|
||||||
|
self.data2.extend(self.signal2.by_ref().take(10));
|
||||||
|
|
||||||
|
self.window[0] += 1.0;
|
||||||
|
self.window[1] += 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&self, frame: &mut Frame) {
|
||||||
let [top, bottom] = Layout::vertical([Constraint::Fill(1); 2]).areas(frame.area());
|
let [top, bottom] = Layout::vertical([Constraint::Fill(1); 2]).areas(frame.area());
|
||||||
let [animated_chart, bar_chart] =
|
let [animated_chart, bar_chart] =
|
||||||
Layout::horizontal([Constraint::Fill(1), Constraint::Length(29)]).areas(top);
|
Layout::horizontal([Constraint::Fill(1), Constraint::Length(29)]).areas(top);
|
||||||
let [line_chart, scatter] = Layout::horizontal([Constraint::Fill(1); 2]).areas(bottom);
|
let [line_chart, scatter] = Layout::horizontal([Constraint::Fill(1); 2]).areas(bottom);
|
||||||
|
|
||||||
render_animated_chart(frame, animated_chart, app);
|
self.render_animated_chart(frame, animated_chart);
|
||||||
render_barchart(frame, bar_chart);
|
render_barchart(frame, bar_chart);
|
||||||
render_line_chart(frame, line_chart);
|
render_line_chart(frame, line_chart);
|
||||||
render_scatter(frame, scatter);
|
render_scatter(frame, scatter);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_animated_chart(f: &mut Frame, area: Rect, app: &App) {
|
fn render_animated_chart(&self, frame: &mut Frame, area: Rect) {
|
||||||
let x_labels = vec![
|
let x_labels = vec![
|
||||||
Span::styled(
|
Span::styled(
|
||||||
format!("{}", app.window[0]),
|
format!("{}", self.window[0]),
|
||||||
Style::default().add_modifier(Modifier::BOLD),
|
Style::default().add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
Span::raw(format!("{}", (app.window[0] + app.window[1]) / 2.0)),
|
Span::raw(format!("{}", (self.window[0] + self.window[1]) / 2.0)),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
format!("{}", app.window[1]),
|
format!("{}", self.window[1]),
|
||||||
Style::default().add_modifier(Modifier::BOLD),
|
Style::default().add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
@ -179,12 +146,12 @@ fn render_animated_chart(f: &mut Frame, area: Rect, app: &App) {
|
||||||
.name("data2")
|
.name("data2")
|
||||||
.marker(symbols::Marker::Dot)
|
.marker(symbols::Marker::Dot)
|
||||||
.style(Style::default().fg(Color::Cyan))
|
.style(Style::default().fg(Color::Cyan))
|
||||||
.data(&app.data1),
|
.data(&self.data1),
|
||||||
Dataset::default()
|
Dataset::default()
|
||||||
.name("data3")
|
.name("data3")
|
||||||
.marker(symbols::Marker::Braille)
|
.marker(symbols::Marker::Braille)
|
||||||
.style(Style::default().fg(Color::Yellow))
|
.style(Style::default().fg(Color::Yellow))
|
||||||
.data(&app.data2),
|
.data(&self.data2),
|
||||||
];
|
];
|
||||||
|
|
||||||
let chart = Chart::new(datasets)
|
let chart = Chart::new(datasets)
|
||||||
|
@ -194,7 +161,7 @@ fn render_animated_chart(f: &mut Frame, area: Rect, app: &App) {
|
||||||
.title("X Axis")
|
.title("X Axis")
|
||||||
.style(Style::default().fg(Color::Gray))
|
.style(Style::default().fg(Color::Gray))
|
||||||
.labels(x_labels)
|
.labels(x_labels)
|
||||||
.bounds(app.window),
|
.bounds(self.window),
|
||||||
)
|
)
|
||||||
.y_axis(
|
.y_axis(
|
||||||
Axis::default()
|
Axis::default()
|
||||||
|
@ -204,7 +171,8 @@ fn render_animated_chart(f: &mut Frame, area: Rect, app: &App) {
|
||||||
.bounds([-20.0, 20.0]),
|
.bounds([-20.0, 20.0]),
|
||||||
);
|
);
|
||||||
|
|
||||||
f.render_widget(chart, area);
|
frame.render_widget(chart, area);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_barchart(frame: &mut Frame, bar_chart: Rect) {
|
fn render_barchart(frame: &mut Frame, bar_chart: Rect) {
|
||||||
|
@ -252,7 +220,7 @@ fn render_barchart(frame: &mut Frame, bar_chart: Rect) {
|
||||||
frame.render_widget(chart, bar_chart);
|
frame.render_widget(chart, bar_chart);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_line_chart(f: &mut Frame, area: Rect) {
|
fn render_line_chart(frame: &mut Frame, area: Rect) {
|
||||||
let datasets = vec![Dataset::default()
|
let datasets = vec![Dataset::default()
|
||||||
.name("Line from only 2 points".italic())
|
.name("Line from only 2 points".italic())
|
||||||
.marker(symbols::Marker::Braille)
|
.marker(symbols::Marker::Braille)
|
||||||
|
@ -285,10 +253,10 @@ fn render_line_chart(f: &mut Frame, area: Rect) {
|
||||||
.legend_position(Some(LegendPosition::TopLeft))
|
.legend_position(Some(LegendPosition::TopLeft))
|
||||||
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
|
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
|
||||||
|
|
||||||
f.render_widget(chart, area);
|
frame.render_widget(chart, area);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_scatter(f: &mut Frame, area: Rect) {
|
fn render_scatter(frame: &mut Frame, area: Rect) {
|
||||||
let datasets = vec![
|
let datasets = vec![
|
||||||
Dataset::default()
|
Dataset::default()
|
||||||
.name("Heavy")
|
.name("Heavy")
|
||||||
|
@ -334,7 +302,7 @@ fn render_scatter(f: &mut Frame, area: Rect) {
|
||||||
)
|
)
|
||||||
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
|
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
|
||||||
|
|
||||||
f.render_widget(chart, area);
|
frame.render_widget(chart, area);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data from https://ourworldindata.org/space-exploration-satellites
|
// Data from https://ourworldindata.org/space-exploration-satellites
|
||||||
|
|
|
@ -16,55 +16,37 @@
|
||||||
// This example shows all the colors supported by ratatui. It will render a grid of foreground
|
// This example shows all the colors supported by ratatui. It will render a grid of foreground
|
||||||
// and background colors with their names and indexes.
|
// and background colors with their names and indexes.
|
||||||
|
|
||||||
use std::{
|
use color_eyre::Result;
|
||||||
error::Error,
|
|
||||||
io::{self, Stdout},
|
|
||||||
result,
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::{Backend, CrosstermBackend},
|
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||||
crossterm::{
|
|
||||||
event::{self, Event, KeyCode},
|
|
||||||
execute,
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
},
|
|
||||||
layout::{Alignment, Constraint, Layout, Rect},
|
layout::{Alignment, Constraint, Layout, Rect},
|
||||||
style::{Color, Style, Stylize},
|
style::{Color, Style, Stylize},
|
||||||
text::Line,
|
text::Line,
|
||||||
widgets::{Block, Borders, Paragraph},
|
widgets::{Block, Borders, Paragraph},
|
||||||
Frame, Terminal,
|
DefaultTerminal, Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
type Result<T> = result::Result<T, Box<dyn Error>>;
|
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let mut terminal = setup_terminal()?;
|
color_eyre::install()?;
|
||||||
let res = run_app(&mut terminal);
|
let terminal = ratatui::init();
|
||||||
restore_terminal(terminal)?;
|
let app_result = run(terminal);
|
||||||
if let Err(err) = res {
|
ratatui::restore();
|
||||||
eprintln!("{err:?}");
|
app_result
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(ui)?;
|
terminal.draw(draw)?;
|
||||||
|
|
||||||
if event::poll(Duration::from_millis(250))? {
|
|
||||||
if let Event::Key(key) = event::read()? {
|
if let Event::Key(key) = event::read()? {
|
||||||
if key.code == KeyCode::Char('q') {
|
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn ui(frame: &mut Frame) {
|
fn draw(frame: &mut Frame) {
|
||||||
let layout = Layout::vertical([
|
let layout = Layout::vertical([
|
||||||
Constraint::Length(30),
|
Constraint::Length(30),
|
||||||
Constraint::Length(17),
|
Constraint::Length(17),
|
||||||
|
@ -271,20 +253,3 @@ fn render_indexed_grayscale(frame: &mut Frame, area: Rect) {
|
||||||
frame.render_widget(paragraph, layout[i as usize - 232]);
|
frame.render_widget(paragraph, layout[i as usize - 232]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
|
|
||||||
enable_raw_mode()?;
|
|
||||||
let mut stdout = io::stdout();
|
|
||||||
execute!(stdout, EnterAlternateScreen)?;
|
|
||||||
let backend = CrosstermBackend::new(stdout);
|
|
||||||
let mut terminal = Terminal::new(backend)?;
|
|
||||||
terminal.hide_cursor()?;
|
|
||||||
Ok(terminal)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn restore_terminal(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
|
||||||
disable_raw_mode()?;
|
|
||||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
|
||||||
terminal.show_cursor()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
|
@ -26,29 +26,28 @@
|
||||||
// is useful when the state is only used by the widget and doesn't need to be shared with
|
// is useful when the state is only used by the widget and doesn't need to be shared with
|
||||||
// other widgets.
|
// other widgets.
|
||||||
|
|
||||||
use std::{
|
use std::time::{Duration, Instant};
|
||||||
io::stdout,
|
|
||||||
panic,
|
|
||||||
time::{Duration, Instant},
|
|
||||||
};
|
|
||||||
|
|
||||||
use color_eyre::{config::HookBuilder, eyre, Result};
|
use color_eyre::Result;
|
||||||
use palette::{convert::FromColorUnclamped, Okhsv, Srgb};
|
use palette::{convert::FromColorUnclamped, Okhsv, Srgb};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::{Backend, CrosstermBackend},
|
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
crossterm::{
|
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||||
event::{self, Event, KeyCode, KeyEventKind},
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
ExecutableCommand,
|
|
||||||
},
|
|
||||||
layout::{Constraint, Layout, Position, Rect},
|
layout::{Constraint, Layout, Position, Rect},
|
||||||
style::Color,
|
style::Color,
|
||||||
text::Text,
|
text::Text,
|
||||||
widgets::Widget,
|
widgets::Widget,
|
||||||
Terminal,
|
DefaultTerminal,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
let terminal = ratatui::init();
|
||||||
|
let app_result = App::default().run(terminal);
|
||||||
|
ratatui::restore();
|
||||||
|
app_result
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
struct App {
|
struct App {
|
||||||
/// The current state of the app (running or quit)
|
/// The current state of the app (running or quit)
|
||||||
|
@ -99,19 +98,11 @@ struct ColorsWidget {
|
||||||
frame_count: usize,
|
frame_count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
|
||||||
install_error_hooks()?;
|
|
||||||
let terminal = init_terminal()?;
|
|
||||||
App::default().run(terminal)?;
|
|
||||||
restore_terminal()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
/// Run the app
|
/// Run the app
|
||||||
///
|
///
|
||||||
/// This is the main event loop for the app.
|
/// This is the main event loop for the app.
|
||||||
pub fn run(mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
|
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
while self.is_running() {
|
while self.is_running() {
|
||||||
terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?;
|
terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?;
|
||||||
self.handle_events()?;
|
self.handle_events()?;
|
||||||
|
@ -263,36 +254,3 @@ impl ColorsWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Install `color_eyre` panic and error hooks
|
|
||||||
///
|
|
||||||
/// The hooks restore the terminal to a usable state before printing the error message.
|
|
||||||
fn install_error_hooks() -> Result<()> {
|
|
||||||
let (panic, error) = HookBuilder::default().into_hooks();
|
|
||||||
let panic = panic.into_panic_hook();
|
|
||||||
let error = error.into_eyre_hook();
|
|
||||||
eyre::set_hook(Box::new(move |e| {
|
|
||||||
let _ = restore_terminal();
|
|
||||||
error(e)
|
|
||||||
}))?;
|
|
||||||
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 mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
|
|
||||||
terminal.clear()?;
|
|
||||||
terminal.hide_cursor()?;
|
|
||||||
Ok(terminal)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn restore_terminal() -> Result<()> {
|
|
||||||
disable_raw_mode()?;
|
|
||||||
stdout().execute(LeaveAlternateScreen)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
|
@ -13,18 +13,11 @@
|
||||||
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||||
|
|
||||||
use std::io::{self, stdout};
|
use color_eyre::Result;
|
||||||
|
|
||||||
use color_eyre::{config::HookBuilder, Result};
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::{Backend, CrosstermBackend},
|
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
crossterm::{
|
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||||
event::{self, Event, KeyCode, KeyEventKind},
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
ExecutableCommand,
|
|
||||||
},
|
|
||||||
layout::{
|
layout::{
|
||||||
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
|
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
|
||||||
Flex, Layout, Rect,
|
Flex, Layout, Rect,
|
||||||
|
@ -36,10 +29,18 @@ use ratatui::{
|
||||||
symbols::{self, line},
|
symbols::{self, line},
|
||||||
text::{Line, Span, Text},
|
text::{Line, Span, Text},
|
||||||
widgets::{Block, Paragraph, Widget, Wrap},
|
widgets::{Block, Paragraph, Widget, Wrap},
|
||||||
Terminal,
|
DefaultTerminal,
|
||||||
};
|
};
|
||||||
use strum::{Display, EnumIter, FromRepr};
|
use strum::{Display, EnumIter, FromRepr};
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
let terminal = ratatui::init();
|
||||||
|
let app_result = App::default().run(terminal);
|
||||||
|
ratatui::restore();
|
||||||
|
app_result
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct App {
|
struct App {
|
||||||
mode: AppMode,
|
mode: AppMode,
|
||||||
|
@ -90,21 +91,13 @@ struct ConstraintBlock {
|
||||||
/// ```
|
/// ```
|
||||||
struct SpacerBlock;
|
struct SpacerBlock;
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
|
||||||
init_error_hooks()?;
|
|
||||||
let terminal = init_terminal()?;
|
|
||||||
App::default().run(terminal)?;
|
|
||||||
restore_terminal()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// App behaviour
|
// App behaviour
|
||||||
impl App {
|
impl App {
|
||||||
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
|
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
self.insert_test_defaults();
|
self.insert_test_defaults();
|
||||||
|
|
||||||
while self.is_running() {
|
while self.is_running() {
|
||||||
self.draw(&mut terminal)?;
|
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
|
||||||
self.handle_events()?;
|
self.handle_events()?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -124,11 +117,6 @@ impl App {
|
||||||
self.mode == AppMode::Running
|
self.mode == AppMode::Running
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(&self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
|
|
||||||
terminal.draw(|frame| frame.render_widget(self, frame.area()))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_events(&mut self) -> Result<()> {
|
fn handle_events(&mut self) -> Result<()> {
|
||||||
match event::read()? {
|
match event::read()? {
|
||||||
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
|
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
|
||||||
|
@ -621,32 +609,3 @@ impl ConstraintName {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
|
|
|
@ -13,17 +13,10 @@
|
||||||
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||||
|
|
||||||
use std::io::{self, stdout};
|
use color_eyre::Result;
|
||||||
|
|
||||||
use color_eyre::{config::HookBuilder, Result};
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::{Backend, CrosstermBackend},
|
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
crossterm::{
|
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||||
event::{self, Event, KeyCode, KeyEventKind},
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
ExecutableCommand,
|
|
||||||
},
|
|
||||||
layout::{
|
layout::{
|
||||||
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
|
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
|
||||||
Layout, Rect,
|
Layout, Rect,
|
||||||
|
@ -35,7 +28,7 @@ use ratatui::{
|
||||||
Block, Padding, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget,
|
Block, Padding, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget,
|
||||||
Tabs, Widget,
|
Tabs, Widget,
|
||||||
},
|
},
|
||||||
Terminal,
|
DefaultTerminal,
|
||||||
};
|
};
|
||||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||||
|
|
||||||
|
@ -53,6 +46,14 @@ const RATIO_COLOR: Color = tailwind::SLATE.c900;
|
||||||
// priority 4
|
// priority 4
|
||||||
const FILL_COLOR: Color = tailwind::SLATE.c950;
|
const FILL_COLOR: Color = tailwind::SLATE.c950;
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
let terminal = ratatui::init();
|
||||||
|
let app_result = App::default().run(terminal);
|
||||||
|
ratatui::restore();
|
||||||
|
app_result
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, Copy)]
|
#[derive(Default, Clone, Copy)]
|
||||||
struct App {
|
struct App {
|
||||||
selected_tab: SelectedTab,
|
selected_tab: SelectedTab,
|
||||||
|
@ -82,22 +83,11 @@ enum AppState {
|
||||||
Quit,
|
Quit,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
|
||||||
init_error_hooks()?;
|
|
||||||
let terminal = init_terminal()?;
|
|
||||||
|
|
||||||
App::default().run(terminal)?;
|
|
||||||
|
|
||||||
restore_terminal()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
|
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
self.update_max_scroll_offset();
|
self.update_max_scroll_offset();
|
||||||
while self.is_running() {
|
while self.is_running() {
|
||||||
self.draw(&mut terminal)?;
|
terminal.draw(|frame| frame.render_widget(self, frame.area()))?;
|
||||||
self.handle_events()?;
|
self.handle_events()?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -111,11 +101,6 @@ impl App {
|
||||||
self.state == AppState::Running
|
self.state == AppState::Running
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
|
|
||||||
terminal.draw(|frame| frame.render_widget(self, frame.area()))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_events(&mut self) -> Result<()> {
|
fn handle_events(&mut self) -> Result<()> {
|
||||||
if let Event::Key(key) = event::read()? {
|
if let Event::Key(key) = event::read()? {
|
||||||
if key.kind != KeyEventKind::Press {
|
if key.kind != KeyEventKind::Press {
|
||||||
|
@ -418,32 +403,3 @@ impl Example {
|
||||||
Paragraph::new(text).centered().block(block)
|
Paragraph::new(text).centered().block(block)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
|
|
|
@ -13,10 +13,10 @@
|
||||||
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||||
|
|
||||||
use std::{error::Error, io, ops::ControlFlow, time::Duration};
|
use std::{io::stdout, ops::ControlFlow, time::Duration};
|
||||||
|
|
||||||
|
use color_eyre::Result;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::{Backend, CrosstermBackend},
|
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
crossterm::{
|
crossterm::{
|
||||||
event::{
|
event::{
|
||||||
|
@ -24,15 +24,26 @@ use ratatui::{
|
||||||
MouseEventKind,
|
MouseEventKind,
|
||||||
},
|
},
|
||||||
execute,
|
execute,
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
},
|
},
|
||||||
layout::{Constraint, Layout, Rect},
|
layout::{Constraint, Layout, Rect},
|
||||||
style::{Color, Style},
|
style::{Color, Style},
|
||||||
text::Line,
|
text::Line,
|
||||||
widgets::{Paragraph, Widget},
|
widgets::{Paragraph, Widget},
|
||||||
Frame, Terminal,
|
DefaultTerminal, Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
let terminal = ratatui::init();
|
||||||
|
execute!(stdout(), EnableMouseCapture)?;
|
||||||
|
let app_result = run(terminal);
|
||||||
|
ratatui::restore();
|
||||||
|
if let Err(err) = execute!(stdout(), DisableMouseCapture) {
|
||||||
|
eprintln!("Error disabling mouse capture: {err}");
|
||||||
|
}
|
||||||
|
app_result
|
||||||
|
}
|
||||||
|
|
||||||
/// A custom widget that renders a button with a label, theme and state.
|
/// A custom widget that renders a button with a label, theme and state.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct Button<'a> {
|
struct Button<'a> {
|
||||||
|
@ -143,38 +154,11 @@ impl Button<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
// 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)?;
|
|
||||||
|
|
||||||
// create app and run it
|
|
||||||
let res = run_app(&mut terminal);
|
|
||||||
|
|
||||||
// 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<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
|
||||||
let mut selected_button: usize = 0;
|
let mut selected_button: usize = 0;
|
||||||
let mut button_states = [State::Selected, State::Normal, State::Normal];
|
let mut button_states = [State::Selected, State::Normal, State::Normal];
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(|frame| ui(frame, button_states))?;
|
terminal.draw(|frame| draw(frame, button_states))?;
|
||||||
if !event::poll(Duration::from_millis(100))? {
|
if !event::poll(Duration::from_millis(100))? {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -196,7 +180,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ui(frame: &mut Frame, states: [State; 3]) {
|
fn draw(frame: &mut Frame, states: [State; 3]) {
|
||||||
let vertical = Layout::vertical([
|
let vertical = Layout::vertical([
|
||||||
Constraint::Length(1),
|
Constraint::Length(1),
|
||||||
Constraint::Max(3),
|
Constraint::Max(3),
|
||||||
|
|
|
@ -26,7 +26,7 @@ pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn E
|
||||||
|
|
||||||
// create app and run it
|
// create app and run it
|
||||||
let app = App::new("Crossterm Demo", enhanced_graphics);
|
let app = App::new("Crossterm Demo", enhanced_graphics);
|
||||||
let res = run_app(&mut terminal, app, tick_rate);
|
let app_result = run_app(&mut terminal, app, tick_rate);
|
||||||
|
|
||||||
// restore terminal
|
// restore terminal
|
||||||
disable_raw_mode()?;
|
disable_raw_mode()?;
|
||||||
|
@ -37,7 +37,7 @@ pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn E
|
||||||
)?;
|
)?;
|
||||||
terminal.show_cursor()?;
|
terminal.show_cursor()?;
|
||||||
|
|
||||||
if let Err(err) = res {
|
if let Err(err) = app_result {
|
||||||
println!("{err:?}");
|
println!("{err:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ fn run_app<B: Backend>(
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
let mut last_tick = Instant::now();
|
let mut last_tick = Instant::now();
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(|f| ui::draw(f, &mut app))?;
|
terminal.draw(|frame| ui::draw(frame, &mut app))?;
|
||||||
|
|
||||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||||
if event::poll(timeout)? {
|
if event::poll(timeout)? {
|
||||||
|
|
|
@ -39,7 +39,7 @@ fn run_app<B: Backend>(
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> Result<(), Box<dyn Error>> {
|
||||||
let events = events(tick_rate);
|
let events = events(tick_rate);
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(|f| ui::draw(f, &mut app))?;
|
terminal.draw(|frame| ui::draw(frame, &mut app))?;
|
||||||
|
|
||||||
match events.recv()? {
|
match events.recv()? {
|
||||||
Event::Input(key) => match key {
|
Event::Input(key) => match key {
|
||||||
|
|
|
@ -22,12 +22,12 @@ pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn E
|
||||||
|
|
||||||
// create app and run it
|
// create app and run it
|
||||||
let app = App::new("Termwiz Demo", enhanced_graphics);
|
let app = App::new("Termwiz Demo", enhanced_graphics);
|
||||||
let res = run_app(&mut terminal, app, tick_rate);
|
let app_result = run_app(&mut terminal, app, tick_rate);
|
||||||
|
|
||||||
terminal.show_cursor()?;
|
terminal.show_cursor()?;
|
||||||
terminal.flush()?;
|
terminal.flush()?;
|
||||||
|
|
||||||
if let Err(err) = res {
|
if let Err(err) = app_result {
|
||||||
println!("{err:?}");
|
println!("{err:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ fn run_app(
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> Result<(), Box<dyn Error>> {
|
||||||
let mut last_tick = Instant::now();
|
let mut last_tick = Instant::now();
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(|f| ui::draw(f, &mut app))?;
|
terminal.draw(|frame| ui::draw(frame, &mut app))?;
|
||||||
|
|
||||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||||
if let Some(input) = terminal
|
if let Some(input) = terminal
|
||||||
|
|
|
@ -13,8 +13,8 @@ use ratatui::{
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
|
|
||||||
pub fn draw(f: &mut Frame, app: &mut App) {
|
pub fn draw(frame: &mut Frame, app: &mut App) {
|
||||||
let chunks = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).split(f.area());
|
let chunks = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).split(frame.area());
|
||||||
let tabs = app
|
let tabs = app
|
||||||
.tabs
|
.tabs
|
||||||
.titles
|
.titles
|
||||||
|
@ -24,28 +24,28 @@ pub fn draw(f: &mut Frame, app: &mut App) {
|
||||||
.block(Block::bordered().title(app.title))
|
.block(Block::bordered().title(app.title))
|
||||||
.highlight_style(Style::default().fg(Color::Yellow))
|
.highlight_style(Style::default().fg(Color::Yellow))
|
||||||
.select(app.tabs.index);
|
.select(app.tabs.index);
|
||||||
f.render_widget(tabs, chunks[0]);
|
frame.render_widget(tabs, chunks[0]);
|
||||||
match app.tabs.index {
|
match app.tabs.index {
|
||||||
0 => draw_first_tab(f, app, chunks[1]),
|
0 => draw_first_tab(frame, app, chunks[1]),
|
||||||
1 => draw_second_tab(f, app, chunks[1]),
|
1 => draw_second_tab(frame, app, chunks[1]),
|
||||||
2 => draw_third_tab(f, app, chunks[1]),
|
2 => draw_third_tab(frame, app, chunks[1]),
|
||||||
_ => {}
|
_ => {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_first_tab(f: &mut Frame, app: &mut App, area: Rect) {
|
fn draw_first_tab(frame: &mut Frame, app: &mut App, area: Rect) {
|
||||||
let chunks = Layout::vertical([
|
let chunks = Layout::vertical([
|
||||||
Constraint::Length(9),
|
Constraint::Length(9),
|
||||||
Constraint::Min(8),
|
Constraint::Min(8),
|
||||||
Constraint::Length(7),
|
Constraint::Length(7),
|
||||||
])
|
])
|
||||||
.split(area);
|
.split(area);
|
||||||
draw_gauges(f, app, chunks[0]);
|
draw_gauges(frame, app, chunks[0]);
|
||||||
draw_charts(f, app, chunks[1]);
|
draw_charts(frame, app, chunks[1]);
|
||||||
draw_text(f, chunks[2]);
|
draw_text(frame, chunks[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) {
|
fn draw_gauges(frame: &mut Frame, app: &mut App, area: Rect) {
|
||||||
let chunks = Layout::vertical([
|
let chunks = Layout::vertical([
|
||||||
Constraint::Length(2),
|
Constraint::Length(2),
|
||||||
Constraint::Length(3),
|
Constraint::Length(3),
|
||||||
|
@ -54,7 +54,7 @@ fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) {
|
||||||
.margin(1)
|
.margin(1)
|
||||||
.split(area);
|
.split(area);
|
||||||
let block = Block::bordered().title("Graphs");
|
let block = Block::bordered().title("Graphs");
|
||||||
f.render_widget(block, area);
|
frame.render_widget(block, area);
|
||||||
|
|
||||||
let label = format!("{:.2}%", app.progress * 100.0);
|
let label = format!("{:.2}%", app.progress * 100.0);
|
||||||
let gauge = Gauge::default()
|
let gauge = Gauge::default()
|
||||||
|
@ -68,7 +68,7 @@ fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) {
|
||||||
.use_unicode(app.enhanced_graphics)
|
.use_unicode(app.enhanced_graphics)
|
||||||
.label(label)
|
.label(label)
|
||||||
.ratio(app.progress);
|
.ratio(app.progress);
|
||||||
f.render_widget(gauge, chunks[0]);
|
frame.render_widget(gauge, chunks[0]);
|
||||||
|
|
||||||
let sparkline = Sparkline::default()
|
let sparkline = Sparkline::default()
|
||||||
.block(Block::new().title("Sparkline:"))
|
.block(Block::new().title("Sparkline:"))
|
||||||
|
@ -79,7 +79,7 @@ fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) {
|
||||||
} else {
|
} else {
|
||||||
symbols::bar::THREE_LEVELS
|
symbols::bar::THREE_LEVELS
|
||||||
});
|
});
|
||||||
f.render_widget(sparkline, chunks[1]);
|
frame.render_widget(sparkline, chunks[1]);
|
||||||
|
|
||||||
let line_gauge = LineGauge::default()
|
let line_gauge = LineGauge::default()
|
||||||
.block(Block::new().title("LineGauge:"))
|
.block(Block::new().title("LineGauge:"))
|
||||||
|
@ -90,11 +90,11 @@ fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) {
|
||||||
symbols::line::NORMAL
|
symbols::line::NORMAL
|
||||||
})
|
})
|
||||||
.ratio(app.progress);
|
.ratio(app.progress);
|
||||||
f.render_widget(line_gauge, chunks[2]);
|
frame.render_widget(line_gauge, chunks[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) {
|
fn draw_charts(frame: &mut Frame, app: &mut App, area: Rect) {
|
||||||
let constraints = if app.show_chart {
|
let constraints = if app.show_chart {
|
||||||
vec![Constraint::Percentage(50), Constraint::Percentage(50)]
|
vec![Constraint::Percentage(50), Constraint::Percentage(50)]
|
||||||
} else {
|
} else {
|
||||||
|
@ -120,7 +120,7 @@ fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) {
|
||||||
.block(Block::bordered().title("List"))
|
.block(Block::bordered().title("List"))
|
||||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
||||||
.highlight_symbol("> ");
|
.highlight_symbol("> ");
|
||||||
f.render_stateful_widget(tasks, chunks[0], &mut app.tasks.state);
|
frame.render_stateful_widget(tasks, chunks[0], &mut app.tasks.state);
|
||||||
|
|
||||||
// Draw logs
|
// Draw logs
|
||||||
let info_style = Style::default().fg(Color::Blue);
|
let info_style = Style::default().fg(Color::Blue);
|
||||||
|
@ -146,7 +146,7 @@ fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) {
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let logs = List::new(logs).block(Block::bordered().title("List"));
|
let logs = List::new(logs).block(Block::bordered().title("List"));
|
||||||
f.render_stateful_widget(logs, chunks[1], &mut app.logs.state);
|
frame.render_stateful_widget(logs, chunks[1], &mut app.logs.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
let barchart = BarChart::default()
|
let barchart = BarChart::default()
|
||||||
|
@ -167,7 +167,7 @@ fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) {
|
||||||
)
|
)
|
||||||
.label_style(Style::default().fg(Color::Yellow))
|
.label_style(Style::default().fg(Color::Yellow))
|
||||||
.bar_style(Style::default().fg(Color::Green));
|
.bar_style(Style::default().fg(Color::Green));
|
||||||
f.render_widget(barchart, chunks[1]);
|
frame.render_widget(barchart, chunks[1]);
|
||||||
}
|
}
|
||||||
if app.show_chart {
|
if app.show_chart {
|
||||||
let x_labels = vec![
|
let x_labels = vec![
|
||||||
|
@ -227,11 +227,11 @@ fn draw_charts(f: &mut Frame, app: &mut App, area: Rect) {
|
||||||
Span::styled("20", Style::default().add_modifier(Modifier::BOLD)),
|
Span::styled("20", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
f.render_widget(chart, chunks[1]);
|
frame.render_widget(chart, chunks[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_text(f: &mut Frame, area: Rect) {
|
fn draw_text(frame: &mut Frame, area: Rect) {
|
||||||
let text = vec![
|
let text = vec![
|
||||||
text::Line::from("This is a paragraph with several lines. You can change style your text the way you want"),
|
text::Line::from("This is a paragraph with several lines. You can change style your text the way you want"),
|
||||||
text::Line::from(""),
|
text::Line::from(""),
|
||||||
|
@ -266,10 +266,10 @@ fn draw_text(f: &mut Frame, area: Rect) {
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
));
|
));
|
||||||
let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
|
let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
|
||||||
f.render_widget(paragraph, area);
|
frame.render_widget(paragraph, area);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_second_tab(f: &mut Frame, app: &mut App, area: Rect) {
|
fn draw_second_tab(frame: &mut Frame, app: &mut App, area: Rect) {
|
||||||
let chunks =
|
let chunks =
|
||||||
Layout::horizontal([Constraint::Percentage(30), Constraint::Percentage(70)]).split(area);
|
Layout::horizontal([Constraint::Percentage(30), Constraint::Percentage(70)]).split(area);
|
||||||
let up_style = Style::default().fg(Color::Green);
|
let up_style = Style::default().fg(Color::Green);
|
||||||
|
@ -298,7 +298,7 @@ fn draw_second_tab(f: &mut Frame, app: &mut App, area: Rect) {
|
||||||
.bottom_margin(1),
|
.bottom_margin(1),
|
||||||
)
|
)
|
||||||
.block(Block::bordered().title("Servers"));
|
.block(Block::bordered().title("Servers"));
|
||||||
f.render_widget(table, chunks[0]);
|
frame.render_widget(table, chunks[0]);
|
||||||
|
|
||||||
let map = Canvas::default()
|
let map = Canvas::default()
|
||||||
.block(Block::bordered().title("World"))
|
.block(Block::bordered().title("World"))
|
||||||
|
@ -352,10 +352,10 @@ fn draw_second_tab(f: &mut Frame, app: &mut App, area: Rect) {
|
||||||
})
|
})
|
||||||
.x_bounds([-180.0, 180.0])
|
.x_bounds([-180.0, 180.0])
|
||||||
.y_bounds([-90.0, 90.0]);
|
.y_bounds([-90.0, 90.0]);
|
||||||
f.render_widget(map, chunks[1]);
|
frame.render_widget(map, chunks[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_third_tab(f: &mut Frame, _app: &mut App, area: Rect) {
|
fn draw_third_tab(frame: &mut Frame, _app: &mut App, area: Rect) {
|
||||||
let chunks = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).split(area);
|
let chunks = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]).split(area);
|
||||||
let colors = [
|
let colors = [
|
||||||
Color::Reset,
|
Color::Reset,
|
||||||
|
@ -396,5 +396,5 @@ fn draw_third_tab(f: &mut Frame, _app: &mut App, area: Rect) {
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.block(Block::bordered().title("Colors"));
|
.block(Block::bordered().title("Colors"));
|
||||||
f.render_widget(table, chunks[0]);
|
frame.render_widget(table, chunks[0]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use color_eyre::{eyre::Context, Result};
|
use color_eyre::{eyre::Context, Result};
|
||||||
|
use crossterm::event;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::Backend,
|
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind},
|
crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind},
|
||||||
layout::{Constraint, Layout, Rect},
|
layout::{Constraint, Layout, Rect},
|
||||||
style::Color,
|
style::Color,
|
||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
widgets::{Block, Tabs, Widget},
|
widgets::{Block, Tabs, Widget},
|
||||||
Terminal,
|
DefaultTerminal, Frame,
|
||||||
};
|
};
|
||||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
destroy,
|
destroy,
|
||||||
tabs::{AboutTab, EmailTab, RecipeTab, TracerouteTab, WeatherTab},
|
tabs::{AboutTab, EmailTab, RecipeTab, TracerouteTab, WeatherTab},
|
||||||
term, THEME,
|
THEME,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
|
@ -49,15 +49,13 @@ enum Tab {
|
||||||
Weather,
|
Weather,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(terminal: &mut Terminal<impl Backend>) -> Result<()> {
|
|
||||||
App::default().run(terminal)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
/// Run the app until the user quits.
|
/// Run the app until the user quits.
|
||||||
pub fn run(&mut self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
|
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
while self.is_running() {
|
while self.is_running() {
|
||||||
self.draw(terminal)?;
|
terminal
|
||||||
|
.draw(|frame| self.draw(frame))
|
||||||
|
.wrap_err("terminal.draw")?;
|
||||||
self.handle_events()?;
|
self.handle_events()?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -68,16 +66,11 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw a single frame of the app.
|
/// Draw a single frame of the app.
|
||||||
fn draw(&self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
|
fn draw(&self, frame: &mut Frame) {
|
||||||
terminal
|
|
||||||
.draw(|frame| {
|
|
||||||
frame.render_widget(self, frame.area());
|
frame.render_widget(self, frame.area());
|
||||||
if self.mode == Mode::Destroy {
|
if self.mode == Mode::Destroy {
|
||||||
destroy::destroy(frame);
|
destroy::destroy(frame);
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.wrap_err("terminal.draw")?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle events from the terminal.
|
/// Handle events from the terminal.
|
||||||
|
@ -86,8 +79,11 @@ impl App {
|
||||||
/// 1/50th of a second. This was chosen to try to match the default frame rate of a GIF in VHS.
|
/// 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<()> {
|
fn handle_events(&mut self) -> Result<()> {
|
||||||
let timeout = Duration::from_secs_f64(1.0 / 50.0);
|
let timeout = Duration::from_secs_f64(1.0 / 50.0);
|
||||||
match term::next_event(timeout)? {
|
if !event::poll(timeout)? {
|
||||||
Some(Event::Key(key)) if key.kind == KeyEventKind::Press => self.handle_key_press(key),
|
return Ok(());
|
||||||
|
}
|
||||||
|
match event::read()? {
|
||||||
|
Event::Key(key) if key.kind == KeyEventKind::Press => self.handle_key_press(key),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
use color_eyre::{config::HookBuilder, Result};
|
|
||||||
|
|
||||||
use crate::term;
|
|
||||||
|
|
||||||
pub fn init_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 _ = term::restore();
|
|
||||||
error(e)
|
|
||||||
}))?;
|
|
||||||
std::panic::set_hook(Box::new(move |info| {
|
|
||||||
let _ = term::restore();
|
|
||||||
panic(info);
|
|
||||||
}));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -22,12 +22,18 @@
|
||||||
mod app;
|
mod app;
|
||||||
mod colors;
|
mod colors;
|
||||||
mod destroy;
|
mod destroy;
|
||||||
mod errors;
|
|
||||||
mod tabs;
|
mod tabs;
|
||||||
mod term;
|
|
||||||
mod theme;
|
mod theme;
|
||||||
|
|
||||||
|
use std::io::stdout;
|
||||||
|
|
||||||
|
use app::App;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
|
use crossterm::{
|
||||||
|
execute,
|
||||||
|
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
|
};
|
||||||
|
use ratatui::{layout::Rect, TerminalOptions, Viewport};
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
colors::{color_from_oklab, RgbSwatch},
|
colors::{color_from_oklab, RgbSwatch},
|
||||||
|
@ -35,9 +41,14 @@ pub use self::{
|
||||||
};
|
};
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
errors::init_hooks()?;
|
color_eyre::install()?;
|
||||||
let terminal = &mut term::init()?;
|
// this size is to match the size of the terminal when running the demo
|
||||||
app::run(terminal)?;
|
// using vhs in a 1280x640 sized window (github social preview size)
|
||||||
term::restore()?;
|
let viewport = Viewport::Fixed(Rect::new(0, 0, 81, 18));
|
||||||
Ok(())
|
let terminal = ratatui::init_with_options(TerminalOptions { viewport });
|
||||||
|
execute!(stdout(), EnterAlternateScreen).expect("failed to enter alternate screen");
|
||||||
|
let app_result = App::default().run(terminal);
|
||||||
|
execute!(stdout(), LeaveAlternateScreen).expect("failed to leave alternate screen");
|
||||||
|
ratatui::restore();
|
||||||
|
app_result
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
use std::{
|
|
||||||
io::{self, stdout},
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use color_eyre::{eyre::WrapErr, Result};
|
|
||||||
use ratatui::{
|
|
||||||
backend::{Backend, CrosstermBackend},
|
|
||||||
crossterm::{
|
|
||||||
event::{self, Event},
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
ExecutableCommand,
|
|
||||||
},
|
|
||||||
layout::Rect,
|
|
||||||
Terminal, TerminalOptions, Viewport,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn init() -> Result<Terminal<impl Backend>> {
|
|
||||||
// this size is to match the size of the terminal when running the demo
|
|
||||||
// using vhs in a 1280x640 sized window (github social preview size)
|
|
||||||
let options = TerminalOptions {
|
|
||||||
viewport: Viewport::Fixed(Rect::new(0, 0, 81, 18)),
|
|
||||||
};
|
|
||||||
let terminal = Terminal::with_options(CrosstermBackend::new(io::stdout()), options)?;
|
|
||||||
enable_raw_mode().context("enable raw mode")?;
|
|
||||||
stdout()
|
|
||||||
.execute(EnterAlternateScreen)
|
|
||||||
.wrap_err("enter alternate screen")?;
|
|
||||||
Ok(terminal)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn restore() -> Result<()> {
|
|
||||||
disable_raw_mode().context("disable raw mode")?;
|
|
||||||
stdout()
|
|
||||||
.execute(LeaveAlternateScreen)
|
|
||||||
.wrap_err("leave alternate screen")?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn next_event(timeout: Duration) -> Result<Option<Event>> {
|
|
||||||
if !event::poll(timeout)? {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
let event = event::read()?;
|
|
||||||
Ok(Some(event))
|
|
||||||
}
|
|
|
@ -13,47 +13,51 @@
|
||||||
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||||
|
|
||||||
use std::io::{self, stdout};
|
use color_eyre::Result;
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::CrosstermBackend,
|
crossterm::event::{self, Event, KeyCode},
|
||||||
crossterm::{
|
|
||||||
event::{self, Event, KeyCode},
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
ExecutableCommand,
|
|
||||||
},
|
|
||||||
layout::{Constraint, Layout},
|
layout::{Constraint, Layout},
|
||||||
style::{Color, Modifier, Style, Stylize},
|
style::{Color, Modifier, Style, Stylize},
|
||||||
text::{Line, Span, Text},
|
text::{Line, Span, Text},
|
||||||
widgets::{Block, Borders, Paragraph},
|
widgets::{Block, Borders, Paragraph},
|
||||||
Frame, Terminal,
|
DefaultTerminal, Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Example code for lib.rs
|
/// Example code for lib.rs
|
||||||
///
|
///
|
||||||
/// When cargo-rdme supports doc comments that import from code, this will be imported
|
/// When cargo-rdme supports doc comments that import from code, this will be imported
|
||||||
/// rather than copied to the lib.rs file.
|
/// rather than copied to the lib.rs file.
|
||||||
fn main() -> io::Result<()> {
|
fn main() -> Result<()> {
|
||||||
let arg = std::env::args().nth(1).unwrap_or_default();
|
color_eyre::install()?;
|
||||||
enable_raw_mode()?;
|
let first_arg = std::env::args().nth(1).unwrap_or_default();
|
||||||
stdout().execute(EnterAlternateScreen)?;
|
let terminal = ratatui::init();
|
||||||
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
|
let app_result = run(terminal, &first_arg);
|
||||||
|
ratatui::restore();
|
||||||
|
app_result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(mut terminal: DefaultTerminal, first_arg: &str) -> Result<()> {
|
||||||
let mut should_quit = false;
|
let mut should_quit = false;
|
||||||
while !should_quit {
|
while !should_quit {
|
||||||
terminal.draw(match arg.as_str() {
|
terminal.draw(match first_arg {
|
||||||
"layout" => layout,
|
"layout" => layout,
|
||||||
"styling" => styling,
|
"styling" => styling,
|
||||||
_ => hello_world,
|
_ => hello_world,
|
||||||
})?;
|
})?;
|
||||||
should_quit = handle_events()?;
|
should_quit = handle_events()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
disable_raw_mode()?;
|
|
||||||
stdout().execute(LeaveAlternateScreen)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_events() -> std::io::Result<bool> {
|
||||||
|
if let Event::Key(key) = event::read()? {
|
||||||
|
if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
fn hello_world(frame: &mut Frame) {
|
fn hello_world(frame: &mut Frame) {
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
|
Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
|
||||||
|
@ -61,17 +65,6 @@ fn hello_world(frame: &mut Frame) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_events() -> io::Result<bool> {
|
|
||||||
if event::poll(std::time::Duration::from_millis(50))? {
|
|
||||||
if let Event::Key(key) = event::read()? {
|
|
||||||
if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn layout(frame: &mut Frame) {
|
fn layout(frame: &mut Frame) {
|
||||||
let vertical = Layout::vertical([
|
let vertical = Layout::vertical([
|
||||||
Constraint::Length(1),
|
Constraint::Length(1),
|
||||||
|
|
|
@ -13,20 +13,12 @@
|
||||||
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||||
|
|
||||||
use std::{
|
use std::num::NonZeroUsize;
|
||||||
io::{self, stdout},
|
|
||||||
num::NonZeroUsize,
|
|
||||||
};
|
|
||||||
|
|
||||||
use color_eyre::{config::HookBuilder, Result};
|
use color_eyre::Result;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::{Backend, CrosstermBackend},
|
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
crossterm::{
|
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||||
event::{self, Event, KeyCode, KeyEventKind},
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
ExecutableCommand,
|
|
||||||
},
|
|
||||||
layout::{
|
layout::{
|
||||||
Alignment,
|
Alignment,
|
||||||
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
|
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
|
||||||
|
@ -39,10 +31,18 @@ use ratatui::{
|
||||||
block::Title, Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState,
|
block::Title, Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState,
|
||||||
StatefulWidget, Tabs, Widget,
|
StatefulWidget, Tabs, Widget,
|
||||||
},
|
},
|
||||||
Terminal,
|
DefaultTerminal,
|
||||||
};
|
};
|
||||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
let terminal = ratatui::init();
|
||||||
|
let app_result = App::default().run(terminal);
|
||||||
|
ratatui::restore();
|
||||||
|
app_result
|
||||||
|
}
|
||||||
|
|
||||||
const EXAMPLE_DATA: &[(&str, &[Constraint])] = &[
|
const EXAMPLE_DATA: &[(&str, &[Constraint])] = &[
|
||||||
(
|
(
|
||||||
"Min(u16) takes any excess space always",
|
"Min(u16) takes any excess space always",
|
||||||
|
@ -157,25 +157,18 @@ enum SelectedTab {
|
||||||
SpaceBetween,
|
SpaceBetween,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
|
||||||
// assuming the user changes spacing about a 100 times or so
|
|
||||||
Layout::init_cache(
|
|
||||||
NonZeroUsize::new(EXAMPLE_DATA.len() * SelectedTab::iter().len() * 100).unwrap(),
|
|
||||||
);
|
|
||||||
init_error_hooks()?;
|
|
||||||
let terminal = init_terminal()?;
|
|
||||||
App::default().run(terminal)?;
|
|
||||||
|
|
||||||
restore_terminal()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
|
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
self.draw(&mut terminal)?;
|
// increase the layout cache to account for the number of layout events. This ensures that
|
||||||
|
// layout is not generally reprocessed on every frame (which would lead to possible janky
|
||||||
|
// results when there are more than one possible solution to the requested layout). This
|
||||||
|
// assumes the user changes spacing about a 100 times or so.
|
||||||
|
let cache_size = EXAMPLE_DATA.len() * SelectedTab::iter().len() * 100;
|
||||||
|
Layout::init_cache(NonZeroUsize::new(cache_size).unwrap());
|
||||||
|
|
||||||
while self.is_running() {
|
while self.is_running() {
|
||||||
|
terminal.draw(|frame| frame.render_widget(self, frame.area()))?;
|
||||||
self.handle_events()?;
|
self.handle_events()?;
|
||||||
self.draw(&mut terminal)?;
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -184,11 +177,6 @@ impl App {
|
||||||
self.state == AppState::Running
|
self.state == AppState::Running
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(self, terminal: &mut Terminal<impl Backend>) -> io::Result<()> {
|
|
||||||
terminal.draw(|frame| frame.render_widget(self, frame.area()))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_events(&mut self) -> Result<()> {
|
fn handle_events(&mut self) -> Result<()> {
|
||||||
match event::read()? {
|
match event::read()? {
|
||||||
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
|
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
|
||||||
|
@ -532,35 +520,6 @@ const fn color_for_constraint(constraint: Constraint) -> Color {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::cast_possible_truncation)]
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
fn get_description_height(s: &str) -> u16 {
|
fn get_description_height(s: &str) -> u16 {
|
||||||
if s.is_empty() {
|
if s.is_empty() {
|
||||||
|
|
|
@ -13,22 +13,17 @@
|
||||||
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||||
|
|
||||||
use std::{io::stdout, time::Duration};
|
use std::time::Duration;
|
||||||
|
|
||||||
use color_eyre::{config::HookBuilder, Result};
|
use color_eyre::Result;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::{Backend, CrosstermBackend},
|
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
crossterm::{
|
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||||
event::{self, Event, KeyCode, KeyEventKind},
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
ExecutableCommand,
|
|
||||||
},
|
|
||||||
layout::{Alignment, Constraint, Layout, Rect},
|
layout::{Alignment, Constraint, Layout, Rect},
|
||||||
style::{palette::tailwind, Color, Style, Stylize},
|
style::{palette::tailwind, Color, Style, Stylize},
|
||||||
text::Span,
|
text::Span,
|
||||||
widgets::{block::Title, Block, Borders, Gauge, Padding, Paragraph, Widget},
|
widgets::{block::Title, Block, Borders, Gauge, Padding, Paragraph, Widget},
|
||||||
Terminal,
|
DefaultTerminal,
|
||||||
};
|
};
|
||||||
|
|
||||||
const GAUGE1_COLOR: Color = tailwind::RED.c800;
|
const GAUGE1_COLOR: Color = tailwind::RED.c800;
|
||||||
|
@ -56,28 +51,23 @@ enum AppState {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
init_error_hooks()?;
|
color_eyre::install()?;
|
||||||
let terminal = init_terminal()?;
|
let terminal = ratatui::init();
|
||||||
App::default().run(terminal)?;
|
let app_result = App::default().run(terminal);
|
||||||
restore_terminal()?;
|
ratatui::restore();
|
||||||
Ok(())
|
app_result
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
|
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
while self.state != AppState::Quitting {
|
while self.state != AppState::Quitting {
|
||||||
self.draw(&mut terminal)?;
|
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
|
||||||
self.handle_events()?;
|
self.handle_events()?;
|
||||||
self.update(terminal.size()?.width);
|
self.update(terminal.size()?.width);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(&self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
|
|
||||||
terminal.draw(|f| f.render_widget(self, f.area()))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(&mut self, terminal_width: u16) {
|
fn update(&mut self, terminal_width: u16) {
|
||||||
if self.state != AppState::Started {
|
if self.state != AppState::Started {
|
||||||
return;
|
return;
|
||||||
|
@ -213,32 +203,3 @@ fn title_block(title: &str) -> Block {
|
||||||
.title(title)
|
.title(title)
|
||||||
.fg(CUSTOM_LABEL_COLOR)
|
.fg(CUSTOM_LABEL_COLOR)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_error_hooks() -> color_eyre::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() -> color_eyre::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() -> color_eyre::Result<()> {
|
|
||||||
disable_raw_mode()?;
|
|
||||||
stdout().execute(LeaveAlternateScreen)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
|
@ -13,21 +13,13 @@
|
||||||
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||||
|
|
||||||
use std::{
|
use std::time::Duration;
|
||||||
io::{self, Stdout},
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use color_eyre::{eyre::Context, Result};
|
use color_eyre::{eyre::Context, Result};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::CrosstermBackend,
|
crossterm::event::{self, Event, KeyCode},
|
||||||
crossterm::{
|
|
||||||
event::{self, Event, KeyCode},
|
|
||||||
execute,
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
},
|
|
||||||
widgets::Paragraph,
|
widgets::Paragraph,
|
||||||
Frame, Terminal,
|
DefaultTerminal, Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// This is a bare minimum example. There are many approaches to running an application loop, so
|
/// This is a bare minimum example. There are many approaches to running an application loop, so
|
||||||
|
@ -38,50 +30,19 @@ use ratatui::{
|
||||||
/// and exits when the user presses 'q'.
|
/// and exits when the user presses 'q'.
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
color_eyre::install()?; // augment errors / panics with easy to read messages
|
color_eyre::install()?; // augment errors / panics with easy to read messages
|
||||||
let mut terminal = init_terminal().context("setup failed")?;
|
let terminal = ratatui::init();
|
||||||
let result = run(&mut terminal).context("app loop failed");
|
let app_result = run(terminal).context("app loop failed");
|
||||||
restore_terminal();
|
ratatui::restore();
|
||||||
result
|
app_result
|
||||||
}
|
|
||||||
|
|
||||||
/// Setup the terminal. This is where you would enable raw mode, enter the alternate screen, and
|
|
||||||
/// hide the cursor. This example does not handle errors.
|
|
||||||
fn init_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
|
|
||||||
set_panic_hook();
|
|
||||||
let mut stdout = io::stdout();
|
|
||||||
enable_raw_mode().context("failed to enable raw mode")?;
|
|
||||||
execute!(stdout, EnterAlternateScreen).context("unable to enter alternate screen")?;
|
|
||||||
Terminal::new(CrosstermBackend::new(stdout)).context("creating terminal failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Restore the terminal. This is where you disable raw mode, leave the alternate screen, and show
|
|
||||||
/// the cursor.
|
|
||||||
fn restore_terminal() {
|
|
||||||
// There's not a lot we can do if these fail, so we just print an error message.
|
|
||||||
if let Err(err) = disable_raw_mode() {
|
|
||||||
eprintln!("Error disabling raw mode: {err}");
|
|
||||||
}
|
|
||||||
if let Err(err) = execute!(io::stdout(), LeaveAlternateScreen) {
|
|
||||||
eprintln!("Error leaving alternate screen: {err}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Replace the default panic hook with one that restores the terminal before panicking.
|
|
||||||
fn set_panic_hook() {
|
|
||||||
let hook = std::panic::take_hook();
|
|
||||||
std::panic::set_hook(Box::new(move |panic_info| {
|
|
||||||
restore_terminal();
|
|
||||||
hook(panic_info);
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the application loop. This is where you would handle events and update the application
|
/// Run the application loop. This is where you would handle events and update the application
|
||||||
/// state. This example exits when the user presses 'q'. Other styles of application loops are
|
/// state. This example exits when the user presses 'q'. Other styles of application loops are
|
||||||
/// possible, for example, you could have multiple application states and switch between them based
|
/// possible, for example, you could have multiple application states and switch between them based
|
||||||
/// on events, or you could have a single application state and update it based on events.
|
/// on events, or you could have a single application state and update it based on events.
|
||||||
fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(render_app)?;
|
terminal.draw(draw)?;
|
||||||
if should_quit()? {
|
if should_quit()? {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -89,19 +50,18 @@ fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render the application. This is where you would draw the application UI. This example just
|
/// Render the application. This is where you would draw the application UI. This example draws a
|
||||||
/// draws a greeting.
|
/// greeting.
|
||||||
fn render_app(frame: &mut Frame) {
|
fn draw(frame: &mut Frame) {
|
||||||
let greeting = Paragraph::new("Hello World! (press 'q' to quit)");
|
let greeting = Paragraph::new("Hello World! (press 'q' to quit)");
|
||||||
frame.render_widget(greeting, frame.area());
|
frame.render_widget(greeting, frame.area());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the user has pressed 'q'. This is where you would handle events. This example just
|
/// Check if the user has pressed 'q'. This is where you would handle events. This example just
|
||||||
/// checks if the user has pressed 'q' and returns true if they have. It does not handle any other
|
/// checks if the user has pressed 'q' and returns true if they have. It does not handle any other
|
||||||
/// events. There is a 250ms timeout on the event poll so that the application can exit in a timely
|
/// events. There is a 250ms timeout on the event poll to ensure that the terminal is rendered at
|
||||||
/// manner, and to ensure that the terminal is rendered at least once every 250ms. This allows you
|
/// least once every 250ms. This allows you to do other work in the application loop, such as
|
||||||
/// to do other work in the application loop, such as updating the application state, without
|
/// updating the application state, without blocking the event loop for too long.
|
||||||
/// blocking the event loop for too long.
|
|
||||||
fn should_quit() -> Result<bool> {
|
fn should_quit() -> Result<bool> {
|
||||||
if event::poll(Duration::from_millis(250)).context("event poll failed")? {
|
if event::poll(Duration::from_millis(250)).context("event poll failed")? {
|
||||||
if let Event::Key(key) = event::read().context("event read failed")? {
|
if let Event::Key(key) = event::read().context("event read failed")? {
|
||||||
|
|
|
@ -16,39 +16,24 @@
|
||||||
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||||
|
|
||||||
use std::{
|
use color_eyre::Result;
|
||||||
io::{self, stdout, Stdout},
|
|
||||||
panic,
|
|
||||||
};
|
|
||||||
|
|
||||||
use color_eyre::{
|
|
||||||
config::{EyreHook, HookBuilder, PanicHook},
|
|
||||||
eyre, Result,
|
|
||||||
};
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::CrosstermBackend,
|
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
crossterm::{
|
crossterm::event::{self, Event, KeyCode},
|
||||||
event::{self, Event, KeyCode},
|
|
||||||
execute,
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
ExecutableCommand,
|
|
||||||
},
|
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
style::Stylize,
|
style::Stylize,
|
||||||
text::{Line, Text},
|
text::{Line, Text},
|
||||||
widgets::WidgetRef,
|
widgets::Widget,
|
||||||
Terminal,
|
DefaultTerminal,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
init_error_handling()?;
|
color_eyre::install()?;
|
||||||
let mut terminal = init_terminal()?;
|
let terminal = ratatui::init();
|
||||||
let app = App::new();
|
let app_result = App::new().run(terminal);
|
||||||
app.run(&mut terminal)?;
|
ratatui::restore();
|
||||||
restore_terminal()?;
|
app_result
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct App {
|
struct App {
|
||||||
|
@ -62,7 +47,7 @@ impl App {
|
||||||
Self { hyperlink }
|
Self { hyperlink }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(self, terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> io::Result<()> {
|
fn run(self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(|frame| frame.render_widget(&self.hyperlink, frame.area()))?;
|
terminal.draw(|frame| frame.render_widget(&self.hyperlink, frame.area()))?;
|
||||||
if let Event::Key(key) = event::read()? {
|
if let Event::Key(key) = event::read()? {
|
||||||
|
@ -92,9 +77,9 @@ impl<'content> Hyperlink<'content> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WidgetRef for Hyperlink<'_> {
|
impl Widget for &Hyperlink<'_> {
|
||||||
fn render_ref(&self, area: Rect, buffer: &mut Buffer) {
|
fn render(self, area: Rect, buffer: &mut Buffer) {
|
||||||
self.text.render_ref(area, buffer);
|
(&self.text).render(area, buffer);
|
||||||
|
|
||||||
// this is a hacky workaround for https://github.com/ratatui/ratatui/issues/902, a bug
|
// this is a hacky workaround for https://github.com/ratatui/ratatui/issues/902, a bug
|
||||||
// in the terminal code that incorrectly calculates the width of ANSI escape sequences. It
|
// in the terminal code that incorrectly calculates the width of ANSI escape sequences. It
|
||||||
|
@ -114,44 +99,3 @@ impl WidgetRef for Hyperlink<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize the terminal with raw mode and alternate screen.
|
|
||||||
fn init_terminal() -> io::Result<Terminal<CrosstermBackend<Stdout>>> {
|
|
||||||
enable_raw_mode()?;
|
|
||||||
stdout().execute(EnterAlternateScreen)?;
|
|
||||||
Terminal::new(CrosstermBackend::new(stdout()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Restore the terminal to its original state.
|
|
||||||
fn restore_terminal() -> io::Result<()> {
|
|
||||||
disable_raw_mode()?;
|
|
||||||
execute!(stdout(), LeaveAlternateScreen)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initialize error handling with color-eyre.
|
|
||||||
pub fn init_error_handling() -> Result<()> {
|
|
||||||
let (panic_hook, eyre_hook) = HookBuilder::default().into_hooks();
|
|
||||||
set_panic_hook(panic_hook);
|
|
||||||
set_error_hook(eyre_hook)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Install a panic hook that restores the terminal before printing the panic.
|
|
||||||
fn set_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 set_error_hook(eyre_hook: EyreHook) -> Result<()> {
|
|
||||||
let eyre_hook = eyre_hook.into_eyre_hook();
|
|
||||||
eyre::set_hook(Box::new(move |error| {
|
|
||||||
let _ = restore_terminal();
|
|
||||||
eyre_hook(error)
|
|
||||||
}))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
|
@ -15,20 +15,16 @@
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeMap, VecDeque},
|
collections::{BTreeMap, VecDeque},
|
||||||
error::Error,
|
|
||||||
io,
|
|
||||||
sync::mpsc,
|
sync::mpsc,
|
||||||
thread,
|
thread,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use color_eyre::Result;
|
||||||
use rand::distributions::{Distribution, Uniform};
|
use rand::distributions::{Distribution, Uniform};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::{Backend, CrosstermBackend},
|
backend::Backend,
|
||||||
crossterm::{
|
crossterm::event,
|
||||||
event,
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode},
|
|
||||||
},
|
|
||||||
layout::{Alignment, Constraint, Layout, Rect},
|
layout::{Alignment, Constraint, Layout, Rect},
|
||||||
style::{Color, Modifier, Style},
|
style::{Color, Modifier, Style},
|
||||||
symbols,
|
symbols,
|
||||||
|
@ -37,11 +33,33 @@ use ratatui::{
|
||||||
Frame, Terminal, TerminalOptions, Viewport,
|
Frame, Terminal, TerminalOptions, Viewport,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
let mut terminal = ratatui::init_with_options(TerminalOptions {
|
||||||
|
viewport: Viewport::Inline(8),
|
||||||
|
});
|
||||||
|
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
input_handling(tx.clone());
|
||||||
|
let workers = workers(tx);
|
||||||
|
let mut downloads = downloads();
|
||||||
|
|
||||||
|
for w in &workers {
|
||||||
|
let d = downloads.next(w.id).unwrap();
|
||||||
|
w.tx.send(d).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let app_result = run(&mut terminal, workers, downloads, rx);
|
||||||
|
|
||||||
|
ratatui::restore();
|
||||||
|
|
||||||
|
app_result
|
||||||
|
}
|
||||||
|
|
||||||
const NUM_DOWNLOADS: usize = 10;
|
const NUM_DOWNLOADS: usize = 10;
|
||||||
|
|
||||||
type DownloadId = usize;
|
type DownloadId = usize;
|
||||||
type WorkerId = usize;
|
type WorkerId = usize;
|
||||||
|
|
||||||
enum Event {
|
enum Event {
|
||||||
Input(event::KeyEvent),
|
Input(event::KeyEvent),
|
||||||
Tick,
|
Tick,
|
||||||
|
@ -49,7 +67,6 @@ enum Event {
|
||||||
DownloadUpdate(WorkerId, DownloadId, f64),
|
DownloadUpdate(WorkerId, DownloadId, f64),
|
||||||
DownloadDone(WorkerId, DownloadId),
|
DownloadDone(WorkerId, DownloadId),
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Downloads {
|
struct Downloads {
|
||||||
pending: VecDeque<Download>,
|
pending: VecDeque<Download>,
|
||||||
in_progress: BTreeMap<WorkerId, DownloadInProgress>,
|
in_progress: BTreeMap<WorkerId, DownloadInProgress>,
|
||||||
|
@ -73,52 +90,20 @@ impl Downloads {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DownloadInProgress {
|
struct DownloadInProgress {
|
||||||
id: DownloadId,
|
id: DownloadId,
|
||||||
started_at: Instant,
|
started_at: Instant,
|
||||||
progress: f64,
|
progress: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Download {
|
struct Download {
|
||||||
id: DownloadId,
|
id: DownloadId,
|
||||||
size: usize,
|
size: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Worker {
|
struct Worker {
|
||||||
id: WorkerId,
|
id: WorkerId,
|
||||||
tx: mpsc::Sender<Download>,
|
tx: mpsc::Sender<Download>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
|
||||||
enable_raw_mode()?;
|
|
||||||
let stdout = io::stdout();
|
|
||||||
let backend = CrosstermBackend::new(stdout);
|
|
||||||
let mut terminal = Terminal::with_options(
|
|
||||||
backend,
|
|
||||||
TerminalOptions {
|
|
||||||
viewport: Viewport::Inline(8),
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let (tx, rx) = mpsc::channel();
|
|
||||||
input_handling(tx.clone());
|
|
||||||
let workers = workers(tx);
|
|
||||||
let mut downloads = downloads();
|
|
||||||
|
|
||||||
for w in &workers {
|
|
||||||
let d = downloads.next(w.id).unwrap();
|
|
||||||
w.tx.send(d).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
run_app(&mut terminal, workers, downloads, rx)?;
|
|
||||||
|
|
||||||
disable_raw_mode()?;
|
|
||||||
terminal.clear()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn input_handling(tx: mpsc::Sender<Event>) {
|
fn input_handling(tx: mpsc::Sender<Event>) {
|
||||||
let tick_rate = Duration::from_millis(200);
|
let tick_rate = Duration::from_millis(200);
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
|
@ -182,16 +167,16 @@ fn downloads() -> Downloads {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
fn run_app<B: Backend>(
|
fn run(
|
||||||
terminal: &mut Terminal<B>,
|
terminal: &mut Terminal<impl Backend>,
|
||||||
workers: Vec<Worker>,
|
workers: Vec<Worker>,
|
||||||
mut downloads: Downloads,
|
mut downloads: Downloads,
|
||||||
rx: mpsc::Receiver<Event>,
|
rx: mpsc::Receiver<Event>,
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> Result<()> {
|
||||||
let mut redraw = true;
|
let mut redraw = true;
|
||||||
loop {
|
loop {
|
||||||
if redraw {
|
if redraw {
|
||||||
terminal.draw(|f| ui(f, &downloads))?;
|
terminal.draw(|frame| draw(frame, &downloads))?;
|
||||||
}
|
}
|
||||||
redraw = true;
|
redraw = true;
|
||||||
|
|
||||||
|
@ -243,11 +228,11 @@ fn run_app<B: Backend>(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ui(f: &mut Frame, downloads: &Downloads) {
|
fn draw(frame: &mut Frame, downloads: &Downloads) {
|
||||||
let area = f.area();
|
let area = frame.area();
|
||||||
|
|
||||||
let block = Block::new().title(block::Title::from("Progress").alignment(Alignment::Center));
|
let block = Block::new().title(block::Title::from("Progress").alignment(Alignment::Center));
|
||||||
f.render_widget(block, area);
|
frame.render_widget(block, area);
|
||||||
|
|
||||||
let vertical = Layout::vertical([Constraint::Length(2), Constraint::Length(4)]).margin(1);
|
let vertical = Layout::vertical([Constraint::Length(2), Constraint::Length(4)]).margin(1);
|
||||||
let horizontal = Layout::horizontal([Constraint::Percentage(20), Constraint::Percentage(80)]);
|
let horizontal = Layout::horizontal([Constraint::Percentage(20), Constraint::Percentage(80)]);
|
||||||
|
@ -261,7 +246,7 @@ fn ui(f: &mut Frame, downloads: &Downloads) {
|
||||||
.filled_style(Style::default().fg(Color::Blue))
|
.filled_style(Style::default().fg(Color::Blue))
|
||||||
.label(format!("{done}/{NUM_DOWNLOADS}"))
|
.label(format!("{done}/{NUM_DOWNLOADS}"))
|
||||||
.ratio(done as f64 / NUM_DOWNLOADS as f64);
|
.ratio(done as f64 / NUM_DOWNLOADS as f64);
|
||||||
f.render_widget(progress, progress_area);
|
frame.render_widget(progress, progress_area);
|
||||||
|
|
||||||
// in progress downloads
|
// in progress downloads
|
||||||
let items: Vec<ListItem> = downloads
|
let items: Vec<ListItem> = downloads
|
||||||
|
@ -284,7 +269,7 @@ fn ui(f: &mut Frame, downloads: &Downloads) {
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let list = List::new(items);
|
let list = List::new(items);
|
||||||
f.render_widget(list, list_area);
|
frame.render_widget(list, list_area);
|
||||||
|
|
||||||
#[allow(clippy::cast_possible_truncation)]
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
for (i, (_, download)) in downloads.in_progress.iter().enumerate() {
|
for (i, (_, download)) in downloads.in_progress.iter().enumerate() {
|
||||||
|
@ -294,7 +279,7 @@ fn ui(f: &mut Frame, downloads: &Downloads) {
|
||||||
if gauge_area.top().saturating_add(i as u16) > area.bottom() {
|
if gauge_area.top().saturating_add(i as u16) > area.bottom() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
f.render_widget(
|
frame.render_widget(
|
||||||
gauge,
|
gauge,
|
||||||
Rect {
|
Rect {
|
||||||
x: gauge_area.left(),
|
x: gauge_area.left(),
|
||||||
|
|
|
@ -13,68 +13,40 @@
|
||||||
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||||
|
|
||||||
use std::{error::Error, io};
|
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::{Backend, CrosstermBackend},
|
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||||
crossterm::{
|
|
||||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
|
||||||
execute,
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
},
|
|
||||||
layout::{
|
layout::{
|
||||||
Constraint,
|
Constraint::{self, Length, Max, Min, Percentage, Ratio},
|
||||||
Constraint::{Length, Max, Min, Percentage, Ratio},
|
|
||||||
Layout, Rect,
|
Layout, Rect,
|
||||||
},
|
},
|
||||||
style::{Color, Style, Stylize},
|
style::{Color, Style, Stylize},
|
||||||
text::Line,
|
text::Line,
|
||||||
widgets::{Block, Paragraph},
|
widgets::{Block, Paragraph},
|
||||||
Frame, Terminal,
|
DefaultTerminal, Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
fn main() -> color_eyre::Result<()> {
|
||||||
// setup terminal
|
color_eyre::install()?;
|
||||||
enable_raw_mode()?;
|
let terminal = ratatui::init();
|
||||||
let mut stdout = io::stdout();
|
let app_result = run(terminal);
|
||||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
ratatui::restore();
|
||||||
let backend = CrosstermBackend::new(stdout);
|
app_result
|
||||||
let mut terminal = Terminal::new(backend)?;
|
|
||||||
|
|
||||||
// create app and run it
|
|
||||||
let res = run_app(&mut terminal);
|
|
||||||
|
|
||||||
// restore terminal
|
|
||||||
disable_raw_mode()?;
|
|
||||||
execute!(
|
|
||||||
terminal.backend_mut(),
|
|
||||||
LeaveAlternateScreen,
|
|
||||||
DisableMouseCapture
|
|
||||||
)?;
|
|
||||||
terminal.show_cursor()?;
|
|
||||||
|
|
||||||
if let Err(err) = res {
|
|
||||||
println!("{err:?}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
fn run(mut terminal: DefaultTerminal) -> color_eyre::Result<()> {
|
||||||
}
|
|
||||||
|
|
||||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(ui)?;
|
terminal.draw(draw)?;
|
||||||
|
|
||||||
if let Event::Key(key) = event::read()? {
|
if let Event::Key(key) = event::read()? {
|
||||||
if key.code == KeyCode::Char('q') {
|
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||||
return Ok(());
|
break Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
fn ui(frame: &mut Frame) {
|
fn draw(frame: &mut Frame) {
|
||||||
let vertical = Layout::vertical([
|
let vertical = Layout::vertical([
|
||||||
Length(4), // text
|
Length(4), // text
|
||||||
Length(50), // examples
|
Length(50), // examples
|
||||||
|
|
|
@ -13,25 +13,28 @@
|
||||||
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||||
|
|
||||||
use std::{io::stdout, time::Duration};
|
use std::time::Duration;
|
||||||
|
|
||||||
use color_eyre::{config::HookBuilder, Result};
|
use color_eyre::Result;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::{Backend, CrosstermBackend},
|
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
crossterm::{
|
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||||
event::{self, Event, KeyCode, KeyEventKind},
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
ExecutableCommand,
|
|
||||||
},
|
|
||||||
layout::{Alignment, Constraint, Layout, Rect},
|
layout::{Alignment, Constraint, Layout, Rect},
|
||||||
style::{palette::tailwind, Color, Style, Stylize},
|
style::{palette::tailwind, Color, Style, Stylize},
|
||||||
widgets::{block::Title, Block, Borders, LineGauge, Padding, Paragraph, Widget},
|
widgets::{block::Title, Block, Borders, LineGauge, Padding, Paragraph, Widget},
|
||||||
Terminal,
|
DefaultTerminal,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CUSTOM_LABEL_COLOR: Color = tailwind::SLATE.c200;
|
const CUSTOM_LABEL_COLOR: Color = tailwind::SLATE.c200;
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
let terminal = ratatui::init();
|
||||||
|
let app_result = App::default().run(terminal);
|
||||||
|
ratatui::restore();
|
||||||
|
app_result
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy)]
|
#[derive(Debug, Default, Clone, Copy)]
|
||||||
struct App {
|
struct App {
|
||||||
state: AppState,
|
state: AppState,
|
||||||
|
@ -47,29 +50,16 @@ enum AppState {
|
||||||
Quitting,
|
Quitting,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
|
||||||
init_error_hooks()?;
|
|
||||||
let terminal = init_terminal()?;
|
|
||||||
App::default().run(terminal)?;
|
|
||||||
restore_terminal()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
|
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
while self.state != AppState::Quitting {
|
while self.state != AppState::Quitting {
|
||||||
self.draw(&mut terminal)?;
|
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
|
||||||
self.handle_events()?;
|
self.handle_events()?;
|
||||||
self.update(terminal.size()?.width);
|
self.update(terminal.size()?.width);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(&self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
|
|
||||||
terminal.draw(|f| f.render_widget(self, f.area()))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(&mut self, terminal_width: u16) {
|
fn update(&mut self, terminal_width: u16) {
|
||||||
if self.state != AppState::Started {
|
if self.state != AppState::Started {
|
||||||
return;
|
return;
|
||||||
|
@ -187,32 +177,3 @@ fn title_block(title: &str) -> Block {
|
||||||
.fg(CUSTOM_LABEL_COLOR)
|
.fg(CUSTOM_LABEL_COLOR)
|
||||||
.padding(Padding::vertical(1))
|
.padding(Padding::vertical(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_error_hooks() -> color_eyre::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() -> color_eyre::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() -> color_eyre::Result<()> {
|
|
||||||
disable_raw_mode()?;
|
|
||||||
stdout().execute(LeaveAlternateScreen)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
|
@ -13,10 +13,8 @@
|
||||||
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||||
|
|
||||||
use std::{error::Error, io};
|
use color_eyre::Result;
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::Backend,
|
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind},
|
crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind},
|
||||||
layout::{Constraint, Layout, Rect},
|
layout::{Constraint, Layout, Rect},
|
||||||
|
@ -30,7 +28,7 @@ use ratatui::{
|
||||||
Block, Borders, HighlightSpacing, List, ListItem, ListState, Padding, Paragraph,
|
Block, Borders, HighlightSpacing, List, ListItem, ListState, Padding, Paragraph,
|
||||||
StatefulWidget, Widget, Wrap,
|
StatefulWidget, Widget, Wrap,
|
||||||
},
|
},
|
||||||
Terminal,
|
DefaultTerminal,
|
||||||
};
|
};
|
||||||
|
|
||||||
const TODO_HEADER_STYLE: Style = Style::new().fg(SLATE.c100).bg(BLUE.c800);
|
const TODO_HEADER_STYLE: Style = Style::new().fg(SLATE.c100).bg(BLUE.c800);
|
||||||
|
@ -40,15 +38,12 @@ const SELECTED_STYLE: Style = Style::new().bg(SLATE.c800).add_modifier(Modifier:
|
||||||
const TEXT_FG_COLOR: Color = SLATE.c200;
|
const TEXT_FG_COLOR: Color = SLATE.c200;
|
||||||
const COMPLETED_TEXT_FG_COLOR: Color = GREEN.c500;
|
const COMPLETED_TEXT_FG_COLOR: Color = GREEN.c500;
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
fn main() -> Result<()> {
|
||||||
tui::init_error_hooks()?;
|
color_eyre::install()?;
|
||||||
let terminal = tui::init_terminal()?;
|
let terminal = ratatui::init();
|
||||||
|
let app_result = App::default().run(terminal);
|
||||||
let mut app = App::default();
|
ratatui::restore();
|
||||||
app.run(terminal)?;
|
app_result
|
||||||
|
|
||||||
tui::restore_terminal()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This struct holds the current state of the app. In particular, it has the `todo_list` field
|
/// This struct holds the current state of the app. In particular, it has the `todo_list` field
|
||||||
|
@ -118,9 +113,9 @@ impl TodoItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> io::Result<()> {
|
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
while !self.should_exit {
|
while !self.should_exit {
|
||||||
terminal.draw(|f| f.render_widget(&mut *self, f.area()))?;
|
terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?;
|
||||||
if let Event::Key(key) = event::read()? {
|
if let Event::Key(key) = event::read()? {
|
||||||
self.handle_key(key);
|
self.handle_key(key);
|
||||||
};
|
};
|
||||||
|
@ -290,45 +285,3 @@ impl From<&TodoItem> for ListItem<'_> {
|
||||||
ListItem::new(line)
|
ListItem::new(line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mod tui {
|
|
||||||
use std::{io, io::stdout};
|
|
||||||
|
|
||||||
use color_eyre::config::HookBuilder;
|
|
||||||
use ratatui::{
|
|
||||||
backend::{Backend, CrosstermBackend},
|
|
||||||
crossterm::{
|
|
||||||
terminal::{
|
|
||||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
|
||||||
},
|
|
||||||
ExecutableCommand,
|
|
||||||
},
|
|
||||||
Terminal,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn init_error_hooks() -> color_eyre::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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init_terminal() -> io::Result<Terminal<impl Backend>> {
|
|
||||||
stdout().execute(EnterAlternateScreen)?;
|
|
||||||
enable_raw_mode()?;
|
|
||||||
Terminal::new(CrosstermBackend::new(stdout()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn restore_terminal() -> io::Result<()> {
|
|
||||||
stdout().execute(LeaveAlternateScreen)?;
|
|
||||||
disable_raw_mode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
//! # [Ratatui] Minimal example
|
//! # [Ratatui] Minimal example
|
||||||
//!
|
//!
|
||||||
|
//! This is a bare minimum example. There are many approaches to running an application loop, so
|
||||||
|
//! this is not meant to be prescriptive. See the [examples] folder for more complete examples.
|
||||||
|
//! In particular, the [hello-world] example is a good starting point.
|
||||||
|
//!
|
||||||
|
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||||
|
//! [hello-world]: https://github.com/ratatui-org/ratatui/blob/main/examples/hello_world.rs
|
||||||
|
//!
|
||||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
//! The latest version of this example 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
|
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||||
|
@ -14,35 +21,20 @@
|
||||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::CrosstermBackend,
|
crossterm::event::{self, Event},
|
||||||
crossterm::{
|
|
||||||
event::{self, Event, KeyCode, KeyEventKind},
|
|
||||||
execute,
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
},
|
|
||||||
text::Text,
|
text::Text,
|
||||||
Terminal,
|
Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// This is a bare minimum example. There are many approaches to running an application loop, so
|
fn main() {
|
||||||
/// this is not meant to be prescriptive. See the [examples] folder for more complete examples.
|
let mut terminal = ratatui::init();
|
||||||
/// In particular, the [hello-world] example is a good starting point.
|
|
||||||
///
|
|
||||||
/// [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
|
||||||
/// [hello-world]: https://github.com/ratatui/ratatui/blob/main/examples/hello_world.rs
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stdout()))?;
|
|
||||||
enable_raw_mode()?;
|
|
||||||
execute!(terminal.backend_mut(), EnterAlternateScreen)?;
|
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(|frame| frame.render_widget(Text::raw("Hello World!"), frame.area()))?;
|
terminal
|
||||||
if let Event::Key(key) = event::read()? {
|
.draw(|frame: &mut Frame| frame.render_widget(Text::raw("Hello World!"), frame.area()))
|
||||||
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
.expect("Failed to draw");
|
||||||
|
if matches!(event::read().expect("failed to read event"), Event::Key(_)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
ratatui::restore();
|
||||||
disable_raw_mode()?;
|
|
||||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,56 +17,40 @@
|
||||||
// It will render a grid of combinations of foreground and background colors with all
|
// It will render a grid of combinations of foreground and background colors with all
|
||||||
// modifiers applied to them.
|
// modifiers applied to them.
|
||||||
|
|
||||||
use std::{
|
use std::{error::Error, iter::once, result};
|
||||||
error::Error,
|
|
||||||
io::{self, Stdout},
|
|
||||||
iter::once,
|
|
||||||
result,
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::{Backend, CrosstermBackend},
|
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||||
crossterm::{
|
|
||||||
event::{self, Event, KeyCode},
|
|
||||||
execute,
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
},
|
|
||||||
layout::{Constraint, Layout},
|
layout::{Constraint, Layout},
|
||||||
style::{Color, Modifier, Style, Stylize},
|
style::{Color, Modifier, Style, Stylize},
|
||||||
text::Line,
|
text::Line,
|
||||||
widgets::Paragraph,
|
widgets::Paragraph,
|
||||||
Frame, Terminal,
|
DefaultTerminal, Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
type Result<T> = result::Result<T, Box<dyn Error>>;
|
type Result<T> = result::Result<T, Box<dyn Error>>;
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let mut terminal = setup_terminal()?;
|
color_eyre::install()?;
|
||||||
let res = run_app(&mut terminal);
|
let terminal = ratatui::init();
|
||||||
restore_terminal(terminal)?;
|
let app_result = run(terminal);
|
||||||
if let Err(err) = res {
|
ratatui::restore();
|
||||||
eprintln!("{err:?}");
|
app_result
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(ui)?;
|
terminal.draw(draw)?;
|
||||||
|
|
||||||
if event::poll(Duration::from_millis(250))? {
|
|
||||||
if let Event::Key(key) = event::read()? {
|
if let Event::Key(key) = event::read()? {
|
||||||
if key.code == KeyCode::Char('q') {
|
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn ui(frame: &mut Frame) {
|
fn draw(frame: &mut Frame) {
|
||||||
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
|
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
|
||||||
let [text_area, main_area] = vertical.areas(frame.area());
|
let [text_area, main_area] = vertical.areas(frame.area());
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
|
@ -114,20 +98,3 @@ fn ui(frame: &mut Frame) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
|
|
||||||
enable_raw_mode()?;
|
|
||||||
let mut stdout = io::stdout();
|
|
||||||
execute!(stdout, EnterAlternateScreen)?;
|
|
||||||
let backend = CrosstermBackend::new(stdout);
|
|
||||||
let mut terminal = Terminal::new(backend)?;
|
|
||||||
terminal.hide_cursor()?;
|
|
||||||
Ok(terminal)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn restore_terminal(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
|
||||||
disable_raw_mode()?;
|
|
||||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
|
||||||
terminal.show_cursor()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
|
@ -12,142 +12,99 @@
|
||||||
//! [Ratatui]: https://github.com/ratatui/ratatui
|
//! [Ratatui]: https://github.com/ratatui/ratatui
|
||||||
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||||
|
|
||||||
//! How to use a panic hook to reset the terminal before printing the panic to
|
|
||||||
//! the terminal.
|
|
||||||
//!
|
//!
|
||||||
//! When exiting normally or when handling `Result::Err`, we can reset the
|
//! Prior to Ratatui 0.28.1, a panic hook had to be manually set up to ensure that the terminal was
|
||||||
//! terminal manually at the end of `main` just before we print the error.
|
//! reset when a panic occurred. This was necessary because a panic would interrupt the normal
|
||||||
|
//! control flow and leave the terminal in a distorted state.
|
||||||
//!
|
//!
|
||||||
//! Because a panic interrupts the normal control flow, manually resetting the
|
//! Starting with Ratatui 0.28.1, the panic hook is automatically set up by the new `ratatui::init`
|
||||||
//! terminal at the end of `main` won't do us any good. Instead, we need to
|
//! function, so you no longer need to manually set up the panic hook. This example now demonstrates
|
||||||
//! make sure to set up a panic hook that first resets the terminal before
|
//! how the panic hook acts when it is enabled by default.
|
||||||
//! handling the panic. This both reuses the standard panic hook to ensure a
|
|
||||||
//! consistent panic handling UX and properly resets the terminal to not
|
|
||||||
//! distort the output.
|
|
||||||
//!
|
//!
|
||||||
//! That's why this example is set up to show both situations, with and without
|
//! When exiting normally or when handling `Result::Err`, we can reset the terminal manually at the
|
||||||
//! the chained panic hook, to see the difference.
|
//! end of `main` just before we print the error.
|
||||||
|
//!
|
||||||
use std::{error::Error, io};
|
//! Because a panic interrupts the normal control flow, manually resetting the terminal at the end
|
||||||
|
//! of `main` won't do us any good. Instead, we need to make sure to set up a panic hook that first
|
||||||
|
//! resets the terminal before handling the panic. This both reuses the standard panic hook to
|
||||||
|
//! ensure a consistent panic handling UX and properly resets the terminal to not distort the
|
||||||
|
//! output.
|
||||||
|
//!
|
||||||
|
//! That's why this example is set up to show both situations, with and without the panic hook, to
|
||||||
|
//! see the difference.
|
||||||
|
//!
|
||||||
|
//! For more information on how to set this up manually, see the [Color Eyre recipe] in the Ratatui
|
||||||
|
//! website.
|
||||||
|
//!
|
||||||
|
//! [Color Eyre recipe]: https://ratatui.rs/recipes/apps/color-eyre
|
||||||
|
|
||||||
|
use color_eyre::{eyre::bail, Result};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::{Backend, CrosstermBackend},
|
crossterm::event::{self, Event, KeyCode},
|
||||||
crossterm::{
|
|
||||||
event::{self, Event, KeyCode},
|
|
||||||
execute,
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
},
|
|
||||||
text::Line,
|
text::Line,
|
||||||
widgets::{Block, Paragraph},
|
widgets::{Block, Paragraph},
|
||||||
Frame, Terminal,
|
DefaultTerminal, Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
#[derive(Default)]
|
let terminal = ratatui::init();
|
||||||
|
let app_result = App::new().run(terminal);
|
||||||
|
ratatui::restore();
|
||||||
|
app_result
|
||||||
|
}
|
||||||
struct App {
|
struct App {
|
||||||
hook_enabled: bool,
|
hook_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
fn chain_hook(&mut self) {
|
const fn new() -> Self {
|
||||||
let original_hook = std::panic::take_hook();
|
Self { hook_enabled: true }
|
||||||
|
|
||||||
std::panic::set_hook(Box::new(move |panic| {
|
|
||||||
reset_terminal().unwrap();
|
|
||||||
original_hook(panic);
|
|
||||||
}));
|
|
||||||
|
|
||||||
self.hook_enabled = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
let mut terminal = init_terminal()?;
|
|
||||||
|
|
||||||
let mut app = App::default();
|
|
||||||
let res = run_tui(&mut terminal, &mut app);
|
|
||||||
|
|
||||||
reset_terminal()?;
|
|
||||||
|
|
||||||
if let Err(err) = res {
|
|
||||||
println!("{err:?}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initializes the terminal.
|
|
||||||
fn init_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> {
|
|
||||||
execute!(io::stdout(), EnterAlternateScreen)?;
|
|
||||||
enable_raw_mode()?;
|
|
||||||
|
|
||||||
let backend = CrosstermBackend::new(io::stdout());
|
|
||||||
|
|
||||||
let mut terminal = Terminal::new(backend)?;
|
|
||||||
terminal.hide_cursor()?;
|
|
||||||
|
|
||||||
Ok(terminal)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resets the terminal.
|
|
||||||
fn reset_terminal() -> Result<()> {
|
|
||||||
disable_raw_mode()?;
|
|
||||||
execute!(io::stdout(), LeaveAlternateScreen)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Runs the TUI loop.
|
|
||||||
fn run_tui<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<()> {
|
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(|f| ui(f, app))?;
|
terminal.draw(|frame| self.draw(frame))?;
|
||||||
|
|
||||||
if let Event::Key(key) = event::read()? {
|
if let Event::Key(key) = event::read()? {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Char('p') => {
|
KeyCode::Char('p') => panic!("intentional demo panic"),
|
||||||
panic!("intentional demo panic");
|
KeyCode::Char('e') => bail!("intentional demo error"),
|
||||||
}
|
KeyCode::Char('h') => {
|
||||||
|
let _ = std::panic::take_hook();
|
||||||
KeyCode::Char('e') => {
|
self.hook_enabled = false;
|
||||||
app.chain_hook();
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => {
|
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
|
KeyCode::Char('q') => return Ok(()),
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render the TUI.
|
fn draw(&self, frame: &mut Frame) {
|
||||||
fn ui(f: &mut Frame, app: &App) {
|
|
||||||
let text = vec![
|
let text = vec![
|
||||||
if app.hook_enabled {
|
if self.hook_enabled {
|
||||||
Line::from("HOOK IS CURRENTLY **ENABLED**")
|
Line::from("HOOK IS CURRENTLY **ENABLED**")
|
||||||
} else {
|
} else {
|
||||||
Line::from("HOOK IS CURRENTLY **DISABLED**")
|
Line::from("HOOK IS CURRENTLY **DISABLED**")
|
||||||
},
|
},
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
Line::from("press `p` to panic"),
|
Line::from("Press `p` to cause a panic"),
|
||||||
Line::from("press `e` to enable the terminal-resetting panic hook"),
|
Line::from("Press `e` to cause an error"),
|
||||||
Line::from("press any other key to quit without panic"),
|
Line::from("Press `h` to disable the panic hook"),
|
||||||
|
Line::from("Press `q` to quit"),
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
Line::from("when you panic without the chained hook,"),
|
Line::from("When your app panics without a panic hook, you will likely have to"),
|
||||||
Line::from("you will likely have to reset your terminal afterwards"),
|
Line::from("reset your terminal afterwards with the `reset` command"),
|
||||||
Line::from("with the `reset` command"),
|
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
Line::from("with the chained panic hook enabled,"),
|
Line::from("Try first with the panic handler enabled, and then with it disabled"),
|
||||||
Line::from("you should see the panic report as you would without ratatui"),
|
Line::from("to see the difference"),
|
||||||
Line::from(""),
|
|
||||||
Line::from("try first without the panic handler to see the difference"),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let paragraph = Paragraph::new(text)
|
let paragraph = Paragraph::new(text)
|
||||||
.block(Block::bordered().title("Panic Handler Demo"))
|
.block(Block::bordered().title("Panic Handler Demo"))
|
||||||
.centered();
|
.centered();
|
||||||
|
|
||||||
f.render_widget(paragraph, f.area());
|
frame.render_widget(paragraph, frame.area());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ use std::{
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use color_eyre::Result;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||||
|
@ -25,17 +26,15 @@ use ratatui::{
|
||||||
style::{Color, Stylize},
|
style::{Color, Stylize},
|
||||||
text::{Line, Masked, Span},
|
text::{Line, Masked, Span},
|
||||||
widgets::{Block, Paragraph, Widget, Wrap},
|
widgets::{Block, Paragraph, Widget, Wrap},
|
||||||
|
DefaultTerminal,
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::common::{init_terminal, install_hooks, restore_terminal, Tui};
|
fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
fn main() -> color_eyre::Result<()> {
|
let terminal = ratatui::init();
|
||||||
install_hooks()?;
|
let app_result = App::new().run(terminal);
|
||||||
let mut terminal = init_terminal()?;
|
ratatui::restore();
|
||||||
let mut app = App::new();
|
app_result
|
||||||
app.run(&mut terminal)?;
|
|
||||||
restore_terminal()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -59,9 +58,9 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the app until the user exits.
|
/// Run the app until the user exits.
|
||||||
fn run(&mut self, terminal: &mut Tui) -> io::Result<()> {
|
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
while !self.should_exit {
|
while !self.should_exit {
|
||||||
self.draw(terminal)?;
|
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
|
||||||
self.handle_events()?;
|
self.handle_events()?;
|
||||||
if self.last_tick.elapsed() >= Self::TICK_RATE {
|
if self.last_tick.elapsed() >= Self::TICK_RATE {
|
||||||
self.on_tick();
|
self.on_tick();
|
||||||
|
@ -71,12 +70,6 @@ impl App {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw the app to the terminal.
|
|
||||||
fn draw(&mut self, terminal: &mut Tui) -> io::Result<()> {
|
|
||||||
terminal.draw(|frame| frame.render_widget(self, frame.area()))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle events from the terminal.
|
/// Handle events from the terminal.
|
||||||
fn handle_events(&mut self) -> io::Result<()> {
|
fn handle_events(&mut self) -> io::Result<()> {
|
||||||
let timeout = Self::TICK_RATE.saturating_sub(self.last_tick.elapsed());
|
let timeout = Self::TICK_RATE.saturating_sub(self.last_tick.elapsed());
|
||||||
|
@ -96,7 +89,7 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for &mut App {
|
impl Widget for &App {
|
||||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
let areas = Layout::vertical([Constraint::Max(9); 4]).split(area);
|
let areas = Layout::vertical([Constraint::Max(9); 4]).split(area);
|
||||||
Paragraph::new(create_lines(area))
|
Paragraph::new(create_lines(area))
|
||||||
|
@ -158,75 +151,3 @@ fn create_lines(area: Rect) -> Vec<Line<'static>> {
|
||||||
]),
|
]),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A module for common functionality used in the examples.
|
|
||||||
mod common {
|
|
||||||
use std::{
|
|
||||||
io::{self, stdout, Stdout},
|
|
||||||
panic,
|
|
||||||
};
|
|
||||||
|
|
||||||
use color_eyre::{
|
|
||||||
config::{EyreHook, HookBuilder, PanicHook},
|
|
||||||
eyre,
|
|
||||||
};
|
|
||||||
use ratatui::{
|
|
||||||
backend::CrosstermBackend,
|
|
||||||
crossterm::{
|
|
||||||
terminal::{
|
|
||||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
|
||||||
},
|
|
||||||
ExecutableCommand,
|
|
||||||
},
|
|
||||||
Terminal,
|
|
||||||
};
|
|
||||||
|
|
||||||
// A simple alias for the terminal type used in this example.
|
|
||||||
pub type Tui = Terminal<CrosstermBackend<Stdout>>;
|
|
||||||
|
|
||||||
/// Initialize the terminal and enter alternate screen mode.
|
|
||||||
pub fn init_terminal() -> io::Result<Tui> {
|
|
||||||
enable_raw_mode()?;
|
|
||||||
stdout().execute(EnterAlternateScreen)?;
|
|
||||||
let backend = CrosstermBackend::new(stdout());
|
|
||||||
Terminal::new(backend)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Restore the terminal to its original state.
|
|
||||||
pub fn restore_terminal() -> io::Result<()> {
|
|
||||||
disable_raw_mode()?;
|
|
||||||
stdout().execute(LeaveAlternateScreen)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -16,68 +16,38 @@
|
||||||
// See also https://github.com/joshka/tui-popup and
|
// See also https://github.com/joshka/tui-popup and
|
||||||
// https://github.com/sephiroth74/tui-confirm-dialog
|
// https://github.com/sephiroth74/tui-confirm-dialog
|
||||||
|
|
||||||
use std::{error::Error, io};
|
use color_eyre::Result;
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::{Backend, CrosstermBackend},
|
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||||
crossterm::{
|
layout::{Constraint, Flex, Layout, Rect},
|
||||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
|
||||||
execute,
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
},
|
|
||||||
layout::{Constraint, Layout, Rect},
|
|
||||||
style::Stylize,
|
style::Stylize,
|
||||||
widgets::{Block, Clear, Paragraph, Wrap},
|
widgets::{Block, Clear, Paragraph, Wrap},
|
||||||
Frame, Terminal,
|
DefaultTerminal, Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
let terminal = ratatui::init();
|
||||||
|
let app_result = App::default().run(terminal);
|
||||||
|
ratatui::restore();
|
||||||
|
app_result
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
struct App {
|
struct App {
|
||||||
show_popup: bool,
|
show_popup: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
const fn new() -> Self {
|
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
Self { show_popup: false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
|
||||||
// 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)?;
|
|
||||||
|
|
||||||
// create app and run it
|
|
||||||
let app = App::new();
|
|
||||||
let res = run_app(&mut terminal, app);
|
|
||||||
|
|
||||||
// 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<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
|
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(|f| ui(f, &app))?;
|
terminal.draw(|frame| self.draw(frame))?;
|
||||||
|
|
||||||
if let Event::Key(key) = event::read()? {
|
if let Event::Key(key) = event::read()? {
|
||||||
if key.kind == KeyEventKind::Press {
|
if key.kind == KeyEventKind::Press {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Char('q') => return Ok(()),
|
KeyCode::Char('q') => return Ok(()),
|
||||||
KeyCode::Char('p') => app.show_popup = !app.show_popup,
|
KeyCode::Char('p') => self.show_popup = !self.show_popup,
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,13 +55,13 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ui(f: &mut Frame, app: &App) {
|
fn draw(&self, frame: &mut Frame) {
|
||||||
let area = f.area();
|
let area = frame.area();
|
||||||
|
|
||||||
let vertical = Layout::vertical([Constraint::Percentage(20), Constraint::Percentage(80)]);
|
let vertical = Layout::vertical([Constraint::Percentage(20), Constraint::Percentage(80)]);
|
||||||
let [instructions, content] = vertical.areas(area);
|
let [instructions, content] = vertical.areas(area);
|
||||||
|
|
||||||
let text = if app.show_popup {
|
let text = if self.show_popup {
|
||||||
"Press p to close the popup"
|
"Press p to close the popup"
|
||||||
} else {
|
} else {
|
||||||
"Press p to show the popup"
|
"Press p to show the popup"
|
||||||
|
@ -99,32 +69,25 @@ fn ui(f: &mut Frame, app: &App) {
|
||||||
let paragraph = Paragraph::new(text.slow_blink())
|
let paragraph = Paragraph::new(text.slow_blink())
|
||||||
.centered()
|
.centered()
|
||||||
.wrap(Wrap { trim: true });
|
.wrap(Wrap { trim: true });
|
||||||
f.render_widget(paragraph, instructions);
|
frame.render_widget(paragraph, instructions);
|
||||||
|
|
||||||
let block = Block::bordered().title("Content").on_blue();
|
let block = Block::bordered().title("Content").on_blue();
|
||||||
f.render_widget(block, content);
|
frame.render_widget(block, content);
|
||||||
|
|
||||||
if app.show_popup {
|
if self.show_popup {
|
||||||
let block = Block::bordered().title("Popup");
|
let block = Block::bordered().title("Popup");
|
||||||
let area = centered_rect(60, 20, area);
|
let area = popup_area(area, 60, 20);
|
||||||
f.render_widget(Clear, area); //this clears out the background
|
frame.render_widget(Clear, area); //this clears out the background
|
||||||
f.render_widget(block, area);
|
frame.render_widget(block, area);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// helper function to create a centered rect using up certain percentage of the available rect `r`
|
/// helper function to create a centered rect using up certain percentage of the available rect `r`
|
||||||
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
|
fn popup_area(area: Rect, percent_x: u16, percent_y: u16) -> Rect {
|
||||||
let popup_layout = Layout::vertical([
|
let vertical = Layout::vertical([Constraint::Percentage(percent_y)]).flex(Flex::Center);
|
||||||
Constraint::Percentage((100 - percent_y) / 2),
|
let horizontal = Layout::horizontal([Constraint::Percentage(percent_x)]).flex(Flex::Center);
|
||||||
Constraint::Percentage(percent_y),
|
let [area] = vertical.areas(area);
|
||||||
Constraint::Percentage((100 - percent_y) / 2),
|
let [area] = horizontal.areas(area);
|
||||||
])
|
area
|
||||||
.split(r);
|
|
||||||
|
|
||||||
Layout::horizontal([
|
|
||||||
Constraint::Percentage((100 - percent_x) / 2),
|
|
||||||
Constraint::Percentage(percent_x),
|
|
||||||
Constraint::Percentage((100 - percent_x) / 2),
|
|
||||||
])
|
|
||||||
.split(popup_layout[1])[1]
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,19 +14,14 @@
|
||||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
io::{self, stdout},
|
io::{self},
|
||||||
thread::sleep,
|
thread::sleep,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use itertools::izip;
|
use itertools::izip;
|
||||||
use ratatui::{
|
use ratatui::{widgets::Paragraph, TerminalOptions, Viewport};
|
||||||
backend::{Backend, CrosstermBackend},
|
|
||||||
crossterm::terminal::{disable_raw_mode, enable_raw_mode},
|
|
||||||
widgets::Paragraph,
|
|
||||||
Terminal, TerminalOptions, Viewport,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A fun example of using half block characters to draw a logo
|
/// A fun example of using half block characters to draw a logo
|
||||||
#[allow(clippy::many_single_char_names)]
|
#[allow(clippy::many_single_char_names)]
|
||||||
|
@ -63,23 +58,12 @@ fn logo() -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> io::Result<()> {
|
fn main() -> io::Result<()> {
|
||||||
let mut terminal = init()?;
|
let mut terminal = ratatui::init_with_options(TerminalOptions {
|
||||||
|
viewport: Viewport::Inline(3),
|
||||||
|
});
|
||||||
terminal.draw(|frame| frame.render_widget(Paragraph::new(logo()), frame.area()))?;
|
terminal.draw(|frame| frame.render_widget(Paragraph::new(logo()), frame.area()))?;
|
||||||
sleep(Duration::from_secs(5));
|
sleep(Duration::from_secs(5));
|
||||||
restore()?;
|
ratatui::restore();
|
||||||
println!();
|
println!();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init() -> io::Result<Terminal<impl Backend>> {
|
|
||||||
enable_raw_mode()?;
|
|
||||||
let options = TerminalOptions {
|
|
||||||
viewport: Viewport::Inline(3),
|
|
||||||
};
|
|
||||||
Terminal::with_options(CrosstermBackend::new(stdout()), options)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn restore() -> io::Result<()> {
|
|
||||||
disable_raw_mode()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
|
@ -15,25 +15,17 @@
|
||||||
|
|
||||||
#![warn(clippy::pedantic)]
|
#![warn(clippy::pedantic)]
|
||||||
|
|
||||||
use std::{
|
use std::time::{Duration, Instant};
|
||||||
error::Error,
|
|
||||||
io,
|
|
||||||
time::{Duration, Instant},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
use color_eyre::Result;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::{Backend, CrosstermBackend},
|
crossterm::event::{self, Event, KeyCode},
|
||||||
crossterm::{
|
|
||||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
|
||||||
execute,
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
},
|
|
||||||
layout::{Alignment, Constraint, Layout, Margin},
|
layout::{Alignment, Constraint, Layout, Margin},
|
||||||
style::{Color, Style, Stylize},
|
style::{Color, Style, Stylize},
|
||||||
symbols::scrollbar,
|
symbols::scrollbar,
|
||||||
text::{Line, Masked, Span},
|
text::{Line, Masked, Span},
|
||||||
widgets::{Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState},
|
widgets::{Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState},
|
||||||
Frame, Terminal,
|
DefaultTerminal, Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -44,43 +36,20 @@ struct App {
|
||||||
pub horizontal_scroll: usize,
|
pub horizontal_scroll: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
fn main() -> Result<()> {
|
||||||
// setup terminal
|
color_eyre::install()?;
|
||||||
enable_raw_mode()?;
|
let terminal = ratatui::init();
|
||||||
let mut stdout = io::stdout();
|
let app_result = App::default().run(terminal);
|
||||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
ratatui::restore();
|
||||||
let backend = CrosstermBackend::new(stdout);
|
app_result
|
||||||
let mut terminal = Terminal::new(backend)?;
|
}
|
||||||
|
|
||||||
// create app and run it
|
impl App {
|
||||||
|
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
let tick_rate = Duration::from_millis(250);
|
let tick_rate = Duration::from_millis(250);
|
||||||
let app = App::default();
|
|
||||||
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<B: Backend>(
|
|
||||||
terminal: &mut Terminal<B>,
|
|
||||||
mut app: App,
|
|
||||||
tick_rate: Duration,
|
|
||||||
) -> io::Result<()> {
|
|
||||||
let mut last_tick = Instant::now();
|
let mut last_tick = Instant::now();
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(|f| ui(f, &mut app))?;
|
terminal.draw(|frame| self.draw(frame))?;
|
||||||
|
|
||||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||||
if event::poll(timeout)? {
|
if event::poll(timeout)? {
|
||||||
|
@ -88,24 +57,26 @@ fn run_app<B: Backend>(
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Char('q') => return Ok(()),
|
KeyCode::Char('q') => return Ok(()),
|
||||||
KeyCode::Char('j') | KeyCode::Down => {
|
KeyCode::Char('j') | KeyCode::Down => {
|
||||||
app.vertical_scroll = app.vertical_scroll.saturating_add(1);
|
self.vertical_scroll = self.vertical_scroll.saturating_add(1);
|
||||||
app.vertical_scroll_state =
|
self.vertical_scroll_state =
|
||||||
app.vertical_scroll_state.position(app.vertical_scroll);
|
self.vertical_scroll_state.position(self.vertical_scroll);
|
||||||
}
|
}
|
||||||
KeyCode::Char('k') | KeyCode::Up => {
|
KeyCode::Char('k') | KeyCode::Up => {
|
||||||
app.vertical_scroll = app.vertical_scroll.saturating_sub(1);
|
self.vertical_scroll = self.vertical_scroll.saturating_sub(1);
|
||||||
app.vertical_scroll_state =
|
self.vertical_scroll_state =
|
||||||
app.vertical_scroll_state.position(app.vertical_scroll);
|
self.vertical_scroll_state.position(self.vertical_scroll);
|
||||||
}
|
}
|
||||||
KeyCode::Char('h') | KeyCode::Left => {
|
KeyCode::Char('h') | KeyCode::Left => {
|
||||||
app.horizontal_scroll = app.horizontal_scroll.saturating_sub(1);
|
self.horizontal_scroll = self.horizontal_scroll.saturating_sub(1);
|
||||||
app.horizontal_scroll_state =
|
self.horizontal_scroll_state = self
|
||||||
app.horizontal_scroll_state.position(app.horizontal_scroll);
|
.horizontal_scroll_state
|
||||||
|
.position(self.horizontal_scroll);
|
||||||
}
|
}
|
||||||
KeyCode::Char('l') | KeyCode::Right => {
|
KeyCode::Char('l') | KeyCode::Right => {
|
||||||
app.horizontal_scroll = app.horizontal_scroll.saturating_add(1);
|
self.horizontal_scroll = self.horizontal_scroll.saturating_add(1);
|
||||||
app.horizontal_scroll_state =
|
self.horizontal_scroll_state = self
|
||||||
app.horizontal_scroll_state.position(app.horizontal_scroll);
|
.horizontal_scroll_state
|
||||||
|
.position(self.horizontal_scroll);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
@ -118,11 +89,12 @@ fn run_app<B: Backend>(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines, clippy::cast_possible_truncation)]
|
#[allow(clippy::too_many_lines, clippy::cast_possible_truncation)]
|
||||||
fn ui(f: &mut Frame, app: &mut App) {
|
fn draw(&mut self, frame: &mut Frame) {
|
||||||
let area = f.area();
|
let area = frame.area();
|
||||||
|
|
||||||
// Words made "loooong" to demonstrate line breaking.
|
// Words made "loooong" to demonstrate line breaking.
|
||||||
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
|
let s =
|
||||||
|
"Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
|
||||||
let mut long_line = s.repeat(usize::from(area.width) / s.len() + 4);
|
let mut long_line = s.repeat(usize::from(area.width) / s.len() + 4);
|
||||||
long_line.push('\n');
|
long_line.push('\n');
|
||||||
|
|
||||||
|
@ -157,27 +129,27 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||||
Span::styled(Masked::new("password", '*'), Style::new().fg(Color::Red)),
|
Span::styled(Masked::new("password", '*'), Style::new().fg(Color::Red)),
|
||||||
]),
|
]),
|
||||||
];
|
];
|
||||||
app.vertical_scroll_state = app.vertical_scroll_state.content_length(text.len());
|
self.vertical_scroll_state = self.vertical_scroll_state.content_length(text.len());
|
||||||
app.horizontal_scroll_state = app.horizontal_scroll_state.content_length(long_line.len());
|
self.horizontal_scroll_state = self.horizontal_scroll_state.content_length(long_line.len());
|
||||||
|
|
||||||
let create_block = |title: &'static str| Block::bordered().gray().title(title.bold());
|
let create_block = |title: &'static str| Block::bordered().gray().title(title.bold());
|
||||||
|
|
||||||
let title = Block::new()
|
let title = Block::new()
|
||||||
.title_alignment(Alignment::Center)
|
.title_alignment(Alignment::Center)
|
||||||
.title("Use h j k l or ◄ ▲ ▼ ► to scroll ".bold());
|
.title("Use h j k l or ◄ ▲ ▼ ► to scroll ".bold());
|
||||||
f.render_widget(title, chunks[0]);
|
frame.render_widget(title, chunks[0]);
|
||||||
|
|
||||||
let paragraph = Paragraph::new(text.clone())
|
let paragraph = Paragraph::new(text.clone())
|
||||||
.gray()
|
.gray()
|
||||||
.block(create_block("Vertical scrollbar with arrows"))
|
.block(create_block("Vertical scrollbar with arrows"))
|
||||||
.scroll((app.vertical_scroll as u16, 0));
|
.scroll((self.vertical_scroll as u16, 0));
|
||||||
f.render_widget(paragraph, chunks[1]);
|
frame.render_widget(paragraph, chunks[1]);
|
||||||
f.render_stateful_widget(
|
frame.render_stateful_widget(
|
||||||
Scrollbar::new(ScrollbarOrientation::VerticalRight)
|
Scrollbar::new(ScrollbarOrientation::VerticalRight)
|
||||||
.begin_symbol(Some("↑"))
|
.begin_symbol(Some("↑"))
|
||||||
.end_symbol(Some("↓")),
|
.end_symbol(Some("↓")),
|
||||||
chunks[1],
|
chunks[1],
|
||||||
&mut app.vertical_scroll_state,
|
&mut self.vertical_scroll_state,
|
||||||
);
|
);
|
||||||
|
|
||||||
let paragraph = Paragraph::new(text.clone())
|
let paragraph = Paragraph::new(text.clone())
|
||||||
|
@ -185,9 +157,9 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||||
.block(create_block(
|
.block(create_block(
|
||||||
"Vertical scrollbar without arrows, without track symbol and mirrored",
|
"Vertical scrollbar without arrows, without track symbol and mirrored",
|
||||||
))
|
))
|
||||||
.scroll((app.vertical_scroll as u16, 0));
|
.scroll((self.vertical_scroll as u16, 0));
|
||||||
f.render_widget(paragraph, chunks[2]);
|
frame.render_widget(paragraph, chunks[2]);
|
||||||
f.render_stateful_widget(
|
frame.render_stateful_widget(
|
||||||
Scrollbar::new(ScrollbarOrientation::VerticalLeft)
|
Scrollbar::new(ScrollbarOrientation::VerticalLeft)
|
||||||
.symbols(scrollbar::VERTICAL)
|
.symbols(scrollbar::VERTICAL)
|
||||||
.begin_symbol(None)
|
.begin_symbol(None)
|
||||||
|
@ -197,7 +169,7 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||||
vertical: 1,
|
vertical: 1,
|
||||||
horizontal: 0,
|
horizontal: 0,
|
||||||
}),
|
}),
|
||||||
&mut app.vertical_scroll_state,
|
&mut self.vertical_scroll_state,
|
||||||
);
|
);
|
||||||
|
|
||||||
let paragraph = Paragraph::new(text.clone())
|
let paragraph = Paragraph::new(text.clone())
|
||||||
|
@ -205,9 +177,9 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||||
.block(create_block(
|
.block(create_block(
|
||||||
"Horizontal scrollbar with only begin arrow & custom thumb symbol",
|
"Horizontal scrollbar with only begin arrow & custom thumb symbol",
|
||||||
))
|
))
|
||||||
.scroll((0, app.horizontal_scroll as u16));
|
.scroll((0, self.horizontal_scroll as u16));
|
||||||
f.render_widget(paragraph, chunks[3]);
|
frame.render_widget(paragraph, chunks[3]);
|
||||||
f.render_stateful_widget(
|
frame.render_stateful_widget(
|
||||||
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
|
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
|
||||||
.thumb_symbol("🬋")
|
.thumb_symbol("🬋")
|
||||||
.end_symbol(None),
|
.end_symbol(None),
|
||||||
|
@ -215,7 +187,7 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||||
vertical: 0,
|
vertical: 0,
|
||||||
horizontal: 1,
|
horizontal: 1,
|
||||||
}),
|
}),
|
||||||
&mut app.horizontal_scroll_state,
|
&mut self.horizontal_scroll_state,
|
||||||
);
|
);
|
||||||
|
|
||||||
let paragraph = Paragraph::new(text.clone())
|
let paragraph = Paragraph::new(text.clone())
|
||||||
|
@ -223,9 +195,9 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||||
.block(create_block(
|
.block(create_block(
|
||||||
"Horizontal scrollbar without arrows & custom thumb and track symbol",
|
"Horizontal scrollbar without arrows & custom thumb and track symbol",
|
||||||
))
|
))
|
||||||
.scroll((0, app.horizontal_scroll as u16));
|
.scroll((0, self.horizontal_scroll as u16));
|
||||||
f.render_widget(paragraph, chunks[4]);
|
frame.render_widget(paragraph, chunks[4]);
|
||||||
f.render_stateful_widget(
|
frame.render_stateful_widget(
|
||||||
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
|
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
|
||||||
.thumb_symbol("░")
|
.thumb_symbol("░")
|
||||||
.track_symbol(Some("─")),
|
.track_symbol(Some("─")),
|
||||||
|
@ -233,6 +205,7 @@ fn ui(f: &mut Frame, app: &mut App) {
|
||||||
vertical: 0,
|
vertical: 0,
|
||||||
horizontal: 1,
|
horizontal: 1,
|
||||||
}),
|
}),
|
||||||
&mut app.horizontal_scroll_state,
|
&mut self.horizontal_scroll_state,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -13,29 +13,36 @@
|
||||||
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||||
|
|
||||||
use std::{
|
use std::time::{Duration, Instant};
|
||||||
error::Error,
|
|
||||||
io,
|
|
||||||
time::{Duration, Instant},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
use color_eyre::Result;
|
||||||
use rand::{
|
use rand::{
|
||||||
distributions::{Distribution, Uniform},
|
distributions::{Distribution, Uniform},
|
||||||
rngs::ThreadRng,
|
rngs::ThreadRng,
|
||||||
};
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::{Backend, CrosstermBackend},
|
crossterm::event::{self, Event, KeyCode},
|
||||||
crossterm::{
|
|
||||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
|
||||||
execute,
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
},
|
|
||||||
layout::{Constraint, Layout},
|
layout::{Constraint, Layout},
|
||||||
style::{Color, Style},
|
style::{Color, Style},
|
||||||
widgets::{Block, Borders, Sparkline},
|
widgets::{Block, Borders, Sparkline},
|
||||||
Frame, Terminal,
|
DefaultTerminal, Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
let terminal = ratatui::init();
|
||||||
|
let app_result = App::new().run(terminal);
|
||||||
|
ratatui::restore();
|
||||||
|
app_result
|
||||||
|
}
|
||||||
|
|
||||||
|
struct App {
|
||||||
|
signal: RandomSignal,
|
||||||
|
data1: Vec<u64>,
|
||||||
|
data2: Vec<u64>,
|
||||||
|
data3: Vec<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct RandomSignal {
|
struct RandomSignal {
|
||||||
distribution: Uniform<u64>,
|
distribution: Uniform<u64>,
|
||||||
|
@ -58,13 +65,6 @@ impl Iterator for RandomSignal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct App {
|
|
||||||
signal: RandomSignal,
|
|
||||||
data1: Vec<u64>,
|
|
||||||
data2: Vec<u64>,
|
|
||||||
data3: Vec<u64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
let mut signal = RandomSignal::new(0, 100);
|
let mut signal = RandomSignal::new(0, 100);
|
||||||
|
@ -90,45 +90,13 @@ impl App {
|
||||||
self.data3.pop();
|
self.data3.pop();
|
||||||
self.data3.insert(0, value);
|
self.data3.insert(0, value);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
// 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)?;
|
|
||||||
|
|
||||||
// create app and run it
|
|
||||||
let tick_rate = Duration::from_millis(250);
|
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<B: Backend>(
|
|
||||||
terminal: &mut Terminal<B>,
|
|
||||||
mut app: App,
|
|
||||||
tick_rate: Duration,
|
|
||||||
) -> io::Result<()> {
|
|
||||||
let mut last_tick = Instant::now();
|
let mut last_tick = Instant::now();
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(|f| ui(f, &app))?;
|
terminal.draw(|frame| self.draw(frame))?;
|
||||||
|
|
||||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||||
if event::poll(timeout)? {
|
if event::poll(timeout)? {
|
||||||
|
@ -139,37 +107,37 @@ fn run_app<B: Backend>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if last_tick.elapsed() >= tick_rate {
|
if last_tick.elapsed() >= tick_rate {
|
||||||
app.on_tick();
|
self.on_tick();
|
||||||
last_tick = Instant::now();
|
last_tick = Instant::now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ui(f: &mut Frame, app: &App) {
|
fn draw(&self, frame: &mut Frame) {
|
||||||
let chunks = Layout::vertical([
|
let chunks = Layout::vertical([
|
||||||
Constraint::Length(3),
|
Constraint::Length(3),
|
||||||
Constraint::Length(3),
|
Constraint::Length(3),
|
||||||
Constraint::Min(0),
|
Constraint::Min(0),
|
||||||
])
|
])
|
||||||
.split(f.area());
|
.split(frame.area());
|
||||||
let sparkline = Sparkline::default()
|
let sparkline = Sparkline::default()
|
||||||
.block(
|
.block(
|
||||||
Block::new()
|
Block::new()
|
||||||
.borders(Borders::LEFT | Borders::RIGHT)
|
.borders(Borders::LEFT | Borders::RIGHT)
|
||||||
.title("Data1"),
|
.title("Data1"),
|
||||||
)
|
)
|
||||||
.data(&app.data1)
|
.data(&self.data1)
|
||||||
.style(Style::default().fg(Color::Yellow));
|
.style(Style::default().fg(Color::Yellow));
|
||||||
f.render_widget(sparkline, chunks[0]);
|
frame.render_widget(sparkline, chunks[0]);
|
||||||
let sparkline = Sparkline::default()
|
let sparkline = Sparkline::default()
|
||||||
.block(
|
.block(
|
||||||
Block::new()
|
Block::new()
|
||||||
.borders(Borders::LEFT | Borders::RIGHT)
|
.borders(Borders::LEFT | Borders::RIGHT)
|
||||||
.title("Data2"),
|
.title("Data2"),
|
||||||
)
|
)
|
||||||
.data(&app.data2)
|
.data(&self.data2)
|
||||||
.style(Style::default().bg(Color::Green));
|
.style(Style::default().bg(Color::Green));
|
||||||
f.render_widget(sparkline, chunks[1]);
|
frame.render_widget(sparkline, chunks[1]);
|
||||||
// Multiline
|
// Multiline
|
||||||
let sparkline = Sparkline::default()
|
let sparkline = Sparkline::default()
|
||||||
.block(
|
.block(
|
||||||
|
@ -177,7 +145,8 @@ fn ui(f: &mut Frame, app: &App) {
|
||||||
.borders(Borders::LEFT | Borders::RIGHT)
|
.borders(Borders::LEFT | Borders::RIGHT)
|
||||||
.title("Data3"),
|
.title("Data3"),
|
||||||
)
|
)
|
||||||
.data(&app.data3)
|
.data(&self.data3)
|
||||||
.style(Style::default().fg(Color::Red));
|
.style(Style::default().fg(Color::Red));
|
||||||
f.render_widget(sparkline, chunks[2]);
|
frame.render_widget(sparkline, chunks[2]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,16 +13,10 @@
|
||||||
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||||
|
|
||||||
use std::{error::Error, io};
|
use color_eyre::Result;
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::{Backend, CrosstermBackend},
|
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||||
crossterm::{
|
|
||||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
|
||||||
execute,
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
},
|
|
||||||
layout::{Constraint, Layout, Margin, Rect},
|
layout::{Constraint, Layout, Margin, Rect},
|
||||||
style::{self, Color, Modifier, Style, Stylize},
|
style::{self, Color, Modifier, Style, Stylize},
|
||||||
text::{Line, Text},
|
text::{Line, Text},
|
||||||
|
@ -30,7 +24,7 @@ use ratatui::{
|
||||||
Block, BorderType, Cell, HighlightSpacing, Paragraph, Row, Scrollbar, ScrollbarOrientation,
|
Block, BorderType, Cell, HighlightSpacing, Paragraph, Row, Scrollbar, ScrollbarOrientation,
|
||||||
ScrollbarState, Table, TableState,
|
ScrollbarState, Table, TableState,
|
||||||
},
|
},
|
||||||
Frame, Terminal,
|
DefaultTerminal, Frame,
|
||||||
};
|
};
|
||||||
use style::palette::tailwind;
|
use style::palette::tailwind;
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
@ -46,6 +40,13 @@ const INFO_TEXT: &str =
|
||||||
|
|
||||||
const ITEM_HEIGHT: usize = 4;
|
const ITEM_HEIGHT: usize = 4;
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
let terminal = ratatui::init();
|
||||||
|
let app_result = App::new().run(terminal);
|
||||||
|
ratatui::restore();
|
||||||
|
app_result
|
||||||
|
}
|
||||||
struct TableColors {
|
struct TableColors {
|
||||||
buffer_bg: Color,
|
buffer_bg: Color,
|
||||||
header_bg: Color,
|
header_bg: Color,
|
||||||
|
@ -117,6 +118,7 @@ impl App {
|
||||||
items: data_vec,
|
items: data_vec,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next(&mut self) {
|
pub fn next(&mut self) {
|
||||||
let i = match self.state.selected() {
|
let i = match self.state.selected() {
|
||||||
Some(i) => {
|
Some(i) => {
|
||||||
|
@ -159,6 +161,115 @@ impl App {
|
||||||
pub fn set_colors(&mut self) {
|
pub fn set_colors(&mut self) {
|
||||||
self.colors = TableColors::new(&PALETTES[self.color_index]);
|
self.colors = TableColors::new(&PALETTES[self.color_index]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
|
loop {
|
||||||
|
terminal.draw(|frame| self.draw(frame))?;
|
||||||
|
|
||||||
|
if let Event::Key(key) = event::read()? {
|
||||||
|
if key.kind == KeyEventKind::Press {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
|
||||||
|
KeyCode::Char('j') | KeyCode::Down => self.next(),
|
||||||
|
KeyCode::Char('k') | KeyCode::Up => self.previous(),
|
||||||
|
KeyCode::Char('l') | KeyCode::Right => self.next_color(),
|
||||||
|
KeyCode::Char('h') | KeyCode::Left => self.previous_color(),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&mut self, frame: &mut Frame) {
|
||||||
|
let vertical = &Layout::vertical([Constraint::Min(5), Constraint::Length(3)]);
|
||||||
|
let rects = vertical.split(frame.area());
|
||||||
|
|
||||||
|
self.set_colors();
|
||||||
|
|
||||||
|
self.render_table(frame, rects[0]);
|
||||||
|
self.render_scrollbar(frame, rects[0]);
|
||||||
|
self.render_footer(frame, rects[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_table(&mut self, frame: &mut Frame, area: Rect) {
|
||||||
|
let header_style = Style::default()
|
||||||
|
.fg(self.colors.header_fg)
|
||||||
|
.bg(self.colors.header_bg);
|
||||||
|
let selected_style = Style::default()
|
||||||
|
.add_modifier(Modifier::REVERSED)
|
||||||
|
.fg(self.colors.selected_style_fg);
|
||||||
|
|
||||||
|
let header = ["Name", "Address", "Email"]
|
||||||
|
.into_iter()
|
||||||
|
.map(Cell::from)
|
||||||
|
.collect::<Row>()
|
||||||
|
.style(header_style)
|
||||||
|
.height(1);
|
||||||
|
let rows = self.items.iter().enumerate().map(|(i, data)| {
|
||||||
|
let color = match i % 2 {
|
||||||
|
0 => self.colors.normal_row_color,
|
||||||
|
_ => self.colors.alt_row_color,
|
||||||
|
};
|
||||||
|
let item = data.ref_array();
|
||||||
|
item.into_iter()
|
||||||
|
.map(|content| Cell::from(Text::from(format!("\n{content}\n"))))
|
||||||
|
.collect::<Row>()
|
||||||
|
.style(Style::new().fg(self.colors.row_fg).bg(color))
|
||||||
|
.height(4)
|
||||||
|
});
|
||||||
|
let bar = " █ ";
|
||||||
|
let t = Table::new(
|
||||||
|
rows,
|
||||||
|
[
|
||||||
|
// + 1 is for padding.
|
||||||
|
Constraint::Length(self.longest_item_lens.0 + 1),
|
||||||
|
Constraint::Min(self.longest_item_lens.1 + 1),
|
||||||
|
Constraint::Min(self.longest_item_lens.2),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.header(header)
|
||||||
|
.highlight_style(selected_style)
|
||||||
|
.highlight_symbol(Text::from(vec![
|
||||||
|
"".into(),
|
||||||
|
bar.into(),
|
||||||
|
bar.into(),
|
||||||
|
"".into(),
|
||||||
|
]))
|
||||||
|
.bg(self.colors.buffer_bg)
|
||||||
|
.highlight_spacing(HighlightSpacing::Always);
|
||||||
|
frame.render_stateful_widget(t, area, &mut self.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_scrollbar(&mut self, frame: &mut Frame, area: Rect) {
|
||||||
|
frame.render_stateful_widget(
|
||||||
|
Scrollbar::default()
|
||||||
|
.orientation(ScrollbarOrientation::VerticalRight)
|
||||||
|
.begin_symbol(None)
|
||||||
|
.end_symbol(None),
|
||||||
|
area.inner(Margin {
|
||||||
|
vertical: 1,
|
||||||
|
horizontal: 1,
|
||||||
|
}),
|
||||||
|
&mut self.scroll_state,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_footer(&self, frame: &mut Frame, area: Rect) {
|
||||||
|
let info_footer = Paragraph::new(Line::from(INFO_TEXT))
|
||||||
|
.style(
|
||||||
|
Style::new()
|
||||||
|
.fg(self.colors.row_fg)
|
||||||
|
.bg(self.colors.buffer_bg),
|
||||||
|
)
|
||||||
|
.centered()
|
||||||
|
.block(
|
||||||
|
Block::bordered()
|
||||||
|
.border_type(BorderType::Double)
|
||||||
|
.border_style(Style::new().fg(self.colors.footer_border_color)),
|
||||||
|
);
|
||||||
|
frame.render_widget(info_footer, area);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_fake_names() -> Vec<Data> {
|
fn generate_fake_names() -> Vec<Data> {
|
||||||
|
@ -186,114 +297,6 @@ fn generate_fake_names() -> Vec<Data> {
|
||||||
.collect_vec()
|
.collect_vec()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
|
||||||
// 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)?;
|
|
||||||
|
|
||||||
// create app and run it
|
|
||||||
let app = App::new();
|
|
||||||
let res = run_app(&mut terminal, app);
|
|
||||||
|
|
||||||
// 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<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
|
|
||||||
loop {
|
|
||||||
terminal.draw(|f| ui(f, &mut app))?;
|
|
||||||
|
|
||||||
if let Event::Key(key) = event::read()? {
|
|
||||||
if key.kind == KeyEventKind::Press {
|
|
||||||
match key.code {
|
|
||||||
KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
|
|
||||||
KeyCode::Char('j') | KeyCode::Down => app.next(),
|
|
||||||
KeyCode::Char('k') | KeyCode::Up => app.previous(),
|
|
||||||
KeyCode::Char('l') | KeyCode::Right => app.next_color(),
|
|
||||||
KeyCode::Char('h') | KeyCode::Left => app.previous_color(),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ui(f: &mut Frame, app: &mut App) {
|
|
||||||
let rects = Layout::vertical([Constraint::Min(5), Constraint::Length(3)]).split(f.area());
|
|
||||||
|
|
||||||
app.set_colors();
|
|
||||||
|
|
||||||
render_table(f, app, rects[0]);
|
|
||||||
|
|
||||||
render_scrollbar(f, app, rects[0]);
|
|
||||||
|
|
||||||
render_footer(f, app, rects[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_table(f: &mut Frame, app: &mut App, area: Rect) {
|
|
||||||
let header_style = Style::default()
|
|
||||||
.fg(app.colors.header_fg)
|
|
||||||
.bg(app.colors.header_bg);
|
|
||||||
let selected_style = Style::default()
|
|
||||||
.add_modifier(Modifier::REVERSED)
|
|
||||||
.fg(app.colors.selected_style_fg);
|
|
||||||
|
|
||||||
let header = ["Name", "Address", "Email"]
|
|
||||||
.into_iter()
|
|
||||||
.map(Cell::from)
|
|
||||||
.collect::<Row>()
|
|
||||||
.style(header_style)
|
|
||||||
.height(1);
|
|
||||||
let rows = app.items.iter().enumerate().map(|(i, data)| {
|
|
||||||
let color = match i % 2 {
|
|
||||||
0 => app.colors.normal_row_color,
|
|
||||||
_ => app.colors.alt_row_color,
|
|
||||||
};
|
|
||||||
let item = data.ref_array();
|
|
||||||
item.into_iter()
|
|
||||||
.map(|content| Cell::from(Text::from(format!("\n{content}\n"))))
|
|
||||||
.collect::<Row>()
|
|
||||||
.style(Style::new().fg(app.colors.row_fg).bg(color))
|
|
||||||
.height(4)
|
|
||||||
});
|
|
||||||
let bar = " █ ";
|
|
||||||
let t = Table::new(
|
|
||||||
rows,
|
|
||||||
[
|
|
||||||
// + 1 is for padding.
|
|
||||||
Constraint::Length(app.longest_item_lens.0 + 1),
|
|
||||||
Constraint::Min(app.longest_item_lens.1 + 1),
|
|
||||||
Constraint::Min(app.longest_item_lens.2),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.header(header)
|
|
||||||
.highlight_style(selected_style)
|
|
||||||
.highlight_symbol(Text::from(vec![
|
|
||||||
"".into(),
|
|
||||||
bar.into(),
|
|
||||||
bar.into(),
|
|
||||||
"".into(),
|
|
||||||
]))
|
|
||||||
.bg(app.colors.buffer_bg)
|
|
||||||
.highlight_spacing(HighlightSpacing::Always);
|
|
||||||
f.render_stateful_widget(t, area, &mut app.state);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn constraint_len_calculator(items: &[Data]) -> (u16, u16, u16) {
|
fn constraint_len_calculator(items: &[Data]) -> (u16, u16, u16) {
|
||||||
let name_len = items
|
let name_len = items
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -319,32 +322,6 @@ fn constraint_len_calculator(items: &[Data]) -> (u16, u16, u16) {
|
||||||
(name_len as u16, address_len as u16, email_len as u16)
|
(name_len as u16, address_len as u16, email_len as u16)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_scrollbar(f: &mut Frame, app: &mut App, area: Rect) {
|
|
||||||
f.render_stateful_widget(
|
|
||||||
Scrollbar::default()
|
|
||||||
.orientation(ScrollbarOrientation::VerticalRight)
|
|
||||||
.begin_symbol(None)
|
|
||||||
.end_symbol(None),
|
|
||||||
area.inner(Margin {
|
|
||||||
vertical: 1,
|
|
||||||
horizontal: 1,
|
|
||||||
}),
|
|
||||||
&mut app.scroll_state,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_footer(f: &mut Frame, app: &App, area: Rect) {
|
|
||||||
let info_footer = Paragraph::new(Line::from(INFO_TEXT))
|
|
||||||
.style(Style::new().fg(app.colors.row_fg).bg(app.colors.buffer_bg))
|
|
||||||
.centered()
|
|
||||||
.block(
|
|
||||||
Block::bordered()
|
|
||||||
.border_type(BorderType::Double)
|
|
||||||
.border_style(Style::new().fg(app.colors.footer_border_color)),
|
|
||||||
);
|
|
||||||
f.render_widget(info_footer, area);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::Data;
|
use crate::Data;
|
||||||
|
|
|
@ -13,26 +13,27 @@
|
||||||
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||||
|
|
||||||
use std::io::stdout;
|
use color_eyre::Result;
|
||||||
|
|
||||||
use color_eyre::{config::HookBuilder, Result};
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::{Backend, CrosstermBackend},
|
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
crossterm::{
|
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||||
event::{self, Event, KeyCode, KeyEventKind},
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
ExecutableCommand,
|
|
||||||
},
|
|
||||||
layout::{Constraint, Layout, Rect},
|
layout::{Constraint, Layout, Rect},
|
||||||
style::{palette::tailwind, Color, Stylize},
|
style::{palette::tailwind, Color, Stylize},
|
||||||
symbols,
|
symbols,
|
||||||
text::Line,
|
text::Line,
|
||||||
widgets::{Block, Padding, Paragraph, Tabs, Widget},
|
widgets::{Block, Padding, Paragraph, Tabs, Widget},
|
||||||
Terminal,
|
DefaultTerminal,
|
||||||
};
|
};
|
||||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
let terminal = ratatui::init();
|
||||||
|
let app_result = App::default().run(terminal);
|
||||||
|
ratatui::restore();
|
||||||
|
app_result
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct App {
|
struct App {
|
||||||
state: AppState,
|
state: AppState,
|
||||||
|
@ -59,28 +60,15 @@ enum SelectedTab {
|
||||||
Tab4,
|
Tab4,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
|
||||||
init_error_hooks()?;
|
|
||||||
let mut terminal = init_terminal()?;
|
|
||||||
App::default().run(&mut terminal)?;
|
|
||||||
restore_terminal()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
fn run(&mut self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
|
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
while self.state == AppState::Running {
|
while self.state == AppState::Running {
|
||||||
self.draw(terminal)?;
|
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
|
||||||
self.handle_events()?;
|
self.handle_events()?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(&self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
|
|
||||||
terminal.draw(|frame| frame.render_widget(self, frame.area()))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_events(&mut self) -> std::io::Result<()> {
|
fn handle_events(&mut self) -> std::io::Result<()> {
|
||||||
if let Event::Key(key) = event::read()? {
|
if let Event::Key(key) = event::read()? {
|
||||||
if key.kind == KeyEventKind::Press {
|
if key.kind == KeyEventKind::Press {
|
||||||
|
@ -226,32 +214,3 @@ impl SelectedTab {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_error_hooks() -> color_eyre::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() -> color_eyre::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() -> color_eyre::Result<()> {
|
|
||||||
disable_raw_mode()?;
|
|
||||||
stdout().execute(LeaveAlternateScreen)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
|
@ -27,39 +27,32 @@
|
||||||
// [tracing]: https://crates.io/crates/tracing
|
// [tracing]: https://crates.io/crates/tracing
|
||||||
// [tui-logger]: https://crates.io/crates/tui-logger
|
// [tui-logger]: https://crates.io/crates/tui-logger
|
||||||
|
|
||||||
use std::{fs::File, io::stdout, panic, time::Duration};
|
use std::{fs::File, time::Duration};
|
||||||
|
|
||||||
use color_eyre::{
|
use color_eyre::{eyre::Context, Result};
|
||||||
config::HookBuilder,
|
|
||||||
eyre::{self, Context},
|
|
||||||
Result,
|
|
||||||
};
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::{Backend, CrosstermBackend},
|
crossterm::event::{self, Event, KeyCode},
|
||||||
crossterm::{
|
|
||||||
event::{self, Event, KeyCode},
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
ExecutableCommand,
|
|
||||||
},
|
|
||||||
widgets::{Block, Paragraph},
|
widgets::{Block, Paragraph},
|
||||||
Terminal,
|
Frame,
|
||||||
};
|
};
|
||||||
use tracing::{debug, info, instrument, trace, Level};
|
use tracing::{debug, info, instrument, trace, Level};
|
||||||
use tracing_appender::{non_blocking, non_blocking::WorkerGuard};
|
use tracing_appender::{non_blocking, non_blocking::WorkerGuard};
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
init_error_hooks()?;
|
color_eyre::install()?;
|
||||||
|
|
||||||
let _guard = init_tracing()?;
|
let _guard = init_tracing()?;
|
||||||
info!("Starting tracing example");
|
info!("Starting tracing example");
|
||||||
|
|
||||||
let mut terminal = init_terminal()?;
|
let mut terminal = ratatui::init();
|
||||||
let mut events = vec![]; // a buffer to store the recent events to display in the UI
|
let mut events = vec![]; // a buffer to store the recent events to display in the UI
|
||||||
while !should_exit(&events) {
|
while !should_exit(&events) {
|
||||||
handle_events(&mut events)?;
|
handle_events(&mut events)?;
|
||||||
terminal.draw(|frame| ui(frame, &events))?;
|
terminal.draw(|frame| draw(frame, &events))?;
|
||||||
}
|
}
|
||||||
restore_terminal()?;
|
ratatui::restore();
|
||||||
|
|
||||||
info!("Exiting tracing example");
|
info!("Exiting tracing example");
|
||||||
println!("See the tracing.log file for the logs");
|
println!("See the tracing.log file for the logs");
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -85,7 +78,7 @@ fn handle_events(events: &mut Vec<Event>) -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
fn ui(frame: &mut ratatui::Frame, events: &[Event]) {
|
fn draw(frame: &mut Frame, events: &[Event]) {
|
||||||
// To view this event, run the example with `RUST_LOG=tracing=debug cargo run --example tracing`
|
// To view this event, run the example with `RUST_LOG=tracing=debug cargo run --example tracing`
|
||||||
trace!(frame_count = frame.count(), event_count = events.len());
|
trace!(frame_count = frame.count(), event_count = events.len());
|
||||||
let events = events.iter().map(|e| format!("{e:?}")).collect::<Vec<_>>();
|
let events = events.iter().map(|e| format!("{e:?}")).collect::<Vec<_>>();
|
||||||
|
@ -116,38 +109,3 @@ fn init_tracing() -> Result<WorkerGuard> {
|
||||||
.init();
|
.init();
|
||||||
Ok(guard)
|
Ok(guard)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize the error hooks to ensure that the terminal is restored to a sane state before
|
|
||||||
/// exiting
|
|
||||||
fn init_error_hooks() -> Result<()> {
|
|
||||||
let (panic, error) = HookBuilder::default().into_hooks();
|
|
||||||
let panic = panic.into_panic_hook();
|
|
||||||
let error = error.into_eyre_hook();
|
|
||||||
eyre::set_hook(Box::new(move |e| {
|
|
||||||
let _ = restore_terminal();
|
|
||||||
error(e)
|
|
||||||
}))?;
|
|
||||||
panic::set_hook(Box::new(move |info| {
|
|
||||||
let _ = restore_terminal();
|
|
||||||
panic(info);
|
|
||||||
}));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument]
|
|
||||||
fn init_terminal() -> Result<Terminal<impl Backend>> {
|
|
||||||
enable_raw_mode()?;
|
|
||||||
stdout().execute(EnterAlternateScreen)?;
|
|
||||||
let backend = CrosstermBackend::new(stdout());
|
|
||||||
let terminal = Terminal::new(backend)?;
|
|
||||||
debug!("terminal initialized");
|
|
||||||
Ok(terminal)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument]
|
|
||||||
fn restore_terminal() -> Result<()> {
|
|
||||||
disable_raw_mode()?;
|
|
||||||
stdout().execute(LeaveAlternateScreen)?;
|
|
||||||
debug!("terminal restored");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
|
@ -27,25 +27,22 @@
|
||||||
//
|
//
|
||||||
// See also https://github.com/rhysd/tui-textarea and https://github.com/sayanarijit/tui-input/
|
// See also https://github.com/rhysd/tui-textarea and https://github.com/sayanarijit/tui-input/
|
||||||
|
|
||||||
use std::{error::Error, io};
|
use color_eyre::Result;
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::{Backend, CrosstermBackend},
|
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||||
crossterm::{
|
|
||||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
|
||||||
execute,
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
},
|
|
||||||
layout::{Constraint, Layout, Position},
|
layout::{Constraint, Layout, Position},
|
||||||
style::{Color, Modifier, Style, Stylize},
|
style::{Color, Modifier, Style, Stylize},
|
||||||
text::{Line, Span, Text},
|
text::{Line, Span, Text},
|
||||||
widgets::{Block, List, ListItem, Paragraph},
|
widgets::{Block, List, ListItem, Paragraph},
|
||||||
Frame, Terminal,
|
DefaultTerminal, Frame,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum InputMode {
|
fn main() -> Result<()> {
|
||||||
Normal,
|
color_eyre::install()?;
|
||||||
Editing,
|
let terminal = ratatui::init();
|
||||||
|
let app_result = App::new().run(terminal);
|
||||||
|
ratatui::restore();
|
||||||
|
app_result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// App holds the state of the application
|
/// App holds the state of the application
|
||||||
|
@ -60,6 +57,11 @@ struct App {
|
||||||
messages: Vec<String>,
|
messages: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum InputMode {
|
||||||
|
Normal,
|
||||||
|
Editing,
|
||||||
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
const fn new() -> Self {
|
const fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -133,45 +135,16 @@ impl App {
|
||||||
self.input.clear();
|
self.input.clear();
|
||||||
self.reset_cursor();
|
self.reset_cursor();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
// 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)?;
|
|
||||||
|
|
||||||
// create app and run it
|
|
||||||
let app = App::new();
|
|
||||||
let res = run_app(&mut terminal, app);
|
|
||||||
|
|
||||||
// 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<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
|
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(|f| ui(f, &app))?;
|
terminal.draw(|frame| self.draw(frame))?;
|
||||||
|
|
||||||
if let Event::Key(key) = event::read()? {
|
if let Event::Key(key) = event::read()? {
|
||||||
match app.input_mode {
|
match self.input_mode {
|
||||||
InputMode::Normal => match key.code {
|
InputMode::Normal => match key.code {
|
||||||
KeyCode::Char('e') => {
|
KeyCode::Char('e') => {
|
||||||
app.input_mode = InputMode::Editing;
|
self.input_mode = InputMode::Editing;
|
||||||
}
|
}
|
||||||
KeyCode::Char('q') => {
|
KeyCode::Char('q') => {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
@ -179,22 +152,12 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
InputMode::Editing if key.kind == KeyEventKind::Press => match key.code {
|
InputMode::Editing if key.kind == KeyEventKind::Press => match key.code {
|
||||||
KeyCode::Enter => app.submit_message(),
|
KeyCode::Enter => self.submit_message(),
|
||||||
KeyCode::Char(to_insert) => {
|
KeyCode::Char(to_insert) => self.enter_char(to_insert),
|
||||||
app.enter_char(to_insert);
|
KeyCode::Backspace => self.delete_char(),
|
||||||
}
|
KeyCode::Left => self.move_cursor_left(),
|
||||||
KeyCode::Backspace => {
|
KeyCode::Right => self.move_cursor_right(),
|
||||||
app.delete_char();
|
KeyCode::Esc => self.input_mode = InputMode::Normal,
|
||||||
}
|
|
||||||
KeyCode::Left => {
|
|
||||||
app.move_cursor_left();
|
|
||||||
}
|
|
||||||
KeyCode::Right => {
|
|
||||||
app.move_cursor_right();
|
|
||||||
}
|
|
||||||
KeyCode::Esc => {
|
|
||||||
app.input_mode = InputMode::Normal;
|
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
InputMode::Editing => {}
|
InputMode::Editing => {}
|
||||||
|
@ -203,15 +166,15 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ui(f: &mut Frame, app: &App) {
|
fn draw(&self, frame: &mut Frame) {
|
||||||
let vertical = Layout::vertical([
|
let vertical = Layout::vertical([
|
||||||
Constraint::Length(1),
|
Constraint::Length(1),
|
||||||
Constraint::Length(3),
|
Constraint::Length(3),
|
||||||
Constraint::Min(1),
|
Constraint::Min(1),
|
||||||
]);
|
]);
|
||||||
let [help_area, input_area, messages_area] = vertical.areas(f.area());
|
let [help_area, input_area, messages_area] = vertical.areas(frame.area());
|
||||||
|
|
||||||
let (msg, style) = match app.input_mode {
|
let (msg, style) = match self.input_mode {
|
||||||
InputMode::Normal => (
|
InputMode::Normal => (
|
||||||
vec![
|
vec![
|
||||||
"Press ".into(),
|
"Press ".into(),
|
||||||
|
@ -235,32 +198,32 @@ fn ui(f: &mut Frame, app: &App) {
|
||||||
};
|
};
|
||||||
let text = Text::from(Line::from(msg)).patch_style(style);
|
let text = Text::from(Line::from(msg)).patch_style(style);
|
||||||
let help_message = Paragraph::new(text);
|
let help_message = Paragraph::new(text);
|
||||||
f.render_widget(help_message, help_area);
|
frame.render_widget(help_message, help_area);
|
||||||
|
|
||||||
let input = Paragraph::new(app.input.as_str())
|
let input = Paragraph::new(self.input.as_str())
|
||||||
.style(match app.input_mode {
|
.style(match self.input_mode {
|
||||||
InputMode::Normal => Style::default(),
|
InputMode::Normal => Style::default(),
|
||||||
InputMode::Editing => Style::default().fg(Color::Yellow),
|
InputMode::Editing => Style::default().fg(Color::Yellow),
|
||||||
})
|
})
|
||||||
.block(Block::bordered().title("Input"));
|
.block(Block::bordered().title("Input"));
|
||||||
f.render_widget(input, input_area);
|
frame.render_widget(input, input_area);
|
||||||
match app.input_mode {
|
match self.input_mode {
|
||||||
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here
|
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here
|
||||||
InputMode::Normal => {}
|
InputMode::Normal => {}
|
||||||
|
|
||||||
// Make the cursor visible and ask ratatui to put it at the specified coordinates after
|
// Make the cursor visible and ask ratatui to put it at the specified coordinates after
|
||||||
// rendering
|
// rendering
|
||||||
#[allow(clippy::cast_possible_truncation)]
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
InputMode::Editing => f.set_cursor_position(Position::new(
|
InputMode::Editing => frame.set_cursor_position(Position::new(
|
||||||
// Draw the cursor at the current position in the input field.
|
// Draw the cursor at the current position in the input field.
|
||||||
// This position is can be controlled via the left and right arrow key
|
// This position is can be controlled via the left and right arrow key
|
||||||
input_area.x + app.character_index as u16 + 1,
|
input_area.x + self.character_index as u16 + 1,
|
||||||
// Move one line down, from the border to the input line
|
// Move one line down, from the border to the input line
|
||||||
input_area.y + 1,
|
input_area.y + 1,
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
|
|
||||||
let messages: Vec<ListItem> = app
|
let messages: Vec<ListItem> = self
|
||||||
.messages
|
.messages
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
|
@ -270,5 +233,6 @@ fn ui(f: &mut Frame, app: &App) {
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let messages = List::new(messages).block(Block::bordered().title("Messages"));
|
let messages = List::new(messages).block(Block::bordered().title("Messages"));
|
||||||
f.render_widget(messages, messages_area);
|
frame.render_widget(messages, messages_area);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
63
src/lib.rs
63
src/lib.rs
|
@ -102,53 +102,28 @@
|
||||||
//! ### Example
|
//! ### Example
|
||||||
//!
|
//!
|
||||||
//! ```rust,no_run
|
//! ```rust,no_run
|
||||||
//! use std::io::{self, stdout};
|
|
||||||
//!
|
|
||||||
//! use ratatui::{
|
//! use ratatui::{
|
||||||
//! backend::CrosstermBackend,
|
//! crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||||
//! crossterm::{
|
|
||||||
//! event::{self, Event, KeyCode},
|
|
||||||
//! terminal::{
|
|
||||||
//! disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
|
||||||
//! },
|
|
||||||
//! ExecutableCommand,
|
|
||||||
//! },
|
|
||||||
//! widgets::{Block, Paragraph},
|
//! widgets::{Block, Paragraph},
|
||||||
//! Frame, Terminal,
|
|
||||||
//! };
|
//! };
|
||||||
//!
|
//!
|
||||||
//! fn main() -> io::Result<()> {
|
//! fn main() -> std::io::Result<()> {
|
||||||
//! enable_raw_mode()?;
|
//! let mut terminal = ratatui::init();
|
||||||
//! stdout().execute(EnterAlternateScreen)?;
|
//! loop {
|
||||||
//! let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
|
//! terminal.draw(|frame| {
|
||||||
//!
|
|
||||||
//! let mut should_quit = false;
|
|
||||||
//! while !should_quit {
|
|
||||||
//! terminal.draw(ui)?;
|
|
||||||
//! should_quit = handle_events()?;
|
|
||||||
//! }
|
|
||||||
//!
|
|
||||||
//! disable_raw_mode()?;
|
|
||||||
//! stdout().execute(LeaveAlternateScreen)?;
|
|
||||||
//! Ok(())
|
|
||||||
//! }
|
|
||||||
//!
|
|
||||||
//! fn handle_events() -> io::Result<bool> {
|
|
||||||
//! if event::poll(std::time::Duration::from_millis(50))? {
|
|
||||||
//! if let Event::Key(key) = event::read()? {
|
|
||||||
//! if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
|
||||||
//! return Ok(true);
|
|
||||||
//! }
|
|
||||||
//! }
|
|
||||||
//! }
|
|
||||||
//! Ok(false)
|
|
||||||
//! }
|
|
||||||
//!
|
|
||||||
//! fn ui(frame: &mut Frame) {
|
|
||||||
//! frame.render_widget(
|
//! frame.render_widget(
|
||||||
//! Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
|
//! Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
|
||||||
//! frame.size(),
|
//! frame.area(),
|
||||||
//! );
|
//! );
|
||||||
|
//! })?;
|
||||||
|
//! if let Event::Key(key) = event::read()? {
|
||||||
|
//! if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||||
|
//! break;
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! ratatui::restore();
|
||||||
|
//! Ok(())
|
||||||
//! }
|
//! }
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
|
@ -170,7 +145,7 @@
|
||||||
//! Frame,
|
//! Frame,
|
||||||
//! };
|
//! };
|
||||||
//!
|
//!
|
||||||
//! fn ui(frame: &mut Frame) {
|
//! fn draw(frame: &mut Frame) {
|
||||||
//! let [title_area, main_area, status_area] = Layout::vertical([
|
//! let [title_area, main_area, status_area] = Layout::vertical([
|
||||||
//! Constraint::Length(1),
|
//! Constraint::Length(1),
|
||||||
//! Constraint::Min(0),
|
//! Constraint::Min(0),
|
||||||
|
@ -213,7 +188,7 @@
|
||||||
//! Frame,
|
//! Frame,
|
||||||
//! };
|
//! };
|
||||||
//!
|
//!
|
||||||
//! fn ui(frame: &mut Frame) {
|
//! fn draw(frame: &mut Frame) {
|
||||||
//! let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.size());
|
//! let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.size());
|
||||||
//!
|
//!
|
||||||
//! let line = Line::from(vec![
|
//! let line = Line::from(vec![
|
||||||
|
@ -320,6 +295,10 @@
|
||||||
/// re-export the `crossterm` crate so that users don't have to add it as a dependency
|
/// re-export the `crossterm` crate so that users don't have to add it as a dependency
|
||||||
#[cfg(feature = "crossterm")]
|
#[cfg(feature = "crossterm")]
|
||||||
pub use crossterm;
|
pub use crossterm;
|
||||||
|
#[cfg(feature = "crossterm")]
|
||||||
|
pub use terminal::{
|
||||||
|
init, init_with_options, restore, try_init, try_init_with_options, try_restore, DefaultTerminal,
|
||||||
|
};
|
||||||
pub use terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport};
|
pub use terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport};
|
||||||
/// re-export the `termion` crate so that users don't have to add it as a dependency
|
/// re-export the `termion` crate so that users don't have to add it as a dependency
|
||||||
#[cfg(all(not(windows), feature = "termion"))]
|
#[cfg(all(not(windows), feature = "termion"))]
|
||||||
|
|
|
@ -32,9 +32,15 @@
|
||||||
//! [`Buffer`]: crate::buffer::Buffer
|
//! [`Buffer`]: crate::buffer::Buffer
|
||||||
|
|
||||||
mod frame;
|
mod frame;
|
||||||
|
#[cfg(feature = "crossterm")]
|
||||||
|
mod init;
|
||||||
mod terminal;
|
mod terminal;
|
||||||
mod viewport;
|
mod viewport;
|
||||||
|
|
||||||
pub use frame::{CompletedFrame, Frame};
|
pub use frame::{CompletedFrame, Frame};
|
||||||
|
#[cfg(feature = "crossterm")]
|
||||||
|
pub use init::{
|
||||||
|
init, init_with_options, restore, try_init, try_init_with_options, try_restore, DefaultTerminal,
|
||||||
|
};
|
||||||
pub use terminal::{Options as TerminalOptions, Terminal};
|
pub use terminal::{Options as TerminalOptions, Terminal};
|
||||||
pub use viewport::Viewport;
|
pub use viewport::Viewport;
|
||||||
|
|
244
src/terminal/init.rs
Normal file
244
src/terminal/init.rs
Normal file
|
@ -0,0 +1,244 @@
|
||||||
|
use std::io::{self, stdout, Stdout};
|
||||||
|
|
||||||
|
use crossterm::{
|
||||||
|
execute,
|
||||||
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::TerminalOptions;
|
||||||
|
use crate::{backend::CrosstermBackend, Terminal};
|
||||||
|
|
||||||
|
/// A type alias for the default terminal type.
|
||||||
|
///
|
||||||
|
/// This is a [`Terminal`] using the [`CrosstermBackend`] which writes to [`Stdout`]. This is a
|
||||||
|
/// reasonable default for most applications. To use a different backend or output stream, instead
|
||||||
|
/// use [`Terminal`] and a [backend][`crate::backend`] of your choice directly.
|
||||||
|
pub type DefaultTerminal = Terminal<CrosstermBackend<Stdout>>;
|
||||||
|
|
||||||
|
/// Initialize a terminal with reasonable defaults for most applications.
|
||||||
|
///
|
||||||
|
/// This will create a new [`DefaultTerminal`] and initialize it with the following defaults:
|
||||||
|
///
|
||||||
|
/// - Backend: [`CrosstermBackend`] writing to [`Stdout`]
|
||||||
|
/// - Raw mode is enabled
|
||||||
|
/// - Alternate screen buffer enabled
|
||||||
|
/// - A panic hook is installed that restores the terminal before panicking. Ensure that this method
|
||||||
|
/// is called after any other panic hooks that may be installed to ensure that the terminal is
|
||||||
|
/// restored before those hooks are called.
|
||||||
|
///
|
||||||
|
/// For more control over the terminal initialization, use [`Terminal::new`] or
|
||||||
|
/// [`Terminal::with_options`].
|
||||||
|
///
|
||||||
|
/// Ensure that this method is called *after* your app installs any other panic hooks to ensure the
|
||||||
|
/// terminal is restored before the other hooks are called.
|
||||||
|
///
|
||||||
|
/// Generally, use this function instead of [`try_init`] to ensure that the terminal is restored
|
||||||
|
/// correctly if any of the initialization steps fail. If you need to handle the error yourself, use
|
||||||
|
/// [`try_init`] instead.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function will panic if any of the following steps fail:
|
||||||
|
///
|
||||||
|
/// - Enabling raw mode
|
||||||
|
/// - Entering the alternate screen buffer
|
||||||
|
/// - Creating the terminal fails due to being unable to calculate the terminal size
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// let terminal = ratatui::init();
|
||||||
|
/// ```
|
||||||
|
pub fn init() -> DefaultTerminal {
|
||||||
|
try_init().expect("failed to initialize terminal")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to initialize a terminal using reasonable defaults for most applications.
|
||||||
|
///
|
||||||
|
/// This function will attempt to create a [`DefaultTerminal`] and initialize it with the following
|
||||||
|
/// defaults:
|
||||||
|
///
|
||||||
|
/// - Raw mode is enabled
|
||||||
|
/// - Alternate screen buffer enabled
|
||||||
|
/// - A panic hook is installed that restores the terminal before panicking.
|
||||||
|
/// - A [`Terminal`] is created using [`CrosstermBackend`] writing to [`Stdout`]
|
||||||
|
///
|
||||||
|
/// If any of these steps fail, the error is returned.
|
||||||
|
///
|
||||||
|
/// Ensure that this method is called *after* your app installs any other panic hooks to ensure the
|
||||||
|
/// terminal is restored before the other hooks are called.
|
||||||
|
///
|
||||||
|
/// Generally, you should use [`init`] instead of this function, as the panic hook installed by this
|
||||||
|
/// function will ensure that any failures during initialization will restore the terminal before
|
||||||
|
/// panicking. This function is provided for cases where you need to handle the error yourself.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// let terminal = ratatui::try_init()?;
|
||||||
|
/// # Ok::<(), std::io::Error>(())
|
||||||
|
/// ```
|
||||||
|
pub fn try_init() -> io::Result<DefaultTerminal> {
|
||||||
|
set_panic_hook();
|
||||||
|
enable_raw_mode()?;
|
||||||
|
execute!(stdout(), EnterAlternateScreen)?;
|
||||||
|
let backend = CrosstermBackend::new(stdout());
|
||||||
|
Terminal::new(backend)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize a terminal with the given options and reasonable defaults.
|
||||||
|
///
|
||||||
|
/// This function allows the caller to specify a custom [`Viewport`] via the [`TerminalOptions`]. It
|
||||||
|
/// will create a new [`DefaultTerminal`] and initialize it with the given options and the following
|
||||||
|
/// defaults:
|
||||||
|
///
|
||||||
|
/// [`Viewport`]: crate::Viewport
|
||||||
|
///
|
||||||
|
/// - Raw mode is enabled
|
||||||
|
/// - A panic hook is installed that restores the terminal before panicking.
|
||||||
|
///
|
||||||
|
/// Unlike [`init`], this function does not enter the alternate screen buffer as this may not be
|
||||||
|
/// desired in all cases. If you need the alternate screen buffer, you should enable it manually
|
||||||
|
/// after calling this function.
|
||||||
|
///
|
||||||
|
/// For more control over the terminal initialization, use [`Terminal::with_options`].
|
||||||
|
///
|
||||||
|
/// Ensure that this method is called *after* your app installs any other panic hooks to ensure the
|
||||||
|
/// terminal is restored before the other hooks are called.
|
||||||
|
///
|
||||||
|
/// Generally, use this function instead of [`try_init_with_options`] to ensure that the terminal is
|
||||||
|
/// restored correctly if any of the initialization steps fail. If you need to handle the error
|
||||||
|
/// yourself, use [`try_init_with_options`] instead.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function will panic if any of the following steps fail:
|
||||||
|
///
|
||||||
|
/// - Enabling raw mode
|
||||||
|
/// - Creating the terminal fails due to being unable to calculate the terminal size
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use ratatui::{TerminalOptions, Viewport};
|
||||||
|
///
|
||||||
|
/// let options = TerminalOptions {
|
||||||
|
/// viewport: Viewport::Inline(5),
|
||||||
|
/// };
|
||||||
|
/// let terminal = ratatui::init_with_options(options);
|
||||||
|
/// ```
|
||||||
|
pub fn init_with_options(options: TerminalOptions) -> DefaultTerminal {
|
||||||
|
try_init_with_options(options).expect("failed to initialize terminal")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to initialize a terminal with the given options and reasonable defaults.
|
||||||
|
///
|
||||||
|
/// This function allows the caller to specify a custom [`Viewport`] via the [`TerminalOptions`]. It
|
||||||
|
/// will attempt to create a [`DefaultTerminal`] and initialize it with the given options and the
|
||||||
|
/// following defaults:
|
||||||
|
///
|
||||||
|
/// [`Viewport`]: crate::Viewport
|
||||||
|
///
|
||||||
|
/// - Raw mode is enabled
|
||||||
|
/// - A panic hook is installed that restores the terminal before panicking.
|
||||||
|
///
|
||||||
|
/// Unlike [`try_init`], this function does not enter the alternate screen buffer as this may not be
|
||||||
|
/// desired in all cases. If you need the alternate screen buffer, you should enable it manually
|
||||||
|
/// after calling this function.
|
||||||
|
///
|
||||||
|
/// If any of these steps fail, the error is returned.
|
||||||
|
///
|
||||||
|
/// Ensure that this method is called *after* your app installs any other panic hooks to ensure the
|
||||||
|
/// terminal is restored before the other hooks are called.
|
||||||
|
///
|
||||||
|
/// Generally, you should use [`init_with_options`] instead of this function, as the panic hook
|
||||||
|
/// installed by this function will ensure that any failures during initialization will restore the
|
||||||
|
/// terminal before panicking. This function is provided for cases where you need to handle the
|
||||||
|
/// error yourself.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use ratatui::{TerminalOptions, Viewport};
|
||||||
|
///
|
||||||
|
/// let options = TerminalOptions {
|
||||||
|
/// viewport: Viewport::Inline(5),
|
||||||
|
/// };
|
||||||
|
/// let terminal = ratatui::try_init_with_options(options)?;
|
||||||
|
/// # Ok::<(), std::io::Error>(())
|
||||||
|
/// ```
|
||||||
|
pub fn try_init_with_options(options: TerminalOptions) -> io::Result<DefaultTerminal> {
|
||||||
|
set_panic_hook();
|
||||||
|
enable_raw_mode()?;
|
||||||
|
let backend = CrosstermBackend::new(stdout());
|
||||||
|
Terminal::with_options(backend, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restores the terminal to its original state.
|
||||||
|
///
|
||||||
|
/// This function should be called before the program exits to ensure that the terminal is
|
||||||
|
/// restored to its original state.
|
||||||
|
///
|
||||||
|
/// This function will attempt to restore the terminal to its original state by performing the
|
||||||
|
/// following steps:
|
||||||
|
///
|
||||||
|
/// 1. Raw mode is disabled.
|
||||||
|
/// 2. The alternate screen buffer is left.
|
||||||
|
///
|
||||||
|
/// If either of these steps fail, the error is printed to stderr and ignored.
|
||||||
|
///
|
||||||
|
/// Use this function over [`try_restore`] when you don't need to handle the error yourself, as
|
||||||
|
/// ignoring the error is generally the correct behavior when cleaning up before exiting. If you
|
||||||
|
/// need to handle the error yourself, use [`try_restore`] instead.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// ratatui::restore();
|
||||||
|
/// ```
|
||||||
|
pub fn restore() {
|
||||||
|
if let Err(err) = try_restore() {
|
||||||
|
// There's not much we can do if restoring the terminal fails, so we just print the error
|
||||||
|
eprintln!("Failed to restore terminal: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restore the terminal to its original state.
|
||||||
|
///
|
||||||
|
/// This function will attempt to restore the terminal to its original state by performing the
|
||||||
|
/// following steps:
|
||||||
|
///
|
||||||
|
/// 1. Raw mode is disabled.
|
||||||
|
/// 2. The alternate screen buffer is left.
|
||||||
|
///
|
||||||
|
/// If either of these steps fail, the error is returned.
|
||||||
|
///
|
||||||
|
/// Use [`restore`] instead of this function when you don't need to handle the error yourself, as
|
||||||
|
/// ignoring the error is generally the correct behavior when cleaning up before exiting. If you
|
||||||
|
/// need to handle the error yourself, use this function instead.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// ratatui::try_restore()?;
|
||||||
|
/// # Ok::<(), std::io::Error>(())
|
||||||
|
/// ```
|
||||||
|
pub fn try_restore() -> io::Result<()> {
|
||||||
|
// disabling raw mode first is important as it has more side effects than leaving the alternate
|
||||||
|
// screen buffer
|
||||||
|
disable_raw_mode()?;
|
||||||
|
execute!(stdout(), LeaveAlternateScreen)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a panic hook that restores the terminal before panicking.
|
||||||
|
///
|
||||||
|
/// Replaces the panic hook with a one that will restore the terminal state before calling the
|
||||||
|
/// original panic hook. This ensures that the terminal is left in a good state when a panic occurs.
|
||||||
|
fn set_panic_hook() {
|
||||||
|
let hook = std::panic::take_hook();
|
||||||
|
std::panic::set_hook(Box::new(move |info| {
|
||||||
|
restore();
|
||||||
|
hook(info);
|
||||||
|
}));
|
||||||
|
}
|
Loading…
Reference in a new issue