From c91cf6dd05417fe369ae081b7380c27f516740b4 Mon Sep 17 00:00:00 2001 From: nearxos Date: Mon, 23 Feb 2026 18:07:14 +0200 Subject: [PATCH] Update first-boot configuration and scripts for enhanced kiosk functionality Modify the first-boot configuration to include the gir1.2-gtklayershell-0.1 package for improved GTK layer shell support. Update the first-boot script to enhance the portal status reporting with connection timeouts. Additionally, implement a restart mechanism for the kanshi service in rotation scripts to ensure immediate application of configuration changes. Introduce a Chromium kiosk extension to disable text selection, improving user experience in kiosk mode. These changes streamline the setup process and enhance the overall functionality of the kiosk environment. --- .../fileserver/01-set-rotation-once.sh | 8 ++ .../chromium-kiosk-launcher.desktop | 2 +- .../chromium-kiosk-no-select/inject.js | 37 ++++++++ .../chromium-kiosk-no-select/manifest.json | 14 +++ .../chromium-kiosk-no-select/no-select.css | 15 ++++ .../fileserver/five-tap-close-chromium.py | 86 +++++++++++++------ .../fileserver/set-rotation-at-login.sh | 7 ++ .../cloud-init/fileserver/start-chromium.sh | 6 +- .../fileserver/steps/04-kiosk_files.sh | 12 +++ .../cloud-init/fileserver/steps/06-lightdm.sh | 9 ++ .../fileserver/steps/11-oneshots.sh | 12 ++- .../fileserver/touchscreen-touch-only.quirks | 11 +++ emmc-provisioning/cloud-init/first-boot.conf | 2 +- .../cloud-init/first-boot.conf.example | 2 +- emmc-provisioning/cloud-init/first-boot.sh | 2 +- 15 files changed, 194 insertions(+), 31 deletions(-) create mode 100644 emmc-provisioning/cloud-init/fileserver/chromium-kiosk-no-select/inject.js create mode 100644 emmc-provisioning/cloud-init/fileserver/chromium-kiosk-no-select/manifest.json create mode 100644 emmc-provisioning/cloud-init/fileserver/chromium-kiosk-no-select/no-select.css create mode 100644 emmc-provisioning/cloud-init/fileserver/touchscreen-touch-only.quirks diff --git a/emmc-provisioning/cloud-init/fileserver/01-set-rotation-once.sh b/emmc-provisioning/cloud-init/fileserver/01-set-rotation-once.sh index 23ddf4f..0268f92 100644 --- a/emmc-provisioning/cloud-init/fileserver/01-set-rotation-once.sh +++ b/emmc-provisioning/cloud-init/fileserver/01-set-rotation-once.sh @@ -28,6 +28,14 @@ profile { EOF log "kanshi config written to $KANSHI_CONFIG" +# Restart kanshi so the config is applied immediately +if pgrep -u "$(id -u)" kanshi >/dev/null 2>&1; then + pkill -u "$(id -u)" kanshi 2>/dev/null + sleep 0.3 +fi +nohup /usr/bin/kanshi >/dev/null 2>&1 & +log "kanshi restarted" + # Set GTK dark theme (same as first-boot step 08) and force dark mode via gsettings GTK_THEME_NAME="PiXnoir" [[ -d /usr/share/themes/Adwaita-dark ]] && ! [[ -d /usr/share/themes/PiXnoir ]] && GTK_THEME_NAME="Adwaita-dark" diff --git a/emmc-provisioning/cloud-init/fileserver/chromium-kiosk-launcher.desktop b/emmc-provisioning/cloud-init/fileserver/chromium-kiosk-launcher.desktop index 7bf382d..d2e1a73 100644 --- a/emmc-provisioning/cloud-init/fileserver/chromium-kiosk-launcher.desktop +++ b/emmc-provisioning/cloud-init/fileserver/chromium-kiosk-launcher.desktop @@ -3,7 +3,7 @@ Type=Application Name=Start Chromium Kiosk Comment=Start Chromium in kiosk mode (use if the browser was closed) Exec=/home/pi/start-chromium.sh -Icon=chromium-browser +Icon=chromium Terminal=false Categories=Utility;Kiosk; StartupNotify=false diff --git a/emmc-provisioning/cloud-init/fileserver/chromium-kiosk-no-select/inject.js b/emmc-provisioning/cloud-init/fileserver/chromium-kiosk-no-select/inject.js new file mode 100644 index 0000000..5fdcded --- /dev/null +++ b/emmc-provisioning/cloud-init/fileserver/chromium-kiosk-no-select/inject.js @@ -0,0 +1,37 @@ +// Disable text selection: inject CSS and clear any selection (including from scrollbar). +(function() { + var style = document.createElement('style'); + style.id = 'kiosk-no-select'; + style.textContent = [ + 'html, body, body *, body *::before, body *::after {', + ' user-select: none !important; -webkit-user-select: none !important;', + ' -moz-user-select: none !important; -ms-user-select: none !important;', + '}', + 'input:focus, textarea:focus, [contenteditable="true"]:focus {', + ' user-select: text !important; -webkit-user-select: text !important;', + '}' + ].join(''); + (document.head || document.documentElement).appendChild(style); + + function allowSelect(el) { + return el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable); + } + + function preventSelect(e) { + if (allowSelect(e.target)) return; + e.preventDefault(); + } + + function clearSelection() { + var sel = document.getSelection(); + if (!sel || sel.rangeCount === 0) return; + var node = sel.anchorNode; + var el = node && (node.nodeType === 1 ? node : node.parentElement); + if (el && !allowSelect(el)) sel.removeAllRanges(); + } + + document.addEventListener('selectstart', preventSelect, true); + document.addEventListener('contextmenu', preventSelect, true); + document.addEventListener('selectionchange', clearSelection, true); + document.addEventListener('mousedown', function(e) { if (!allowSelect(e.target)) clearSelection(); }, true); +})(); diff --git a/emmc-provisioning/cloud-init/fileserver/chromium-kiosk-no-select/manifest.json b/emmc-provisioning/cloud-init/fileserver/chromium-kiosk-no-select/manifest.json new file mode 100644 index 0000000..84c791c --- /dev/null +++ b/emmc-provisioning/cloud-init/fileserver/chromium-kiosk-no-select/manifest.json @@ -0,0 +1,14 @@ +{ + "manifest_version": 2, + "name": "Kiosk: No Text Selection", + "version": "1.1", + "description": "Disables text selection so touch-drag scrolls instead of selecting.", + "content_scripts": [ + { + "matches": [""], + "css": ["no-select.css"], + "js": ["inject.js"], + "run_at": "document_start" + } + ] +} diff --git a/emmc-provisioning/cloud-init/fileserver/chromium-kiosk-no-select/no-select.css b/emmc-provisioning/cloud-init/fileserver/chromium-kiosk-no-select/no-select.css new file mode 100644 index 0000000..65791bc --- /dev/null +++ b/emmc-provisioning/cloud-init/fileserver/chromium-kiosk-no-select/no-select.css @@ -0,0 +1,15 @@ +/* Disable text selection so touch-drag scrolls. Apply to everything. */ +html, body, body *, +body *::before, body *::after, +input, textarea, [contenteditable="true"] { + user-select: none !important; + -webkit-user-select: none !important; + -moz-user-select: none !important; + -ms-user-select: none !important; +} + +/* Allow selection in inputs so users can edit if needed */ +input:focus, textarea:focus, [contenteditable="true"]:focus { + user-select: text !important; + -webkit-user-select: text !important; +} diff --git a/emmc-provisioning/cloud-init/fileserver/five-tap-close-chromium.py b/emmc-provisioning/cloud-init/fileserver/five-tap-close-chromium.py index 3eb4501..b3c8d76 100644 --- a/emmc-provisioning/cloud-init/fileserver/five-tap-close-chromium.py +++ b/emmc-provisioning/cloud-init/fileserver/five-tap-close-chromium.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 # 5 taps in the top-right corner of the screen close Chromium (kiosk). -# Run from session autostart. Requires: python3, PyGObject (Gtk), Wayland or X11. +# Uses wlr-layer-shell to create an overlay surface above all windows (including fullscreen). +# Falls back to a regular GTK window on X11. +# Run from session autostart. Requires: python3, PyGObject (Gtk3), gir1.2-gtklayershell-0.1. import logging import subprocess @@ -23,25 +25,19 @@ except Exception as e: log.error("need PyGObject Gtk: %s", e) sys.exit(1) +HAS_LAYER_SHELL = False +try: + gi.require_version("GtkLayerShell", "0.1") + from gi.repository import GtkLayerShell + HAS_LAYER_SHELL = True +except Exception: + log.warning("GtkLayerShell not available, falling back to regular window") + CORNER_SIZE = 80 TAP_WINDOW_SEC = 2.0 CHROMIUM_KILL_CMD = ["pkill", "-f", "chromium"] -def get_screen_size(): - display = Gdk.Display.get_default() - if not display: - log.warning("no display found, using fallback 800x1280") - return 800, 1280 - monitor = display.get_monitor(0) if display.get_n_monitors() else None - if monitor: - geom = monitor.get_geometry() - log.info("screen size: %dx%d", geom.width, geom.height) - return geom.width, geom.height - log.warning("no monitor found, using fallback 800x1280") - return 800, 1280 - - def on_button_press(widget, event, data): count, reset_timer = data count[0] += 1 @@ -69,6 +65,40 @@ def on_button_press(widget, event, data): return False +def get_screen_size(): + display = Gdk.Display.get_default() + if not display: + log.warning("no display found, using fallback 800x1280") + return 800, 1280 + monitor = display.get_monitor(0) if display.get_n_monitors() else None + if monitor: + geom = monitor.get_geometry() + log.info("screen size: %dx%d", geom.width, geom.height) + return geom.width, geom.height + log.warning("no monitor found, using fallback 800x1280") + return 800, 1280 + + +def make_window_layer_shell(win): + """Anchor an overlay surface to the top-right corner via wlr-layer-shell.""" + GtkLayerShell.init_for_window(win) + GtkLayerShell.set_layer(win, GtkLayerShell.Layer.OVERLAY) + GtkLayerShell.set_anchor(win, GtkLayerShell.Edge.TOP, True) + GtkLayerShell.set_anchor(win, GtkLayerShell.Edge.RIGHT, True) + GtkLayerShell.set_keyboard_mode(win, GtkLayerShell.KeyboardMode.NONE) + log.info("layer-shell overlay anchored top-right (%dx%d)", CORNER_SIZE, CORNER_SIZE) + + +def make_window_fallback(win): + """X11 fallback: position with move() and hope keep-above works.""" + win.set_keep_above(True) + win.realize() + w, _h = get_screen_size() + x = max(0, w - CORNER_SIZE) + win.move(x, 0) + log.info("X11 fallback: window positioned at x=%d y=0", x) + + def main(): win = Gtk.Window() win.set_decorated(False) @@ -76,27 +106,33 @@ def main(): win.set_default_size(CORNER_SIZE, CORNER_SIZE) win.set_skip_taskbar_hint(True) win.set_skip_pager_hint(True) - win.set_keep_above(True) - win.set_opacity(0.01) win.set_accept_focus(False) win.set_focus_on_map(False) - # Allow closing from compositor / taskbar + + # Make the window nearly invisible + screen = win.get_screen() + visual = screen.get_rgba_visual() + if visual: + win.set_visual(visual) + win.set_app_paintable(True) + win.connect("draw", lambda w, cr: (cr.set_source_rgba(0, 0, 0, 0.01), cr.paint())) + + if HAS_LAYER_SHELL and GtkLayerShell.is_supported(): + make_window_layer_shell(win) + else: + make_window_fallback(win) + win.connect("destroy", Gtk.main_quit) - # Count 5 taps + count = [0] reset_timer = [None] win.connect("button-press-event", on_button_press, (count, reset_timer)) - # Touch events often come as button-press with button=1 win.set_events( win.get_events() | Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.TOUCH_MASK ) - win.realize() - w, h = get_screen_size() - x = max(0, w - CORNER_SIZE) - win.move(x, 0) - log.info("window %dx%d positioned at x=%d y=0", CORNER_SIZE, CORNER_SIZE, x) + win.show_all() log.info("listening for taps (5 within %.1fs to kill chromium)", TAP_WINDOW_SEC) Gtk.main() diff --git a/emmc-provisioning/cloud-init/fileserver/set-rotation-at-login.sh b/emmc-provisioning/cloud-init/fileserver/set-rotation-at-login.sh index 760d673..a425d68 100644 --- a/emmc-provisioning/cloud-init/fileserver/set-rotation-at-login.sh +++ b/emmc-provisioning/cloud-init/fileserver/set-rotation-at-login.sh @@ -19,6 +19,13 @@ profile { } EOF +# Restart kanshi so the config is applied immediately +if pgrep -u "$(id -u)" kanshi >/dev/null 2>&1; then + pkill -u "$(id -u)" kanshi 2>/dev/null + sleep 0.3 +fi +nohup /usr/bin/kanshi >/dev/null 2>&1 & + # Set GTK dark theme (same as first-boot step 08) and force dark mode via gsettings GTK_THEME_NAME="PiXnoir" [[ -d /usr/share/themes/Adwaita-dark ]] && ! [[ -d /usr/share/themes/PiXnoir ]] && GTK_THEME_NAME="Adwaita-dark" diff --git a/emmc-provisioning/cloud-init/fileserver/start-chromium.sh b/emmc-provisioning/cloud-init/fileserver/start-chromium.sh index 5ec2d33..1dc6ba0 100755 --- a/emmc-provisioning/cloud-init/fileserver/start-chromium.sh +++ b/emmc-provisioning/cloud-init/fileserver/start-chromium.sh @@ -29,7 +29,11 @@ CHROMIUM_APP_URL="${CHROMIUM_APP_URL:-https://tototheo.com}" # Mode: "kiosk" (fills whole screen, no taskbar gap) or "fullscreen" CHROMIUM_MODE="${CHROMIUM_MODE:-kiosk}" -CHROMIUM_OPTS="--noerrdialogs --disable-infobars --disable-session-crashed-bubble --disable-restore-session-state --no-first-run --password-store=basic --use-mock-keychain --ozone-platform=wayland --enable-features=WaylandWindowDecorations --disable-features=UseChromeOSDirectVideoDecoder --app=${CHROMIUM_APP_URL}" +# Extension disables text selection so touch-drag scrolls (see chromium-kiosk-no-select) +EXTENSION_DIR="${HOME}/.local/share/chromium-kiosk-no-select" +CHROMIUM_OPTS="--noerrdialogs --disable-infobars --disable-session-crashed-bubble --disable-restore-session-state --no-first-run --password-store=basic --use-mock-keychain --ozone-platform=wayland --enable-features=WaylandWindowDecorations,TouchpadOverscrollHistoryNavigation --disable-features=UseChromeOSDirectVideoDecoder --touch-events=enabled --enable-pinch --disable-translate" +[ -d "$EXTENSION_DIR" ] && CHROMIUM_OPTS="$CHROMIUM_OPTS --load-extension=$EXTENSION_DIR" +CHROMIUM_OPTS="$CHROMIUM_OPTS --app=${CHROMIUM_APP_URL}" case "${CHROMIUM_MODE}" in kiosk) CHROMIUM_OPTS="--kiosk ${CHROMIUM_OPTS}" ;; diff --git a/emmc-provisioning/cloud-init/fileserver/steps/04-kiosk_files.sh b/emmc-provisioning/cloud-init/fileserver/steps/04-kiosk_files.sh index 92152c5..ce70fcc 100644 --- a/emmc-provisioning/cloud-init/fileserver/steps/04-kiosk_files.sh +++ b/emmc-provisioning/cloud-init/fileserver/steps/04-kiosk_files.sh @@ -4,6 +4,16 @@ step_04_kiosk_files() { mkdir -p "$AUTOSTART" "$PI_HOME/Desktop" "$PI_HOME/.local/share/applications" + # Extension: disable text selection so touch-drag scrolls + EXT_DIR="$PI_HOME/.local/share/chromium-kiosk-no-select" + mkdir -p "$EXT_DIR" + if curl -fsSL "${FILE_SERVER}/chromium-kiosk-no-select/manifest.json" -o "$EXT_DIR/manifest.json" 2>/dev/null && \ + curl -fsSL "${FILE_SERVER}/chromium-kiosk-no-select/no-select.css" -o "$EXT_DIR/no-select.css" 2>/dev/null && \ + curl -fsSL "${FILE_SERVER}/chromium-kiosk-no-select/inject.js" -o "$EXT_DIR/inject.js" 2>/dev/null; then + chown -R "$PI_USER:$PI_USER" "$EXT_DIR" + log "Chromium kiosk no-select extension installed" + fi + curl -fsSL "${FILE_SERVER}/start-chromium.sh" -o "$PI_HOME/start-chromium.sh" curl -fsSL "${FILE_SERVER}/chromium-kiosk.desktop" -o "$AUTOSTART/chromium-kiosk.desktop" chmod 755 "$PI_HOME/start-chromium.sh" @@ -17,6 +27,8 @@ step_04_kiosk_files() { chmod 755 "$PI_HOME/Desktop/Chromium Kiosk.desktop" chmod 644 "$PI_HOME/.local/share/applications/chromium-kiosk-launcher.desktop" chown "$PI_USER:$PI_USER" "$PI_HOME/Desktop/Chromium Kiosk.desktop" "$PI_HOME/.local/share/applications/chromium-kiosk-launcher.desktop" + # Mark as trusted so pcmanfm shows the icon instead of a text file + sudo -u "$PI_USER" gio set "$PI_HOME/Desktop/Chromium Kiosk.desktop" metadata::trusted true 2>/dev/null || true rm -f /tmp/chromium-kiosk-launcher.desktop log "Chromium kiosk launcher installed" fi diff --git a/emmc-provisioning/cloud-init/fileserver/steps/06-lightdm.sh b/emmc-provisioning/cloud-init/fileserver/steps/06-lightdm.sh index 48ad3f2..0391d6e 100644 --- a/emmc-provisioning/cloud-init/fileserver/steps/06-lightdm.sh +++ b/emmc-provisioning/cloud-init/fileserver/steps/06-lightdm.sh @@ -36,4 +36,13 @@ AWAITSVC systemctl daemon-reload systemctl enable cm4-await-display.service 2>/dev/null || true log "cm4-await-display.service installed (wait for DSI, max 30s)" + + # Enable VNC (wayvnc) for remote access + if command -v raspi-config >/dev/null 2>&1; then + raspi-config nonint do_vnc 0 + log "VNC (wayvnc) enabled on port 5900" + else + log "WARNING: raspi-config not found, skipping VNC setup" + fi + } diff --git a/emmc-provisioning/cloud-init/fileserver/steps/11-oneshots.sh b/emmc-provisioning/cloud-init/fileserver/steps/11-oneshots.sh index 3dee2c4..4df3aff 100644 --- a/emmc-provisioning/cloud-init/fileserver/steps/11-oneshots.sh +++ b/emmc-provisioning/cloud-init/fileserver/steps/11-oneshots.sh @@ -2,9 +2,19 @@ # Step 11: Install one-shot and per-login scripts step_11_oneshots() { - # Rotation: install set-rotation-at-login (re-applies kanshi config every login) + # Rotation: pre-create kanshi config so it's ready before the desktop session starts, + # then install set-rotation-at-login to re-apply on every login. if [[ -n "$DSI_ROTATE" ]]; then log "Rotation $DSI_ROTATE set via cmdline" + local kanshi_dir="$PI_HOME/.config/kanshi" + mkdir -p "$kanshi_dir" + cat > "$kanshi_dir/config" </dev/null; then chmod 755 "$PI_HOME/set-rotation-at-login.sh" chown "$PI_USER:$PI_USER" "$PI_HOME/set-rotation-at-login.sh" diff --git a/emmc-provisioning/cloud-init/fileserver/touchscreen-touch-only.quirks b/emmc-provisioning/cloud-init/fileserver/touchscreen-touch-only.quirks new file mode 100644 index 0000000..c964c39 --- /dev/null +++ b/emmc-provisioning/cloud-init/fileserver/touchscreen-touch-only.quirks @@ -0,0 +1,11 @@ +# libinput local overrides: make touchscreen touch-only (no pointer emulation). +# Prevents touch-drag from being interpreted as mouse selection in Chromium. +# Match reTerminal DM Goodix touchscreen; disables INPUT_PROP_POINTER so +# libinput sends only touch events — scrolling works, text selection does not. +# +# Install to: /etc/libinput/local-overrides.quirks +# Then: udevadm trigger (or reboot). See device-quirks.html in libinput doc. + +[Touchscreen touch-only for kiosk] +MatchName=*Goodix*TouchScreen* +AttrInputProp=-INPUT_PROP_POINTER diff --git a/emmc-provisioning/cloud-init/first-boot.conf b/emmc-provisioning/cloud-init/first-boot.conf index 13aa363..e2439a7 100644 --- a/emmc-provisioning/cloud-init/first-boot.conf +++ b/emmc-provisioning/cloud-init/first-boot.conf @@ -8,7 +8,7 @@ HOSTNAME="guard" PI_USER="pi" # --- Packages --- -PACKAGES="git chromium wmctrl openssh-server swaybg wlr-randr maliit-keyboard xinput-calibrator python3-gi python3-gi-cairo" +PACKAGES="git chromium wmctrl openssh-server swaybg wlr-randr maliit-keyboard xinput-calibrator python3-gi python3-gi-cairo gir1.2-gtklayershell-0.1" # --- Desktop & theme --- LIGHTDM_SESSION="rpd-labwc" diff --git a/emmc-provisioning/cloud-init/first-boot.conf.example b/emmc-provisioning/cloud-init/first-boot.conf.example index 952ebca..ab45c91 100644 --- a/emmc-provisioning/cloud-init/first-boot.conf.example +++ b/emmc-provisioning/cloud-init/first-boot.conf.example @@ -13,7 +13,7 @@ HOSTNAME="guard" PI_USER="pi" # --- Packages --- -PACKAGES="git chromium wmctrl openssh-server swaybg wlr-randr maliit-keyboard xinput-calibrator python3-gi python3-gi-cairo" +PACKAGES="git chromium wmctrl openssh-server swaybg wlr-randr maliit-keyboard xinput-calibrator python3-gi python3-gi-cairo gir1.2-gtklayershell-0.1" # --- Desktop & theme --- LIGHTDM_SESSION="rpd-labwc" diff --git a/emmc-provisioning/cloud-init/first-boot.sh b/emmc-provisioning/cloud-init/first-boot.sh index 2678857..77eea6d 100644 --- a/emmc-provisioning/cloud-init/first-boot.sh +++ b/emmc-provisioning/cloud-init/first-boot.sh @@ -68,7 +68,7 @@ log "FILE_SERVER=$FILE_SERVER PI_USER=$PI_USER HOSTNAME=$HOSTNAME" # ── Portal status API ────────────────────────────────────────────────── report_status() { [[ -z "$PORTAL_BASE" ]] && return 0 - curl -s -X POST -H "Content-Type: application/json" \ + curl -s --connect-timeout 5 --max-time 10 -X POST -H "Content-Type: application/json" \ -d "{\"phase\":\"$1\",\"message\":\"$2\",\"step\":\"$3\",\"step_name\":\"$4\",\"hostname\":\"$HOSTNAME\",\"ip\":\"$5\"}" \ "${PORTAL_BASE}/api/first-boot-status" || true }