import tkinter as tk import time import threading from datetime import datetime import requests import csv import io import os import json # Get the user's Documents folder (cross-platform) documents_folder = os.path.join(os.path.expanduser("~"), "Documents") # Create your app folder inside Documents app_folder = os.path.join(documents_folder, "RocketLaunchCountdown") os.makedirs(app_folder, exist_ok=True) # Define file paths COUNTDOWN_HTML = os.path.join(app_folder, "countdown.html") GONOGO_HTML = os.path.join(app_folder, "gonogo.html") SHEET_LINK = "https://docs.google.com/spreadsheets/d/1UPJTW8vH2mgEzispjg_Y_zSqYTFaLoxuoZnqleVlSZ0/export?format=csv&gid=855477916" session = requests.Session() appVersion = "0.3.0" SETTINGS_FILE = os.path.join(app_folder, "settings.json") # Default settings DEFAULT_SETTINGS = { "mode": "spreadsheet", # or 'buttons' "sheet_link": SHEET_LINK, # rows are 1-based as shown in spreadsheet; default 2,3,4 -> indices 1,2,3 "range_row": 2, "weather_row": 3, "vehicle_row": 4, "column": 12 # 1-based column (default column 12 -> index 11) } def load_settings(): try: if os.path.exists(SETTINGS_FILE): with open(SETTINGS_FILE, 'r', encoding='utf-8') as fh: return json.load(fh) except Exception: pass # ensure default saved save_settings(DEFAULT_SETTINGS) return DEFAULT_SETTINGS.copy() def save_settings(s): try: with open(SETTINGS_FILE, 'w', encoding='utf-8') as fh: json.dump(s, fh, indent=2) except Exception: pass # ------------------------- # Fetch Go/No-Go Data # ------------------------- def fetch_gonogo(): """Fetch Go/No-Go parameters either from configured spreadsheet or return manual button values.""" settings = load_settings() mode = settings.get('mode', 'spreadsheet') # If manual mode, read values from a runtime stash (set by the GUI buttons) if mode == 'buttons': # stored values will be on the app class; fallback to N/A try: return [getattr(fetch_gonogo, 'manual_range', 'N/A'), getattr(fetch_gonogo, 'manual_weather', 'N/A'), getattr(fetch_gonogo, 'manual_vehicle', 'N/A')] except Exception: return ['N/A', 'N/A', 'N/A'] # spreadsheet mode link = settings.get('sheet_link', SHEET_LINK) col = max(1, int(settings.get('column', 12))) - 1 rows = [int(settings.get('range_row', 2)) - 1, int(settings.get('weather_row', 3)) - 1, int(settings.get('vehicle_row', 4)) - 1] try: resp = session.get(link, timeout=3) resp.raise_for_status() reader = csv.reader(io.StringIO(resp.text)) data = list(reader) gonogo = [] for r in rows: val = 'N/A' if 0 <= r < len(data) and len(data[r]) > col: val = data[r][col] gonogo.append(val.strip().upper()) return gonogo except Exception as e: print(f"[ERROR] Failed to fetch Go/No-Go from sheet: {e}") return ["ERROR", "ERROR", "ERROR"] # ------------------------- # Helper for color # ------------------------- def get_status_color(status): """Return color name for a Go/No-Go status string.""" try: return "green" if str(status).strip().upper() == "GO" else "red" except Exception: return "white" # ------------------------- # Write Countdown HTML # ------------------------- def write_countdown_html(mission_name, timer_text): html = f"""
{mission_name}
{timer_text}
""" with open(COUNTDOWN_HTML, "w", encoding="utf-8") as f: f.write(html) # ------------------------- # Write Go/No-Go HTML # ------------------------- def write_gonogo_html(gonogo_values=None): if gonogo_values is None: gonogo_values = ["N/A", "N/A", "N/A"] html = f"""
Range: {gonogo_values[0]}
Vehicle: {gonogo_values[2]}
Weather: {gonogo_values[1]}
""" with open(GONOGO_HTML, "w", encoding="utf-8") as f: f.write(html) # ------------------------- # Countdown App # ------------------------- class CountdownApp: def __init__(self, root): self.root = root self.root.title(f"RocketLaunchCountdown {appVersion}") self.root.config(bg="black") self.root.attributes("-topmost", True) self.root.geometry("800x615") # State self.running = False self.on_hold = False self.scrubbed = False self.counting_up = False self.target_time = None self.hold_start_time = None self.remaining_time = 0 self.mission_name = "Placeholder Mission" # fetch_gonogo() returns [Range, Weather, Vehicle] to match gonogo.html writer self.gonogo_values = fetch_gonogo() self.last_gonogo_update = time.time() # Title self.titletext = tk.Label(root, text="RocketLaunchCountdown", font=("Consolas", 24), fg="white", bg="black") self.titletext.pack(pady=(10, 0)) # Display self.text = tk.Label(root, text="T-00:00:00", font=("Consolas", 80, "bold"), fg="white", bg="black") self.text.pack(pady=(0, 5)) # Mission name input frame_top = tk.Frame(root, bg="black") frame_top.pack(pady=5) tk.Label(frame_top, text="Mission Name:", fg="white", bg="black").pack(side="left") self.mission_entry = tk.Entry(frame_top, width=20, font=("Arial", 18)) self.mission_entry.insert(0, self.mission_name) self.mission_entry.pack(side="left") # Mode toggle frame_mode = tk.Frame(root, bg="black") frame_mode.pack(pady=5) self.mode_var = tk.StringVar(value="duration") self.radio_duration = tk.Radiobutton( frame_mode, text="Duration", variable=self.mode_var, value="duration", fg="white", bg="black", selectcolor="black", # makes the dot visible command=self.update_inputs ) self.radio_duration.pack(side="left", padx=5) self.radio_clock = tk.Radiobutton( frame_mode, text="Clock Time", variable=self.mode_var, value="clock", fg="white", bg="black", selectcolor="black", # makes the dot visible command=self.update_inputs ) self.radio_clock.pack(side="left", padx=5) # Duration inputs frame_duration = tk.Frame(root, bg="black") frame_duration.pack(pady=5) tk.Label(frame_duration, text="H", fg="white", bg="black").pack(side="left") self.hours_entry = tk.Entry(frame_duration, width=3, font=("Arial", 18)) self.hours_entry.insert(0, "0") self.hours_entry.pack(side="left", padx=2) tk.Label(frame_duration, text="M", fg="white", bg="black").pack(side="left") self.minutes_entry = tk.Entry(frame_duration, width=3, font=("Arial", 18)) self.minutes_entry.insert(0, "5") self.minutes_entry.pack(side="left", padx=2) tk.Label(frame_duration, text="S", fg="white", bg="black").pack(side="left") self.seconds_entry = tk.Entry(frame_duration, width=3, font=("Arial", 18)) self.seconds_entry.insert(0, "0") self.seconds_entry.pack(side="left", padx=2) # Clock time input frame_clock = tk.Frame(root, bg="black") frame_clock.pack(pady=5) tk.Label(frame_clock, text="HH:MM", fg="white", bg="black").pack(side="left") self.clock_entry = tk.Entry(frame_clock, width=7, font=("Arial", 18)) self.clock_entry.insert(0, "14:00") self.clock_entry.pack(side="left", padx=5) # Control buttons frame_buttons = tk.Frame(root, bg="black") frame_buttons.pack(pady=10) self.start_btn = tk.Button(frame_buttons, text="▶ Start", command=self.start, font=("Arial", 14)) self.start_btn.grid(row=0, column=0, padx=5) # Hold and resume share the same position self.hold_btn = tk.Button(frame_buttons, text="⏸ Hold", command=self.hold, font=("Arial", 14)) self.hold_btn.grid(row=0, column=1, padx=5) self.resume_btn = tk.Button(frame_buttons, text="⏵ Resume", command=self.resume, font=("Arial", 14)) self.resume_btn.grid(row=0, column=1, padx=5) self.resume_btn.grid_remove() # hidden at start self.scrub_btn = tk.Button(frame_buttons, text="🚫 Scrub", command=self.scrub, font=("Arial", 14), fg="red") self.scrub_btn.grid(row=0, column=2, padx=5) self.reset_btn = tk.Button(frame_buttons, text="⟳ Reset", command=self.reset, font=("Arial", 14)) self.reset_btn.grid(row=0, column=3, padx=5) # Settings button moved next to control buttons (match size/style) settings_btn = tk.Button(frame_buttons, text="Settings", command=self.show_settings_window, font=("Arial", 14), width=10) settings_btn.grid(row=0, column=4, padx=6) # Note: gonogo mode switching remains in Settings; manual buttons appear when mode == 'buttons' # Manual Go/No-Go buttons will go next to control buttons self.manual_frame = tk.Frame(root, bg="black") self.manual_frame.pack(pady=6) # Buttons now toggle current state between GO and NOGO self.range_toggle_btn = tk.Button(self.manual_frame, text="Range: Toggle", width=12, command=lambda: self._toggle_manual('range')) self.weather_toggle_btn = tk.Button(self.manual_frame, text="Weather: Toggle", width=12, command=lambda: self._toggle_manual('weather')) self.vehicle_toggle_btn = tk.Button(self.manual_frame, text="Vehicle: Toggle", width=12, command=lambda: self._toggle_manual('vehicle')) # Placeholders; visibility will be controlled by settings self.range_toggle_btn.grid(row=0, column=0, padx=4, pady=2) self.weather_toggle_btn.grid(row=0, column=1, padx=4, pady=2) self.vehicle_toggle_btn.grid(row=0, column=2, padx=4, pady=2) frame_gn = tk.Frame(root, bg="black") frame_gn.pack(pady=10) # Labels displayed: Range, Weather, Vehicle — match write_gonogo_html ordering self.range_label = tk.Label(frame_gn, text="RANGE: N/A", font=("Consolas", 20), fg="white", bg="black") self.range_label.pack() self.weather_label = tk.Label(frame_gn, text="WEATHER: N/A", font=("Consolas", 20), fg="white", bg="black") self.weather_label.pack() self.vehicle_label = tk.Label(frame_gn, text="VEHICLE: N/A", font=("Consolas", 20), fg="white", bg="black") self.vehicle_label.pack() # Footer footer_frame = tk.Frame(root, bg="black") footer_frame.pack(side="bottom", pady=0, fill="x") self.footer_label = tk.Label( footer_frame, text="Made by HamsterSpaceNerd3000", # or whatever you want font=("Consolas", 12), fg="black", bg="white" ) self.footer_label.pack(fill="x") self.update_inputs() # set initial manual button visibility from settings self.update_manual_visibility() self.update_clock() # ---------------------------- # Settings window # ---------------------------- def show_settings_window(self): settings = load_settings() win = tk.Toplevel(self.root) win.config(bg="black") win.title("Settings") win.geometry("560x200") win.transient(self.root) # Mode selection frame_mode = tk.Frame(win) frame_mode.config(bg="black") frame_mode.pack(fill='x', pady=8, padx=8) tk.Label(frame_mode, text="Mode:", fg="white", bg="black").pack(side='left') mode_var = tk.StringVar(value=settings.get('mode', 'spreadsheet')) tk.Radiobutton(frame_mode, text='Spreadsheet', variable=mode_var, value='spreadsheet', fg="white", bg="black", selectcolor="black").pack(side='left', padx=8) tk.Radiobutton(frame_mode, text='Buttons (manual)', variable=mode_var, value='buttons', fg="white", bg="black", selectcolor="black").pack(side='left', padx=8) # Spreadsheet config frame_sheet = tk.LabelFrame(win, text='Spreadsheet configuration', fg='white', bg='black') frame_sheet.config(bg="black") frame_sheet.pack(fill='x', padx=8, pady=6) tk.Label(frame_sheet, text='Sheet link (CSV export):', fg='white', bg='black').pack(anchor='w') sheet_entry = tk.Entry(frame_sheet, width=80, fg='white', bg='#222', insertbackground='white') sheet_entry.pack(fill='x', padx=6, pady=4) sheet_entry.insert(0, settings.get('sheet_link', SHEET_LINK)) # Accept cells in 'L3' format for each parameter cell_frame = tk.Frame(frame_sheet) cell_frame.config(bg="black") cell_frame.pack(fill='x', padx=6, pady=2) tk.Label(cell_frame, text='Range cell (e.g. L3):', fg='white', bg='black').grid(row=0, column=0) range_cell = tk.Entry(cell_frame, width=8, fg='white', bg='#222', insertbackground='white') range_cell.grid(row=0, column=1, padx=4) # show as L3 if present, otherwise build from numeric settings try: if 'range_cell' in settings: range_cell.insert(0, settings.get('range_cell')) else: # convert numeric row/column to cell like L3 col = settings.get('column', DEFAULT_SETTINGS['column']) row = settings.get('range_row', DEFAULT_SETTINGS['range_row']) # column number to letters def col_to_letters(n): s = '' while n > 0: n, r = divmod(n - 1, 26) s = chr(ord('A') + r) + s return s range_cell.insert(0, f"{col_to_letters(col)}{row}") except Exception: range_cell.insert(0, f"L3") tk.Label(cell_frame, text='Weather cell (e.g. L4):', fg='white', bg='black').grid(row=0, column=2) weather_cell = tk.Entry(cell_frame, width=8, fg='white', bg='#222', insertbackground='white') weather_cell.grid(row=0, column=3, padx=4) try: if 'weather_cell' in settings: weather_cell.insert(0, settings.get('weather_cell')) else: col = settings.get('column', DEFAULT_SETTINGS['column']) row = settings.get('weather_row', DEFAULT_SETTINGS['weather_row']) def col_to_letters(n): s = '' while n > 0: n, r = divmod(n - 1, 26) s = chr(ord('A') + r) + s return s weather_cell.insert(0, f"{col_to_letters(col)}{row}") except Exception: weather_cell.insert(0, f"L4") tk.Label(cell_frame, text='Vehicle cell (e.g. L5):', fg='white', bg='black').grid(row=0, column=4) vehicle_cell = tk.Entry(cell_frame, width=8, fg='white', bg='#222', insertbackground='white') vehicle_cell.grid(row=0, column=5, padx=4) try: if 'vehicle_cell' in settings: vehicle_cell.insert(0, settings.get('vehicle_cell')) else: col = settings.get('column', DEFAULT_SETTINGS['column']) row = settings.get('vehicle_row', DEFAULT_SETTINGS['vehicle_row']) def col_to_letters(n): s = '' while n > 0: n, r = divmod(n - 1, 26) s = chr(ord('A') + r) + s return s vehicle_cell.insert(0, f"{col_to_letters(col)}{row}") except Exception: vehicle_cell.insert(0, f"L5") # Manual buttons config frame_buttons_cfg = tk.LabelFrame(win, text='Manual Go/No-Go (Buttons mode)', fg='white', bg='black') frame_buttons_cfg.config(bg='black') frame_buttons_cfg.pack(fill='x', padx=8, pady=6) def set_manual(val_type, val): # store on fetch_gonogo func for now if val_type == 'range': fetch_gonogo.manual_range = val elif val_type == 'weather': fetch_gonogo.manual_weather = val elif val_type == 'vehicle': fetch_gonogo.manual_vehicle = val # helper to set manual and update UI from main app def set_manual_and_update(val_type, val): set_manual(val_type, val) # update labels and write html self.gonogo_values = fetch_gonogo() # update GUI labels immediately self.range_label.config(text=f"RANGE: {self.gonogo_values[0]}", fg=get_status_color(self.gonogo_values[0])) self.weather_label.config(text=f"WEATHER: {self.gonogo_values[1]}", fg=get_status_color(self.gonogo_values[1])) self.vehicle_label.config(text=f"VEHICLE: {self.gonogo_values[2]}", fg=get_status_color(self.gonogo_values[2])) write_gonogo_html(self.gonogo_values) # Save/Cancel def cell_to_rc(cell_str): s = (cell_str or '').strip().upper() if not s: return None, None # split letters and digits letters = '' digits = '' for ch in s: if ch.isalpha(): letters += ch elif ch.isdigit(): digits += ch if not digits: return None, None # convert letters to number col = 0 for ch in letters: col = col * 26 + (ord(ch) - ord('A') + 1) row = int(digits) return row, col def on_save(): # parse cells r_row, r_col = cell_to_rc(range_cell.get()) w_row, w_col = cell_to_rc(weather_cell.get()) v_row, v_col = cell_to_rc(vehicle_cell.get()) # fallbacks if r_row is None: r_row = DEFAULT_SETTINGS['range_row'] if w_row is None: w_row = DEFAULT_SETTINGS['weather_row'] if v_row is None: v_row = DEFAULT_SETTINGS['vehicle_row'] # determine column to use (prefer range column, else weather, else vehicle, else default) col_val = r_col or w_col or v_col or DEFAULT_SETTINGS['column'] new_settings = { 'mode': mode_var.get(), 'sheet_link': sheet_entry.get().strip() or SHEET_LINK, 'range_row': int(r_row), 'weather_row': int(w_row), 'vehicle_row': int(v_row), 'column': int(col_val), # persist the textual cells for convenience 'range_cell': range_cell.get().strip().upper(), 'weather_cell': weather_cell.get().strip().upper(), 'vehicle_cell': vehicle_cell.get().strip().upper(), # persist manual values if present 'manual_range': getattr(fetch_gonogo, 'manual_range', None), 'manual_weather': getattr(fetch_gonogo, 'manual_weather', None), 'manual_vehicle': getattr(fetch_gonogo, 'manual_vehicle', None) } save_settings(new_settings) # update immediately self.gonogo_values = fetch_gonogo() write_gonogo_html(self.gonogo_values) # update manual visibility in main UI self.update_manual_visibility() win.destroy() def on_cancel(): win.destroy() btn_frame = tk.Frame(win) btn_frame = tk.Frame(win, bg='black') btn_frame.pack(fill='x', pady=8) tk.Button(btn_frame, text='Save', command=on_save, fg='white', bg='#333', activebackground='#444').pack(side='right', padx=8) tk.Button(btn_frame, text='Cancel', command=on_cancel, fg='white', bg='#333', activebackground='#444').pack(side='right') # ---------------------------- # Update input visibility based on mode # ---------------------------- def update_inputs(self): if self.mode_var.get() == "duration": self.hours_entry.config(state="normal") self.minutes_entry.config(state="normal") self.seconds_entry.config(state="normal") self.clock_entry.config(state="disabled") else: self.hours_entry.config(state="disabled") self.minutes_entry.config(state="disabled") self.seconds_entry.config(state="disabled") self.clock_entry.config(state="normal") # ---------------------------- # Manual controls & helpers # ---------------------------- def set_manual(self, which, val): # normalize v = (val or '').strip().upper() if which == 'range': fetch_gonogo.manual_range = v elif which == 'weather': fetch_gonogo.manual_weather = v elif which == 'vehicle': fetch_gonogo.manual_vehicle = v # update GUI and HTML self.gonogo_values = fetch_gonogo() try: self.range_label.config(text=f"RANGE: {self.gonogo_values[0]}", fg=get_status_color(self.gonogo_values[0])) self.weather_label.config(text=f"WEATHER: {self.gonogo_values[1]}", fg=get_status_color(self.gonogo_values[1])) self.vehicle_label.config(text=f"VEHICLE: {self.gonogo_values[2]}", fg=get_status_color(self.gonogo_values[2])) except Exception: pass write_gonogo_html(self.gonogo_values) # persist manual values immediately so they survive restarts try: s = load_settings() s['manual_range'] = getattr(fetch_gonogo, 'manual_range', s.get('manual_range')) s['manual_weather'] = getattr(fetch_gonogo, 'manual_weather', s.get('manual_weather')) s['manual_vehicle'] = getattr(fetch_gonogo, 'manual_vehicle', s.get('manual_vehicle')) save_settings(s) except Exception: pass def update_manual_visibility(self): s = load_settings() mode = s.get('mode', 'spreadsheet') visible = (mode == 'buttons') # show or hide manual frame if visible: self.manual_frame.pack(pady=6) else: self.manual_frame.pack_forget() def _toggle_manual(self, which): # get current values (Range, Weather, Vehicle) cur = fetch_gonogo() # map which to index idx_map = {'range': 0, 'weather': 1, 'vehicle': 2} idx = idx_map.get(which, 0) try: cur_val = (cur[idx] or '').strip().upper() except Exception: cur_val = 'N/A' # toggle: if GO -> NOGO, else -> GO new_val = 'NOGO' if cur_val == 'GO' else 'GO' self.set_manual(which, new_val) # ---------------------------- # Control logic # ---------------------------- def start(self): self.mission_name = self.mission_entry.get().strip() or "Placeholder Mission" self.running = True self.on_hold = False self.scrubbed = False self.counting_up = False self.show_hold_button() try: if self.mode_var.get() == "duration": h = int(self.hours_entry.get()) m = int(self.minutes_entry.get()) s = int(self.seconds_entry.get()) total_seconds = h * 3600 + m * 60 + s else: now = datetime.now() parts = [int(p) for p in self.clock_entry.get().split(":")] h, m = parts[0], parts[1] s = parts[2] if len(parts) == 3 else 0 target_today = now.replace(hour=h, minute=m, second=s, microsecond=0) if target_today < now: target_today = target_today.replace(day=now.day + 1) total_seconds = (target_today - now).total_seconds() except Exception: self.text.config(text="Invalid time") write_countdown_html(self.mission_name, "Invalid time") return self.target_time = time.time() + total_seconds self.remaining_time = total_seconds def hold(self): if self.running and not self.on_hold and not self.scrubbed: self.on_hold = True self.hold_start_time = time.time() self.remaining_time = max(0, self.target_time - self.hold_start_time) self.show_resume_button() def resume(self): if self.running and self.on_hold and not self.scrubbed: self.on_hold = False self.target_time = time.time() + self.remaining_time self.show_hold_button() def show_hold_button(self): self.resume_btn.grid_remove() self.hold_btn.grid() def show_resume_button(self): self.hold_btn.grid_remove() self.resume_btn.grid() def scrub(self): self.scrubbed = True self.running = False write_countdown_html(self.mission_name, "SCRUB") self.text.config(text="SCRUB") def reset(self): self.running = False self.on_hold = False self.scrubbed = False self.counting_up = False self.text.config(text="T-00:00:00") write_countdown_html(self.mission_name, "T-00:00:00") self.show_hold_button() # ---------------------------- # Clock updating # ---------------------------- def format_time(self, seconds, prefix="T-"): h = int(seconds // 3600) m = int((seconds % 3600) // 60) s = int(seconds % 60) return f"{prefix}{h:02}:{m:02}:{s:02}" def update_clock(self): now_time = time.time() # Update timer if self.running and not self.scrubbed: if self.on_hold: elapsed = int(now_time - self.hold_start_time) timer_text = self.format_time(elapsed, "H+") elif self.target_time: diff = int(self.target_time - now_time) if diff <= 0 and not self.counting_up: self.counting_up = True self.target_time = now_time diff = 0 if self.counting_up: elapsed = int(now_time - self.target_time) timer_text = self.format_time(elapsed, "T+") else: timer_text = self.format_time(diff, "T-") else: timer_text = "T-00:00:00" else: timer_text = self.text.cget("text") self.text.config(text=timer_text) write_countdown_html(self.mission_name, timer_text) # Update Go/No-Go every 10 seconds if now_time - self.last_gonogo_update > 0.1: # fetch_gonogo returns [Range, Weather, Vehicle] self.range_status, self.weather, self.vehicle = fetch_gonogo() self.range_label.config(text=f"RANGE: {self.range_status}", fg=get_status_color(self.range_status)) self.weather_label.config(text=f"WEATHER: {self.weather}", fg=get_status_color(self.weather)) self.vehicle_label.config(text=f"VEHICLE: {self.vehicle}", fg=get_status_color(self.vehicle)) self.gonogo_values = [self.range_status, self.weather, self.vehicle] write_gonogo_html(self.gonogo_values) self.last_gonogo_update = now_time self.root.after(200, self.update_clock) if __name__ == "__main__": # Show a small splash/loading GUI while we fetch initial data and write HTML files. def show_splash_and_start(): splash = tk.Tk() splash.title("RocketLaunchCountdown — Initialaization") splash.config(bg="black") splash.geometry("400x175") splash.attributes("-topmost", True) title = tk.Label(splash, text="RocketLaunchCountdown", fg="white", bg="black", font=("Arial", 20, "bold")) title.pack(pady=(10,0)) lbl = tk.Label(splash, text="Loading resources...", fg="white", bg="black", font=("Arial", 14)) lbl.pack(pady=(0,5)) info = tk.Label(splash, text="Fetching Go/No-Go and preparing HTML files.", fg="#ccc", bg="black", font=("Arial", 10)) info.pack() cont_btn = tk.Button(splash, text="Continue", state="disabled", width=12) cont_btn.pack(pady=8) # Footer footer_frame = tk.Frame(splash, bg="black") footer_frame.pack(side="bottom", pady=0, fill="x") footer_label = tk.Label( footer_frame, text="Made by HamsterSpaceNerd3000", # or whatever you want font=("Consolas", 12), fg="black", bg="white" ) footer_label.pack(fill="x") # Shared flag to indicate initialization complete init_state = { 'done': False, 'error': None } def init_worker(): try: # perform the same initial writes you had before gonogo = fetch_gonogo() write_countdown_html("Placeholder Mission", "T-00:00:00") write_gonogo_html(gonogo) init_state['done'] = True except Exception as e: init_state['error'] = str(e) init_state['done'] = True # Start background initialization threading.Thread(target=init_worker, daemon=True).start() def check_init(): if init_state['done']: if init_state['error']: info.config(text=f"Initialization error: {init_state['error']}") else: info.config(text="Ready. You may open browser sources now, then click Continue.") cont_btn.config(state="normal") splash.after(5000, on_continue) return splash.after(200, check_init) def on_continue(): splash.destroy() # now create the real main window root = tk.Tk() app = CountdownApp(root) root.mainloop() cont_btn.config(command=on_continue) # begin polling splash.after(100, check_init) splash.mainloop() show_splash_and_start()