Update first-boot configuration and scripts for enhanced kiosk functionality</message>

<message>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.
This commit is contained in:
nearxos
2026-02-23 18:07:14 +02:00
parent 25bf710c67
commit c91cf6dd05
15 changed files with 194 additions and 31 deletions

View File

@@ -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"

View File

@@ -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

View File

@@ -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);
})();

View File

@@ -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": ["<all_urls>"],
"css": ["no-select.css"],
"js": ["inject.js"],
"run_at": "document_start"
}
]
}

View File

@@ -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;
}

View File

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

View File

@@ -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"

View File

@@ -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}" ;;

View File

@@ -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

View File

@@ -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
}

View File

@@ -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" <<KANSHI
profile {
output DSI-1 enable scale 1.000000 mode 800x1280@60.000 position 0,0 transform $DSI_ROTATE
}
KANSHI
chown -R "$PI_USER:$PI_USER" "$kanshi_dir"
log "Pre-created kanshi config (transform $DSI_ROTATE)"
if curl -fsSL "${FILE_SERVER}/set-rotation-at-login.sh" -o "$PI_HOME/set-rotation-at-login.sh" 2>/dev/null; then
chmod 755 "$PI_HOME/set-rotation-at-login.sh"
chown "$PI_USER:$PI_USER" "$PI_HOME/set-rotation-at-login.sh"

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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
}