""" Hermes Bridge Panel — Mini UI for controlling the Hermes-Houdini Bridge. Run in Houdini Python Shell: exec(open('/path/to/hermes_bridge_panel.py').read()) Or install as a permanent Python Panel. """ import hou import socket import os import json try: from PySide2 import QtWidgets, QtCore, QtGui except ImportError: from PySide6 import QtWidgets, QtCore, QtGui # ── Config persistence ────────────────────────────────── CONFIG_FILE = os.path.join(hou.homeHoudiniDirectory(), "hermes_bridge.json") def load_config(): try: with open(CONFIG_FILE) as f: return json.load(f) except (FileNotFoundError, json.JSONDecodeError): return {"port": 12345} def save_config(cfg): with open(CONFIG_FILE, "w") as f: json.dump(cfg, f, indent=2) # ── Bridge logic ──────────────────────────────────────── def bridge_status(port): result = hou.hscript("openport")[0] return f"Port {port} is open" in result def open_bridge(port): hou.hscript(f"closeport {port}") result, _ = hou.hscript(f"openport {port} 0.0.0.0") return result.strip() def close_bridge(port): result, _ = hou.hscript(f"closeport {port}") return result.strip() # ── UI ────────────────────────────────────────────────── class HermesBridgePanel(QtWidgets.QWidget): def __init__(self): super().__init__() self.cfg = load_config() self.port = self.cfg["port"] self.hostname = socket.gethostname() self._init_ui() self._refresh_status() def _init_ui(self): self.setWindowTitle("Hermes Bridge") self.setMinimumWidth(380) self.setStyleSheet(""" QWidget { background: #1e1e1e; color: #e0e0e0; font-size: 13px; } QPushButton { border: none; border-radius: 4px; padding: 8px 16px; font-weight: bold; } QPushButton#btnToggle { background: #2d2d2d; color: #e0e0e0; padding: 10px; font-size: 14px; } QPushButton#btnToggle[active="true"] { background: #1a3a1a; border: 1px solid #4a4; } QPushButton#btnCopy { background: #1565c0; color: white; } QPushButton#btnCopy:hover { background: #1976d2; } QLineEdit, QSpinBox { background: #2d2d2d; border: 1px solid #444; border-radius: 3px; padding: 6px 8px; color: #e0e0e0; } QLabel#statusLabel { font-size: 16px; font-weight: bold; } QLabel#hintLabel { color: #888; font-size: 11px; } """) layout = QtWidgets.QVBoxLayout() layout.setSpacing(10) layout.setContentsMargins(16, 16, 16, 16) # ── Header ────────────────────────────────────── header = QtWidgets.QHBoxLayout() icon = QtWidgets.QLabel("\U0001f916") # robot icon.setStyleSheet("font-size: 24px;") title = QtWidgets.QLabel("Hermes Bridge") title.setStyleSheet("font-size: 18px; font-weight: bold; color: #fff;") header.addWidget(icon) header.addWidget(title) header.addStretch() layout.addLayout(header) # ── Status ────────────────────────────────────── self.status_label = QtWidgets.QLabel("\u25cf Not connected") self.status_label.setObjectName("statusLabel") self.status_label.setStyleSheet("color: #ff5555;") layout.addWidget(self.status_label) # ── Port control ──────────────────────────────── port_layout = QtWidgets.QHBoxLayout() port_layout.addWidget(QtWidgets.QLabel("Port:")) self.port_spin = QtWidgets.QSpinBox() self.port_spin.setRange(1024, 65535) self.port_spin.setValue(self.port) self.port_spin.setFixedWidth(100) self.port_spin.valueChanged.connect(self._on_port_changed) port_layout.addWidget(self.port_spin) port_layout.addStretch() layout.addLayout(port_layout) # ── Toggle button ─────────────────────────────── self.btn_toggle = QtWidgets.QPushButton("Open Bridge") self.btn_toggle.setObjectName("btnToggle") self.btn_toggle.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) self.btn_toggle.clicked.connect(self._toggle_bridge) layout.addWidget(self.btn_toggle) # ── Prompt preset ─────────────────────────────── prompt_label = QtWidgets.QLabel("Copy this to Hermes:") prompt_label.setObjectName("hintLabel") layout.addWidget(prompt_label) prompt_layout = QtWidgets.QHBoxLayout() self.prompt_field = QtWidgets.QLineEdit() self.prompt_field.setReadOnly(True) self.prompt_field.setText(self._build_prompt()) self.prompt_field.setCursorPosition(0) prompt_layout.addWidget(self.prompt_field) self.btn_copy = QtWidgets.QPushButton("Copy") self.btn_copy.setObjectName("btnCopy") self.btn_copy.setFixedWidth(70) self.btn_copy.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) self.btn_copy.clicked.connect(self._copy_prompt) prompt_layout.addWidget(self.btn_copy) layout.addLayout(prompt_layout) # ── Alternatives ──────────────────────────────── alt_label = QtWidgets.QLabel( "Or just say: \"Hermes, I have Houdini open\"" ) alt_label.setObjectName("hintLabel") alt_label.setWordWrap(True) layout.addWidget(alt_label) # ── Bottom ────────────────────────────────────── layout.addStretch() version = QtWidgets.QLabel("v1.0 | hhb " + self.hostname) version.setObjectName("hintLabel") layout.addWidget(version) self.setLayout(layout) def _build_prompt(self): return ( f"Hey Hermes, I have a Houdini session running on " f"{self.hostname} (port {self.port})." ) def _refresh_status(self): is_open = bridge_status(self.port) if is_open: self.status_label.setText(f"\u25cf Connected \u2014 {self.hostname}:{self.port}") self.status_label.setStyleSheet("color: #4caf50; font-size: 16px; font-weight: bold;") self.btn_toggle.setText("Close Bridge") self.btn_toggle.setProperty("active", "true") self.btn_toggle.setStyleSheet(""" QPushButton#btnToggle { background: #2d1a1a; border: 1px solid #a44; color: #ff8888; padding: 10px; font-size: 14px; font-weight: bold; border-radius: 4px; } QPushButton#btnToggle:hover { background: #3d2020; } """) else: self.status_label.setText(f"\u25cf Not connected") self.status_label.setStyleSheet("color: #ff5555; font-size: 16px; font-weight: bold;") self.btn_toggle.setText("Open Bridge") self.btn_toggle.setProperty("active", "false") self.btn_toggle.setStyleSheet(""" QPushButton#btnToggle { background: #1a2d1a; border: 1px solid #4a4; color: #88cc88; padding: 10px; font-size: 14px; font-weight: bold; border-radius: 4px; } QPushButton#btnToggle:hover { background: #203d20; } """) self.prompt_field.setText(self._build_prompt()) def _toggle_bridge(self): if bridge_status(self.port): close_bridge(self.port) else: open_bridge(self.port) self._refresh_status() def _on_port_changed(self, value): was_open = bridge_status(self.port) if was_open: close_bridge(self.port) self.port = value self.cfg["port"] = value save_config(self.cfg) if was_open: open_bridge(self.port) self._refresh_status() def _copy_prompt(self): QtWidgets.QApplication.clipboard().setText(self.prompt_field.text()) self.btn_copy.setText("\u2713 Copied!") QtCore.QTimer.singleShot(2000, lambda: self.btn_copy.setText("Copy")) # ── Launch ────────────────────────────────────────────── def show(): """Show or focus the Hermes Bridge panel.""" for widget in QtWidgets.QApplication.allWidgets(): if isinstance(widget, HermesBridgePanel): widget.raise_() widget.activateWindow() return widget panel = HermesBridgePanel() panel.show() return panel