Async GitHub
Demonstrates fetching and displaying GitHub API data asynchronously. Source async-github.
git clone https://github.com/ratatui/ratatui.git --branch ratatui-v0.30.2 --depth 1cd ratatuicargo run -p async-github
//! # [Ratatui] Async example//!//! This example demonstrates how to use Ratatui with widgets that fetch data asynchronously. It//! uses the `octocrab` crate to fetch a list of pull requests from the GitHub API.//!//! <https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token>//! <https://github.com/settings/tokens/new> to create a new token (select classic, and no scopes)//!//! This example does not cover message passing between threads, it only demonstrates how to manage//! shared state between the main thread and a background task, which acts mostly as a one-shot//! fetcher. For more complex scenarios, you may need to use channels or other synchronization//! primitives.//!//! A simple app might have multiple widgets that fetch data from different sources, and each widget//! would have its own background task to fetch the data. The main thread would then render the//! widgets with the latest data.//!//! 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//! repository. This means that you may not be able to compile with the latest release version on//! crates.io, or the one that you have installed locally.//!//! See the [examples readme] for more information on finding examples that match the version of the//! library you are using.//!//! [Ratatui]: https://github.com/ratatui/ratatui//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.mduse std::sync::{Arc, RwLock};use std::time::Duration;
use color_eyre::Result;use crossterm::event::{Event, EventStream, KeyCode};use octocrab::Page;use octocrab::params::Direction;use octocrab::params::pulls::Sort;use ratatui::buffer::Buffer;use ratatui::layout::{Constraint, Layout, Rect};use ratatui::style::{Style, Stylize};use ratatui::text::Line;use ratatui::widgets::{Block, HighlightSpacing, Row, StatefulWidget, Table, TableState, Widget};use ratatui::{DefaultTerminal, Frame};use tokio_stream::StreamExt;
#[tokio::main]async fn main() -> Result<()> { color_eyre::install()?; let terminal = ratatui::init(); let app_result = App::default().run(terminal).await; ratatui::restore(); app_result}
#[derive(Debug, Default)]struct App { should_quit: bool, pull_requests: PullRequestListWidget,}
impl App { const FRAMES_PER_SECOND: f32 = 60.0;
pub async fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { self.pull_requests.run();
let period = Duration::from_secs_f32(1.0 / Self::FRAMES_PER_SECOND); let mut interval = tokio::time::interval(period); let mut events = EventStream::new();
while !self.should_quit { tokio::select! { _ = interval.tick() => { terminal.draw(|frame| self.render(frame))?; }, Some(Ok(event)) = events.next() => self.handle_event(&event), } } Ok(()) }
fn render(&self, frame: &mut Frame) { let layout = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]); let [title_area, body_area] = frame.area().layout(&layout); let title = Line::from("Ratatui async example").centered().bold(); frame.render_widget(title, title_area); frame.render_widget(&self.pull_requests, body_area); }
fn handle_event(&mut self, event: &Event) { if let Some(key) = event.as_key_press_event() { match key.code { KeyCode::Char('q') | KeyCode::Esc => self.should_quit = true, 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.////// This is an async widget that fetches the list of pull requests from the GitHub API. It contains/// an inner `Arc<RwLock<PullRequestListState>>` that holds the state of the widget. Cloning the/// widget will clone the Arc, so you can pass it around to other threads, and this is used to spawn/// a background task to fetch the pull requests.#[derive(Debug, Clone, Default)]struct PullRequestListWidget { state: Arc<RwLock<PullRequestListState>>,}
#[derive(Debug, Default)]struct PullRequestListState { pull_requests: Vec<PullRequest>, loading_state: LoadingState, table_state: TableState,}
#[derive(Debug, Clone)]struct PullRequest { id: String, title: String, url: String,}
#[derive(Debug, Clone, Default, PartialEq, Eq)]enum LoadingState { #[default] Idle, Loading, Loaded, Error(String),}
impl PullRequestListWidget { /// Start fetching the pull requests in the background. /// /// This method spawns a background task that fetches the pull requests from the GitHub API. /// The result of the fetch is then passed to the `on_load` or `on_err` methods. fn run(&self) { let this = self.clone(); // clone the widget to pass to the background task tokio::spawn(this.fetch_pulls()); }
async fn fetch_pulls(self) { // this runs once, but you could also run this in a loop, using a channel that accepts // messages to refresh on demand, or with an interval timer to refresh every N seconds self.set_loading_state(LoadingState::Loading); match octocrab::instance() .pulls("ratatui", "ratatui") .list() .sort(Sort::Updated) .direction(Direction::Descending) .send() .await { Ok(page) => self.on_load(&page), Err(err) => self.on_err(&err), } } fn on_load(&self, page: &Page<OctoPullRequest>) { let prs = page.items.iter().filter_map(PullRequest::from_octo); let mut state = self.state.write().unwrap(); state.loading_state = LoadingState::Loaded; state.pull_requests.extend(prs); if !state.pull_requests.is_empty() { state.table_state.select(Some(0)); } }
fn on_err(&self, err: &octocrab::Error) { self.set_loading_state(LoadingState::Error(err.to_string())); }
fn set_loading_state(&self, state: LoadingState) { self.state.write().unwrap().loading_state = state; }
fn scroll_down(&self) { self.state.write().unwrap().table_state.scroll_down_by(1); }
fn scroll_up(&self) { self.state.write().unwrap().table_state.scroll_up_by(1); }}
type OctoPullRequest = octocrab::models::pulls::PullRequest;
impl PullRequest { fn from_octo(pr: &OctoPullRequest) -> Option<Self> { Some(Self { id: pr.number?.to_string(), title: pr.title.clone()?, url: pr.html_url.as_ref()?.to_string(), }) }}
impl Widget for &PullRequestListWidget { fn render(self, area: Rect, buf: &mut Buffer) { let mut state = self.state.write().unwrap();
// a block with a right aligned title with the loading state on the right let loading_state = Line::from(format!("{:?}", state.loading_state)).right_aligned(); let block = Block::bordered() .title("Pull Requests") .title(loading_state) .title_bottom("j/k to scroll, q to quit");
// a table with the list of pull requests let rows = state.pull_requests.iter(); let widths = [ Constraint::Length(5), Constraint::Fill(1), Constraint::Max(49), ]; let table = Table::new(rows, widths) .block(block) .highlight_spacing(HighlightSpacing::Always) .highlight_symbol(">>") .row_highlight_style(Style::new().on_blue());
StatefulWidget::render(table, area, buf, &mut state.table_state); }}
impl From<&PullRequest> for Row<'_> { fn from(pr: &PullRequest) -> Self { let pr = pr.clone(); Row::new(vec![pr.id, pr.title, pr.url]) }}