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:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
})();
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}" ;;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user