From 4ab30dbc71c435c01923e8efc478d9ea7d9a73e8 Mon Sep 17 00:00:00 2001 From: Adithyan H Date: Fri, 13 Mar 2026 23:42:55 +0530 Subject: [PATCH] Update typing_speed_test.py --- typing_speed_test.py | 698 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 620 insertions(+), 78 deletions(-) diff --git a/typing_speed_test.py b/typing_speed_test.py index 6d55744..47d5723 100644 --- a/typing_speed_test.py +++ b/typing_speed_test.py @@ -1,92 +1,634 @@ -import tkinter as tk -from tkinter import messagebox +import customtkinter as ctk import random import time +import json +import os +from datetime import datetime, timedelta + +# winsound is Windows-only; fail gracefully on other platforms +try: + import winsound + def beep(freq, dur): + winsound.Beep(freq, dur) +except ImportError: + def beep(freq, dur): + pass # Silent fallback on macOS / Linux + +ctk.set_appearance_mode("light") +ctk.set_default_color_theme("blue") + +DEFAULT_DURATION = 60 +DATA_FILE = "streak_data.json" + +# ============================================================ +# TEXT POOLS – keyed by difficulty / length category +# ============================================================ + +TEXT_POOLS = { + "Short Sentence": [ + "Technology is the most effective way to change the world.", + "Innovation is the ability to see change as an opportunity.", + "Artificial Intelligence helps humans solve complex problems.", + "Data science is transforming industries across the globe.", + "Programming improves logical thinking and creativity.", + "The quick brown fox jumps over the lazy dog.", + "Every great developer was once a beginner.", + "Code is like humor: when you have to explain it, it is bad.", + "Simplicity is the soul of efficiency.", + "First solve the problem, then write the code.", + ], + "Paragraph": [ + ( + "Technology is reshaping every aspect of modern life. From the way " + "we communicate to the way we work, digital tools have become " + "indispensable. Embracing change and learning new skills is the key " + "to staying relevant in a rapidly evolving landscape. Those who " + "adapt will thrive, while those who resist may be left behind." + ), + ( + "Open-source software has revolutionized the technology industry by " + "allowing developers around the world to collaborate freely. Projects " + "like Linux, Python, and Firefox demonstrate that community-driven " + "development can produce software that rivals or surpasses proprietary " + "alternatives. Contributing to open source is a rewarding experience " + "that sharpens skills and builds professional networks." + ), + ( + "Cloud computing enables businesses to scale their infrastructure on " + "demand without the need for expensive hardware. Services such as " + "virtual machines, managed databases, and serverless functions allow " + "teams to focus on building products rather than managing servers. " + "The pay-as-you-go model makes cutting-edge technology accessible to " + "startups and enterprises alike." + ), + ], + "Long Text": [ + ( + "The history of computing stretches back centuries, from the abacus " + "to Charles Babbage's Analytical Engine. In the twentieth century, " + "pioneers like Alan Turing and John von Neumann laid the theoretical " + "groundwork for modern computers. The invention of the transistor in " + "1947 sparked a revolution that led to integrated circuits, " + "microprocessors, and eventually the personal computer. Today, " + "billions of interconnected devices form a global network that " + "carries virtually all of human knowledge. Machine learning " + "algorithms sift through enormous datasets to uncover patterns no " + "human could detect alone. Quantum computing promises to solve " + "problems that are currently intractable, from drug discovery to " + "cryptography. As technology continues to advance at an exponential " + "pace, society must grapple with questions of ethics, privacy, and " + "equity. Ensuring that the benefits of innovation are shared broadly " + "is one of the defining challenges of our time. Education systems " + "must evolve to prepare students not just to use technology, but to " + "understand and shape it. The future belongs to those who can think " + "critically, collaborate effectively, and adapt to change with " + "resilience and creativity." + ), + ( + "Software engineering is both an art and a science. Writing clean, " + "maintainable code requires not only technical skill but also " + "empathy for the developers who will read and modify it in the " + "future. Version control systems like Git enable teams to work on " + "the same codebase without stepping on each other's toes, while " + "continuous integration pipelines catch bugs before they reach " + "production. Test-driven development encourages programmers to think " + "about edge cases early, resulting in more robust software. Code " + "reviews foster knowledge sharing and help maintain consistent " + "standards across a project. Documentation, often neglected, is " + "crucial for onboarding new team members and preserving " + "institutional knowledge. Agile methodologies break large projects " + "into manageable sprints, allowing teams to deliver value " + "incrementally and respond to changing requirements. DevOps " + "practices blur the line between development and operations, " + "promoting a culture of shared responsibility for reliability and " + "performance. Ultimately, great software is not just about " + "algorithms and data structures; it is about people working together " + "to solve meaningful problems." + ), + ], +} + +DURATION_OPTIONS = { + "15 seconds": 15, + "30 seconds": 30, + "60 seconds": 60, + "120 seconds": 120, +} + +TEXT_LENGTH_OPTIONS = list(TEXT_POOLS.keys()) + + +# ====================== +# LOAD / SAVE DATA +# ====================== + +def load_data(): + if os.path.exists(DATA_FILE): + with open(DATA_FILE, "r") as f: + return json.load(f) + return { + "daily_streak": 0, + "last_practice_date": "", + "improvement_streak": 0, + "personal_best_streak": 0, + "best_wpm": 0, + "last_wpm": 0, + } + + +def save_data(data): + with open(DATA_FILE, "w") as f: + json.dump(data, f) + + +class TypingSpeedTest(ctk.CTk): + + def __init__(self): + super().__init__() + + self.title("Typing Speed Test") + self.geometry("750x720") -# List of sample sentences -SENTENCES = [ - "Technology is the most effective way to change the world", - "Innovation is the ability to see change as an opportunity -not a threat", - "Artificial Intelligence is not a threat to creativity; it's a catalyst for innovation.", - "Data is the canvas, and AI is the brush that paints the picture of insights.", - "Artificial Intelligence: where innovation meets computation in the pursuit of a smarter tomorrow." -] - -class TypingSpeedTest: - def __init__(self, root): - self.root = root - self.root.title("Typing Speed Test") - self.root.geometry("600x400") - self.root.resizable(False, False) - - self.start_time = None self.current_sentence = "" - - # Title Label - self.title_label = tk.Label(root, text="Typing Speed Test", font=("Helvetica", 18, "bold")) - self.title_label.pack(pady=10) - - # Instruction Label - self.instruction_label = tk.Label(root, text="Type the exact sentence below as fast as you can:", font=("Helvetica", 14)) - self.instruction_label.pack(pady=10) - - # Sentence Display - self.sentence_label = tk.Label(root, text="", font=("Helvetica", 16), wraplength=550, justify="center") - self.sentence_label.pack(pady=10) - - # Input Textbox - self.input_textbox = tk.Entry(root, font=("Helvetica", 14), width=50) + self.start_time = None + self.test_duration = DEFAULT_DURATION + self.time_left = self.test_duration + self.timer_running = False + self.paused = False + self.countdown = 3 + self.after_id = None + self.live_wpm_after_id = None + + self.data = load_data() + + # ── TITLE ────────────────────────────────────────────── + self.title_label = ctk.CTkLabel( + self, + text="Typing Speed Test", + font=("Helvetica", 30, "bold"), + ) + self.title_label.pack(pady=15) + + # ── SETTINGS FRAME (duration + text length selectors) ─ + self.settings_frame = ctk.CTkFrame(self) + self.settings_frame.pack(pady=(0, 10)) + + # Duration selector + self.duration_label = ctk.CTkLabel( + self.settings_frame, + text="Duration:", + font=("Helvetica", 14), + ) + self.duration_label.grid(row=0, column=0, padx=(10, 5), pady=5) + + self.duration_var = ctk.StringVar(value="60 seconds") + self.duration_menu = ctk.CTkOptionMenu( + self.settings_frame, + variable=self.duration_var, + values=list(DURATION_OPTIONS.keys()), + command=self.on_duration_change, + width=140, + ) + self.duration_menu.grid(row=0, column=1, padx=(0, 20), pady=5) + + # Text-length / difficulty selector + self.text_length_label = ctk.CTkLabel( + self.settings_frame, + text="Text Length:", + font=("Helvetica", 14), + ) + self.text_length_label.grid(row=0, column=2, padx=(10, 5), pady=5) + + self.text_length_var = ctk.StringVar(value="Short Sentence") + self.text_length_menu = ctk.CTkOptionMenu( + self.settings_frame, + variable=self.text_length_var, + values=TEXT_LENGTH_OPTIONS, + width=160, + ) + self.text_length_menu.grid(row=0, column=3, padx=(0, 10), pady=5) + + # ── STATS FRAME (timer + live WPM side by side) ─────── + self.stats_frame = ctk.CTkFrame(self, fg_color="transparent") + self.stats_frame.pack(pady=(5, 0)) + + # TIMER + self.timer_label = ctk.CTkLabel( + self.stats_frame, + text=f"⏱ Time Remaining: {self.test_duration}s", + font=("Helvetica", 18, "bold"), + ) + self.timer_label.grid(row=0, column=0, padx=(20, 40)) + + # LIVE WPM + self.live_wpm_label = ctk.CTkLabel( + self.stats_frame, + text="⌨ Live WPM: 0.00", + font=("Helvetica", 18, "bold"), + text_color="#555555", + ) + self.live_wpm_label.grid(row=0, column=1, padx=(40, 20)) + + # ── SENTENCE FRAME ───────────────────────────────────── + self.sentence_frame = ctk.CTkFrame(self, width=680, height=120) + self.sentence_frame.pack(pady=20) + self.sentence_frame.pack_propagate(False) + + self.sentence_textbox = ctk.CTkTextbox( + self.sentence_frame, + wrap="word", + font=("Helvetica", 20, "bold"), + border_width=0, + fg_color="transparent", + ) + self.sentence_textbox.pack(padx=10, pady=10, fill="both", expand=True) + self.sentence_textbox.insert("1.0", "Press Enter or click Start to begin") + self.sentence_textbox.configure(state="disabled") + + self.sentence_textbox.tag_config("correct", foreground="green") + self.sentence_textbox.tag_config("incorrect", foreground="red") + + # ── INPUT BOX ────────────────────────────────────────── + self.input_textbox = ctk.CTkTextbox( + self, + width=660, + height=120, + font=("Helvetica", 16), + ) self.input_textbox.pack(pady=10) - self.input_textbox.bind("", self.check_result) # Bind Enter key to check result - - # Start Button - self.start_button = tk.Button(root, text="Start Test", font=("Helvetica", 14), command=self.start_test) - self.start_button.pack(pady=20) - - # Result Label - self.result_label = tk.Label(root, text="", font=("Helvetica", 14, "italic"), fg="green") + self.input_textbox.configure(state="disabled") + + self.input_textbox.bind("", self.handle_typing) + # Suppress newline insertion so Enter doesn't add a blank line + self.input_textbox.bind("", lambda e: "break") + + # ── RESULT LABEL ─────────────────────────────────────── + self.result_label = ctk.CTkLabel( + self, text="", font=("Helvetica", 18, "bold") + ) self.result_label.pack(pady=10) - + + # ── STREAK LABEL ─────────────────────────────────────── + self.streak_label = ctk.CTkLabel( + self, text=self.get_streak_text(), font=("Helvetica", 16) + ) + self.streak_label.pack(pady=5) + + # ── BUTTON FRAME ─────────────────────────────────────── + self.button_frame = ctk.CTkFrame(self) + self.button_frame.pack(pady=10) + + self.start_button = ctk.CTkButton( + self.button_frame, + text="Start Test", + command=self.start_test, + ) + self.start_button.grid(row=0, column=0, padx=10) + + self.pause_button = ctk.CTkButton( + self.button_frame, + text="Pause", + command=self.toggle_pause, + state="disabled", + ) + self.pause_button.grid(row=0, column=1, padx=10) + + # Bind Enter to start the test when not typing + self.bind("", self.handle_enter) + + # ====================== + # SETTINGS CALLBACKS + # ====================== + + def on_duration_change(self, choice: str): + """Update the displayed timer immediately when the user picks a new duration.""" + self.test_duration = DURATION_OPTIONS.get(choice, DEFAULT_DURATION) + if not self.timer_running: + self.timer_label.configure( + text=f"⏱ Time Remaining: {self.test_duration}s" + ) + + # ====================== + # HANDLE ENTER KEY + # ====================== + + def handle_enter(self, event=None): + if self.start_button.cget("state") == "normal": + self.start_test() + + # ====================== + # STREAK TEXT + # ====================== + + def get_streak_text(self): + return ( + f"🔥 Daily Streak: {self.data.get('daily_streak', 0)} days\n" + f"📈 Improvement Streak: {self.data.get('improvement_streak', 0)}\n" + f"🏆 Personal Best Streak: {self.data.get('personal_best_streak', 0)}\n" + f"⭐ Best WPM: {self.data.get('best_wpm', 0)}" + ) + + # ====================== + # CALCULATE CURRENT WPM + # ====================== + + def calculate_current_wpm(self): + """Return the current WPM based on correct characters typed so far.""" + if not self.start_time: + return 0.0 + + typed_text = self.input_textbox.get("1.0", "end-1c") + elapsed_time = time.time() - self.start_time + + if elapsed_time <= 0: + return 0.0 + + correct_chars = sum( + 1 + for i, char in enumerate(typed_text) + if i < len(self.current_sentence) and self.current_sentence[i] == char + ) + + wpm = (correct_chars / 5) / (elapsed_time / 60) + return wpm + + # ====================== + # LIVE WPM UPDATE LOOP + # ====================== + + def update_live_wpm(self): + """Scheduled loop that refreshes the live WPM label every second.""" + if not self.timer_running or self.paused: + return + + current_wpm = self.calculate_current_wpm() + self.live_wpm_label.configure(text=f"⌨ Live WPM: {current_wpm:.2f}") + + # Color-code the live WPM for quick visual feedback + if current_wpm >= 60: + self.live_wpm_label.configure(text_color="#00AA00") # Green – fast + elif current_wpm >= 30: + self.live_wpm_label.configure(text_color="#CC8800") # Orange – moderate + else: + self.live_wpm_label.configure(text_color="#CC0000") # Red – slow + + self.live_wpm_after_id = self.after(1000, self.update_live_wpm) + + # ====================== + # START TEST + # ====================== + def start_test(self): - """Starts the typing test.""" - self.result_label.config(text="") - self.current_sentence = random.choice(SENTENCES) - self.sentence_label.config(text=self.current_sentence) - self.input_textbox.delete(0, tk.END) + if self.timer_running: + return + + # Read user-selected duration + self.test_duration = DURATION_OPTIONS.get( + self.duration_var.get(), DEFAULT_DURATION + ) + self.timer_label.configure( + text=f"⏱ Time Remaining: {self.test_duration}s" + ) + + # Reset live WPM display + self.live_wpm_label.configure( + text="⌨ Live WPM: 0.00", text_color="#555555" + ) + + self.result_label.configure(text="") + self.countdown = 3 + self.input_textbox.configure(state="disabled") + self.start_button.configure(state="disabled") + self.pause_button.configure(text="Pause") + + # Disable settings while a test is in progress + self.duration_menu.configure(state="disabled") + self.text_length_menu.configure(state="disabled") + + self.sentence_textbox.configure(state="normal") + self.sentence_textbox.delete("1.0", "end") + self.sentence_textbox.configure(state="disabled") + + self.show_countdown() + + # ====================== + # COUNTDOWN + # ====================== + + def show_countdown(self): + if self.countdown > 0: + self.sentence_textbox.configure(state="normal") + self.sentence_textbox.delete("1.0", "end") + self.sentence_textbox.insert("1.0", f"Starting in {self.countdown}...") + self.sentence_textbox.configure(state="disabled") + self.countdown -= 1 + self.after_id = self.after(1000, self.show_countdown) + else: + self.sentence_textbox.configure(state="normal") + self.sentence_textbox.delete("1.0", "end") + self.sentence_textbox.insert("1.0", "GO!") + self.sentence_textbox.configure(state="disabled") + self.after(800, self.begin_test) + + # ====================== + # BEGIN TEST + # ====================== + + def begin_test(self): + # Pick text from the selected category + selected_length = self.text_length_var.get() + pool = TEXT_POOLS.get(selected_length, TEXT_POOLS["Short Sentence"]) + self.current_sentence = random.choice(pool) + + self.update_sentence_display() + + self.input_textbox.configure(state="normal") + self.input_textbox.delete("1.0", "end") self.input_textbox.focus() + self.start_time = time.time() + self.time_left = self.test_duration + self.timer_running = True + self.paused = False - # update button to restart test - self.start_button.config(text="Restart Test", state=tk.DISABLED) - self.input_textbox.bind("", self.enable_button_after_typing) - - def enable_button_after_typing(self, event=None): - """Enables the button once typing starts.""" - self.start_button.config(state=tk.NORMAL) - self.input_textbox.unbind("") - - def check_result(self, event=None): - """Checks the typing speed and accuracy.""" - if not self.start_time: - messagebox.showwarning("Warning", "Click 'Start Test' first!") + self.pause_button.configure(state="normal") + + self.update_timer() + self.update_live_wpm() # Start the live WPM loop + + # ====================== + # TIMER (wall-clock based to avoid drift) + # ====================== + + def update_timer(self): + if not self.timer_running or self.paused: return - - end_time = time.time() - elapsed_time = end_time - self.start_time - - typed_text = self.input_textbox.get() - if typed_text.strip() == self.current_sentence: - word_count = len(self.current_sentence.split()) - wpm = (word_count / elapsed_time) * 60 - self.result_label.config(text=f"Well done! Your typing speed is {wpm:.2f} WPM.") + + elapsed = int(time.time() - self.start_time) + remaining = max(0, self.test_duration - elapsed) + self.timer_label.configure(text=f"⏱ Time Remaining: {remaining}s") + + if remaining == 0: + self.check_result() else: - self.result_label.config(text="Incorrect typing! Try again.", fg="red") - - # Reset the start time for the next test - self.start_time = None + self.after_id = self.after(500, self.update_timer) + + # ====================== + # PAUSE + # ====================== + + def toggle_pause(self): + if not self.timer_running: + return + + self.paused = not self.paused + if self.paused: + self.pause_button.configure(text="Resume") + self.pause_start = time.time() + if self.after_id: + self.after_cancel(self.after_id) + if self.live_wpm_after_id: + self.after_cancel(self.live_wpm_after_id) + else: + self.pause_button.configure(text="Pause") + # Shift start_time forward by the length of the pause + pause_duration = time.time() - self.pause_start + self.start_time += pause_duration + self.update_timer() + self.update_live_wpm() # Resume the live WPM loop + + # ====================== + # HANDLE TYPING + SOUND + # ====================== + + def handle_typing(self, event): + if not self.timer_running or self.paused: + return + + typed = self.input_textbox.get("1.0", "end-1c") + index = len(typed) + + if event.keysym == "BackSpace": + beep(500, 40) + elif event.keysym not in ( + "Return", "Shift_L", "Shift_R", + "Control_L", "Control_R", "Alt_L", "Alt_R", + ): + # Per-character correct/incorrect sound + if index <= len(self.current_sentence) and index > 0: + expected = self.current_sentence[index - 1] + if typed[-1] == expected: + beep(800, 30) + else: + beep(300, 80) + + self.update_sentence_display() + + # Finish early if sentence completed + if typed.strip() == self.current_sentence.strip(): + self.check_result() + + # ====================== + # UPDATE SENTENCE DISPLAY + # ====================== + + def update_sentence_display(self): + self.sentence_textbox.configure(state="normal") + self.sentence_textbox.delete("1.0", "end") + + typed_text = self.input_textbox.get("1.0", "end-1c") + + for i, char in enumerate(self.current_sentence): + tag = "" + if i < len(typed_text): + tag = "correct" if typed_text[i] == char else "incorrect" + self.sentence_textbox.insert(f"1.{i}", char, tag if tag else ()) + + self.sentence_textbox.configure(state="disabled") + + # ====================== + # UPDATE STREAKS + # ====================== + + def update_streaks(self, wpm): + today = datetime.now().date() + last_date_str = self.data.get("last_practice_date", "") + + if last_date_str: + last_date = datetime.strptime(last_date_str, "%Y-%m-%d").date() + if today == last_date + timedelta(days=1): + self.data["daily_streak"] += 1 + elif today != last_date: + self.data["daily_streak"] = 1 + else: + self.data["daily_streak"] = 1 + + self.data["last_practice_date"] = today.strftime("%Y-%m-%d") + + if wpm > self.data.get("last_wpm", 0): + self.data["improvement_streak"] = ( + self.data.get("improvement_streak", 0) + 1 + ) + else: + self.data["improvement_streak"] = 0 + + self.data["last_wpm"] = wpm + + if wpm > self.data.get("best_wpm", 0): + self.data["best_wpm"] = wpm + self.data["personal_best_streak"] = ( + self.data.get("personal_best_streak", 0) + 1 + ) + + save_data(self.data) + + # ====================== + # RESULT + # ====================== + + def check_result(self): + if self.after_id: + self.after_cancel(self.after_id) + self.after_id = None + + if self.live_wpm_after_id: + self.after_cancel(self.live_wpm_after_id) + self.live_wpm_after_id = None + + if not self.timer_running: + return # Guard against double-fire + + self.timer_running = False + beep(1200, 300) + + # Final WPM calculation + final_wpm = self.calculate_current_wpm() + + # Update live WPM label to show final value + self.live_wpm_label.configure( + text=f"⌨ Final WPM: {final_wpm:.2f}", + text_color="#0055CC", + ) + + self.update_streaks(final_wpm) + + self.result_label.configure(text=f"Typing Speed: {final_wpm:.2f} WPM") + self.streak_label.configure(text=self.get_streak_text()) + + self.input_textbox.configure(state="disabled") + self.pause_button.configure(state="disabled") + self.start_button.configure(state="normal") + + # Re-enable settings selectors + self.duration_menu.configure(state="normal") + self.text_length_menu.configure(state="normal") + + self.sentence_textbox.configure(state="normal") + self.sentence_textbox.delete("1.0", "end") + self.sentence_textbox.insert("1.0", "Press Enter or click Start to begin") + self.sentence_textbox.configure(state="disabled") + -# Run the game if __name__ == "__main__": - root = tk.Tk() - app = TypingSpeedTest(root) - root.mainloop() + app = TypingSpeedTest() + app.mainloop()