To reproduce
- Run This simple textual app in vscode integrated terminal
Minimal repro app
from textual.app import App, ComposeResult
from textual.widgets import Footer, Header, Static
class MinimalApp(App[None]):
CSS = """
#ruler {
width: 1;
height: 1fr;
margin-left: 95;
background: $primary;
}
"""
def compose(self) -> ComposeResult:
yield Static("", id="ruler")
if __name__ == "__main__":
MinimalApp().run()
- Reload vscode (ctrl shift p + reload window)
- Move the mouse to the right of the vertical bar
It crashes
The bug
When a terminal is torn down and rebuilt underneath a running Textual app — a tmux detach/reattach, a VS Code window reload, an SSH reconnect, etc. — the emulator loses the mouse-mode negotiation the app performed at startup. The app is still running and still believes SGR mouse mode (?1006) is active, but the freshly rebuilt terminal has reset to the legacy X10 mouse encoding.
In X10 encoding a mouse report is ESC [ M Cb Cx Cy, where each coordinate is a raw byte equal to value + 32. Once the pointer is past roughly column 95 the coordinate byte exceeds 0x7F and is no longer valid ASCII/UTF-8 — it is an invalid or incomplete UTF-8 lead byte.
The Linux driver decodes stdin with a strict incremental UTF-8 decoder:
https://github.com/Textualize/textual/blob/main/src/textual/drivers/linux_driver.py — _run_input_thread:
The terminal, having lost the SGR negotiation, sends an X10 report; the strict decoder throws UnicodeDecodeError.
utf8_decoder = getincrementaldecoder("utf-8")().decode # strict
...
unicode_data = decode(read(fileno, 1024 * 4), final=final and last)
So the first X10 mouse report emitted after a reattach (with the pointer past ~col 95) raises UnicodeDecodeError on that raw coordinate byte and crashes the input thread / the app.
Why it's a driver issue
The X10 fallback is entirely outside the app's control — it happens because the emulator is rebuilt, not because of anything the app does. Two things compound:
- The strict UTF-8 decoder turns any non-UTF-8 stdin byte into a hard crash, rather than tolerating malformed/legacy input.
- Textual re-arms other modes on resume but doesn't re-assert mouse mode after a terminal rebuild, so the terminal stays in X10.
Suggested fixes (either would help)
- Decode stdin with
errors="replace" (or otherwise tolerate non-UTF-8 bytes) so a stray legacy mouse byte can never crash the input thread.
- Re-assert SGR mouse mode on resume / when an X10 report (
ESC [ M) is seen, so the terminal switches back to SGR encoding.
Environment
- Textual 8.2.8
- Linux driver (
linux_driver.py)
- Triggered under tmux reattach and VS Code terminal reload.
To reproduce
Minimal repro app
It crashes
The bug
When a terminal is torn down and rebuilt underneath a running Textual app — a tmux detach/reattach, a VS Code window reload, an SSH reconnect, etc. — the emulator loses the mouse-mode negotiation the app performed at startup. The app is still running and still believes SGR mouse mode (
?1006) is active, but the freshly rebuilt terminal has reset to the legacy X10 mouse encoding.In X10 encoding a mouse report is
ESC [ M Cb Cx Cy, where each coordinate is a raw byte equal tovalue + 32. Once the pointer is past roughly column 95 the coordinate byte exceeds0x7Fand is no longer valid ASCII/UTF-8 — it is an invalid or incomplete UTF-8 lead byte.The Linux driver decodes stdin with a strict incremental UTF-8 decoder:
https://github.com/Textualize/textual/blob/main/src/textual/drivers/linux_driver.py —
_run_input_thread:The terminal, having lost the SGR negotiation, sends an X10 report; the strict decoder throws
UnicodeDecodeError.So the first X10 mouse report emitted after a reattach (with the pointer past ~col 95) raises
UnicodeDecodeErroron that raw coordinate byte and crashes the input thread / the app.Why it's a driver issue
The X10 fallback is entirely outside the app's control — it happens because the emulator is rebuilt, not because of anything the app does. Two things compound:
Suggested fixes (either would help)
errors="replace"(or otherwise tolerate non-UTF-8 bytes) so a stray legacy mouse byte can never crash the input thread.ESC [ M) is seen, so the terminal switches back to SGR encoding.Environment
linux_driver.py)