From 833d9e1b7164e7ed175b868b666f5d7c368b7ea2 Mon Sep 17 00:00:00 2001 From: zyndor1548 Date: Thu, 26 Feb 2026 18:25:06 +0530 Subject: [PATCH 1/4] added requirements.txt --- requirements.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..420a1f6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +customtkinter==5.2.2 +darkdetect==0.8.0 +packaging==26.0 From 8307cbd06ecc9c0608684ca1f2b4f17769eb5d3d Mon Sep 17 00:00:00 2001 From: zyndor1548 Date: Thu, 26 Feb 2026 18:47:30 +0530 Subject: [PATCH 2/4] added random text api --- typing_speed_test.py | 69 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 16 deletions(-) diff --git a/typing_speed_test.py b/typing_speed_test.py index 6c0eb48..7fc0d6b 100644 --- a/typing_speed_test.py +++ b/typing_speed_test.py @@ -1,19 +1,25 @@ import customtkinter as ctk import random import time +import threading +import urllib.request +import json # --- Theme Configuration --- ctk.set_appearance_mode("light") ctk.set_default_color_theme("blue") -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.", +# Fallback sentences used when the API is unreachable +FALLBACK_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 is 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." ] +QUOTE_API_URL = "http://api.quotable.io/random" + class TypingSpeedTest(ctk.CTk): def __init__(self): super().__init__() @@ -82,28 +88,60 @@ def __init__(self): self.result_label = ctk.CTkLabel(self, text="", font=("Helvetica", 18, "italic")) self.result_label.pack(pady=10) - # Available sentences - self.available_sentences = SENTENCES.copy() - random.shuffle(self.available_sentences) + # Fallback pool (shuffled), used only when API is unavailable + self.fallback_pool = FALLBACK_SENTENCES.copy() + random.shuffle(self.fallback_pool) + + def fetch_quote(self): + """Fetch a random quote from the API. Returns the quote string or None on failure.""" + try: + with urllib.request.urlopen(QUOTE_API_URL, timeout=5) as response: + data = json.loads(response.read().decode()) + return data.get("content", "").strip() + except Exception: + return None def start_test(self): # --- Reset everything for a new test --- self.result_label.configure(text="") self.feedback_label.configure(text="") - # Clear input field properly + # Clear input field and disable it while loading self.input_textbox.configure(state="normal") self.input_textbox.delete("1.0", "end") - self.input_textbox.focus() - self.input_textbox.update() + self.input_textbox.configure(state="disabled") + + # Show loading state + self.sentence_label.configure(text="Loading quote...", text_color="gray") + self.start_button.configure(state="disabled") + self.result_button.configure(state="disabled", fg_color="gray") - # Pick new sentence - if not self.available_sentences: - self.available_sentences = SENTENCES.copy() - random.shuffle(self.available_sentences) - self.current_sentence = self.available_sentences.pop() + # Fetch the quote in a background thread to keep UI responsive + threading.Thread(target=self._load_quote_and_begin, daemon=True).start() + + def _load_quote_and_begin(self): + """Background thread: fetch quote, then schedule UI update on main thread.""" + quote = self.fetch_quote() + if not quote: + # Fallback to built-in sentences + if not self.fallback_pool: + self.fallback_pool = FALLBACK_SENTENCES.copy() + random.shuffle(self.fallback_pool) + quote = self.fallback_pool.pop() + # Schedule the rest of the setup back on the main thread + self.after(0, lambda q=quote: self._begin_test(q)) + + def _begin_test(self, sentence): + """Called on the main thread once the quote is ready.""" + self.current_sentence = sentence self.sentence_label.configure(text=self.current_sentence, text_color="white") + # Re-enable input and focus + self.input_textbox.configure(state="normal") + self.input_textbox.delete("1.0", "end") + self.input_textbox.focus() + self.input_textbox.update() + # Reset timer self.time_left = 60 self.timer_running = True @@ -111,7 +149,6 @@ def start_test(self): self.start_time = time.time() # Update buttons - self.start_button.configure(state="disabled") self.result_button.configure(state="normal", fg_color="#3B8ED0") # Start timer From 99b7ac5f53e1c533398ca85dac7b1eb2acf95b62 Mon Sep 17 00:00:00 2001 From: zyndor1548 Date: Thu, 26 Feb 2026 19:04:10 +0530 Subject: [PATCH 3/4] added score card --- typing_speed_test.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/typing_speed_test.py b/typing_speed_test.py index 7fc0d6b..96d60d7 100644 --- a/typing_speed_test.py +++ b/typing_speed_test.py @@ -32,6 +32,7 @@ def __init__(self): self.current_sentence = "" self.time_left = 60 self.timer_running = False + self.scores = [] # --- UI LAYOUT --- self.title_label = ctk.CTkLabel(self, text="Typing Speed Test", font=("Helvetica", 28, "bold")) @@ -85,6 +86,17 @@ def __init__(self): ) self.result_button.grid(row=0, column=1, padx=10) + self.scoreboard_button = ctk.CTkButton( + self.button_frame, + text="🏆 Scoreboard", + font=("Helvetica", 14, "bold"), + command=self.show_scoreboard, + height=40, + fg_color="#5A5A8A", + hover_color="#3D3D6B" + ) + self.scoreboard_button.grid(row=0, column=2, padx=10) + self.result_label = ctk.CTkLabel(self, text="", font=("Helvetica", 18, "italic")) self.result_label.pack(pady=10) @@ -210,6 +222,35 @@ def check_result(self, event=None): self.result_button.configure(state="disabled", fg_color="gray") self.start_time = None + # Update in-memory scoreboard (top 5) + self.scores.append(round(wpm, 2)) + self.scores.sort(reverse=True) + self.scores = self.scores[:5] + + def show_scoreboard(self): + win = ctk.CTkToplevel(self) + win.title("Scoreboard") + win.geometry("300x320") + win.resizable(False, False) + win.grab_set() + + ctk.CTkLabel(win, text="🏆 Top 5 Scores", font=("Helvetica", 20, "bold")).pack(pady=16) + + medals = ["🥇", "🥈", "🥉", "4️⃣", "5️⃣"] + + if not self.scores: + ctk.CTkLabel(win, text="No scores yet.\nComplete a test to see results!", + font=("Helvetica", 14), text_color="gray", justify="center").pack(pady=20) + else: + for i, score in enumerate(self.scores): + ctk.CTkLabel( + win, + text=f"{medals[i]} {score:.2f} WPM", + font=("Helvetica", 16) + ).pack(pady=6) + + ctk.CTkButton(win, text="Close", command=win.destroy, height=36).pack(pady=16) + if __name__ == "__main__": app = TypingSpeedTest() app.mainloop() \ No newline at end of file From 9d506c328c4961ed6b5b51b7c5f7b9c720338bb5 Mon Sep 17 00:00:00 2001 From: zyndor1548 Date: Thu, 26 Feb 2026 19:12:50 +0530 Subject: [PATCH 4/4] added 15s mode --- typing_speed_test.py | 52 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/typing_speed_test.py b/typing_speed_test.py index 96d60d7..2ac9fed 100644 --- a/typing_speed_test.py +++ b/typing_speed_test.py @@ -33,6 +33,7 @@ def __init__(self): self.time_left = 60 self.timer_running = False self.scores = [] + self.mode = "60s" # "60s" or "15s" # --- UI LAYOUT --- self.title_label = ctk.CTkLabel(self, text="Typing Speed Test", font=("Helvetica", 28, "bold")) @@ -62,9 +63,37 @@ def __init__(self): self.feedback_label = ctk.CTkLabel(self, text="", font=("Helvetica", 12, "bold")) self.feedback_label.pack() + # Mode Selector + self.mode_frame = ctk.CTkFrame(self, fg_color="transparent") + self.mode_frame.pack(pady=(10, 0)) + + self.btn_60s = ctk.CTkButton( + self.mode_frame, + text="⏱ 60s Mode", + font=("Helvetica", 13, "bold"), + command=lambda: self.set_mode("60s"), + height=34, + width=130, + fg_color="#3B8ED0", + hover_color="#2775b3" + ) + self.btn_60s.grid(row=0, column=0, padx=6) + + self.btn_15s = ctk.CTkButton( + self.mode_frame, + text="⚡ 15s Mode", + font=("Helvetica", 13, "bold"), + command=lambda: self.set_mode("15s"), + height=34, + width=130, + fg_color="gray", + hover_color="#555555" + ) + self.btn_15s.grid(row=0, column=1, padx=6) + # Button Container self.button_frame = ctk.CTkFrame(self, fg_color="transparent") - self.button_frame.pack(pady=20) + self.button_frame.pack(pady=10) self.start_button = ctk.CTkButton( self.button_frame, @@ -104,6 +133,17 @@ def __init__(self): self.fallback_pool = FALLBACK_SENTENCES.copy() random.shuffle(self.fallback_pool) + def set_mode(self, mode): + """Switch between '60s' and '15s' modes (only when not mid-test).""" + if self.timer_running: + return # Ignore mode switch during an active test + self.mode = mode + duration = 60 if mode == "60s" else 15 + self.timer_label.configure(text=f"Time Remaining: {duration}s", text_color="#3B8ED0") + # Highlight active mode button + self.btn_60s.configure(fg_color="#3B8ED0" if mode == "60s" else "gray") + self.btn_15s.configure(fg_color="#3B8ED0" if mode == "15s" else "gray") + def fetch_quote(self): """Fetch a random quote from the API. Returns the quote string or None on failure.""" try: @@ -154,10 +194,11 @@ def _begin_test(self, sentence): self.input_textbox.focus() self.input_textbox.update() - # Reset timer - self.time_left = 60 + # Reset timer based on selected mode + duration = 60 if self.mode == "60s" else 15 + self.time_left = duration self.timer_running = True - self.timer_label.configure(text="Time Remaining: 60s", text_color="#3B8ED0") + self.timer_label.configure(text=f"Time Remaining: {duration}s", text_color="#3B8ED0") self.start_time = time.time() # Update buttons @@ -170,7 +211,8 @@ def update_timer(self): if self.time_left > 0 and self.timer_running: self.time_left -= 1 self.timer_label.configure(text=f"Time Remaining: {self.time_left}s") - if self.time_left <= 10: + red_zone = 5 if self.mode == "15s" else 10 + if self.time_left <= red_zone: self.timer_label.configure(text_color="#FF4C4C") self.after(1000, self.update_timer) elif self.time_left == 0: