v0.3: PySide2 mini-UI panel + installer

- 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
This commit is contained in:
Hermes
2026-07-12 12:46:02 +00:00
parent 79aa35df6b
commit 716852aef8
4 changed files with 358 additions and 4 deletions
+17 -4
View File
@@ -17,13 +17,26 @@ No custom server needed — uses Houdini's built-in `openport` + `hcommand` prot
## Quick Start
### 1. Install the Shelf Tool (once)
### 1. Install (once)
Copy `houdini/shelf/hermes_bridge.shelf` to your Houdini toolbar, or run the shelf script directly in Houdini's Python Shell.
```bash
cd houdini && python3 install.py
```
### 2. Open the Bridge (every session)
Or manually: copy `hermes_bridge_panel.py` to `~/houdini21.0/scripts/python/`.
Click the **Hermes Bridge** shelf button in Houdini. It opens port 12345 and shows a status dialog.
### 2. Open the Panel (every session)
In Houdini Python Shell (`Alt+Shift+P`):
```python
exec(open(hou.homeHoudiniDirectory() + '/scripts/python/hermes_bridge_panel.py').read())
show()
```
The panel lets you:
- Choose a port number
- Open/Close the bridge with one click
- Copy a ready-to-use prompt: *"Hey Hermes, I have a Houdini session running on [hostname] (port 12345)"*
### 3. Use from Hermes
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""
Install Hermes Bridge into Houdini.
Copies the Python panel to ~/houdini21.0/scripts/python/
and the shelf tool to ~/houdini21.0/toolbar/
Usage: python install.py [--houdini-dir ~/houdini21.0]
"""
import os
import sys
import shutil
import argparse
FILES = {
"hermes_bridge_panel.py": "scripts/python/",
"hermes_bridge.shelf": "toolbar/",
}
def get_houdini_dir():
"""Auto-detect Houdini prefs directory."""
home = os.path.expanduser("~")
for d in sorted(os.listdir(home), reverse=True):
if d.startswith("houdini") and os.path.isdir(os.path.join(home, d)):
return os.path.join(home, d)
return os.path.join(home, "houdini21.0")
def main():
parser = argparse.ArgumentParser(description="Install Hermes Bridge into Houdini")
parser.add_argument(
"--houdini-dir",
default=get_houdini_dir(),
help=f"Houdini preferences directory (default: {get_houdini_dir()})",
)
args = parser.parse_args()
src_dir = os.path.dirname(os.path.abspath(__file__))
for filename, subdir in FILES.items():
src = os.path.join(src_dir, filename)
dst_dir = os.path.join(os.path.expanduser(args.houdini_dir), subdir)
dst = os.path.join(dst_dir, filename)
if not os.path.exists(src):
print(f" SKIP {filename} — source not found")
continue
os.makedirs(dst_dir, exist_ok=True)
shutil.copy2(src, dst)
print(f" COPY {filename} -> {dst}")
print(f"\nInstalled to {args.houdini_dir}")
print("Restart Houdini — the shelf tool appears under 'Hermes Bridge'.")
if __name__ == "__main__":
main()
+269
View File
@@ -0,0 +1,269 @@
"""
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
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<shelfDocument>
<tools>
<tool name="hermes_bridge" label="Hermes Bridge" icon="MISC_python">
<script scriptType="python"><![CDATA[
import sys
sys.path.insert(0, hou.getenv("HHB_PATH") or hou.homeHoudiniDirectory() + "/scripts/python")
exec(open(hou.homeHoudiniDirectory() + "/scripts/python/hermes_bridge_panel.py").read())
hermes_bridge_panel.show()
]]></script>
</tool>
</tools>
</shelfDocument>