commit 9f15ad3348e05f3dc9ef661fe580b0c02e4fc5c4 Author: Daniel Dybing Date: Tue Jan 13 16:35:58 2026 +0100 Add build workflow and requirements diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml new file mode 100644 index 0000000..f063465 --- /dev/null +++ b/.gitea/workflows/build.yaml @@ -0,0 +1,57 @@ +name: Build Binaries +run-name: ${{ gitea.actor }} is building binaries 🚀 + +on: [push] + +jobs: + build-linux: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install Dependencies + run: | + sudo apt-get update + sudo apt-get install -y python3-tk + pip install -r requirements.txt + + - name: Build with PyInstaller + run: | + pyinstaller --onefile --windowed --name vhs-decode-gui-linux vhs_decode_gui.py + + - name: Upload Linux Artifact + uses: actions/upload-artifact@v3 + with: + name: vhs-decode-gui-linux + path: dist/vhs-decode-gui-linux + + build-windows: + runs-on: windows-latest + steps: + - name: Check out repository code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install Dependencies + run: | + pip install -r requirements.txt + + - name: Build with PyInstaller + run: | + pyinstaller --onefile --windowed --name vhs-decode-gui-windows.exe vhs_decode_gui.py + + - name: Upload Windows Artifact + uses: actions/upload-artifact@v3 + with: + name: vhs-decode-gui-windows.exe + path: dist/vhs-decode-gui-windows.exe diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4f740c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +*.pyc +build/ +dist/ +*.spec +.env diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ef376ca --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pyinstaller diff --git a/session_summary.md b/session_summary.md new file mode 100644 index 0000000..645f136 --- /dev/null +++ b/session_summary.md @@ -0,0 +1,25 @@ +# Session Summary: VHS Decode GUI Development + +This session focused on creating a graphical user interface (GUI) wrapper for the `vhs-decode` command-line application. + +## Key Accomplishments: + +1. **CLI Argument Investigation**: Explored the `vhs-decode` command-line interface to understand its various arguments, including input/output files, video systems (NTSC, PAL), tape formats, tape speeds, and advanced settings like frequency and threads. +2. **Technology Choice**: Opted for Python with `tkinter` (using `ttk` for improved aesthetics) for the GUI development, balancing cross-platform compatibility with ease of development. +3. **Virtual Environment Setup**: Guided the user to create and activate a Python virtual environment (`.venv`) to manage dependencies. +4. **Basic GUI Implementation**: Developed the initial GUI structure including: + * Input and output file/directory selection with browse buttons. + * Core settings for Video System, Tape Format, Tape Speed, and RF Frequency (with CXADC option). + * Advanced settings for Threads, Start Frame, Length, Overwrite, Chroma Trap, and Debugging. + * "Start Decode Now" and "Stop Decode" buttons. + * An output log area to display the command-line application's output. +5. **Video System Refinement**: Modified the "Video System" dropdown to remove the "Default" option, ensuring users explicitly select a system (PAL, NTSC, PAL-M, NTSC-J) and setting "PAL" as the initial default. +6. **Queue System Integration**: Implemented a job queuing mechanism, inspired by tools like Handbrake: + * Added an "Add to Queue" button to capture current settings as a job. + * Introduced a `ttk.Notebook` widget at the bottom, separating the output log into a "Log" tab and adding a new "Queue" tab. + * The "Queue" tab features a `ttk.Treeview` to display queued jobs with their status (Pending, Running, Completed, Failed), input/output files, video system, and tape format. + * Added "Start Queue", "Remove Selected", and "Clear Queue" controls for queue management. + * Developed the logic to process queued jobs sequentially, updating their status in the UI and logging output to the "Log" tab. +7. **Enhanced File Dialogs (Linux)**: Integrated `zenity` to provide a more functional and native-looking file and directory selection experience on Linux systems, allowing users to create new folders within the dialog. The application gracefully falls back to `tkinter.filedialog` on Windows (which uses native Windows dialogs) or if `zenity` is not available on Linux. + +The resulting `vhs_decode_gui.py` script provides a user-friendly interface for interacting with the `vhs-decode` CLI application. \ No newline at end of file diff --git a/vhs_decode_gui.py b/vhs_decode_gui.py new file mode 100644 index 0000000..d51b29c --- /dev/null +++ b/vhs_decode_gui.py @@ -0,0 +1,507 @@ +import tkinter as tk +from tkinter import ttk, filedialog, messagebox +import subprocess +import threading +import queue +import os + +class VHSDecodeGUI: + def __init__(self, master): + self.master = master + master.title("VHS Decode GUI") + master.geometry("800x700") + + self.process = None + self.log_queue = queue.Queue() + self.job_queue = [] + self.is_queue_running = False + self.stop_queue_requested = False + + self.create_widgets() + self.master.after(100, self.poll_log_queue) # Start polling the log queue + + def create_widgets(self): + # Configure grid for responsiveness + self.master.columnconfigure(0, weight=1) + self.master.rowconfigure(5, weight=1) + + # Input File Selection + input_frame = ttk.LabelFrame(self.master, text="Input File") + input_frame.grid(row=0, column=0, sticky="ew", padx=10, pady=5) + input_frame.columnconfigure(0, weight=1) + + self.input_file_path = tk.StringVar() + self.input_entry = ttk.Entry(input_frame, textvariable=self.input_file_path) + self.input_entry.grid(row=0, column=0, sticky="ew", padx=5, pady=5) + self.input_button = ttk.Button(input_frame, text="Browse", command=self.browse_input_file) + self.input_button.grid(row=0, column=1, sticky="e", padx=5, pady=5) + + # Output File Selection + output_frame = ttk.LabelFrame(self.master, text="Output File (Base Name)") + output_frame.grid(row=1, column=0, sticky="ew", padx=10, pady=5) + output_frame.columnconfigure(0, weight=1) + + self.output_file_path = tk.StringVar() + self.output_entry = ttk.Entry(output_frame, textvariable=self.output_file_path) + self.output_entry.grid(row=0, column=0, sticky="ew", padx=5, pady=5) + self.output_button = ttk.Button(output_frame, text="Browse", command=self.browse_output_file) + self.output_button.grid(row=0, column=1, sticky="e", padx=5, pady=5) + + # Core Settings + settings_frame = ttk.LabelFrame(self.master, text="Core Settings") + settings_frame.grid(row=2, column=0, sticky="ew", padx=10, pady=5) + settings_frame.columnconfigure(1, weight=1) + + # System + ttk.Label(settings_frame, text="Video System:").grid(row=0, column=0, sticky="w", padx=5, pady=2) + self.system_var = tk.StringVar(value="PAL") + system_options = ["PAL", "NTSC", "PAL-M", "NTSC-J"] + self.system_menu = ttk.OptionMenu(settings_frame, self.system_var, "PAL", *system_options) + self.system_menu.grid(row=0, column=1, sticky="ew", padx=5, pady=2) + + # Tape Format + ttk.Label(settings_frame, text="Tape Format:").grid(row=1, column=0, sticky="w", padx=5, pady=2) + self.tape_format_var = tk.StringVar(value="VHS") + tape_formats = ["VHS", "SVHS", "HI8", "BETAMAX", "U-MATIC", "VIDEO2000", "UMATIC_HI", "VCR_LP", "VCR", "EIAJ", "VHSHQ", "VIDEO8", "BETAMAX_HIFI", "SVHS_ET", "QUADRUPLEX", "TYPEB", "SUPERBETA", "TYPEC"] + self.tape_format_menu = ttk.OptionMenu(settings_frame, self.tape_format_var, *tape_formats) + self.tape_format_menu.grid(row=1, column=1, sticky="ew", padx=5, pady=2) + + # Tape Speed + ttk.Label(settings_frame, text="Tape Speed:").grid(row=2, column=0, sticky="w", padx=5, pady=2) + self.tape_speed_var = tk.StringVar(value="SP") + tape_speeds = ["SP", "LP", "SLP", "EP", "VP"] + self.tape_speed_menu = ttk.OptionMenu(settings_frame, self.tape_speed_var, *tape_speeds) + self.tape_speed_menu.grid(row=2, column=1, sticky="ew", padx=5, pady=2) + + # Frequency + ttk.Label(settings_frame, text="RF Frequency (MHz):").grid(row=3, column=0, sticky="w", padx=5, pady=2) + self.frequency_var = tk.StringVar(value="40") + self.frequency_entry = ttk.Entry(settings_frame, textvariable=self.frequency_var) + self.frequency_entry.grid(row=3, column=1, sticky="ew", padx=5, pady=2) + self.cxadc_var = tk.BooleanVar(value=False) + self.cxadc_check = ttk.Checkbutton(settings_frame, text="Use CXADC frequency (~28.63 MHz)", variable=self.cxadc_var) + self.cxadc_check.grid(row=4, column=0, columnspan=2, sticky="w", padx=5, pady=2) + + # Advanced Settings + advanced_frame = ttk.LabelFrame(self.master, text="Advanced Settings") + advanced_frame.grid(row=3, column=0, sticky="ew", padx=10, pady=5) + advanced_frame.columnconfigure(0, weight=1) + + ttk.Label(advanced_frame, text="Threads:").grid(row=0, column=0, sticky="w", padx=5, pady=2) + self.threads_var = tk.IntVar(value=os.cpu_count() or 1) + self.threads_spinbox = ttk.Spinbox(advanced_frame, from_=1, to=os.cpu_count() * 2 or 16, textvariable=self.threads_var, width=5) + self.threads_spinbox.grid(row=0, column=1, sticky="ew", padx=5, pady=2) + + ttk.Label(advanced_frame, text="Start Frame:").grid(row=1, column=0, sticky="w", padx=5, pady=2) + self.start_frame_var = tk.IntVar(value=0) + self.start_frame_entry = ttk.Entry(advanced_frame, textvariable=self.start_frame_var, width=10) + self.start_frame_entry.grid(row=1, column=1, sticky="ew", padx=5, pady=2) + + ttk.Label(advanced_frame, text="Length (frames):").grid(row=2, column=0, sticky="w", padx=5, pady=2) + self.length_var = tk.IntVar(value=0) + self.length_entry = ttk.Entry(advanced_frame, textvariable=self.length_var, width=10) + self.length_entry.grid(row=2, column=1, sticky="ew", padx=5, pady=2) + + self.overwrite_var = tk.BooleanVar(value=False) + self.overwrite_check = ttk.Checkbutton(advanced_frame, text="Overwrite existing files", variable=self.overwrite_var) + self.overwrite_check.grid(row=3, column=0, columnspan=2, sticky="w", padx=5, pady=2) + + self.chroma_trap_var = tk.BooleanVar(value=False) + self.chroma_trap_check = ttk.Checkbutton(advanced_frame, text="Enable Chroma Trap", variable=self.chroma_trap_var) + self.chroma_trap_check.grid(row=4, column=0, columnspan=2, sticky="w", padx=5, pady=2) + + self.debug_var = tk.BooleanVar(value=False) + self.debug_check = ttk.Checkbutton(advanced_frame, text="Enable Debug Logging", variable=self.debug_var) + self.debug_check.grid(row=5, column=0, columnspan=2, sticky="w", padx=5, pady=2) + + self.ire0_adjust_var = tk.BooleanVar(value=False) + self.ire0_adjust_check = ttk.Checkbutton(advanced_frame, text="ire0_adjust", variable=self.ire0_adjust_var) + self.ire0_adjust_check.grid(row=6, column=0, columnspan=2, sticky="w", padx=5, pady=2) + + self.recheck_phase_var = tk.BooleanVar(value=False) + self.recheck_phase_check = ttk.Checkbutton(advanced_frame, text="recheck_phase", variable=self.recheck_phase_var) + self.recheck_phase_check.grid(row=7, column=0, columnspan=2, sticky="w", padx=5, pady=2) + + + # Control Buttons + button_frame = ttk.Frame(self.master) + button_frame.grid(row=4, column=0, sticky="ew", padx=10, pady=5) + button_frame.columnconfigure(0, weight=1) + button_frame.columnconfigure(1, weight=1) + button_frame.columnconfigure(2, weight=1) + + self.start_button = ttk.Button(button_frame, text="Start Decode Now", command=self.run_current_job) + self.start_button.grid(row=0, column=0, sticky="ew", padx=5, pady=5) + + self.add_queue_button = ttk.Button(button_frame, text="Add to Queue", command=self.add_to_queue) + self.add_queue_button.grid(row=0, column=1, sticky="ew", padx=5, pady=5) + + self.stop_button = ttk.Button(button_frame, text="Stop", command=self.stop_decode, state=tk.DISABLED) + self.stop_button.grid(row=0, column=2, sticky="ew", padx=5, pady=5) + + + # Bottom Area: Notebook for Queue and Log + self.notebook = ttk.Notebook(self.master) + self.notebook.grid(row=5, column=0, sticky="nsew", padx=10, pady=5) + + # Queue Tab + queue_frame = ttk.Frame(self.notebook) + self.notebook.add(queue_frame, text="Queue") + queue_frame.columnconfigure(0, weight=1) + queue_frame.rowconfigure(0, weight=1) + + # Queue Treeview + columns = ("status", "input", "output", "system", "format") + self.queue_tree = ttk.Treeview(queue_frame, columns=columns, show="headings", height=8) + self.queue_tree.heading("status", text="Status") + self.queue_tree.heading("input", text="Input File") + self.queue_tree.heading("output", text="Output Base") + self.queue_tree.heading("system", text="System") + self.queue_tree.heading("format", text="Format") + + self.queue_tree.column("status", width=80) + self.queue_tree.column("input", width=200) + self.queue_tree.column("output", width=150) + self.queue_tree.column("system", width=60) + self.queue_tree.column("format", width=60) + + self.queue_tree.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) + + queue_scrollbar = ttk.Scrollbar(queue_frame, orient="vertical", command=self.queue_tree.yview) + queue_scrollbar.grid(row=0, column=1, sticky="ns") + self.queue_tree.configure(yscrollcommand=queue_scrollbar.set) + + # Queue Controls + queue_controls = ttk.Frame(queue_frame) + queue_controls.grid(row=1, column=0, columnspan=2, sticky="ew", padx=5, pady=5) + + self.start_queue_button = ttk.Button(queue_controls, text="Start Queue", command=self.start_queue) + self.start_queue_button.pack(side=tk.LEFT, padx=5) + + self.remove_queue_button = ttk.Button(queue_controls, text="Remove Selected", command=self.remove_from_queue) + self.remove_queue_button.pack(side=tk.LEFT, padx=5) + + self.clear_queue_button = ttk.Button(queue_controls, text="Clear Queue", command=self.clear_queue) + self.clear_queue_button.pack(side=tk.LEFT, padx=5) + + + # Log Tab + log_frame = ttk.Frame(self.notebook) + self.notebook.add(log_frame, text="Log") + log_frame.columnconfigure(0, weight=1) + log_frame.rowconfigure(0, weight=1) + + self.log_text = tk.Text(log_frame, state=tk.DISABLED, wrap=tk.WORD, height=10) + self.log_text.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) + log_scrollbar = ttk.Scrollbar(log_frame, command=self.log_text.yview) + log_scrollbar.grid(row=0, column=1, sticky="ns") + self.log_text['yscrollcommand'] = log_scrollbar.set + + def _linux_file_dialog(self, title, filetypes): + # Construct zenity command + cmd = ["zenity", "--file-selection", f"--title={title}"] + + # Add file filters + # filetypes is a list of tuples: [("Name", "*.ext *.ext2"), ...] + for name, pattern in filetypes: + # Zenity format: --file-filter=Name | *.ext *.ext2 + filter_str = f"{name} | {pattern}" + cmd.append(f"--file-filter={filter_str}") + + try: + result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if result.returncode == 0: + return result.stdout.strip() + except FileNotFoundError: + return None + return None + + def _linux_dir_dialog(self, title, initialdir=None): + cmd = ["zenity", "--file-selection", "--directory", f"--title={title}"] + + try: + result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if result.returncode == 0: + return result.stdout.strip() + except FileNotFoundError: + return None + return None + + def browse_input_file(self): + if os.name != 'nt' and os.path.exists("/usr/bin/zenity"): + file_path = self._linux_file_dialog( + title="Select Input RF File", + filetypes=[("RF Files", "*.u8 *.u16"), ("All Files", "*")] + ) + else: + file_path = filedialog.askopenfilename( + title="Select Input RF File", + filetypes=[("RF Files", "*.u8 *.u16"), ("All Files", "*.*")] + ) + + if file_path: + self.input_file_path.set(file_path) + + def browse_output_file(self): + initial_dir = os.path.dirname(self.input_file_path.get()) if self.input_file_path.get() else "" + initial_file = os.path.splitext(os.path.basename(self.input_file_path.get()))[0] if self.input_file_path.get() else "output" + + if os.name != 'nt' and os.path.exists("/usr/bin/zenity"): + dir_path = self._linux_dir_dialog(title="Select Output Directory", initialdir=initial_dir) + else: + dir_path = filedialog.askdirectory(title="Select Output Directory", initialdir=initial_dir) + + if dir_path: + self.output_file_path.set(os.path.join(dir_path, initial_file)) + + def get_command_args(self): + input_file = self.input_file_path.get() + output_file = self.output_file_path.get() + + if not input_file: + return None, "Please select an input file." + if not output_file: + return None, "Please specify an output file base name." + + command = [] + command.append(input_file) + command.append(output_file) + + system_selection = self.system_var.get() + if system_selection == "NTSC": + command.append("-n") + elif system_selection == "PAL": + command.append("-p") + elif system_selection == "PAL-M": + command.append("--pm") + elif system_selection == "NTSC-J": + command.append("--NTSCJ") + + command.extend(["--tf", self.tape_format_var.get()]) + command.extend(["--ts", self.tape_speed_var.get()]) + + if self.cxadc_var.get(): + command.append("--cxadc") + else: + try: + freq_val = float(self.frequency_var.get()) + if freq_val > 0: + command.extend(["-f", self.frequency_var.get()]) + except ValueError: + pass # Ignore invalid frequency, let default handle or it was already warned? + # Ideally validation happens before calling this. + + threads = self.threads_var.get() + if threads > 0: + command.extend(["-t", str(threads)]) + + start_frame = self.start_frame_var.get() + if start_frame > 0: + command.extend(["-s", str(start_frame)]) + + length = self.length_var.get() + if length > 0: + command.extend(["-l", str(length)]) + + if self.overwrite_var.get(): + command.append("--overwrite") + if self.chroma_trap_var.get(): + command.append("--ct") + if self.debug_var.get(): + command.append("--debug") + if self.ire0_adjust_var.get(): + command.append("--ire0_adjust") + if self.recheck_phase_var.get(): + command.append("--recheck_phase") + + job_details = { + 'input': input_file, + 'output': output_file, + 'system': system_selection, + 'format': self.tape_format_var.get(), + 'command': command + } + return job_details, None + + def add_to_queue(self): + job_details, error = self.get_command_args() + if error: + messagebox.showerror("Error", error) + return + + self.job_queue.append(job_details) + self.queue_tree.insert("", tk.END, values=("Pending", job_details['input'], job_details['output'], job_details['system'], job_details['format'])) + self.notebook.select(0) # Switch to queue tab + + def remove_from_queue(self): + selected_item = self.queue_tree.selection() + if not selected_item: + return + + # We need to map treeview item to list index. + # Since we append to both, indices should match. + # However, if we remove from middle, we need to be careful. + # It's better to just rebuild the list or store the ID. + # Simple approach: Iterate backwards to avoid index shifting issues if multiple selected? + # Treeview selection returns tuple of IDs. + + # Let's find the index of the selected item + for item in selected_item: + index = self.queue_tree.index(item) + del self.job_queue[index] + self.queue_tree.delete(item) + + def clear_queue(self): + self.job_queue = [] + self.queue_tree.delete(*self.queue_tree.get_children()) + + def run_current_job(self): + job_details, error = self.get_command_args() + if error: + messagebox.showerror("Error", error) + return + + self.notebook.select(1) # Switch to Log tab + threading.Thread(target=self._run_single_job_thread, args=(job_details['command'],)).start() + + def start_queue(self): + if self.is_queue_running: + return + if not self.job_queue: + messagebox.showinfo("Queue", "Queue is empty.") + return + + self.is_queue_running = True + self.stop_queue_requested = False + self.start_queue_button.config(state=tk.DISABLED) + self.remove_queue_button.config(state=tk.DISABLED) + self.clear_queue_button.config(state=tk.DISABLED) + self.add_queue_button.config(state=tk.DISABLED) + self.start_button.config(state=tk.DISABLED) + + self.notebook.select(1) # Switch to Log tab + threading.Thread(target=self._run_queue_thread).start() + + def _run_subprocess(self, command): + if os.name == 'nt': + cli_command = "decode" + else: + cli_command = "vhs-decode" + + full_command = [cli_command] + command + + try: + self.process = subprocess.Popen( + full_command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=1, + universal_newlines=True + ) + self.stop_button.config(state=tk.NORMAL) + + for line in self.process.stdout: + self.log_queue.put(line) + for line in self.process.stderr: + self.log_queue.put(line) + + self.process.wait() + return self.process.returncode + except FileNotFoundError: + self.log_queue.put(f"Error: '{cli_command}' command not found.\n") + return -1 + except Exception as e: + self.log_queue.put(f"An unexpected error occurred: {e}\n") + return -1 + finally: + self.process = None + self.master.after(0, lambda: self.stop_button.config(state=tk.DISABLED)) + + def _run_single_job_thread(self, command): + self.start_button.config(state=tk.DISABLED) + self.log_queue.put("--- Starting Decode Process ---\n") + + self._run_subprocess(command) + + self.log_queue.put("\n--- Process Finished ---\n") + self.master.after(0, lambda: self.start_button.config(state=tk.NORMAL)) + + def _run_queue_thread(self): + # Iterate through a copy or just index? + # We want to process items that are "Pending". + + items = self.queue_tree.get_children() + for index, item_id in enumerate(items): + if self.stop_queue_requested: + self.log_queue.put("\n--- Queue Execution Stopped by User ---\n") + break + + # Check status + values = self.queue_tree.item(item_id, "values") + if values[0] == "Completed": + continue + + # Update status to Running + new_values = ("Running",) + values[1:] + self.master.after(0, lambda i=item_id, v=new_values: self.queue_tree.item(i, values=v)) + + # Get command + job = self.job_queue[index] + self.log_queue.put(f"\n--- Starting Job {index + 1}/{len(items)}: {job['input']} ---\n") + + # Run + return_code = self._run_subprocess(job['command']) + + # Update Status + status = "Completed" if return_code == 0 else "Failed" + final_values = (status,) + values[1:] + self.master.after(0, lambda i=item_id, v=final_values: self.queue_tree.item(i, values=v)) + + if return_code != 0: + self.log_queue.put(f"Job failed with code {return_code}. Continuing queue...\n") + # Optionally break? For now continue. + + self.is_queue_running = False + self.master.after(0, lambda: self.start_queue_button.config(state=tk.NORMAL)) + self.master.after(0, lambda: self.remove_queue_button.config(state=tk.NORMAL)) + self.master.after(0, lambda: self.clear_queue_button.config(state=tk.NORMAL)) + self.master.after(0, lambda: self.add_queue_button.config(state=tk.NORMAL)) + self.master.after(0, lambda: self.start_button.config(state=tk.NORMAL)) + + def stop_decode(self): + if self.is_queue_running: + self.stop_queue_requested = True + + if self.process and self.process.poll() is None: + self.log_queue.put("\n--- Attempting to terminate process... ---\n") + try: + os.killpg(os.getpgid(self.process.pid), subprocess.SIGTERM) + self.process.wait(timeout=5) + self.log_queue.put("--- Process terminated. ---\n") + except Exception as e: + if self.process: + self.process.kill() + self.log_queue.put(f"Error terminating process: {e}. Force killing...\n") + else: + self.log_queue.put("\n--- No active process to stop. ---\n") + + def poll_log_queue(self): + while not self.log_queue.empty(): + try: + line = self.log_queue.get_nowait() + self.log_text.config(state=tk.NORMAL) + self.log_text.insert(tk.END, line) + self.log_text.see(tk.END) + self.log_text.config(state=tk.DISABLED) + except queue.Empty: + pass + self.master.after(100, self.poll_log_queue) + +def main(): + root = tk.Tk() + app = VHSDecodeGUI(root) + root.mainloop() + +if __name__ == "__main__": + main() \ No newline at end of file