From 8a38a21be81628366f30a6843674f271e5b79e5d Mon Sep 17 00:00:00 2001 From: Matt Parrett Date: Sun, 11 Oct 2026 04:24:42 +0000 Subject: [PATCH] shells: hide the xterm viewport scrollbar xterm.css sets .xterm-viewport to overflow-y:scroll, which reserves a scrollbar even with scrollback:0. Wherever scrollbars are classic (Linux, Windows, macOS with a mouse attached) that painted a 15px bar over the right edge of the mandelbrot and pathtrace images. Nothing scrolls in either demo, so hide it; it needs !important because the inlined xterm.css comes after the shell's styles. Headless Chromium hides scrollbars by default, so CI never saw it. The viewport test now launches with scrollbars drawn and fails on any gutter; against the current live site it reports 15px at both sizes. Co-Authored-By: Claude Opus 5.5 --- demos/mandelbrot/shell.html | 5 +++++ demos/pathtrace/shell.html | 5 +++++ test/viewport_fit_test.py | 10 ++++++++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/demos/mandelbrot/shell.html b/demos/mandelbrot/shell.html index 5e65c2b..0ddac56 100644 --- a/demos/mandelbrot/shell.html +++ b/demos/mandelbrot/shell.html @@ -25,6 +25,11 @@ @media (orientation: landscape) { body { justify-content: center; } } /* transition eases the pop-to-bright when a freshly computed frame lands */ #app { flex: 0 0 auto; transition: filter 0.15s ease-out; } + /* xterm.css gives the viewport overflow-y:scroll, which reserves a scrollbar + even with scrollback:0. Where scrollbars are classic (Linux, Windows, macOS + with a mouse) it paints a 15px bar over the image's right edge. Nothing + scrolls here, so hide it; !important because xterm.css loads after us. */ + .xterm .xterm-viewport { overflow-y: hidden !important; } /* Touch-only ⌨ toggle (created in JS): re-summons the soft keyboard we suppress by default, so the keyboard controls (hjkl ,. r q) stay reachable on a phone. Floats over the top-right corner; .on marks it active. */ diff --git a/demos/pathtrace/shell.html b/demos/pathtrace/shell.html index 75b7abc..4caeea8 100644 --- a/demos/pathtrace/shell.html +++ b/demos/pathtrace/shell.html @@ -19,6 +19,11 @@ body { display: flex; flex-direction: column; align-items: center; justify-content: flex-start; } @media (orientation: landscape) { body { justify-content: center; } } #app { flex: 0 0 auto; } + /* xterm.css gives the viewport overflow-y:scroll, which reserves a scrollbar + even with scrollback:0. Where scrollbars are classic (Linux, Windows, macOS + with a mouse) it paints a 15px bar over the image's right edge. Nothing + scrolls here, so hide it; !important because xterm.css loads after us. */ + .xterm .xterm-viewport { overflow-y: hidden !important; } /* Touch-only ⌨ toggle: re-summons the soft keyboard so the keyboard controls (hjkl n r q) stay reachable on a phone. */ #kbd-toggle { diff --git a/test/viewport_fit_test.py b/test/viewport_fit_test.py index 0625b10..649d97e 100644 --- a/test/viewport_fit_test.py +++ b/test/viewport_fit_test.py @@ -4,6 +4,7 @@ Drives the served browser bundle with Playwright. For each viewport it asserts: - no horizontal overflow (the terminal box fits the viewport width), + - no scrollbar on the xterm viewport (Chromium runs with scrollbars drawn), - the program adapted SCALE (smaller on the phone, 3 on desktop), - no console/page errors, - a center click keeps the center x within one cell and zooms in, @@ -71,7 +72,9 @@ def new_page(p, w, h): """Launch a chromium page at the given viewport with error capture wired and the document loaded. Returns (page, errs); errs accumulates pageerror + console-error text for the caller to assert on.""" - pg = p.chromium.launch().new_context(viewport={"width": w, "height": h}).new_page() + # Headless Chromium hides scrollbars by default; draw them so a classic + # scrollbar over the terminal shows up as a measurable gutter. + pg = p.chromium.launch(ignore_default_args=["--hide-scrollbars"]).new_context(viewport={"width": w, "height": h}).new_page() errs = [] pg.on("pageerror", lambda e: errs.append(str(e))) pg.on("console", lambda m: errs.append(m.text) if m.type == "error" else None) @@ -108,8 +111,11 @@ def check_viewport(p, tag, w, h, want_scale): "() => {const el=document.getElementById('app')||document.getElementById('terminal');" "const r=el.getBoundingClientRect();" "return {scale:window.__lgScale, grid:window.__lgGrid, termW:Math.round(r.width), " - "vpW:window.innerWidth, overflowX:Math.round(r.width)>window.innerWidth+1};}" + "vpW:window.innerWidth, overflowX:Math.round(r.width)>window.innerWidth+1, " + "gutter:(v=>v?v.offsetWidth-v.clientWidth:0)(document.querySelector('.xterm-viewport'))};}" ) + if info["gutter"]: + fails.append(f"{tag}: {info['gutter']}px scrollbar on the xterm viewport") if info["overflowX"]: fails.append(f"{tag}: terminal {info['termW']}px overflows viewport {info['vpW']}px (clipped)") if info["scale"] != want_scale: