v0.4: Official Houdini package system integration

- hermes_houdini_bridge.json: package descriptor (generated by install.sh)
- scripts/python/hermes_bridge_panel.py: moved to package-standard location
- python_panels/hermes_bridge.pypanel: registered in Windows menu
- install.sh: one-command installer (generates JSON with correct path)
- git clone + install.sh = fully installed; git pull = update
- Panel opens via Windows -> Hermes Bridge (no Python Shell needed)
This commit is contained in:
Hermes
2026-07-12 13:02:28 +00:00
parent bc3aa89db5
commit 06b9de183b
5 changed files with 323 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
__pycache__/
+8
View File
@@ -0,0 +1,8 @@
{
"hpath": "/home/niklas/Documents/projects_privat/hermes-houdini-bridge",
"env": [
{"HHB_PACKAGE": "/home/niklas/Documents/projects_privat/hermes-houdini-bridge"}
],
"load_package_once": true,
"enable": "houdini_version >= '21.0'"
}
Executable
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# ──────────────────────────────────────────────────────────
# Hermes-Houdini Bridge — Installer
# Generates the Houdini package descriptor with the correct
# absolute path to this repo, so Houdini finds the panel.
# ──────────────────────────────────────────────────────────
set -euo pipefail
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
HOUDINI_PREFS="${HOUDINI_USER_PREF_DIR:-$HOME/houdini21.0}"
PACKAGES_DIR="$HOUDINI_PREFS/packages"
DESCRIPTOR="$PACKAGES_DIR/hermes_houdini_bridge.json"
echo "=== Hermes-Houdini Bridge Installer ==="
echo "Repo: $REPO_DIR"
echo "Houdini: $HOUDINI_PREFS"
echo ""
mkdir -p "$PACKAGES_DIR"
cat > "$DESCRIPTOR" << EOF
{
"hpath": "$REPO_DIR",
"env": [
{"HHB_PACKAGE": "$REPO_DIR"}
],
"load_package_once": true,
"enable": "houdini_version >= '21.0'"
}
EOF
echo "✓ Package descriptor installed"
echo " $DESCRIPTOR"
echo ""
echo "Restart Houdini, then find the panel under:"
echo " Windows → Hermes Bridge"
echo ""
echo "To update: cd $REPO_DIR && git pull"
echo "To uninstall: rm $DESCRIPTOR"
+6
View File
@@ -0,0 +1,6 @@
{
"interface": "hermes_bridge_panel.show",
"label": "Hermes Bridge",
"icon": "MISC_python",
"help_url": "https://git.niklashmotion.art/Hermes/hermes-houdini-bridge"
}
+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