Files
senior-kiosk/browser.py
T
jgitta aac4605e66 v0.07: switch to Chromium snap, remove Google Chrome
- Replace google-chrome with /snap/bin/chromium
- Remove --test-type flag (was triggering Google bot detection/CAPTCHA)
- Add --disable-blink-features=AutomationControlled (hides webdriver flag)
- Move profile dir to /home/jgitta/chromium-kiosk (snap can't write to .config)
- Uninstall google-chrome-stable and its apt repo
2026-05-22 12:28:52 +00:00

674 lines
26 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
SeniorNet Kiosk — PyQt5 single-row floating toolbar over Google Chrome.
Navigation: ◀ ▶ ⌂ icon buttons + ☰ hamburger menu for bookmarks.
Chrome is controlled via Chrome DevTools Protocol (CDP) over WebSocket.
"""
import sys, json, os, subprocess, time, threading, urllib.request
from PyQt5.QtCore import Qt, QTimer, QPoint
from PyQt5.QtGui import QFont, QColor, QPalette
from PyQt5.QtWidgets import (
QApplication, QWidget, QHBoxLayout, QVBoxLayout,
QPushButton, QLabel, QScrollArea, QDialog, QLineEdit,
QMessageBox, QFrame, QMenu, QAction,
QTreeWidget, QTreeWidgetItem, QAbstractItemView
)
HOME_URL = "file:///home/jgitta/kiosk-home/index.html"
BOOKMARKS_FILE = "/home/jgitta/kiosk-browser/bookmarks.json"
CDP_PORT = 9222
TOOLBAR_H = 72 # single row — was 142 (82 + 60)
TOP_H = TOOLBAR_H
SCREEN_W = 1920
SCREEN_H = 1080
# ── Colour palette ────────────────────────────────────────────────
TOOLBAR_COLOR = "#1a3a5c"
BTN_COLOR = "#2e6da4"; BTN_HOVER = "#3a87cc"
GOLD = "#e8a020"; GOLD_HOVER = "#f0b030"
GREEN = "#2e7d32"; GREEN_HOVER = "#388e3c"
RED = "#c62828"; RED_HOVER = "#d32f2f"
AMBER = "#7a5a10"; AMBER_HOVER = "#9a7a20"
PURPLE = "#5a5a8a"; PURPLE_HOVER = "#7a7aaa"
FONT_SIZE = 18
MENU_STYLE = """
QMenu {
background: #1e3a5c; color: white;
border: 2px solid #3a6da4; border-radius: 8px; padding: 4px;
}
QMenu::item { padding: 14px 36px 14px 16px; border-radius: 6px; font-size: 17px; }
QMenu::item:selected { background: #3a6da4; color: white; }
QMenu::separator { height: 1px; background: #3a6da4; margin: 4px 8px; }
"""
# ── Helpers ───────────────────────────────────────────────────────
def mk_btn(label, color, hover, min_w=120, min_h=52, fs=FONT_SIZE):
b = QPushButton(label)
b.setFont(QFont("Arial", fs, QFont.Bold))
b.setMinimumSize(min_w, min_h)
b.setCursor(Qt.PointingHandCursor)
b.setStyleSheet(f"""
QPushButton {{ background:{color}; color:white; border:none;
border-radius:10px; padding:6px 14px; }}
QPushButton:hover {{ background:{hover}; }}
QPushButton:pressed {{ background:#222; }}
QPushButton:disabled {{ background:#444; color:#888; }}
""")
return b
def mk_icon_btn(icon, color, hover, size=58, fs=26):
"""Square icon button for Back / Forward / Home / Hamburger."""
b = QPushButton(icon)
b.setFont(QFont("Arial", fs))
b.setFixedSize(size, size)
b.setCursor(Qt.PointingHandCursor)
b.setStyleSheet(f"""
QPushButton {{ background:{color}; color:white; border:none;
border-radius:12px; }}
QPushButton:hover {{ background:{hover}; }}
QPushButton:pressed {{ background:#222; }}
""")
return b
def load_bm():
try:
with open(BOOKMARKS_FILE) as f: return json.load(f)
except: return []
def save_bm(data):
with open(BOOKMARKS_FILE, "w") as f: json.dump(data, f, indent=2)
def is_folder(item): return "children" in item
# ── CDP controller ────────────────────────────────────────────────
class CDP:
"""Thin Chrome DevTools Protocol client — navigate, back, forward."""
def __init__(self):
self.ready = False
self._lock = threading.Lock()
def wait_ready(self, timeout=30):
for _ in range(timeout * 2):
try:
urllib.request.urlopen(
f"http://127.0.0.1:{CDP_PORT}/json", timeout=1)
self.ready = True
print("CDP ready")
return True
except Exception:
time.sleep(0.5)
print("CDP not available after timeout")
return False
def _ws_url(self):
raw = urllib.request.urlopen(
f"http://127.0.0.1:{CDP_PORT}/json", timeout=2).read()
tabs = json.loads(raw)
page = next((t for t in tabs if t.get("type") == "page"), None)
return page["webSocketDebuggerUrl"].replace("localhost", "127.0.0.1") if page else None
def _send(self, method, params=None):
if not self.ready:
return
import websocket
with self._lock:
try:
ws = websocket.create_connection(self._ws_url(), timeout=3)
ws.send(json.dumps({"id": 1, "method": method,
**( {"params": params} if params else {})}))
ws.recv()
ws.close()
except Exception as e:
print(f"CDP error ({method}): {e}")
def _async(self, method, params=None):
threading.Thread(target=self._send, args=(method, params),
daemon=True).start()
def navigate(self, url):
self._async("Page.navigate", {"url": url})
def back(self):
self._async("Runtime.evaluate",
{"expression": "window.history.back()"})
def forward(self):
self._async("Runtime.evaluate",
{"expression": "window.history.forward()"})
# ── Folder name dialog ────────────────────────────────────────────
def ask_name(parent, title="Name", current=""):
d = QDialog(parent)
d.setWindowTitle(title)
d.setMinimumWidth(480)
d.setStyleSheet("background:#1e2a3a; color:white;")
lay = QVBoxLayout(d)
lay.setContentsMargins(28, 28, 28, 28)
lay.setSpacing(16)
lbl = QLabel("Folder name:")
lbl.setFont(QFont("Arial", 16))
lbl.setStyleSheet("color:#aac8e8;")
lay.addWidget(lbl)
edit = QLineEdit(current)
edit.setFont(QFont("Arial", 18))
edit.setMinimumHeight(50)
edit.setStyleSheet("background:#2a3f55;color:white;border:2px solid #3a6da4;"
"border-radius:8px;padding:6px;")
lay.addWidget(edit)
row = QHBoxLayout()
ok = mk_btn("💾 Save", GREEN, GREEN_HOVER, 150, 56, 16)
can = mk_btn("✖ Cancel", RED, RED_HOVER, 150, 56, 16)
ok.clicked.connect(d.accept); can.clicked.connect(d.reject)
row.addStretch(); row.addWidget(ok); row.addWidget(can)
lay.addLayout(row)
return edit.text().strip() if d.exec_() == QDialog.Accepted else None
# ── Bookmark add / edit dialog ────────────────────────────────────
class BookmarkDialog(QDialog):
def __init__(self, parent=None, name="", url=""):
super().__init__(parent)
self.setWindowTitle("Bookmark")
self.setMinimumWidth(560)
self.setStyleSheet("background:#1e2a3a; color:white;")
lay = QVBoxLayout(self)
lay.setSpacing(18); lay.setContentsMargins(30, 30, 30, 30)
for lbl_text, attr, val in [("Name:", "name_edit", name),
("URL:", "url_edit", url)]:
l = QLabel(lbl_text)
l.setFont(QFont("Arial", 16)); l.setStyleSheet("color:#aac8e8;")
lay.addWidget(l)
e = QLineEdit(val)
e.setFont(QFont("Arial", 18)); e.setMinimumHeight(50)
e.setStyleSheet("background:#2a3f55;color:white;border:2px solid #3a6da4;"
"border-radius:8px;padding:6px;")
lay.addWidget(e); setattr(self, attr, e)
row = QHBoxLayout()
ok = mk_btn("💾 Save", GREEN, GREEN_HOVER, 160, 58, 16)
can = mk_btn("✖ Cancel", RED, RED_HOVER, 160, 58, 16)
ok.clicked.connect(self.accept); can.clicked.connect(self.reject)
row.addStretch(); row.addWidget(ok); row.addWidget(can)
lay.addLayout(row)
def values(self):
return self.name_edit.text().strip(), self.url_edit.text().strip()
# ── Bookmark Manager ──────────────────────────────────────────────
class ManagerDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Manage Bookmarks")
self.setMinimumSize(860, 640)
self.setStyleSheet("background:#1e2a3a; color:white;")
lay = QVBoxLayout(self)
lay.setContentsMargins(20, 20, 20, 20); lay.setSpacing(12)
hdr = QLabel("📋 Bookmarks")
hdr.setFont(QFont("Arial", 22, QFont.Bold))
hdr.setStyleSheet("color:white;")
lay.addWidget(hdr)
hint = QLabel("Select a folder to add inside it, or nothing to add at top level. "
"▲ ▼ move items within the same folder.")
hint.setFont(QFont("Arial", 13))
hint.setStyleSheet("color:#88aacc;"); hint.setWordWrap(True)
lay.addWidget(hint)
self.tree = QTreeWidget()
self.tree.setHeaderHidden(True)
self.tree.setFont(QFont("Arial", 15))
self.tree.setSelectionMode(QAbstractItemView.SingleSelection)
self.tree.setStyleSheet("""
QTreeWidget { background:#2a3f55; color:white;
border:2px solid #3a6da4; border-radius:8px; }
QTreeWidget::item { padding:8px 4px; border-bottom:1px solid #1a2f45; }
QTreeWidget::item:selected { background:#3a6da4; }
QTreeWidget::branch { background:#2a3f55; }
""")
self.tree.setIndentation(30)
self._reload_tree()
lay.addWidget(self.tree)
row = QHBoxLayout(); row.setSpacing(8)
btns = [mk_btn(" Bookmark", GREEN, GREEN_HOVER, 155, 54, 14),
mk_btn("📁 Folder", AMBER, AMBER_HOVER, 130, 54, 14),
mk_btn("✏️ Edit", BTN_COLOR,BTN_HOVER, 120, 54, 14),
mk_btn("🗑 Delete", RED, RED_HOVER, 120, 54, 14),
mk_btn("▲ Up", PURPLE, PURPLE_HOVER, 90, 54, 14),
mk_btn("▼ Down", PURPLE, PURPLE_HOVER, 90, 54, 14),
mk_btn("✖ Close", "#555", "#777", 120, 54, 14)]
acts = [self.add_bookmark, self.add_folder, self.edit_item,
self.delete_item, lambda: self.move_item(-1),
lambda: self.move_item(1), self.accept]
for b, a in zip(btns, acts):
b.clicked.connect(a); row.addWidget(b)
lay.addLayout(row)
def _reload_tree(self):
self.tree.clear()
self._build_nodes(load_bm(), self.tree.invisibleRootItem())
self.tree.expandAll()
def _build_nodes(self, items, parent):
for item in items:
node = QTreeWidgetItem(parent)
if is_folder(item):
node.setText(0, f"[+] {item['name']}")
node.setData(0, Qt.UserRole, {"name": item["name"], "_folder": True})
self._build_nodes(item["children"], node)
else:
node.setText(0, f" {item['name']} - {item['url']}")
node.setData(0, Qt.UserRole, {"name": item["name"], "url": item["url"]})
def _to_list(self, parent=None):
if parent is None: parent = self.tree.invisibleRootItem()
result = []
for i in range(parent.childCount()):
n = parent.child(i); d = n.data(0, Qt.UserRole)
if "_folder" in d:
result.append({"name": d["name"], "children": self._to_list(n)})
else:
result.append({"name": d["name"], "url": d["url"]})
return result
def _sel(self):
items = self.tree.selectedItems()
return items[0] if items else None
def _commit(self): save_bm(self._to_list())
def _insert(self, node):
sel = self._sel()
if sel:
d = sel.data(0, Qt.UserRole)
if "_folder" in d:
sel.addChild(node); sel.setExpanded(True); return
par = sel.parent() or self.tree.invisibleRootItem()
par.insertChild(par.indexOfChild(sel) + 1, node)
else:
self.tree.invisibleRootItem().addChild(node)
def add_bookmark(self):
dlg = BookmarkDialog(self)
if dlg.exec_() != QDialog.Accepted: return
name, url = dlg.values()
if not name or not url: return
node = QTreeWidgetItem()
node.setText(0, f" {name} - {url}")
node.setData(0, Qt.UserRole, {"name": name, "url": url})
self._insert(node); self._commit()
def add_folder(self):
name = ask_name(self, "New Folder")
if not name: return
node = QTreeWidgetItem()
node.setText(0, f"[+] {name}")
node.setData(0, Qt.UserRole, {"name": name, "_folder": True})
self._insert(node); self._commit()
def edit_item(self):
sel = self._sel()
if not sel: return
d = sel.data(0, Qt.UserRole)
if "_folder" in d:
name = ask_name(self, "Edit Folder", d["name"])
if not name: return
sel.setText(0, f"[+] {name}")
sel.setData(0, Qt.UserRole, {"name": name, "_folder": True})
else:
dlg = BookmarkDialog(self, d["name"], d["url"])
if dlg.exec_() != QDialog.Accepted: return
name, url = dlg.values()
if not name or not url: return
sel.setText(0, f" {name} - {url}")
sel.setData(0, Qt.UserRole, {"name": name, "url": url})
self._commit()
def delete_item(self):
sel = self._sel()
if not sel: return
d = sel.data(0, Qt.UserRole)
fld = "_folder" in d
msg = (f"Delete folder '{d['name']}' and ALL its bookmarks?"
if fld else f"Delete '{d['name']}'?")
box = QMessageBox(self)
box.setWindowTitle("Delete"); box.setText(msg)
box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
box.setStyleSheet("background:#1e2a3a; color:white; font-size:17px;")
if box.exec_() == QMessageBox.Yes:
par = sel.parent() or self.tree.invisibleRootItem()
par.removeChild(sel); self._commit()
def move_item(self, direction):
sel = self._sel()
if not sel: return
par = sel.parent() or self.tree.invisibleRootItem()
idx = par.indexOfChild(sel); ni = idx + direction
if 0 <= ni < par.childCount():
par.takeChild(idx); par.insertChild(ni, sel)
self.tree.setCurrentItem(sel); self._commit()
# ── Bookmark dropdown panel ───────────────────────────────────────
class BookmarkPanel(QWidget):
"""
Custom dropdown that replaces QMenu for bookmarks.
Uses the same Qt.Tool | WindowStaysOnTopHint flags as the toolbar so it
always appears above Chrome, unlike QMenu which has z-order issues.
"""
def __init__(self, cdp, overlay):
super().__init__()
self.cdp = cdp
self.overlay = overlay
self.setWindowFlags(
Qt.Tool | Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_ShowWithoutActivating)
self.setStyleSheet(f"""
QWidget {{ background: #1e3a5c; }}
QPushButton {{ background: transparent; color: white; border: none;
text-align: left; padding: 8px 20px;
font-size: 17px; border-radius: 6px; }}
QPushButton:hover {{ background: #3a6da4; }}
QPushButton:pressed {{ background: #222; }}
QLabel {{ color: #aac8e8; padding: 6px 20px 2px 20px;
font-size: 14px; font-weight: bold; }}
""")
QApplication.instance().installEventFilter(self)
def rebuild(self, items):
# Remove old layout
old = self.layout()
if old:
while old.count():
w = old.takeAt(0).widget()
if w:
w.deleteLater()
QWidget().setLayout(old)
scroll_content = QWidget()
scroll_content.setStyleSheet("background: #1e3a5c;")
vbox = QVBoxLayout(scroll_content)
vbox.setContentsMargins(6, 6, 6, 6)
vbox.setSpacing(1)
self._add_items(vbox, items, depth=0)
sep = QFrame()
sep.setFrameShape(QFrame.HLine)
sep.setStyleSheet("background: #3a6da4; margin: 4px 8px;")
sep.setFixedHeight(1)
vbox.addWidget(sep)
mgr = QPushButton("⚙ Manage Bookmarks…")
mgr.setFont(QFont("Arial", 16))
mgr.setMinimumHeight(48)
mgr.setCursor(Qt.PointingHandCursor)
mgr.clicked.connect(self._on_manage)
vbox.addWidget(mgr)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
scroll.setStyleSheet("background: transparent; border: none;")
scroll.setWidget(scroll_content)
outer = QVBoxLayout(self)
outer.setContentsMargins(2, 2, 2, 2)
outer.setSpacing(0)
outer.addWidget(scroll)
# Size: cap height at 80 % of screen below toolbar
scroll_content.adjustSize()
ph = scroll_content.sizeHint().height() + 16
pw = max(scroll_content.sizeHint().width() + 16, 340)
max_h = SCREEN_H - TOOLBAR_H - 20
self.resize(pw, min(ph, max_h))
self.move(SCREEN_W - pw - 4, TOOLBAR_H)
def _add_items(self, vbox, items, depth):
indent = " " * depth
for item in items:
if is_folder(item):
lbl = QLabel(f"{indent}{item['name']}")
lbl.setFont(QFont("Arial", 14, QFont.Bold))
vbox.addWidget(lbl)
self._add_items(vbox, item["children"], depth + 1)
else:
btn = QPushButton(f"{indent} {item['name']}")
btn.setFont(QFont("Arial", 17))
btn.setMinimumHeight(46)
btn.setCursor(Qt.PointingHandCursor)
url = item["url"]
btn.clicked.connect(lambda _, u=url: self._nav(u))
vbox.addWidget(btn)
def _nav(self, url):
self.hide()
self.cdp.navigate(url)
def _on_manage(self):
self.hide()
self.overlay.open_manager()
def eventFilter(self, obj, event):
from PyQt5.QtCore import QEvent
if event.type() == QEvent.MouseButtonPress and self.isVisible():
gp = event.globalPos()
if not self.geometry().contains(gp):
self.hide()
return False
# ── Floating toolbar overlay ──────────────────────────────────────
class KioskOverlay(QWidget):
def __init__(self, cdp):
super().__init__()
self.cdp = cdp
self.setWindowFlags(
Qt.FramelessWindowHint |
Qt.WindowStaysOnTopHint |
Qt.Tool
)
self.setAttribute(Qt.WA_ShowWithoutActivating)
self.setGeometry(0, 0, SCREEN_W, TOP_H)
self.setStyleSheet("background:transparent;")
QApplication.instance().installEventFilter(self)
vbox = QVBoxLayout(self)
vbox.setContentsMargins(0, 0, 0, 0); vbox.setSpacing(0)
vbox.addWidget(self._build_toolbar())
self.bm_panel = BookmarkPanel(cdp, self)
self.show()
def _build_toolbar(self):
bar = QWidget(); bar.setFixedHeight(TOOLBAR_H)
bar.setStyleSheet(f"background:{TOOLBAR_COLOR};")
tl = QHBoxLayout(bar)
tl.setContentsMargins(24, 7, 16, 7); tl.setSpacing(10)
# Brand label
brand = QLabel("SeniorNet")
brand.setFont(QFont("Arial", 22, QFont.Bold))
brand.setStyleSheet("color:white;")
tl.addWidget(brand)
tl.addStretch()
# ◀ Back
back_btn = mk_icon_btn("", BTN_COLOR, BTN_HOVER)
back_btn.setToolTip("Go Back")
back_btn.clicked.connect(self.cdp.back)
tl.addWidget(back_btn)
# ▶ Forward
fwd_btn = mk_icon_btn("", BTN_COLOR, BTN_HOVER)
fwd_btn.setToolTip("Go Forward")
fwd_btn.clicked.connect(self.cdp.forward)
tl.addWidget(fwd_btn)
# ⌂ Home
home_btn = mk_icon_btn("", GOLD, GOLD_HOVER)
home_btn.setToolTip("Go Home")
home_btn.clicked.connect(lambda: self.cdp.navigate(HOME_URL))
tl.addWidget(home_btn)
# ☰ Hamburger — bookmarks menu
self.burger_btn = mk_icon_btn("", PURPLE, PURPLE_HOVER)
self.burger_btn.setToolTip("Bookmarks")
self.burger_btn.clicked.connect(self._show_bookmarks_menu)
tl.addWidget(self.burger_btn)
return bar
def _show_bookmarks_menu(self):
if self.bm_panel.isVisible():
self.bm_panel.hide()
else:
self.bm_panel.rebuild(load_bm())
self.bm_panel.show()
self.bm_panel.raise_()
def open_manager(self):
d = ManagerDialog(self)
d.exec_()
def keyPressEvent(self, e):
if e.key() == Qt.Key_Home:
self.cdp.navigate(HOME_URL)
e.accept()
def eventFilter(self, obj, event):
from PyQt5.QtCore import QEvent
if event.type() == QEvent.KeyPress:
key = event.key()
mods = event.modifiers()
blocked_keys = {Qt.Key_F4, Qt.Key_W, Qt.Key_T, Qt.Key_N,
Qt.Key_Tab, Qt.Key_F11, Qt.Key_Escape,
Qt.Key_F1, Qt.Key_F2, Qt.Key_F3,
Qt.Key_F5, Qt.Key_F6, Qt.Key_F10}
if mods & (Qt.AltModifier | Qt.ControlModifier):
if key in blocked_keys:
return True
if key in {Qt.Key_Escape, Qt.Key_F11}:
return True
return super().eventFilter(obj, event)
# ── Chrome launcher ───────────────────────────────────────────────
def launch_chrome():
import pathlib, json as _json, shutil
profile_dir = pathlib.Path("/home/jgitta/chromium-kiosk")
profile_dir.mkdir(parents=True, exist_ok=True)
for name in ("SingletonLock", "SingletonSocket", "SingletonCookie"):
try: (profile_dir / name).unlink()
except FileNotFoundError: pass
for rel in ("GraphiteDawnCache", "GrShaderCache", "ShaderCache",
"OptGuideOnDeviceClassifierModel", "OptGuideOnDeviceModel",
"optimization_guide_model_store",
"Default/GPUCache", "Default/DawnWebGPUCache",
"Default/DawnGraphiteCache", "Default/Segmentation Platform"):
p = profile_dir / rel
if p.exists():
shutil.rmtree(p, ignore_errors=True)
ls_path = profile_dir / "Local State"
if ls_path.exists():
try:
d = _json.loads(ls_path.read_text())
d.setdefault("profile", {})["exit_type"] = "Normal"
d["hardware_acceleration_mode_previous"] = False
ls_path.write_text(_json.dumps(d))
except Exception:
pass
# Clear the stale Roundcube session cookie so the autologin plugin always
# fires cleanly. Without this, Chrome sends the old cookie after a reboot,
# the PHP session in MariaDB has expired, and Roundcube shows
# "session is invalid or expired" instead of auto-logging in.
cookies_db = profile_dir / "Default" / "Cookies"
if cookies_db.exists():
try:
import sqlite3
conn = sqlite3.connect(str(cookies_db))
conn.execute(
"DELETE FROM cookies WHERE host_key LIKE '%mail.jgitta.com%'")
conn.commit()
conn.close()
print("Cleared stale Roundcube session cookie")
except Exception as e:
print(f"Cookie clear warning: {e}")
chrome_h = SCREEN_H - TOP_H
cmd = [
"chromium",
"--no-first-run",
"--no-default-browser-check",
"--disable-translate",
"--disable-infobars",
"--disable-session-crashed-bubble",
"--disable-save-password-bubble",
"--disable-restore-session-state",
"--suppress-message-center-popups",
"--disable-default-browser-check",
"--no-first-run",
"--disable-features=TranslateUI,OptimizationHints,OptimizationHintsFetching,OptimizationTargetPrediction,OptimizationGuideModelDownloading",
"--disable-component-update",
"--disable-background-networking",
"--disable-extensions",
"--disable-gpu",
"--no-sandbox",
"--disable-blink-features=AutomationControlled",
"--disable-dev-shm-usage",
"--user-data-dir=/home/jgitta/chromium-kiosk",
f"--remote-debugging-port={CDP_PORT}",
"--remote-allow-origins=*",
f"--window-position=0,{TOP_H}",
f"--window-size={SCREEN_W},{chrome_h}",
f"--app={HOME_URL}",
]
return subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
# ── Main ──────────────────────────────────────────────────────────
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setApplicationName("SeniorNet")
p = app.palette(); p.setColor(QPalette.Window, QColor("#1a3a5c")); app.setPalette(p)
cdp = CDP()
overlay = KioskOverlay(cdp)
chrome_proc = launch_chrome()
def dismiss_chrome_dialogs():
"""
Chrome shows startup dialogs (first-run welcome, profile errors) on a
fresh or corrupted profile. Dismiss them all automatically so the
senior never sees them. Runs for ~15 seconds after startup.
"""
dialogs = [
"Welcome to Google Chrome",
"Profile error occurred",
]
for _ in range(15):
time.sleep(1)
for title in dialogs:
subprocess.run(
["xdotool", "search", "--name", title, "key", "Return"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
def init_cdp():
if cdp.wait_ready(timeout=30):
print("CDP ready — kiosk running")
dismiss_chrome_dialogs()
threading.Thread(target=init_cdp, daemon=True).start()
sys.exit(app.exec_())