Test Notifications & Title Updates
All checks were successful
Build & Push Docker Image / build-and-publish (push) Successful in 27s
All checks were successful
Build & Push Docker Image / build-and-publish (push) Successful in 27s
This commit is contained in:
81
90-start-services
Normal file
81
90-start-services
Normal file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
# Start notification bridge and window title sync services
|
||||
# This runs during container initialization
|
||||
|
||||
echo "[init] Starting background services for notifications and window title sync"
|
||||
|
||||
# Create log directory
|
||||
mkdir -p /config/logs
|
||||
chown abc:abc /config/logs
|
||||
|
||||
# Wait for X server in background, then start services
|
||||
(
|
||||
# Log current environment
|
||||
echo "[init] $(date) - Starting background services" >> /config/logs/startup.log
|
||||
echo "[init] Running as user: $(whoami)" >> /config/logs/startup.log
|
||||
echo "[init] Python version: $(python3 --version 2>&1)" >> /config/logs/startup.log
|
||||
|
||||
# Wait for X server to be ready
|
||||
echo "[init] Waiting for X server..." >> /config/logs/startup.log
|
||||
timeout=60
|
||||
while [ $timeout -gt 0 ]; do
|
||||
if DISPLAY=:0 xdpyinfo >/dev/null 2>&1; then
|
||||
echo "[init] X server is ready!" >> /config/logs/startup.log
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
((timeout--))
|
||||
done
|
||||
|
||||
if [ $timeout -eq 0 ]; then
|
||||
echo "[init] ERROR: X server failed to start within 60 seconds" >> /config/logs/startup.log
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Give X server and desktop environment time to fully initialize
|
||||
echo "[init] Waiting 10 seconds for desktop environment to stabilize..." >> /config/logs/startup.log
|
||||
sleep 10
|
||||
|
||||
# Create initial title file in web directory
|
||||
echo "RuneLite" > /usr/share/selkies/www/title.txt
|
||||
chmod 666 /usr/share/selkies/www/title.txt
|
||||
echo "[]" > /usr/share/selkies/www/notifications.json
|
||||
chmod 666 /usr/share/selkies/www/notifications.json
|
||||
echo "[init] Created title.txt and notifications.json in web directory" >> /config/logs/startup.log
|
||||
|
||||
# Start notification bridge with proper permissions
|
||||
echo "[init] Starting notification bridge..." >> /config/logs/startup.log
|
||||
s6-setuidgid abc env DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS:-unix:path=/run/user/911/bus}" \
|
||||
/usr/local/bin/notification-bridge >> /config/logs/notification-bridge.log 2>&1 &
|
||||
NOTIF_PID=$!
|
||||
echo "[init] Notification bridge started with PID: $NOTIF_PID" >> /config/logs/startup.log
|
||||
|
||||
# Verify it's running
|
||||
sleep 1
|
||||
if kill -0 $NOTIF_PID 2>/dev/null; then
|
||||
echo "[init] Notification bridge is running" >> /config/logs/startup.log
|
||||
else
|
||||
echo "[init] WARNING: Notification bridge may have failed to start" >> /config/logs/startup.log
|
||||
fi
|
||||
|
||||
# Start window title sync with proper permissions
|
||||
echo "[init] Starting window title sync..." >> /config/logs/startup.log
|
||||
s6-setuidgid abc env DISPLAY=:0 /usr/local/bin/window-title-sync >> /config/logs/window-title-sync.log 2>&1 &
|
||||
TITLE_PID=$!
|
||||
echo "[init] Window title sync started with PID: $TITLE_PID" >> /config/logs/startup.log
|
||||
|
||||
# Verify it's running
|
||||
sleep 1
|
||||
if kill -0 $TITLE_PID 2>/dev/null; then
|
||||
echo "[init] Window title sync is running" >> /config/logs/startup.log
|
||||
else
|
||||
echo "[init] WARNING: Window title sync may have failed to start" >> /config/logs/startup.log
|
||||
fi
|
||||
|
||||
echo "[init] All background services started successfully at $(date)" >> /config/logs/startup.log
|
||||
|
||||
# Keep log files readable
|
||||
chown abc:abc /config/logs/*.log 2>/dev/null
|
||||
) &
|
||||
|
||||
echo "[init] Background services initialization launched"
|
||||
30
95-inject-javascript
Normal file
30
95-inject-javascript
Normal file
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/with-contenv bash
|
||||
# Inject RuneLite browser integration JavaScript into Selkies index.html
|
||||
# This runs after Selkies creates the index.html file
|
||||
|
||||
echo "[custom-init] Injecting RuneLite browser integration..."
|
||||
|
||||
# Wait for Selkies to create index.html
|
||||
timeout=30
|
||||
while [ $timeout -gt 0 ]; do
|
||||
if [ -f /usr/share/selkies/www/index.html ]; then
|
||||
echo "[custom-init] Found index.html, injecting JavaScript..."
|
||||
|
||||
# Check if already injected
|
||||
if ! grep -q "RuneLite browser integration" /usr/share/selkies/www/index.html; then
|
||||
# Inject before closing body tag
|
||||
sed -i 's|</body>|<script>\n// RuneLite browser integration for dynamic title updates and notifications\n(function() {\n "use strict";\n const CHECK_INTERVAL = 2000;\n let lastNotificationId = 0;\n let notificationPermission = false;\n \n // Request notification permission\n if ("Notification" in window \&\& Notification.permission === "default") {\n Notification.requestPermission().then(function(permission) {\n notificationPermission = permission === "granted";\n console.log("[RuneLite] Notification permission:", permission);\n });\n } else if (Notification.permission === "granted") {\n notificationPermission = true;\n }\n \n // Update title\n async function updateTitle() {\n try {\n const response = await fetch("/title.txt");\n if (response.ok) {\n const title = await response.text();\n if (title \&\& title.trim() \&\& document.title !== title.trim()) {\n document.title = title.trim();\n console.log("[RuneLite] Browser title updated to:", title.trim());\n }\n }\n } catch (e) {}\n }\n \n // Check for new notifications\n async function checkNotifications() {\n if (!notificationPermission) return;\n try {\n const response = await fetch("/notifications.json");\n if (response.ok) {\n const notifications = await response.json();\n for (const notif of notifications) {\n if (notif.id > lastNotificationId) {\n new Notification(notif.title, {\n body: notif.body,\n icon: "/icon.png",\n tag: "runelite-" + notif.id\n });\n lastNotificationId = notif.id;\n console.log("[RuneLite] Notification shown:", notif.title);\n }\n }\n }\n } catch (e) {}\n }\n \n setInterval(updateTitle, CHECK_INTERVAL);\n setInterval(checkNotifications, CHECK_INTERVAL);\n setTimeout(updateTitle, 1000);\n setTimeout(checkNotifications, 2000);\n console.log("[RuneLite] Browser integration loaded - title sync and notifications enabled");\n})();\n</script>\n</body>|' /usr/share/selkies/www/index.html
|
||||
|
||||
echo "[custom-init] JavaScript injection complete"
|
||||
else
|
||||
echo "[custom-init] JavaScript already injected, skipping"
|
||||
fi
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
((timeout--))
|
||||
done
|
||||
|
||||
if [ $timeout -eq 0 ]; then
|
||||
echo "[custom-init] WARNING: index.html not found after 30 seconds"
|
||||
fi
|
||||
22
Dockerfile
22
Dockerfile
@@ -19,6 +19,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
wget \
|
||||
libnotify-bin \
|
||||
notification-daemon \
|
||||
dbus-x11 \
|
||||
python3 \
|
||||
python3-gi \
|
||||
python3-xlib \
|
||||
gir1.2-notify-0.7 \
|
||||
xdotool \
|
||||
wmctrl \
|
||||
x11-utils \
|
||||
# GPU/OpenGL libraries for RuneLite GPU plugin
|
||||
mesa-utils \
|
||||
libgl1-mesa-dri \
|
||||
@@ -33,13 +41,25 @@ RUN if [ -f /usr/share/selkies/www/index.html ]; then \
|
||||
sed -i '/<\/head>/r /usr/share/selkies/www/webapp-meta.html' /usr/share/selkies/www/index.html ; \
|
||||
fi
|
||||
|
||||
# Copy runtime JavaScript injection script
|
||||
COPY 95-inject-javascript /etc/cont-init.d/95-inject-javascript
|
||||
RUN chmod +x /etc/cont-init.d/95-inject-javascript
|
||||
|
||||
|
||||
|
||||
ENV RUNELITE_URL="https://github.com/runelite/launcher/releases/latest/download/RuneLite.jar"
|
||||
ADD runelite /usr/local/bin
|
||||
COPY notification-bridge.py /usr/local/bin/notification-bridge
|
||||
COPY window-title-sync.py /usr/local/bin/window-title-sync
|
||||
COPY test-features.sh /usr/local/bin/test-features
|
||||
COPY 90-start-services /etc/cont-init.d/90-start-services
|
||||
RUN wget $RUNELITE_URL -P /usr/local/bin \
|
||||
&& chmod +x /usr/local/bin/RuneLite.jar \
|
||||
&& chmod +x /usr/local/bin/runelite
|
||||
&& chmod +x /usr/local/bin/runelite \
|
||||
&& chmod +x /usr/local/bin/notification-bridge \
|
||||
&& chmod +x /usr/local/bin/window-title-sync \
|
||||
&& chmod +x /usr/local/bin/test-features \
|
||||
&& chmod +x /etc/cont-init.d/90-start-services
|
||||
|
||||
# Configure window manager to hide title bars and maximize windows
|
||||
RUN mkdir -p /etc/xdg/openbox
|
||||
|
||||
140
notification-bridge.py
Normal file
140
notification-bridge.py
Normal file
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
D-Bus notification bridge for Selkies - forwards to browser notifications
|
||||
|
||||
Monitors desktop notifications and writes them to a JSON file that the browser can read
|
||||
to display as native Web Notifications.
|
||||
"""
|
||||
|
||||
import gi # type: ignore
|
||||
gi.require_version('Notify', '0.7') # type: ignore
|
||||
from gi.repository import Notify, GLib # type: ignore
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
# File where notifications will be written for browser to read
|
||||
NOTIFICATION_FILE = '/usr/share/selkies/www/notifications.json'
|
||||
MAX_NOTIFICATIONS = 10 # Keep last N notifications
|
||||
|
||||
class NotificationBridge:
|
||||
def __init__(self):
|
||||
self.notifications = []
|
||||
|
||||
def add_notification(self, title, body, urgency='normal'):
|
||||
"""Add a notification to the queue"""
|
||||
notification = {
|
||||
'id': int(time.time() * 1000), # Timestamp as ID
|
||||
'title': title,
|
||||
'body': body,
|
||||
'urgency': urgency,
|
||||
'timestamp': time.time()
|
||||
}
|
||||
|
||||
self.notifications.append(notification)
|
||||
|
||||
# Keep only recent notifications
|
||||
if len(self.notifications) > MAX_NOTIFICATIONS:
|
||||
self.notifications = self.notifications[-MAX_NOTIFICATIONS:]
|
||||
|
||||
self.save_notifications()
|
||||
print(f"Notification captured: {title} - {body}", flush=True)
|
||||
|
||||
def save_notifications(self):
|
||||
"""Save notifications to JSON file for browser to read"""
|
||||
try:
|
||||
with open(NOTIFICATION_FILE, 'w') as f:
|
||||
json.dump(self.notifications, f)
|
||||
os.chmod(NOTIFICATION_FILE, 0o644)
|
||||
except Exception as e:
|
||||
print(f"Error saving notifications: {e}", file=sys.stderr, flush=True)
|
||||
|
||||
# Global bridge instance
|
||||
bridge = NotificationBridge()
|
||||
|
||||
def on_notification_callback(notification, action=None):
|
||||
"""Callback when notification is shown"""
|
||||
# This gets called but we're using manual monitoring instead
|
||||
pass
|
||||
|
||||
def main():
|
||||
# Ensure DISPLAY is set
|
||||
if not os.getenv('DISPLAY'):
|
||||
os.environ['DISPLAY'] = ':0'
|
||||
print("Set DISPLAY to :0", file=sys.stderr, flush=True)
|
||||
|
||||
# Initialize libnotify
|
||||
if not Notify.init("notification-bridge"): # type: ignore
|
||||
print("Failed to initialize libnotify", file=sys.stderr, flush=True)
|
||||
sys.exit(1)
|
||||
|
||||
print("Notification bridge started - forwarding to browser notifications", flush=True)
|
||||
print(f"D-Bus session address: {os.getenv('DBUS_SESSION_BUS_ADDRESS', 'not set')}", flush=True)
|
||||
|
||||
# Initialize notification file
|
||||
bridge.save_notifications()
|
||||
|
||||
# Monitor D-Bus for org.freedesktop.Notifications
|
||||
# We'll use dbus-monitor to capture notifications
|
||||
import subprocess
|
||||
import threading
|
||||
|
||||
def monitor_dbus():
|
||||
"""Monitor D-Bus for notification signals"""
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
['dbus-monitor', '--session', "interface='org.freedesktop.Notifications',member='Notify'"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
bufsize=1
|
||||
)
|
||||
|
||||
current_notification = {}
|
||||
|
||||
for line in proc.stdout:
|
||||
line = line.strip()
|
||||
|
||||
# Parse notification data from dbus-monitor output
|
||||
if 'string "' in line:
|
||||
value = line.split('string "')[1].rstrip('"')
|
||||
|
||||
# First string is app name, second is summary, third is body
|
||||
if 'app_name' not in current_notification:
|
||||
current_notification['app_name'] = value
|
||||
elif 'title' not in current_notification:
|
||||
current_notification['title'] = value
|
||||
elif 'body' not in current_notification:
|
||||
current_notification['body'] = value
|
||||
|
||||
# We have a complete notification
|
||||
if current_notification['title'] or current_notification['body']:
|
||||
bridge.add_notification(
|
||||
title=current_notification.get('title', 'Notification'),
|
||||
body=current_notification.get('body', ''),
|
||||
urgency='normal'
|
||||
)
|
||||
|
||||
current_notification = {}
|
||||
|
||||
except Exception as e:
|
||||
print(f"D-Bus monitor error: {e}", file=sys.stderr, flush=True)
|
||||
|
||||
# Start D-Bus monitor in background thread
|
||||
monitor_thread = threading.Thread(target=monitor_dbus, daemon=True)
|
||||
monitor_thread.start()
|
||||
print("D-Bus notification monitor started", flush=True)
|
||||
|
||||
# Keep the script running
|
||||
try:
|
||||
loop = GLib.MainLoop() # type: ignore
|
||||
print("Entering main loop - ready to forward notifications", flush=True)
|
||||
loop.run()
|
||||
except KeyboardInterrupt:
|
||||
print("\nNotification bridge stopped", flush=True)
|
||||
Notify.uninit() # type: ignore
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
32
selkies-integration.html
Normal file
32
selkies-integration.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<script>
|
||||
// RuneLite browser integration for dynamic title updates
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const CHECK_INTERVAL = 2000; // Check every 2 seconds
|
||||
|
||||
// Function to update browser tab title from Selkies web directory
|
||||
async function updateTitle() {
|
||||
try {
|
||||
const response = await fetch('/title.txt');
|
||||
if (response.ok) {
|
||||
const title = await response.text();
|
||||
if (title && title.trim() && document.title !== title.trim()) {
|
||||
document.title = title.trim();
|
||||
console.log('[RuneLite] Browser title updated to:', title.trim());
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Silently fail if endpoint doesn't exist yet
|
||||
}
|
||||
}
|
||||
|
||||
// Update title periodically
|
||||
setInterval(updateTitle, CHECK_INTERVAL);
|
||||
|
||||
// Initial update
|
||||
setTimeout(updateTitle, 1000);
|
||||
|
||||
console.log('[RuneLite] Browser integration loaded - dynamic title updates enabled');
|
||||
})();
|
||||
</script>
|
||||
48
test-features.sh
Normal file
48
test-features.sh
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
# Test script to verify notifications and title changes are working
|
||||
# Run this inside the container to test the features
|
||||
|
||||
echo "Testing notification and title sync features..."
|
||||
|
||||
# Test notification
|
||||
echo "Sending test notification..."
|
||||
notify-send "Test Notification" "If you see this in your browser, notifications are working!" -u normal
|
||||
|
||||
# Test window title detection
|
||||
echo ""
|
||||
echo "Checking for RuneLite window..."
|
||||
if xdotool search --name "RuneLite" > /dev/null 2>&1; then
|
||||
WINDOW_ID=$(xdotool search --name "RuneLite" | head -1)
|
||||
WINDOW_TITLE=$(xdotool getwindowname "$WINDOW_ID")
|
||||
echo "Found RuneLite window: $WINDOW_TITLE"
|
||||
else
|
||||
echo "RuneLite window not found. Make sure RuneLite is running."
|
||||
fi
|
||||
|
||||
# Check if services are running
|
||||
echo ""
|
||||
echo "Checking service status..."
|
||||
if pgrep -f "notification-bridge" > /dev/null; then
|
||||
echo "✓ Notification bridge is running (PID: $(pgrep -f 'notification-bridge'))"
|
||||
else
|
||||
echo "✗ Notification bridge is NOT running"
|
||||
fi
|
||||
|
||||
if pgrep -f "window-title-sync" > /dev/null; then
|
||||
echo "✓ Window title sync is running (PID: $(pgrep -f 'window-title-sync'))"
|
||||
else
|
||||
echo "✗ Window title sync is NOT running"
|
||||
fi
|
||||
|
||||
# Show log files if they exist
|
||||
echo ""
|
||||
echo "Log files location: /config/logs/"
|
||||
if [ -d "/config/logs" ]; then
|
||||
echo "Available logs:"
|
||||
ls -lh /config/logs/
|
||||
echo ""
|
||||
echo "To view logs, run:"
|
||||
echo " cat /config/logs/startup.log"
|
||||
echo " cat /config/logs/notification-bridge.log"
|
||||
echo " cat /config/logs/window-title-sync.log"
|
||||
fi
|
||||
100
window-title-sync.py
Normal file
100
window-title-sync.py
Normal file
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Window title synchronization for Selkies
|
||||
Monitors the active window title and updates the browser tab title dynamically
|
||||
|
||||
Note: This script is designed to run inside a Linux container with X11 tools installed.
|
||||
Pylance warnings about missing imports are expected in the development environment.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import time
|
||||
import os
|
||||
import sys
|
||||
|
||||
def get_active_window_title():
|
||||
"""Get the title of the currently active window"""
|
||||
try:
|
||||
# Try using xdotool first
|
||||
result = subprocess.run(
|
||||
['xdotool', 'getactivewindow', 'getwindowname'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=1
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return result.stdout.strip()
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
pass
|
||||
|
||||
try:
|
||||
# Fallback to wmctrl
|
||||
result = subprocess.run(
|
||||
['wmctrl', '-l'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=1
|
||||
)
|
||||
if result.returncode == 0:
|
||||
lines = result.stdout.strip().split('\n')
|
||||
for line in lines:
|
||||
if 'RuneLite' in line:
|
||||
# Extract title (after the third column)
|
||||
parts = line.split(None, 3)
|
||||
if len(parts) >= 4:
|
||||
return parts[3]
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def update_browser_title(title):
|
||||
"""Update the browser tab title via file in Selkies web directory"""
|
||||
title_file = '/usr/share/selkies/www/title.txt'
|
||||
|
||||
try:
|
||||
with open(title_file, 'w') as f:
|
||||
f.write(title)
|
||||
print(f"Title file updated: {title}", flush=True)
|
||||
except Exception as e:
|
||||
print(f"Error updating title file: {e}", file=sys.stderr, flush=True)
|
||||
|
||||
def main():
|
||||
print("Window title sync started - monitoring RuneLite window", flush=True)
|
||||
|
||||
# Ensure DISPLAY is set
|
||||
if 'DISPLAY' not in os.environ:
|
||||
os.environ['DISPLAY'] = ':0'
|
||||
print("Set DISPLAY to :0", flush=True)
|
||||
|
||||
last_title = None
|
||||
|
||||
# Initial delay to let X11 and RuneLite window appear
|
||||
print("Waiting for RuneLite window to appear...", flush=True)
|
||||
time.sleep(10)
|
||||
|
||||
while True:
|
||||
try:
|
||||
current_title = get_active_window_title()
|
||||
|
||||
# Only update if title has changed and contains "RuneLite"
|
||||
if current_title and 'RuneLite' in current_title and current_title != last_title:
|
||||
print(f"Window title changed to: {current_title}", flush=True)
|
||||
update_browser_title(current_title)
|
||||
last_title = current_title
|
||||
elif current_title and last_title is None:
|
||||
# Log first title found for debugging
|
||||
print(f"First window title detected: {current_title}", flush=True)
|
||||
|
||||
# Check every 2 seconds
|
||||
time.sleep(2)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nWindow title sync stopped", flush=True)
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Error in title monitoring: {e}", file=sys.stderr, flush=True)
|
||||
time.sleep(5)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user