Skip to content

UnicodeDecodeError crash: terminal rebuild (tmux reattach / VS Code reload) drops SGR mouse negotiation, falls back to X10 raw bytes #6668

Description

@le-codeur-rapide

To reproduce

  1. 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()
  1. Reload vscode (ctrl shift p + reload window)
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions