use std::{collections::HashMap, time::Duration};
use color_eyre::eyre::Result;
crossterm::event::{KeyCode, KeyEvent},
layout::{Alignment, Constraint, Layout, Margin, Position, Rect},
style::{Color, Modifier, Style, Stylize},
widgets::{block, Block, BorderType, Borders, Clear, Paragraph, Row, Table},
use tokio::sync::mpsc::UnboundedSender;
use tui_input::{backend::crossterm::EventHandler, Input};
use crate::{action::Action, config::key_event_to_string};
#[derive(Default, Copy, Clone, PartialEq, Eq)]
pub render_ticker: usize,
pub action_tx: Option<UnboundedSender<Action>>,
pub keymap: HashMap<KeyEvent, Action>,
pub last_events: Vec<KeyEvent>,
pub fn keymap(mut self, keymap: HashMap<KeyEvent, Action>) -> Self {
self.app_ticker = self.app_ticker.saturating_add(1);
self.last_events.drain(..);
pub fn render_tick(&mut self) {
log::debug!("Render Tick");
self.render_ticker = self.render_ticker.saturating_add(1);
pub fn add(&mut self, s: String) {
pub fn schedule_increment(&mut self, i: usize) {
let tx = self.action_tx.clone().unwrap();
tokio::spawn(async move {
tx.send(Action::EnterProcessing).unwrap();
tokio::time::sleep(Duration::from_secs(1)).await;
tx.send(Action::Increment(i)).unwrap();
tx.send(Action::ExitProcessing).unwrap();
pub fn schedule_decrement(&mut self, i: usize) {
let tx = self.action_tx.clone().unwrap();
tokio::spawn(async move {
tx.send(Action::EnterProcessing).unwrap();
tokio::time::sleep(Duration::from_secs(1)).await;
tx.send(Action::Decrement(i)).unwrap();
tx.send(Action::ExitProcessing).unwrap();
pub fn increment(&mut self, i: usize) {
self.counter = self.counter.saturating_add(i);
pub fn decrement(&mut self, i: usize) {
self.counter = self.counter.saturating_sub(i);
impl Component for Home {
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
self.action_tx = Some(tx);
fn handle_key_events(&mut self, key: KeyEvent) -> Result<Option<Action>> {
self.last_events.push(key);
let action = match self.mode {
Mode::Normal | Mode::Processing => return Ok(None),
Mode::Insert => match key.code {
KeyCode::Esc => Action::EnterNormal,
if let Some(sender) = &self.action_tx {
if let Err(e) = sender.send(Action::CompleteInput(self.input.value().to_string())) {
error!("Failed to send action: {:?}", e);
self.input.handle_event(&ratatui::crossterm::event::Event::Key(key));
fn update(&mut self, action: Action) -> Result<Option<Action>> {
Action::Tick => self.tick(),
Action::Render => self.render_tick(),
Action::ToggleShowHelp => self.show_help = !self.show_help,
Action::ScheduleIncrement => self.schedule_increment(1),
Action::ScheduleDecrement => self.schedule_decrement(1),
Action::Increment(i) => self.increment(i),
Action::Decrement(i) => self.decrement(i),
Action::CompleteInput(s) => self.add(s),
self.mode = Mode::Normal;
self.mode = Mode::Insert;
Action::EnterProcessing => {
self.mode = Mode::Processing;
Action::ExitProcessing => {
// TODO: Make this go to previous mode instead
self.mode = Mode::Normal;
fn draw(&mut self, f: &mut Frame<'_>, rect: Rect) -> Result<()> {
let rects = Layout::default().constraints([Constraint::Percentage(100), Constraint::Min(3)].as_ref()).split(rect);
let mut text: Vec<Line> = self.text.clone().iter().map(|l| Line::from(l.clone())).collect();
text.insert(0, "".into());
text.insert(0, "Type into input and hit enter to display here".dim().into());
text.insert(0, "".into());
text.insert(0, format!("Render Ticker: {}", self.render_ticker).into());
text.insert(0, format!("App Ticker: {}", self.app_ticker).into());
text.insert(0, format!("Counter: {}", self.counter).into());
text.insert(0, "".into());
Span::styled("j", Style::default().fg(Color::Red)),
Span::styled("k", Style::default().fg(Color::Red)),
Span::styled("increment", Style::default().fg(Color::Yellow)),
Span::styled("decrement", Style::default().fg(Color::Yellow)),
text.insert(0, "".into());
.title("ratatui async template")
.title_alignment(Alignment::Center)
.border_style(match self.mode {
Mode::Processing => Style::default().fg(Color::Yellow),
.border_type(BorderType::Rounded),
.style(Style::default().fg(Color::Cyan))
.alignment(Alignment::Center),
let width = rects[1].width.max(3) - 3; // keep 2 for borders and 1 for cursor
let scroll = self.input.visual_scroll(width as usize);
let input = Paragraph::new(self.input.value())
Mode::Insert => Style::default().fg(Color::Yellow),
.scroll((0, scroll as u16))
.block(Block::default().borders(Borders::ALL).title(Line::from(vec![
Span::raw("Enter Input Mode "),
Span::styled("(Press ", Style::default().fg(Color::DarkGray)),
Span::styled("/", Style::default().add_modifier(Modifier::BOLD).fg(Color::Gray)),
Span::styled(" to start, ", Style::default().fg(Color::DarkGray)),
Span::styled("ESC", Style::default().add_modifier(Modifier::BOLD).fg(Color::Gray)),
Span::styled(" to finish)", Style::default().fg(Color::DarkGray)),
f.render_widget(input, rects[1]);
if self.mode == Mode::Insert {
let position = Position {
x: (rects[1].x + 1 + self.input.cursor() as u16).min(rects[1].x + rects[1].width - 2),
f.set_cursor_position(position)
let rect = rect.inner(Margin { horizontal: 4, vertical: 2 });
f.render_widget(Clear, rect);
let block = Block::default()
.title(Line::from(vec![Span::styled("Key Bindings", Style::default().add_modifier(Modifier::BOLD))]))
.border_style(Style::default().fg(Color::Yellow));
f.render_widget(block, rect);
Row::new(vec!["j", "Increment"]),
Row::new(vec!["k", "Decrement"]),
Row::new(vec!["/", "Enter Input"]),
Row::new(vec!["ESC", "Exit Input"]),
Row::new(vec!["Enter", "Submit Input"]),
Row::new(vec!["q", "Quit"]),
Row::new(vec!["?", "Open Help"]),
let table = Table::new(rows, [Constraint::Percentage(10), Constraint::Percentage(90)])
.header(Row::new(vec!["Key", "Action"]).bottom_margin(1).style(Style::default().add_modifier(Modifier::BOLD)))
f.render_widget(table, rect.inner(Margin { vertical: 4, horizontal: 2 }));
ratatui::widgets::block::Title::from(format!(
&self.last_events.iter().map(key_event_to_string).collect::<Vec<_>>()
.alignment(Alignment::Right),
.title_style(Style::default().add_modifier(Modifier::BOLD)),
Rect { x: rect.x + 1, y: rect.height.saturating_sub(1), width: rect.width.saturating_sub(2), height: 1 },