#!/usr/bin/env python3 """dim-probe.py — measure what a headless kiosk actually renders. A kiosk has no keyboard, no monitor you can read and (on Silverblue) no xdotool/grim to ask the compositor. So ask the browser instead: this serves one page that reports its own viewport and screen size back as a request, which lands in this server's log. Run it on a machine the kiosk can reach: python3 dim-probe.py # listens on 0.0.0.0:8099 then point the kiosk at it for a moment: sudo KIOSK_URL=http://:8099/ sh install-kiosk.sh sudo pkill -u kiosk firefox # the kiosk loop reloads and relaunches `innerWidth == screen.width` and `fullscreenElement == null` is the signature of a real fullscreen window: the page occupies the whole output, so there is no browser chrome and no panel left over to measure. (Firefox does not report itself as a document.fullscreenElement when the window is fullscreen at startup — that API is for in-page requestFullscreen.) Read-only, no dependencies, ctrl-C to stop. """ from __future__ import annotations import http.server import socketserver import sys import time PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8099 PAGE = b"""dim-probe
dim-probe
""" PNG1x1 = bytes.fromhex( "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489" "0000000d4944415478da6360000002000100fdff03fd0000000049454e44ae426082" ) class Handler(http.server.BaseHTTPRequestHandler): def do_GET(self) -> None: if self.path.startswith("/d?"): query = self.path.split("?", 1)[1] fields = dict(kv.split("=", 1) for kv in query.split("&") if "=" in kv) print( f"{time.strftime('%H:%M:%S')} " f"{self.client_address[0]:<15} " f"{fields.get('tag', '?'):<5} " f"viewport={fields.get('inner', '?'):<11} " f"window={fields.get('outer', '?'):<11} " f"screen={fields.get('screen', '?'):<11} " f"available={fields.get('avail', '?'):<11} " f"fsElement={fields.get('fsElement', '?')}", flush=True, ) self.send_response(200) self.send_header("Content-Type", "image/png") self.send_header("Cache-Control", "no-store") self.send_header("Content-Length", str(len(PNG1x1))) self.end_headers() self.wfile.write(PNG1x1) return self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Cache-Control", "no-store") self.send_header("Content-Length", str(len(PAGE))) self.end_headers() self.wfile.write(PAGE) def log_message(self, *args: object) -> None: """Silence the default access log; the /d beacon above is the signal.""" if __name__ == "__main__": socketserver.TCPServer.allow_reuse_address = True with socketserver.ThreadingTCPServer(("0.0.0.0", PORT), Handler) as httpd: print(f"dim-probe on http://0.0.0.0:{PORT}/ — waiting for the kiosk…", flush=True) try: httpd.serve_forever() except KeyboardInterrupt: sys.exit(0)