jag

typing_rust

public

A small typing test program in Rust

Star0
Watch0

Commit 84c1613e43da

tarball

Jaglekon <58274093+Jaglekon@users.noreply.github.com>2026-09-24 21:10 UTC

3 files changed+329-113
added time
Modified file: Cargo.lock+69 additions−10 deletions
@@ -650,14 +650,6 @@ dependencies = [
650650 "windows-sys",
651651 ]
652652
653-[[package]]
654-name = "monkey"
655-version = "0.1.0"
656-dependencies = [
657- "crossterm",
658- "ratatui",
659-]
660-
661653 [[package]]
662654 name = "nix"
663655 version = "0.29.0"
@@ -856,7 +848,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
856848 checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
857849 dependencies = [
858850 "phf_shared",
859- "rand",
851+ "rand 0.8.8",
860852 ]
861853
862854 [[package]]
@@ -899,6 +891,15 @@ version = "0.2.0"
899891 source = "registry+https://github.com/rust-lang/crates.io-index"
900892 checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
901893
894+[[package]]
895+name = "ppv-lite86"
896+version = "0.2.21"
897+source = "registry+https://github.com/rust-lang/crates.io-index"
898+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
899+dependencies = [
900+ "zerocopy",
901+]
902+
902903 [[package]]
903904 name = "proc-macro2"
904905 version = "1.0.107"
@@ -935,7 +936,27 @@ version = "0.8.8"
935936 source = "registry+https://github.com/rust-lang/crates.io-index"
936937 checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c"
937938 dependencies = [
938- "rand_core",
939+ "rand_core 0.6.4",
940+]
941+
942+[[package]]
943+name = "rand"
944+version = "0.9.5"
945+source = "registry+https://github.com/rust-lang/crates.io-index"
946+checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
947+dependencies = [
948+ "rand_chacha",
949+ "rand_core 0.9.5",
950+]
951+
952+[[package]]
953+name = "rand_chacha"
954+version = "0.9.0"
955+source = "registry+https://github.com/rust-lang/crates.io-index"
956+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
957+dependencies = [
958+ "ppv-lite86",
959+ "rand_core 0.9.5",
939960 ]
940961
941962 [[package]]
@@ -944,6 +965,15 @@ version = "0.6.4"
944965 source = "registry+https://github.com/rust-lang/crates.io-index"
945966 checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
946967
968+[[package]]
969+name = "rand_core"
970+version = "0.9.5"
971+source = "registry+https://github.com/rust-lang/crates.io-index"
972+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
973+dependencies = [
974+ "getrandom 0.3.4",
975+]
976+
947977 [[package]]
948978 name = "ratatui"
949979 version = "0.30.2"
@@ -1428,6 +1458,15 @@ version = "1.20.1"
14281458 source = "registry+https://github.com/rust-lang/crates.io-index"
14291459 checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
14301460
1461+[[package]]
1462+name = "typing_rust"
1463+version = "0.1.0"
1464+dependencies = [
1465+ "crossterm",
1466+ "rand 0.9.5",
1467+ "ratatui",
1468+]
1469+
14311470 [[package]]
14321471 name = "ucd-trie"
14331472 version = "0.1.7"
@@ -1670,3 +1709,23 @@ name = "wit-bindgen"
16701709 version = "0.57.1"
16711710 source = "registry+https://github.com/rust-lang/crates.io-index"
16721711 checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
1712+
1713+[[package]]
1714+name = "zerocopy"
1715+version = "0.8.58"
1716+source = "registry+https://github.com/rust-lang/crates.io-index"
1717+checksum = "c17e8fafad82b542ff3717217ecdc736231b59e387768c9630123b4ce4d2db44"
1718+dependencies = [
1719+ "zerocopy-derive",
1720+]
1721+
1722+[[package]]
1723+name = "zerocopy-derive"
1724+version = "0.8.58"
1725+source = "registry+https://github.com/rust-lang/crates.io-index"
1726+checksum = "595f56e044df4f46a0c9a626f65c3d99eb8488f7e8a8baa12dd76326d9710bf2"
1727+dependencies = [
1728+ "proc-macro2",
1729+ "quote",
1730+ "syn 2.0.119",
1731+]
Modified file: Cargo.toml+2 additions−1 deletions
@@ -1,8 +1,9 @@
11 [package]
2-name = "monkey"
2+name = "typing_rust"
33 version = "0.1.0"
44 edition = "2024"
55
66 [dependencies]
77 crossterm = "0.29.0"
88 ratatui = "0.30.2"
9+rand = "0.9"
Modified file: src/main.rs+258 additions−102 deletions
@@ -1,156 +1,312 @@
1-use std::io;
1+use std::{
2+ io,
3+ time::{
4+ Duration,
5+ Instant,
6+ }
7+};
8+
9+use rand::prelude::*;
10+
11+use crossterm::event::{
12+ self,
13+ poll,
14+ KeyCode,
15+ KeyEvent,
16+ KeyEventKind,
17+ Event,
18+};
219
3-use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
420 use ratatui::{
5- buffer::Buffer,
6- layout::{Rect, Layout, Direction, Constraint},
7- style::Stylize,
8- symbols::border,
9- text::{Line, Text},
10- widgets::{Block, Paragraph, Widget},
11- DefaultTerminal, Frame,
21+ DefaultTerminal,
22+ buffer::Buffer,
23+ layout::Rect,
24+ style::{Stylize, Modifier},
25+ symbols::border,
26+ text::{Line, Text},
27+ widgets::{Block, Widget, Paragraph, Wrap},
28+ Frame,
1229 };
1330
14-#[derive(Debug, PartialEq, Eq)]
15-enum States {
16- Running,
17- Paused,
18- Quit,
31+fn split_at_char(input: &str, n: usize) -> (&str, &str) {
32+ let split = input
33+ .char_indices()
34+ .nth(n)
35+ .map(|(idx , _)| idx)
36+ .unwrap_or(input.len());
37+
38+ input.split_at(split)
39+
1940 }
2041
21-impl Default for States {
22- fn default() -> Self {
23- States::Running
42+
43+#[derive(Debug)]
44+struct TextCombine {
45+ text: String,
46+ len: usize,
47+ pos: usize,
48+ err_n: usize,
49+ err_s: String,
50+}
51+
52+impl TextCombine {
53+ fn new() -> Self {
54+ let words = [
55+ "hello",
56+ "how",
57+ "to",
58+ "back",
59+ "big",
60+ "small",
61+ "something",
62+ "glad",
63+ "general",
64+ "tall",
65+ "nail",
66+ "whatever",
67+ "joke",
68+ "feral",
69+ "apple",
70+ "plow"
71+ ];
72+
73+ /*let text = words
74+ .choose_multiple(&mut rand::rng(), 10)
75+ .cloned()
76+ .collect::<Vec<_>>()
77+ .join(" ");*/
78+ let mut rng = rand::rng();
79+
80+ let text = (0..60)
81+ .filter_map(|_| words.choose(&mut rng))
82+ .cloned()
83+ .collect::<Vec<_>>()
84+ .join(" ");
85+
86+ let len = text.chars().count();
87+
88+ Self {
89+ text,
90+ len,
91+ pos: 0,
92+ err_n: 0,
93+ err_s: String::new(),
94+ }
95+
96+ }
97+
98+ fn curr_char(&self) -> Option<char> {
99+ self.pos_char(self.real_pos())
100+ }
101+
102+ fn pos_char(&self, pos: usize) -> Option<char> {
103+ self
104+ .text
105+ .chars()
106+ .nth(pos)
107+ }
108+
109+ fn real_pos(&self) -> usize {
110+ self.pos + self.err_n
111+ }
112+
113+ fn back(&mut self) {
114+ if self.err_s.len() > 0 {
115+ self.err_s.pop() ;
116+ } else if self.err_n > 0 {
117+ self.err_n -= 1;
118+ } else if self.pos > 0 {
119+ self.pos -= 1;
120+ }
24121 }
25122 }
26123
27-#[derive(Debug, Default)]
28-pub struct App {
29- counter: u8,
30- state: States,
124+#[derive(Debug)]
125+pub struct Timer {
126+ start: Instant,
127+ duration: Duration,
31128 }
32129
33-impl Widget for &App {
34- fn render(self, area: Rect, buf: &mut Buffer) {
130+impl Timer {
131+ fn new(duration: Duration) -> Self {
132+ Self {
133+ start: Instant::now(),
134+ duration,
135+ }
136+ }
137+
138+ fn remaining (&self) -> Duration {
139+ self.duration.saturating_sub(self.elapsed())
140+ }
141+
142+ fn elapsed (&self) -> Duration {
143+ self.start.elapsed()
144+ }
145+}
35146
36- let title = Line::from(" Counter App Tutorial ".bold());
37- let instructions = Line::from(vec![
38- " Decrement ".into(),
39- "<Left>".blue().bold(),
40- " Increment ".into(),
41- "<Right>".blue().bold(),
42- " Pause ".into(),
43- "<P>".blue().bold(),
44- " Quit".into(),
45- "<Q> ".blue().bold(),
147+#[derive(Debug)]
148+pub struct TypingTest {
149+ status: bool,
150+ errors: usize,
151+ typing: TextCombine,
152+ words: usize,
153+ timer: Timer,
154+}
155+
156+impl Default for TypingTest {
157+ fn default() -> Self {
158+ Self {
159+ status: true,
160+ errors: 0,
161+ words: 0,
162+ typing: TextCombine::new(),
163+ timer: Timer::new(Duration::from_secs(30))
164+ }
165+ }
166+}
167+
168+impl Widget for &TypingTest {
169+ fn render(self, area: Rect, buf: &mut Buffer) {
170+ let title = Line::from(vec![
171+ " WPM ".into(),
172+ format!("{:4.1}" ,(self.words as f64 / self.timer.elapsed().as_secs_f64()) * 60.0).into(),
173+ " // Time ".into(),
174+ format!("{:2}", (self.timer.remaining().as_secs())).into(),
175+ " // Mistakes ".into(),
176+ self.errors.to_string().into(),
177+ (" ").into()
178+ ]);
179+ let bottom = Line::from(vec![
180+ " Exit ".into(),
181+ "<Esc> ".into(),
46182 ]);
183+
47184 let block = Block::bordered()
48- .title(title.centered())
49- .title_bottom(instructions.centered())
185+ .title(title.centered().bold())
186+ .title_bottom(bottom.centered().bold())
50187 .border_set(border::THICK);
188+
189+ let (first, rest) =
190+ split_at_char(self.typing.text.as_str(), self.typing.pos);
51191
52- let counter_text = Text::from(vec![Line::from(vec![
53- "Value: ".into(),
54- self.counter.to_string().yellow(),
55- ])]);
192+ let (second, rest2) =
193+ split_at_char(rest, self.typing.err_n);
56194
57- Paragraph::new(counter_text)
58- .centered()
59- .block(block)
60- .render(area, buf);
195+ let (third, fourth) =
196+ split_at_char(rest2, 1);
61197
62- if self.state == States::Paused {
63-
64- let pause_area = area.centered(
65- Constraint::Length(20),
66- Constraint::Length(5),
67- );
198+
68199
69- let pause_block = Block::bordered()
70- .border_set(border::ROUNDED);
200+ let text = Line::from(vec![
201+ first.green().into(),
202+ second.red().into(),
203+ self.typing.err_s.clone().red().into(),
204+ if third.len() != 0 {
205+ third
206+ } else {
207+ " "
208+ }
209+ .add_modifier(Modifier::UNDERLINED | Modifier::BOLD)
210+ .into(),
211+ fourth.into(),
212+ ]);
71213
72- let pause_text = Text::from("PAUSE");
73-
74- let pause_text_area = pause_area.centered(
75- Constraint::Length(pause_text.width() as u16),
76- Constraint::Length(1),
77- );
78-
79- pause_block.render(pause_area, buf);
80214
81- Paragraph::new(pause_text)
82- .render(pause_text_area, buf);
83- }
215+ Paragraph::new(text)
216+ .wrap(Wrap {trim: true})
217+ .block(block)
218+ .render(area, buf);
84219 }
85220 }
86221
87-impl App {
88222
89- pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
90- while self.state != States::Quit {
223+impl TypingTest {
224+
225+ fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
226+
227+ while self.status {
91228 terminal.draw(|frame| self.draw(frame))?;
92- self.handle_events()?;
229+ if poll(Duration::from_millis(100))? {
230+ self.handle_events()?;
231+ }
93232 }
233+
94234 Ok(())
95235 }
96-
236+
97237 fn draw(&self, frame: &mut Frame) {
98238 frame.render_widget(self, frame.area());
99239 }
100240
101- fn handle_events(&mut self) -> io::Result<()> {
241+ fn handle_events(&mut self) -> io::Result<()>{
102242 match event::read()? {
103243 Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
104- self.handle_key_event(key_event)
105- },
244+ self.handle_key_event(key_event);
245+ },
106246 _ => {},
107- };
247+ }
108248 Ok(())
109249 }
110250
111251 fn handle_key_event(&mut self, key_event: KeyEvent) {
112- match self.state {
113- States::Running =>
114- match key_event.code {
115- KeyCode::Char('p') => self.pause(),
116- KeyCode::Char('q') => self.exit(),
117- KeyCode::Left => self.decrement_counter(),
118- KeyCode::Right => self.increment_counter(),
119- _ => {}
120- },
121- States::Paused =>
122- match key_event.code {
123- KeyCode::Char('p') => self.pause(),
124- KeyCode::Char('q') => self.exit(),
125- _ => {},
126- },
127-
128- States::Quit => {},
252+ match key_event.code {
253+ KeyCode::Esc => self.exit(),
254+ KeyCode::Backspace => self.back(),
255+ KeyCode::Char(c) => self.handle_char(c),
256+ _ => {},
129257 }
130258 }
131259
132- fn pause(&mut self) {
133- if self.state == States::Paused {
134- self.state = States::Running;
135- } else {
136- self.state = States::Paused;
260+ fn back(&mut self) {
261+
262+ if let Some(pos) = self.typing
263+ .pos_char(self.typing.real_pos().saturating_sub(1))
264+ {
265+ if pos == ' ' {return;}
137266 }
138- }
139267
140- fn exit(&mut self) {
141- self.state = States::Quit;
268+ self.typing.back();
142269 }
143270
144- fn increment_counter(&mut self) {
145- self.counter += 1;
271+ fn handle_char(&mut self, c: char) {
272+
273+ if self.typing.real_pos() >= self.typing.len {
274+ return;
275+ }
276+
277+ if let Some(curr) = self.typing
278+ .curr_char()
279+ {
280+
281+ if curr == c
282+ && self.typing.err_n == 0
283+ && self.typing.err_s.len() == 0
284+ {
285+ self.typing.pos += 1;
286+ if c == ' ' {
287+ self.words += 1;
288+ }
289+ } else if c != ' ' {
290+ if curr != ' ' {
291+ self.typing.err_n += 1;
292+ self.errors += 1;
293+ } else if self.typing.err_s.len() < 16 {
294+ self.typing.err_s.push(c);
295+ self.errors += 1;
296+ }
297+ }
298+
299+ }
300+
146301 }
147302
148- fn decrement_counter(&mut self) {
149- self.counter -= 1;
303+ fn exit(&mut self) {
304+ self.status = false;
150305 }
151306 }
152307
153308
154-fn main() -> io::Result<()> {
155- ratatui::run(|terminal| App::default().run(terminal))
309+
310+fn main () -> io::Result<()> {
311+ ratatui::run(|term| TypingTest::default().run(term))
156312 }