import tkinter as tk
from tkinter import ttk, messagebox, simpledialog
import time
import random
import json
import os
STATS_FILE = "typing_stats.json"
SAMPLE_PARAGRAPHS = {
"Easy": [
"The quick brown fox jumps over the lazy dog.",
"Practice makes perfect. Keep typing every day.",
"Sunrise over the hills gave a golden glow."
],
"Medium": [
"Learning to type faster requires consistent short sessions and focused practice.",
"Productivity improves when repetitive tasks are automated and simplified.",
"A small daily habit can compound into significant improvement over time."
],
"Hard": [
"Synthesis of knowledge emerges when creativity meets disciplined experimentation and reflection.",
"Contributing to open-source projects accelerates learning through real-world code review and collaboration.",
"Concurrency issues often surface under load where assumptions about ordering and state break down."
]
}
# -----------------------
# Utilities: stats file
# -----------------------
def load_stats():
if not os.path.exists(STATS_FILE):
return []
try:
with open(STATS_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return []
def save_stats(stats):
with open(STATS_FILE, "w", encoding="utf-8") as f:
json.dump(stats, f, indent=2)
# -----------------------
# Typing logic helpers
# -----------------------
def calc_wpm(char_count, elapsed_seconds):
# Standard WPM uses 5 characters per word
minutes = elapsed_seconds / 60.0 if elapsed_seconds > 0 else 1/60.0
return (char_count / 5.0) / minutes
def calc_accuracy(correct_chars, total_typed):
if total_typed == 0:
return 0.0
return (correct_chars / total_typed) * 100.0
# -----------------------
# Main App
# -----------------------
class TypingTrainerApp:
def __init__(self, root):
self.root = root
self.root.title("Smart Typing Speed Trainer")
self.root.geometry("900x600")
self.difficulty = tk.StringVar(value="Easy")
self.username = tk.StringVar(value="Guest")
self.paragraph = ""
self.start_time = None
self.end_time = None
self.running = False
self.correct_chars = 0
self.total_typed = 0
self.errors = 0
self.stats = load_stats()
self.build_ui()
self.new_paragraph()
def build_ui(self):
top = ttk.Frame(self.root)
top.pack(side=tk.TOP, fill=tk.X, padx=8, pady=8)
ttk.Label(top, text="Name:").pack(side=tk.LEFT)
name_entry = ttk.Entry(top, textvariable=self.username, width=18)
name_entry.pack(side=tk.LEFT, padx=6)
ttk.Label(top, text="Difficulty:").pack(side=tk.LEFT, padx=(12,0))
diff_menu = ttk.OptionMenu(top, self.difficulty, self.difficulty.get(), *SAMPLE_PARAGRAPHS.keys())
diff_menu.pack(side=tk.LEFT, padx=6)
ttk.Button(top, text="Next Paragraph", command=self.new_paragraph).pack(side=tk.LEFT, padx=6)
ttk.Button(top, text="Restart", command=self.restart).pack(side=tk.LEFT, padx=6)
ttk.Button(top, text="Save Result", command=self.save_result).pack(side=tk.LEFT, padx=6)
ttk.Button(top, text="Leaderboard", command=self.show_leaderboard).pack(side=tk.LEFT, padx=6)
ttk.Button(top, text="Clear Stats", command=self.clear_stats).pack(side=tk.RIGHT, padx=6)
# Paragraph display
paragraph_frame = ttk.LabelFrame(self.root, text="Type the text below")
paragraph_frame.pack(fill=tk.BOTH, expand=False, padx=10, pady=8)
self.paragraph_text = tk.Text(paragraph_frame, height=6, wrap="word", font=("Consolas", 14), padx=8, pady=8, state="disabled")
self.paragraph_text.pack(fill=tk.BOTH, expand=True)
# Typing area
typing_frame = ttk.LabelFrame(self.root, text="Your typing")
typing_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=8)
self.typing_text = tk.Text(typing_frame, height=10, wrap="word", font=("Consolas", 14), padx=8, pady=8)
self.typing_text.pack(fill=tk.BOTH, expand=True)
self.typing_text.bind("<Key>", self.on_key_press)
# Disable paste
self.typing_text.bind("<<Paste>>", lambda e: "break")
self.typing_text.bind("<Control-v>", lambda e: "break")
self.typing_text.bind("<Button-2>", lambda e: "break")
# Stats
stats_frame = ttk.Frame(self.root)
stats_frame.pack(fill=tk.X, padx=10, pady=6)
self.wpm_var = tk.StringVar(value="WPM: 0.0")
self.acc_var = tk.StringVar(value="Accuracy: 0.0%")
self.err_var = tk.StringVar(value="Errors: 0")
self.time_var = tk.StringVar(value="Time: 0.0s")
ttk.Label(stats_frame, textvariable=self.wpm_var, font=("Arial", 12)).pack(side=tk.LEFT, padx=8)
ttk.Label(stats_frame, textvariable=self.acc_var, font=("Arial", 12)).pack(side=tk.LEFT, padx=8)
ttk.Label(stats_frame, textvariable=self.err_var, font=("Arial", 12)).pack(side=tk.LEFT, padx=8)
ttk.Label(stats_frame, textvariable=self.time_var, font=("Arial", 12)).pack(side=tk.LEFT, padx=8)
# Progress / highlight correct vs incorrect
lower = ttk.Frame(self.root)
lower.pack(fill=tk.BOTH, expand=False, padx=10, pady=6)
ttk.Label(lower, text="Tips: Start typing to begin the timer. Pasting is disabled.").pack(side=tk.LEFT)
def new_paragraph(self):
self.pause()
diff = self.difficulty.get()
pool = SAMPLE_PARAGRAPHS.get(diff, SAMPLE_PARAGRAPHS["Easy"])
self.paragraph = random.choice(pool)
# show paragraph readonly
self.paragraph_text.config(state="normal")
self.paragraph_text.delete("1.0", tk.END)
self.paragraph_text.insert(tk.END, self.paragraph)
self.paragraph_text.config(state="disabled")
self.restart()
def restart(self):
self.pause()
self.typing_text.delete("1.0", tk.END)
self.start_time = None
self.end_time = None
self.running = False
self.correct_chars = 0
self.total_typed = 0
self.errors = 0
self.update_stats_display(0.0)
self.typing_text.focus_set()
def pause(self):
self.running = False
def on_key_press(self, event):
# ignore non-printing keys that don't change text (Shift, Ctrl, Alt)
if event.keysym in ("Shift_L","Shift_R","Control_L","Control_R","Alt_L","Alt_R","Caps_Lock","Tab","Escape"):
return
# start timer on first real key
if not self.running:
self.start_time = time.time()
self.running = True
# schedule first update
self.root.after(100, self.periodic_update)
# schedule update immediately after Tk has applied the key to the widget
self.root.after(1, self.evaluate_typing)
def evaluate_typing(self):
typed = self.typing_text.get("1.0", tk.END)[:-1] # drop trailing newline Tk adds
target = self.paragraph
# compute per-character correctness up to typed length
total = len(typed)
correct = 0
errors = 0
for i, ch in enumerate(typed):
if i < len(target) and ch == target[i]:
correct += 1
else:
errors += 1
# count omissions beyond target length as errors too if user keeps typing
if total > len(target):
errors += total - len(target)
self.correct_chars = correct
self.total_typed = total
self.errors = errors
# if user finished (typed length equals paragraph length), stop timer
if total >= len(target):
# check final correctness
if correct == len(target):
self.end_time = time.time()
self.running = False
self.update_stats_display(final=True)
messagebox.showinfo("Completed", f"Well done, {self.username.get()}!\nYou finished the paragraph.")
return
# otherwise update live stats
self.update_stats_display(final=False)
def periodic_update(self):
if self.running:
self.update_stats_display(final=False)
self.root.after(200, self.periodic_update)
def update_stats_display(self, final=False):
elapsed = (self.end_time - self.start_time) if (self.start_time and self.end_time) else (time.time() - self.start_time if self.start_time else 0.0)
wpm = calc_wpm(self.total_typed, elapsed) if self.total_typed > 0 else 0.0
acc = calc_accuracy(self.correct_chars, self.total_typed) if self.total_typed > 0 else 0.0
self.wpm_var.set(f"WPM: {wpm:.1f}")
self.acc_var.set(f"Accuracy: {acc:.1f}%")
self.err_var.set(f"Errors: {self.errors}")
self.time_var.set(f"Time: {elapsed:.1f}s")
if final and self.start_time:
# show final summary and auto-save to stats (ask username)
pass
def save_result(self):
if not self.start_time:
messagebox.showwarning("No attempt", "Start typing first to record a result.")
return
# If still running, finalize end time
if self.running:
self.end_time = time.time()
self.running = False
self.evaluate_typing()
elapsed = (self.end_time - self.start_time) if (self.start_time and self.end_time) else 0.0
wpm = calc_wpm(self.total_typed, elapsed) if elapsed > 0 else 0.0
acc = calc_accuracy(self.correct_chars, self.total_typed) if self.total_typed > 0 else 0.0
name = self.username.get().strip() or "Guest"
record = {
"name": name,
"difficulty": self.difficulty.get(),
"wpm": round(wpm, 1),
"accuracy": round(acc, 1),
"errors": int(self.errors),
"chars_typed": int(self.total_typed),
"time_seconds": round(elapsed, 1),
"paragraph": self.paragraph
}
self.stats.append(record)
# keep only latest 200
self.stats = self.stats[-200:]
save_stats(self.stats)
messagebox.showinfo("Saved", f"Result saved for {name}.\nWPM={record['wpm']}, Accuracy={record['accuracy']}%")
# refresh leaderboard
self.show_leaderboard()
def show_leaderboard(self):
lb_win = tk.Toplevel(self.root)
lb_win.title("Leaderboard / Recent Attempts")
lb_win.geometry("700x400")
frame = ttk.Frame(lb_win)
frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=8)
cols = ("name", "difficulty", "wpm", "accuracy", "errors", "time_seconds")
tree = ttk.Treeview(frame, columns=cols, show="headings")
for c in cols:
tree.heading(c, text=c.capitalize())
tree.column(c, width=100, anchor="center")
tree.pack(fill=tk.BOTH, expand=True)
# sort by wpm desc
sorted_stats = sorted(self.stats, key=lambda r: r.get("wpm", 0), reverse=True)
for rec in sorted_stats:
tree.insert("", "end", values=(rec["name"], rec["difficulty"], rec["wpm"], rec["accuracy"], rec["errors"], rec["time_seconds"]))
btn_frame = ttk.Frame(lb_win)
btn_frame.pack(fill=tk.X, pady=6)
ttk.Button(btn_frame, text="Close", command=lb_win.destroy).pack(side=tk.RIGHT, padx=6)
def clear_stats(self):
if messagebox.askyesno("Confirm", "Clear all saved stats? This cannot be undone."):
self.stats = []
save_stats(self.stats)
messagebox.showinfo("Cleared", "All saved stats removed.")
# -----------------------
# Run
# -----------------------
def main():
root = tk.Tk()
app = TypingTrainerApp(root)
root.mainloop()
if __name__ == "__main__":
main()