diff --git a/dist/exe/0.5.0/main.exe b/dist/exe/0.5.0/main.exe
new file mode 100644
index 0000000..5e3f5af
Binary files /dev/null and b/dist/exe/0.5.0/main.exe differ
diff --git a/dist/installers/0.5.0/RLCInstaller.exe b/dist/installers/0.5.0/RLCInstaller.exe
new file mode 100644
index 0000000..cfd3c42
Binary files /dev/null and b/dist/installers/0.5.0/RLCInstaller.exe differ
diff --git a/main.py b/main.py
index be25bad..d434ec3 100644
--- a/main.py
+++ b/main.py
@@ -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"""
@@ -129,16 +180,16 @@ def write_countdown_html(mission_name, timer_text):
-
-
Range: {gonogo_values[0]}
-
Vehicle: {gonogo_values[2]}
-
Weather: {gonogo_values[1]}
+
+
Range: {disp0}
+
Vehicle: {disp2}
+
Weather: {disp1}
"""
@@ -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('
', _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()
diff --git a/readme.md b/readme.md
index 15f4ae7..27a392a 100644
--- a/readme.md
+++ b/readme.md
@@ -1,9 +1,11 @@
RocketLaunchCountdown is a python and HTML system to operate a launch countdown and go/nogo indicator. The go/nogo indicator is operated either by a spreadsheet, using a sharelink inserted in the settings windows. Or there are buttons in the app that can manage it as well. Which mode is used is dictated by a simple radio button in the settings window.
-Latest release: [RocketLaunchCountdown 0.4.0 THE CLOCKS UPDATE](https://github.com/HamsterSpaceNerd3000/RocketLaunchCountdown/releases/tag/Prealpha_040)
+Latest release: [RocketLaunchCountdown 0.5.0](https://github.com/HamsterSpaceNerd3000/RocketLaunchCountdown/releases/tag/Prealpha_050)
Features Added:
- Full rework of the clock system (countdown method based on clock time, not how long until).
- Minor background fixes
+ UI Customization is here! Light mode has been added and can be turned on in the settings window. Also in the settings window is the ability to adjust the colors of the HTML files for the clock and go/no-go.
+ More background optimizations and simplifications.
+ Added game version to title on main screen.
+ Added more Nitrogen
INSTALL INSTRUCTIONS