This commit is contained in:
SpaceNerd0717
2025-10-21 20:09:37 -04:00
parent cc62ae1a5a
commit cd9c1d2fd0
11 changed files with 15134 additions and 8306 deletions

726
main.py
View File

@@ -1,7 +1,9 @@
import tkinter as tk
from tkinter import colorchooser
import time
import threading
from datetime import datetime, timedelta
import re
import requests
import csv
import io
@@ -12,7 +14,6 @@ try:
except Exception:
ZoneInfo = None
# Get the user's Documents folder (cross-platform)
documents_folder = os.path.join(os.path.expanduser("~"), "Documents")
@@ -25,29 +26,52 @@ COUNTDOWN_HTML = os.path.join(app_folder, "countdown.html")
GONOGO_HTML = os.path.join(app_folder, "gonogo.html")
SHEET_LINK = ""
session = requests.Session()
appVersion = "0.4.0"
appVersion = "0.5.0"
SETTINGS_FILE = os.path.join(app_folder, "settings.json")
# Default settings
DEFAULT_SETTINGS = {
"mode": "spreadsheet", # or 'buttons'
"mode": "spreadsheet",
"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)
"column": 12
}
# default timezone: 'local' uses system local tz, otherwise an IANA name or 'UTC'
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')
# 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))
# 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'
]
def load_settings():
try:
if os.path.exists(SETTINGS_FILE):
@@ -59,7 +83,6 @@ 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:
@@ -114,14 +137,42 @@ def fetch_gonogo():
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"
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'
# fallback: treat unknown/empty as 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'
return s
except Exception:
return str(status or '')
# -------------------------
# Write Countdown HTML
# -------------------------
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)))
html = f"""<!DOCTYPE html>
<html>
<head>
@@ -129,16 +180,16 @@ def write_countdown_html(mission_name, timer_text):
<style>
body {{
margin: 0;
background-color: black;
background-color: {bg};
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: white;
font-family: Consolas, monospace;
color: {text};
font-family: {font};
}}
#mission {{ font-size: 4vw; margin-bottom: 0; }}
#timer {{ font-size: 8vw; margin-bottom: 40px; }}
#mission {{ font-size: {mission_px}px; margin-bottom: 0; }}
#timer {{ font-size: {timer_px}px; margin-bottom: 40px; }}
</style>
<script>
setTimeout(() => location.reload(), 1000);
@@ -158,6 +209,24 @@ setTimeout(() => location.reload(), 1000);
def write_gonogo_html(gonogo_values=None):
if gonogo_values is 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)))
# 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())
html = f"""<!DOCTYPE html>
<html>
<head>
@@ -165,9 +234,9 @@ def write_gonogo_html(gonogo_values=None):
<style>
body {{
margin: 0;
background-color: black;
color: white;
font-family: Consolas, monospace;
background-color: {bg};
color: {text};
font-family: {font};
display: flex;
justify-content: center;
align-items: center;
@@ -178,24 +247,24 @@ body {{
gap: 40px;
}}
.status-box {{
border: 2px solid white;
border: 2px solid {gn_border};
padding: 20px 40px;
font-size: 2.5vw;
font-size: {gn_px}px;
text-align: center;
background-color: #111;
background-color: {gn_bg};
}}
.go {{ color: #0f0; }}
.nogo {{ color: #f00; }}
.go {{ color: {gn_go}; }}
.nogo {{ color: {gn_nogo}; }}
</style>
<script>
setTimeout(() => location.reload(), 5000);
</script>
</head>
<body>
<div id="gonogo">
<div class="status-box {'go' if gonogo_values[0].lower()=='go' else 'nogo'}">Range: {gonogo_values[0]}</div>
<div class="status-box {'go' if gonogo_values[2].lower()=='go' else 'nogo'}">Vehicle: {gonogo_values[2]}</div>
<div class="status-box {'go' if gonogo_values[1].lower()=='go' else 'nogo'}">Weather: {gonogo_values[1]}</div>
<div id="gonogo">
<div class="status-box {'go' if n0=='GO' else 'nogo'}">Range: {disp0}</div>
<div class="status-box {'go' if n2=='GO' else 'nogo'}">Vehicle: {disp2}</div>
<div class="status-box {'go' if n1=='GO' else 'nogo'}">Weather: {disp1}</div>
</div>
</body>
</html>"""
@@ -227,7 +296,7 @@ class CountdownApp:
self.last_gonogo_update = time.time()
# Title
self.titletext = tk.Label(root, text="RocketLaunchCountdown", 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
@@ -325,8 +394,8 @@ class CountdownApp:
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)
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'
@@ -372,6 +441,11 @@ class CountdownApp:
self.update_inputs()
# set initial manual button visibility from settings
self.update_manual_visibility()
# Apply appearance settings at startup so the mission entry and other widgets reflect the saved mode
try:
self.apply_appearance_settings()
except Exception:
pass
self.update_clock()
# ----------------------------
@@ -379,37 +453,68 @@ class CountdownApp:
# ----------------------------
def show_settings_window(self):
settings = load_settings()
win = tk.Toplevel(self.root)
win.config(bg="black")
win.title("Settings")
win.geometry("560x250")
win.transient(self.root)
win.title("Settings")
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'
else:
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)
# 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')
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('<Destroy>', _clear_settings_ref)
except Exception:
pass
# Mode selection
frame_mode = tk.Frame(win)
frame_mode.config(bg="black")
frame_mode.config(bg=win_bg)
frame_mode.pack(fill='x', pady=8, padx=8)
tk.Label(frame_mode, text="Mode:", fg="white", bg="black").pack(side='left')
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="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)
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='white', bg='black')
frame_sheet.config(bg="black")
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='white', bg='black').pack(anchor='w')
sheet_entry = tk.Entry(frame_sheet, width=80, fg='white', bg='#222', insertbackground='white')
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))
# Accept cells in 'L3' format for each parameter
cell_frame = tk.Frame(frame_sheet)
cell_frame.config(bg="black")
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='white', bg='black').grid(row=0, column=0)
range_cell = tk.Entry(cell_frame, width=8, fg='white', bg='#222', insertbackground='white')
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:
@@ -430,8 +535,8 @@ class CountdownApp:
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')
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:
@@ -449,8 +554,8 @@ class CountdownApp:
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')
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:
@@ -469,18 +574,23 @@ class CountdownApp:
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 = 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)
# 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')
# Timezone selector
tz_frame = tk.Frame(frame_sheet, bg='black')
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='white', bg='black').pack(side='left')
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='white', bg='#222', activebackground='#333')
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):
@@ -554,24 +664,45 @@ class CountdownApp:
'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()
'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))
}
# 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'))
except Exception:
new_settings['appearance_mode'] = DEFAULT_SETTINGS.get('appearance_mode', 'dark')
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()
# appearance changes are applied only from the Appearance window
win.destroy()
def on_cancel():
win.destroy()
btn_frame = tk.Frame(win)
btn_frame = tk.Frame(win, bg='black')
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='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')
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
# ----------------------------
@@ -634,6 +765,436 @@ class CountdownApp:
else:
self.manual_frame.pack_forget()
def apply_appearance_settings(self):
"""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')
# 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'))
# 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':
lbl.config(fg=gn_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))
# 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'
else:
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):
try:
# preserve scrub button's custom color (red) if set
try:
cur_fg = btn.cget('fg')
except Exception:
cur_fg = None
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:
btn.config(bg=btn_bg, fg=btn_fg, activebackground=active_bg)
except Exception:
pass
# Manual toggle buttons
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:
pass
# manual frame and footer
try:
self.manual_frame.config(bg=bg)
# 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'
else:
footer_bg = '#000000'
footer_fg = '#FFFFFF'
try:
self.footer_label.config(bg=footer_bg, fg=footer_fg)
except Exception:
# fall back to generic theme
self.footer_label.config(bg=bg, fg=text)
except Exception:
pass
except Exception:
pass
# Recursively theme frames and common widgets so no frame is left with old colors
try:
self._theme_recursive(self.root, bg, text, btn_bg, btn_fg)
except Exception:
pass
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')
# 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(fg=gn_go)
elif rnorm == 'NOGO':
self.range_label.config(fg=gn_nogo)
else:
self.range_label.config(fg=text)
except Exception:
pass
# 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(fg=gn_go)
elif wnorm == 'NOGO':
self.weather_label.config(fg=gn_nogo)
else:
self.weather_label.config(fg=text)
except Exception:
pass
# 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(fg=gn_go)
elif vnorm == 'NOGO':
self.vehicle_label.config(fg=gn_nogo)
else:
self.vehicle_label.config(fg=text)
except Exception:
pass
def _theme_recursive(self, widget, bg, text, btn_bg, btn_fg):
# load settings so we can theme GN label backgrounds if configured
s = load_settings()
for child in widget.winfo_children():
# Frame and LabelFrame
try:
if isinstance(child, (tk.Frame, tk.LabelFrame)):
try:
child.config(bg=bg)
except Exception:
pass
# Labels: set bg, but don't override GN label fg
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)):
# 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):
# footer_label was already styled by apply_appearance_settings; don't override it here
pass
else:
child.config(bg=bg, fg=text)
except Exception:
pass
# Entries: avoid overriding entries that were explicitly styled (like mission_entry)
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)
else:
# light mode entries should contrast with the white background
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')
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)
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')
else:
child.config(bg=btn_bg, fg=btn_fg, activebackground='#555')
except Exception:
pass
except Exception:
pass
# Recurse
try:
if hasattr(child, 'winfo_children'):
self._theme_recursive(child, bg, text, btn_bg, btn_fg)
except Exception:
pass
def show_appearance_window(self):
# Re-implemented appearance window with proper theming and layout
settings = load_settings()
win = tk.Toplevel(self.root)
win.transient(self.root)
win.title('Appearance')
win.geometry('520x450')
# 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'
else:
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']
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)
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
},
'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.update(p)
save_settings(s)
try:
self.apply_appearance_settings()
write_countdown_html(self.mission_name, self.text.cget('text'))
write_gonogo_html(self.gonogo_values)
except Exception:
pass
# close appearance window
win.destroy()
# also close the settings window if it is open
try:
if getattr(self, 'settings_win', None):
try:
self.settings_win.destroy()
except Exception:
pass
except Exception:
pass
def choose_color(entry_widget):
try:
col = colorchooser.askcolor()
if col and col[1]:
entry_widget.delete(0, tk.END)
entry_widget.insert(0, col[1])
except Exception:
pass
s = load_settings()
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)
# 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)
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)
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)
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)
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)
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)
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)
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='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))))
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')
try:
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())
except Exception:
pass
save_settings(s_local)
write_countdown_html(self.mission_name, self.text.cget('text'))
write_gonogo_html(self.gonogo_values)
except Exception:
pass
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')
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'))
write_gonogo_html(self.gonogo_values)
except Exception:
pass
html_btns = tk.Frame(html_frame, bg=win_bg)
html_btns.grid(row=9, 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')
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')
try:
self._theme_recursive(win, win_bg, win_text, btn_bg, btn_fg)
except Exception:
pass
def _toggle_manual(self, which):
# get current values (Range, Weather, Vehicle)
cur = fetch_gonogo()
@@ -645,7 +1206,7 @@ class CountdownApp:
except Exception:
cur_val = 'N/A'
# toggle: if GO -> NOGO, else -> GO
new_val = 'NOGO' if cur_val == 'GO' else 'GO'
new_val = 'NO-GO' if cur_val == 'GO' else 'GO'
self.set_manual(which, new_val)
# ----------------------------
@@ -781,9 +1342,14 @@ class CountdownApp:
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))
# update texts and styles using theme
try:
self.update_gn_labels(self.range_status, self.weather, self.vehicle)
except Exception:
# fallback to simple config
self.range_label.config(text=f"RANGE: {self.range_status}")
self.weather_label.config(text=f"WEATHER: {self.weather}")
self.vehicle_label.config(text=f"VEHICLE: {self.vehicle}")
self.gonogo_values = [self.range_status, self.weather, self.vehicle]
write_gonogo_html(self.gonogo_values)
self.last_gonogo_update = now_time
@@ -816,12 +1382,22 @@ if __name__ == "__main__":
footer_frame = tk.Frame(splash, bg="black")
footer_frame.pack(side="bottom", pady=0, fill="x")
# 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'
else:
splash_footer_bg = '#000000'
splash_footer_fg = '#FFFFFF'
footer_label = tk.Label(
footer_frame,
text="Made by HamsterSpaceNerd3000", # or whatever you want
text="Made by HamsterSpaceNerd3000",
font=("Consolas", 12),
fg="black",
bg="white"
fg=splash_footer_fg,
bg=splash_footer_bg
)
footer_label.pack(fill="x")
@@ -847,9 +1423,24 @@ if __name__ == "__main__":
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)
# show a visible countdown before auto-start; allow Continue to skip
AUTO_START_SECONDS = 5
remaining = AUTO_START_SECONDS
cont_btn.config(state='normal')
def tick():
nonlocal remaining
if remaining <= 0:
on_continue()
return
info.config(text=f"Ready — auto-starting in {remaining}...")
cont_btn.config(text=f"Continue ({remaining})")
remaining -= 1
splash.after(1000, tick)
# clicking Continue will immediately proceed
cont_btn.config(command=on_continue)
tick()
return
splash.after(200, check_init)
@@ -860,7 +1451,6 @@ if __name__ == "__main__":
app = CountdownApp(root)
root.mainloop()
cont_btn.config(command=on_continue)
# begin polling
splash.after(100, check_init)
splash.mainloop()