Skip to content

Commit 758994b

Browse files
ZDStudiosclaude
andcommitted
Show which version is running, and check for newer ones
It was not possible to tell a fixed copy from a stale one from inside the app, which is exactly the confusion a bad build causes. - version next to the name in the top bar; click it to open Settings - Settings gains an About panel: version, whether it is the source file or a bundled app, the exact path, when that file was last changed, Python/Tk/OS, and the number of blocks loaded - a Check for updates button that asks the GitHub releases API, on demand only, never on startup - Copy these details, for bug reports - a --version flag, which also works on the bundled app - version bumped to 1.0.2 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 9ea5f2a commit 758994b

5 files changed

Lines changed: 217 additions & 7 deletions

File tree

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,28 @@ No Python at all? Take one from the
155155
Each build carries its own Python, so your block programs run even on a machine
156156
that has none. (Installing packages with pip still wants a normal Python.)
157157

158+
### Which version am I running?
159+
160+
The version sits next to the name in the purple bar, and **Settings → About this
161+
copy** shows the exact file it is running from, when that file was last changed,
162+
and a **Check for updates** button that asks GitHub. From a terminal:
163+
164+
```bash
165+
python scratchpy_studio.py --version
166+
```
167+
168+
```
169+
ScratchPy Studio 1.0.2
170+
Running from the source file:
171+
C:\...\scratchpy_studio.py
172+
last changed 05 Aug 2026, 12:23
173+
Python 3.14.6, Tk 8.6, Windows 11
174+
125 blocks loaded
175+
```
176+
177+
Handy when you have both a checkout and a downloaded app on the same machine and
178+
want to know which one you just opened.
179+
158180
### Other switches
159181

160182
```bash

forever.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
forever - generated by ScratchPy Studio 1.0.
2+
forever - generated by ScratchPy Studio 1.0.2.
33
44
This file is written automatically from the blocks in the
55
'forever' tab. Editing it by hand is fine, but the next time

helper.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
helper - generated by ScratchPy Studio 1.0.
2+
helper - generated by ScratchPy Studio 1.0.2.
33
44
This file is written automatically from the blocks in the
55
'helper' tab. Editing it by hand is fine, but the next time

runtest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
runtest - generated by ScratchPy Studio 1.0.
2+
runtest - generated by ScratchPy Studio 1.0.2.
33
44
This file is written automatically from the blocks in the
55
'runtest' tab. Editing it by hand is fine, but the next time

scratchpy_studio.py

Lines changed: 192 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,15 @@
4848
from tkinter import font as tkfont
4949

5050
APP_NAME = "ScratchPy Studio"
51-
APP_VERSION = "1.0"
51+
APP_VERSION = "1.0.2"
5252
PROJECT_EXT = ".spy"
5353
IS_WINDOWS = sys.platform.startswith("win")
5454

55+
REPO_URL = "https://github.com/ZDStudios/scratchpy-studio"
56+
RELEASES_URL = REPO_URL + "/releases/latest"
57+
RELEASES_API = ("https://api.github.com/repos/ZDStudios/scratchpy-studio"
58+
"/releases/latest")
59+
5560
# --------------------------------------------------------------------------- #
5661
# Palette of colours - lifted from the Scratch 3 design language
5762
# --------------------------------------------------------------------------- #
@@ -3781,6 +3786,65 @@ def on_release(self, ev):
37813786
ASSET_DIR = os.path.join(APP_DIR, "scratchpy_assets")
37823787

37833788

3789+
def running_from() -> str:
3790+
"""The exact file this copy of ScratchPy is running out of."""
3791+
if FROZEN:
3792+
return sys.executable
3793+
try:
3794+
return os.path.abspath(__file__)
3795+
except NameError: # pragma: no cover
3796+
return "unknown"
3797+
3798+
3799+
def build_kind() -> str:
3800+
return "bundled app" if FROZEN else "source file"
3801+
3802+
3803+
def build_stamp() -> str:
3804+
"""When the running copy was last changed - catches a stale build."""
3805+
try:
3806+
when = time.localtime(os.path.getmtime(running_from()))
3807+
return time.strftime("%d %b %Y, %H:%M", when)
3808+
except Exception:
3809+
return "unknown"
3810+
3811+
3812+
def build_summary() -> str:
3813+
return "\n".join([
3814+
"%s %s" % (APP_NAME, APP_VERSION),
3815+
"Running from the %s:" % build_kind(),
3816+
" %s" % running_from(),
3817+
" last changed %s" % build_stamp(),
3818+
"Python %s, Tk %s, %s %s" % (platform.python_version(),
3819+
tk.TkVersion, platform.system(),
3820+
platform.release()),
3821+
"%d blocks loaded" % len(SPECS),
3822+
])
3823+
3824+
3825+
def version_tuple(text: str) -> tuple:
3826+
parts = []
3827+
for chunk in str(text).lstrip("vV").split("."):
3828+
digits = "".join(c for c in chunk if c.isdigit())
3829+
parts.append(int(digits) if digits else 0)
3830+
return tuple(parts + [0, 0, 0])[:3]
3831+
3832+
3833+
def latest_release(timeout: float = 12.0) -> Tuple[str, str]:
3834+
"""Ask GitHub for the newest published version. Returns (tag, error)."""
3835+
import urllib.request
3836+
request = urllib.request.Request(
3837+
RELEASES_API,
3838+
headers={"User-Agent": "ScratchPyStudio/" + APP_VERSION,
3839+
"Accept": "application/vnd.github+json"})
3840+
try:
3841+
with urllib.request.urlopen(request, timeout=timeout) as reply:
3842+
data = json.loads(reply.read().decode("utf-8", "replace"))
3843+
return str(data.get("tag_name") or ""), ""
3844+
except Exception as exc:
3845+
return "", "%s: %s" % (type(exc).__name__, exc)
3846+
3847+
37843848
def find_python() -> str:
37853849
"""The interpreter used to run your programs and to drive pip.
37863850
@@ -5158,6 +5222,11 @@ def build_topbar(self):
51585222
outline="")
51595223
tk.Label(bar, text=APP_NAME, bg=UI["topbar"], fg="#FFFFFF",
51605224
font=(FONT_FAMILY, 13, "bold")).pack(side="left")
5225+
version = tk.Label(bar, text=" v" + APP_VERSION, bg=UI["topbar"],
5226+
fg="#C9B6FF", font=(FONT_FAMILY, 9),
5227+
cursor="hand2")
5228+
version.pack(side="left", padx=(4, 0))
5229+
version.bind("<Button-1>", lambda e: self.open_settings())
51615230
self.title_var = tk.StringVar(value="Untitled")
51625231
tk.Label(bar, textvariable=self.title_var, bg=UI["topbar"],
51635232
fg="#E4D9FF", font=(FONT_FAMILY, 10)).pack(side="left",
@@ -6157,10 +6226,11 @@ def help_guide(self):
61576226
def about(self):
61586227
messagebox.showinfo(
61596228
"About " + APP_NAME,
6160-
"%s %s\n\nA Scratch style editor that writes real Python.\n"
6229+
"A Scratch style editor that writes real Python.\n"
61616230
"Everything lives in one file, using nothing but the\n"
6162-
"Python standard library.\n\nRunning on Python %s" %
6163-
(APP_NAME, APP_VERSION, platform.python_version()))
6231+
"Python standard library.\n\n"
6232+
+ build_summary() + "\n\n" + REPO_URL +
6233+
"\n\nSettings has a 'Check for updates' button.")
61646234

61656235
def on_close(self):
61666236
self.workspace.close_editor(True)
@@ -6344,6 +6414,56 @@ def __init__(self, parent, app: "App"):
63446414
activebackground=UI["panel"], bd=0, highlightthickness=0,
63456415
font=(FONT_FAMILY, 10)).pack(anchor="w", padx=10)
63466416

6417+
box3 = tk.LabelFrame(top, text=" About this copy ", bg=UI["panel"],
6418+
fg=UI["text"], bd=1, relief="solid",
6419+
font=(FONT_FAMILY, 9, "bold"), labelanchor="nw")
6420+
box3.pack(fill="x", padx=24, pady=6, ipady=6)
6421+
6422+
head = tk.Frame(box3, bg=UI["panel"])
6423+
head.pack(fill="x", padx=10, pady=(6, 0))
6424+
tk.Label(head, text="%s %s" % (APP_NAME, APP_VERSION), bg=UI["panel"],
6425+
fg=UI["accent"],
6426+
font=(FONT_FAMILY, 11, "bold")).pack(side="left")
6427+
tk.Label(head, text=" (%s)" % build_kind(), bg=UI["panel"],
6428+
fg="#8A93A5", font=(FONT_FAMILY, 9)).pack(side="left")
6429+
6430+
tk.Label(box3, text=running_from(), bg=UI["panel"], fg=UI["text"],
6431+
font=(MONO_FAMILY, 8), anchor="w", justify="left",
6432+
wraplength=430).pack(anchor="w", padx=10, pady=(2, 0))
6433+
tk.Label(box3,
6434+
text="last changed %s Python %s, Tk %s, %s %s"
6435+
% (build_stamp(), platform.python_version(),
6436+
tk.TkVersion, platform.system(), platform.release()),
6437+
bg=UI["panel"], fg="#8A93A5", font=(FONT_FAMILY, 8),
6438+
anchor="w").pack(anchor="w", padx=10)
6439+
tk.Label(box3, text="%d blocks loaded" % len(SPECS), bg=UI["panel"],
6440+
fg="#8A93A5", font=(FONT_FAMILY, 8),
6441+
anchor="w").pack(anchor="w", padx=10)
6442+
6443+
row3 = tk.Frame(box3, bg=UI["panel"])
6444+
row3.pack(fill="x", padx=10, pady=(8, 2))
6445+
self.update_btn = tk.Button(row3, text="Check for updates",
6446+
command=self.check_updates, relief="flat",
6447+
bd=0, bg=UI["accent"], fg="#FFFFFF",
6448+
activebackground="#3373CC",
6449+
activeforeground="#FFFFFF",
6450+
font=(FONT_FAMILY, 9, "bold"),
6451+
cursor="hand2", padx=12, pady=3)
6452+
self.update_btn.pack(side="left")
6453+
tk.Button(row3, text="Copy these details", command=self.copy_details,
6454+
relief="flat", bd=0, bg="#EEF1F6", fg=UI["text"],
6455+
font=(FONT_FAMILY, 8), cursor="hand2",
6456+
padx=8).pack(side="left", padx=6)
6457+
tk.Button(row3, text="Open the project page", command=self.open_repo,
6458+
relief="flat", bd=0, bg="#EEF1F6", fg=UI["text"],
6459+
font=(FONT_FAMILY, 8), cursor="hand2",
6460+
padx=8).pack(side="left")
6461+
self.update_state = tk.Label(box3, text="", bg=UI["panel"],
6462+
fg="#8A93A5", font=(FONT_FAMILY, 8),
6463+
anchor="w", justify="left",
6464+
wraplength=430)
6465+
self.update_state.pack(anchor="w", padx=10, pady=(2, 0))
6466+
63476467
buttons = tk.Frame(top, bg=UI["panel"])
63486468
buttons.pack(pady=(10, 18))
63496469
tk.Button(buttons, text="Cancel", command=top.destroy, relief="flat",
@@ -6386,6 +6506,71 @@ def refresh(self):
63866506
self.make_btn.configure(text="Repair the venv" if exists
63876507
else "Create the venv now")
63886508

6509+
# -- about this copy ---------------------------------------------------- #
6510+
6511+
def copy_details(self):
6512+
self.top.clipboard_clear()
6513+
self.top.clipboard_append(build_summary())
6514+
self.update_state.configure(text="Copied. Paste it into a bug report.")
6515+
6516+
def open_repo(self):
6517+
import webbrowser
6518+
try:
6519+
webbrowser.open(REPO_URL)
6520+
except Exception:
6521+
self.update_state.configure(text=REPO_URL)
6522+
6523+
def check_updates(self):
6524+
"""Ask GitHub what the newest release is. Only ever when asked."""
6525+
self.update_btn.configure(state="disabled", text="Checking...")
6526+
self.update_state.configure(text="Asking github.com...")
6527+
6528+
def worker():
6529+
tag, error = latest_release()
6530+
self.app.ui(lambda: self.show_update(tag, error))
6531+
threading.Thread(target=worker, daemon=True).start()
6532+
6533+
def show_update(self, tag: str, error: str):
6534+
try:
6535+
self.update_btn.configure(state="normal", text="Check for updates")
6536+
except Exception:
6537+
return # the dialog was closed while we waited
6538+
if error:
6539+
self.update_state.configure(
6540+
fg="#B36B00",
6541+
text="Could not reach github.com (%s). You can look at\n%s"
6542+
% (error.split(":")[0], RELEASES_URL))
6543+
return
6544+
newest = version_tuple(tag)
6545+
mine = version_tuple(APP_VERSION)
6546+
if newest > mine:
6547+
self.update_state.configure(
6548+
fg="#B36B00",
6549+
text="Version %s is out. You have %s.\nDownload it from %s"
6550+
% (tag.lstrip("vV"), APP_VERSION, RELEASES_URL))
6551+
if messagebox.askyesno(
6552+
"Update available",
6553+
"ScratchPy Studio %s is available and you have %s.\n\n"
6554+
"Open the download page?" % (tag.lstrip("vV"), APP_VERSION),
6555+
parent=self.top):
6556+
self.open_repo_releases()
6557+
elif newest < mine:
6558+
self.update_state.configure(
6559+
fg="#8A93A5",
6560+
text="You are running %s, which is newer than the published "
6561+
"%s." % (APP_VERSION, tag.lstrip("vV")))
6562+
else:
6563+
self.update_state.configure(
6564+
fg="#2E9E5B",
6565+
text="Up to date. %s is the newest version." % APP_VERSION)
6566+
6567+
def open_repo_releases(self):
6568+
import webbrowser
6569+
try:
6570+
webbrowser.open(RELEASES_URL)
6571+
except Exception:
6572+
pass
6573+
63896574
def browse(self):
63906575
chosen = filedialog.askdirectory(title="Where should the venv live?",
63916576
parent=self.top)
@@ -7682,6 +7867,9 @@ def main(argv: Optional[List[str]] = None) -> int:
76827867
if target and target.startswith("-"):
76837868
target = None
76847869
return MCPServer(target).serve()
7870+
if "--version" in args or "-V" in args:
7871+
print(build_summary())
7872+
return 0
76857873
if "--selftest" in args:
76867874
return selftest()
76877875
if "--make-icons" in args:

0 commit comments

Comments
 (0)