How Hippopotamoose keeps the UI responsive while running long operations.
PyQt6 UI code runs on a single thread — the main thread. Any blocking operation (network call, file write, subprocess wait) run directly on the main thread will freeze the entire UI until it completes. All long-running work in Hippopotamoose runs in QThread subclasses so the UI stays responsive.
Every worker follows the same structure:
class XxxWorker(QThread):
finished = pyqtSignal(bool, str) # success + result
status = pyqtSignal(str) # progress message
def __init__(self, ...):
super().__init__()
# store parameters
def run(self): # called when thread starts
try:
# ... do blocking work ...
self.status.emit("Working...")
self.finished.emit(True, result)
except Exception as e:
self.finished.emit(False, str(e))
Signals emitted from the worker thread are automatically queued and delivered to connected slots on the main thread by Qt's event loop — no manual locking required.
File: core/webcopy_worker.py
Purpose: Downloads an entire website using Cyotek WebCopy.
| Signal | Parameters | Meaning |
|---|---|---|
finished | (bool, slug, error_msg) | Download complete. bool = success. |
status | (str) | Current status message. |
progress | (int done, int total) | File count progress. |
Timeout: 5 minutes hard limit. Process killed if exceeded.
Progress parsing: Reads wcopy.exe stdout and extracts [done/total] patterns.
File: core/webcopy_queue.py
Purpose: Singleton concurrency pool. Ensures at most 3 WebCopy jobs run simultaneously. All new jobs wait in queue until a slot opens.
| Signal | Meaning |
|---|---|
copy_done(slug) | Pipeline site download succeeded. |
copy_failed(slug, error) | Pipeline download failed. |
example_done(slug) | Example site download succeeded. |
example_failed(slug, error) | Example download failed. |
queue_changed() | Queue length changed — update any UI counters. |
Failed jobs are tracked in a _failed list. Call retry_all_failed() to re-queue them.
File: core/deploy_worker.py
Purpose: Deploys a site to Cloudflare Pages via the deploy-demo.ps1 script.
| Signal | Parameters | Meaning |
|---|---|---|
finished | (bool, demo_url) | Deployment complete. demo_url is the live HTTPS URL on success. |
status | (str) | Current status. |
Timeout: 3 minutes. The PowerShell process runs hidden (no visible window).
File: core/cold_call_worker.py
Purpose: Runs Claude silently to extract contacts from downloaded site HTML.
Process: Runs claude -p "<prompt>" --model claude-sonnet-4-6 --effort low. Parses stdout for a JSON array. If parsing fails, falls back to regex extraction of phone numbers and emails.
| Signal | Parameters | Meaning |
|---|---|---|
finished | (bool, contacts_list) | Scraping complete. contacts_list is a list of dicts with type and value. |
status | (str) | Current status. |
File: core/process_monitor.py
Purpose: Watches a spawned terminal process until it exits. The AI steps (Demo, Form, Reiterate) open visible terminal windows — this worker detects when those windows close.
class ProcessMonitor(QThread):
finished = pyqtSignal(int) # exit code
def run(self):
self.process.wait()
self.finished.emit(self.process.returncode)
The pipeline row connects to this signal to update the step status to complete (exit code 0) or failed (any other exit code).
File: core/discovery_worker.py
Purpose: Iterates over spiral tiles and calls discover_businesses() for each one.
| Signal | Parameters | Meaning |
|---|---|---|
business_found | (name, url) | A new qualifying business was found. UI adds it to the queue list immediately. |
log | (str) | Informational message for the Discovery Panel status area. |
finished | (int count) | All tiles processed. count = total businesses found. |
error | (str) | API error occurred. |
File: core/backup_worker.py
Purpose: Copies edited_sites/<slug>/ to Site Backups/<slug>/Edition N/. N is auto-incremented. Called automatically after Form/Implement completes.