716852aef8
- New: hermes_bridge_panel.py — PySide2 floating panel with: - Port configuration (spinner) - Open/Close toggle with green/red status - Auto-detected hostname - One-click copy prompt: 'Hey Hermes, I have Houdini on [host] port [N]' - Persists port setting across sessions - New: install.py — auto-detects houdini dir, copies panel + shelf - New: hermes_bridge.shelf — shelf tool integration - Updated: README with panel usage instructions
270 lines
9.8 KiB
Python
270 lines
9.8 KiB
Python
"""
|
|
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
|