use color_eyre::eyre::Result;
use crossterm::event::{Event, KeyCode, KeyEvent};
use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, List as ListWidget, ListItem, Paragraph};
use tears::prelude::*;
use tears::subscription::{
terminal::TerminalEvents,
time::{Message as TimerMessage, Timer},
};
#[derive(Debug, Clone)]
enum View {
Menu { selected: usize },
Counter { count: u32 },
Input { text: String, history: Vec<String> },
List { items: Vec<String>, selected: usize },
}
#[derive(Debug, Clone)]
enum Message {
GoToMenu,
GoToCounter,
GoToInput,
GoToList,
Quit,
MenuUp,
MenuDown,
MenuSelect,
Tick,
InputChar(char),
InputSubmit,
InputBackspace,
ListUp,
ListDown,
Terminal(Event),
TerminalError(String),
}
struct App {
view: View,
}
impl Application for App {
type Message = Message;
type Flags = ();
fn new(_flags: ()) -> (Self, Command<Self::Message>) {
let app = Self {
view: View::Menu { selected: 0 },
};
(app, Command::none())
}
fn update(&mut self, msg: Message) -> Command<Self::Message> {
match msg {
Message::GoToMenu
| Message::GoToCounter
| Message::GoToInput
| Message::GoToList
| Message::Quit => self.update_navigation(&msg),
Message::MenuUp | Message::MenuDown | Message::MenuSelect => self.update_menu(&msg),
Message::Tick => self.update_counter(),
Message::InputChar(c) => self.update_input_char(c),
Message::InputSubmit => self.update_input_submit(),
Message::InputBackspace => self.update_input_backspace(),
Message::ListUp | Message::ListDown => self.update_list(&msg),
Message::Terminal(Event::Key(key)) => handle_key_event(&self.view, key),
Message::Terminal(_) => Command::none(),
Message::TerminalError(e) => handle_terminal_error(&e),
}
}
fn view(&self, frame: &mut Frame) {
match &self.view {
View::Menu { selected } => render_menu(frame, *selected),
View::Counter { count } => render_counter(frame, *count),
View::Input { text, history } => render_input(frame, text, history),
View::List { items, selected } => render_list(frame, items, *selected),
}
}
fn subscriptions(&self) -> Vec<Subscription<Self::Message>> {
let mut subs = vec![
Subscription::new(TerminalEvents::new()).map(|result| match result {
Ok(event) => Message::Terminal(event),
Err(e) => Message::TerminalError(e.to_string()),
}),
];
if matches!(self.view, View::Counter { .. }) {
subs.push(
Subscription::new(Timer::new(1000)).map(|timer_msg| match timer_msg {
TimerMessage::Tick => Message::Tick,
}),
);
}
subs
}
}
impl App {
fn update_navigation(&mut self, msg: &Message) -> Command<Message> {
match msg {
Message::GoToMenu => self.view = View::Menu { selected: 0 },
Message::GoToCounter => self.view = View::Counter { count: 0 },
Message::GoToInput => {
self.view = View::Input {
text: String::new(),
history: vec![],
};
}
Message::GoToList => {
self.view = View::List {
items: vec![
"Item 1".to_string(),
"Item 2".to_string(),
"Item 3".to_string(),
"Item 4".to_string(),
"Item 5".to_string(),
],
selected: 0,
};
}
Message::Quit => return Command::effect(Action::Quit),
_ => {}
}
Command::none()
}
fn update_menu(&mut self, msg: &Message) -> Command<Message> {
let View::Menu { selected } = &mut self.view else {
return Command::none();
};
match msg {
Message::MenuUp => *selected = selected.saturating_sub(1),
Message::MenuDown => *selected = (*selected + 1).min(3),
Message::MenuSelect => {
return match *selected {
0 => Command::message(Message::GoToCounter),
1 => Command::message(Message::GoToInput),
2 => Command::message(Message::GoToList),
3 => Command::message(Message::Quit),
_ => Command::none(),
};
}
_ => {}
}
Command::none()
}
const fn update_counter(&mut self) -> Command<Message> {
if let View::Counter { count } = &mut self.view {
*count += 1;
}
Command::none()
}
fn update_input_char(&mut self, c: char) -> Command<Message> {
if let View::Input { text, .. } = &mut self.view {
text.push(c);
}
Command::none()
}
fn update_input_backspace(&mut self) -> Command<Message> {
if let View::Input { text, .. } = &mut self.view {
text.pop();
}
Command::none()
}
fn update_input_submit(&mut self) -> Command<Message> {
if let View::Input { text, history } = &mut self.view
&& !text.is_empty()
{
history.push(text.clone());
text.clear();
}
Command::none()
}
fn update_list(&mut self, msg: &Message) -> Command<Message> {
let View::List { items, selected } = &mut self.view else {
return Command::none();
};
match msg {
Message::ListUp => *selected = selected.saturating_sub(1),
Message::ListDown => *selected = (*selected + 1).min(items.len().saturating_sub(1)),
_ => {}
}
Command::none()
}
}
#[allow(clippy::use_self)]
fn handle_key_event(view: &View, key: KeyEvent) -> Command<Message> {
match view {
View::Menu { .. } => match key.code {
KeyCode::Up => Command::message(Message::MenuUp),
KeyCode::Down => Command::message(Message::MenuDown),
KeyCode::Enter => Command::message(Message::MenuSelect),
KeyCode::Char('q') => Command::message(Message::Quit),
_ => Command::none(),
},
View::Counter { .. } => match key.code {
KeyCode::Char('b') | KeyCode::Esc => Command::message(Message::GoToMenu),
KeyCode::Char('q') => Command::message(Message::Quit),
_ => Command::none(),
},
View::Input { .. } => match key.code {
KeyCode::Char(c) => Command::message(Message::InputChar(c)),
KeyCode::Backspace => Command::message(Message::InputBackspace),
KeyCode::Enter => Command::message(Message::InputSubmit),
KeyCode::Esc => Command::message(Message::GoToMenu),
_ => Command::none(),
},
View::List { .. } => match key.code {
KeyCode::Up => Command::message(Message::ListUp),
KeyCode::Down => Command::message(Message::ListDown),
KeyCode::Char('b') | KeyCode::Esc => Command::message(Message::GoToMenu),
KeyCode::Char('q') => Command::message(Message::Quit),
_ => Command::none(),
},
}
}
fn handle_terminal_error(error: &str) -> Command<Message> {
eprintln!("Terminal error: {error}");
Command::effect(Action::Quit)
}
fn render_menu(frame: &mut Frame, selected: usize) {
let area = frame.area();
let menu_items = ["Counter", "Input", "List", "Quit"];
let items: Vec<ListItem> = menu_items
.iter()
.enumerate()
.map(|(i, item)| {
let content = if i == selected {
format!("> {item}")
} else {
format!(" {item}")
};
ListItem::new(content)
})
.collect();
let list = ListWidget::new(items).block(
Block::default()
.borders(Borders::ALL)
.title("Menu (↑/↓: navigate, Enter: select, q: quit)"),
);
frame.render_widget(list, area);
}
fn render_counter(frame: &mut Frame, count: u32) {
let area = frame.area();
let text = format!("Count: {count}\n\nPress 'b' or Esc to go back\nPress 'q' to quit");
let paragraph =
Paragraph::new(text).block(Block::default().borders(Borders::ALL).title("Counter"));
frame.render_widget(paragraph, area);
}
fn render_input(frame: &mut Frame, text: &str, history: &[String]) {
use std::fmt::Write;
let area = frame.area();
let mut content = String::from("Type to enter text, Enter to submit, Esc to go back\n\n");
let _ = write!(content, "Input: {text}_\n\n");
content.push_str("History:\n");
for (i, item) in history.iter().enumerate() {
let _ = writeln!(content, " {}. {item}", i + 1);
}
let paragraph =
Paragraph::new(content).block(Block::default().borders(Borders::ALL).title("Text Input"));
frame.render_widget(paragraph, area);
}
fn render_list(frame: &mut Frame, items: &[String], selected: usize) {
let area = frame.area();
let list_items: Vec<ListItem> = items
.iter()
.enumerate()
.map(|(i, item)| {
let content = if i == selected {
format!("> {item}")
} else {
format!(" {item}")
};
ListItem::new(content)
})
.collect();
let list = ListWidget::new(list_items).block(
Block::default()
.borders(Borders::ALL)
.title("List (↑/↓: navigate, Esc: back, q: quit)"),
);
frame.render_widget(list, area);
}
#[tokio::main]
async fn main() -> Result<()> {
color_eyre::install()?;
let mut terminal = ratatui::init();
let runtime = Runtime::<App>::new((), 60);
let result = runtime.run(&mut terminal).await;
ratatui::restore();
result?;
Ok(())
}