-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscreen.py
More file actions
263 lines (210 loc) · 8.3 KB
/
screen.py
File metadata and controls
263 lines (210 loc) · 8.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
import platform
import sys
from time import sleep, time
from pynput import keyboard
from rich.align import Align
from rich.console import Console
from rich.layout import Layout
from rich.live import Live
from rich.padding import Padding
from rich.text import Text
from storage import ResultsStorage
console = Console()
class TypingScreen:
def __init__(self, words: str, duration: int = 30):
self.words = words
self.duration = duration
self.typed_text = ""
self.running = True
self.old_settings = None
self.start_time: float = 0.0
self.end_time: float = 0.0
self.test_started = False
def create_display_text(self):
"""Create colored text based on what user has typed"""
display = Text()
for i, char in enumerate(self.words):
if i < len(self.typed_text):
# User has typed this character
if self.typed_text[i] == char:
# Correct - show in green/normal
display.append(char, style="green")
else:
# Incorrect - show in red
display.append(char, style="red bold")
else:
# Not yet typed - show dimmed
display.append(char, style="dim")
return display
def create_layout(self, remaining: int):
"""Create the layout with current state"""
layout = Layout()
layout.split_column(Layout(name="main"), Layout(name="footer", size=1))
# Create display text with colors
display_text = self.create_display_text()
padded_obj = Padding(display_text, (0, 5), expand=False)
# Footer with timer or waiting message
if not self.test_started:
footer_obj = Text("Press any key to start...", style="yellow dim")
else:
footer_obj = Text(f"Time: {remaining}s", style="dim")
layout["main"].update(Align.center(padded_obj, vertical="middle"))
layout["footer"].update(Align.center(footer_obj))
return layout
def on_press(self, key):
"""Handle keyboard input"""
# Start the test on first keypress
if not self.test_started:
self.test_started = True
self.start_time = time()
# Handle backspace
if key == keyboard.Key.backspace:
if self.typed_text:
self.typed_text = self.typed_text[:-1]
return
# Handle escape
if key == keyboard.Key.esc:
self.running = False
return
# Handle space
if key == keyboard.Key.space:
self.typed_text += " "
# Check if user finished typing
if len(self.typed_text) >= len(self.words):
self.running = False
return
# Handle regular character keys
try:
if hasattr(key, "char") and key.char is not None:
self.typed_text += key.char
# Check if user finished typing
if len(self.typed_text) >= len(self.words):
self.running = False
except AttributeError:
pass
def calculate_results(self):
"""Calculate WPM and Accuracy"""
# Calculate time taken
time_taken = self.end_time - self.start_time
minutes = time_taken / 60
# Calculate correct and incorrect characters
correct_chars = 0
incorrect_chars = 0
min_length = min(len(self.typed_text), len(self.words))
for i in range(min_length):
if self.typed_text[i] == self.words[i]:
correct_chars += 1
else:
incorrect_chars += 1
# Extra characters typed (beyond the original text)
if len(self.typed_text) > len(self.words):
incorrect_chars += len(self.typed_text) - len(self.words)
total_chars = len(self.typed_text)
# Calculate accuracy (percentage of correct characters)
accuracy = (correct_chars / total_chars * 100) if total_chars > 0 else 0
# Calculate WPM
# Standard: 1 word = 5 characters (including spaces)
words_typed = correct_chars / 5
wpm = words_typed / minutes if minutes > 0 else 0
return {
"wpm": round(wpm, 2),
"accuracy": round(accuracy, 2),
"correct_chars": correct_chars,
"incorrect_chars": incorrect_chars,
"total_chars": total_chars,
"time_taken": round(time_taken, 2),
"duration": self.duration,
}
def render(self):
"""Main render loop with timer and typing"""
# Only use termios on Unix-like systems
if platform.system() != "Windows":
try:
import termios
import tty
self.old_settings = termios.tcgetattr(sys.stdin)
tty.setcbreak(sys.stdin.fileno())
except:
pass
try:
# Start keyboard listener with suppress=True to prevent echo
listener = keyboard.Listener(on_press=self.on_press, suppress=True)
listener.start()
# Use Live to update without scrolling
with Live(
self.create_layout(self.duration),
console=console,
screen=True,
refresh_per_second=10,
) as live:
while self.running:
# If test hasn't started yet, just keep showing waiting message
if not self.test_started:
live.update(self.create_layout(self.duration))
sleep(0.1)
continue
# Calculate elapsed time (only after test started)
elapsed = time() - self.start_time
remaining = self.duration - int(elapsed)
# Check if time is up
if remaining <= 0:
self.running = False
break
# Update the live display
live.update(self.create_layout(remaining))
self.end_time = time()
# Stop listener
listener.stop()
finally:
# Restore terminal settings (only on Unix-like systems)
if platform.system() != "Windows" and self.old_settings:
import termios
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.old_settings)
# Calculate results
results = self.calculate_results()
# Show completion message
console.clear()
# Check if user finished early or time ran out
if len(self.typed_text) >= len(self.words):
console.print("[bold green]Completed![/bold green]\n")
else:
console.print("[bold yellow]Time's up![/bold yellow]\n")
console.print(f"[cyan]WPM:[/cyan] [bold]{results['wpm']}[/bold]")
console.print(f"[cyan]Accuracy:[/cyan] [bold]{results['accuracy']}%[/bold]")
console.print(
f"[cyan]Correct Characters:[/cyan] {results['correct_chars']}/{results['total_chars']}"
)
console.print(f"[cyan]Time:[/cyan] {results['time_taken']}s")
return results
def render_screen(words: str, duration: int = 30):
"""
Render the typing test screen with timer and typing functionality.
Args:
words: The text to display
duration: How long to run the test in seconds (default: 30)
Returns:
dict: Test results containing wpm, accuracy, etc.
"""
screen = TypingScreen(words, duration)
results = screen.render()
return results
def render_stats(username):
"""
Render the stats screen.
Args:
username: The username of the user
Returns:
None
"""
storage = ResultsStorage()
stats = storage.get_stats()
if not stats:
print("No stats found. Start typing to build history 🚀")
return
print(f"{username} Stats ")
print("________________________")
print(f"{'Typing Sessions':<20}: {stats['total_tests']}")
print(f"{'Average Speed':<20}: {stats['avg_wpm']} WPM")
print(f"{'Average Accuracy':<20}: {stats['avg_accuracy']} %")
print(f"{'Top Speed':<20}: {stats['best_wpm']} WPM")
print(f"{'Best Accuracy':<20}: {stats['best_accuracy']} %")