#!/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()