#[derive(Default)] struct App { states: Vec, } impl App { fn on_tick(&mut self) { self.states.iter_mut().for_each(|state| state.calc_next()); } } fn main() -> Result<(), Box> { // setup terminal ratatui::crossterm::terminal::enable_raw_mode()?; let mut stdout = std::io::stdout(); ratatui::crossterm::execute!( stdout, ratatui::crossterm::terminal::EnterAlternateScreen, ratatui::crossterm::event::EnableMouseCapture )?; let backend = ratatui::backend::CrosstermBackend::new(stdout); let mut terminal = ratatui::Terminal::new(backend)?; // create app and run it let tick_rate = std::time::Duration::from_millis(250); let app = App::default(); let res = run_app(&mut terminal, app, tick_rate); // restore terminal ratatui::crossterm::terminal::disable_raw_mode()?; ratatui::crossterm::execute!( terminal.backend_mut(), ratatui::crossterm::terminal::LeaveAlternateScreen, ratatui::crossterm::event::DisableMouseCapture )?; terminal.show_cursor()?; if let Err(err) = res { println!("{:?}", err) } Ok(()) } fn run_app( terminal: &mut ratatui::Terminal, mut app: App, tick_rate: std::time::Duration, ) -> std::io::Result<()> { let mut last_tick = std::time::Instant::now(); loop { terminal.draw(|f| ui(f, &mut app))?; let timeout = tick_rate .checked_sub(last_tick.elapsed()) .unwrap_or_else(|| std::time::Duration::from_secs(0)); if ratatui::crossterm::event::poll(timeout)? { if let ratatui::crossterm::event::Event::Key(key) = ratatui::crossterm::event::read()? { if let ratatui::crossterm::event::KeyCode::Char('q') = key.code { return Ok(()); } } } if last_tick.elapsed() >= tick_rate { app.on_tick(); last_tick = std::time::Instant::now(); } } } fn ui(f: &mut ratatui::Frame, app: &mut App) { let all_sets = [ ("ASCII", throbber_widgets_tui::ASCII), ("BOX_DRAWING", throbber_widgets_tui::BOX_DRAWING), ("ARROW", throbber_widgets_tui::ARROW), ("DOUBLE_ARROW", throbber_widgets_tui::DOUBLE_ARROW), ("VERTICAL_BLOCK", throbber_widgets_tui::VERTICAL_BLOCK), ("HORIZONTAL_BLOCK", throbber_widgets_tui::HORIZONTAL_BLOCK), ("QUADRANT_BLOCK", throbber_widgets_tui::QUADRANT_BLOCK), ( "QUADRANT_BLOCK_CRACK", throbber_widgets_tui::QUADRANT_BLOCK_CRACK, ), ("WHITE_SQUARE", throbber_widgets_tui::WHITE_SQUARE), ("WHITE_CIRCLE", throbber_widgets_tui::WHITE_CIRCLE), ("BLACK_CIRCLE", throbber_widgets_tui::BLACK_CIRCLE), ("CLOCK", throbber_widgets_tui::CLOCK), ("BRAILLE_ONE", throbber_widgets_tui::BRAILLE_ONE), ("BRAILLE_SIX", throbber_widgets_tui::BRAILLE_SIX), ("BRAILLE_EIGHT", throbber_widgets_tui::BRAILLE_EIGHT), ("BRAILLE_DOUBLE", throbber_widgets_tui::BRAILLE_DOUBLE), ( "BRAILLE_SIX_DOUBLE", throbber_widgets_tui::BRAILLE_SIX_DOUBLE, ), ( "BRAILLE_EIGHT_DOUBLE", throbber_widgets_tui::BRAILLE_EIGHT_DOUBLE, ), ("OGHAM_A", throbber_widgets_tui::OGHAM_A), ("OGHAM_B", throbber_widgets_tui::OGHAM_B), ("OGHAM_C", throbber_widgets_tui::OGHAM_C), ("PARENTHESIS", throbber_widgets_tui::PARENTHESIS), ("CANADIAN", throbber_widgets_tui::CANADIAN), ]; let horizontal_num = 4; // why +1? because the first line is for title default throbber. let vertical_num = 1 + (all_sets.len() + horizontal_num - 1) / horizontal_num; let verticals = ratatui::layout::Layout::default() .direction(ratatui::layout::Direction::Vertical) .constraints(&vec![ratatui::layout::Constraint::Length(1); vertical_num]) .split(f.area()); let default_throbber = throbber_widgets_tui::Throbber::default() .label("Press q to exit. This line is a default throbber (random step). The followings are incremental step.") .style(ratatui::style::Style::default().fg(ratatui::style::Color::Yellow)); f.render_widget(default_throbber, verticals[0]); let mut chunks: Option<_> = None; for (i, kvp) in all_sets.iter().enumerate() { let (name, set) = kvp; let row = i / horizontal_num; let col = i % horizontal_num; if col == 0 { chunks = Some( ratatui::layout::Layout::default() .direction(ratatui::layout::Direction::Horizontal) .constraints(&vec![ ratatui::layout::Constraint::Percentage( 100 / (horizontal_num as u16) ); horizontal_num ]) .split(verticals[row + 1]), ); } if app.states.len() <= i { app.states .push(throbber_widgets_tui::ThrobberState::default()); } let throbber = throbber_widgets_tui::Throbber::default() .label(name.to_string()) .throbber_set(set.clone()); f.render_stateful_widget(throbber, chunks.clone().unwrap()[col], &mut app.states[i]); } }