WebDriver ignores App.run(mouse=False) and enables mouse reporting anyway
Have you checked closed issues? Yes. #4376 was this same bug in LinuxDriver, fixed by #4379 (see below). #3898 (a request for a runtime toggle, closed not-planned) and #5660 (how to disable mouse support) are about wanting the mouse off. None reports the documented switch having no effect on WebDriver.
Have you checked against the most recent version of Textual? Yes, 8.2.8, current on PyPI, and the code below is byte-identical on main.
The bug
App.run(mouse=False) is documented as controlling mouse support (app.py, run(), "mouse: Enable mouse support."). The three terminal drivers honour it. WebDriver does not, so under textual-serve / Textual Web the parameter is silently ignored.
WebDriver.__init__ accepts mouse and forwards it to Driver.__init__, which stores it as self._mouse. Nothing in web_driver.py ever reads it (linux_driver.py checks it in three places). Two places produce the escapes:
web_driver.py:111_enable_mouse_support(): noself._mousecheck, unlikelinux_driver.py:130,linux_inline_driver.py:81andwindows_driver.py:58, which each guard withif not self._mouse: return.web_driver.py:160:self.write("\033[?1003h"), written directly instart_application_mode()three lines after_enable_mouse_support()at:157. So guarding the method alone would still leak this one.
(web_driver.py:127 _disable_mouse_support() is likewise unguarded, but it is currently unreachable: nothing calls it, including stop_application_mode(). Worth guarding for consistency; it is not part of the observed failure.)
Related changes
#4343 introduced mouse and guarded mouse support in the three terminal drivers. In WebDriver, it added the constructor parameter and forwarded it to Driver, without adding the guards.
#4379 removed the direct ?1003h write from LinuxDriver.start_application_mode(). The equivalent write remains in web_driver.py:160.
Reproducer
Uses only the public API, needs no tty, and should run on any platform (it imports no POSIX-only driver module):
"""Reproducer: WebDriver ignores App.run(mouse=False).
Run with: python repro.py
Needs only `textual` installed (tested on 8.2.8). No terminal/tty required.
"""
import os
import subprocess
import sys
import tempfile
APP = """
from textual.app import App
class MyApp(App):
def on_mount(self) -> None:
self.exit()
MyApp().run(mouse={mouse})
"""
MOUSE_SEQUENCES = [
("\x1b[?1000h", "SET_VT200_MOUSE"),
("\x1b[?1003h", "SET_ANY_EVENT_MOUSE"),
("\x1b[?1015h", "SET_VT200_HIGHLIGHT_MOUSE"),
("\x1b[?1006h", "SET_SGR_EXT_MODE_MOUSE"),
]
HANDSHAKE = b"__GANGLION__\n"
def decode(raw: bytes) -> str:
"""Unwrap WebDriver's `D<4-byte len><payload>` framing into the terminal stream."""
if raw.startswith(HANDSHAKE):
raw = raw[len(HANDSHAKE):]
out, i = [], 0
while i + 5 <= len(raw):
kind = raw[i:i + 1]
size = int.from_bytes(raw[i + 1:i + 5], "big")
if kind == b"D":
out.append(raw[i + 5:i + 5 + size].decode("utf-8", "replace"))
i += 5 + size
return "".join(out)
def run(mouse: bool) -> str:
with tempfile.TemporaryDirectory() as tmp:
path = os.path.join(tmp, "app.py")
with open(path, "w") as file:
file.write(APP.format(mouse=mouse))
env = {
**os.environ,
# Driver and dimensions used by textual-serve.
"TEXTUAL_DRIVER": "textual.drivers.web_driver:WebDriver",
"COLUMNS": "80",
"ROWS": "24",
}
proc = subprocess.run(
[sys.executable, path], env=env,
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
timeout=60,
)
if proc.returncode != 0:
sys.exit(proc.stderr.decode("utf-8", "replace"))
return decode(proc.stdout)
for mouse in (True, False):
stream = run(mouse)
counts = " ".join(f"{name}={stream.count(seq)}" for seq, name in MOUSE_SEQUENCES)
print(f"App.run(mouse={mouse!s:5}) -> {counts}")Output: the two rows are identical, and SET_ANY_EVENT_MOUSE=2 is the duplicate from point 2 above:
App.run(mouse=True ) -> SET_VT200_MOUSE=1 SET_ANY_EVENT_MOUSE=2 SET_VT200_HIGHLIGHT_MOUSE=1 SET_SGR_EXT_MODE_MOUSE=1
App.run(mouse=False) -> SET_VT200_MOUSE=1 SET_ANY_EVENT_MOUSE=2 SET_VT200_HIGHLIGHT_MOUSE=1 SET_SGR_EXT_MODE_MOUSE=1Expected: the mouse=False row is all zeros, as it is for LinuxDriver.
This selects WebDriver through App.run(), using the TEXTUAL_DRIVER value set by textual-serve in AppService._build_environment().
Suggested fix
Add the if not self._mouse: return guard to both methods, and delete web_driver.py:160, matching what #4379 did to linux_driver.py. Deleting rather than making it conditional also removes a duplicate that is redundant even when mouse=True (SET_ANY_EVENT_MOUSE drops from 2 to 1, matching LinuxDriver).
With this change, I still see the alt-screen, hide-cursor, sync-query, bracketed-paste and OSC 22 sequences in the captured output.
Why it matters
Mainly the API contract: a documented parameter is silently ignored on one driver, and an app that opts out of mouse support still gets mouse reporting turned on.
One practical use for mouse=False is ordinary drag-to-select behavior in the host terminal. Mouse reporting can interfere with that behavior, depending on the host and its selection modifiers. Textual's built-in selection, available since v2.0.0, provides another way to select text.
textual diagnose
Textual Diagnostics
Versions
| Name | Value |
|---|---|
| Textual | 8.2.8 |
| Rich | 15.0.0 |
Python
| Name | Value |
|---|---|
| Version | 3.14.2 |
| Implementation | CPython |
| Compiler | Clang 17.0.0 (clang-1700.4.4.1) |
| Executable | <venv> |
Operating System
| Name | Value |
|---|---|
| System | Darwin |
| Release | 25.4.0 |
| Version | Darwin Kernel Version 25.4.0: Thu Mar 19 19:31:09 PDT 2026; root:xnu-12377.101.15~1/RELEASE_ARM64_T8132 |
Terminal
| Name | Value |
|---|---|
| Terminal Application | Apple_Terminal (470) |
| TERM | xterm-256color |
| COLORTERM | truecolor |
| FORCE_COLOR | Not set |
| NO_COLOR | Not set |
Rich Console options
| Name | Value |
|---|---|
| size | width=80, height=25 |
| legacy_windows | False |
| min_width | 1 |
| max_width | 80 |
| is_terminal | True |
| encoding | utf-8 |
| max_height | 25 |
| justify | None |
| overflow | None |
| no_wrap | False |
| highlight | None |
| markup | None |
| height | None |
Happy to open a PR.
Source: Textualize/textual