Python from linux online to Windows offline
Since your online machine is Linux and your offline machine is Windows, you must be extremely careful. By default, a Linux machine will try to download Linux wheels (manylinux). You must force pip to ignore the Linux host and pull Windows files instead.
Here is the summarized workflow to get this right:
Phase 1: On the Linux (Online) Machine
You will create a bundle containing the Windows-specific binaries.
- Create a directory for the bundle:Bash
mkdir pyinstaller_bundle && cd pyinstaller_bundle
- Download the packages using "cross-platform" flags. This tells the Linux
pipto grab Windows files for Python 3.8:Bashpip download \
--platform win_amd64 \
--python-version 3.8 \
--only-binary=:all: \
pyinstaller- Crucial: Check the folder now. If you don't see
pywin32-ctypesoraltgraphin the folder,pipskipped them because it's on Linux.
- Crucial: Check the folder now. If you don't see
- Manual fallback (if files are missing): If any dependencies are missing, download them explicitly into the same folder:Bash
pip download pywin32-ctypes altgraph pefile pyinstaller-hooks-contrib \
--platform win_amd64 \
--python-version 3.8 \
--only-binary=:all:
Phase 2: Transfer
- Compress the folder (optional, but easier for USB transfer):Bash
tar -czvf pyinstaller_bundle.tar.gz pyinstaller_bundle/
- Move the folder/archive to your Offline Windows Machine via USB.
Phase 3: On the Windows (Offline) Machine
- Extract the files so you have a folder full of
.whlfiles (e.g., inC:\pyinstaller_bundle). - Open PowerShell/CMD inside that folder.
- Run the offline install: Use the
--no-indexflag to prevent it from trying to connect to the internet (and failing with that "Connection Refused" error again).PowerShellpip install --no-index --find-links . pyinstaller
Critical Precautions Summary
Precaution | Why? |
Check for | Ensure none of the filenames contain |
Check for | Ensure the wheels match your Python 3.8 version. |
Use | This prevents |
Verify | This is the most common "hidden" Windows dependency that Linux downloads often miss. |
By following this, you bypass the "target machine actively refused it" error because pip never tries to talk to the network—it finds everything it needs right in that folder.
How to locate app folder for particular app.
Locate the "Site-Packages" Folder
If you have already installed a package and want to find where its files live (which originated from a wheel), you can use the __file__ attribute in Python.
Python
import numpy # Replace with your package name
print(numpy.__file__)
Note: This points to the installed .py or .so files. The original .whl file is usually deleted by pip after a successful installation unless you specifically told it to save it.
Download Wheels to a Specific Path
If your goal is to get the .whl files for a project (perhaps to move them to an offline server), you should use the download command. This ensures you have the physical file in a location of your choice.
Bash
# Downloads the wheel for requests into the current folder
pip download requests -d ./my_wheels
"pip is not recognized as an internal or external command"
This means Python isn't in your system PATH. You can fix this by using the Python launcher instead:
PowerShell
python -m pip install package_name.whl
Architecture Mismatch (32-bit vs 64-bit)
If you get an error saying the wheel "is not a supported wheel on this platform," check two things:
- Python Version: Ensure the wheel version (e.g.,
cp310for Python 3.10) matches your installed version (python --version). - System Architecture: Ensure you aren't trying to install a
win_amd64wheel on a 32-bit Python installation.
Installing Dependencies
When you install a local .whl file, pip will still try to connect to the internet to find any dependencies that the package requires. If you are offline, the installation will fail unless you have those dependency wheels locally as well.
Summary of Commands
| Scenario | Command |
| Standard Install | pip install filename.whl |
| Install via Python Launcher | python -m pip install filename.whl |
| Force Reinstall | pip install --force-reinstall filename.whl |
| Install to User Directory | pip install --user filename.whl |
The "No-Network" Installation Command
Run the command like this:
PowerShell
pip install --no-index --find-links . your_file_name.whl
Breakdown of what this does:
--no-index: Tells pip "Do not look at the internet/PyPI."--find-links .: Tells pip "Look in the current folder (represented by the dot) if you need to find any dependencies."
Why it might still fail (The Dependency Problem)
Most .whl files have "friends" (dependencies). For example, if you are installing pandas, it will demand numpy.
- The Error: If you see
Could not find a version that satisfies the requirement [package], it means your.whlfile needs another file that isn't in that folder. - The Solution: You must go to an online machine, download all the required wheels at once, and move them all to the offline machine.
On the Online machine:
Bash
pip download -d ./packages_to_move <package_name>
This downloads the main package AND every single dependency it needs.
On the Offline machine:
- Move that entire folder to your offline PC.
- Run:
PowerShell
pip install --no-index --find-links ./packages_to_move <package_name>
Moving a Python library like pygame from an online Linux machine to an offline Windows machine requires downloading the Wheels (.whl files). Since Windows and Linux use different compiled binaries, you cannot simply copy the installed folders from Linux; you must specifically download the Windows-compatible versions.
Here is the step-by-step process.
Phase 1: On the Online Linux Machine
You will use pip to download the packages without installing them. You must tell pip to look for Windows binaries instead of Linux ones.
- Create a dedicated folder for the files:Bash
mkdir pygame_offline && cd pygame_offline
- Identify your Windows Python Version:You need to know the Python version and architecture (64-bit or 32-bit) on the offline Windows machine (e.g., Python 3.11, 64-bit).
- Download Pygame and dependencies:Run the following command. Replace
3.11with your specific Windows Python version (e.g.,3.10,3.12):Bashpip download pygame \
--only-binary=:all: \
--platform win_amd64 \
--python-version 3.11 \
-d .--platform win_amd64: Targets 64-bit Windows. (Usewin32if the Windows machine is 32-bit).--only-binary=:all:: Ensures you get.whlfiles (binaries) rather than source code that requires a compiler.-d .: Saves the files in the current directory.
- Verify the files:You should see at least one
.whlfile in the folder (usuallypygame-xxx-cp311-cp311-win_amd64.whl).
Phase 2: Transfer
- Copy the entire
pygame_offlinefolder onto a USB drive. - Plug the USB drive into the Offline Windows Machine.
Phase 3: On the Offline Windows Machine
- Open Command Prompt (cmd):Navigate to the folder on the USB drive containing the
.whlfiles.DOScd D:\pygame_offline
- Install the package:Use
pipto install from the local directory. Use a wildcard*to catch the file name:DOSpip install --no-index --find-links . pygame
--no-index: Tells pip not to look at the official online repository (PyPI).--find-links .: Tells pip to look for the installer files in the current folder.
Troubleshooting & Dependencies
- Standard Dependencies:
pygameis generally "self-contained" in its Wheel file for Windows, meaning it includes its own SDL, image, and mixer DLLs. You likely won't need other packages unless your specific code uses something likenumpy. - PIP itself: If the offline Windows machine doesn't even have
pipinstalled, you will need to download theget-pip.pyscript on the Linux machine, move it over, and runpython get-pip.pyfirst. - Version Mismatch: If you get an error saying "not a supported wheel on this platform," double-check that the version you downloaded (e.g.,
cp311) matches your Windows Python version (python --version).
convert to EXE file
python -m pip install pyinstaller
python -m pip show pyinstaller
python -m PyInstaller --noconsole --onefile --collect-all pygame MTKAlarm.py
PyInstaller doesn't just "save as" an EXE; it actually builds a small virtual environment to bundle your code.
Here is a breakdown of what those items are and what you should do with them:
1. The dist Folder (The Goal)
- What it is: Short for "Distribution."
- What to do: Open this folder. Inside is your
MTKAlarm.exe. - The Offline Machine: This is the only thing you need to take to your offline machine. You can delete everything else once you have this file.
2. The build Folder (The Workspace)
- What it is: This is PyInstaller’s "temporary workshop." It’s where it unzips libraries, collects metadata, and prepares the files before stuffing them into the final EXE.
- What to do: You can safely delete this folder now. It takes up space and isn't needed for the app to run.
3. The MTKAlarm.spec File (The Blueprint)
- What it is: This is a configuration file. It stores the settings you used (like
--onefileand--noconsole). - Why it’s useful: If you ever want to update your code and rebuild the EXE, you can just run
python -m PyInstaller MTKAlarm.specinstead of typing out that long command again. It ensures the next EXE is built with the exact same settings.
Final Checklist for your Offline Machine:
- Test it locally first: Double-click the EXE in the
distfolder on your current computer.- Does the window resize correctly?
- Does the alarm sound play?
- Audio Files: Since you are using a "Browse" button in your code, make sure the offline machine actually has an
.mp3or.wavfile on its hard drive for you to select. - USB Transfer: Copy only the
MTKAlarm.exeto your USB.
One minor "Heads Up": The first time you run a PyInstaller EXE, it might take 5–10 seconds to open. This is because it has to "unpack" the Python interpreter and the pygame library into a temporary folder in the background. After the first few seconds, it will run at normal speed!
MOdern window
Since we are moving to a modern Windows look, we’ll ditch the old-school Tkinter "Windows 95" aesthetic and use a library like CustomTkinter. It provides rounded buttons, a dark/light mode, and a much sleeker feel.
⚠️ Note on PyInstaller
To convert this modern version to an .exe, you will need to install the new library first: pip install customtkinter
import tkinter as tk
import customtkinter as ctk
from tkinter import filedialog, messagebox
import pygame
import os
# Set the appearance and color theme
ctk.set_appearance_mode("dark") # "dark" or "light"
ctk.set_default_color_theme("blue")
class ModernRepeatTimer:
def __init__(self, root):
self.root = root
self.root.title("MTK Alarm Pro")
# --- DEFAULTS & WINDOW SETTINGS ---
self.CONFIG_SIZE = "500x700"
self.RUNNING_SIZE = "350x380"
self.DEF_DELAY = {"h": "00", "m": "00", "s": "00"}
self.DEF_DUR = {"h": "00", "m": "00", "s": "00"}
self.DEF_REPS = "1"
# Logical Variables
self.mode = tk.StringVar(value="Countdown")
self.is_running = False
self.is_paused = False
self.in_delay_phase = False
self.timer_after_id = None
self.current_rep = 1
pygame.mixer.init()
self.setup_ui()
self.root.geometry(self.CONFIG_SIZE)
def setup_ui(self):
# Header
self.title_lbl = ctk.CTkLabel(self.root, text="MTK ALARM PRO", font=("Roboto", 24, "bold"))
self.title_lbl.pack(pady=15)
# --- SETTINGS PANEL ---
self.settings_frame = ctk.CTkFrame(self.root)
self.settings_frame.pack(padx=20, pady=10, fill="both", expand=True)
# Mode Selection
ctk.CTkLabel(self.settings_frame, text="Select Mode", font=("Roboto", 12, "bold")).pack(pady=(10, 0))
self.mode_menu = ctk.CTkOptionMenu(self.settings_frame, values=["Countdown", "Countup"], variable=self.mode)
self.mode_menu.pack(pady=5)
# Start Delay (Step 2)
ctk.CTkLabel(self.settings_frame, text="Start Delay (HH:MM:SS)", font=("Roboto", 12, "bold")).pack(pady=5)
delay_f = ctk.CTkFrame(self.settings_frame, fg_color="transparent")
delay_f.pack()
self.d_h = self.create_entry(delay_f, self.DEF_DELAY["h"]); self.d_h.pack(side="left", padx=2)
self.d_m = self.create_entry(delay_f, self.DEF_DELAY["m"]); self.d_m.pack(side="left", padx=2)
self.d_s = self.create_entry(delay_f, self.DEF_DELAY["s"]); self.d_s.pack(side="left", padx=2)
# Duration (Step 3)
ctk.CTkLabel(self.settings_frame, text="Timer Duration (HH:MM:SS)", font=("Roboto", 12, "bold")).pack(pady=5)
dur_f = ctk.CTkFrame(self.settings_frame, fg_color="transparent")
dur_f.pack()
self.h_entry = self.create_entry(dur_f, self.DEF_DUR["h"]); self.h_entry.pack(side="left", padx=2)
self.m_entry = self.create_entry(dur_f, self.DEF_DUR["m"]); self.m_entry.pack(side="left", padx=2)
self.s_entry = self.create_entry(dur_f, self.DEF_DUR["s"]); self.s_entry.pack(side="left", padx=2)
# Quick Select Buttons
quick_f = ctk.CTkFrame(self.settings_frame, fg_color="transparent")
quick_f.pack(pady=5)
for lbl, sec in [("10s", 10), ("30s", 30), ("1m", 60), ("5m", 300)]:
ctk.CTkButton(quick_f, text=lbl, width=50, height=24, command=lambda s=sec: self.set_duration(s)).pack(side="left", padx=2)
# Repetitions
ctk.CTkLabel(self.settings_frame, text="Repetitions", font=("Roboto", 12, "bold")).pack(pady=5)
self.rep_entry = ctk.CTkEntry(self.settings_frame, width=60, justify="center")
self.rep_entry.insert(0, self.DEF_REPS)
self.rep_entry.pack()
# Sound
ctk.CTkButton(self.settings_frame, text="Select Alarm Sound", fg_color="gray30", command=self.browse_sound).pack(pady=15)
self.sound_path = ""
# --- ACTIVE DISPLAY ---
self.display_frame = ctk.CTkFrame(self.root, fg_color="transparent")
self.display_frame.pack(pady=10, fill="both")
self.status_lbl = ctk.CTkLabel(self.display_frame, text="READY", text_color="#3b8ed0", font=("Roboto", 14, "bold"))
self.status_lbl.pack()
self.time_lbl = ctk.CTkLabel(self.display_frame, text="00:00:00", font=("Roboto", 54, "bold"))
self.time_lbl.pack(pady=10)
self.rep_lbl = ctk.CTkLabel(self.display_frame, text="Rep: -", font=("Roboto", 12))
self.rep_lbl.pack()
# --- CONTROLS ---
self.btn_frame = ctk.CTkFrame(self.root, fg_color="transparent")
self.btn_frame.pack(side="bottom", pady=20)
self.start_btn = ctk.CTkButton(self.btn_frame, text="START", fg_color="#2fa572", hover_color="#1e714e", width=100, command=self.start_timer)
self.start_btn.grid(row=0, column=0, padx=5)
self.pause_btn = ctk.CTkButton(self.btn_frame, text="PAUSE", width=100, command=self.toggle_pause)
self.pause_btn.grid(row=0, column=1, padx=5)
self.reset_btn = ctk.CTkButton(self.btn_frame, text="RESET", fg_color="#dc3545", hover_color="#a71d2a", width=100, command=self.reset_timer)
self.reset_btn.grid(row=1, column=0, columnspan=2, pady=10, sticky="ew")
def create_entry(self, parent, default):
e = ctk.CTkEntry(parent, width=45, justify="center")
e.insert(0, default)
return e
def set_duration(self, total_sec):
h, m, s = total_sec // 3600, (total_sec % 3600) // 60, total_sec % 60
for entry, val in zip([self.h_entry, self.m_entry, self.s_entry], [h, m, s]):
entry.delete(0, tk.END); entry.insert(0, f"{val:02d}")
def browse_sound(self):
file = filedialog.askopenfilename(filetypes=[("Audio", "*.mp3 *.wav")])
if file: self.sound_path = file
def reset_timer(self):
self.is_running = False
if self.timer_after_id: self.root.after_cancel(self.timer_after_id)
self.time_lbl.configure(text="00:00:00")
self.status_lbl.configure(text="READY", text_color="#3b8ed0")
self.root.geometry(self.CONFIG_SIZE)
self.settings_frame.pack(padx=20, pady=10, fill="both", expand=True, before=self.display_frame)
pygame.mixer.music.stop()
def start_timer(self):
is_already_min = not self.settings_frame.winfo_manager()
if self.is_running:
if self.timer_after_id: self.root.after_cancel(self.timer_after_id)
self.is_running = False
try:
d_sec = int(self.d_h.get())*3600 + int(self.d_m.get())*60 + int(self.d_s.get())
t_sec = int(self.h_entry.get())*3600 + int(self.m_entry.get())*60 + int(self.s_entry.get())
if t_sec <= 0: return
self.total_seconds = t_sec
self.reps_to_do = int(self.rep_entry.get())
self.current_rep = 1
self.is_running = True
if not is_already_min:
self.settings_frame.pack_forget()
self.root.geometry(self.RUNNING_SIZE)
if d_sec > 0 and not is_already_min:
self.in_delay_phase = True
self.status_lbl.configure(text="PHASE: DELAY", text_color="orange")
self.tick(d_sec)
else:
self.in_delay_phase = False
self.run_cycle()
except: messagebox.showerror("Error", "Invalid Input")
def run_cycle(self):
if self.current_rep > self.reps_to_do:
self.reset_timer()
messagebox.showinfo("Done", "Timer Finished!")
return
self.status_lbl.configure(text=f"ACTIVE (Rep {self.current_rep})", text_color="#2fa572")
self.rep_lbl.configure(text=f"Repetition: {self.current_rep} of {self.reps_to_do}")
val = self.total_seconds if self.mode.get() == "Countdown" else 0
self.tick(val)
def tick(self, current_val):
if not self.is_running or self.is_paused:
if self.is_paused: self.timer_after_id = self.root.after(100, lambda: self.tick(current_val))
return
h, m, s = current_val // 3600, (current_val % 3600) // 60, current_val % 60
self.time_lbl.configure(text=f"{h:02d}:{m:02d}:{s:02d}")
over = False
if self.in_delay_phase or self.mode.get() == "Countdown":
if current_val <= 0: over = True
else: next_v = current_val - 1
else:
if current_val >= self.total_seconds: over = True
else: next_v = current_val + 1
if over:
if self.in_delay_phase:
self.in_delay_phase = False
self.run_cycle()
else:
self.play_alarm()
self.current_rep += 1
self.run_cycle()
else:
self.timer_after_id = self.root.after(1000, lambda: self.tick(next_v))
def toggle_pause(self):
if self.is_running:
self.is_paused = not self.is_paused
self.pause_btn.configure(text="RESUME" if self.is_paused else "PAUSE")
def play_alarm(self):
if self.sound_path and os.path.exists(self.sound_path):
pygame.mixer.music.load(self.sound_path)
pygame.mixer.music.play()
else: self.root.bell()
if __name__ == "__main__":
root = ctk.CTk()
app = ModernRepeatTimer(root)
root.mainloop()
Old code
import tkinter as tk
from tkinter import messagebox, filedialog
import pygame
import os
class RepeatTimer:
def __init__(self, root):
self.root = root
self.root.title("Python Repeat Timer")
# =====================================================
# 1. DEFAULTS & WINDOW SETTINGS (Edit these values)
# =====================================================
# Window Sizes
self.CONFIG_SIZE = "550x700" # Full size for settings
self.RUNNING_SIZE = "350x290" # Compact size for operation
# Default Delay Values
self.DEF_DELAY_H = "00"
self.DEF_DELAY_M = "00"
self.DEF_DELAY_S = "05"
# Default Duration Values
self.DEF_DUR_H = "00"
self.DEF_DUR_M = "00"
self.DEF_DUR_S = "10"
# Default Repetitions
self.DEF_REPS = "10"
# =====================================================
self.root.resizable(True, True)
pygame.mixer.init()
self.root.protocol("WM_DELETE_WINDOW", self.quit_app)
# Logical Variables
self.mode = tk.StringVar(value="Countdown")
self.total_seconds = 0
self.delay_seconds = 0
self.current_rep = 1
self.reps_to_do = 1
self.is_running = False
self.is_paused = False
self.in_delay_phase = False
self.timer_after_id = None
self.setup_ui()
self.root.geometry(self.CONFIG_SIZE)
def setup_ui(self):
# Maker's Name
tk.Label(self.root, text="MAHESH KAMBLE'S TIMER @mtkamble", font=('Arial', 14, 'bold'), fg="blue").pack(pady=10)
# --- SETTINGS BOX ---
self.settings_frame = tk.LabelFrame(self.root, text="Configuration", padx=10, pady=10)
self.settings_frame.pack(padx=20, pady=10, fill="both")
# Step 1: Mode
tk.Label(self.settings_frame, text="Step 1: Select Mode", font=('Arial', 10, 'bold')).pack(pady=2)
tk.OptionMenu(self.settings_frame, self.mode, "Countdown", "Countup").pack()
# Step 2: Start Delay
tk.Label(self.settings_frame, text="Step 2: Start Delay (HH:MM:SS)", font=('Arial', 10, 'bold')).pack(pady=5)
delay_f = tk.Frame(self.settings_frame); delay_f.pack()
self.d_h = tk.Entry(delay_f, width=3, justify='center'); self.d_h.insert(0, self.DEF_DELAY_H); self.d_h.pack(side="left")
tk.Label(delay_f, text=":").pack(side="left")
self.d_m = tk.Entry(delay_f, width=3, justify='center'); self.d_m.insert(0, self.DEF_DELAY_M); self.d_m.pack(side="left")
tk.Label(delay_f, text=":").pack(side="left")
self.d_s = tk.Entry(delay_f, width=3, justify='center'); self.d_s.insert(0, self.DEF_DELAY_S); self.d_s.pack(side="left")
delay_q = tk.Frame(self.settings_frame); delay_q.pack(pady=2)
for lbl, sec in [("5s", 5), ("10s", 10), ("30s", 30), ("1m", 60), ("5m", 300)]:
tk.Button(delay_q, text=lbl, font=('Arial', 8), command=lambda s=sec: self.set_delay(s)).pack(side="left", padx=2)
# Step 3: Duration
tk.Label(self.settings_frame, text="Step 3: Set Duration (HH:MM:SS)", font=('Arial', 10, 'bold')).pack(pady=5)
dur_f = tk.Frame(self.settings_frame); dur_f.pack()
self.h_entry = tk.Entry(dur_f, width=3, justify='center'); self.h_entry.insert(0, self.DEF_DUR_H); self.h_entry.pack(side="left")
tk.Label(dur_f, text=":").pack(side="left")
self.m_entry = tk.Entry(dur_f, width=3, justify='center'); self.m_entry.insert(0, self.DEF_DUR_M); self.m_entry.pack(side="left")
tk.Label(dur_f, text=":").pack(side="left")
self.s_entry = tk.Entry(dur_f, width=3, justify='center'); self.s_entry.insert(0, self.DEF_DUR_S); self.s_entry.pack(side="left")
dur_q = tk.Frame(self.settings_frame); dur_q.pack(pady=2)
for lbl, sec in [("10s", 10), ("30s", 30), ("1m", 60), ("5m", 300), ("15m", 900)]:
tk.Button(dur_q, text=lbl, font=('Arial', 8), command=lambda s=sec: self.set_duration(s)).pack(side="left", padx=2)
# Step 4: Repetitions
tk.Label(self.settings_frame, text="Step 4: Repetitions", font=('Arial', 10, 'bold')).pack(pady=5)
self.rep_entry = tk.Entry(self.settings_frame, width=10, justify='center'); self.rep_entry.insert(0, self.DEF_REPS); self.rep_entry.pack()
rep_q = tk.Frame(self.settings_frame); rep_q.pack(pady=2)
for r in [1, 3, 5, 10]:
tk.Button(rep_q, text=str(r), width=4, command=lambda val=r: self.set_reps(val)).pack(side="left", padx=2)
# Step 5: Sound
tk.Label(self.settings_frame, text="Step 5: Sound File", font=('Arial', 10, 'bold')).pack(pady=5)
snd_f = tk.Frame(self.settings_frame); snd_f.pack()
self.sound_path = tk.Entry(snd_f, width=20); self.sound_path.pack(side="left", padx=2)
tk.Button(snd_f, text="...", command=self.browse_sound).pack(side="left")
# --- ACTIVE DISPLAY AREA ---
self.display_frame = tk.Frame(self.root)
self.display_frame.pack(pady=5, fill="both")
self.status_label = tk.Label(self.display_frame, text="READY", font=('Arial', 10, 'bold'), fg="darkorange")
self.status_label.pack()
self.rep_display = tk.Label(self.display_frame, text="Repetition: -", font=('Arial', 10))
self.rep_display.pack()
self.time_display = tk.Label(self.display_frame, text="00:00:00", font=('Arial', 40, 'bold'))
self.time_display.pack()
self.blink_box = tk.Label(self.display_frame, text="ALARM", bg="grey", width=15, height=1, relief="sunken")
self.blink_box.pack(pady=5)
# Control Buttons
self.ctrl_frame = tk.Frame(self.root)
self.ctrl_frame.pack(side="bottom", pady=10)
self.start_btn = tk.Button(self.ctrl_frame, text="START", bg="green", fg="white", width=10, command=self.start_timer)
self.start_btn.grid(row=0, column=0, padx=2)
self.pause_btn = tk.Button(self.ctrl_frame, text="PAUSE", width=10, command=self.toggle_pause)
self.pause_btn.grid(row=0, column=1, padx=2)
tk.Button(self.ctrl_frame, text="RESET", width=10, command=self.reset_timer).grid(row=1, column=0, pady=5)
tk.Button(self.ctrl_frame, text="QUIT", bg="red", fg="white", width=10, command=self.quit_app).grid(row=1, column=1, pady=5)
def set_delay(self, total_sec):
h, m, s = total_sec // 3600, (total_sec % 3600) // 60, total_sec % 60
self.d_h.delete(0, tk.END); self.d_h.insert(0, f"{h:02d}")
self.d_m.delete(0, tk.END); self.d_m.insert(0, f"{m:02d}")
self.d_s.delete(0, tk.END); self.d_s.insert(0, f"{s:02d}")
def set_duration(self, total_sec):
h, m, s = total_sec // 3600, (total_sec % 3600) // 60, total_sec % 60
self.h_entry.delete(0, tk.END); self.h_entry.insert(0, f"{h:02d}")
self.m_entry.delete(0, tk.END); self.m_entry.insert(0, f"{m:02d}")
self.s_entry.delete(0, tk.END); self.s_entry.insert(0, f"{s:02d}")
def set_reps(self, val):
self.rep_entry.delete(0, tk.END); self.rep_entry.insert(0, str(val))
def format_time(self, seconds):
h, m, s = seconds // 3600, (seconds % 3600) // 60, seconds % 60
return f"{h:02d}:{m:02d}:{s:02d}"
def browse_sound(self):
file = filedialog.askopenfilename(filetypes=[("Audio files", "*.mp3 *.wav")])
if file:
self.sound_path.delete(0, tk.END); self.sound_path.insert(0, file)
def quit_app(self):
self.is_running = False
if self.timer_after_id: self.root.after_cancel(self.timer_after_id)
pygame.mixer.quit()
self.root.destroy()
def reset_timer(self):
self.is_running = False
self.in_delay_phase = False
if self.timer_after_id: self.root.after_cancel(self.timer_after_id)
self.time_display.config(text="00:00:00")
self.status_label.config(text="READY", fg="darkorange")
self.rep_display.config(text="Repetition: -")
self.pause_btn.config(text="PAUSE")
self.is_paused = False
self.blink_box.config(bg="grey")
pygame.mixer.music.stop()
# Expand back to full configuration size
self.root.geometry(self.CONFIG_SIZE)
self.settings_frame.pack(padx=20, pady=10, fill="both", before=self.display_frame)
def toggle_pause(self):
if self.is_running:
self.is_paused = not self.is_paused
self.pause_btn.config(text="RESUME" if self.is_paused else "PAUSE")
def start_timer(self):
is_already_running_ui = not self.settings_frame.winfo_manager()
if self.is_running:
if self.timer_after_id: self.root.after_cancel(self.timer_after_id)
self.is_running = False
try:
self.delay_seconds = (int(self.d_h.get() or 0) * 3600) + (int(self.d_m.get() or 0) * 60) + int(self.d_s.get() or 0)
self.total_seconds = (int(self.h_entry.get() or 0) * 3600) + (int(self.m_entry.get() or 0) * 60) + int(self.s_entry.get() or 0)
if self.total_seconds <= 0:
messagebox.showwarning("Warning", "Duration must be greater than 0.")
return
self.reps_to_do = int(self.rep_entry.get() or 1)
self.current_rep = 1
self.is_running = True
self.is_paused = False
if not is_already_running_ui:
self.settings_frame.pack_forget()
self.root.geometry(self.RUNNING_SIZE)
if self.delay_seconds > 0 and not is_already_running_ui:
self.in_delay_phase = True
self.status_label.config(text="PHASE: DELAY", fg="darkorange")
self.tick(self.delay_seconds)
else:
self.in_delay_phase = False
self.run_cycle()
except ValueError:
messagebox.showerror("Error", "Check your inputs.")
def run_cycle(self):
if self.current_rep > self.reps_to_do:
self.reset_timer()
messagebox.showinfo("Done", "All finished!")
return
self.status_label.config(text=f"PHASE: ACTIVE (Rep {self.current_rep})", fg="green")
self.rep_display.config(text=f"Repetition: {self.current_rep} of {self.reps_to_do}")
val = self.total_seconds if self.mode.get() == "Countdown" else 0
self.tick(val)
def tick(self, current_val):
if not self.is_running: return
if self.is_paused:
self.timer_after_id = self.root.after(100, lambda: self.tick(current_val))
return
self.time_display.config(text=self.format_time(current_val))
over = False
if self.in_delay_phase or self.mode.get() == "Countdown":
if current_val <= 0: over = True
next_val = current_val - 1
else:
if current_val >= self.total_seconds: over = True
next_val = current_val + 1
if over:
if self.in_delay_phase:
self.in_delay_phase = False
self.run_cycle()
else:
self.play_alarm()
self.blink(0)
else:
self.timer_after_id = self.root.after(1000, lambda: self.tick(next_val))
def play_alarm(self):
path = self.sound_path.get()
if path and os.path.exists(path):
try:
pygame.mixer.music.load(path)
pygame.mixer.music.play()
except: self.root.bell()
else: self.root.bell()
def blink(self, count):
if not self.is_running: return
if count < 6:
new_color = "yellow" if count % 2 == 0 else "grey"
self.blink_box.config(bg=new_color)
self.root.after(200, lambda: self.blink(count + 1))
else:
self.current_rep += 1
self.run_cycle()
if __name__ == "__main__":
root = tk.Tk()
app = RepeatTimer(root)
root.mainloop()
better GUI In tk
import os
import sys
# Hide the pygame welcome message in console
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import pygame
import os
class MTKAlarmFull:
def __init__(self, root):
self.root = root
self.root.title("MTK Alarm - Performance Edition")
# =====================================================
# 1. DEFAULTS & WINDOW SETTINGS (Edit these values)
# =====================================================
self.CONFIG_SIZE = "500x850" # Size for settings
self.RUNNING_SIZE = "350x420" # Size during countdown
# Default Delay Values
self.DEF_DELAY_H, self.DEF_DELAY_M, self.DEF_DELAY_S = "00", "00", "00"
# Default Duration Values
self.DEF_DUR_H, self.DEF_DUR_M, self.DEF_DUR_S = "00", "00", "00"
# Default Repetitions
self.DEF_REPS = "1"
# =====================================================
self.root.resizable(True, True)
pygame.mixer.init()
self.root.protocol("WM_DELETE_WINDOW", self.quit_app)
# Logical Variables
self.mode = tk.StringVar(value="Countdown")
self.is_running = False
self.is_paused = False
self.in_delay_phase = False
self.timer_after_id = None
self.current_rep = 1
self.sound_path = ""
self.apply_styles()
self.setup_ui()
self.root.geometry(self.CONFIG_SIZE)
def apply_styles(self):
self.style = ttk.Style()
self.style.theme_use('clam')
self.style.configure("Header.TLabel", font=("Segoe UI", 16, "bold"), foreground="#2c3e50")
self.style.configure("TLabel", font=("Segoe UI", 10))
def setup_ui(self):
# Maker's Name / Header
ttk.Label(self.root, text="MTK ALARM SYSTEM", style="Header.TLabel").pack(pady=15)
# --- SETTINGS BOX ---
self.settings_frame = ttk.LabelFrame(self.root, text=" 1. Configuration ", padding=10)
self.settings_frame.pack(padx=20, pady=10, fill="both", expand=True)
# Mode Selection
ttk.Label(self.settings_frame, text="Step 1: Select Mode").pack()
ttk.OptionMenu(self.settings_frame, self.mode, "Countdown", "Countdown", "Countup").pack(pady=5)
# Start Delay (Step 2)
ttk.Label(self.settings_frame, text="Step 2: Start Delay (HH:MM:SS)").pack(pady=(10, 0))
delay_f = ttk.Frame(self.settings_frame); delay_f.pack()
self.d_h = self.create_entry(delay_f, self.DEF_DELAY_H); self.d_h.pack(side="left")
self.d_m = self.create_entry(delay_f, self.DEF_DELAY_M); self.d_m.pack(side="left")
self.d_s = self.create_entry(delay_f, self.DEF_DELAY_S); self.d_s.pack(side="left")
delay_q = ttk.Frame(self.settings_frame); delay_q.pack(pady=5)
for lbl, sec in [("5s", 5), ("10s", 10), ("30s", 30), ("1m", 60)]:
ttk.Button(delay_q, text=lbl, width=5, command=lambda s=sec: self.set_time_vars(self.d_h, self.d_m, self.d_s, s)).pack(side="left", padx=2)
# Duration (Step 3)
ttk.Label(self.settings_frame, text="Step 3: Set Duration (HH:MM:SS)").pack(pady=(10, 0))
dur_f = ttk.Frame(self.settings_frame); dur_f.pack()
self.h_entry = self.create_entry(dur_f, self.DEF_DUR_H); self.h_entry.pack(side="left")
self.m_entry = self.create_entry(dur_f, self.DEF_DUR_M); self.m_entry.pack(side="left")
self.s_entry = self.create_entry(dur_f, self.DEF_DUR_S); self.s_entry.pack(side="left")
dur_q = ttk.Frame(self.settings_frame); dur_q.pack(pady=5)
for lbl, sec in [("10s", 10), ("30s", 30), ("1m", 60), ("5m", 300), ("15m", 900)]:
ttk.Button(dur_q, text=lbl, width=5, command=lambda s=sec: self.set_time_vars(self.h_entry, self.m_entry, self.s_entry, s)).pack(side="left", padx=2)
# Repetitions (Step 4)
ttk.Label(self.settings_frame, text="Step 4: Repetitions").pack(pady=(10, 0))
self.rep_entry = ttk.Entry(self.settings_frame, width=10, justify="center")
self.rep_entry.insert(0, self.DEF_REPS)
self.rep_entry.pack(pady=5)
rep_q = ttk.Frame(self.settings_frame); rep_q.pack()
for r in [1, 3, 5, 10]:
ttk.Button(rep_q, text=str(r), width=4, command=lambda val=r: self.set_reps(val)).pack(side="left", padx=2)
# Sound (Step 5)
ttk.Button(self.settings_frame, text="Step 5: Select Alarm Sound", command=self.browse_sound).pack(pady=15)
# --- ACTIVE DISPLAY AREA ---
self.display_frame = ttk.Frame(self.root)
self.display_frame.pack(pady=5, fill="both")
self.status_lbl = ttk.Label(self.display_frame, text="READY", font=("Segoe UI", 12, "bold"), foreground="#3498db")
self.status_lbl.pack()
self.time_lbl = tk.Label(self.display_frame, text="00:00:00", font=("Consolas", 48, "bold"), fg="#2c3e50")
self.time_lbl.pack(pady=10)
self.rep_lbl = ttk.Label(self.display_frame, text="Repetition: -")
self.rep_lbl.pack()
# --- CONTROLS ---
self.ctrl_frame = ttk.Frame(self.root)
self.ctrl_frame.pack(side="bottom", pady=20)
self.start_btn = tk.Button(self.ctrl_frame, text="START", bg="#27ae60", fg="white", font=("Segoe UI", 10, "bold"), width=12, command=self.start_timer)
self.start_btn.grid(row=0, column=0, padx=5)
self.pause_btn = ttk.Button(self.ctrl_frame, text="PAUSE", width=12, command=self.toggle_pause)
self.pause_btn.grid(row=0, column=1, padx=5)
self.reset_btn = tk.Button(self.ctrl_frame, text="RESET", bg="#e74c3c", fg="white", font=("Segoe UI", 10, "bold"), width=12, command=self.reset_timer)
self.reset_btn.grid(row=1, column=0, columnspan=2, pady=10, sticky="ew")
def create_entry(self, parent, val):
e = ttk.Entry(parent, width=5, justify="center")
e.insert(0, val)
return e
def set_time_vars(self, h_ent, m_ent, s_ent, total_sec):
h, m, s = total_sec // 3600, (total_sec % 3600) // 60, total_sec % 60
h_ent.delete(0, tk.END); h_ent.insert(0, f"{h:02d}")
m_ent.delete(0, tk.END); m_ent.insert(0, f"{m:02d}")
s_ent.delete(0, tk.END); s_ent.insert(0, f"{s:02d}")
def set_reps(self, val):
self.rep_entry.delete(0, tk.END); self.rep_entry.insert(0, str(val))
def browse_sound(self):
file = filedialog.askopenfilename(filetypes=[("Audio", "*.mp3 *.wav")])
if file: self.sound_path = file
def reset_timer(self):
self.is_running = False
self.in_delay_phase = False
if self.timer_after_id: self.root.after_cancel(self.timer_after_id)
self.time_lbl.config(text="00:00:00")
self.status_lbl.config(text="READY", foreground="#3498db")
self.rep_lbl.config(text="Repetition: -")
self.root.geometry(self.CONFIG_SIZE)
self.settings_frame.pack(padx=20, pady=10, fill="both", expand=True, before=self.display_frame)
pygame.mixer.music.stop()
def start_timer(self):
is_already_minimized = not self.settings_frame.winfo_manager()
if self.is_running:
if self.timer_after_id: self.root.after_cancel(self.timer_after_id)
self.is_running = False
try:
d_sec = int(self.d_h.get() or 0)*3600 + int(self.d_m.get() or 0)*60 + int(self.d_s.get() or 0)
t_sec = int(self.h_entry.get() or 0)*3600 + int(self.m_entry.get() or 0)*60 + int(self.s_entry.get() or 0)
if t_sec <= 0:
messagebox.showwarning("Warning", "Duration must be > 0")
return
self.total_seconds = t_sec
self.reps_to_do = int(self.rep_entry.get() or 1)
self.current_rep = 1
self.is_running = True
self.is_paused = False
if not is_already_minimized:
self.settings_frame.pack_forget()
self.root.geometry(self.RUNNING_SIZE)
if d_sec > 0 and not is_already_minimized:
self.in_delay_phase = True
self.status_lbl.config(text="PHASE: DELAY", foreground="orange")
self.tick(d_sec)
else:
self.in_delay_phase = False
self.run_cycle()
except ValueError:
messagebox.showerror("Error", "Invalid inputs!")
def run_cycle(self):
if self.current_rep > self.reps_to_do:
self.reset_timer()
messagebox.showinfo("Done", "All repetitions finished!")
return
self.status_lbl.config(text=f"ACTIVE (Rep {self.current_rep})", foreground="#27ae60")
self.rep_lbl.config(text=f"Repetition: {self.current_rep} of {self.reps_to_do}")
val = self.total_seconds if self.mode.get() == "Countdown" else 0
self.tick(val)
def tick(self, current_val):
if not self.is_running: return
if self.is_paused:
self.timer_after_id = self.root.after(100, lambda: self.tick(current_val))
return
h, m, s = current_val // 3600, (current_val % 3600) // 60, current_val % 60
self.time_lbl.config(text=f"{h:02d}:{m:02d}:{s:02d}")
over = False
if self.in_delay_phase or self.mode.get() == "Countdown":
if current_val <= 0: over = True
else: next_v = current_val - 1
else:
if current_val >= self.total_seconds: over = True
else: next_v = current_val + 1
if over:
if self.in_delay_phase:
self.in_delay_phase = False
self.run_cycle()
else:
self.play_alarm()
self.current_rep += 1
self.run_cycle()
else:
self.timer_after_id = self.root.after(1000, lambda: self.tick(next_v))
def toggle_pause(self):
if self.is_running:
self.is_paused = not self.is_paused
self.pause_btn.config(text="RESUME" if self.is_paused else "PAUSE")
def play_alarm(self):
if self.sound_path and os.path.exists(self.sound_path):
try:
pygame.mixer.music.load(self.sound_path)
pygame.mixer.music.play()
except: self.root.bell()
else: self.root.bell()
def quit_app(self):
pygame.mixer.quit()
self.root.destroy()
if __name__ == "__main__":
root = tk.Tk()
app = MTKAlarmFull(root)
root.mainloop()
Common Offline Pitfalls
| Problem | Solution |
| "Requirement already satisfied" | Pip thinks it's already there. Use --force-reinstall if you need to overwrite it. |
| Wrong Python Version | Ensure the cp310, cp311, etc., in the filename matches your offline Python version. |
| Wrong Architecture | Ensure you aren't trying to put a win_amd64 (64-bit) wheel on a 32-bit Python install. |