1use chrono::prelude::*;
2use crossterm::event::KeyCode;
3use serde::{Deserialize, Serialize};
4use std::path::Path;
5use std::path::PathBuf;
6use std::{collections::HashMap, rc::Rc};
7use tui::{
8 backend::Backend,
9 style::{Color, Style},
10 text::Span,
11 Frame,
12};
13
14pub mod windows;
15
16#[derive(Deserialize, Serialize, Clone)]
17pub struct TraceRun {
18 wpm: f64,
19 accuracy: f64,
20 total_points: f64,
21 seconds: f64,
22}
23
24impl TraceRun {
25 pub fn to_csv(&self) -> String {
26 format!(
27 "{:},{:},{:},{:}",
28 self.wpm, self.accuracy, self.total_points, self.seconds
29 )
30 }
31}
32
33pub fn get_track_record() -> Vec<TraceRun> {
34 let path = get_app_path(".runs.csv");
35 match csv::Reader::from_path(path) {
36 Ok(mut reader) => {
37 let mut records = vec![];
38 for result in reader.deserialize() {
39 if let Ok(record) = result {
40 records.push(record)
41 }
42 }
43 records
44 }
45 Err(_) => Vec::new(),
46 }
47}
48
49#[derive(Deserialize, Clone)]
50pub struct AppParagraph {
51 content: String,
52 title: String,
53 author: String,
54 date: String,
55}
56
57impl AppParagraph {
58 pub fn new() -> AppParagraph {
59 AppParagraph {
60 content: "".to_string(),
61 title: "".to_string(),
62 author: "".to_string(),
63 date: "".to_string(),
64 }
65 }
66}
67
68#[derive(Clone)]
69pub enum CharStatus {
70 Correct,
71 Wrong,
72 Default,
73 Current,
74}
75
76#[derive(Clone)]
77pub struct ParagraphChar {
78 character: char,
79 status: CharStatus,
80}
81
82impl ParagraphChar {
83 pub fn new(c: char, status: CharStatus) -> ParagraphChar {
84 ParagraphChar {
85 character: c,
86 status,
87 }
88 }
89 pub fn to_span(&self) -> Span {
90 match self.status {
91 CharStatus::Correct => Span::styled(
92 self.character.to_string(),
93 Style::default().fg(Color::Green),
94 ),
95 CharStatus::Current => Span::styled(
96 self.character.to_string(),
97 Style::default().fg(Color::White).bg(Color::DarkGray),
98 ),
99 CharStatus::Wrong => {
100 if self.character == ' ' {
101 Span::styled(self.character.to_string(), Style::default().bg(Color::Red))
102 } else {
103 Span::styled(self.character.to_string(), Style::default().fg(Color::Red))
104 }
105 }
106 CharStatus::Default => Span::styled(
107 self.character.to_string(),
108 Style::default().fg(Color::DarkGray),
109 ),
110 }
111 }
112}
113
114pub fn convert_string_to_chars(s: String) -> Vec<ParagraphChar> {
115 let mut vector = vec![];
116 for elem in s.chars() {
117 vector.push(ParagraphChar::new(elem, CharStatus::Default));
118 }
119 return vector;
120}
121
122#[derive(Clone)]
123pub struct State {
124 user_name: String,
125 chars: Vec<ParagraphChar>,
126 show_bar_charts: bool,
127 paragraph: AppParagraph,
128 initial_time: DateTime<Utc>,
129 end_time: DateTime<Utc>,
130 current_error_count: usize,
131 total_error_count: usize,
132 word_count: usize,
133 index: usize,
134}
135
136impl State {
137 pub fn new() -> State {
138 State {
139 chars: vec![],
140 user_name: String::new(),
141 paragraph: AppParagraph::new(),
142 initial_time: Utc::now(),
143 end_time: Utc::now(),
144 show_bar_charts: false,
145 current_error_count: 0,
146 total_error_count: 0,
147 word_count: 0,
148 index: 0,
149 }
150 }
151 pub fn reset(&mut self) {
152 self.chars = vec![];
153 self.paragraph = AppParagraph::new();
154 self.initial_time = Utc::now();
155 self.end_time = Utc::now();
156 self.show_bar_charts = false;
157 self.current_error_count = 0;
158 self.total_error_count = 0;
159 self.word_count = 0;
160 self.index = 0;
161 }
162 pub fn create_run(&self) -> TraceRun {
163 let accuracy = (self.chars.len() - self.total_error_count) as f64 / self.chars.len() as f64;
164 let duration = self.end_time - self.initial_time;
165 let seconds = (duration.num_milliseconds() as f64) / 1000.0;
166
167 let wpm = self.word_count as f64 / seconds * 60.0;
168 let total_points = (wpm + accuracy * wpm) / 2.0;
169 TraceRun {
170 wpm,
171 accuracy,
172 total_points,
173 seconds,
174 }
175 }
176}
177
178pub struct WindowCommand<B: Backend> {
179 pub activator_key: KeyCode,
180 pub action: Box<dyn Fn(&mut State) -> Option<Window<B>>>,
181}
182
183impl<B: Backend> WindowCommand<B> {
184 pub fn new_char_command(
185 activator: char, command: Box<dyn Fn(&mut State) -> Option<Window<B>>>,
186 ) -> WindowCommand<B> {
187 WindowCommand {
188 activator_key: KeyCode::Char(activator),
189 action: command,
190 }
191 }
192}
193
194pub struct Window<B: Backend> {
195 pub commands: HashMap<KeyCode, WindowCommand<B>>,
196 pub ui: fn(Rc<State>) -> Box<dyn Fn(&mut Frame<B>)>,
197}
198
199pub fn get_app_path(file_path: &str) -> PathBuf {
200 let current_dir = std::env::current_dir().unwrap();
201 Path::new(¤t_dir).join(file_path)
202}
203
204pub fn generate_all_chars() -> Vec<char> {
205 let mut chars = vec![
206 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
207 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
208 ];
209 let mut upper_chars: Vec<char> = chars.iter().map(|a| a.to_ascii_uppercase()).collect();
210 let mut punctuation = vec![
211 ' ', ',', '.', ':', '"', '-', '@', ';', '<', '>', '+', '-', '_', '(', ')', '=', '*', '/',
212 '¡', '!', '¿', '?', '#', '$', '%', '&', '°', '\'', '^', '~', '[', ']', '{', '}',
213 ];
214 let mut numbers = vec!['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'];
215 let mut extras = vec![
216 'á', 'Á', 'é', 'É', 'í', 'Í', 'ó', 'Ó', 'ú', 'Ú', 'ä', 'Ä', 'ë', 'Ë', 'ï', 'Ï', 'ö', 'Ö',
217 'ü', 'Ü', 'ç', 'ñ', 'Ñ',
218 ];
219
220 chars.append(&mut upper_chars);
221 chars.append(&mut punctuation);
222 chars.append(&mut numbers);
223 chars.append(&mut extras);
224
225 chars
226}
227
228pub fn add_to_commands<B: 'static + Backend>(
229 commands: &mut HashMap<KeyCode, WindowCommand<B>>, char_array: &Vec<char>,
230 cmd: Box<dyn Fn(char) -> Box<dyn Fn(&mut State) -> Option<Window<B>>>>,
231) {
232 for elem in char_array {
233 commands.insert(
234 KeyCode::Char(*elem),
235 WindowCommand {
236 activator_key: KeyCode::Char(*elem),
237 action: cmd(*elem),
238 },
239 );
240 }
241}