Smart Typing Speed Trainer

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()


No comments: