Skip to content

Text selection doesn't work inside full-screen programs — alternate-screen scroll detaches buffer lines #239

Description

@lordspace

Symptom

Selecting text with the mouse does nothing while a full-screen program (less,
htop, an editor) is on screen: no highlight, no selection, nothing copied.
Double-click and right-click keep working, which makes it look like a gesture
problem — it isn't.

Reproduced on macOS and Linux, with a mouse and a trackpad, on xterm 4.0.0 (the
current release) and on master.

Reproduction

final terminal = Terminal(maxLines: 10000);
final controller = TerminalController();

terminal.resize(80, 24);
terminal.write('\x1b[?1049h');          // alternate screen

for (var row = 0; row < 40; row++) {    // paint past the last row => scrolls
  terminal.write('row $row\r\n');
}

expect(terminal.buffer.lines[0].attached, isTrue);   // FAILS: false

final base = terminal.buffer.createAnchorFromOffset(const CellOffset(0, 23));
final extent = terminal.buffer.createAnchorFromOffset(const CellOffset(16, 23));

controller.setSelection(base, extent);

expect(controller.selection, isNotNull);             // FAILS: null

Root cause

Buffer.scrollUp (lib/src/core/buffer/buffer.dart) shifts every line up a
slot through the index setter:

this.lines[i] = this.lines[i + lines];

That attaches the line at i but leaves a stale duplicate reference at
i + lines. _adoptChild (lib/src/utils/circular_buffer.dart) then detaches
whatever occupies the target slot unconditionally:

void _adoptChild(int index, T child) {
  final cyclicIndex = _getCyclicIndex(index);
  _array[cyclicIndex]?._detach();                 // <-- detaches the re-homed line
  _array[cyclicIndex] = child.._attach(this, index);
}

So the next iteration's adopt into i + 1 detaches the line that was just
re-homed at i. After a single alternate-screen scroll, effectively every
visible line is detached.

TerminalController.selection returns null when either anchor is detached, so
every drag sets a selection that immediately reads back as nothing. The gesture
layer is fine — the selection pan fires on every drag update and calls
setSelection each time.

Evidence from a running app

A probe on click, printing the clicked cell plus the attachment state:

alt=false  cell=(43,24)  lineAttached=true   anchorAttached=true    (a shell selects fine)
alt=true   cell=(4,2)    lineAttached=false  anchorAttached=false   (nothing selects)

Cell mapping is correct in both cases; only attachment differs.

Why double-click still appears to work

Only the LAST row stays reachable in practice, which is why a double-click on a
pager's status line looks like it works while dragging anywhere does nothing.

Fix

Detach the previous occupant only when this slot is still its home. _attach
already rewrites _absoluteIndex, so a re-homed item maps to a different slot
and is left alone, while an item genuinely being overwritten (an eviction on a
full list) still maps here and detaches as before.

A branch with the fix and two regression tests is available — happy to open a PR.

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