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