Skip to content

fix: restore terminal mouse tracking state on PTY session disconnect #189

@inureyes

Description

@inureyes

Problem / Background

After a bssh PTY session disconnects (whether normally, via Ctrl+C, network failure, or panic), moving the mouse on the local terminal causes raw escape sequences to be printed to the screen, e.g.:

0;59;35M32;58;35M32;57;35M32;56;35M ... 35;38;36M35;38;37M ...

These are X11 mouse tracking events in SGR mode (1006). Format: ESC[<button;x;yM (press/move) and ESC[<button;x;ym (release).

Root cause

When a remote interactive program (vim, tmux, htop, less, etc.) runs inside the PTY, it enables mouse tracking on the local terminal by sending sequences like ESC[?1000h, ESC[?1002h, ESC[?1003h, ESC[?1006h. These sequences travel through the SSH channel and are interpreted by the local terminal emulator.

When the SSH session terminates abruptly, the corresponding disable sequences (ESC[?1000l, ESC[?1002l, ESC[?1003l, ESC[?1006l) are never delivered to the local terminal. The local terminal therefore remains in mouse tracking mode, and any subsequent mouse motion is emitted as raw input — which appears as garbled text since no application is reading it.

Code analysis

In src/pty/terminal.rs:

  • TerminalStateGuard::restore_terminal_state() (line 159) — only restores raw mode and bracketed paste; does not disable mouse tracking.
  • force_terminal_cleanup() (line 198) — only disables raw mode.
  • TerminalOps::disable_mouse() (line 222) — exists but is not called from any cleanup path.

Thus bssh never sends the mouse-tracking-off sequences on exit, even though it has the helper.

Proposed Solution

  1. Extend TerminalStateGuard::restore_terminal_state() in src/pty/terminal.rs to disable all common mouse tracking modes. Use crossterm's DisableMouseCapture and additionally write raw sequences to cover modes that crossterm may not reset (1002, 1003, 1006, 1015). Wrap each call so a failure doesn't abort the rest of the cleanup.

    use std::io::Write;
    use crossterm::event::DisableMouseCapture;
    
    // Best-effort: reset every mouse tracking mode the remote app may have enabled
    let _ = execute!(std::io::stdout(), DisableMouseCapture);
    let _ = std::io::stdout()
        .write_all(b"\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1015l");
    let _ = std::io::stdout().flush();
  2. Update force_terminal_cleanup() to perform the same mouse-tracking reset, since this function is the global last-resort cleanup.

  3. Verify panic & signal handling paths call force_terminal_cleanup() reliably:

    • Confirm a std::panic::set_hook is installed that invokes force_terminal_cleanup() before the default panic handler runs.
    • Confirm SIGINT/SIGTERM handlers (e.g., tokio::signal::ctrl_c) trigger the cleanup before exiting. If missing, add them.
  4. (Optional / nice-to-have) Also reset alternate screen buffer (ESC[?1049l) and cursor visibility (ESC[?25h) in the same cleanup path, since interactive programs also leave those toggled.

Acceptance Criteria

  • After bssh exits a PTY session (normal exit, Ctrl+C, network drop, panic), the local terminal no longer prints mouse tracking escape sequences when the mouse is moved.
  • Verified with at least one mouse-using remote program (e.g., vim with :set mouse=a, or tmux with mouse mode on, or htop).
  • Verified for both clean disconnect and abrupt disconnect (kill -9 the remote sshd, network unplug, Ctrl+C on local).
  • No regression for non-PTY commands (regular bssh exec).
  • force_terminal_cleanup() is reachable from panic and signal paths (document or test).

Reproduction Steps

  1. bssh -H user@host (or any cluster) into a PTY-enabled session.
  2. Run vim, then :set mouse=a, then :q.
  3. Disconnect the SSH session abruptly (e.g., kill the remote process or ~. escape).
  4. Move the mouse over the local terminal — observe garbled 0;X;YM strings being printed.

Technical Considerations

  • File to modify: src/pty/terminal.rs
  • Related helpers: TerminalOps::disable_mouse() (already exists, currently unused in cleanup)
  • Mouse tracking modes: 1000 (X11), 1002 (button event), 1003 (any event), 1006 (SGR), 1015 (urxvt)
  • Cleanup must be best-effort — each escape sequence write should be independently fallible so one failure doesn't prevent subsequent resets.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions