Merge pull request #10 from HamsterSpaceNerd3000/main

Fixing Dev
This commit is contained in:
HamsterSpaceNerd3000
2025-11-13 17:15:17 -05:00
committed by GitHub
6 changed files with 5 additions and 391 deletions

BIN
RLC.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,243 +0,0 @@
#!/usr/bin/env python3
"""
Simple Tkinter GUI to run PyInstaller and make an exe from a .py file.
Features:
- choose script
- choose onefile / dir
- choose windowed (noconsole)
- add icon
- add additional data (file or folder; multiple, separated by semicolons)
- set output folder
- show live PyInstaller output
- cancel build
"""
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext
import subprocess
import threading
import sys
import shlex
import os
from pathlib import Path
class BuilderGUI(tk.Tk):
def __init__(self):
super().__init__()
self.title("Python → EXE (PyInstaller GUI)")
self.geometry("800x600")
self.create_widgets()
self.proc = None # subprocess handle
self.stop_requested = False
def create_widgets(self):
frame_top = tk.Frame(self)
frame_top.pack(fill="x", padx=10, pady=8)
tk.Label(frame_top, text="Script:").grid(row=0, column=0, sticky="e")
self.script_entry = tk.Entry(frame_top, width=70)
self.script_entry.grid(row=0, column=1, padx=6)
tk.Button(frame_top, text="Browse...", command=self.browse_script).grid(row=0, column=2)
tk.Label(frame_top, text="Output folder:").grid(row=1, column=0, sticky="e")
self.out_entry = tk.Entry(frame_top, width=70)
self.out_entry.grid(row=1, column=1, padx=6)
tk.Button(frame_top, text="Choose...", command=self.choose_output).grid(row=1, column=2)
opts_frame = tk.LabelFrame(self, text="Options", padx=8, pady=8)
opts_frame.pack(fill="x", padx=10)
self.onefile_var = tk.BooleanVar(value=True)
tk.Checkbutton(opts_frame, text="Onefile (single exe)", variable=self.onefile_var).grid(row=0, column=0, sticky="w", padx=6, pady=2)
self.windowed_var = tk.BooleanVar(value=False)
tk.Checkbutton(opts_frame, text="Windowed (no console) / --noconsole", variable=self.windowed_var).grid(row=0, column=1, sticky="w", padx=6, pady=2)
tk.Label(opts_frame, text="Icon (.ico):").grid(row=1, column=0, sticky="e")
self.icon_entry = tk.Entry(opts_frame, width=50)
self.icon_entry.grid(row=1, column=1, sticky="w", padx=6)
tk.Button(opts_frame, text="Browse", command=self.browse_icon).grid(row=1, column=2)
tk.Label(opts_frame, text="Additional data (src;dest pairs separated by ';', e.g. resources;resources):").grid(row=2, column=0, columnspan=3, sticky="w", pady=(6,0))
self.data_entry = tk.Entry(opts_frame, width=110)
self.data_entry.grid(row=3, column=0, columnspan=3, padx=6, pady=4)
tk.Label(opts_frame, text="Extra PyInstaller args:").grid(row=4, column=0, sticky="w")
self.extra_entry = tk.Entry(opts_frame, width=80)
self.extra_entry.grid(row=4, column=1, columnspan=2, padx=6, pady=4, sticky="w")
run_frame = tk.Frame(self)
run_frame.pack(fill="x", padx=10, pady=8)
self.build_btn = tk.Button(run_frame, text="Build EXE", command=self.start_build, bg="#2b7a78", fg="white")
self.build_btn.pack(side="left", padx=(0,6))
self.cancel_btn = tk.Button(run_frame, text="Cancel", command=self.request_cancel, state="disabled", bg="#b00020", fg="white")
self.cancel_btn.pack(side="left")
clear_btn = tk.Button(run_frame, text="Clear Log", command=self.clear_log)
clear_btn.pack(side="left", padx=6)
open_out_btn = tk.Button(run_frame, text="Open Output Folder", command=self.open_output)
open_out_btn.pack(side="right")
self.log = scrolledtext.ScrolledText(self, height=18, font=("Consolas", 10))
self.log.pack(fill="both", expand=True, padx=10, pady=(0,10))
def browse_script(self):
path = filedialog.askopenfilename(filetypes=[("Python files", "*.py")])
if path:
self.script_entry.delete(0, tk.END)
self.script_entry.insert(0, path)
# default output to script parent /dist
parent = os.path.dirname(path)
default_out = os.path.join(parent, "dist")
self.out_entry.delete(0, tk.END)
self.out_entry.insert(0, default_out)
def choose_output(self):
path = filedialog.askdirectory()
if path:
self.out_entry.delete(0, tk.END)
self.out_entry.insert(0, path)
def browse_icon(self):
path = filedialog.askopenfilename(filetypes=[("Icon files", "*.ico")])
if path:
self.icon_entry.delete(0, tk.END)
self.icon_entry.insert(0, path)
def clear_log(self):
self.log.delete("1.0", tk.END)
def open_output(self):
out = self.out_entry.get().strip()
if not out:
messagebox.showinfo("Output folder", "No output folder set.")
return
os.startfile(out) if os.name == "nt" else subprocess.run(["xdg-open", out])
def request_cancel(self):
if self.proc and self.proc.poll() is None:
self.stop_requested = True
# try terminate politely
try:
self.proc.terminate()
except Exception:
pass
self.log_insert("\nCancellation requested...\n")
self.cancel_btn.config(state="disabled")
def log_insert(self, text):
self.log.insert(tk.END, text)
self.log.see(tk.END)
def start_build(self):
script = self.script_entry.get().strip()
if not script or not os.path.isfile(script):
messagebox.showerror("Error", "Please choose a valid Python script to build.")
return
out_dir = self.out_entry.get().strip() or os.path.dirname(script)
os.makedirs(out_dir, exist_ok=True)
self.build_btn.config(state="disabled")
self.cancel_btn.config(state="normal")
self.stop_requested = False
self.clear_log()
# Start build on a thread to keep UI responsive
thread = threading.Thread(target=self.run_pyinstaller, args=(script, out_dir), daemon=True)
thread.start()
def run_pyinstaller(self, script, out_dir):
# Build the PyInstaller command
cmd = [sys.executable, "-m", "PyInstaller"]
if self.onefile_var.get():
cmd.append("--onefile")
else:
cmd.append("--onedir")
if self.windowed_var.get():
cmd.append("--noconsole")
icon = self.icon_entry.get().strip()
if icon:
cmd.extend(["--icon", icon])
# add additional data: user can provide pairs like "data;data" or "assets;assets"
data_spec = self.data_entry.get().strip()
if data_spec:
# support multiple separated by semicolons or vertical bars
pairs = [p for p in (data_spec.split(";") + data_spec.split("|")) if p.strip()]
# normalize pairs to PyInstaller format: src;dest (on Windows use ';' in CLI but PyInstaller expects src;dest as single argument)
for p in pairs:
# if user typed "src:dest" or "src->dest", replace with semicolon
p_fixed = p.replace(":", ";").replace("->", ";")
cmd.extend(["--add-data", p_fixed])
# user extra args (raw)
extra = self.extra_entry.get().strip()
if extra:
# split carefully
cmd.extend(shlex.split(extra))
# ensure output path goes to chosen dir: use --distpath
cmd.extend(["--distpath", out_dir])
# entry script
cmd.append(script)
self.log_insert("Running PyInstaller with command:\n" + " ".join(shlex.quote(c) for c in cmd) + "\n\n")
# spawn the process
try:
self.proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=1,
universal_newlines=True,
)
except Exception as e:
self.log_insert(f"Failed to start PyInstaller: {e}\n")
self.build_btn.config(state="normal")
self.cancel_btn.config(state="disabled")
return
# Stream output line by line
try:
for line in self.proc.stdout:
if line:
self.log_insert(line)
if self.stop_requested:
try:
self.proc.terminate()
except Exception:
pass
break
self.proc.wait(timeout=30)
except subprocess.TimeoutExpired:
self.log_insert("Process did not exit in time after termination.\n")
except Exception as e:
self.log_insert(f"Error while running PyInstaller: {e}\n")
retcode = self.proc.returncode if self.proc else None
if self.stop_requested:
self.log_insert("\nBuild cancelled by user.\n")
elif retcode == 0:
self.log_insert("\nBuild finished successfully.\n")
else:
self.log_insert(f"\nBuild finished with return code {retcode}.\n")
# Re-enable buttons
self.build_btn.config(state="normal")
self.cancel_btn.config(state="disabled")
self.proc = None
self.stop_requested = False
if __name__ == "__main__":
app = BuilderGUI()
app.mainloop()

View File

@@ -1,143 +0,0 @@
import tkinter as tk
import requests
import threading
import time
import json
SETTINGS_FILE = "settings.json"
class CountdownApp:
def __init__(self, root):
self.root = root
self.root.title("Launch Control - GO/NOGO")
self.go_nogo_labels = {}
self.sheet_data = {}
self.last_data = {}
self.running = True
# Load settings
self.settings = self.load_settings()
tk.Label(root, text="GO/NOGO STATUS", font=("Arial", 16, "bold")).pack(pady=10)
# Create display area
self.frame = tk.Frame(root)
self.frame.pack(pady=10)
# Buttons
tk.Button(root, text="Add Spreadsheet", command=self.add_spreadsheet_window).pack(pady=5)
tk.Button(root, text="Stop", command=self.stop).pack(pady=5)
self.start_update_thread()
def load_settings(self):
try:
with open(SETTINGS_FILE, "r") as f:
return json.load(f)
except FileNotFoundError:
return {"spreadsheets": []}
def save_settings(self):
with open(SETTINGS_FILE, "w") as f:
json.dump(self.settings, f, indent=4)
def add_spreadsheet_window(self):
win = tk.Toplevel(self.root)
win.title("Add Spreadsheet")
tk.Label(win, text="Name:").grid(row=0, column=0)
name_entry = tk.Entry(win)
name_entry.grid(row=0, column=1)
tk.Label(win, text="Link (CSV export or share link):").grid(row=1, column=0)
link_entry = tk.Entry(win, width=60)
link_entry.grid(row=1, column=1)
tk.Label(win, text="Range cell (e.g., L2):").grid(row=2, column=0)
range_entry = tk.Entry(win)
range_entry.grid(row=2, column=1)
def save_sheet():
name = name_entry.get().strip()
link = link_entry.get().strip()
cell = range_entry.get().strip().upper()
if name and link and cell:
self.settings["spreadsheets"].append({
"name": name,
"link": link,
"cell": cell
})
self.save_settings()
self.add_go_nogo_label(name)
win.destroy()
tk.Button(win, text="Save", command=save_sheet).grid(row=3, column=0, columnspan=2, pady=10)
def add_go_nogo_label(self, name):
if name not in self.go_nogo_labels:
label = tk.Label(self.frame, text=f"{name}: ---", font=("Arial", 14), width=25)
label.pack(pady=2)
self.go_nogo_labels[name] = label
def update_labels(self):
for sheet in self.settings["spreadsheets"]:
name = sheet["name"]
link = sheet["link"]
cell = sheet["cell"]
# Convert normal sheet link to CSV export link if needed
if "/edit" in link and "export" not in link:
link = link.split("/edit")[0] + "/gviz/tq?tqx=out:csv"
try:
r = requests.get(link, timeout=5)
if r.status_code == 200:
content = r.text
if name not in self.last_data or self.last_data[name] != content:
self.last_data[name] = content
# Just read raw content and extract cell text if possible
value = self.extract_cell_value(content, cell)
self.update_label_color(name, value)
except Exception as e:
print(f"Error updating {name}: {e}")
def extract_cell_value(self, csv_data, cell):
# Simple CSV parser to get cell data like L2
try:
rows = [r.split(",") for r in csv_data.splitlines() if r.strip()]
col = ord(cell[0]) - 65
row = int(cell[1:]) - 1
return rows[row][col].strip().upper()
except Exception:
return "ERR"
def update_label_color(self, name, value):
label = self.go_nogo_labels.get(name)
if not label:
return
if "GO" in value:
label.config(text=f"{name}: GO", bg="green", fg="white")
elif "NO" in value:
label.config(text=f"{name}: NO GO", bg="red", fg="white")
else:
label.config(text=f"{name}: ---", bg="gray", fg="black")
def start_update_thread(self):
threading.Thread(target=self.update_loop, daemon=True).start()
def update_loop(self):
while self.running:
self.update_labels()
time.sleep(0.1)
def stop(self):
self.running = False
self.root.destroy()
if __name__ == "__main__":
root = tk.Tk()
app = CountdownApp(root)
root.mainloop()

BIN
dist/exe/0.6.0/main.exe vendored Normal file

Binary file not shown.

BIN
dist/installers/0.6.0/RLCInstaller.exe vendored Normal file

Binary file not shown.

View File

@@ -1,11 +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. 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.5.0](https://github.com/HamsterSpaceNerd3000/RocketLaunchCountdown/releases/tag/Prealpha_050) Latest release: [RocketLaunchCountdown 0.6.0](https://github.com/HamsterSpaceNerd3000/RocketLaunchCountdown/releases/tag/v0.6.0)
Features Added: Features Added:
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. T-/L- Switching has been added.
More background optimizations and simplifications. Light/Dark mode no longer effects the HTML files.
Added game version to title on main screen. Added ability to remove mission name from the HTML output.
Added more Nitrogen Created/added app icon (will be added to the window in future version, currently on desktop shortcut)
INSTALL INSTRUCTIONS INSTALL INSTRUCTIONS