diff --git a/main.py b/main.py index f27e987..f7247f3 100644 --- a/main.py +++ b/main.py @@ -9,6 +9,7 @@ import csv import io import os import json + try: from zoneinfo import ZoneInfo except Exception: @@ -37,49 +38,76 @@ DEFAULT_SETTINGS = { "weather_row": 3, "vehicle_row": 4, "column": 12, - "hide_mission_name": False, + "hide_mission_name": False } # default timezone: 'local' uses system local tz, otherwise an IANA name or 'UTC' -DEFAULT_SETTINGS.setdefault('timezone', 'local') +DEFAULT_SETTINGS.setdefault("timezone", "local") # Appearance defaults -DEFAULT_SETTINGS.setdefault('bg_color', '#000000') -DEFAULT_SETTINGS.setdefault('text_color', '#FFFFFF') -DEFAULT_SETTINGS.setdefault('font_family', 'Consolas') -DEFAULT_SETTINGS.setdefault('mission_font_px', 24) -DEFAULT_SETTINGS.setdefault('timer_font_px', 80) -DEFAULT_SETTINGS.setdefault('gn_bg_color', '#111111') -DEFAULT_SETTINGS.setdefault('gn_border_color', '#FFFFFF') -DEFAULT_SETTINGS.setdefault('gn_go_color', '#00FF00') -DEFAULT_SETTINGS.setdefault('gn_nogo_color', '#FF0000') -DEFAULT_SETTINGS.setdefault('gn_font_px', 20) -DEFAULT_SETTINGS.setdefault('appearance_mode', 'dark') +DEFAULT_SETTINGS.setdefault("bg_color", "#000000") +DEFAULT_SETTINGS.setdefault("text_color", "#FFFFFF") +DEFAULT_SETTINGS.setdefault("font_family", "Consolas") +DEFAULT_SETTINGS.setdefault("mission_font_px", 24) +DEFAULT_SETTINGS.setdefault("timer_font_px", 80) +DEFAULT_SETTINGS.setdefault("gn_bg_color", "#111111") +DEFAULT_SETTINGS.setdefault("gn_border_color", "#FFFFFF") +DEFAULT_SETTINGS.setdefault("gn_go_color", "#00FF00") +DEFAULT_SETTINGS.setdefault("gn_nogo_color", "#FF0000") +DEFAULT_SETTINGS.setdefault("gn_font_px", 20) +DEFAULT_SETTINGS.setdefault("appearance_mode", "dark") # HTML-only appearance defaults (these should not affect the Python GUI) -DEFAULT_SETTINGS.setdefault('html_bg_color', DEFAULT_SETTINGS.get('bg_color', '#000000')) -DEFAULT_SETTINGS.setdefault('html_text_color', DEFAULT_SETTINGS.get('text_color', '#FFFFFF')) -DEFAULT_SETTINGS.setdefault('html_font_family', DEFAULT_SETTINGS.get('font_family', 'Consolas')) -DEFAULT_SETTINGS.setdefault('html_mission_font_px', DEFAULT_SETTINGS.get('mission_font_px', 24)) -DEFAULT_SETTINGS.setdefault('html_timer_font_px', DEFAULT_SETTINGS.get('timer_font_px', 80)) -DEFAULT_SETTINGS.setdefault('html_gn_bg_color', DEFAULT_SETTINGS.get('gn_bg_color', '#111111')) -DEFAULT_SETTINGS.setdefault('html_gn_border_color', DEFAULT_SETTINGS.get('gn_border_color', '#FFFFFF')) -DEFAULT_SETTINGS.setdefault('html_gn_go_color', DEFAULT_SETTINGS.get('gn_go_color', '#00FF00')) -DEFAULT_SETTINGS.setdefault('html_gn_nogo_color', DEFAULT_SETTINGS.get('gn_nogo_color', '#FF0000')) -DEFAULT_SETTINGS.setdefault('html_gn_font_px', DEFAULT_SETTINGS.get('gn_font_px', 20)) +DEFAULT_SETTINGS.setdefault( + "html_bg_color", DEFAULT_SETTINGS.get("bg_color", "#000000") +) +DEFAULT_SETTINGS.setdefault( + "html_text_color", DEFAULT_SETTINGS.get("text_color", "#FFFFFF") +) +DEFAULT_SETTINGS.setdefault( + "html_font_family", DEFAULT_SETTINGS.get("font_family", "Consolas") +) +DEFAULT_SETTINGS.setdefault( + "html_mission_font_px", DEFAULT_SETTINGS.get("mission_font_px", 24) +) +DEFAULT_SETTINGS.setdefault( + "html_timer_font_px", DEFAULT_SETTINGS.get("timer_font_px", 80) +) +DEFAULT_SETTINGS.setdefault( + "html_gn_bg_color", DEFAULT_SETTINGS.get("gn_bg_color", "#111111") +) +DEFAULT_SETTINGS.setdefault( + "html_gn_border_color", DEFAULT_SETTINGS.get("gn_border_color", "#FFFFFF") +) +DEFAULT_SETTINGS.setdefault( + "html_gn_go_color", DEFAULT_SETTINGS.get("gn_go_color", "#00FF00") +) +DEFAULT_SETTINGS.setdefault( + "html_gn_nogo_color", DEFAULT_SETTINGS.get("gn_nogo_color", "#FF0000") +) +DEFAULT_SETTINGS.setdefault("html_gn_font_px", DEFAULT_SETTINGS.get("gn_font_px", 20)) # Auto-hold times: list of seconds before T at which timer should automatically enter hold -DEFAULT_SETTINGS.setdefault('auto_hold_times', []) +DEFAULT_SETTINGS.setdefault("auto_hold_times", []) # A small list of common timezone choices. TIMEZONE_CHOICES = [ - 'local', 'UTC', 'US/Eastern', 'US/Central', 'US/Mountain', 'US/Pacific', - 'Europe/London', 'Europe/Paris', 'Asia/Tokyo', 'Australia/Sydney' + "local", + "UTC", + "US/Eastern", + "US/Central", + "US/Mountain", + "US/Pacific", + "Europe/London", + "Europe/Paris", + "Asia/Tokyo", + "Australia/Sydney", ] + def load_settings(): try: if os.path.exists(SETTINGS_FILE): - with open(SETTINGS_FILE, 'r', encoding='utf-8') as fh: + with open(SETTINGS_FILE, "r", encoding="utf-8") as fh: return json.load(fh) except Exception: pass @@ -87,9 +115,10 @@ def load_settings(): save_settings(DEFAULT_SETTINGS) return DEFAULT_SETTINGS.copy() + def save_settings(s): try: - with open(SETTINGS_FILE, 'w', encoding='utf-8') as fh: + with open(SETTINGS_FILE, "w", encoding="utf-8") as fh: json.dump(s, fh, indent=2) except Exception: pass @@ -101,23 +130,27 @@ def save_settings(s): 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') + mode = settings.get("mode", "spreadsheet") # If manual mode, read values from a runtime stash (set by the GUI buttons) - if mode == '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')] + 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'] + 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] + 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() @@ -125,7 +158,7 @@ def fetch_gonogo(): data = list(reader) gonogo = [] for r in rows: - val = 'N/A' + val = "N/A" if 0 <= r < len(data) and len(data[r]) > col: val = data[r][col] gonogo.append(val.strip().upper()) @@ -141,30 +174,31 @@ def fetch_gonogo(): def get_status_color(status): """Return color name for a Go/No-Go status string.""" try: - s = str(status or '').strip().upper() + s = str(status or "").strip().upper() # normalize to letters only so variants like 'NO GO', 'NO-GO', 'NOGO' match - norm = re.sub(r'[^A-Z]', '', s) - if norm == 'GO': - return 'green' - if norm == 'NOGO': - return 'red' + norm = re.sub(r"[^A-Z]", "", s) + if norm == "GO": + return "green" + if norm == "NOGO": + return "red" # fallback: treat unknown/empty as white - return 'white' + return "white" except Exception: return "white" def format_status_display(status): try: - s = str(status or '').strip().upper() - norm = re.sub(r'[^A-Z]', '', s) - if norm == 'GO': - return 'GO' - if norm == 'NOGO': - return 'NO-GO' + s = str(status or "").strip().upper() + norm = re.sub(r"[^A-Z]", "", s) + if norm == "GO": + return "GO" + if norm == "NOGO": + return "NO-GO" return s except Exception: - return str(status or '') + return str(status or "") + # ------------------------- # Write Countdown HTML @@ -172,15 +206,17 @@ def format_status_display(status): def write_countdown_html(mission_name, timer_text): s = load_settings() # Prefer HTML-specific settings; fall back to GUI appearance settings for backwards compatibility - bg = s.get('html_bg_color', s.get('bg_color', '#000000')) - text = s.get('html_text_color', s.get('text_color', '#FFFFFF')) - font = s.get('html_font_family', s.get('font_family', 'Consolas, monospace')) - mission_px = int(s.get('html_mission_font_px', s.get('mission_font_px', 48))) - timer_px = int(s.get('html_timer_font_px', s.get('timer_font_px', 120))) - + bg = s.get("html_bg_color", s.get("bg_color", "#000000")) + text = s.get("html_text_color", s.get("countdown_text_color", "#FFFFFF")) + font = s.get("html_font_family", s.get("font_family", "Consolas, monospace")) + mission_px = int(s.get("html_mission_font_px", s.get("mission_font_px", 48))) + timer_px = int(s.get("html_timer_font_px", s.get("timer_font_px", 120))) + # Mission name hidden setting hide_mission_name = s.get("hide_mission_name", False) - mission_div_hidden = f'
{mission_name}
' if not hide_mission_name else '' + mission_div_hidden = ( + f'
{mission_name}
' if not hide_mission_name else "" + ) html = f""" @@ -212,6 +248,7 @@ setTimeout(() => location.reload(), 1000); with open(COUNTDOWN_HTML, "w", encoding="utf-8") as f: f.write(html) + # ------------------------- # Write Go/No-Go HTML # ------------------------- @@ -220,21 +257,21 @@ def write_gonogo_html(gonogo_values=None): gonogo_values = ["N/A", "N/A", "N/A"] s = load_settings() # Prefer HTML-specific settings; fall back to GUI appearance settings for backwards compatibility - bg = s.get('html_bg_color', s.get('bg_color', '#000000')) - text = s.get('html_text_color', s.get('text_color', '#FFFFFF')) - font = s.get('html_font_family', s.get('font_family', 'Consolas, monospace')) - gn_bg = s.get('html_gn_bg_color', s.get('gn_bg_color', '#111111')) - gn_border = s.get('html_gn_border_color', s.get('gn_border_color', '#FFFFFF')) - gn_go = s.get('html_gn_go_color', s.get('gn_go_color', '#00FF00')) - gn_nogo = s.get('html_gn_nogo_color', s.get('gn_nogo_color', '#FF0000')) - gn_px = int(s.get('html_gn_font_px', s.get('gn_font_px', 28))) + bg = s.get("html_bg_color", s.get("bg_color", "#000000")) + text = s.get("html_text_color", s.get("text_color", "#FFFFFF")) + font = s.get("html_font_family", s.get("font_family", "Consolas, monospace")) + gn_bg = s.get("html_gn_bg_color", s.get("gn_bg_color", "#111111")) + gn_border = s.get("html_gn_border_color", s.get("gn_border_color", "#FFFFFF")) + gn_go = s.get("html_gn_go_color", s.get("gn_go_color", "#00FF00")) + gn_nogo = s.get("html_gn_nogo_color", s.get("gn_nogo_color", "#FF0000")) + gn_px = int(s.get("html_gn_font_px", s.get("gn_font_px", 28))) # normalize and format display values so variants like 'NO GO' become 'NO-GO' disp0 = format_status_display(gonogo_values[0]) disp1 = format_status_display(gonogo_values[1]) disp2 = format_status_display(gonogo_values[2]) - n0 = re.sub(r'[^A-Z]', '', (str(gonogo_values[0] or '')).strip().upper()) - n1 = re.sub(r'[^A-Z]', '', (str(gonogo_values[1] or '')).strip().upper()) - n2 = re.sub(r'[^A-Z]', '', (str(gonogo_values[2] or '')).strip().upper()) + n0 = re.sub(r"[^A-Z]", "", (str(gonogo_values[0] or "")).strip().upper()) + n1 = re.sub(r"[^A-Z]", "", (str(gonogo_values[1] or "")).strip().upper()) + n2 = re.sub(r"[^A-Z]", "", (str(gonogo_values[2] or "")).strip().upper()) html = f""" @@ -280,6 +317,7 @@ setTimeout(() => location.reload(), 5000); with open(GONOGO_HTML, "w", encoding="utf-8") as f: f.write(html) + # ------------------------- # Countdown App # ------------------------- @@ -305,19 +343,42 @@ class CountdownApp: self.last_gonogo_update = time.time() # track which auto-holds we've already triggered for the current run self._auto_hold_triggered = set() + # count mode + self.count_mode = tk.StringVar(value="T-") # Title - self.titletext = tk.Label(root, text=f"RocketLaunchCountdown {appVersion}", font=("Consolas", 24), fg="white", bg="black") + self.titletext = tk.Label( + root, + text=f"RocketLaunchCountdown {appVersion}", + 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") + frame_count = tk.Frame(root, bg="black") + frame_count.pack(pady=(0, 5)) + + self.text = tk.Label(frame_count, text="T-00:00:00", font=("Consolas", 80, "bold"), fg="white", bg="black") + self.text.pack(side="left", padx=(0, 10)) + + # Static label above the button + self.mode_label = tk.Label(frame_count, text="T-/L- Toggle", font=("Consolas", 14), fg="white", bg="black") + self.mode_label.pack(side="top", pady=(0, 5)) + + # Toggle button below the label + self.toggle_mode_btn = tk.Button(frame_count, text="↺", command=self.toggle_count_mode, font=("Arial", 14), width=3) + self.toggle_mode_btn.pack(side="top") + 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") + 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") @@ -336,7 +397,7 @@ class CountdownApp: fg="white", bg="black", selectcolor="black", # makes the dot visible - command=self.update_inputs + command=self.update_inputs, ) self.radio_duration.pack(side="left", padx=5) @@ -348,7 +409,7 @@ class CountdownApp: fg="white", bg="black", selectcolor="black", # makes the dot visible - command=self.update_inputs + command=self.update_inputs, ) self.radio_clock.pack(side="left", padx=5) @@ -372,37 +433,74 @@ class CountdownApp: def open_autohold_dialog(): dlg = tk.Toplevel(self.root) dlg.transient(self.root) - dlg.title('Set Auto-hold') - dlg.geometry('320x110') + dlg.title("Set Auto-hold") + dlg.geometry("320x110") # theme according to appearance mode ssettings = load_settings() - mode_local = ssettings.get('appearance_mode', DEFAULT_SETTINGS.get('appearance_mode', 'dark')) - if mode_local == 'dark': - dlg_bg = '#000000'; dlg_fg = '#FFFFFF'; entry_bg = '#222222'; btn_bg = '#FFFFFF'; btn_fg = '#000000' + mode_local = ssettings.get( + "appearance_mode", DEFAULT_SETTINGS.get("appearance_mode", "dark") + ) + if mode_local == "dark": + dlg_bg = "#000000" + dlg_fg = "#FFFFFF" + entry_bg = "#222222" + btn_bg = "#FFFFFF" + btn_fg = "#000000" else: - dlg_bg = '#FFFFFF'; dlg_fg = '#000000'; entry_bg = '#b4b4b4'; btn_bg = '#000000'; btn_fg = '#FFFFFF' + dlg_bg = "#FFFFFF" + dlg_fg = "#000000" + entry_bg = "#b4b4b4" + btn_bg = "#000000" + btn_fg = "#FFFFFF" dlg.config(bg=dlg_bg) - tk.Label(dlg, text='Auto-hold time (H M S):', fg=dlg_fg, bg=dlg_bg).pack(pady=(6,0)) + tk.Label(dlg, text="Auto-hold time (H M S):", fg=dlg_fg, bg=dlg_bg).pack( + pady=(6, 0) + ) box = tk.Frame(dlg, bg=dlg_bg) box.pack(pady=6) - h_entry = tk.Entry(box, width=3, font=('Arial', 12), bg=entry_bg, fg=dlg_fg, insertbackground=dlg_fg) - m_entry = tk.Entry(box, width=3, font=('Arial', 12), bg=entry_bg, fg=dlg_fg, insertbackground=dlg_fg) - s_entry = tk.Entry(box, width=3, font=('Arial', 12), bg=entry_bg, fg=dlg_fg, insertbackground=dlg_fg) - h_entry.pack(side='left', padx=4) - tk.Label(box, text='H', fg=dlg_fg, bg=dlg_bg).pack(side='left') - m_entry.pack(side='left', padx=4) - tk.Label(box, text='M', fg=dlg_fg, bg=dlg_bg).pack(side='left') - s_entry.pack(side='left', padx=4) - tk.Label(box, text='S', fg=dlg_fg, bg=dlg_bg).pack(side='left') + h_entry = tk.Entry( + box, + width=3, + font=("Arial", 12), + bg=entry_bg, + fg=dlg_fg, + insertbackground=dlg_fg, + ) + m_entry = tk.Entry( + box, + width=3, + font=("Arial", 12), + bg=entry_bg, + fg=dlg_fg, + insertbackground=dlg_fg, + ) + s_entry = tk.Entry( + box, + width=3, + font=("Arial", 12), + bg=entry_bg, + fg=dlg_fg, + insertbackground=dlg_fg, + ) + h_entry.pack(side="left", padx=4) + tk.Label(box, text="H", fg=dlg_fg, bg=dlg_bg).pack(side="left") + m_entry.pack(side="left", padx=4) + tk.Label(box, text="M", fg=dlg_fg, bg=dlg_bg).pack(side="left") + s_entry.pack(side="left", padx=4) + tk.Label(box, text="S", fg=dlg_fg, bg=dlg_bg).pack(side="left") # populate with first configured value if present try: ssettings = load_settings() - a = (ssettings.get('auto_hold_times') or []) + a = ssettings.get("auto_hold_times") or [] if a: secs = int(a[0]) - hh = secs // 3600; mm = (secs % 3600) // 60; ss = secs % 60 - h_entry.insert(0, str(hh)); m_entry.insert(0, str(mm)); s_entry.insert(0, str(ss)) + hh = secs // 3600 + mm = (secs % 3600) // 60 + ss = secs % 60 + h_entry.insert(0, str(hh)) + m_entry.insert(0, str(mm)) + s_entry.insert(0, str(ss)) except Exception: pass @@ -411,13 +509,13 @@ class CountdownApp: hh = int(h_entry.get() or 0) mm = int(m_entry.get() or 0) ss = int(s_entry.get() or 0) - total = max(0, hh*3600 + mm*60 + ss) + total = max(0, hh * 3600 + mm * 60 + ss) except Exception: total = 0 try: ssettings = load_settings() # replace with single auto-hold time (list with one element) - ssettings['auto_hold_times'] = [int(total)] if total > 0 else [] + ssettings["auto_hold_times"] = [int(total)] if total > 0 else [] save_settings(ssettings) # update runtime set so this run will consider the new value self._auto_hold_triggered = set() @@ -426,9 +524,25 @@ class CountdownApp: dlg.destroy() btnf = tk.Frame(dlg, bg=dlg_bg) - btnf.pack(fill='x', pady=6) - tk.Button(btnf, text='Save', command=do_save, width=10, bg=btn_bg, fg=btn_fg, activebackground='#444').pack(side='right', padx=6) - tk.Button(btnf, text='Cancel', command=dlg.destroy, width=10, bg=btn_bg, fg=btn_fg, activebackground='#444').pack(side='right') + btnf.pack(fill="x", pady=6) + tk.Button( + btnf, + text="Save", + command=do_save, + width=10, + bg=btn_bg, + fg=btn_fg, + activebackground="#444", + ).pack(side="right", padx=6) + tk.Button( + btnf, + text="Cancel", + command=dlg.destroy, + width=10, + bg=btn_bg, + fg=btn_fg, + activebackground="#444", + ).pack(side="right") # recursively theme dialog to ensure consistency try: @@ -436,21 +550,51 @@ class CountdownApp: except Exception: pass - tk.Button(frame_duration, text='Auto-hold...', command=open_autohold_dialog, fg='white', bg='#333', width=12).pack(side='left', padx=8) + tk.Button( + frame_duration, + text="Auto-hold...", + command=open_autohold_dialog, + fg="white", + bg="#333", + width=12, + ).pack(side="left", padx=8) # Clock time input (separate HH, MM, SS boxes) frame_clock = tk.Frame(root, bg="black") frame_clock.pack(pady=5) - tk.Label(frame_clock, text="Clock (HH:MM:SS)", fg="white", bg="black").pack(side="left") - self.clock_hours_entry = tk.Entry(frame_clock, width=3, font=("Arial", 18), fg='white', bg='#111', insertbackground='white') + tk.Label(frame_clock, text="Clock (HH:MM:SS)", fg="white", bg="black").pack( + side="left" + ) + self.clock_hours_entry = tk.Entry( + frame_clock, + width=3, + font=("Arial", 18), + fg="white", + bg="#111", + insertbackground="white", + ) self.clock_hours_entry.insert(0, "14") self.clock_hours_entry.pack(side="left", padx=2) tk.Label(frame_clock, text=":", fg="white", bg="black").pack(side="left") - self.clock_minutes_entry = tk.Entry(frame_clock, width=3, font=("Arial", 18), fg='white', bg='#111', insertbackground='white') + self.clock_minutes_entry = tk.Entry( + frame_clock, + width=3, + font=("Arial", 18), + fg="white", + bg="#111", + insertbackground="white", + ) self.clock_minutes_entry.insert(0, "00") self.clock_minutes_entry.pack(side="left", padx=2) tk.Label(frame_clock, text=":", fg="white", bg="black").pack(side="left") - self.clock_seconds_entry = tk.Entry(frame_clock, width=3, font=("Arial", 18), fg='white', bg='#111', insertbackground='white') + self.clock_seconds_entry = tk.Entry( + frame_clock, + width=3, + font=("Arial", 18), + fg="white", + bg="#111", + insertbackground="white", + ) self.clock_seconds_entry.insert(0, "00") self.clock_seconds_entry.pack(side="left", padx=2) @@ -458,24 +602,44 @@ class CountdownApp: 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 = 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 = 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 = 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 = 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 = 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) - self.settings_btn = tk.Button(frame_buttons, text="Settings", command=self.show_settings_window, font=("Arial", 14), width=10) + self.settings_btn = tk.Button( + frame_buttons, + text="Settings", + command=self.show_settings_window, + font=("Arial", 14), + width=10, + ) self.settings_btn.grid(row=0, column=4, padx=6) # Note: gonogo mode switching remains in Settings; manual buttons appear when mode == 'buttons' @@ -485,12 +649,24 @@ class CountdownApp: 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')) + 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) @@ -500,11 +676,17 @@ class CountdownApp: 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 = 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 = 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 = tk.Label( + frame_gn, text="VEHICLE: N/A", font=("Consolas", 20), fg="white", bg="black" + ) self.vehicle_label.pack() # Footer @@ -516,7 +698,7 @@ class CountdownApp: text="Made by HamsterSpaceNerd3000", # or whatever you want font=("Consolas", 12), fg="black", - bg="white" + bg="white", ) self.footer_label.pack(fill="x") self.update_inputs() @@ -540,149 +722,228 @@ class CountdownApp: win.geometry("560x275") # apply current appearance mode so the settings window matches the main UI s_local = load_settings() - mode_local = s_local.get('appearance_mode', 'dark') - if mode_local == 'dark': - win_bg = '#000000'; win_text = '#FFFFFF'; btn_bg = '#FFFFFF'; btn_fg = '#000000' + mode_local = s_local.get("appearance_mode", "dark") + if mode_local == "dark": + win_bg = "#000000" + win_text = "#FFFFFF" + btn_bg = "#FFFFFF" + btn_fg = "#000000" else: - win_bg = '#FFFFFF'; win_text = '#000000'; btn_bg = '#000000'; btn_fg = '#FFFFFF' + win_bg = "#FFFFFF" + win_text = "#000000" + btn_bg = "#000000" + btn_fg = "#FFFFFF" win.config(bg=win_bg) # set per-window widget defaults so nested widgets inherit the chosen theme try: - win.option_add('*Foreground', win_text) - win.option_add('*Background', win_bg) + win.option_add("*Foreground", win_text) + win.option_add("*Background", win_bg) # entry specific defaults - win.option_add('*Entry.Background', '#222' if mode_local == 'dark' else '#b4b4b4') - win.option_add('*Entry.Foreground', win_text if mode_local == 'dark' else '#000000') + win.option_add( + "*Entry.Background", "#222" if mode_local == "dark" else "#b4b4b4" + ) + win.option_add( + "*Entry.Foreground", win_text if mode_local == "dark" else "#000000" + ) except Exception: pass # keep track of this Toplevel so other dialogs can close it if needed try: self.settings_win = win + def _clear_settings_ref(evt=None): try: self.settings_win = None except Exception: pass - win.bind('', _clear_settings_ref) + + win.bind("", _clear_settings_ref) except Exception: pass # Mode selection frame_mode = tk.Frame(win) frame_mode.config(bg=win_bg) - frame_mode.pack(fill='x', pady=8, padx=8) - tk.Label(frame_mode, text="Mode:", fg=win_text, bg=win_bg).pack(side='left') - mode_var = tk.StringVar(value=settings.get('mode', 'spreadsheet')) - tk.Radiobutton(frame_mode, text='Spreadsheet', variable=mode_var, value='spreadsheet', fg=win_text, bg=win_bg, selectcolor=win_bg).pack(side='left', padx=8) - tk.Radiobutton(frame_mode, text='Buttons (manual)', variable=mode_var, value='buttons', fg=win_text, bg=win_bg, selectcolor=win_bg).pack(side='left', padx=8) + frame_mode.pack(fill="x", pady=8, padx=8) + tk.Label(frame_mode, text="Mode:", fg=win_text, bg=win_bg).pack(side="left") + mode_var = tk.StringVar(value=settings.get("mode", "spreadsheet")) + tk.Radiobutton( + frame_mode, + text="Spreadsheet", + variable=mode_var, + value="spreadsheet", + fg=win_text, + bg=win_bg, + selectcolor=win_bg, + ).pack(side="left", padx=8) + tk.Radiobutton( + frame_mode, + text="Buttons (manual)", + variable=mode_var, + value="buttons", + fg=win_text, + bg=win_bg, + selectcolor=win_bg, + ).pack(side="left", padx=8) # Spreadsheet config - frame_sheet = tk.LabelFrame(win, text='Spreadsheet configuration', fg=win_text, bg=win_bg) + frame_sheet = tk.LabelFrame( + win, text="Spreadsheet configuration", fg=win_text, bg=win_bg + ) frame_sheet.config(bg=win_bg) - frame_sheet.pack(fill='x', padx=8, pady=6) - tk.Label(frame_sheet, text='Sheet link (CSV export):', fg=win_text, bg=win_bg).pack(anchor='w') + frame_sheet.pack(fill="x", padx=8, pady=6) + tk.Label( + frame_sheet, text="Sheet link (CSV export):", fg=win_text, bg=win_bg + ).pack(anchor="w") # entry background chosen to contrast with window background - sheet_entry_bg = '#222' if mode_local == 'dark' else '#b4b4b4' - sheet_entry_fg = win_text if mode_local == 'dark' else '#000000' - sheet_entry = tk.Entry(frame_sheet, width=80, fg=sheet_entry_fg, bg=sheet_entry_bg, insertbackground=sheet_entry_fg) - sheet_entry.pack(fill='x', padx=6, pady=4) - sheet_entry.insert(0, settings.get('sheet_link', SHEET_LINK)) + sheet_entry_bg = "#222" if mode_local == "dark" else "#b4b4b4" + sheet_entry_fg = win_text if mode_local == "dark" else "#000000" + sheet_entry = tk.Entry( + frame_sheet, + width=80, + fg=sheet_entry_fg, + bg=sheet_entry_bg, + insertbackground=sheet_entry_fg, + ) + 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=win_bg) - cell_frame.pack(fill='x', padx=6, pady=2) - tk.Label(cell_frame, text='Range cell (e.g. L3):', fg=win_text, bg=win_bg).grid(row=0, column=0) - range_cell_bg = '#222' if mode_local == 'dark' else '#b4b4b4' - range_cell_fg = win_text if mode_local == 'dark' else '#000000' - range_cell = tk.Entry(cell_frame, width=8, fg=range_cell_fg, bg=range_cell_bg, insertbackground=range_cell_fg) + cell_frame.pack(fill="x", padx=6, pady=2) + tk.Label(cell_frame, text="Range cell (e.g. L3):", fg=win_text, bg=win_bg).grid( + row=0, column=0 + ) + range_cell_bg = "#222" if mode_local == "dark" else "#b4b4b4" + range_cell_fg = win_text if mode_local == "dark" else "#000000" + range_cell = tk.Entry( + cell_frame, + width=8, + fg=range_cell_fg, + bg=range_cell_bg, + insertbackground=range_cell_fg, + ) 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')) + 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']) + 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 = '' + s = "" while n > 0: n, r = divmod(n - 1, 26) - s = chr(ord('A') + r) + s + 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=win_text, bg=win_bg).grid(row=0, column=2) - weather_cell = tk.Entry(cell_frame, width=8, fg=range_cell_fg, bg=range_cell_bg, insertbackground=range_cell_fg) + tk.Label( + cell_frame, text="Weather cell (e.g. L4):", fg=win_text, bg=win_bg + ).grid(row=0, column=2) + weather_cell = tk.Entry( + cell_frame, + width=8, + fg=range_cell_fg, + bg=range_cell_bg, + insertbackground=range_cell_fg, + ) weather_cell.grid(row=0, column=3, padx=4) try: - if 'weather_cell' in settings: - weather_cell.insert(0, settings.get('weather_cell')) + 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']) + col = settings.get("column", DEFAULT_SETTINGS["column"]) + row = settings.get("weather_row", DEFAULT_SETTINGS["weather_row"]) + def col_to_letters(n): - s = '' + s = "" while n > 0: n, r = divmod(n - 1, 26) - s = chr(ord('A') + r) + s + 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=win_text, bg=win_bg).grid(row=0, column=4) - vehicle_cell = tk.Entry(cell_frame, width=8, fg=range_cell_fg, bg=range_cell_bg, insertbackground=range_cell_fg) + tk.Label( + cell_frame, text="Vehicle cell (e.g. L5):", fg=win_text, bg=win_bg + ).grid(row=0, column=4) + vehicle_cell = tk.Entry( + cell_frame, + width=8, + fg=range_cell_fg, + bg=range_cell_bg, + insertbackground=range_cell_fg, + ) vehicle_cell.grid(row=0, column=5, padx=4) try: - if 'vehicle_cell' in settings: - vehicle_cell.insert(0, settings.get('vehicle_cell')) + 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']) + col = settings.get("column", DEFAULT_SETTINGS["column"]) + row = settings.get("vehicle_row", DEFAULT_SETTINGS["vehicle_row"]) + def col_to_letters(n): - s = '' + s = "" while n > 0: n, r = divmod(n - 1, 26) - s = chr(ord('A') + r) + s + 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=win_text, bg=win_bg) + frame_buttons_cfg = tk.LabelFrame( + win, text="Manual Go/No-Go (Buttons mode)", fg=win_text, bg=win_bg + ) frame_buttons_cfg.config(bg=win_bg) - frame_buttons_cfg.pack(fill='x', padx=8, pady=6) + frame_buttons_cfg.pack(fill="x", padx=8, pady=6) # (Auto-hold configuration removed from Settings — managed from main UI) # Appearance settings are in a separate window frame_appearance_btn = tk.Frame(win, bg=win_bg) - frame_appearance_btn.pack(fill='x', padx=8, pady=6) - tk.Button(frame_appearance_btn, text='Appearance...', command=lambda: self.show_appearance_window(), fg=btn_fg, bg=btn_bg, activebackground='#444').pack(side='left') + frame_appearance_btn.pack(fill="x", padx=8, pady=6) + tk.Button( + frame_appearance_btn, + text="Appearance...", + command=lambda: self.show_appearance_window(), + fg=btn_fg, + bg=btn_bg, + activebackground="#444", + ).pack(side="left") # Timezone selector tz_frame = tk.Frame(frame_sheet, bg=win_bg) - tz_frame.pack(fill='x', padx=6, pady=4) - tk.Label(tz_frame, text='Timezone:', fg=win_text, bg=win_bg).pack(side='left') - tz_var = tk.StringVar(value=settings.get('timezone', DEFAULT_SETTINGS.get('timezone', 'local'))) + tz_frame.pack(fill="x", padx=6, pady=4) + tk.Label(tz_frame, text="Timezone:", fg=win_text, bg=win_bg).pack(side="left") + tz_var = tk.StringVar( + value=settings.get("timezone", DEFAULT_SETTINGS.get("timezone", "local")) + ) # OptionMenu with a few choices, but user may edit the text to any IANA name tz_menu = tk.OptionMenu(tz_frame, tz_var, *TIMEZONE_CHOICES) - tz_menu.config(fg=win_text, bg=range_cell_bg, activebackground='#333') - tz_menu.pack(side='left', padx=6) + tz_menu.config(fg=win_text, bg=range_cell_bg, activebackground="#333") + tz_menu.pack(side="left", padx=6) def set_manual(val_type, val): # store on fetch_gonogo func for now - if val_type == 'range': + if val_type == "range": fetch_gonogo.manual_range = val - elif val_type == 'weather': + elif val_type == "weather": fetch_gonogo.manual_weather = val - elif val_type == 'vehicle': + elif val_type == "vehicle": fetch_gonogo.manual_vehicle = val # helper to set manual and update UI from main app @@ -691,19 +952,28 @@ class CountdownApp: # 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])) + 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() + s = (cell_str or "").strip().upper() if not s: return None, None # split letters and digits - letters = '' - digits = '' + letters = "" + digits = "" for ch in s: if ch.isalpha(): letters += ch @@ -714,7 +984,7 @@ class CountdownApp: # convert letters to number col = 0 for ch in letters: - col = col * 26 + (ord(ch) - ord('A') + 1) + col = col * 26 + (ord(ch) - ord("A") + 1) row = int(digits) return row, col @@ -725,48 +995,52 @@ class CountdownApp: v_row, v_col = cell_to_rc(vehicle_cell.get()) # fallbacks if r_row is None: - r_row = DEFAULT_SETTINGS['range_row'] + r_row = DEFAULT_SETTINGS["range_row"] if w_row is None: - w_row = DEFAULT_SETTINGS['weather_row'] + w_row = DEFAULT_SETTINGS["weather_row"] if v_row is None: - v_row = DEFAULT_SETTINGS['vehicle_row'] + 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'] + 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), + "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(), + "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), - 'timezone': tz_var.get(), + "manual_range": getattr(fetch_gonogo, "manual_range", None), + "manual_weather": getattr(fetch_gonogo, "manual_weather", None), + "manual_vehicle": getattr(fetch_gonogo, "manual_vehicle", None), + "timezone": tz_var.get(), # preserve appearance settings (edited in Appearance window) - 'bg_color': settings.get('bg_color', '#000000'), - 'text_color': settings.get('text_color', '#FFFFFF'), - 'gn_bg_color': settings.get('gn_bg_color', '#111111'), - 'gn_border_color': settings.get('gn_border_color', '#FFFFFF'), - 'gn_go_color': settings.get('gn_go_color', '#00FF00'), - 'gn_nogo_color': settings.get('gn_nogo_color', '#FF0000'), - 'font_family': settings.get('font_family', 'Consolas'), - 'mission_font_px': int(settings.get('mission_font_px', 48)), - 'timer_font_px': int(settings.get('timer_font_px', 120)), - 'gn_font_px': int(settings.get('gn_font_px', 28)) + "bg_color": settings.get("bg_color", "#000000"), + "text_color": settings.get("text_color", "#FFFFFF"), + "gn_bg_color": settings.get("gn_bg_color", "#111111"), + "gn_border_color": settings.get("gn_border_color", "#FFFFFF"), + "gn_go_color": settings.get("gn_go_color", "#00FF00"), + "gn_nogo_color": settings.get("gn_nogo_color", "#FF0000"), + "font_family": settings.get("font_family", "Consolas"), + "mission_font_px": int(settings.get("mission_font_px", 48)), + "timer_font_px": int(settings.get("timer_font_px", 120)), + "gn_font_px": int(settings.get("gn_font_px", 28)), } # Auto-hold editing removed from Settings window; keep existing settings value - new_settings['auto_hold_times'] = settings.get('auto_hold_times', []) + new_settings["auto_hold_times"] = settings.get("auto_hold_times", []) # preserve the appearance_mode so saving Settings doesn't accidentally remove it try: - new_settings['appearance_mode'] = settings.get('appearance_mode', DEFAULT_SETTINGS.get('appearance_mode', 'dark')) + new_settings["appearance_mode"] = settings.get( + "appearance_mode", DEFAULT_SETTINGS.get("appearance_mode", "dark") + ) except Exception: - new_settings['appearance_mode'] = DEFAULT_SETTINGS.get('appearance_mode', 'dark') + new_settings["appearance_mode"] = DEFAULT_SETTINGS.get( + "appearance_mode", "dark" + ) save_settings(new_settings) # update immediately self.gonogo_values = fetch_gonogo() @@ -780,16 +1054,29 @@ class CountdownApp: win.destroy() btn_frame = tk.Frame(win, bg=win_bg) - btn_frame.pack(fill='x', pady=8) - tk.Button(btn_frame, text='Save', command=on_save, fg=btn_fg, bg=btn_bg, activebackground='#444').pack(side='right', padx=8) - tk.Button(btn_frame, text='Cancel', command=on_cancel, fg=btn_fg, bg=btn_bg, activebackground='#444').pack(side='right') + btn_frame.pack(fill="x", pady=8) + tk.Button( + btn_frame, + text="Save", + command=on_save, + fg=btn_fg, + bg=btn_bg, + activebackground="#444", + ).pack(side="right", padx=8) + tk.Button( + btn_frame, + text="Cancel", + command=on_cancel, + fg=btn_fg, + bg=btn_bg, + activebackground="#444", + ).pack(side="right") # ensure the new toplevel gets recursively themed like the main window try: self._theme_recursive(win, win_bg, win_text, btn_bg, btn_fg) except Exception: pass - # ---------------------------- # Update input visibility based on mode # ---------------------------- @@ -814,36 +1101,51 @@ class CountdownApp: # ---------------------------- def set_manual(self, which, val): # normalize - v = (val or '').strip().upper() - if which == 'range': + v = (val or "").strip().upper() + if which == "range": fetch_gonogo.manual_range = v - elif which == 'weather': + elif which == "weather": fetch_gonogo.manual_weather = v - elif which == 'vehicle': + 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])) + 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')) + 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') + mode = s.get("mode", "spreadsheet") + visible = mode == "buttons" # show or hide manual frame if visible: self.manual_frame.pack(pady=6) @@ -854,73 +1156,93 @@ class CountdownApp: """Apply appearance-related settings to the running Tk UI.""" s = load_settings() # If an appearance_mode preset is selected, override specific settings with the preset - mode = s.get('appearance_mode', None) - if mode == 'dark': - s.update({ - 'bg_color': '#000000', 'text_color': '#FFFFFF', 'gn_bg_color': '#111111', - 'gn_border_color': '#FFFFFF', 'gn_go_color': '#00FF00', 'gn_nogo_color': '#FF0000', - 'font_family': 'Consolas', 'mission_font_px': 44, 'timer_font_px': 80, 'gn_font_px': 24 - }) - elif mode == 'light': - s.update({ - 'bg_color': '#FFFFFF', 'text_color': '#000000', 'gn_bg_color': '#EEEEEE', - 'gn_border_color': '#333333', 'gn_go_color': '#008800', 'gn_nogo_color': '#AA0000', - 'font_family': 'Consolas', 'mission_font_px': 44, 'timer_font_px': 80, 'gn_font_px': 24 - }) - bg = s.get('bg_color', '#000000') - text = s.get('text_color', '#FFFFFF') - font_family = s.get('font_family', 'Consolas') - timer_px = int(s.get('timer_font_px', 100)) - mission_px = int(s.get('mission_font_px', 48)) - gn_px = int(s.get('gn_font_px', 24)) - gn_bg = s.get('gn_bg_color', '#111111') - gn_border = s.get('gn_border_color', '#FFFFFF') - gn_go = s.get('gn_go_color', '#00FF00') - gn_nogo = s.get('gn_nogo_color', '#FF0000') + mode = s.get("appearance_mode", None) + if mode == "dark": + s.update( + { + "bg_color": "#000000", + "text_color": "#FFFFFF", + "countdown_text_color": "#FFFFFF", + "font_family": "Consolas", + "mission_font_px": 44, + "timer_font_px": 80, + "gn_font_px": 24, + } + ) + elif mode == "light": + s.update( + { + "bg_color": "#FFFFFF", + "text_color": "#000000", + "countdown_text_color": "#FFFFFF", + "font_family": "Consolas", + "mission_font_px": 44, + "timer_font_px": 80, + "gn_font_px": 24, + } + ) + bg = s.get("bg_color", "#000000") + text = s.get("text_color", "#FFFFFF") + font_family = s.get("font_family", "Consolas") + timer_px = int(s.get("timer_font_px", 100)) + mission_px = int(s.get("mission_font_px", 48)) + gn_px = int(s.get("gn_font_px", 24)) + gn_bg = s.get("gn_bg_color", "#111111") + gn_border = s.get("gn_border_color", "#FFFFFF") + gn_go = s.get("gn_go_color", "#00FF00") + gn_nogo = s.get("gn_nogo_color", "#FF0000") # apply to main window elements try: self.root.config(bg=bg) self.titletext.config(fg=text, bg=bg, font=(font_family, 20)) # timer label - self.text.config(fg=text, bg=bg, font=(font_family, timer_px, 'bold')) + self.text.config(fg=text, bg=bg, font=(font_family, timer_px, "bold")) + # GN labels: set bg and font, and color depending on GO/NOGO def style_gn_label(lbl, value): try: lbl.config(bg=bg, font=(font_family, gn_px)) - v = (value or '').strip().upper() - if v == 'GO': + v = (value or "").strip().upper() + if v == "GO": lbl.config(fg=gn_go) - elif v in ('NOGO', 'NO-GO'): + elif v in ("NOGO", "NO-GO"): lbl.config(fg=gn_nogo) else: lbl.config(fg=text) except Exception: pass - style_gn_label(self.range_label, getattr(self, 'range_status', None)) - style_gn_label(self.weather_label, getattr(self, 'weather', None)) - style_gn_label(self.vehicle_label, getattr(self, 'vehicle', None)) + style_gn_label(self.range_label, getattr(self, "range_status", None)) + style_gn_label(self.weather_label, getattr(self, "weather", None)) + style_gn_label(self.vehicle_label, getattr(self, "vehicle", None)) # Buttons: invert colors depending on mode # dark mode -> buttons white bg, black text # light mode -> buttons black bg, white text - if mode == 'dark': - btn_bg = '#FFFFFF' - btn_fg = '#000000' - active_bg = '#DDDDDD' + if mode == "dark": + btn_bg = "#FFFFFF" + btn_fg = "#000000" + active_bg = "#DDDDDD" else: - btn_bg = '#000000' - btn_fg = '#FFFFFF' - active_bg = '#222222' + btn_bg = "#000000" + btn_fg = "#FFFFFF" + active_bg = "#222222" - for btn in (self.start_btn, self.hold_btn, self.resume_btn, self.scrub_btn, self.reset_btn, self.settings_btn): + for btn in ( + self.start_btn, + self.hold_btn, + self.resume_btn, + self.scrub_btn, + self.reset_btn, + self.settings_btn, + ): try: # preserve scrub button's custom color (red) if set try: - cur_fg = btn.cget('fg') + cur_fg = btn.cget("fg") except Exception: cur_fg = None - if btn is getattr(self, 'scrub_btn', None) and cur_fg: + if btn is getattr(self, "scrub_btn", None) and cur_fg: # keep existing foreground (usually red) btn.config(bg=btn_bg, activebackground=active_bg) else: @@ -929,7 +1251,11 @@ class CountdownApp: pass # Manual toggle buttons - for btn in (self.range_toggle_btn, self.weather_toggle_btn, self.vehicle_toggle_btn): + for btn in ( + self.range_toggle_btn, + self.weather_toggle_btn, + self.vehicle_toggle_btn, + ): try: btn.config(bg=btn_bg, fg=btn_fg) except Exception: @@ -941,13 +1267,13 @@ class CountdownApp: # Footer should invert colors depending on mode: # - dark mode -> white background, black text # - light mode -> black background, white text - mode = s.get('appearance_mode', 'dark') - if mode == 'dark': - footer_bg = '#FFFFFF' - footer_fg = '#000000' + mode = s.get("appearance_mode", "dark") + if mode == "dark": + footer_bg = "#FFFFFF" + footer_fg = "#000000" else: - footer_bg = '#000000' - footer_fg = '#FFFFFF' + footer_bg = "#000000" + footer_fg = "#FFFFFF" try: self.footer_label.config(bg=footer_bg, fg=footer_fg) except Exception: @@ -966,21 +1292,23 @@ class CountdownApp: def update_gn_labels(self, range_val, weather_val, vehicle_val): """Update GN label texts and apply theme-aware styling.""" s = load_settings() - gn_px = int(s.get('gn_font_px', 28)) - font_family = s.get('font_family', 'Consolas') - bg = s.get('bg_color', '#000000') - text = s.get('text_color', '#FFFFFF') - gn_go = s.get('gn_go_color', '#00FF00') - gn_nogo = s.get('gn_nogo_color', '#FF0000') + gn_px = int(s.get("gn_font_px", 28)) + font_family = s.get("font_family", "Consolas") + bg = s.get("bg_color", "#000000") + text = s.get("text_color", "#FFFFFF") + gn_go = s.get("gn_go_color", "#00FF00") + gn_nogo = s.get("gn_nogo_color", "#FF0000") # Range try: display_range = format_status_display(range_val) - self.range_label.config(text=f"RANGE: {display_range}", bg=bg, font=(font_family, gn_px)) - rv = (range_val or '').strip().upper() - rnorm = re.sub(r'[^A-Z]', '', rv) - if rnorm == 'GO': + self.range_label.config( + text=f"RANGE: {display_range}", bg=bg, font=(font_family, gn_px) + ) + rv = (range_val or "").strip().upper() + rnorm = re.sub(r"[^A-Z]", "", rv) + if rnorm == "GO": self.range_label.config(fg=gn_go) - elif rnorm == 'NOGO': + elif rnorm == "NOGO": self.range_label.config(fg=gn_nogo) else: self.range_label.config(fg=text) @@ -990,12 +1318,14 @@ class CountdownApp: # Weather try: display_weather = format_status_display(weather_val) - self.weather_label.config(text=f"WEATHER: {display_weather}", bg=bg, font=(font_family, gn_px)) - wv = (weather_val or '').strip().upper() - wnorm = re.sub(r'[^A-Z]', '', wv) - if wnorm == 'GO': + self.weather_label.config( + text=f"WEATHER: {display_weather}", bg=bg, font=(font_family, gn_px) + ) + wv = (weather_val or "").strip().upper() + wnorm = re.sub(r"[^A-Z]", "", wv) + if wnorm == "GO": self.weather_label.config(fg=gn_go) - elif wnorm == 'NOGO': + elif wnorm == "NOGO": self.weather_label.config(fg=gn_nogo) else: self.weather_label.config(fg=text) @@ -1005,12 +1335,14 @@ class CountdownApp: # Vehicle try: display_vehicle = format_status_display(vehicle_val) - self.vehicle_label.config(text=f"VEHICLE: {display_vehicle}", bg=bg, font=(font_family, gn_px)) - vv = (vehicle_val or '').strip().upper() - vnorm = re.sub(r'[^A-Z]', '', vv) - if vnorm == 'GO': + self.vehicle_label.config( + text=f"VEHICLE: {display_vehicle}", bg=bg, font=(font_family, gn_px) + ) + vv = (vehicle_val or "").strip().upper() + vnorm = re.sub(r"[^A-Z]", "", vv) + if vnorm == "GO": self.vehicle_label.config(fg=gn_go) - elif vnorm == 'NOGO': + elif vnorm == "NOGO": self.vehicle_label.config(fg=gn_nogo) else: self.vehicle_label.config(fg=text) @@ -1032,10 +1364,14 @@ class CountdownApp: if isinstance(child, tk.Label): try: # preserve GN label fg colors and don't override the footer label (it has a special inverted style) - if child in (getattr(self, 'range_label', None), getattr(self, 'weather_label', None), getattr(self, 'vehicle_label', None)): + if child in ( + getattr(self, "range_label", None), + getattr(self, "weather_label", None), + getattr(self, "vehicle_label", None), + ): # GN labels keep fg but should have themed bg - child.config(bg=s.get('gn_bg_color', bg)) - elif child is getattr(self, 'footer_label', None): + child.config(bg=s.get("gn_bg_color", bg)) + elif child is getattr(self, "footer_label", None): # footer_label was already styled by apply_appearance_settings; don't override it here pass else: @@ -1046,42 +1382,46 @@ class CountdownApp: if isinstance(child, tk.Entry): try: # Set entry bg/fg depending on appearance mode - mode_local = s.get('appearance_mode', 'dark') - if mode_local == 'dark': - child.config(bg='#222222', fg=text, insertbackground=text) + mode_local = s.get("appearance_mode", "dark") + if mode_local == "dark": + child.config(bg="#222222", fg=text, insertbackground=text) else: # light mode entries should contrast with the white background - child.config(bg='#b4b4b4', fg='#000000', insertbackground='#000000') + child.config( + bg="#b4b4b4", fg="#000000", insertbackground="#000000" + ) except Exception: pass # OptionMenu/Menubutton if isinstance(child, tk.Menubutton): try: - child.config(bg=btn_bg, fg=btn_fg, activebackground='#555') + child.config(bg=btn_bg, fg=btn_fg, activebackground="#555") except Exception: pass # Radiobutton / Checkbutton if isinstance(child, (tk.Radiobutton, tk.Checkbutton)): try: # selectcolor is the indicator background; set it to match the overall bg for neatness - child.config(bg=bg, fg=text, selectcolor=bg, activebackground=bg) + child.config( + bg=bg, fg=text, selectcolor=bg, activebackground=bg + ) except Exception: pass # Buttons: ensure themed background and correct fg if isinstance(child, tk.Button): try: # don't override scrub button's fg if it has a special color - if child is getattr(self, 'scrub_btn', None): - child.config(bg=btn_bg, activebackground='#555') + if child is getattr(self, "scrub_btn", None): + child.config(bg=btn_bg, activebackground="#555") else: - child.config(bg=btn_bg, fg=btn_fg, activebackground='#555') + child.config(bg=btn_bg, fg=btn_fg, activebackground="#555") except Exception: pass except Exception: pass # Recurse try: - if hasattr(child, 'winfo_children'): + if hasattr(child, "winfo_children"): self._theme_recursive(child, bg, text, btn_bg, btn_fg) except Exception: pass @@ -1091,46 +1431,72 @@ class CountdownApp: settings = load_settings() win = tk.Toplevel(self.root) win.transient(self.root) - win.title('Appearance') - win.geometry('520x475') + win.title("Appearance") + win.geometry("520x475") # derive colors from appearance_mode so the dialog matches the main UI - mode_local = settings.get('appearance_mode', 'dark') - if mode_local == 'dark': - win_bg = '#000000'; win_text = '#FFFFFF'; btn_bg = '#FFFFFF'; btn_fg = '#000000'; entry_bg = '#222'; entry_fg = '#FFFFFF' + mode_local = settings.get("appearance_mode", "dark") + if mode_local == "dark": + win_bg = "#000000" + win_text = "#FFFFFF" + btn_bg = "#FFFFFF" + btn_fg = "#000000" + entry_bg = "#222" + entry_fg = "#FFFFFF" else: - win_bg = '#FFFFFF'; win_text = '#000000'; btn_bg = '#000000'; btn_fg = '#FFFFFF'; entry_bg = '#b4b4b4'; entry_fg = '#000000' + win_bg = "#FFFFFF" + win_text = "#000000" + btn_bg = "#000000" + btn_fg = "#FFFFFF" + entry_bg = "#b4b4b4" + entry_fg = "#000000" win.config(bg=win_bg) - tk.Label(win, text='Choose UI mode:', fg=win_text, bg=win_bg).pack(anchor='w', padx=12, pady=(10,0)) - mode_var = tk.StringVar(value=settings.get('appearance_mode', 'dark')) - modes = ['dark', 'light'] + tk.Label(win, text="Choose UI mode:", fg=win_text, bg=win_bg).pack( + anchor="w", padx=12, pady=(10, 0) + ) + mode_var = tk.StringVar(value=settings.get("appearance_mode", "dark")) + modes = ["dark", "light"] mode_menu = tk.OptionMenu(win, mode_var, *modes) - mode_menu.config(fg=win_text, bg=entry_bg, activebackground='#333') - mode_menu.pack(anchor='w', padx=12, pady=6) + mode_menu.config(fg=win_text, bg=entry_bg, activebackground="#333") + mode_menu.pack(anchor="w", padx=12, pady=6) def on_save_mode(): choice = mode_var.get() presets = { - 'dark': { - 'bg_color': '#000000', 'text_color': '#FFFFFF', 'gn_bg_color': '#111111', - 'gn_border_color': '#FFFFFF', 'gn_go_color': '#00FF00', 'gn_nogo_color': '#FF0000', - 'font_family': 'Consolas', 'mission_font_px': 24, 'timer_font_px': 80, 'gn_font_px': 20 + "dark": { + "bg_color": "#000000", + "text_color": "#FFFFFF", + "gn_bg_color": "#111111", + "gn_border_color": "#FFFFFF", + "gn_go_color": "#00FF00", + "gn_nogo_color": "#FF0000", + "font_family": "Consolas", + "mission_font_px": 24, + "timer_font_px": 80, + "gn_font_px": 20, + }, + "light": { + "bg_color": "#FFFFFF", + "text_color": "#000000", + "gn_bg_color": "#EEEEEE", + "gn_border_color": "#333333", + "gn_go_color": "#008800", + "gn_nogo_color": "#AA0000", + "font_family": "Consolas", + "mission_font_px": 24, + "timer_font_px": 80, + "gn_font_px": 20, }, - 'light': { - 'bg_color': '#FFFFFF', 'text_color': '#000000', 'gn_bg_color': '#EEEEEE', - 'gn_border_color': '#333333', 'gn_go_color': '#008800', 'gn_nogo_color': '#AA0000', - 'font_family': 'Consolas', 'mission_font_px': 24, 'timer_font_px': 80, 'gn_font_px': 20 - } } p = presets.get(choice, {}) s = load_settings() - s['appearance_mode'] = choice + s["appearance_mode"] = choice s.update(p) save_settings(s) try: self.apply_appearance_settings() - write_countdown_html(self.mission_name, self.text.cget('text')) + write_countdown_html(self.mission_name, self.text.cget("text")) write_gonogo_html(self.gonogo_values) except Exception: pass @@ -1138,7 +1504,7 @@ class CountdownApp: win.destroy() # also close the settings window if it is open try: - if getattr(self, 'settings_win', None): + if getattr(self, "settings_win", None): try: self.settings_win.destroy() except Exception: @@ -1156,89 +1522,203 @@ class CountdownApp: pass s = load_settings() - html_frame = tk.LabelFrame(win, text='HTML appearance (streaming)', fg=win_text, bg=win_bg) + html_frame = tk.LabelFrame( + win, text="HTML appearance (streaming)", fg=win_text, bg=win_bg + ) html_frame.config(bg=win_bg) - html_frame.pack(fill='x', padx=8, pady=6) + html_frame.pack(fill="x", padx=8, pady=6) # layout HTML appearance fields in a grid - tk.Label(html_frame, text='Background:', fg=win_text, bg=win_bg).grid(row=0, column=0, sticky='w', padx=6, pady=4) - bg_entry = tk.Entry(html_frame, width=12, fg=entry_fg, bg=entry_bg, insertbackground=entry_fg) + tk.Label(html_frame, text="Background:", fg=win_text, bg=win_bg).grid( + row=0, column=0, sticky="w", padx=6, pady=4 + ) + bg_entry = tk.Entry( + html_frame, width=12, fg=entry_fg, bg=entry_bg, insertbackground=entry_fg + ) bg_entry.grid(row=0, column=1, padx=6, pady=4) - bg_entry.insert(0, s.get('html_bg_color', s.get('bg_color', '#000000'))) - tk.Button(html_frame, text='Choose', command=lambda: choose_color(bg_entry), fg=btn_fg, bg=btn_bg).grid(row=0, column=2, padx=6) + bg_entry.insert(0, s.get("html_bg_color", s.get("bg_color", "#000000"))) + tk.Button( + html_frame, + text="Choose", + command=lambda: choose_color(bg_entry), + fg=btn_fg, + bg=btn_bg, + ).grid(row=0, column=2, padx=6) - tk.Label(html_frame, text='Text:', fg=win_text, bg=win_bg).grid(row=1, column=0, sticky='w', padx=6, pady=4) - text_entry = tk.Entry(html_frame, width=12, fg=entry_fg, bg=entry_bg, insertbackground=entry_fg) + tk.Label(html_frame, text="Text:", fg=win_text, bg=win_bg).grid( + row=1, column=0, sticky="w", padx=6, pady=4 + ) + text_entry = tk.Entry( + html_frame, width=12, fg=entry_fg, bg=entry_bg, insertbackground=entry_fg + ) text_entry.grid(row=1, column=1, padx=6, pady=4) - text_entry.insert(0, s.get('html_text_color', s.get('text_color', '#FFFFFF'))) - tk.Button(html_frame, text='Choose', command=lambda: choose_color(text_entry), fg=btn_fg, bg=btn_bg).grid(row=1, column=2, padx=6) + text_entry.insert(0, s.get("html_text_color", s.get("text_color", "#FFFFFF"))) + tk.Button( + html_frame, + text="Choose", + command=lambda: choose_color(text_entry), + fg=btn_fg, + bg=btn_bg, + ).grid(row=1, column=2, padx=6) - tk.Label(html_frame, text='GN GO:', fg=win_text, bg=win_bg).grid(row=2, column=0, sticky='w', padx=6, pady=4) - gn_go_entry = tk.Entry(html_frame, width=12, fg=entry_fg, bg=entry_bg, insertbackground=entry_fg) + tk.Label(html_frame, text="GN GO:", fg=win_text, bg=win_bg).grid( + row=2, column=0, sticky="w", padx=6, pady=4 + ) + gn_go_entry = tk.Entry( + html_frame, width=12, fg=entry_fg, bg=entry_bg, insertbackground=entry_fg + ) gn_go_entry.grid(row=2, column=1, padx=6, pady=4) - gn_go_entry.insert(0, s.get('html_gn_go_color', s.get('gn_go_color', '#00FF00'))) - tk.Button(html_frame, text='Choose', command=lambda: choose_color(gn_go_entry), fg=btn_fg, bg=btn_bg).grid(row=2, column=2, padx=6) + gn_go_entry.insert( + 0, s.get("html_gn_go_color", s.get("gn_go_color", "#00FF00")) + ) + tk.Button( + html_frame, + text="Choose", + command=lambda: choose_color(gn_go_entry), + fg=btn_fg, + bg=btn_bg, + ).grid(row=2, column=2, padx=6) - tk.Label(html_frame, text='GN NO-GO:', fg=win_text, bg=win_bg).grid(row=3, column=0, sticky='w', padx=6, pady=4) - gn_nogo_entry = tk.Entry(html_frame, width=12, fg=entry_fg, bg=entry_bg, insertbackground=entry_fg) + tk.Label(html_frame, text="GN NO-GO:", fg=win_text, bg=win_bg).grid( + row=3, column=0, sticky="w", padx=6, pady=4 + ) + gn_nogo_entry = tk.Entry( + html_frame, width=12, fg=entry_fg, bg=entry_bg, insertbackground=entry_fg + ) gn_nogo_entry.grid(row=3, column=1, padx=6, pady=4) - gn_nogo_entry.insert(0, s.get('html_gn_nogo_color', s.get('gn_nogo_color', '#FF0000'))) - tk.Button(html_frame, text='Choose', command=lambda: choose_color(gn_nogo_entry), fg=btn_fg, bg=btn_bg).grid(row=3, column=2, padx=6) + gn_nogo_entry.insert( + 0, s.get("html_gn_nogo_color", s.get("gn_nogo_color", "#FF0000")) + ) + tk.Button( + html_frame, + text="Choose", + command=lambda: choose_color(gn_nogo_entry), + fg=btn_fg, + bg=btn_bg, + ).grid(row=3, column=2, padx=6) - tk.Label(html_frame, text='GN box bg:', fg=win_text, bg=win_bg).grid(row=4, column=0, sticky='w', padx=6, pady=4) - gn_box_bg_entry = tk.Entry(html_frame, width=12, fg=entry_fg, bg=entry_bg, insertbackground=entry_fg) + tk.Label(html_frame, text="GN box bg:", fg=win_text, bg=win_bg).grid( + row=4, column=0, sticky="w", padx=6, pady=4 + ) + gn_box_bg_entry = tk.Entry( + html_frame, width=12, fg=entry_fg, bg=entry_bg, insertbackground=entry_fg + ) gn_box_bg_entry.grid(row=4, column=1, padx=6, pady=4) - gn_box_bg_entry.insert(0, s.get('html_gn_bg_color', s.get('gn_bg_color', '#111111'))) - tk.Button(html_frame, text='Choose', command=lambda: choose_color(gn_box_bg_entry), fg=btn_fg, bg=btn_bg).grid(row=4, column=2, padx=6) + gn_box_bg_entry.insert( + 0, s.get("html_gn_bg_color", s.get("gn_bg_color", "#111111")) + ) + tk.Button( + html_frame, + text="Choose", + command=lambda: choose_color(gn_box_bg_entry), + fg=btn_fg, + bg=btn_bg, + ).grid(row=4, column=2, padx=6) - tk.Label(html_frame, text='GN border:', fg=win_text, bg=win_bg).grid(row=5, column=0, sticky='w', padx=6, pady=4) - gn_border_entry = tk.Entry(html_frame, width=12, fg=entry_fg, bg=entry_bg, insertbackground=entry_fg) + tk.Label(html_frame, text="GN border:", fg=win_text, bg=win_bg).grid( + row=5, column=0, sticky="w", padx=6, pady=4 + ) + gn_border_entry = tk.Entry( + html_frame, width=12, fg=entry_fg, bg=entry_bg, insertbackground=entry_fg + ) gn_border_entry.grid(row=5, column=1, padx=6, pady=4) - gn_border_entry.insert(0, s.get('html_gn_border_color', s.get('gn_border_color', '#FFFFFF'))) - tk.Button(html_frame, text='Choose', command=lambda: choose_color(gn_border_entry), fg=btn_fg, bg=btn_bg).grid(row=5, column=2, padx=6) + gn_border_entry.insert( + 0, s.get("html_gn_border_color", s.get("gn_border_color", "#FFFFFF")) + ) + tk.Button( + html_frame, + text="Choose", + command=lambda: choose_color(gn_border_entry), + fg=btn_fg, + bg=btn_bg, + ).grid(row=5, column=2, padx=6) - tk.Label(html_frame, text='Font family:', fg=win_text, bg=win_bg).grid(row=6, column=0, sticky='w', padx=6, pady=4) - font_entry = tk.Entry(html_frame, width=20, fg=entry_fg, bg=entry_bg, insertbackground=entry_fg) - font_entry.grid(row=6, column=1, padx=6, pady=4, columnspan=2, sticky='w') - font_entry.insert(0, s.get('html_font_family', s.get('font_family', 'Consolas'))) + tk.Label(html_frame, text="Font family:", fg=win_text, bg=win_bg).grid( + row=6, column=0, sticky="w", padx=6, pady=4 + ) + font_entry = tk.Entry( + html_frame, width=20, fg=entry_fg, bg=entry_bg, insertbackground=entry_fg + ) + font_entry.grid(row=6, column=1, padx=6, pady=4, columnspan=2, sticky="w") + font_entry.insert( + 0, s.get("html_font_family", s.get("font_family", "Consolas")) + ) - tk.Label(html_frame, text='Mission px:', fg=win_text, bg=win_bg).grid(row=7, column=0, sticky='w', padx=6, pady=4) - mission_px_entry = tk.Entry(html_frame, width=6, fg=entry_fg, bg=entry_bg, insertbackground=entry_fg) - mission_px_entry.grid(row=7, column=1, padx=6, pady=4, sticky='w') - mission_px_entry.insert(0, str(s.get('html_mission_font_px', s.get('mission_font_px', 24)))) + tk.Label(html_frame, text="Mission px:", fg=win_text, bg=win_bg).grid( + row=7, column=0, sticky="w", padx=6, pady=4 + ) + mission_px_entry = tk.Entry( + html_frame, width=6, fg=entry_fg, bg=entry_bg, insertbackground=entry_fg + ) + mission_px_entry.grid(row=7, column=1, padx=6, pady=4, sticky="w") + mission_px_entry.insert( + 0, str(s.get("html_mission_font_px", s.get("mission_font_px", 24))) + ) - tk.Label(html_frame, text='Timer px:', fg=win_text, bg=win_bg).grid(row=8, column=0, sticky='w', padx=6, pady=4) - timer_px_entry = tk.Entry(html_frame, width=6, fg=entry_fg, bg=entry_bg, insertbackground=entry_fg) - timer_px_entry.grid(row=8, column=1, padx=6, pady=4, sticky='w') - timer_px_entry.insert(0, str(s.get('html_timer_font_px', s.get('timer_font_px', 80)))) + tk.Label(html_frame, text="Timer px:", fg=win_text, bg=win_bg).grid( + row=8, column=0, sticky="w", padx=6, pady=4 + ) + timer_px_entry = tk.Entry( + html_frame, width=6, fg=entry_fg, bg=entry_bg, insertbackground=entry_fg + ) + timer_px_entry.grid(row=8, column=1, padx=6, pady=4, sticky="w") + timer_px_entry.insert( + 0, str(s.get("html_timer_font_px", s.get("timer_font_px", 80))) + ) # Add a checkbox to hide mission name in HTML output - self.hide_mission_name_var = tk.BooleanVar(value=s.get("hide_mission_name", False)) - hide_mission_name_cb = tk.Checkbutton(html_frame, text="Hide mission name in HTML output", variable=self.hide_mission_name_var, fg=win_text, bg=win_bg, selectcolor=win_bg, activebackground=win_bg) + self.hide_mission_name_var = tk.BooleanVar( + value=s.get("hide_mission_name", False) + ) + hide_mission_name_cb = tk.Checkbutton( + html_frame, + text="Hide mission name in HTML output", + variable=self.hide_mission_name_var, + fg=win_text, + bg=win_bg, + selectcolor=win_bg, + activebackground=win_bg, + ) + + hide_mission_name_cb.grid( + row=9, column=0, columnspan=3, sticky="w", padx=6, pady=4 + ) - hide_mission_name_cb.grid(row=9, column=0, columnspan=3, sticky='w', padx=6, pady=4) - def save_html_prefs(): try: s_local = load_settings() - s_local['html_bg_color'] = bg_entry.get().strip() or s_local.get('html_bg_color') - s_local['html_text_color'] = text_entry.get().strip() or s_local.get('html_text_color') - s_local['html_gn_go_color'] = gn_go_entry.get().strip() or s_local.get('html_gn_go_color') - s_local['html_gn_nogo_color'] = gn_nogo_entry.get().strip() or s_local.get('html_gn_nogo_color') - s_local['html_gn_bg_color'] = gn_box_bg_entry.get().strip() or s_local.get('html_gn_bg_color') - s_local['html_gn_border_color'] = gn_border_entry.get().strip() or s_local.get('html_gn_border_color') - s_local['html_font_family'] = font_entry.get().strip() or s_local.get('html_font_family') + s_local["html_bg_color"] = bg_entry.get().strip() or s_local.get( + "html_bg_color" + ) + s_local["html_text_color"] = text_entry.get().strip() or s_local.get( + "html_text_color" + ) + s_local["html_gn_go_color"] = gn_go_entry.get().strip() or s_local.get( + "html_gn_go_color" + ) + s_local["html_gn_nogo_color"] = ( + gn_nogo_entry.get().strip() or s_local.get("html_gn_nogo_color") + ) + s_local["html_gn_bg_color"] = ( + gn_box_bg_entry.get().strip() or s_local.get("html_gn_bg_color") + ) + s_local["html_gn_border_color"] = ( + gn_border_entry.get().strip() or s_local.get("html_gn_border_color") + ) + s_local["html_font_family"] = font_entry.get().strip() or s_local.get( + "html_font_family" + ) s_local["hide_mission_name"] = self.hide_mission_name_var.get() try: - s_local['html_mission_font_px'] = int(mission_px_entry.get()) + s_local["html_mission_font_px"] = int(mission_px_entry.get()) except Exception: pass try: - s_local['html_timer_font_px'] = int(timer_px_entry.get()) + s_local["html_timer_font_px"] = int(timer_px_entry.get()) except Exception: pass save_settings(s_local) - write_countdown_html(self.mission_name, self.text.cget('text')) + write_countdown_html(self.mission_name, self.text.cget("text")) write_gonogo_html(self.gonogo_values) except Exception: pass @@ -1246,41 +1726,74 @@ class CountdownApp: def reset_html_defaults(): try: s_local = load_settings() - s_local['html_bg_color'] = DEFAULT_SETTINGS.get('html_bg_color') - s_local['html_text_color'] = DEFAULT_SETTINGS.get('html_text_color') - s_local['html_font_family'] = DEFAULT_SETTINGS.get('html_font_family') - s_local['html_mission_font_px'] = DEFAULT_SETTINGS.get('html_mission_font_px') - s_local['html_timer_font_px'] = DEFAULT_SETTINGS.get('html_timer_font_px') - s_local['html_gn_bg_color'] = DEFAULT_SETTINGS.get('html_gn_bg_color') - s_local['html_gn_border_color'] = DEFAULT_SETTINGS.get('html_gn_border_color') - s_local['html_gn_go_color'] = DEFAULT_SETTINGS.get('html_gn_go_color') - s_local['html_gn_nogo_color'] = DEFAULT_SETTINGS.get('html_gn_nogo_color') - s_local['html_gn_font_px'] = DEFAULT_SETTINGS.get('html_gn_font_px') + s_local["html_bg_color"] = DEFAULT_SETTINGS.get("html_bg_color") + s_local["html_text_color"] = DEFAULT_SETTINGS.get("html_text_color") + s_local["html_font_family"] = DEFAULT_SETTINGS.get("html_font_family") + s_local["html_mission_font_px"] = DEFAULT_SETTINGS.get( + "html_mission_font_px" + ) + s_local["html_timer_font_px"] = DEFAULT_SETTINGS.get( + "html_timer_font_px" + ) + s_local["html_gn_bg_color"] = DEFAULT_SETTINGS.get("html_gn_bg_color") + s_local["html_gn_border_color"] = DEFAULT_SETTINGS.get( + "html_gn_border_color" + ) + s_local["html_gn_go_color"] = DEFAULT_SETTINGS.get("html_gn_go_color") + s_local["html_gn_nogo_color"] = DEFAULT_SETTINGS.get( + "html_gn_nogo_color" + ) + s_local["html_gn_font_px"] = DEFAULT_SETTINGS.get("html_gn_font_px") save_settings(s_local) # update UI fields - bg_entry.delete(0, tk.END); bg_entry.insert(0, s_local['html_bg_color']) - text_entry.delete(0, tk.END); text_entry.insert(0, s_local['html_text_color']) - gn_go_entry.delete(0, tk.END); gn_go_entry.insert(0, s_local['html_gn_go_color']) - gn_nogo_entry.delete(0, tk.END); gn_nogo_entry.insert(0, s_local['html_gn_nogo_color']) - gn_box_bg_entry.delete(0, tk.END); gn_box_bg_entry.insert(0, s_local['html_gn_bg_color']) - gn_border_entry.delete(0, tk.END); gn_border_entry.insert(0, s_local['html_gn_border_color']) - font_entry.delete(0, tk.END); font_entry.insert(0, s_local['html_font_family']) - mission_px_entry.delete(0, tk.END); mission_px_entry.insert(0, str(s_local['html_mission_font_px'])) - timer_px_entry.delete(0, tk.END); timer_px_entry.insert(0, str(s_local['html_timer_font_px'])) - write_countdown_html(self.mission_name, self.text.cget('text')) + bg_entry.delete(0, tk.END) + bg_entry.insert(0, s_local["html_bg_color"]) + text_entry.delete(0, tk.END) + text_entry.insert(0, s_local["html_text_color"]) + gn_go_entry.delete(0, tk.END) + gn_go_entry.insert(0, s_local["html_gn_go_color"]) + gn_nogo_entry.delete(0, tk.END) + gn_nogo_entry.insert(0, s_local["html_gn_nogo_color"]) + gn_box_bg_entry.delete(0, tk.END) + gn_box_bg_entry.insert(0, s_local["html_gn_bg_color"]) + gn_border_entry.delete(0, tk.END) + gn_border_entry.insert(0, s_local["html_gn_border_color"]) + font_entry.delete(0, tk.END) + font_entry.insert(0, s_local["html_font_family"]) + mission_px_entry.delete(0, tk.END) + mission_px_entry.insert(0, str(s_local["html_mission_font_px"])) + timer_px_entry.delete(0, tk.END) + timer_px_entry.insert(0, str(s_local["html_timer_font_px"])) + write_countdown_html(self.mission_name, self.text.cget("text")) write_gonogo_html(self.gonogo_values) except Exception: pass html_btns = tk.Frame(html_frame, bg=win_bg) html_btns.grid(row=10, column=0, columnspan=3, pady=6) - tk.Button(html_btns, text='Save (HTML only)', command=save_html_prefs, fg=btn_fg, bg=btn_bg).pack(side='right', padx=6) - tk.Button(html_btns, text='Reset HTML defaults', command=reset_html_defaults, fg=btn_fg, bg=btn_bg).pack(side='right') + tk.Button( + html_btns, + text="Save (HTML only)", + command=save_html_prefs, + fg=btn_fg, + bg=btn_bg, + ).pack(side="right", padx=6) + tk.Button( + html_btns, + text="Reset HTML defaults", + command=reset_html_defaults, + fg=btn_fg, + bg=btn_bg, + ).pack(side="right") btn_frame = tk.Frame(win, bg=win_bg) - btn_frame.pack(fill='x', pady=8, padx=8) - tk.Button(btn_frame, text='Save', command=on_save_mode, fg=btn_fg, bg=btn_bg).pack(side='right', padx=6) - tk.Button(btn_frame, text='Cancel', command=win.destroy, fg=btn_fg, bg=btn_bg).pack(side='right') + btn_frame.pack(fill="x", pady=8, padx=8) + tk.Button( + btn_frame, text="Save", command=on_save_mode, fg=btn_fg, bg=btn_bg + ).pack(side="right", padx=6) + tk.Button( + btn_frame, text="Cancel", command=win.destroy, fg=btn_fg, bg=btn_bg + ).pack(side="right") try: self._theme_recursive(win, win_bg, win_text, btn_bg, btn_fg) @@ -1291,14 +1804,14 @@ class CountdownApp: # get current values (Range, Weather, Vehicle) cur = fetch_gonogo() # map which to index - idx_map = {'range': 0, 'weather': 1, 'vehicle': 2} + idx_map = {"range": 0, "weather": 1, "vehicle": 2} idx = idx_map.get(which, 0) try: - cur_val = (cur[idx] or '').strip().upper() + cur_val = (cur[idx] or "").strip().upper() except Exception: - cur_val = 'N/A' + cur_val = "N/A" # toggle: if GO -> NOGO, else -> GO - new_val = 'NO-GO' if cur_val == 'GO' else 'GO' + new_val = "NO-GO" if cur_val == "GO" else "GO" self.set_manual(which, new_val) # ---------------------------- @@ -1326,10 +1839,14 @@ class CountdownApp: s = int(self.clock_seconds_entry.get() or 0) # determine timezone from settings ssettings = load_settings() - tzname = ssettings.get('timezone', DEFAULT_SETTINGS.get('timezone', 'local')) - if ZoneInfo is None or tzname in (None, '', 'local'): + tzname = ssettings.get( + "timezone", DEFAULT_SETTINGS.get("timezone", "local") + ) + if ZoneInfo is None or tzname in (None, "", "local"): # naive local time handling (existing behavior) — use timedelta to roll day - target_today = now.replace(hour=h, minute=m, second=s, microsecond=0) + target_today = now.replace( + hour=h, minute=m, second=s, microsecond=0 + ) if target_today <= now: target_today = target_today + timedelta(days=1) total_seconds = (target_today - now).total_seconds() @@ -1338,7 +1855,9 @@ class CountdownApp: tz = ZoneInfo(tzname) # construct aware "now" in that timezone and create the target time now_tz = datetime.now(tz) - target = now_tz.replace(hour=h, minute=m, second=s, microsecond=0) + target = now_tz.replace( + hour=h, minute=m, second=s, microsecond=0 + ) # if target already passed in that tz, roll to next day if target <= now_tz: target = target + timedelta(days=1) @@ -1346,7 +1865,9 @@ class CountdownApp: total_seconds = (target - now_tz).total_seconds() except Exception: # fallback to naive local behavior - target_today = now.replace(hour=h, minute=m, second=s, microsecond=0) + target_today = now.replace( + hour=h, minute=m, second=s, microsecond=0 + ) if target_today <= now: target_today = target_today + timedelta(days=1) total_seconds = (target_today - now).total_seconds() @@ -1390,8 +1911,12 @@ class CountdownApp: 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") + + prefix = self.count_mode.get() if hasattr(self, "count_mode") else "T-" + reset_text = f"{prefix}00:00:00" + + self.text.config(text=reset_text) + write_countdown_html(self.mission_name, reset_text) self.show_hold_button() # ---------------------------- @@ -1402,6 +1927,19 @@ class CountdownApp: m = int((seconds % 3600) // 60) s = int(seconds % 60) return f"{prefix}{h:02}:{m:02}:{s:02}" + + # Countdown mode toggle + def toggle_count_mode(self): + current = self.count_mode.get() + next_mode = { + "T-": "L-", + "T+": "L+", + "L-": "T-", + "L+": "T+", + }.get(current, "T-") + self.count_mode.set(next_mode) + if not self.running: + self.reset() def update_clock(self): now_time = time.time() @@ -1416,27 +1954,40 @@ class CountdownApp: # auto-hold detection: if configured times include this remaining value, enter hold try: s = load_settings() - ah = set(int(x) for x in s.get('auto_hold_times', []) or []) + ah = set(int(x) for x in s.get("auto_hold_times", []) or []) except Exception: ah = set() - if diff in ah and diff not in getattr(self, '_auto_hold_triggered', set()): + if diff in ah and diff not in getattr( + self, "_auto_hold_triggered", set() + ): # trigger hold self._auto_hold_triggered.add(diff) self.hold() # show_hold_button/other UI changes handled by hold() # After entering hold, update countdown display via next tick - + 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+") + prefix = self.count_mode.get() + + # Flip mode from T-/L- to T+/L+ if needed + if prefix == "T-": + self.count_mode.set("T+") + elif prefix == "L-": + self.count_mode.set("L+") + + prefix = self.count_mode.get() + timer_text = self.format_time(elapsed, prefix) else: - timer_text = self.format_time(diff, "T-") + prefix = self.count_mode.get() + timer_text = self.format_time(diff, prefix) else: - timer_text = "T-00:00:00" + prefix = self.count_mode.get() + timer_text = self.format_time(diff, prefix) else: timer_text = self.text.cget("text") @@ -1471,13 +2022,31 @@ if __name__ == "__main__": 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)) + 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)) + 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 = 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) @@ -1489,25 +2058,25 @@ if __name__ == "__main__": # Footer uses inverted colors: white bg/black text in dark mode, black bg/white text in light mode s = load_settings() - splash_mode = s.get('appearance_mode', 'dark') - if splash_mode == 'dark': - splash_footer_bg = '#FFFFFF' - splash_footer_fg = '#000000' + splash_mode = s.get("appearance_mode", "dark") + if splash_mode == "dark": + splash_footer_bg = "#FFFFFF" + splash_footer_fg = "#000000" else: - splash_footer_bg = '#000000' - splash_footer_fg = '#FFFFFF' + splash_footer_bg = "#000000" + splash_footer_fg = "#FFFFFF" footer_label = tk.Label( footer_frame, text="Made by HamsterSpaceNerd3000", font=("Consolas", 12), fg=splash_footer_fg, - bg=splash_footer_bg + bg=splash_footer_bg, ) footer_label.pack(fill="x") # Shared flag to indicate initialization complete - init_state = { 'done': False, 'error': None } + init_state = {"done": False, "error": None} def init_worker(): try: @@ -1515,23 +2084,23 @@ if __name__ == "__main__": gonogo = fetch_gonogo() write_countdown_html("Placeholder Mission", "T-00:00:00") write_gonogo_html(gonogo) - init_state['done'] = True + init_state["done"] = True except Exception as e: - init_state['error'] = str(e) - init_state['done'] = True + 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']: + if init_state["done"]: + if init_state["error"]: info.config(text=f"Initialization error: {init_state['error']}") else: # show a visible countdown before auto-start; allow Continue to skip AUTO_START_SECONDS = 5 remaining = AUTO_START_SECONDS - cont_btn.config(state='normal') + cont_btn.config(state="normal") def tick(): nonlocal remaining @@ -1560,4 +2129,4 @@ if __name__ == "__main__": splash.after(100, check_init) splash.mainloop() - show_splash_and_start() \ No newline at end of file + show_splash_and_start()