Skip to content

Commit eb57d26

Browse files
authored
Update help_window.py
1 parent 2f47920 commit eb57d26

File tree

1 file changed

+158
-125
lines changed

1 file changed

+158
-125
lines changed

help_window.py

Lines changed: 158 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,165 +1,198 @@
1-
21
from __future__ import annotations
3-
import platform, subprocess, sys
4-
from workbench_core import resolve_tool_path
2+
3+
import subprocess
4+
import sys
55
from pathlib import Path
6+
from typing import Callable, Optional
7+
68
import tkinter as tk
7-
from tkinter import ttk, messagebox
9+
from tkinter import messagebox, ttk
810
from tkinter.scrolledtext import ScrolledText
911

12+
from workbench_core import resolve_tool_path
13+
1014
APP_NAME = "YT Audio Workbench"
1115
VERSION = ""
1216

1317
__all__ = ["open_help_window", "show_about_dialog", "set_app_meta"]
1418

15-
def _center_on_screen(top: tk.Toplevel):
16-
try:
17-
top.update_idletasks()
18-
w = top.winfo_width() or 800
19-
h = top.winfo_height() or 600
20-
sw = top.winfo_screenwidth()
21-
sh = top.winfo_screenheight()
22-
x = int((sw - w) / 2)
23-
y = int((sh - h) / 2)
24-
top.geometry(f"{w}x{h}+{x}+{y}")
25-
except Exception:
26-
pass
27-
2819

2920
def set_app_meta(app_name: str, version: str) -> None:
30-
"""Set application metadata used by the Help/About windows."""
3121
global APP_NAME, VERSION
32-
APP_NAME, VERSION = app_name, version
22+
APP_NAME = app_name or APP_NAME
23+
VERSION = version or VERSION
24+
3325

34-
def open_help_window(parent: tk.Misc, help_path: Path, get_text, section: str | None = None):
26+
def _center_on_screen(win: tk.Toplevel) -> None:
27+
win.update_idletasks()
28+
w = win.winfo_width()
29+
h = win.winfo_height()
30+
x = (win.winfo_screenwidth() // 2) - (w // 2)
31+
y = (win.winfo_screenheight() // 2) - (h // 2)
32+
win.geometry(f"{w}x{h}+{x}+{y}")
33+
34+
35+
def open_help_window(
36+
parent: tk.Misc,
37+
help_path: Path,
38+
get_text: Callable[[str, str], str],
39+
section: Optional[str] = None,
40+
) -> None:
3541
"""Open a help window rendering the HELP.md text; simple ToC based on headings."""
36-
top = tk.Toplevel(parent); top.title(get_text("dialog.help.title", f"Help — {APP_NAME}").format(app=APP_NAME)); top.geometry("940x680")
42+
top = tk.Toplevel(parent)
43+
top.title(get_text("dialog.help.title", f"Help — {APP_NAME}").format(app=APP_NAME))
44+
top.geometry("940x700")
3745
top.transient(parent)
38-
container = ttk.Frame(top, padding=6); container.pack(fill="both", expand=True)
39-
searchf = ttk.Frame(container); searchf.pack(fill="x")
40-
ttk.Label(searchf, text="Search:").pack(side="left")
41-
qvar = tk.StringVar(); q = ttk.Entry(searchf, textvariable=qvar, width=50); q.pack(side="left", padx=6)
42-
body = ttk.Frame(container); body.pack(fill="both", expand=True)
43-
toc = tk.Listbox(body, width=28); toc.pack(side="left", fill="y", padx=(0,8))
44-
txt = ScrolledText(body, wrap="word"); txt.pack(side="right", fill="both", expand=True)
45-
46-
def do_search(*_):
46+
47+
container = ttk.Frame(top, padding=6)
48+
container.pack(fill="both", expand=True)
49+
50+
# -- search bar
51+
searchf = ttk.Frame(container)
52+
searchf.pack(fill="x")
53+
ttk.Label(searchf, text=get_text("dialog.help.search", "Search:")).pack(side="left")
54+
qvar = tk.StringVar()
55+
q = ttk.Entry(searchf, textvariable=qvar, width=50)
56+
q.pack(side="left", padx=6)
57+
58+
# -- main body (toc + text)
59+
body = ttk.Frame(container)
60+
body.pack(fill="both", expand=True)
61+
62+
toc = tk.Listbox(body, width=28)
63+
toc.pack(side="left", fill="y", padx=(0, 8))
64+
65+
txt = ScrolledText(body, wrap="word")
66+
txt.pack(side="right", fill="both", expand=True)
67+
68+
def do_search(*_args: object) -> None:
4769
term = qvar.get().strip().lower()
48-
if not term: return
70+
if not term:
71+
return
4972
content = txt.get("1.0", "end-1c").lower()
5073
pos = content.find(term)
5174
if pos >= 0:
52-
index = txt.index(f"1.0+{pos}c"); line = index.split(".")[0]
53-
txt.see(f"{line}.0"); txt.tag_remove("sel","1.0","end"); txt.tag_add("sel", f"{line}.0", f"{line}.0 lineend")
75+
index = txt.index(f"1.0+{pos}c")
76+
line = index.split(".")[0]
77+
txt.see(f"{line}.0")
78+
txt.tag_remove("sel", "1.0", "end")
79+
txt.tag_add("sel", f"{line}.0", f"{line}.0 lineend")
80+
5481
q.bind("<Return>", do_search)
5582

83+
# -- load help
5684
try:
57-
raw = Path(help_path).read_text(encoding="utf-8")
85+
raw = Path(help_path).read_text(encoding="utf-8", errors="ignore")
5886
except Exception:
5987
raw = "# Help\n\n" + get_text("dialog.help.not_found", "Help file not found.")
60-
txt.insert("end", raw); txt.config(state="disabled")
88+
txt.insert("end", raw)
89+
txt.config(state="disabled")
6190

6291
_center_on_screen(top)
6392

64-
lines = raw.splitlines(); anchors = []
65-
for i, line in enumerate(lines, start=1):
93+
# -- build toc
94+
anchors: list[tuple[str, int, int]] = []
95+
for i, line in enumerate(raw.splitlines(), start=1):
6696
if line.startswith("#"):
6797
depth = len(line) - len(line.lstrip("#"))
6898
title = line.lstrip("#").strip()
69-
anchors.append((title, i, depth)); toc.insert("end", (" "*(depth-1)) + title)
99+
anchors.append((title, i, depth))
100+
toc.insert("end", (" " * (depth - 1)) + title)
70101

71-
def jump(evt=None, target=None):
102+
def jump(_evt: Optional[tk.Event] = None, target: Optional[str] = None) -> None:
103+
lineno: Optional[int]
72104
if target is None:
73-
sel = toc.curselection();
74-
if not sel: return
75-
idx = sel[0]; _, lineno, _ = anchors[idx]
105+
sel = toc.curselection()
106+
if not sel:
107+
return
108+
idx = sel[0]
109+
_, lineno, _ = anchors[idx]
76110
else:
77111
lineno = None
78-
for title, ln, _ in anchors:
79-
if title.lower().startswith(target.lower()): lineno = ln; break
80-
if lineno is None: return
81-
txt.see(f"{lineno}.0"); txt.tag_remove("sel","1.0","end"); txt.tag_add("sel", f"{lineno}.0", f"{lineno}.0 lineend")
112+
t_lower = target.lower()
113+
for title, ln, _depth in anchors:
114+
if title.lower().startswith(t_lower):
115+
lineno = ln
116+
break
117+
if lineno is None:
118+
return
119+
txt.see(f"{lineno}.0")
120+
txt.tag_remove("sel", "1.0", "end")
121+
txt.tag_add("sel", f"{lineno}.0", f"{lineno}.0 lineend")
122+
82123
toc.bind("<<ListboxSelect>>", jump)
83-
if section: jump(target=section)
124+
if section:
125+
jump(target=section)
126+
127+
128+
def show_about_dialog(parent: tk.Misc, _help_path: Path, get_text: Callable[[str, str], str]) -> None:
129+
top = tk.Toplevel(parent)
130+
top.title(get_text("dialog.about.title", "About"))
131+
top.resizable(False, False)
132+
133+
frm = ttk.Frame(top, padding=12)
134+
frm.pack(fill="both", expand=True)
84135

85-
def show_about_dialog(parent: tk.Misc, help_path: Path, get_text):
86-
top = tk.Toplevel(parent); top.title(get_text("dialog.about.title", "About")); top.resizable(False, False)
87-
frm = ttk.Frame(top, padding=12); frm.pack(fill="both", expand=True)
88136
ttk.Label(frm, text=APP_NAME, font=("TkDefaultFont", 12, "bold")).pack(anchor="w")
89-
ttk.Label(frm, text=get_text("dialog.about.version", "Version: {version}").format(version=VERSION)).pack(anchor="w", pady=(0,8))
90-
ttk.Button(frm, text="Copy diagnostic info", command=lambda: _copy_diag(top)).pack(side="left")
91-
ttk.Button(frm, text="Open Help → Troubleshooting", command=lambda: open_help_window(parent, help_path, section="Troubleshooting")).pack(side="left", padx=6)
92-
ttk.Button(frm, text="Close", command=top.destroy).pack(side="right")
137+
ttk.Label(frm, text=get_text("dialog.about.version", "Version: {version}").format(version=VERSION)).pack(
138+
anchor="w", pady=(0, 8)
139+
)
140+
141+
info = ttk.Label(
142+
frm,
143+
text=get_text(
144+
"dialog.about.blurb",
145+
"A reliability-first Python GUI for yt-dlp and ffmpeg, designed for "
146+
"creating high-quality, tagged, and normalized MP3 archives from YouTube.",
147+
),
148+
wraplength=420,
149+
justify="left",
150+
)
151+
info.pack(anchor="w", pady=(0, 8))
152+
153+
def _tool_info(cmd: str, version_args: list[str]) -> list[str]:
154+
out: list[str] = []
155+
path = resolve_tool_path(cmd)
156+
if not path:
157+
out.append(f"{cmd}: not found")
158+
return out
159+
out.append(f"{cmd}: {path}")
160+
try:
161+
p = subprocess.run([path, *version_args], capture_output=True, text=True, check=False)
162+
first = (
163+
p.stdout.splitlines()[0]
164+
if p.stdout.strip()
165+
else (p.stderr.splitlines()[0] if p.stderr.strip() else "(no output)")
166+
)
167+
out.append(f"{cmd} version: {first}")
168+
except Exception as e:
169+
out.append(f"{cmd} version: error: {e}")
170+
return out
171+
172+
def _copy_diagnostics() -> None:
173+
lines: list[str] = []
174+
for tool, args in [
175+
("yt-dlp", ["--version"]),
176+
("ffmpeg", ["-version"]),
177+
("ffprobe", ["-version"]),
178+
("mp3gain", ["-v"]),
179+
]:
180+
lines.extend(_tool_info(tool, args))
181+
try:
182+
parent.clipboard_clear()
183+
parent.clipboard_append("\n".join(lines))
184+
except Exception:
185+
pass
186+
messagebox.showinfo(
187+
get_text("dialog.copied.title", "Diagnostics copied"),
188+
get_text("dialog.copied.body", "Diagnostic info copied to clipboard."),
189+
)
190+
191+
btns = ttk.Frame(frm)
192+
btns.pack(fill="x", pady=(8, 0))
193+
ttk.Button(btns, text=get_text("dialog.about.copy_diagnostics", "Copy diagnostics"), command=_copy_diagnostics).pack(
194+
side="left"
195+
)
196+
ttk.Button(btns, text=get_text("dialog.about.close", "Close"), command=top.destroy).pack(side="right")
93197

94198
_center_on_screen(top)
95-
96-
def _copy_diag(parent: tk.Misc):
97-
lines = []
98-
lines.append(f"{APP_NAME} {VERSION}".strip())
99-
lines.append(f"Python: {sys.version.splitlines()[0]}")
100-
lines.append(f"OS: {platform.system()} {platform.release()}")
101-
for tool, args in [("yt-dlp", ["--version"]),("ffmpeg", ["-version"]),("ffprobe", ["-version"]),("mp3gain", ["-v"])]:
102-
lines.extend(_tool_info(tool, args))
103-
try: parent.clipboard_clear(); parent.clipboard_append("\n".join(lines))
104-
except Exception: pass
105-
messagebox.showinfo(get_text("dialog.copied.title", "Diagnostics copied"), get_text("dialog.copied.body", "Diagnostic info copied to clipboard."))
106-
107-
def _find_tool_path(cmd: str) -> str | None:
108-
import shutil, os, sys
109-
# 1) PATH
110-
p = shutil.which(cmd)
111-
if p:
112-
return p
113-
# 2) Windows common locations
114-
if os.name == "nt":
115-
candidates = []
116-
local = os.environ.get("LOCALAPPDATA","")
117-
userprofile = os.environ.get("USERPROFILE","")
118-
program_files = os.environ.get("ProgramFiles","")
119-
program_files_x86 = os.environ.get("ProgramFiles(x86)","")
120-
# WinGet shim
121-
if local:
122-
candidates.append(os.path.join(local, "Microsoft", "WinGet", "Links", f"{cmd}.exe"))
123-
# Chocolatey shims
124-
candidates.append(os.path.join(os.environ.get("ProgramData","C:\\ProgramData"), "chocolatey", "bin", f"{cmd}.exe"))
125-
# Scoop shims
126-
if userprofile:
127-
candidates.append(os.path.join(userprofile, "scoop", "shims", f"{cmd}.exe"))
128-
# App-specific installs
129-
if cmd.lower() == "mp3gain":
130-
if program_files_x86:
131-
candidates.append(os.path.join(program_files_x86, "MP3Gain", "mp3gain.exe"))
132-
if program_files:
133-
candidates.append(os.path.join(program_files, "MP3Gain", "mp3gain.exe"))
134-
# ffmpeg/ffprobe common dir
135-
for base in [program_files, program_files_x86]:
136-
if base:
137-
candidates.append(os.path.join(base, "FFmpeg", "bin", f"{cmd}.exe"))
138-
for c in candidates:
139-
if c and os.path.exists(c):
140-
return c
141-
# 3) macOS/Homebrew typical
142-
if sys.platform == "darwin":
143-
for pth in ["/opt/homebrew/bin","/usr/local/bin"]:
144-
cand = os.path.join(pth, cmd)
145-
if os.path.exists(cand):
146-
return cand
147-
# 4) Linux common
148-
for pth in ["/usr/bin","/usr/local/bin"]:
149-
cand = os.path.join(pth, cmd)
150-
if os.path.exists(cand):
151-
return cand
152-
return None
153-
154-
def _tool_info(cmd: str, version_args: list[str]) -> list[str]:
155-
import shutil
156-
out = []; path = resolve_tool_path(cmd)
157-
if not path:
158-
out.append(f"{cmd}: not found"); return out
159-
out.append(f"{cmd}: {path}")
160-
try:
161-
p = subprocess.run([path, *version_args], capture_output=True, text=True, timeout=3)
162-
first = p.stdout.splitlines()[0] if p.stdout.strip() else (p.stderr.splitlines()[0] if p.stderr.strip() else "(no output)")
163-
out.append(f"{cmd} version: {first}")
164-
except Exception as e: out.append(f"{cmd} version: error: {e}")
165-
return out

0 commit comments

Comments
 (0)