Initial Version
This commit is contained in:
commit
bdf4330738
5 changed files with 383 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
*.log
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
79
README.md
Normal file
79
README.md
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
# HudFX LNX
|
||||||
|
|
||||||
|
A transparent, always-on-top screen overlay for HudFX alerts on Linux. Displays your HudFX browser overlay URL on a chosen monitor without interfering with other windows or inputs. Optionally launches automatically with OBS.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
**Python 3** and the following packages:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install python3-pyqt5 python3-pyqt5.qtwebengine
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### 1. Configure the overlay
|
||||||
|
|
||||||
|
Run the tray app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ~/projects/HudFX_LNX/hudfx.py
|
||||||
|
```
|
||||||
|
|
||||||
|
A tray icon will appear. Right-click → **Settings** and enter:
|
||||||
|
- Your HudFX overlay URL
|
||||||
|
- Which monitor to display on (picked from a list of detected screens)
|
||||||
|
|
||||||
|
Settings are saved to `~/.config/hudfx-lnx/config.json`.
|
||||||
|
|
||||||
|
### 2. Using the overlay
|
||||||
|
|
||||||
|
The overlay starts automatically when the tray app launches. Right-click the tray icon to:
|
||||||
|
- **Stop overlay** / **Start overlay** — toggle it on or off
|
||||||
|
- **Settings** — change URL or monitor
|
||||||
|
- **Quit** — close everything
|
||||||
|
|
||||||
|
### 3. Auto-start with OBS (optional)
|
||||||
|
|
||||||
|
To have the overlay launch and close automatically with OBS:
|
||||||
|
|
||||||
|
1. Open OBS → **Tools → Scripts**
|
||||||
|
2. Click **+** and select `~/projects/HudFX_LNX/obs_hudfx.py`
|
||||||
|
|
||||||
|
The overlay will start when OBS finishes loading and stop when OBS exits. The tray icon still appears so you can toggle it off mid-session if needed.
|
||||||
|
|
||||||
|
The OBS script auto-detects whether OBS is installed as a Flatpak or natively and adjusts accordingly — no manual configuration needed.
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
When launched via OBS, the overlay logs to:
|
||||||
|
|
||||||
|
```
|
||||||
|
~/projects/HudFX_LNX/hudfx.log
|
||||||
|
```
|
||||||
|
|
||||||
|
OBS script activity (start/stop events, errors) is visible in OBS under **Tools → Scripts → Script Log**.
|
||||||
|
|
||||||
|
To follow the overlay log in real time:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tail -f ~/projects/HudFX_LNX/hudfx.log
|
||||||
|
```
|
||||||
|
|
||||||
|
`hudfx.log` is excluded from version control via `.gitignore`.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `hudfx.py` | Main tray app — run this directly for standalone use |
|
||||||
|
| `obs_hudfx.py` | OBS script — add via OBS Tools → Scripts |
|
||||||
|
| `overlay.py` | Standalone overlay (no tray UI, takes URL as argument) |
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The overlay is click-through — you can interact with everything underneath it normally
|
||||||
|
- Audio and video autoplay are enabled — no interaction needed for alerts to play
|
||||||
|
- No cookies or data are written to disk (off-the-record browser profile)
|
||||||
|
- Tested on Linux Mint 21.3 (Virginia) with OBS 31.1.2 (Flatpak), 3-monitor setup (2560×1440 center + two 1080p)
|
||||||
|
- Compatible with both native and Flatpak OBS installations
|
||||||
171
hudfx.py
Normal file
171
hudfx.py
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import signal
|
||||||
|
import os
|
||||||
|
from PyQt5.QtCore import Qt, QUrl, QTimer
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QApplication, QMainWindow, QSystemTrayIcon, QMenu, QAction,
|
||||||
|
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
|
||||||
|
QComboBox, QPushButton, QMessageBox
|
||||||
|
)
|
||||||
|
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile, QWebEnginePage, QWebEngineSettings
|
||||||
|
from PyQt5.QtGui import QColor, QIcon
|
||||||
|
|
||||||
|
CONFIG_PATH = os.path.expanduser("~/.config/hudfx-lnx/config.json")
|
||||||
|
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
if os.path.exists(CONFIG_PATH):
|
||||||
|
with open(CONFIG_PATH) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return {"url": "", "screen_index": 0}
|
||||||
|
|
||||||
|
|
||||||
|
def save_config(config):
|
||||||
|
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
|
||||||
|
with open(CONFIG_PATH, "w") as f:
|
||||||
|
json.dump(config, f, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
class Overlay(QMainWindow):
|
||||||
|
def __init__(self, url, screen):
|
||||||
|
super().__init__()
|
||||||
|
self.setWindowFlags(
|
||||||
|
Qt.FramelessWindowHint |
|
||||||
|
Qt.WindowStaysOnTopHint |
|
||||||
|
Qt.Tool |
|
||||||
|
Qt.WindowTransparentForInput
|
||||||
|
)
|
||||||
|
self.setAttribute(Qt.WA_TranslucentBackground)
|
||||||
|
|
||||||
|
view = QWebEngineView()
|
||||||
|
profile = QWebEngineProfile(view)
|
||||||
|
page = QWebEnginePage(profile, view)
|
||||||
|
view.setPage(page)
|
||||||
|
view.page().setBackgroundColor(QColor(0, 0, 0, 0))
|
||||||
|
view.page().setAudioMuted(False)
|
||||||
|
|
||||||
|
settings = view.page().settings()
|
||||||
|
settings.setAttribute(QWebEngineSettings.PlaybackRequiresUserGesture, False)
|
||||||
|
settings.setAttribute(QWebEngineSettings.JavascriptEnabled, True)
|
||||||
|
|
||||||
|
view.load(QUrl(url))
|
||||||
|
self.setCentralWidget(view)
|
||||||
|
self.setGeometry(screen.geometry())
|
||||||
|
self.show()
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsDialog(QDialog):
|
||||||
|
def __init__(self, config, screens, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setWindowTitle("HudFX Settings")
|
||||||
|
self.setMinimumWidth(420)
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
layout.addWidget(QLabel("Overlay URL:"))
|
||||||
|
self.url_input = QLineEdit(config.get("url", ""))
|
||||||
|
self.url_input.setPlaceholderText("https://...")
|
||||||
|
layout.addWidget(self.url_input)
|
||||||
|
|
||||||
|
layout.addWidget(QLabel("Display monitor:"))
|
||||||
|
self.screen_combo = QComboBox()
|
||||||
|
for i, screen in enumerate(screens):
|
||||||
|
s = screen.size()
|
||||||
|
name = f"Monitor {i + 1} — {s.width()}x{s.height()} ({screen.name()})"
|
||||||
|
self.screen_combo.addItem(name)
|
||||||
|
self.screen_combo.setCurrentIndex(config.get("screen_index", 0))
|
||||||
|
layout.addWidget(self.screen_combo)
|
||||||
|
|
||||||
|
buttons = QHBoxLayout()
|
||||||
|
save_btn = QPushButton("Save")
|
||||||
|
cancel_btn = QPushButton("Cancel")
|
||||||
|
save_btn.clicked.connect(self.accept)
|
||||||
|
cancel_btn.clicked.connect(self.reject)
|
||||||
|
buttons.addWidget(save_btn)
|
||||||
|
buttons.addWidget(cancel_btn)
|
||||||
|
layout.addLayout(buttons)
|
||||||
|
|
||||||
|
def get_values(self):
|
||||||
|
return {
|
||||||
|
"url": self.url_input.text().strip(),
|
||||||
|
"screen_index": self.screen_combo.currentIndex(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TrayApp:
|
||||||
|
def __init__(self, app):
|
||||||
|
self.app = app
|
||||||
|
self.config = load_config()
|
||||||
|
self.overlay = None
|
||||||
|
|
||||||
|
self.tray = QSystemTrayIcon()
|
||||||
|
self.tray.setToolTip("HudFX Overlay")
|
||||||
|
|
||||||
|
icon = QIcon.fromTheme("video-display")
|
||||||
|
if icon.isNull():
|
||||||
|
icon = QIcon.fromTheme("preferences-desktop-display")
|
||||||
|
self.tray.setIcon(icon)
|
||||||
|
|
||||||
|
self.menu = QMenu()
|
||||||
|
|
||||||
|
self.toggle_action = QAction("Start overlay")
|
||||||
|
self.toggle_action.triggered.connect(self.toggle_overlay)
|
||||||
|
self.menu.addAction(self.toggle_action)
|
||||||
|
|
||||||
|
self.settings_action = QAction("Settings...")
|
||||||
|
self.settings_action.triggered.connect(self.open_settings)
|
||||||
|
self.menu.addAction(self.settings_action)
|
||||||
|
|
||||||
|
self.menu.addSeparator()
|
||||||
|
|
||||||
|
self.quit_action = QAction("Quit")
|
||||||
|
self.quit_action.triggered.connect(self.quit)
|
||||||
|
self.menu.addAction(self.quit_action)
|
||||||
|
|
||||||
|
self.tray.setContextMenu(self.menu)
|
||||||
|
self.tray.show()
|
||||||
|
|
||||||
|
self.toggle_overlay()
|
||||||
|
|
||||||
|
def toggle_overlay(self):
|
||||||
|
if self.overlay:
|
||||||
|
self.overlay.close()
|
||||||
|
self.overlay = None
|
||||||
|
self.toggle_action.setText("Start overlay")
|
||||||
|
else:
|
||||||
|
url = self.config.get("url", "").strip()
|
||||||
|
if not url:
|
||||||
|
QMessageBox.warning(None, "HudFX", "No URL set. Open Settings first.")
|
||||||
|
return
|
||||||
|
idx = self.config.get("screen_index", 0)
|
||||||
|
screens = self.app.screens()
|
||||||
|
screen = screens[idx] if idx < len(screens) else self.app.primaryScreen()
|
||||||
|
self.overlay = Overlay(url, screen)
|
||||||
|
self.toggle_action.setText("Stop overlay")
|
||||||
|
|
||||||
|
def open_settings(self):
|
||||||
|
dialog = SettingsDialog(self.config, self.app.screens())
|
||||||
|
if dialog.exec_() == QDialog.Accepted:
|
||||||
|
self.config = dialog.get_values()
|
||||||
|
save_config(self.config)
|
||||||
|
|
||||||
|
def quit(self):
|
||||||
|
if self.overlay:
|
||||||
|
self.overlay.close()
|
||||||
|
self.app.quit()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
app.setQuitOnLastWindowClosed(False)
|
||||||
|
|
||||||
|
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||||
|
timer = QTimer()
|
||||||
|
timer.start(200)
|
||||||
|
timer.timeout.connect(lambda: None)
|
||||||
|
|
||||||
|
tray = TrayApp(app)
|
||||||
|
sys.exit(app.exec_())
|
||||||
67
obs_hudfx.py
Normal file
67
obs_hudfx.py
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
import obspython as obs
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
OVERLAY_SCRIPT = "/home/bashy/projects/HudFX_LNX/hudfx.py"
|
||||||
|
PYTHON_BIN = "/usr/bin/python3"
|
||||||
|
FLATPAK_SPAWN = "/usr/bin/flatpak-spawn"
|
||||||
|
LOG_FILE = "/home/bashy/projects/HudFX_LNX/hudfx.log"
|
||||||
|
|
||||||
|
process = None
|
||||||
|
|
||||||
|
|
||||||
|
def ts():
|
||||||
|
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
|
||||||
|
def is_flatpak():
|
||||||
|
return bool(os.environ.get("FLATPAK_ID")) or os.path.exists("/run/host/etc")
|
||||||
|
|
||||||
|
|
||||||
|
def build_cmd():
|
||||||
|
if is_flatpak():
|
||||||
|
return [FLATPAK_SPAWN, "--host", PYTHON_BIN, OVERLAY_SCRIPT]
|
||||||
|
return [PYTHON_BIN, OVERLAY_SCRIPT]
|
||||||
|
|
||||||
|
|
||||||
|
def script_description():
|
||||||
|
return "Starts HudFX overlay when OBS loads and stops it when OBS closes."
|
||||||
|
|
||||||
|
|
||||||
|
def script_load(settings):
|
||||||
|
obs.obs_frontend_add_event_callback(on_event)
|
||||||
|
mode = "Flatpak" if is_flatpak() else "native"
|
||||||
|
print(f"[HudFX] {ts()} Script loaded (OBS mode: {mode})")
|
||||||
|
|
||||||
|
|
||||||
|
def on_event(event):
|
||||||
|
if event == obs.OBS_FRONTEND_EVENT_FINISHED_LOADING:
|
||||||
|
_start()
|
||||||
|
elif event == obs.OBS_FRONTEND_EVENT_EXIT:
|
||||||
|
_stop()
|
||||||
|
|
||||||
|
|
||||||
|
def script_unload():
|
||||||
|
_stop()
|
||||||
|
|
||||||
|
|
||||||
|
def _start():
|
||||||
|
global process
|
||||||
|
if process is None or process.poll() is not None:
|
||||||
|
try:
|
||||||
|
log = open(LOG_FILE, "a")
|
||||||
|
cmd = build_cmd()
|
||||||
|
process = subprocess.Popen(cmd, env=os.environ.copy(), stdout=log, stderr=log)
|
||||||
|
print(f"[HudFX] {ts()} Overlay started (pid {process.pid})")
|
||||||
|
print(f"[HudFX] {ts()} Log: {LOG_FILE}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[HudFX] {ts()} Failed to start overlay: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def _stop():
|
||||||
|
global process
|
||||||
|
if process and process.poll() is None:
|
||||||
|
process.terminate()
|
||||||
|
print(f"[HudFX] {ts()} Overlay stopped")
|
||||||
|
process = None
|
||||||
62
overlay.py
Normal file
62
overlay.py
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import sys
|
||||||
|
import signal
|
||||||
|
from PyQt5.QtCore import Qt, QUrl, QTimer
|
||||||
|
from PyQt5.QtWidgets import QApplication, QMainWindow
|
||||||
|
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile, QWebEnginePage, QWebEngineSettings
|
||||||
|
from PyQt5.QtGui import QColor
|
||||||
|
|
||||||
|
|
||||||
|
def find_screen(app, width=2560, height=1440):
|
||||||
|
for screen in app.screens():
|
||||||
|
s = screen.size()
|
||||||
|
if s.width() == width and s.height() == height:
|
||||||
|
return screen
|
||||||
|
print(f"Warning: no {width}x{height} screen found, falling back to primary")
|
||||||
|
return app.primaryScreen()
|
||||||
|
|
||||||
|
|
||||||
|
class Overlay(QMainWindow):
|
||||||
|
def __init__(self, url, screen):
|
||||||
|
super().__init__()
|
||||||
|
self.setWindowFlags(
|
||||||
|
Qt.FramelessWindowHint |
|
||||||
|
Qt.WindowStaysOnTopHint |
|
||||||
|
Qt.Tool |
|
||||||
|
Qt.WindowTransparentForInput
|
||||||
|
)
|
||||||
|
self.setAttribute(Qt.WA_TranslucentBackground)
|
||||||
|
|
||||||
|
view = QWebEngineView()
|
||||||
|
profile = QWebEngineProfile(view) # off-the-record, no disk storage
|
||||||
|
page = QWebEnginePage(profile, view)
|
||||||
|
view.setPage(page)
|
||||||
|
view.page().setBackgroundColor(QColor(0, 0, 0, 0))
|
||||||
|
view.page().setAudioMuted(False)
|
||||||
|
|
||||||
|
settings = view.page().settings()
|
||||||
|
settings.setAttribute(QWebEngineSettings.PlaybackRequiresUserGesture, False)
|
||||||
|
settings.setAttribute(QWebEngineSettings.JavascriptEnabled, True)
|
||||||
|
|
||||||
|
view.load(QUrl(url))
|
||||||
|
self.setCentralWidget(view)
|
||||||
|
|
||||||
|
self.setGeometry(screen.geometry())
|
||||||
|
self.show()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: python3 overlay.py <url>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||||
|
timer = QTimer()
|
||||||
|
timer.start(200)
|
||||||
|
timer.timeout.connect(lambda: None)
|
||||||
|
|
||||||
|
screen = find_screen(app)
|
||||||
|
window = Overlay(sys.argv[1], screen)
|
||||||
|
sys.exit(app.exec_())
|
||||||
Loading…
Add table
Add a link
Reference in a new issue