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())
}
#[allow(clippy::too_many_lines)]
fn update(&mut self, msg: Message) -> Command<Self::Message> {
match msg {
Message::GoToMenu => {
self.view = View::Menu { selected: 0 };
Command::none()
}
Message::GoToCounter => {
self.view = View::Counter { count: 0 };
Command::none()
}
Message::GoToInput => {
self.view = View::Input {
text: String::new(),
history: vec![],
};
Command::none()
}
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,
};
Command::none()
}
Message::Quit => Command::effect(Action::Quit),
Message::MenuUp => {
if let View::Menu { selected } = &mut self.view {
*selected = selected.saturating_sub(1);
}
Command::none()
}
Message::MenuDown => {
if let View::Menu { selected } = &mut self.view {
*selected = (*selected + 1).min(3);
}
Command::none()
}
Message::MenuSelect => {
if let View::Menu { selected } = self.view {
match selected {
0 => Command::message(Message::GoToCounter),
1 => Command::message(Message::GoToInput),
2 => Command::message(Message::GoToList),
3 => Command::message(Message::Quit),
_ => Command::none(),
}
} else {
Command::none()
}
}
Message::Tick => {
if let View::Counter { count } = &mut self.view {
*count += 1;
}
Command::none()
}
Message::InputChar(c) => {
if let View::Input { text, .. } = &mut self.view {
text.push(c);
}
Command::none()
}
Message::InputBackspace => {
if let View::Input { text, .. } = &mut self.view {
text.pop();
}
Command::none()
}
Message::InputSubmit => {
if let View::Input { text, history } = &mut self.view {
if !text.is_empty() {
history.push(text.clone());
text.clear();
}
}
Command::none()
}
Message::ListUp => {
if let View::List { selected, .. } = &mut self.view {
*selected = selected.saturating_sub(1);
}
Command::none()
}
Message::ListDown => {
if let View::List { items, selected } = &mut self.view {
*selected = (*selected + 1).min(items.len().saturating_sub(1));
}
Command::none()
}
Message::Terminal(Event::Key(key)) => handle_key_event(&self.view, key),
Message::Terminal(_) => Command::none(),
Message::TerminalError(e) => {
eprintln!("Terminal error: {e}");
Command::effect(Action::Quit)
}
}
}
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
}
}
#[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 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(())
}