1use std::error;
2
3use ratatui::{
4 style::{self, Color, Style, Stylize},
5 text::Span,
6};
7use tokio::sync::mpsc::UnboundedSender;
8
9use crate::{event::Event, utils::gen_text};
10
11pub type AppResult<T> = std::result::Result<T, Box<dyn error::Error>>;
13
14#[derive(Debug, PartialEq)]
15pub enum Screen {
16 Start,
17 Typing,
18 Result,
19}
20
21enum CursorMovement {
22 Forwards,
23 Backwards,
24}
25
26#[derive(Debug)]
28pub struct App {
29 pub screen: Screen,
30 pub is_typing: bool,
31 pub text: String,
32 pub span_vec: Vec<Span<'static>>,
33 pub cursor_idx: usize,
34 pub countdown: usize,
35 pub key_count: f64,
36 pub exit: bool,
37 pub _sender: UnboundedSender<Event>,
38}
39
40impl App {
41 pub fn new(_sender: UnboundedSender<Event>) -> Self {
43 let text = gen_text();
44
45 let span_vec = string_to_spans(&text);
46
47 let mut app = Self {
48 screen: Screen::Typing,
50 text,
51 span_vec,
52 cursor_idx: 0,
53 key_count: 0.0,
54 countdown: 0,
55 exit: false,
56 is_typing: false,
57 _sender,
58 };
59
60 app.paint_cursor(0, true);
61
62 return app;
63 }
64
65 pub fn tick(&mut self) {
67 self.countdown += 1;
68 if self.countdown == 30 {
69 self.screen = Screen::Result
70 }
71 }
72
73 pub fn quit(&mut self) {
75 self.exit = true;
76 }
77
78 pub fn enter_char(&mut self, ch: char) {
79 if let Some(equal) = self.char_match(ch) {
80 if equal {
81 self.key_count += 1.0;
82 }
83 self.update_span(self.cursor_idx, equal);
84 self.move_cursor(CursorMovement::Forwards);
85 self.paint_cursor(self.cursor_idx, true);
86 };
87 }
88
89 fn move_cursor(&mut self, movement: CursorMovement) {
91 match movement {
92 CursorMovement::Forwards => {
93 self.cursor_idx = self.cursor_idx.saturating_add(1);
94 }
95 CursorMovement::Backwards => {
96 if self.cursor_idx == 0 {
97 return;
98 }
99
100 self.cursor_idx = self.cursor_idx.saturating_sub(1);
101 }
102 }
103 }
104
105 pub fn remove_char(&mut self) {
107 self.paint_cursor(self.cursor_idx, false);
108 self.move_cursor(CursorMovement::Backwards);
109 self.paint_cursor(self.cursor_idx, true);
110 }
111
112 fn char_match(&self, want: char) -> Option<bool> {
114 self.span_vec.get(self.cursor_idx).map(|span| {
115 let got = span.to_string().chars().next().unwrap_or('\n');
116 if got == want {
117 return true;
118 } else {
119 return false;
120 }
121 })
122 }
123
124 fn update_span(&mut self, idx: usize, matched: bool) {
125 let color = if matched { Color::Green } else { Color::Red };
126 let span = self.span_vec.get(idx);
127 if let Some(span) = span {
128 let span = span.clone();
129 self.span_vec[idx] = span.style(Style::new().fg(color)).not_underlined();
130 }
131 }
132
133 fn paint_cursor(&mut self, idx: usize, paint: bool) {
134 if idx > self.span_vec.len() - 1 {
135 return;
136 }
137
138 let span = self.span_vec.get(idx);
139 if let Some(span) = span {
140 let span = span.clone();
141 match paint {
142 true => self.span_vec[idx] = span.style(Style::new()).underlined(),
143 false => self.span_vec[idx] = span.style(Style::new()).not_underlined(),
144 }
145 }
146 }
147
148 pub fn reset(&mut self) {
149 self.screen = Screen::Typing;
151 self.text = gen_text();
152 self.span_vec = string_to_spans(&self.text);
153 self.cursor_idx = 0;
154 self.key_count = 0.0;
155 self.countdown = 0;
156 self.is_typing = false;
157 }
158}
159
160fn string_to_spans(text: &String) -> Vec<Span<'static>> {
161 let span_vec: Vec<Span<'static>> = text
162 .chars()
163 .map(|f| Span::styled(f.to_string(), Style::new().fg(style::Color::DarkGray)))
164 .collect();
165
166 span_vec
167}