Test Notifications & Title Updates
All checks were successful
Build & Push Docker Image / build-and-publish (push) Successful in 27s

This commit is contained in:
2026-02-05 16:24:18 -05:00
parent 08b959d93e
commit 6272b3f1c3
7 changed files with 452 additions and 1 deletions

81
90-start-services Normal file
View 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
View 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

View File

@@ -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
View 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
View 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
View 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
View 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()