<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.
143 lines
4.4 KiB
Python
143 lines
4.4 KiB
Python
#!/usr/bin/env python3
|
|
# 5 taps in the top-right corner of the screen close Chromium (kiosk).
|
|
# 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
|
|
import sys
|
|
|
|
LOG_TAG = "five-tap"
|
|
logging.basicConfig(
|
|
stream=sys.stderr,
|
|
level=logging.DEBUG,
|
|
format=f"{LOG_TAG}: %(message)s",
|
|
)
|
|
log = logging.getLogger(LOG_TAG)
|
|
|
|
try:
|
|
import gi
|
|
gi.require_version("Gdk", "3.0")
|
|
gi.require_version("Gtk", "3.0")
|
|
from gi.repository import Gdk, Gtk, GLib
|
|
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 on_button_press(widget, event, data):
|
|
count, reset_timer = data
|
|
count[0] += 1
|
|
log.debug("tap %d of 5 at (%.0f, %.0f)", count[0], event.x_root, event.y_root)
|
|
if reset_timer[0]:
|
|
GLib.source_remove(reset_timer[0])
|
|
if count[0] >= 5:
|
|
count[0] = 0
|
|
log.info("5 taps reached — killing chromium")
|
|
try:
|
|
result = subprocess.run(CHROMIUM_KILL_CMD, timeout=2, capture_output=True)
|
|
log.info("pkill returncode=%d", result.returncode)
|
|
except Exception as e:
|
|
log.error("pkill failed: %s", e)
|
|
if reset_timer[0]:
|
|
GLib.source_remove(reset_timer[0])
|
|
reset_timer[0] = None
|
|
return
|
|
def reset():
|
|
log.debug("tap count reset (%.1fs timeout)", TAP_WINDOW_SEC)
|
|
count[0] = 0
|
|
reset_timer[0] = None
|
|
return False
|
|
reset_timer[0] = GLib.timeout_add(int(TAP_WINDOW_SEC * 1000), reset)
|
|
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)
|
|
win.set_resizable(False)
|
|
win.set_default_size(CORNER_SIZE, CORNER_SIZE)
|
|
win.set_skip_taskbar_hint(True)
|
|
win.set_skip_pager_hint(True)
|
|
win.set_accept_focus(False)
|
|
win.set_focus_on_map(False)
|
|
|
|
# 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 = [0]
|
|
reset_timer = [None]
|
|
win.connect("button-press-event", on_button_press, (count, reset_timer))
|
|
win.set_events(
|
|
win.get_events()
|
|
| Gdk.EventMask.BUTTON_PRESS_MASK
|
|
| Gdk.EventMask.TOUCH_MASK
|
|
)
|
|
|
|
win.show_all()
|
|
log.info("listening for taps (5 within %.1fs to kill chromium)", TAP_WINDOW_SEC)
|
|
Gtk.main()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|