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.

  1. Create a directory for the bundle:Bash
    mkdir pyinstaller_bundle && cd pyinstaller_bundle
  2. Download the packages using "cross-platform" flags. This tells the Linux pip to grab Windows files for Python 3.8:Bash
    pip download \
    --platform win_amd64 \
    --python-version 3.8 \
    --only-binary=:all: \
    pyinstaller
    • Crucial: Check the folder now. If you don't see pywin32-ctypes or altgraph in the folder, pip skipped them because it's on Linux.
  3. 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

  1. Compress the folder (optional, but easier for USB transfer):Bash
    tar -czvf pyinstaller_bundle.tar.gz pyinstaller_bundle/
  2. Move the folder/archive to your Offline Windows Machine via USB.

Phase 3: On the Windows (Offline) Machine

  1. Extract the files so you have a folder full of .whl files (e.g., in C:\pyinstaller_bundle).
  2. Open PowerShell/CMD inside that folder.
  3. Run the offline install: Use the --no-index flag to prevent it from trying to connect to the internet (and failing with that "Connection Refused" error again).PowerShell
    pip install --no-index --find-links . pyinstaller

Critical Precautions Summary

Precaution

Why?

Check forwin_amd64

Ensure none of the filenames containmanylinux. If they do, they won't work on Windows.

Check forcp38

Ensure the wheels match your Python 3.8 version.

Use--only-binary=:all:

This preventspipfrom downloading a.tar.gz(source) file, which usually requires an internet connection to compile.

Verifypywin32-ctypes

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:

  1. Python Version: Ensure the wheel version (e.g., cp310 for Python 3.10) matches your installed version (python --version).
  2. System Architecture: Ensure you aren't trying to install a win_amd64 wheel 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

ScenarioCommand
Standard Installpip install filename.whl
Install via Python Launcherpython -m pip install filename.whl
Force Reinstallpip install --force-reinstall filename.whl
Install to User Directorypip 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 .whl file 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:

  1. Move that entire folder to your offline PC.
  2. 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.

  1. Create a dedicated folder for the files:Bash
    mkdir pygame_offline && cd pygame_offline
  2. 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).
  3. Download Pygame and dependencies:Run the following command. Replace 3.11 with your specific Windows Python version (e.g., 3.10, 3.12):Bash
    pip download pygame \
    --only-binary=:all: \
    --platform win_amd64 \
    --python-version 3.11 \
    -d .
    • --platform win_amd64: Targets 64-bit Windows. (Use win32 if the Windows machine is 32-bit).
    • --only-binary=:all:: Ensures you get .whl files (binaries) rather than source code that requires a compiler.
    • -d .: Saves the files in the current directory.
  4. Verify the files:You should see at least one .whl file in the folder (usually pygame-xxx-cp311-cp311-win_amd64.whl).

Phase 2: Transfer

  1. Copy the entire pygame_offline folder onto a USB drive.
  2. Plug the USB drive into the Offline Windows Machine.

Phase 3: On the Offline Windows Machine

  1. Open Command Prompt (cmd):Navigate to the folder on the USB drive containing the .whl files.DOS
    cd D:\pygame_offline
  2. Install the package:Use pip to install from the local directory. Use a wildcard * to catch the file name:DOS
    pip 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: pygame is 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 like numpy.
  • PIP itself: If the offline Windows machine doesn't even have pip installed, you will need to download the get-pip.py script on the Linux machine, move it over, and run python get-pip.py first.
  • 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 --onefile and --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.spec instead 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:

  1. Test it locally first: Double-click the EXE in the dist folder on your current computer.
    • Does the window resize correctly?
    • Does the alarm sound play?
  2. Audio Files: Since you are using a "Browse" button in your code, make sure the offline machine actually has an .mp3 or .wav file on its hard drive for you to select.
  3. USB Transfer: Copy only the MTKAlarm.exe to 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

ProblemSolution
"Requirement already satisfied"Pip thinks it's already there. Use --force-reinstall if you need to overwrite it.
Wrong Python VersionEnsure the cp310, cp311, etc., in the filename matches your offline Python version.
Wrong ArchitectureEnsure you aren't trying to put a win_amd64 (64-bit) wheel on a 32-bit Python install.
powered by lykhari