""" nuke_runner.py — Launch Nuke in terminal mode and stream its output. Nuke is started ONCE with: Nuke.exe -t process.py jobs.json stdout/stderr are merged and forwarded line-by-line via Qt signals so the GUI log window updates in real time. Progress is inferred from the structured output lines that process.py prints. """ from __future__ import annotations import re import subprocess from PySide6.QtCore import QThread, Signal # Lines emitted by process.py that carry progress information: # [3/10] Rendering: UNG_101_012_010_BG01_TT_v001 # [3/10] Done: UNG_101_012_010_BG01_TT_v001 _RENDERING_RE = re.compile(r"^\[(\d+)/(\d+)\]\s+Rendering:\s+(.+)$") _DONE_RE = re.compile(r"^\[(\d+)/(\d+)\]\s+Done:\s+(.+)$") class NukeRunner(QThread): """Runs a single Nuke process and emits its output as Qt signals. Signals ------- log_message(str) Each non-empty output line from Nuke. progress_update(int, int) (jobs_done, jobs_total) whenever a job completes. job_started(str) basename of the job Nuke has just started rendering. job_done(str) basename of the job Nuke has just finished. finished_signal(bool) True if Nuke exited with code 0, False otherwise. """ log_message: Signal = Signal(str) progress_update: Signal = Signal(int, int) job_started: Signal = Signal(str) job_done: Signal = Signal(str) finished_signal: Signal = Signal(bool) def __init__( self, nuke_exe: str, process_script: str, jobs_file: str, total: int = 0, parent=None, ) -> None: super().__init__(parent) self.nuke_exe = nuke_exe self.process_script = process_script self.jobs_file = jobs_file self.total = total def set_total(self, total: int) -> None: self.total = total # ── Thread entry point ──────────────────────────────────────────── def run(self) -> None: cmd = [self.nuke_exe, "--indie", "-t", self.process_script, self.jobs_file] self.log_message.emit("Launching: " + " ".join(cmd)) try: proc = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, ) except FileNotFoundError: self.log_message.emit( f"ERROR: Nuke executable not found: {self.nuke_exe}" ) self.finished_signal.emit(False) return except OSError as exc: self.log_message.emit(f"ERROR: Could not launch Nuke: {exc}") self.finished_signal.emit(False) return for raw_line in proc.stdout: line = raw_line.rstrip() if not line: continue self.log_message.emit(line) m = _DONE_RE.match(line) if m: current, total, basename = int(m.group(1)), int(m.group(2)), m.group(3) self.total = total self.progress_update.emit(current, total) self.job_done.emit(basename.strip()) continue m = _RENDERING_RE.match(line) if m: basename = m.group(3) self.job_started.emit(basename.strip()) proc.wait() self.finished_signal.emit(proc.returncode == 0)