Building a Full-Featured Countdown Timer in Python

Introduction

Creating a countdown timer in Python can be a fun and rewarding project, especially when you extend its capabilities beyond a basic terminal application. In this guide, we will walk through the process of building a robust countdown timer that includes a graphical user interface (GUI), support for multiple timers, desktop notifications, and sound alerts. By the end of this tutorial, you will have a comprehensive understanding of how to create a feature-rich countdown timer in Python that can be used in various scenarios, from managing study sessions to timing workouts.

Step 1: Setting Up the Basic Countdown Timer

We begin by creating a simple countdown timer that works in the terminal. This timer will take the number of seconds as input and will count down to zero, displaying the time in a “HH:MM” format.

import time

def countdown_timer(seconds):
    while seconds:
        hours, remainder = divmod(seconds, 3600)
        mins, secs = divmod(remainder, 60)
        time_format = '{:02d}:{:02d}:{:02d}'.format(hours, mins, secs)
        print(time_format, end='\r')
        time.sleep(1)
        seconds -= 1
    
    print("Time's up!")

Explanation:

  • We use divmod to break down the total seconds into hours, minutes, and seconds.
  • The print statement with end='\r' allows us to overwrite the previous output, creating an updating display.

Step 2: Enhancing the Timer with Pause/Resume and Sound Notifications

Next, we introduce the ability to pause and resume the timer, as well as play a sound when the timer finishes. We also include logging to keep track of the timer’s activity.

import time
import threading
import os
from datetime import datetime

class CountdownTimer:
    def __init__(self, name, seconds, end_message="Time's up!", sound=False):
        self.name = name
        self.total_seconds = seconds
        self.seconds = seconds
        self.end_message = end_message
        self.sound = sound
        self.running = False
        self.paused = False

    def start(self):
        self.running = True
        self.log(f"Timer '{self.name}' started.")
        self.run()

    def run(self):
        while self.seconds and self.running:
            if not self.paused:
                hours, remainder = divmod(self.seconds, 3600)
                mins, secs = divmod(remainder, 60)
                time_format = '{:02d}:{:02d}:{:02d}'.format(hours, mins, secs)
                print(f"{self.name}: {time_format}", end='\r')
                time.sleep(1)
                self.seconds -= 1
            else:
                time.sleep(1)
        
        if self.running and not self.paused:
            self.finish()

    def pause(self):
        self.paused = True
        self.log(f"Timer '{self.name}' paused.")

    def resume(self):
        self.paused = False
        self.log(f"Timer '{self.name}' resumed.")
        self.run()

    def stop(self):
        self.running = False
        self.log(f"Timer '{self.name}' stopped.")
        print(f"\nTimer '{self.name}' stopped.")

    def finish(self):
        print(f"\n{self.name}: {self.end_message}")
        self.log(f"Timer '{self.name}' finished.")
        if self.sound:
            self.play_sound()

    def play_sound(self):
        if os.name == 'posix':  # macOS, Linux
            os.system('afplay /System/Library/Sounds/Glass.aiff')
        elif os.name == 'nt':  # Windows
            import winsound
            winsound.Beep(1000, 500)

    def log(self, message):
        with open("timer_log.txt", "a") as log_file:
            log_file.write(f"{datetime.now()}: {message}\n")

Explanation:

  • Pause/Resume: The pause and resume methods allow the timer to stop and start without resetting the countdown.
  • Sound Notification: The play_sound method plays a sound when the timer finishes, using platform-specific commands.
  • Logging: The log method writes events (start, pause, resume, stop, finish) to a timer_log.txt file for record-keeping.

Step 3: Adding a Graphical User Interface (GUI) with Tkinter

To make our timer more user-friendly, we can add a GUI using Tkinter, Python’s standard GUI library. The GUI will allow users to input the timer details (name, duration, and end message) and start the timer with a click of a button.

import tkinter as tk
from tkinter import messagebox

class TimerApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Countdown Timer")
        self.root.geometry("400x300")
        self.root.configure(bg='#2c2c2c')  # Dark mode
        self.timers = []

        self.create_widgets()

    def create_widgets(self):
        tk.Label(self.root, text="Timer Name:", bg='#2c2c2c', fg='white').pack(pady=10)
        self.name_entry = tk.Entry(self.root)
        self.name_entry.pack()

        tk.Label(self.root, text="Time (seconds):", bg='#2c2c2c', fg='white').pack(pady=10)
        self.time_entry = tk.Entry(self.root)
        self.time_entry.pack()

        tk.Label(self.root, text="End Message:", bg='#2c2c2c', fg='white').pack(pady=10)
        self.message_entry = tk.Entry(self.root)
        self.message_entry.pack()

        self.sound_var = tk.BooleanVar()
        tk.Checkbutton(self.root, text="Sound", variable=self.sound_var, bg='#2c2c2c', fg='white').pack(pady=5)

        tk.Button(self.root, text="Start Timer", command=self.start_timer).pack(pady=10)
        tk.Button(self.root, text="Quit", command=self.root.quit).pack(pady=10)

    def start_timer(self):
        try:
            name = self.name_entry.get()
            seconds = int(self.time_entry.get())
            message = self.message_entry.get() or "Time's up!"
            sound = self.sound_var.get()

            timer = CountdownTimer(name, seconds, message, sound)
            self.timers.append(timer)

            timer_thread = threading.Thread(target=timer.start)
            timer_thread.start()

            messagebox.showinfo("Timer Started", f"Timer '{name}' started for {seconds} seconds.")

        except ValueError:
            messagebox.showerror("Invalid Input", "Please enter a valid number for the time in seconds.")
 

Explanation:

  • Tkinter Widgets: We use labels, entry fields, and buttons to create an interactive interface for the user.
  • Dark Mode: The GUI is styled with a dark background for a modern look.
  • Event Handling: The start_timer method is triggered when the user clicks the “Start Timer” button, launching a new timer in a separate thread.

Step 4: Implementing Desktop Notifications and Command-Line Arguments

To make our timer even more versatile, we add desktop notifications using the plyer library and allow the timer to be configured via command-line arguments using the argparse library.

from plyer import notification
import argparse

def parse_arguments():
    parser = argparse.ArgumentParser(description="Command-line Countdown Timer")
    parser.add_argument("--name", type=str, default="Timer", help="Name of the timer")
    parser.add_argument("--time", type=int, required=True, help="Time in seconds")
    parser.add_argument("--message", type=str, default="Time's up!", help="End message")
    parser.add_argument("--sound", action="store_true", help="Play sound when timer ends")
    
    return parser.parse_args()

def main():
    args = parse_arguments()

    if args.time:
        timer = CountdownTimer(args.name, args.time, args.message, args.sound)
        timer_thread = threading.Thread(target=timer.start)
        timer_thread.start()

    root = tk.Tk()
    app = TimerApp(root)
    root.mainloop()

if __name__ == "__main__":
    main()

Explanation:

  • Command-Line Arguments: Users can now start a timer from the terminal by specifying the name, duration, end message, and whether a sound should be played.
  • Desktop Notifications: The plyer library is used to send a desktop notification when the timer ends, ensuring the user is alerted even if the GUI is minimized.

Final Product: Full Python Script

Here’s the complete script with all the features we’ve discussed:

import time
import threading
import os
from datetime import datetime
import tkinter as tk
from tkinter import messagebox
from plyer import notification
import argparse

class CountdownTimer:
    def __init__(self, name, seconds, end_message="Time's up!", sound=False):
        self.name = name
        self.total_seconds = seconds
        self.seconds = seconds
        self.end_message = end_message
        self.sound = sound
        self.running = False
        self.paused = False

    def start(self):
        self.running = True
        self.log(f"Timer '{self.name}' started.")
        self.run()

    def run(self):
        while self.seconds and self.running:
            if not self.paused:
                hours, remainder = divmod(self.seconds, 3600)
                mins, secs = divmod(remainder, 60)
                time_format = '{:02d}:{:02d}:{:02d}'.format(hours, mins, secs)
                print(f"{self.name}: {time_format}", end='\r')
                time.sleep(1)
                self.seconds -= 1
            else:
                time.sleep(1)
        
        if self.running and not self.paused:
            self.finish()

    def pause(self):
        self.paused = True
        self.log(f"Timer '{self.name}' paused.")

    def resume(self):
        self.paused = False
        self.log(f"Timer '{self.name}' resumed.")
        self.run()

    def stop(self):
        self.running = False
        self.log(f"Timer '{self.name}' stopped.")
        print(f"\nTimer '{self.name}' stopped.")

    def finish(self):
        print(f"\n{self.name}: {self.end_message}")
        self.log(f"Timer '{self.name}' finished.")
        if self.sound:
            self.play_sound()
        notification.notify(
            title=f"Timer '{self.name}' Finished",
            message=self.end_message,
            timeout=5
        )

    def play_sound(self):
        if os.name == 'posix':  # macOS, Linux
            os.system('afplay /System/Library/Sounds/Glass.aiff')
        elif os.name == 'nt':  # Windows
            import winsound
            winsound.Beep(1000, 500)

    def log(self, message):
        with open("timer_log.txt", "a") as log_file:
            log_file.write(f"{datetime.now()}: {message}\n")

class TimerApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Countdown Timer")
        self.root.geometry("400x300")
        self.root.configure(bg='#2c2c2c')  # Dark mode
        self.timers = []

        self.create_widgets()

    def create_widgets(self):
        tk.Label(self.root, text="Timer Name:", bg='#2c2c2c', fg='white').pack(pady=10)
        self.name_entry = tk.Entry(self.root)
        self.name_entry.pack()

        tk.Label(self.root, text="Time (seconds):", bg='#2c2c2c', fg='white').pack(pady=10)
        self.time_entry = tk.Entry(self.root)
        self.time_entry.pack()

        tk.Label(self.root, text="End Message:", bg='#2c2c2c', fg='white').pack(pady=10)
        self.message_entry = tk.Entry(self.root)
        self.message_entry.pack()

        self.sound_var = tk.BooleanVar()
        tk.Checkbutton(self.root, text="Sound", variable=self.sound_var, bg='#2c2c2c', fg='white').pack(pady=5)

        tk.Button(self.root, text="Start Timer", command=self.start_timer).pack(pady=10)
        tk.Button(self.root, text="Quit", command=self.root.quit).pack(pady=10)

    def start_timer(self):
        try:
            name = self.name_entry.get()
            seconds = int(self.time_entry.get())
            message = self.message_entry.get() or "Time's up!"
            sound = self.sound_var.get()

            timer = CountdownTimer(name, seconds, message, sound)
            self.timers.append(timer)

            timer_thread = threading.Thread(target=timer.start)
            timer_thread.start()

            messagebox.showinfo("Timer Started", f"Timer '{name}' started for {seconds} seconds.")

        except ValueError:
            messagebox.showerror("Invalid Input", "Please enter a valid number for the time in seconds.")

def parse_arguments():
    parser = argparse.ArgumentParser(description="Command-line Countdown Timer")
    parser.add_argument("--name", type=str, default="Timer", help="Name of the timer")
    parser.add_argument("--time", type=int, required=True, help="Time in seconds")
    parser.add_argument("--message", type=str, default="Time's up!", help="End message")
    parser.add_argument("--sound", action="store_true", help="Play sound when timer ends")
    
    return parser.parse_args()

def main():
    args = parse_arguments()

    if args.time:
        timer = CountdownTimer(args.name, args.time, args.message, args.sound)
        timer_thread = threading.Thread(target=timer.start)
        timer_thread.start()

    root = tk.Tk()
    app = TimerApp(root)
    root.mainloop()

if __name__ == "__main__":
    main()

Conclusion

Building a countdown timer in Python is an excellent way to explore different programming concepts, such as multi-threading, GUI development, and user interaction. By following the steps in this guide, you’ve created a versatile timer that can be used in various situations, from time management to productivity enhancement. You now have a powerful tool that can be expanded further or integrated into larger projects. Happy coding!

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *